feat: unified navigation hub + categories tab + mobile scroll fix
Some checks failed
CI / ci (push) Has been cancelled

- Navigation: 4 items (Home, Tracker, Finance, Settings)
- Tracker page: tabs for Habits, Tasks, Stats
- Finance: added Categories tab (CRUD)
- AddTransactionModal: fixed mobile scroll with sticky button
- Home: added finance balance widget
- Legacy routes (/habits, /tasks, /stats) redirect to /tracker
This commit is contained in:
Cosmo
2026-03-01 04:34:59 +00:00
parent 0ec0eede76
commit 8baddf1914
10 changed files with 478 additions and 140 deletions

View File

@@ -13,6 +13,7 @@ import ForgotPassword from "./pages/ForgotPassword"
import Stats from "./pages/Stats" import Stats from "./pages/Stats"
import Settings from "./pages/Settings" import Settings from "./pages/Settings"
import Finance from "./pages/Finance" import Finance from "./pages/Finance"
import Tracker from "./pages/Tracker"
function ProtectedRoute({ children }) { function ProtectedRoute({ children }) {
const { isAuthenticated, isLoading } = useAuthStore() const { isAuthenticated, isLoading } = useAuthStore()
@@ -93,11 +94,20 @@ export default function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/tracker"
element={
<ProtectedRoute>
<Tracker />
</ProtectedRoute>
}
/>
{/* Legacy routes redirect to tracker */}
<Route <Route
path="/habits" path="/habits"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Habits /> <Navigate to="/tracker" replace />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -105,7 +115,15 @@ export default function App() {
path="/tasks" path="/tasks"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Tasks /> <Navigate to="/tracker" replace />
</ProtectedRoute>
}
/>
<Route
path="/stats"
element={
<ProtectedRoute>
<Navigate to="/tracker" replace />
</ProtectedRoute> </ProtectedRoute>
} }
/> />
@@ -117,14 +135,6 @@ export default function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/stats"
element={
<ProtectedRoute>
<Stats />
</ProtectedRoute>
}
/>
<Route <Route
path="/finance" path="/finance"
element={ element={

View File

@@ -1,14 +1,11 @@
import { NavLink } from "react-router-dom" import { NavLink } from "react-router-dom"
import { Home, ListChecks, CheckSquare, BarChart3, PiggyBank, Wallet, Settings } from "lucide-react" import { Home, BarChart3, Wallet, Settings } from "lucide-react"
import clsx from "clsx" import clsx from "clsx"
export default function Navigation() { export default function Navigation() {
const navItems = [ const navItems = [
{ to: "/", icon: Home, label: "Сегодня" }, { to: "/", icon: Home, label: "Главная" },
{ to: "/habits", icon: ListChecks, label: "Привычки" }, { to: "/tracker", icon: BarChart3, label: "Трекер" },
{ to: "/tasks", icon: CheckSquare, label: "Задачи" },
{ to: "/stats", icon: BarChart3, label: "Статистика" },
{ to: "/savings", icon: PiggyBank, label: "Накопления" },
{ to: "/finance", icon: Wallet, label: "Финансы" }, { to: "/finance", icon: Wallet, label: "Финансы" },
{ to: "/settings", icon: Settings, label: "Настройки" }, { to: "/settings", icon: Settings, label: "Настройки" },
] ]
@@ -21,17 +18,18 @@ export default function Navigation() {
<NavLink <NavLink
key={to} key={to}
to={to} to={to}
end={to === "/"}
className={({ isActive }) => className={({ isActive }) =>
clsx( clsx(
"flex flex-col items-center gap-0.5 px-1.5 py-1.5 rounded-xl transition-all", "flex flex-col items-center gap-0.5 px-3 py-2 rounded-xl transition-all",
isActive isActive
? "text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/30" ? "text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/30"
: "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300" : "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
) )
} }
> >
<Icon size={18} /> <Icon size={22} />
<span className="text-[10px] font-medium">{label}</span> <span className="text-[11px] font-medium">{label}</span>
</NavLink> </NavLink>
))} ))}
</div> </div>

View File

@@ -59,15 +59,21 @@ export default function AddTransactionModal({ onClose, onSaved }) {
onClick={onClose} onClick={onClose}
> >
<div <div
className="bg-white dark:bg-gray-900 rounded-t-3xl w-full max-w-lg p-6 max-h-[90vh] overflow-y-auto" className="bg-white dark:bg-gray-900 rounded-t-3xl w-full max-w-lg flex flex-col"
style={{ maxHeight: "85vh" }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Header - fixed */}
<div className="px-6 pt-4 pb-2 flex-shrink-0">
<div className="w-12 h-1 bg-gray-300 dark:bg-gray-700 rounded-full mx-auto mb-4" /> <div className="w-12 h-1 bg-gray-300 dark:bg-gray-700 rounded-full mx-auto mb-4" />
<h2 className="text-lg font-display font-bold text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-display font-bold text-gray-900 dark:text-white">
Новая запись Новая запись
</h2> </h2>
</div>
<div className="space-y-5"> {/* Scrollable content */}
<div className="flex-1 overflow-y-auto px-6 pb-4" style={{ WebkitOverflowScrolling: "touch" }}>
<div className="space-y-5 pt-2">
{/* Type toggle */} {/* Type toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-xl p-1"> <div className="flex bg-gray-100 dark:bg-gray-800 rounded-xl p-1">
{[ {[
@@ -121,6 +127,7 @@ export default function AddTransactionModal({ onClose, onSaved }) {
<div className="relative"> <div className="relative">
<input <input
type="number" type="number"
inputMode="decimal"
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} onChange={(e) => setAmount(e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-2xl font-bold text-gray-900 dark:text-white outline-none" className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-2xl font-bold text-gray-900 dark:text-white outline-none"
@@ -179,8 +186,11 @@ export default function AddTransactionModal({ onClose, onSaved }) {
className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none" className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none"
/> />
</div> </div>
</div>
</div>
{/* Submit */} {/* Sticky submit button */}
<div className="px-6 py-4 border-t border-gray-100 dark:border-gray-800 flex-shrink-0 bg-white dark:bg-gray-900">
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={saving || !amount || !categoryId} disabled={saving || !amount || !categoryId}

View File

@@ -0,0 +1,238 @@
import { useState, useEffect } from "react"
import { financeApi } from "../../api/finance"
const EMOJI_OPTIONS = ["🏠","🍔","🚗","👕","🏥","🎮","📱","✈️","🎁","🛒","🛍️","💎","📦","💰","💼","📈","🎓","🏋️","🐶","🎵","💊","🏪","☕","🍕","🎬","📚","🔧","💡","🌐","🎯"]
const fmt = (n) => n?.toLocaleString("ru-RU") + " ₽"
export default function CategoriesManager({ refreshKey }) {
const [categories, setCategories] = useState([])
const [tab, setTab] = useState("expense")
const [showModal, setShowModal] = useState(false)
const [editing, setEditing] = useState(null)
const [loading, setLoading] = useState(true)
// Form state
const [emoji, setEmoji] = useState("📦")
const [name, setName] = useState("")
const [type, setType] = useState("expense")
const [budget, setBudget] = useState("")
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [saving, setSaving] = useState(false)
const [deleting, setDeleting] = useState(null)
const load = async () => {
try {
setLoading(true)
const cats = await financeApi.listCategories()
setCategories(cats)
} catch (e) {
console.error(e)
} finally {
setLoading(false)
}
}
useEffect(() => { load() }, [refreshKey])
const cats = categories.filter(c => c.type === tab)
const openAdd = () => {
setEditing(null)
setEmoji("📦")
setName("")
setType(tab)
setBudget("")
setShowModal(true)
}
const openEdit = (cat) => {
setEditing(cat)
setEmoji(cat.emoji || "📦")
setName(cat.name)
setType(cat.type)
setBudget(cat.budget ? String(cat.budget) : "")
setShowModal(true)
}
const handleSave = async () => {
if (!name.trim()) return
setSaving(true)
try {
const data = { name: name.trim(), emoji, type, budget: budget ? parseFloat(budget) : 0 }
if (editing) {
await financeApi.updateCategory(editing.id, data)
} else {
await financeApi.createCategory(data)
}
setShowModal(false)
load()
} catch (e) {
console.error(e)
alert("Ошибка при сохранении")
} finally {
setSaving(false)
}
}
const handleDelete = async (id) => {
if (!confirm("Удалить категорию? Транзакции с ней останутся.")) return
setDeleting(id)
try {
await financeApi.deleteCategory(id)
load()
} catch (e) {
console.error(e)
alert("Ошибка при удалении")
} finally {
setDeleting(null)
}
}
if (loading) {
return (
<div className="space-y-3">
{[1,2,3].map(i => (
<div key={i} className="card p-4 animate-pulse">
<div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-xl bg-gray-200 dark:bg-gray-700" />
<div className="flex-1"><div className="h-5 bg-gray-200 dark:bg-gray-700 rounded w-1/2" /></div>
</div>
</div>
))}
</div>
)
}
return (
<div>
{/* Sub-tabs: expense / income */}
<div className="flex gap-2 mb-4">
{[["expense","Расходы"],["income","Доходы"]].map(([k,l]) => (
<button key={k} onClick={() => setTab(k)}
className={`px-4 py-2 rounded-xl text-sm font-semibold transition ${
tab === k ? "bg-primary-500 text-white" : "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}>
{l}
</button>
))}
<div className="flex-1" />
<button onClick={openAdd} className="px-4 py-2 bg-primary-500 text-white rounded-xl text-sm font-medium hover:bg-primary-600 transition">
+ Добавить
</button>
</div>
{/* Category list */}
<div className="space-y-3">
{cats.length === 0 ? (
<div className="card p-8 text-center">
<p className="text-gray-500 dark:text-gray-400">Нет категорий</p>
<button onClick={openAdd} className="mt-3 text-sm text-primary-600 dark:text-primary-400 font-medium">+ Добавить</button>
</div>
) : cats.map(c => {
const pct = c.budget > 0 ? Math.min(Math.round((c.spent || 0) / c.budget * 100), 100) : 0
const isOver = c.budget > 0 && (c.spent || 0) > c.budget * 0.9
return (
<div key={c.id} className="card p-4">
<div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-2xl flex-shrink-0">
{c.emoji}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center">
<span className="font-medium text-gray-900 dark:text-white truncate">{c.name}</span>
{c.budget > 0 && (
<span className={`text-xs font-semibold ml-2 whitespace-nowrap ${isOver ? "text-red-500" : "text-gray-500 dark:text-gray-400"}`}>
{fmt(c.spent || 0)} / {fmt(c.budget)}
</span>
)}
</div>
{c.budget > 0 && (
<div className="mt-2 h-2 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
<div className={`h-full rounded-full transition-all ${isOver ? "bg-red-500" : pct > 70 ? "bg-yellow-500" : "bg-primary-500"}`}
style={{ width: pct + "%" }} />
</div>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0">
<button onClick={() => openEdit(c)} className="p-2 text-gray-400 hover:text-primary-500 rounded-lg transition">
</button>
<button onClick={() => handleDelete(c.id)} disabled={deleting === c.id}
className="p-2 text-gray-400 hover:text-red-500 rounded-lg transition disabled:opacity-50">
{deleting === c.id ? "⏳" : "🗑️"}
</button>
</div>
</div>
</div>
)
})}
</div>
{/* Add/Edit Modal */}
{showModal && (
<div className="fixed inset-0 bg-black/50 flex items-end justify-center z-50" onClick={() => setShowModal(false)}>
<div className="bg-white dark:bg-gray-900 rounded-t-3xl w-full max-w-lg p-6" onClick={e => e.stopPropagation()}>
<div className="w-12 h-1 bg-gray-300 dark:bg-gray-700 rounded-full mx-auto mb-4" />
<h2 className="text-lg font-display font-bold text-gray-900 dark:text-white mb-4">
{editing ? "Редактировать категорию" : "Новая категория"}
</h2>
<div className="space-y-4">
{/* Emoji + Name */}
<div className="flex gap-3">
<button onClick={() => setShowEmojiPicker(!showEmojiPicker)}
className="w-14 h-14 rounded-xl bg-gray-100 dark:bg-gray-800 text-3xl flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition flex-shrink-0">
{emoji}
</button>
<input value={name} onChange={e => setName(e.target.value)}
className="flex-1 px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none"
placeholder="Название категории" />
</div>
{/* Emoji picker */}
{showEmojiPicker && (
<div className="grid grid-cols-8 gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-xl">
{EMOJI_OPTIONS.map(e => (
<button key={e} onClick={() => { setEmoji(e); setShowEmojiPicker(false) }}
className={`text-2xl p-1 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition ${emoji === e ? "bg-primary-100 dark:bg-primary-900" : ""}`}>
{e}
</button>
))}
</div>
)}
{/* Type toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-xl p-1">
{[["expense","Расход"],["income","Доход"]].map(([k,l]) => (
<button key={k} onClick={() => setType(k)}
className={`flex-1 py-2.5 rounded-lg text-sm font-semibold transition ${
type === k ? "bg-primary-500 text-white" : "text-gray-500"
}`}>
{l}
</button>
))}
</div>
{/* Budget */}
<div>
<label className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase block mb-1">Бюджет (опц.)</label>
<div className="relative">
<input type="number" value={budget} onChange={e => setBudget(e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white outline-none"
placeholder="0" />
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400"></span>
</div>
</div>
{/* Save */}
<button onClick={handleSave} disabled={saving || !name.trim()}
className="w-full py-3.5 bg-primary-500 hover:bg-primary-600 disabled:opacity-50 text-white rounded-xl font-semibold transition">
{saving ? "Сохраняю..." : editing ? "Сохранить" : "Создать"}
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -3,12 +3,14 @@ import Navigation from "../components/Navigation"
import FinanceDashboard from "../components/finance/FinanceDashboard" import FinanceDashboard from "../components/finance/FinanceDashboard"
import TransactionList from "../components/finance/TransactionList" import TransactionList from "../components/finance/TransactionList"
import FinanceAnalytics from "../components/finance/FinanceAnalytics" import FinanceAnalytics from "../components/finance/FinanceAnalytics"
import CategoriesManager from "../components/finance/CategoriesManager"
import AddTransactionModal from "../components/finance/AddTransactionModal" import AddTransactionModal from "../components/finance/AddTransactionModal"
const tabs = [ const tabs = [
{ key: "dashboard", label: "Обзор", icon: "📊" }, { key: "dashboard", label: "Обзор", icon: "📊" },
{ key: "transactions", label: "Транзакции", icon: "📋" }, { key: "transactions", label: "Транзакции", icon: "📋" },
{ key: "analytics", label: "Аналитика", icon: "📈" }, { key: "analytics", label: "Аналитика", icon: "📈" },
{ key: "categories", label: "Категории", icon: "🏷️" },
] ]
export default function Finance() { export default function Finance() {
@@ -34,12 +36,12 @@ export default function Finance() {
+ +
</button> </button>
</div> </div>
<div className="max-w-lg mx-auto px-4 pb-3 flex gap-2"> <div className="max-w-lg mx-auto px-4 pb-3 flex gap-1.5 overflow-x-auto scrollbar-hide">
{tabs.map((t) => ( {tabs.map((t) => (
<button <button
key={t.key} key={t.key}
onClick={() => setActiveTab(t.key)} onClick={() => setActiveTab(t.key)}
className={`flex-1 py-2 rounded-xl text-sm font-semibold transition ${ className={`flex-1 min-w-0 py-2 rounded-xl text-xs sm:text-sm font-semibold transition whitespace-nowrap px-2 ${
activeTab === t.key activeTab === t.key
? "bg-primary-500 text-white" ? "bg-primary-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400" : "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
@@ -57,6 +59,7 @@ export default function Finance() {
<TransactionList key={refreshKey} onAdd={() => setShowAdd(true)} /> <TransactionList key={refreshKey} onAdd={() => setShowAdd(true)} />
)} )}
{activeTab === "analytics" && <FinanceAnalytics key={refreshKey} />} {activeTab === "analytics" && <FinanceAnalytics key={refreshKey} />}
{activeTab === "categories" && <CategoriesManager refreshKey={refreshKey} />}
</div> </div>
{showAdd && ( {showAdd && (

View File

@@ -10,7 +10,7 @@ import EditHabitModal from '../components/EditHabitModal'
import Navigation from '../components/Navigation' import Navigation from '../components/Navigation'
import clsx from 'clsx' import clsx from 'clsx'
export default function Habits() { export default function Habits({ embedded = false }) {
const [showCreateModal, setShowCreateModal] = useState(false) const [showCreateModal, setShowCreateModal] = useState(false)
const [editingHabit, setEditingHabit] = useState(null) const [editingHabit, setEditingHabit] = useState(null)
const [showArchived, setShowArchived] = useState(false) const [showArchived, setShowArchived] = useState(false)
@@ -67,8 +67,8 @@ export default function Habits() {
const archivedList = habits.filter(h => h.is_archived) const archivedList = habits.filter(h => h.is_archived)
return ( return (
<div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24 transition-colors duration-300"> <div className={embedded ? "" : "min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24 transition-colors duration-300"}>
<header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10"> {!embedded && <header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10">
<div className="max-w-lg mx-auto px-4 py-4 flex items-center justify-between"> <div className="max-w-lg mx-auto px-4 py-4 flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">Мои привычки</h1> <h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">Мои привычки</h1>
@@ -79,7 +79,7 @@ export default function Habits() {
Новая Новая
</button> </button>
</div> </div>
</header> </header>}
<main className="max-w-lg mx-auto px-4 py-6 space-y-6"> <main className="max-w-lg mx-auto px-4 py-6 space-y-6">
{isLoading ? ( {isLoading ? (
@@ -168,7 +168,7 @@ export default function Habits() {
)} )}
</main> </main>
<Navigation /> {!embedded && <Navigation />}
<CreateHabitModal open={showCreateModal} onClose={() => setShowCreateModal(false)} /> <CreateHabitModal open={showCreateModal} onClose={() => setShowCreateModal(false)} />
<EditHabitModal open={!!editingHabit} onClose={() => setEditingHabit(null)} habit={editingHabit} /> <EditHabitModal open={!!editingHabit} onClose={() => setEditingHabit(null)} habit={editingHabit} />
</div> </div>

View File

@@ -6,6 +6,7 @@ import { format, startOfWeek, differenceInDays, parseISO, isToday, isTomorrow, i
import { ru } from 'date-fns/locale' import { ru } from 'date-fns/locale'
import { habitsApi } from '../api/habits' import { habitsApi } from '../api/habits'
import { tasksApi } from '../api/tasks' import { tasksApi } from '../api/tasks'
import { financeApi } from '../api/finance'
import { useAuthStore } from '../store/auth' import { useAuthStore } from '../store/auth'
import Navigation from '../components/Navigation' import Navigation from '../components/Navigation'
import CreateTaskModal from '../components/CreateTaskModal' import CreateTaskModal from '../components/CreateTaskModal'
@@ -96,6 +97,11 @@ export default function Home() {
queryFn: tasksApi.today, queryFn: tasksApi.today,
}) })
const { data: financeSummary } = useQuery({
queryKey: ["finance-summary"],
queryFn: () => financeApi.getSummary(),
})
useEffect(() => { useEffect(() => {
if (habits.length > 0) { if (habits.length > 0) {
loadTodayLogs() loadTodayLogs()
@@ -301,6 +307,27 @@ export default function Home() {
)} )}
{/* Tasks */} {/* Tasks */}
{/* Finance Summary */}
{financeSummary && (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="card p-5">
<h2 className="font-semibold text-gray-900 dark:text-white mb-3">💰 Баланс</h2>
<div className="grid grid-cols-3 gap-3">
<div className="text-center">
<p className="text-lg font-bold text-green-500">+{(financeSummary.total_income || 0).toLocaleString("ru-RU")} </p>
<p className="text-xs text-gray-500">Доходы</p>
</div>
<div className="text-center">
<p className="text-lg font-bold text-red-500">-{(financeSummary.total_expense || 0).toLocaleString("ru-RU")} </p>
<p className="text-xs text-gray-500">Расходы</p>
</div>
<div className="text-center">
<p className={"text-lg font-bold " + ((financeSummary.balance || 0) >= 0 ? "text-primary-500" : "text-red-500")}>{(financeSummary.balance || 0).toLocaleString("ru-RU")} </p>
<p className="text-xs text-gray-500">Баланс</p>
</div>
</div>
</motion.div>
)}
{(activeTasks.length > 0 || !tasksLoading) && ( {(activeTasks.length > 0 || !tasksLoading) && (
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">

View File

@@ -147,7 +147,7 @@ const SectionHeader = ({ icon: Icon, title, subtitle }) => (
</div> </div>
) )
export default function Stats() { export default function Stats({ embedded = false }) {
const [selectedHabitId, setSelectedHabitId] = useState(null) const [selectedHabitId, setSelectedHabitId] = useState(null)
const [allHabitLogs, setAllHabitLogs] = useState({}) const [allHabitLogs, setAllHabitLogs] = useState({})
const [allHabitStats, setAllHabitStats] = useState({}) const [allHabitStats, setAllHabitStats] = useState({})
@@ -356,7 +356,7 @@ export default function Stats() {
<div className="absolute bottom-1/4 right-0 w-80 h-80 bg-teal-500/5 rounded-full blur-3xl" /> <div className="absolute bottom-1/4 right-0 w-80 h-80 bg-teal-500/5 rounded-full blur-3xl" />
</div> </div>
<header className="relative bg-gray-900/50 backdrop-blur-xl border-b border-gray-800/50 sticky top-0 z-20"> {!embedded && <header className="relative bg-gray-900/50 backdrop-blur-xl border-b border-gray-800/50 sticky top-0 z-20">
<div className="max-w-lg mx-auto px-5 py-5"> <div className="max-w-lg mx-auto px-5 py-5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-primary-500 to-teal-600 flex items-center justify-center shadow-lg shadow-primary-500/25"> <div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-primary-500 to-teal-600 flex items-center justify-center shadow-lg shadow-primary-500/25">
@@ -371,7 +371,7 @@ export default function Stats() {
</div> </div>
</div> </div>
</div> </div>
</header> </header>}
<main className="relative max-w-lg mx-auto px-5 py-6 space-y-8"> <main className="relative max-w-lg mx-auto px-5 py-6 space-y-8">
@@ -689,7 +689,7 @@ export default function Stats() {
)} )}
</main> </main>
<Navigation /> {!embedded && <Navigation />}
</div> </div>
) )
} }

View File

@@ -32,7 +32,7 @@ function formatDueDate(dateStr) {
return format(date, 'd MMM', { locale: ru }) return format(date, 'd MMM', { locale: ru })
} }
export default function Tasks() { export default function Tasks({ embedded = false }) {
const [showCreate, setShowCreate] = useState(false) const [showCreate, setShowCreate] = useState(false)
const [editingTask, setEditingTask] = useState(null) const [editingTask, setEditingTask] = useState(null)
const [filter, setFilter] = useState('active') const [filter, setFilter] = useState('active')
@@ -68,8 +68,8 @@ export default function Tasks() {
} }
return ( return (
<div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24 transition-colors duration-300"> <div className={embedded ? "" : "min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24 transition-colors duration-300"}>
<header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10"> {!embedded && <header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10">
<div className="max-w-lg mx-auto px-4 py-4"> <div className="max-w-lg mx-auto px-4 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">Задачи</h1> <h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">Задачи</h1>
@@ -99,7 +99,7 @@ export default function Tasks() {
))} ))}
</div> </div>
</div> </div>
</header> </header>}
<main className="max-w-lg mx-auto px-4 py-6"> <main className="max-w-lg mx-auto px-4 py-6">
{isLoading ? ( {isLoading ? (
@@ -145,7 +145,7 @@ export default function Tasks() {
)} )}
</main> </main>
<Navigation /> {!embedded && <Navigation />}
<CreateTaskModal open={showCreate} onClose={() => setShowCreate(false)} /> <CreateTaskModal open={showCreate} onClose={() => setShowCreate(false)} />
<EditTaskModal open={!!editingTask} onClose={() => setEditingTask(null)} task={editingTask} /> <EditTaskModal open={!!editingTask} onClose={() => setEditingTask(null)} task={editingTask} />
</div> </div>

52
src/pages/Tracker.jsx Normal file
View File

@@ -0,0 +1,52 @@
import { useState, lazy, Suspense } from "react"
import Navigation from "../components/Navigation"
// Import pages as components (they render their own content but we strip their Navigation)
import HabitsContent from "./Habits"
import TasksContent from "./Tasks"
import StatsContent from "./Stats"
const tabs = [
{ key: "habits", label: "Привычки", icon: "🎯" },
{ key: "tasks", label: "Задачи", icon: "✅" },
{ key: "stats", label: "Статистика", icon: "📊" },
]
export default function Tracker() {
const [activeTab, setActiveTab] = useState("habits")
return (
<div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24">
<header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10">
<div className="max-w-lg mx-auto px-4 py-4">
<h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">
📊 Трекер
</h1>
</div>
<div className="max-w-lg mx-auto px-4 pb-3 flex gap-2">
{tabs.map((t) => (
<button
key={t.key}
onClick={() => setActiveTab(t.key)}
className={`flex-1 py-2 rounded-xl text-sm font-semibold transition ${
activeTab === t.key
? "bg-primary-500 text-white"
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
}`}
>
{t.icon} {t.label}
</button>
))}
</div>
</header>
<div>
{activeTab === "habits" && <HabitsContent embedded />}
{activeTab === "tasks" && <TasksContent embedded />}
{activeTab === "stats" && <StatsContent embedded />}
</div>
<Navigation />
</div>
)
}