Skip to content

Commit 9aab1d0

Browse files
useruser
authored andcommitted
Simplify layout and add timer editing
Made-with: Cursor
1 parent 820c17b commit 9aab1d0

9 files changed

Lines changed: 161 additions & 115 deletions

File tree

src/app/page.tsx

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function HomePage() {
3636
const [mounted, setMounted] = useState(false);
3737

3838
const t = useMemo(() => getMessages(locale), [locale]);
39-
const { settings, setTheme, setShowMilliseconds, setCompactMode, setDefaultInputMode } = useSettings();
39+
const { settings, setTheme, setShowMilliseconds } = useSettings();
4040
const {
4141
sessions,
4242
hydrated,
@@ -46,6 +46,7 @@ export default function HomePage() {
4646
clearHistory,
4747
getActiveSessions,
4848
removeSession,
49+
updateSessionTarget,
4950
} = useSession();
5051
const { permission, supported, requestPermission, sendNotification } = useNotifications();
5152
const pip = usePictureInPicture(targetDate);
@@ -230,16 +231,29 @@ export default function HomePage() {
230231
setShowComplete(false);
231232
}, []);
232233

234+
const handleEditSession = useCallback(
235+
(id: string, nextTarget: Date) => {
236+
updateSessionTarget(id, nextTarget);
237+
if (id === selectedSessionId) {
238+
const duration = nextTarget.getTime() - Date.now();
239+
if (duration > 0) {
240+
setTargetDate(nextTarget);
241+
setTotalDuration(duration);
242+
setShowComplete(false);
243+
}
244+
}
245+
},
246+
[updateSessionTarget, selectedSessionId],
247+
);
248+
233249
if (!mounted) return null;
234250

235251
const isActive = targetDate !== null && !showComplete;
236-
const contentMax = settings.compactMode ? "max-w-5xl" : "max-w-6xl";
237-
const cardPadding = settings.compactMode ? "p-5 sm:p-7" : "p-6 sm:p-10";
238252

239253
return (
240254
<LocaleContext value={{ locale, setLocale, t }}>
241255
<div className="flex min-h-dvh min-h-[100dvh] flex-col overflow-x-hidden touch-pan-y">
242-
<header className="mx-auto flex w-full items-center justify-between gap-2 px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))] sm:px-6 sm:py-4 shrink-0">
256+
<header className="mx-auto flex w-full max-w-6xl items-center justify-between gap-2 px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))] sm:px-6 sm:py-4 shrink-0">
243257
<h1 className="text-lg font-bold text-gradient sm:text-xl md:text-2xl">Count23</h1>
244258
<div className="flex items-center gap-2">
245259
<button
@@ -259,7 +273,7 @@ export default function HomePage() {
259273
</div>
260274
</header>
261275

262-
<main className={`mx-auto flex w-full ${contentMax} flex-1 flex-col items-center justify-center px-4 pb-8`}>
276+
<main className="mx-auto flex w-full max-w-6xl flex-1 flex-col items-center justify-center px-4 pb-8">
263277
<AnimatePresence mode="wait">
264278
{showComplete ? (
265279
<motion.div key="success" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
@@ -273,7 +287,7 @@ export default function HomePage() {
273287
animate={{ opacity: 1, scale: 1 }}
274288
exit={{ opacity: 0, scale: 0.95 }}
275289
>
276-
<div className={`glass-card glow-blue w-full max-w-xl ${cardPadding}`}>
290+
<div className="glass-card glow-blue w-full max-w-xl p-6 sm:p-10">
277291
<CountdownCircle
278292
state={state}
279293
labels={{
@@ -310,12 +324,8 @@ export default function HomePage() {
310324
<p className="text-sm text-slate-400">{t.app.tagline}</p>
311325
</div>
312326

313-
<div className={`glass-card w-full max-w-xl ${settings.compactMode ? "p-6 sm:p-7" : "p-8 sm:p-10"}`}>
314-
<TimeInput
315-
onStart={handleStart}
316-
compact={settings.compactMode}
317-
defaultMode={settings.defaultInputMode}
318-
/>
327+
<div className="glass-card w-full max-w-xl p-7 sm:p-10">
328+
<TimeInput onStart={handleStart} />
319329
</div>
320330
</motion.div>
321331
)}
@@ -335,6 +345,7 @@ export default function HomePage() {
335345
selectedSessionId={selectedSessionId}
336346
onSelectSession={setSelectedSessionId}
337347
onCancelSession={handleCancel}
348+
onEditSession={handleEditSession}
338349
onClearHistory={clearHistory}
339350
onRemoveSession={removeSession}
340351
onAddTimer={() => setShowAddForm(true)}
@@ -345,7 +356,7 @@ export default function HomePage() {
345356

346357
{showAddForm && isActive && (
347358
<motion.div
348-
className={`mt-4 w-full max-w-2xl glass-card ${settings.compactMode ? "p-4" : "p-6"}`}
359+
className="mt-4 w-full max-w-2xl glass-card p-6"
349360
initial={{ opacity: 0, height: 0 }}
350361
animate={{ opacity: 1, height: "auto" }}
351362
exit={{ opacity: 0, height: 0 }}
@@ -360,11 +371,7 @@ export default function HomePage() {
360371
{t.timer.cancel}
361372
</button>
362373
</div>
363-
<TimeInput
364-
onStart={handleStart}
365-
compact={settings.compactMode}
366-
defaultMode={settings.defaultInputMode}
367-
/>
374+
<TimeInput onStart={handleStart} />
368375
</motion.div>
369376
)}
370377
</main>
@@ -381,24 +388,16 @@ export default function HomePage() {
381388
close: t.settings.close,
382389
theme: t.settings.theme,
383390
showMilliseconds: t.settings.showMilliseconds,
384-
compactMode: t.settings.compactMode,
385-
defaultInputMode: t.settings.defaultInputMode,
386391
themes: {
387392
midnight: t.settings.themeMidnight,
388393
ocean: t.settings.themeOcean,
389394
sunset: t.settings.themeSunset,
390395
forest: t.settings.themeForest,
391396
},
392-
modes: {
393-
quick: t.timer.modeQuick,
394-
datetime: t.timer.modePickDate,
395-
},
396397
}}
397398
onClose={() => setShowSettings(false)}
398399
onThemeChange={setTheme}
399400
onShowMillisecondsChange={setShowMilliseconds}
400-
onCompactModeChange={setCompactMode}
401-
onDefaultInputModeChange={setDefaultInputMode}
402401
/>
403402
</div>
404403
</LocaleContext>

src/features/countdown/components/SessionList.tsx

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
"use client";
22

3+
import { useState } from "react";
34
import { motion, AnimatePresence } from "framer-motion";
45
import { useLocale } from "@/features/i18n";
5-
import { formatTimeLocale, formatRemaining } from "../utils/time";
6+
import {
7+
formatTimeLocale,
8+
formatRemaining,
9+
formatTargetDisplay,
10+
parseFutureDatetimeLocal,
11+
toDatetimeLocalValue,
12+
} from "../utils/time";
613
import type { CountdownSession } from "../types";
714

815
interface SessionListProps {
@@ -11,6 +18,7 @@ interface SessionListProps {
1118
selectedSessionId: string | null;
1219
onSelectSession: (id: string) => void;
1320
onCancelSession: (sessionId: string) => void;
21+
onEditSession: (id: string, target: Date) => void;
1422
onClearHistory: () => void;
1523
onRemoveSession: (id: string) => void;
1624
onAddTimer: () => void;
@@ -28,11 +36,15 @@ export function SessionList({
2836
selectedSessionId,
2937
onSelectSession,
3038
onCancelSession,
39+
onEditSession,
3140
onClearHistory,
3241
onRemoveSession,
3342
onAddTimer,
3443
}: SessionListProps) {
3544
const { t, locale } = useLocale();
45+
const [editingId, setEditingId] = useState<string | null>(null);
46+
const [editValue, setEditValue] = useState("");
47+
const [editError, setEditError] = useState<string | null>(null);
3648

3749
const historySessions = sessions.filter((s) => s.status !== "active");
3850
const hasHistory = historySessions.length > 0;
@@ -51,24 +63,24 @@ export function SessionList({
5163
}
5264

5365
return (
54-
<div className="flex flex-col gap-4">
66+
<div className="flex flex-col gap-5">
5567
<div className="flex items-center justify-between">
56-
<h3 className="text-sm font-semibold uppercase tracking-wider text-slate-400">
68+
<h3 className="text-xs font-semibold uppercase tracking-wider text-slate-400">
5769
{t.sessions.title}
5870
</h3>
5971
{activeSessions.length > 0 && (
6072
<button
6173
type="button"
6274
onClick={onAddTimer}
63-
className="text-xs font-medium text-blue-400 hover:text-blue-300 transition-colors"
75+
className="rounded-full border border-white/15 bg-white/5 px-3 py-1.5 text-xs font-medium text-slate-200 transition-colors hover:bg-white/10"
6476
>
6577
+ {t.sessions.addTimer}
6678
</button>
6779
)}
6880
</div>
6981

7082
{activeSessions.length > 0 && (
71-
<div className="flex flex-col gap-2">
83+
<div className="flex flex-col gap-2.5">
7284
<span className="text-[10px] uppercase tracking-wider text-slate-500">
7385
{t.sessions.active}
7486
</span>
@@ -77,6 +89,7 @@ export function SessionList({
7789
const target = new Date(session.targetTime);
7890
const remaining = Math.max(0, target.getTime() - Date.now());
7991
const isSelected = session.id === selectedSessionId;
92+
const isEditing = editingId === session.id;
8093
return (
8194
<motion.div
8295
key={session.id}
@@ -85,33 +98,94 @@ export function SessionList({
8598
animate={{ opacity: 1, y: 0 }}
8699
exit={{ opacity: 0, x: -12 }}
87100
onClick={() => onSelectSession(session.id)}
88-
className={`flex items-center justify-between rounded-xl px-4 py-3 border cursor-pointer transition-colors
89-
${isSelected ? "bg-blue-500/20 border-blue-500/40" : "bg-white/[0.03] border-white/5 hover:bg-white/[0.06]"}`}
101+
className={`rounded-2xl border p-4 cursor-pointer transition-colors ${
102+
isSelected ? "bg-blue-500/20 border-blue-500/40" : "bg-white/[0.03] border-white/10 hover:bg-white/[0.06]"
103+
}`}
90104
>
91-
<div className="flex flex-col gap-0.5 min-w-0">
92-
<span className="text-sm font-medium text-slate-200 truncate">
93-
{formatTimeLocale(target, locale)}
94-
</span>
95-
<span className="text-xs text-slate-500 tabular-nums">
96-
{formatRemaining(remaining)} left
97-
</span>
98-
</div>
99-
<div className="flex items-center gap-2 shrink-0">
105+
<div className="flex items-start justify-between gap-3">
106+
<div className="min-w-0">
107+
<span className="block text-base font-semibold text-slate-100 truncate">
108+
{formatTargetDisplay(target, locale)}
109+
</span>
110+
<span className="text-xs text-slate-400 tabular-nums">
111+
{formatRemaining(remaining)} left
112+
</span>
113+
</div>
100114
<span className={`rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase ${STATUS_STYLES.active}`}>
101-
{isSelected ? "•" : ""} {t.sessions.active}
115+
{isSelected ? "• " : ""}
116+
{t.sessions.active}
102117
</span>
118+
</div>
119+
120+
<div className="mt-3 flex flex-wrap items-center gap-2">
121+
<button
122+
type="button"
123+
onClick={(e) => {
124+
e.stopPropagation();
125+
setEditingId(session.id);
126+
setEditValue(toDatetimeLocalValue(target));
127+
setEditError(null);
128+
}}
129+
className="rounded-lg border border-white/15 bg-white/5 px-3 py-1.5 text-xs font-medium text-slate-200 hover:bg-white/10"
130+
>
131+
{t.sessions.edit}
132+
</button>
103133
<button
104134
type="button"
105135
onClick={(e) => {
106136
e.stopPropagation();
107137
onCancelSession(session.id);
108138
}}
109-
className="rounded-lg px-2 py-1 text-[10px] font-medium text-red-400 hover:bg-red-500/20 transition-colors"
139+
className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-1.5 text-xs font-medium text-red-300 hover:bg-red-500/20 transition-colors"
110140
aria-label={t.timer.cancel}
111141
>
112142
{t.timer.cancel}
113143
</button>
114144
</div>
145+
146+
{isEditing && (
147+
<div className="mt-3 rounded-xl border border-white/10 bg-black/20 p-3" onClick={(e) => e.stopPropagation()}>
148+
<label className="mb-2 block text-[11px] uppercase tracking-wider text-slate-400">{t.sessions.editTarget}</label>
149+
<input
150+
type="datetime-local"
151+
value={editValue}
152+
min={toDatetimeLocalValue(new Date())}
153+
onChange={(e) => {
154+
setEditValue(e.target.value);
155+
setEditError(null);
156+
}}
157+
className="w-full rounded-lg border border-white/15 bg-white/5 px-3 py-2 text-sm text-slate-100 outline-none focus:border-blue-500/50"
158+
/>
159+
{editError && <p className="mt-2 text-xs text-amber-400">{editError}</p>}
160+
<div className="mt-3 flex items-center gap-2">
161+
<button
162+
type="button"
163+
onClick={() => {
164+
const parsed = parseFutureDatetimeLocal(editValue);
165+
if (!parsed) {
166+
setEditError(t.timer.errorPast);
167+
return;
168+
}
169+
onEditSession(session.id, parsed);
170+
setEditingId(null);
171+
}}
172+
className="rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-blue-500"
173+
>
174+
{t.sessions.save}
175+
</button>
176+
<button
177+
type="button"
178+
onClick={() => {
179+
setEditingId(null);
180+
setEditError(null);
181+
}}
182+
className="rounded-lg border border-white/15 px-3 py-1.5 text-xs text-slate-300 hover:bg-white/10"
183+
>
184+
{t.settings.close}
185+
</button>
186+
</div>
187+
</div>
188+
)}
115189
</motion.div>
116190
);
117191
})}
@@ -120,7 +194,7 @@ export function SessionList({
120194
)}
121195

122196
{hasHistory && (
123-
<div className="flex flex-col gap-2">
197+
<div className="flex flex-col gap-2.5">
124198
<div className="flex items-center justify-between">
125199
<span className="text-[10px] uppercase tracking-wider text-slate-500">
126200
{t.sessions.completed} / {t.sessions.cancelled}

src/features/countdown/components/TimeInput.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@ const DRAFT_KEY = "count23_input_draft";
1515

1616
interface TimeInputProps {
1717
onStart: (target: Date) => void;
18-
defaultMode?: InputMode;
19-
compact?: boolean;
2018
}
2119

22-
export function TimeInput({ onStart, defaultMode = "quick", compact = false }: TimeInputProps) {
20+
export function TimeInput({ onStart }: TimeInputProps) {
2321
const { t } = useLocale();
24-
const [mode, setMode] = useState<InputMode>(defaultMode);
22+
const [mode, setMode] = useState<InputMode>("datetime");
2523
const [timeValue, setTimeValue] = useState("");
2624
const [datetimeValue, setDatetimeValue] = useState("");
2725
const [minLocal, setMinLocal] = useState(() => toDatetimeLocalValue(new Date()));
@@ -31,10 +29,6 @@ export function TimeInput({ onStart, defaultMode = "quick", compact = false }: T
3129
setMinLocal(toDatetimeLocalValue(new Date()));
3230
}, []);
3331

34-
useEffect(() => {
35-
setMode(defaultMode);
36-
}, [defaultMode]);
37-
3832
useEffect(() => {
3933
if (typeof window === "undefined") return;
4034
const raw = localStorage.getItem(DRAFT_KEY);
@@ -100,7 +94,7 @@ export function TimeInput({ onStart, defaultMode = "quick", compact = false }: T
10094
initial={{ opacity: 0, y: 20 }}
10195
animate={{ opacity: 1, y: 0 }}
10296
exit={{ opacity: 0, y: -20 }}
103-
className={`flex flex-col items-center ${compact ? "gap-3" : "gap-4"}`}
97+
className="flex flex-col items-center gap-4"
10498
>
10599
<div
106100
className="flex w-full max-w-sm flex-wrap justify-center gap-1 rounded-xl border border-white/10 bg-white/5 p-1"

0 commit comments

Comments
 (0)