Add activity scroller to home page
- Add time-based activity scroller showing lastFeed, lastSleep, lastDiaper times
- Scrolling marquee animation below vaccine reminders
- Fix diaper icon to 🧷
- Link baby profile card to growth page
- Add loading state for recent logs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
b10bfb0e2f
commit
9e258d74b4
2 changed files with 65 additions and 11 deletions
|
|
@ -19,6 +19,15 @@
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes marquee {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
100% { transform: translateX(-50%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-marquee {
|
||||||
|
animation: marquee 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ export async function processOfflineQueue() {
|
||||||
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed));
|
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- AI Session Functions (using database) ---
|
|
||||||
async function getSessions(cid: string): Promise<ChatSession[]> {
|
async function getSessions(cid: string): Promise<ChatSession[]> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/chat?childId=${cid}`);
|
const res = await fetch(`/api/chat?childId=${cid}`);
|
||||||
|
|
@ -134,6 +133,48 @@ function getGreeting() {
|
||||||
return "Good evening";
|
return "Good evening";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTimeAgo(dateStr: string | null | undefined) {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
if (diffMins < 1) return "just now";
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
return `${diffDays}d ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActivityScroller({ lastLogs }: { lastLogs: any[] }) {
|
||||||
|
const feedLog = lastLogs.find(l => l?.type === "feed");
|
||||||
|
const sleepLog = lastLogs.find(l => l?.type === "sleep");
|
||||||
|
const diaperLog = lastLogs.find(l => l?.type === "diaper");
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ icon: "🍼", label: "Fed", time: feedLog?.logged_at },
|
||||||
|
{ icon: "😴", label: "Sleep", time: sleepLog?.logged_at },
|
||||||
|
{ icon: "🧷", label: "Diaper", time: diaperLog?.logged_at },
|
||||||
|
].filter(item => item.time);
|
||||||
|
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-4 mb-4 overflow-hidden">
|
||||||
|
<div className="flex animate-marquee gap-4">
|
||||||
|
{[...items, ...items].map((item, i) => (
|
||||||
|
<div key={i} className="flex-shrink-0 flex items-center gap-2 px-3 py-2 bg-white dark:bg-gray-800 rounded-full shadow-sm">
|
||||||
|
<span className="text-lg">{item.icon}</span>
|
||||||
|
<span className="text-sm dark:text-gray-300">{item.label}:</span>
|
||||||
|
<span className="text-sm font-medium text-rose-500">{formatTimeAgo(item.time)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const QUICK_QUESTIONS = ["How much should my baby eat?", "When should baby sleep?", "Is fever normal?", "How to increase milk supply?", "Baby won't sleep", "Starting solids?"];
|
const QUICK_QUESTIONS = ["How much should my baby eat?", "When should baby sleep?", "Is fever normal?", "How to increase milk supply?", "Baby won't sleep", "Starting solids?"];
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
|
@ -145,6 +186,7 @@ export default function HomePage() {
|
||||||
const [homeSessionId, setHomeSessionId] = useState<string | null>(null);
|
const [homeSessionId, setHomeSessionId] = useState<string | null>(null);
|
||||||
const [pendingCount, setPendingCount] = useState(0);
|
const [pendingCount, setPendingCount] = useState(0);
|
||||||
const [lastLogs, setLastLogs] = useState<any[]>([]);
|
const [lastLogs, setLastLogs] = useState<any[]>([]);
|
||||||
|
const [logsLoading, setLogsLoading] = useState(true);
|
||||||
const [vaccineReminders, setVaccineReminders] = useState<any[]>([]);
|
const [vaccineReminders, setVaccineReminders] = useState<any[]>([]);
|
||||||
const { theme, toggle: toggleTheme } = useTheme();
|
const { theme, toggle: toggleTheme } = useTheme();
|
||||||
const { childId, child, familyId, loading } = useFamily();
|
const { childId, child, familyId, loading } = useFamily();
|
||||||
|
|
@ -173,18 +215,21 @@ export default function HomePage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!childId) return;
|
if (!childId) return;
|
||||||
|
setLogsLoading(true);
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch(`/api/logs?type=feed&childId=${childId}&limit=1`).then(r => r.json()),
|
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=sleep&childId=${childId}&limit=1`).then(r => r.json()),
|
||||||
fetch(`/api/logs?type=diaper&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)));
|
]).then(([feed, sleep, diaper]) => {
|
||||||
|
setLastLogs([feed.entries?.[0], sleep.entries?.[0], diaper.entries?.[0]].filter(Boolean));
|
||||||
|
setLogsLoading(false);
|
||||||
|
}).catch(() => setLogsLoading(false));
|
||||||
}, [childId]);
|
}, [childId]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
|
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not logged in - redirect to login
|
|
||||||
if (!familyId) {
|
if (!familyId) {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.location.href = "/login";
|
window.location.href = "/login";
|
||||||
|
|
@ -200,7 +245,6 @@ export default function HomePage() {
|
||||||
toggleTheme();
|
toggleTheme();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Unified AI chat — creates a fresh session per modal open, reuses it for follow-ups
|
|
||||||
const handleAiChat = async (question?: string) => {
|
const handleAiChat = async (question?: string) => {
|
||||||
const q = question || aiInput;
|
const q = question || aiInput;
|
||||||
if (!q.trim() || aiLoading) return;
|
if (!q.trim() || aiLoading) return;
|
||||||
|
|
@ -210,7 +254,6 @@ export default function HomePage() {
|
||||||
const inputVal = q.trim();
|
const inputVal = q.trim();
|
||||||
if (!question) setAiInput("");
|
if (!question) setAiInput("");
|
||||||
|
|
||||||
// Reuse session within same modal open, or create fresh one
|
|
||||||
let sessionId = homeSessionId;
|
let sessionId = homeSessionId;
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
const newSession = await createSession(childId);
|
const newSession = await createSession(childId);
|
||||||
|
|
@ -220,7 +263,6 @@ export default function HomePage() {
|
||||||
setAiChats([]);
|
setAiChats([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimistically show user message
|
|
||||||
const userMsg: AIChat = { id: "tmp-" + Date.now(), role: "user", content: inputVal, createdAt: new Date().toISOString() };
|
const userMsg: AIChat = { id: "tmp-" + Date.now(), role: "user", content: inputVal, createdAt: new Date().toISOString() };
|
||||||
setAiChats(prev => [...prev, userMsg]);
|
setAiChats(prev => [...prev, userMsg]);
|
||||||
|
|
||||||
|
|
@ -264,12 +306,13 @@ export default function HomePage() {
|
||||||
<p className="text-gray-600 dark:text-gray-300">How is {child?.name || "your baby"} doing today?</p>
|
<p className="text-gray-600 dark:text-gray-300">How is {child?.name || "your baby"} doing today?</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-4 mb-4 p-4 bg-white dark:bg-gray-800 rounded-2xl shadow-md">
|
<Link href="/growth" className="mx-4 mb-4 p-4 bg-white dark:bg-gray-800 rounded-2xl shadow-md block">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-16 h-16 bg-rose-100 dark:bg-rose-900 rounded-full flex items-center justify-center text-2xl">👶</div>
|
<div className="w-16 h-16 bg-rose-100 dark:bg-rose-900 rounded-full flex items-center justify-center text-2xl">👶</div>
|
||||||
<div><div className="text-lg font-semibold">{child?.name || "Baby"}</div><div className="text-gray-500 dark:text-gray-400">{calculateAge(child?.birthDate || "")}</div></div>
|
<div className="flex-1"><div className="text-lg font-semibold">{child?.name || "Baby"}</div><div className="text-gray-500 dark:text-gray-400">{calculateAge(child?.birthDate || "")}</div></div>
|
||||||
|
<div className="text-2xl">→</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
|
|
||||||
{pendingCount > 0 && <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" : ""}</div>}
|
{pendingCount > 0 && <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" : ""}</div>}
|
||||||
|
|
||||||
|
|
@ -289,12 +332,14 @@ export default function HomePage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ActivityScroller lastLogs={lastLogs} />
|
||||||
|
|
||||||
<div className="px-4 mb-4">
|
<div className="px-4 mb-4">
|
||||||
<h2 className="font-semibold mb-3 ml-1">Quick Log</h2>
|
<h2 className="font-semibold mb-3 ml-1">Quick Log</h2>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
<button onClick={() => setModalType("feed")} className="flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm"><span className="text-3xl">🍼</span><span className="text-sm mt-1">Feed</span></button>
|
<button onClick={() => setModalType("feed")} className="flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm"><span className="text-3xl">🍼</span><span className="text-sm mt-1">Feed</span></button>
|
||||||
<button onClick={() => setModalType("sleep")} className="flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm"><span className="text-3xl">😴</span><span className="text-sm mt-1">Sleep</span></button>
|
<button onClick={() => setModalType("sleep")} className="flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm"><span className="text-3xl">😴</span><span className="text-sm mt-1">Sleep</span></button>
|
||||||
<button onClick={() => setModalType("diaper")} className="flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm"><span className="text-3xl">👶</span><span className="text-sm mt-1">Diaper</span></button>
|
<button onClick={() => setModalType("diaper")} className="flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm"><span className="text-3xl">🧷</span><span className="text-sm mt-1">Diaper</span></button>
|
||||||
<Link href="/medical" className="flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm"><span className="text-3xl">💊</span><span className="text-sm mt-1">Medical</span></Link>
|
<Link href="/medical" className="flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm"><span className="text-3xl">💊</span><span className="text-sm mt-1">Medical</span></Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -318,7 +363,7 @@ export default function HomePage() {
|
||||||
<div className="px-4 mt-4">
|
<div className="px-4 mt-4">
|
||||||
<h2 className="font-semibold mb-3 ml-1">Recent Activity</h2>
|
<h2 className="font-semibold mb-3 ml-1">Recent Activity</h2>
|
||||||
<div className="space-y-2">
|
<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) => (
|
{logsLoading ? <p className="text-gray-400 text-sm">Loading...</p> : 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 dark:bg-gray-800 rounded-xl">
|
<div key={i} className="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-xl">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xl">{log.type === "feed" && "🍼"}{log.type === "sleep" && "😴"}{log.type === "diaper" && "👶"}</span>
|
<span className="text-xl">{log.type === "feed" && "🍼"}{log.type === "sleep" && "😴"}{log.type === "diaper" && "👶"}</span>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue