Initial commit
This commit is contained in:
144
src/renderer/src/pages/Dashboard.tsx
Normal file
144
src/renderer/src/pages/Dashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user