Add better home UI with greeting, age card, quick log, recent activity
This commit is contained in:
parent
0adc609e4b
commit
f24c5e7680
1 changed files with 132 additions and 89 deletions
205
src/app/page.tsx
205
src/app/page.tsx
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const OFFLINE_QUEUE_KEY = "tia_offline_queue";
|
||||
|
||||
|
|
@ -20,7 +21,6 @@ export interface OfflineEntry {
|
|||
timestamp: number;
|
||||
}
|
||||
|
||||
// Get queue from localStorage
|
||||
export function getOfflineQueue(): OfflineEntry[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
|
|
@ -31,7 +31,6 @@ export function getOfflineQueue(): OfflineEntry[] {
|
|||
}
|
||||
}
|
||||
|
||||
// Add to queue
|
||||
export function addToOfflineQueue(entry: Omit<OfflineEntry, "id" | "timestamp">) {
|
||||
const queue = getOfflineQueue();
|
||||
queue.push({
|
||||
|
|
@ -42,7 +41,6 @@ export function addToOfflineQueue(entry: Omit<OfflineEntry, "id" | "timestamp">)
|
|||
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue));
|
||||
}
|
||||
|
||||
// Process queue when online
|
||||
export async function processOfflineQueue() {
|
||||
const queue = getOfflineQueue();
|
||||
if (queue.length === 0) return;
|
||||
|
|
@ -89,7 +87,6 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
|||
};
|
||||
|
||||
try {
|
||||
// Try online first
|
||||
const res = await fetch("/api/logs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -97,12 +94,10 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
|||
});
|
||||
|
||||
if (!res.ok && !navigator.onLine) {
|
||||
// Offline - add to queue
|
||||
addToOfflineQueue({ type: type as any, data });
|
||||
}
|
||||
onClose();
|
||||
} catch {
|
||||
// Network error - add to queue
|
||||
addToOfflineQueue({ type: type as any, data });
|
||||
onClose();
|
||||
}
|
||||
|
|
@ -120,32 +115,18 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
|||
|
||||
{type === "feed" && (
|
||||
<>
|
||||
<select
|
||||
value={subType}
|
||||
onChange={(e) => setSubType(e.target.value)}
|
||||
className="w-full p-3 border rounded-xl mb-3"
|
||||
>
|
||||
<select value={subType} onChange={(e) => setSubType(e.target.value)} className="w-full p-3 border rounded-xl mb-3">
|
||||
<option value="breast_milk">Breast Milk</option>
|
||||
<option value="formula">Formula</option>
|
||||
<option value="solid">Solid Food</option>
|
||||
<option value="water">Water</option>
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Amount (ml)"
|
||||
value={amountMl}
|
||||
onChange={(e) => setAmountMl(e.target.value)}
|
||||
className="w-full p-3 border rounded-xl mb-3"
|
||||
/>
|
||||
<input type="number" placeholder="Amount (ml)" value={amountMl} onChange={(e) => setAmountMl(e.target.value)} className="w-full p-3 border rounded-xl mb-3" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "diaper" && (
|
||||
<select
|
||||
value={subType}
|
||||
onChange={(e) => setSubType(e.target.value)}
|
||||
className="w-full p-3 border rounded-xl mb-3"
|
||||
>
|
||||
<select value={subType} onChange={(e) => setSubType(e.target.value)} className="w-full p-3 border rounded-xl mb-3">
|
||||
<option value="wet">Wet</option>
|
||||
<option value="dirty">Dirty</option>
|
||||
<option value="both">Both</option>
|
||||
|
|
@ -153,33 +134,17 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
|||
)}
|
||||
|
||||
{type === "sleep" && (
|
||||
<select
|
||||
value={subType}
|
||||
onChange={(e) => setSubType(e.target.value)}
|
||||
className="w-full p-3 border rounded-xl mb-3"
|
||||
>
|
||||
<select value={subType} onChange={(e) => setSubType(e.target.value)} className="w-full p-3 border rounded-xl mb-3">
|
||||
<option value="nap">Nap</option>
|
||||
<option value="night">Night Sleep</option>
|
||||
</select>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Notes (optional)"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
className="w-full p-3 border rounded-xl mb-4"
|
||||
/>
|
||||
<input type="text" placeholder="Notes (optional)" value={notes} onChange={(e) => setNotes(e.target.value)} className="w-full p-3 border rounded-xl mb-4" />
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button onClick={onClose} className="flex-1 p-3 border rounded-xl">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
className="flex-1 p-3 bg-rose-400 text-white rounded-xl"
|
||||
>
|
||||
<button onClick={onClose} className="flex-1 p-3 border rounded-xl">Cancel</button>
|
||||
<button onClick={handleSubmit} disabled={loading} className="flex-1 p-3 bg-rose-400 text-white rounded-xl">
|
||||
{loading ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -188,74 +153,152 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
|||
);
|
||||
}
|
||||
|
||||
// Helper to calculate age
|
||||
function calculateAge(birthDate: string) {
|
||||
const birth = new Date(birthDate);
|
||||
const now = new Date();
|
||||
const years = now.getFullYear() - birth.getFullYear();
|
||||
const months = now.getMonth() - birth.getMonth();
|
||||
const days = Math.floor((now.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (years > 0) return `${years} year${years > 1 ? "s" : ""} old`;
|
||||
if (months > 0) return `${months} month${months > 1 ? "s" : ""} old`;
|
||||
return `${days} days old`;
|
||||
}
|
||||
|
||||
function getGreeting() {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return "Good morning";
|
||||
if (hour < 18) return "Good afternoon";
|
||||
return "Good evening";
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter();
|
||||
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
|
||||
const [childId] = useState("5ad3b16a-1e0d-45ab-bc91-038397d75d0a");
|
||||
const [pendingCount, setPendingCount] = useState(0);
|
||||
const [lastLogs, setLastLogs] = useState<any[]>([]);
|
||||
|
||||
const child = { name: "Baby Tia", birthDate: "2024-01-15" };
|
||||
|
||||
// Check queue on mount
|
||||
useEffect(() => {
|
||||
const queue = getOfflineQueue();
|
||||
setPendingCount(queue.length);
|
||||
|
||||
// Process queue when coming online
|
||||
const handleOnline = () => processOfflineQueue();
|
||||
window.addEventListener("online", handleOnline);
|
||||
|
||||
// Register service worker
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/sw.js").catch(console.error);
|
||||
}
|
||||
|
||||
return () => window.removeEventListener("online", handleOnline);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch recent logs
|
||||
Promise.all([
|
||||
fetch(`/api/logs?type=feed&childId=${childId}&limit=1`).then(r => r.json()),
|
||||
fetch(`/api/logs?type=sleep&childId=${childId}&limit=1`).then(r => r.json()),
|
||||
fetch(`/api/logs?type=diaper&childId=${childId}&limit=1`).then(r => r.json()),
|
||||
]).then(([feed, sleep, diaper]) => {
|
||||
setLastLogs([
|
||||
feed.entries?.[0],
|
||||
sleep.entries?.[0],
|
||||
diaper.entries?.[0],
|
||||
].filter(Boolean));
|
||||
}).catch(() => {});
|
||||
}, [childId]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Tia 👶</h1>
|
||||
<p className="text-gray-600 mt-2">Your baby tracking companion</p>
|
||||
{/* Header */}
|
||||
<div className="p-4 flex justify-between items-center">
|
||||
<button onClick={() => router.push("/menu")} className="p-2">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => router.push("/settings")} className="p-2">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.83-1.419 2.366-2.317 4.083-2.317 1.717 0 3.253.898 4.083 2.317M12 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Welcome */}
|
||||
<div className="px-6 pb-4">
|
||||
<h1 className="text-2xl font-bold">{getGreeting()} 👋</h1>
|
||||
<p className="text-gray-600">How is {child.name} doing today?</p>
|
||||
</div>
|
||||
|
||||
{/* Age Card */}
|
||||
<div className="mx-4 mb-4 p-4 bg-white rounded-2xl shadow-md">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-rose-100 rounded-full flex items-center justify-center text-2xl">
|
||||
👶
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold">{child.name}</div>
|
||||
<div className="text-gray-500">{calculateAge(child.birthDate)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Offline */}
|
||||
{pendingCount > 0 && (
|
||||
<div className="bg-amber-100 text-amber-800 px-4 py-2 rounded-xl text-center mb-4">
|
||||
{pendingCount} pending log{pendingCount > 1 ? "s" : ""} (offline)
|
||||
<div className="mx-4 mb-4 bg-amber-100 text-amber-800 px-4 py-2 rounded-xl text-center">
|
||||
{pendingCount} pending log{pendingCount > 1 ? "s" : ""} (will sync when online)
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 max-w-md mx-auto">
|
||||
<button
|
||||
onClick={() => setModalType("feed")}
|
||||
className="p-6 bg-white rounded-2xl shadow-md hover:shadow-lg transition text-center"
|
||||
>
|
||||
<div className="text-3xl">🍼</div>
|
||||
<div className="font-medium mt-2">Feed</div>
|
||||
{/* Quick Actions */}
|
||||
<div className="px-4 mb-4">
|
||||
<h2 className="font-semibold mb-3 ml-1">Quick Log</h2>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<button onClick={() => setModalType("feed")} className="flex flex-col items-center p-3 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-2xl">🍼</span>
|
||||
<span className="text-xs mt-1">Feed</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModalType("sleep")}
|
||||
className="p-6 bg-white rounded-2xl shadow-md hover:shadow-lg transition text-center"
|
||||
>
|
||||
<div className="text-3xl">😴</div>
|
||||
<div className="font-medium mt-2">Sleep</div>
|
||||
<button onClick={() => setModalType("sleep")} className="flex flex-col items-center p-3 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-2xl">😴</span>
|
||||
<span className="text-xs mt-1">Sleep</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModalType("diaper")}
|
||||
className="p-6 bg-white rounded-2xl shadow-md hover:shadow-lg transition text-center"
|
||||
>
|
||||
<div className="text-3xl">👶</div>
|
||||
<div className="font-medium mt-2">Diaper</div>
|
||||
<button onClick={() => setModalType("diaper")} className="flex flex-col items-center p-3 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-2xl">👶</span>
|
||||
<span className="text-xs mt-1">Diaper</span>
|
||||
</button>
|
||||
<button className="p-6 bg-white rounded-2xl shadow-md hover:shadow-lg transition text-center">
|
||||
<Link href="/medical">
|
||||
<div className="text-3xl">💊</div>
|
||||
<div className="font-medium mt-2">Medical</div>
|
||||
<Link href="/medical" className="flex flex-col items-center p-3 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-2xl">💊</span>
|
||||
<span className="text-xs mt-1">Medical</span>
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-gray-500">Baby: Baby Tia</p>
|
||||
{/* Recent Activity */}
|
||||
<div className="px-4">
|
||||
<h2 className="font-semibold mb-3 ml-1">Recent Activity</h2>
|
||||
<div className="space-y-2">
|
||||
{lastLogs.length === 0 ? (
|
||||
<p className="text-gray-400 text-sm">No logs yet today</p>
|
||||
) : (
|
||||
lastLogs.filter(Boolean).map((log: any, i: number) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 bg-white rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">
|
||||
{log.type === "feed" && "🍼"}
|
||||
{log.type === "sleep" && "😴"}
|
||||
{log.type === "diaper" && "👶"}
|
||||
</span>
|
||||
<div>
|
||||
<div className="font-medium capitalize">{log.type}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{new Date(log.logged_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{log.amount_ml && <span className="text-sm text-gray-500">{log.amount_ml}ml</span>}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue