Add Activity page with timeline view
- Timeline view showing logs grouped by day - Filter by log type (feed/sleep/diaper) - Toggle between timeline and calendar views - Calendar view placeholder for future implementation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
38a773f882
commit
6ffa2dd875
1 changed files with 167 additions and 0 deletions
167
src/app/activity/page.tsx
Normal file
167
src/app/activity/page.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
type ViewMode = "timeline" | "calendar";
|
||||
type LogType = "feed" | "sleep" | "diaper";
|
||||
|
||||
interface Log {
|
||||
id: string;
|
||||
type: LogType;
|
||||
subType?: string;
|
||||
amount?: number;
|
||||
notes?: string;
|
||||
loggedAt: string;
|
||||
}
|
||||
|
||||
interface DayLogs {
|
||||
date: string;
|
||||
logs: Log[];
|
||||
}
|
||||
|
||||
export default function ActivityPage() {
|
||||
const [view, setView] = useState<ViewMode>("timeline");
|
||||
const [logs, setLogs] = useState<Log[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState<LogType | "all">("all");
|
||||
const childId = "default";
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, []);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/logs?childId=${childId}&limit=100`);
|
||||
const data = await res.json();
|
||||
setLogs(data.entries || []);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch:", err);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const filteredLogs = filter === "all" ? logs : logs.filter((l) => l.type === filter);
|
||||
|
||||
const groupedByDay = filteredLogs.reduce((acc: DayLogs[], log) => {
|
||||
const date = new Date(log.loggedAt).toDateString();
|
||||
const existing = acc.find((d) => d.date === date);
|
||||
if (existing) {
|
||||
existing.logs.push(log);
|
||||
} else {
|
||||
acc.push({ date, logs: [log] });
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const getIcon = (type: LogType) => {
|
||||
switch (type) {
|
||||
case "feed": return "🍼";
|
||||
case "sleep": return "😴";
|
||||
case "diaper": return "👶";
|
||||
default: return "📝";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
|
||||
{/* Header */}
|
||||
<div className="p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/menu" className="p-2">←</Link>
|
||||
<h1 className="text-xl font-bold">Activity</h1>
|
||||
</div>
|
||||
{/* View Toggle */}
|
||||
<div className="flex bg-white dark:bg-gray-800 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setView("timeline")}
|
||||
className={`px-3 py-1 rounded-md text-sm ${view === "timeline" ? "bg-rose-400 text-white" : ""}`}
|
||||
>
|
||||
Timeline
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView("calendar")}
|
||||
className={`px-3 py-1 rounded-md text-sm ${view === "calendar" ? "bg-rose-400 text-white" : ""}`}
|
||||
>
|
||||
Calendar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="px-4 mb-4 flex gap-2 overflow-x-auto">
|
||||
{(["all", "feed", "sleep", "diaper"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-4 py-2 rounded-full text-sm whitespace-nowrap ${
|
||||
filter === f
|
||||
? "bg-rose-400 text-white"
|
||||
: "bg-white dark:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
{f === "all" ? "All" : f.charAt(0).toUpperCase() + f.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 pb-20">
|
||||
{loading ? (
|
||||
<div className="text-center py-20 text-gray-400">Loading...</div>
|
||||
) : view === "timeline" ? (
|
||||
/* Timeline View */
|
||||
groupedByDay.length === 0 ? (
|
||||
<div className="text-center py-20 text-gray-400">
|
||||
<div className="text-6xl mb-4">📊</div>
|
||||
<p>No activity yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{groupedByDay.map((day) => (
|
||||
<div key={day.date}>
|
||||
<div className="text-sm font-medium text-gray-500 mb-2">
|
||||
{new Date(day.date).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{day.logs
|
||||
.sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime())
|
||||
.map((log) => (
|
||||
<div key={log.id} className="flex items-center gap-3 p-3 bg-white dark:bg-gray-800 rounded-xl">
|
||||
<span className="text-2xl">{getIcon(log.type)}</span>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium capitalize">{log.type}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{log.subType && `${log.subType} · `}
|
||||
{log.amount && `${log.amount}ml`}
|
||||
{log.notes && ` · ${log.notes}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
/* Calendar View */
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-4">
|
||||
<div className="text-center text-gray-400 py-20">
|
||||
<div className="text-6xl mb-4">📅</div>
|
||||
<p>Calendar view coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue