feat(v0.5.0): history + streak + heatmap, quiet hours, partial reps, README
== История и стрики (#1) == - HistoryEntry { ts, exerciseId, action: done|skip|snooze, actualReps? } персистится в app-state.json, лимит 10k записей (~3 года), trim oldest 10% - markDone/snooze/skip пишут в историю; markDone принимает optional actualReps - IPC: getHistory(sinceMs?), clearHistory(beforeTs?) + preload bindings - Renderer helpers (src/renderer/src/lib/history.ts): * dayKey(ts) — YYYY-MM-DD local * dailyReps(entries, exs, dayKey) — суммирует actualReps || planned * dailyRepsRange(entries, exs, days) — для heatmap, заполняет gaps нулями * currentStreak(entries) — consecutive days, today или yesterday (grace) - Dashboard теперь 4 hero-карточки: Today (повторов за день) / Streak (дней подряд) / Next / Tracking - Новый компонент HistoryHeatmap — GitHub-style 12-недельный календарь с 5 интенсивностями, локализованными подписями дней/месяцев == Тихие часы (#2) == - shared/types.ts: QuietHours { enabled, from, to, days[] } + isQuietAt() helper с правильной обработкой wrap-around окон (22:00→08:00) - DEFAULT_SETTINGS.quietHours = disabled, 22:00→08:00, все дни - main/scheduler.ts: проверка isQuietAt перед fire; deferred fires поднимаются после окончания окна - Settings UI: новая секция "Тихие часы" с toggle, time-pickers, day-of-week pills == Сделал частично (#3) == - ReminderApp: stepper [−][число][+] вокруг счётчика повторов - При adjusted (actualReps !== exercise.reps) число подсвечивается accent и появляется подпись "Засчитаем X из Y" - markDone передаёт actualReps только если юзер реально изменил — иначе undefined чтобы история фиксировала планируемое значение чисто == README.md (#4) == - Описание, фичи, скриншоты (TODO-плейсхолдер), установка, dev-команды, архитектура, тесты, stack, ссылка на RELEASING.md - Бэйджи version / tests / platform == i18n == - ~14 новых ключей × 2 языка: dashboard.stat.today_done, streak, settings.quiet.* (3 row'а), reminder.partial == Тесты — 51 (было 33) == - shared/quiet-hours.test.ts (5): disabled, same-day, wrap-around, day filtering, zero-length - renderer/lib/history.test.ts (13): dayKey, dailyReps (planned vs actual vs ignore non-done), currentStreak (empty, today gap, consecutive, yesterday grace, multi-entry same day), dailyRepsRange Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Plus, Pause, Play, Flame, Activity } from 'lucide-react'
|
||||
import { Plus, Pause, Play, Flame, Activity, TrendingUp } from 'lucide-react'
|
||||
import { useAppStore } from '../store/appStore'
|
||||
import { ExerciseCard } from '../components/ExerciseCard'
|
||||
import { ExerciseEditor } from '../components/ExerciseEditor'
|
||||
import { HistoryHeatmap } from '../components/HistoryHeatmap'
|
||||
import { Button } from '../components/ui/Button'
|
||||
import type { Exercise } from '@shared/types'
|
||||
import type { Exercise, HistoryEntry } from '@shared/types'
|
||||
import { formatCountdown } from '../lib/format'
|
||||
import { useT } from '../i18n'
|
||||
import { currentStreak, dailyReps, todayKey } from '../lib/history'
|
||||
|
||||
export default function Dashboard(): JSX.Element {
|
||||
const state = useAppStore((s) => s.state)
|
||||
@@ -20,6 +22,18 @@ export default function Dashboard(): JSX.Element {
|
||||
const settings = state?.settings
|
||||
const gamesEnabled = Object.values(state?.gamesEnabled ?? {}).some(Boolean)
|
||||
|
||||
// Local history mirror; reloaded whenever app-state changes.
|
||||
const [history, setHistory] = useState<HistoryEntry[]>([])
|
||||
useEffect(() => {
|
||||
void window.api.getHistory().then(setHistory)
|
||||
}, [state])
|
||||
|
||||
const todayDone = useMemo(
|
||||
() => dailyReps(history, exercises, todayKey()),
|
||||
[history, exercises]
|
||||
)
|
||||
const streak = useMemo(() => currentStreak(history), [history])
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const enabled = exercises.filter((e) => e.enabled)
|
||||
const next = enabled
|
||||
@@ -94,13 +108,20 @@ export default function Dashboard(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-8">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-8">
|
||||
<HeroStat
|
||||
tone="accent"
|
||||
label={t('dashboard.stat.active')}
|
||||
value={`${stats.active}`}
|
||||
subvalue={t('dashboard.stat.active.of', { total: stats.total })}
|
||||
icon={<Activity size={14} strokeWidth={2.6} />}
|
||||
label={t('dashboard.stat.today_done')}
|
||||
value={`${todayDone}`}
|
||||
subvalue={t('dashboard.stat.today_done.subtitle')}
|
||||
icon={<TrendingUp size={14} strokeWidth={2.6} />}
|
||||
/>
|
||||
<HeroStat
|
||||
tone={streak > 0 ? 'warning' : 'muted'}
|
||||
label={t('dashboard.stat.streak')}
|
||||
value={`${streak}`}
|
||||
subvalue={t('dashboard.stat.streak.subtitle', { n: streak })}
|
||||
icon={<Flame size={14} strokeWidth={2.6} />}
|
||||
/>
|
||||
<HeroStat
|
||||
tone="info"
|
||||
@@ -117,7 +138,7 @@ export default function Dashboard(): JSX.Element {
|
||||
? t('dashboard.stat.next.subtitle_paused')
|
||||
: t('dashboard.stat.next.subtitle_running')
|
||||
}
|
||||
icon={<Flame size={14} strokeWidth={2.6} />}
|
||||
icon={<Activity size={14} strokeWidth={2.6} />}
|
||||
/>
|
||||
<HeroStat
|
||||
tone={gamesEnabled ? 'success' : 'muted'}
|
||||
@@ -143,6 +164,16 @@ export default function Dashboard(): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{history.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<HistoryHeatmap
|
||||
history={history}
|
||||
exercises={exercises}
|
||||
lang={lang}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{paused && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -4 }}
|
||||
@@ -214,7 +245,7 @@ function HeroStat({
|
||||
subvalue,
|
||||
icon
|
||||
}: {
|
||||
tone: 'accent' | 'info' | 'success' | 'muted'
|
||||
tone: 'accent' | 'info' | 'success' | 'warning' | 'muted'
|
||||
label: string
|
||||
value: string
|
||||
subvalue?: string
|
||||
@@ -227,7 +258,9 @@ function HeroStat({
|
||||
? 'bg-info'
|
||||
: tone === 'success'
|
||||
? 'bg-success'
|
||||
: 'bg-text/40'
|
||||
: tone === 'warning'
|
||||
? 'bg-warning'
|
||||
: 'bg-text/40'
|
||||
|
||||
return (
|
||||
<div className="bg-surface rounded-2xl p-5 shadow-card dark:ring-0.5 dark:ring-hairline/30">
|
||||
|
||||
Reference in New Issue
Block a user