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