fix(milestones): align page UI to match activity and growth pages
- Rose gradient background instead of flat gray - Header with ← back link and child name subtitle - Active filter/category pills use bg-rose-400 instead of purple - Progress bar gradient updated to rose-to-pink - Full-width px-4 layout (removed max-w-2xl wrapper) - Date picker uses design system Input + Button components - Empty state with icon, consistent with other pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7189fc766c
commit
a75a543373
1 changed files with 78 additions and 75 deletions
|
|
@ -3,6 +3,7 @@ import { useEffect, useState, useMemo } from "react";
|
||||||
import { useFamily } from "@/app/FamilyProvider";
|
import { useFamily } from "@/app/FamilyProvider";
|
||||||
import { useStageCheck } from "@/hooks/useStageCheck";
|
import { useStageCheck } from "@/hooks/useStageCheck";
|
||||||
import { MILESTONES, type MilestoneDef } from "@/lib/milestones";
|
import { MILESTONES, type MilestoneDef } from "@/lib/milestones";
|
||||||
|
import { Button, Input } from "@/components/ui";
|
||||||
|
|
||||||
type Category = "all" | "social" | "motor" | "language" | "cognitive";
|
type Category = "all" | "social" | "motor" | "language" | "cognitive";
|
||||||
type Filter = "all" | "achieved" | "upcoming";
|
type Filter = "all" | "achieved" | "upcoming";
|
||||||
|
|
@ -29,7 +30,6 @@ export default function MilestonesPage() {
|
||||||
const [filter, setFilter] = useState<Filter>("all");
|
const [filter, setFilter] = useState<Filter>("all");
|
||||||
const [category, setCategory] = useState<Category>("all");
|
const [category, setCategory] = useState<Category>("all");
|
||||||
|
|
||||||
// date picker state per milestone key
|
|
||||||
const [pendingKey, setPendingKey] = useState<string | null>(null);
|
const [pendingKey, setPendingKey] = useState<string | null>(null);
|
||||||
const [pendingDate, setPendingDate] = useState<string>(
|
const [pendingDate, setPendingDate] = useState<string>(
|
||||||
new Date().toISOString().slice(0, 10)
|
new Date().toISOString().slice(0, 10)
|
||||||
|
|
@ -46,7 +46,6 @@ export default function MilestonesPage() {
|
||||||
|
|
||||||
async function toggleMilestone(m: MilestoneWithStatus) {
|
async function toggleMilestone(m: MilestoneWithStatus) {
|
||||||
if (m.achieved) {
|
if (m.achieved) {
|
||||||
// un-mark
|
|
||||||
await fetch(`/api/milestones/${m.key}?childId=${child!.id}`, { method: "DELETE" });
|
await fetch(`/api/milestones/${m.key}?childId=${child!.id}`, { method: "DELETE" });
|
||||||
setItems(prev => prev.map(x => x.key === m.key ? { ...x, achieved: false, achievedAt: null } : x));
|
setItems(prev => prev.map(x => x.key === m.key ? { ...x, achieved: false, achievedAt: null } : x));
|
||||||
if (pendingKey === m.key) setPendingKey(null);
|
if (pendingKey === m.key) setPendingKey(null);
|
||||||
|
|
@ -84,51 +83,57 @@ export default function MilestonesPage() {
|
||||||
}, [items, stage]);
|
}, [items, stage]);
|
||||||
|
|
||||||
if (!child) return (
|
if (!child) return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center">
|
||||||
<p className="text-gray-500">Select a child to view milestones.</p>
|
<p className="text-gray-500">Select a child to view milestones.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 pb-20">
|
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800 pb-20">
|
||||||
<div className="max-w-2xl mx-auto px-4 py-6">
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="p-4 flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<a href="/menu" className="p-2">←</a>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Milestones</h1>
|
<h1 className="text-xl font-bold">Milestones 🌱</h1>
|
||||||
<p className="text-sm text-gray-500">{child.name}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">{child.name}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{stage && (
|
{stage && (
|
||||||
<span className="text-sm font-medium px-3 py-1 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
|
<span className="text-xs font-medium px-3 py-1.5 rounded-full bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 shadow-sm">
|
||||||
{stage.emoji} {stage.label}
|
{stage.emoji} {stage.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 mb-4 shadow-sm">
|
<div className="mx-4 mb-4 p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||||
<div className="flex items-center justify-between text-sm mb-2">
|
<div className="flex items-center justify-between text-sm mb-2">
|
||||||
<span className="font-medium text-gray-700 dark:text-gray-300">{achievedCount} of {items.length} milestones</span>
|
<span className="font-medium text-gray-700 dark:text-gray-300">
|
||||||
<span className="text-gray-400">{Math.round(achievedCount / Math.max(items.length, 1) * 100)}%</span>
|
{achievedCount} of {items.length} milestones
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{Math.round(achievedCount / Math.max(items.length, 1) * 100)}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
<div className="h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gradient-to-r from-pink-400 to-purple-400 rounded-full transition-all duration-500"
|
className="h-full bg-gradient-to-r from-rose-400 to-pink-400 rounded-full transition-all duration-500"
|
||||||
style={{ width: `${achievedCount / Math.max(items.length, 1) * 100}%` }}
|
style={{ width: `${achievedCount / Math.max(items.length, 1) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter tabs */}
|
{/* Filter tabs */}
|
||||||
<div className="flex gap-2 mb-3 overflow-x-auto pb-1 scrollbar-hide">
|
<div className="px-4 mb-3 flex gap-2 overflow-x-auto">
|
||||||
{(["all", "achieved", "upcoming"] as Filter[]).map(f => (
|
{(["all", "achieved", "upcoming"] as Filter[]).map(f => (
|
||||||
<button
|
<button
|
||||||
key={f}
|
key={f}
|
||||||
onClick={() => setFilter(f)}
|
onClick={() => setFilter(f)}
|
||||||
className={`px-4 py-1.5 rounded-full text-sm font-medium whitespace-nowrap transition-colors ${
|
className={`px-4 py-2 rounded-full text-sm whitespace-nowrap transition-colors ${
|
||||||
filter === f
|
filter === f
|
||||||
? "bg-purple-500 text-white"
|
? "bg-rose-400 text-white"
|
||||||
: "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border dark:border-gray-700"
|
: "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{f.charAt(0).toUpperCase() + f.slice(1)}
|
{f.charAt(0).toUpperCase() + f.slice(1)}
|
||||||
|
|
@ -137,15 +142,15 @@ export default function MilestonesPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category chips */}
|
{/* Category chips */}
|
||||||
<div className="flex gap-2 mb-5 overflow-x-auto pb-1 scrollbar-hide">
|
<div className="px-4 mb-4 flex gap-2 overflow-x-auto">
|
||||||
{(["all", "social", "motor", "language", "cognitive"] as Category[]).map(c => (
|
{(["all", "social", "motor", "language", "cognitive"] as Category[]).map(c => (
|
||||||
<button
|
<button
|
||||||
key={c}
|
key={c}
|
||||||
onClick={() => setCategory(c)}
|
onClick={() => setCategory(c)}
|
||||||
className={`px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap transition-colors ${
|
className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-colors ${
|
||||||
category === c
|
category === c
|
||||||
? "bg-gray-800 dark:bg-gray-200 text-white dark:text-gray-900"
|
? "bg-rose-400 text-white"
|
||||||
: "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border dark:border-gray-700"
|
: "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{c.charAt(0).toUpperCase() + c.slice(1)}
|
{c.charAt(0).toUpperCase() + c.slice(1)}
|
||||||
|
|
@ -154,6 +159,7 @@ export default function MilestonesPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Milestone grid */}
|
{/* Milestone grid */}
|
||||||
|
<div className="px-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
|
@ -161,7 +167,10 @@ export default function MilestonesPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : filtered.length === 0 ? (
|
) : filtered.length === 0 ? (
|
||||||
<p className="text-center text-gray-400 py-12">No milestones match this filter.</p>
|
<div className="text-center py-20 text-gray-400">
|
||||||
|
<div className="text-5xl mb-3">🌱</div>
|
||||||
|
<p>No milestones match this filter.</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
{filtered.map(m => (
|
{filtered.map(m => (
|
||||||
|
|
@ -195,28 +204,22 @@ export default function MilestonesPage() {
|
||||||
|
|
||||||
{/* Inline date picker */}
|
{/* Inline date picker */}
|
||||||
{pendingKey === m.key && (
|
{pendingKey === m.key && (
|
||||||
<div className="mt-2 bg-white dark:bg-gray-800 rounded-xl p-3 shadow-md border dark:border-gray-700">
|
<div className="mt-2 bg-white dark:bg-gray-800 rounded-xl p-3 shadow-md border border-gray-100 dark:border-gray-700">
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">When did this happen?</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">When did this happen?</p>
|
||||||
<input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
value={pendingDate}
|
value={pendingDate}
|
||||||
max={new Date().toISOString().slice(0, 10)}
|
max={new Date().toISOString().slice(0, 10)}
|
||||||
onChange={e => setPendingDate(e.target.value)}
|
onChange={e => setPendingDate(e.target.value)}
|
||||||
className="w-full text-sm border dark:border-gray-600 rounded-lg px-2 py-1.5 dark:bg-gray-700 dark:text-white mb-2"
|
className="mb-2"
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button size="sm" fullWidth onClick={() => confirmAchieved(m.key)}>
|
||||||
onClick={() => confirmAchieved(m.key)}
|
|
||||||
className="flex-1 bg-green-500 text-white rounded-lg py-1.5 text-xs font-medium"
|
|
||||||
>
|
|
||||||
Mark achieved
|
Mark achieved
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button size="sm" variant="secondary" fullWidth onClick={() => setPendingKey(null)}>
|
||||||
onClick={() => setPendingKey(null)}
|
|
||||||
className="flex-1 border dark:border-gray-600 rounded-lg py-1.5 text-xs text-gray-600 dark:text-gray-400"
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue