Initial commit

This commit is contained in:
AnRil
2026-05-16 13:43:29 +07:00
commit 688a86b611
208 changed files with 44350 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
import { useMemo, useState } from 'react'
import { AnimatePresence } from 'framer-motion'
import { Plus, Pause, Play, Clock } from 'lucide-react'
import { useAppStore } from '../store/appStore'
import { ExerciseCard } from '../components/ExerciseCard'
import { ExerciseEditor } from '../components/ExerciseEditor'
import { Button } from '../components/ui/Button'
import type { Exercise } from '@shared/types'
import { formatCountdown } from '../lib/format'
export default function Dashboard(): JSX.Element {
const state = useAppStore((s) => s.state)
const ticks = useAppStore((s) => s.ticks)
const [editorOpen, setEditorOpen] = useState(false)
const [editing, setEditing] = useState<Exercise | null>(null)
const exercises = state?.exercises ?? []
const settings = state?.settings
const stats = useMemo(() => {
const enabled = exercises.filter((e) => e.enabled)
const next = enabled
.map((e) => ({ id: e.id, ms: e.nextFireAt - Date.now() }))
.sort((a, b) => a.ms - b.ms)[0]
return {
total: exercises.length,
active: enabled.length,
nextMs: next?.ms ?? Infinity
}
}, [exercises, ticks])
function openCreate(): void {
setEditing(null)
setEditorOpen(true)
}
function openEdit(ex: Exercise): void {
setEditing(ex)
setEditorOpen(true)
}
async function handleSave(draft: {
name: string
reps: number
icon: string
intervalMinutes: number
enabled: boolean
}): Promise<void> {
if (editing) {
await window.api.updateExercise(editing.id, draft)
} else {
await window.api.addExercise(draft)
}
setEditorOpen(false)
}
async function handleDelete(id: string): Promise<void> {
await window.api.deleteExercise(id)
}
async function togglePause(): Promise<void> {
if (!settings) return
await window.api.updateSettings({ globalEnabled: !settings.globalEnabled })
}
return (
<div className="p-8 overflow-y-auto h-full">
<div className="flex items-end justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">Дашборд</h1>
<p className="text-sm text-muted mt-1">
{stats.active} активных из {stats.total} упражнений
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="secondary" onClick={togglePause}>
{settings?.globalEnabled ? (
<>
<Pause size={16} /> Пауза
</>
) : (
<>
<Play size={16} /> Возобновить
</>
)}
</Button>
<Button onClick={openCreate}>
<Plus size={16} /> Новое
</Button>
</div>
</div>
<div className="mb-6 rounded-2xl border border-border bg-surface px-5 py-4 flex items-center gap-4">
<div className="w-11 h-11 rounded-xl bg-accent/15 text-accent grid place-items-center">
<Clock size={20} />
</div>
<div className="flex-1">
<div className="text-xs text-muted uppercase tracking-wider">
Ближайшее напоминание
</div>
<div className="text-lg font-semibold mt-0.5">
{stats.nextMs === Infinity
? 'Нет активных упражнений'
: `через ${formatCountdown(stats.nextMs)}`}
</div>
</div>
{!settings?.globalEnabled && (
<div className="px-3 py-1.5 rounded-full bg-amber-500/15 text-amber-500 text-xs font-medium">
Напоминания на паузе
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<AnimatePresence>
{exercises.map((ex) => (
<ExerciseCard
key={ex.id}
exercise={ex}
tick={ticks[ex.id]}
onEdit={() => openEdit(ex)}
onDelete={() => handleDelete(ex.id)}
onToggle={(enabled) => window.api.toggleExercise(ex.id, enabled)}
onMarkDone={() => window.api.markDone(ex.id)}
/>
))}
</AnimatePresence>
</div>
{exercises.length === 0 && (
<div className="mt-10 text-center text-muted">
<p>Нет упражнений. Добавьте первое.</p>
</div>
)}
<ExerciseEditor
open={editorOpen}
exercise={editing}
onClose={() => setEditorOpen(false)}
onSave={handleSave}
/>
</div>
)
}