tia/src/app/page.tsx

265 lines
No EOL
7.8 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
const OFFLINE_QUEUE_KEY = "tia_offline_queue";
export interface OfflineEntry {
id: string;
type: "feed" | "diaper" | "sleep";
data: {
type: "feed" | "diaper" | "sleep";
childId: string;
subType: string;
amountMl?: number;
notes?: string;
startedAt?: string;
endedAt?: string;
};
timestamp: number;
}
// Get queue from localStorage
export function getOfflineQueue(): OfflineEntry[] {
if (typeof window === "undefined") return [];
try {
const data = localStorage.getItem(OFFLINE_QUEUE_KEY);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
}
// Add to queue
export function addToOfflineQueue(entry: Omit<OfflineEntry, "id" | "timestamp">) {
const queue = getOfflineQueue();
queue.push({
...entry,
id: crypto.randomUUID(),
timestamp: Date.now(),
});
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue));
}
// Process queue when online
export async function processOfflineQueue() {
const queue = getOfflineQueue();
if (queue.length === 0) return;
const failed: OfflineEntry[] = [];
for (const entry of queue) {
try {
await fetch("/api/logs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(entry.data),
});
} catch {
failed.push(entry);
}
}
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed));
}
interface LogModalProps {
type: "feed" | "diaper" | "sleep" | null;
childId: string;
onClose: () => void;
}
function LogModal({ type, childId, onClose }: LogModalProps) {
const [loading, setLoading] = useState(false);
const [subType, setSubType] = useState("breast_milk");
const [amountMl, setAmountMl] = useState("");
const [notes, setNotes] = useState("");
if (!type) return null;
const handleSubmit = async () => {
setLoading(true);
const data = {
type,
childId,
subType,
amountMl: amountMl ? Number(amountMl) : undefined,
notes: notes || undefined,
};
try {
// Try online first
const res = await fetch("/api/logs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
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();
}
setLoading(false);
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl p-6 w-full max-w-sm mx-4">
<h2 className="text-xl font-bold mb-4">
{type === "feed" && "Log Feed"}
{type === "diaper" && "Log Diaper"}
{type === "sleep" && "Log Sleep"}
</h2>
{type === "feed" && (
<>
<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"
/>
</>
)}
{type === "diaper" && (
<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>
</select>
)}
{type === "sleep" && (
<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"
/>
<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"
>
{loading ? "Saving..." : "Save"}
</button>
</div>
</div>
</div>
);
}
export default function HomePage() {
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
const [childId] = useState("5ad3b16a-1e0d-45ab-bc91-038397d75d0a");
const [pendingCount, setPendingCount] = useState(0);
// 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);
}, []);
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>
</div>
{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>
)}
<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>
</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>
<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>
<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>
</button>
</div>
<div className="mt-8 text-center">
<p className="text-gray-500">Baby: Baby Tia</p>
</div>
</div>
<LogModal type={modalType} childId={childId} onClose={() => setModalType(null)} />
</div>
);
}