feat(home): overhaul home screen with bottom nav and UX improvements

- Add persistent bottom navigation bar (Home / Activity / AI / Menu)
- Fix TodaySummary bug: last-log times now show today's events only
- Replace 6 hardcoded AI chips with 3 AI-generated context-aware chips
- Show child's real profile photo in baby card (fallback to 👶 emoji)
- Recent Activity limited to 3 items with "See all →" link to /activity
- "Suggested now" promoted to prominent amber banner with "Log it →" CTA
- Offline pending banner is now a tappable retry button
- Branded loading state with bouncing emoji (🍼 😴 🚼 👶)
- Remove unused Button import from page.tsx
- Expose image_url via /api/children and Child type/FamilyProvider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-23 19:08:58 +05:30
parent 8a75adb94e
commit 70ff02c930
7 changed files with 136 additions and 37 deletions

View file

@ -86,6 +86,7 @@ export function FamilyProvider({ children: providerChildren }: { children: React
name: c.name,
birthDate: c.birthDate,
sex: c.sex,
imageUrl: c.imageUrl ?? null,
}));
setChildren(childList);

View file

@ -153,7 +153,7 @@ export default function ActivityPage() {
})()}
{/* Content */}
<div className="px-4 pb-20">
<div className="px-4 pb-24">
{loading ? (
<div className="text-center py-20 text-gray-400">Loading</div>
) : view === "calendar" ? (

View file

@ -13,7 +13,7 @@ export async function GET(request: Request) {
try {
const children = await sql.unsafe(
`SELECT id, name, birth_date as "birthDate", sex, stage, created_at as "createdAt" FROM children WHERE family_id = $1 ORDER BY created_at DESC`,
`SELECT id, name, birth_date as "birthDate", sex, stage, image_url as "imageUrl", created_at as "createdAt" FROM children WHERE family_id = $1 ORDER BY created_at DESC`,
[familyId]
);
return NextResponse.json({ children: children || [] });

View file

@ -3,6 +3,7 @@ import { Geist, Geist_Mono, Caveat } from "next/font/google";
import { ThemeProvider } from "./ThemeProvider";
import { FamilyProvider } from "./FamilyProvider";
import { PageTransition } from "@/components/PageTransition";
import { BottomNav } from "@/components/BottomNav";
import "./globals.css";
const geistSans = Geist({
@ -41,6 +42,7 @@ export default function RootLayout({
<ThemeProvider>
<FamilyProvider>
<PageTransition>{children}</PageTransition>
<BottomNav />
</FamilyProvider>
</ThemeProvider>
</body>

View file

@ -5,7 +5,6 @@ import Link from "next/link";
import { useTheme } from "./ThemeProvider";
import { useFamily } from "./FamilyProvider";
import { useStageCheck, type BabyStage } from "@/hooks/useStageCheck";
import { Button } from "@/components/ui";
import { LogModal, type LogType } from "@/components/LogModal";
import { getOfflineQueue, processOfflineQueue } from "@/lib/offline-queue";
import { calculateAge, formatTimeAgo } from "@/lib/formatting";
@ -47,21 +46,21 @@ function TodaySummary({ logs }: { logs: Log[] }) {
diaper: today.filter(l => l.type === "diaper").length,
sleep: today.filter(l => l.type === "sleep").length,
};
const lastFeed = logs.find(l => l.type === "feed");
const lastDiaper = logs.find(l => l.type === "diaper");
const lastSleep = logs.find(l => l.type === "sleep");
const lastFeed = today.find(l => l.type === "feed");
const lastDiaper = today.find(l => l.type === "diaper");
const lastSleep = today.find(l => l.type === "sleep");
if (!lastFeed && !lastDiaper && !lastSleep) return null;
return (
<div className="mx-4 mb-4 bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden">
<div className="mx-4 mb-3 bg-white dark:bg-gray-800 rounded-2xl shadow-sm overflow-hidden">
<div className="grid grid-cols-3 divide-x divide-gray-100 dark:divide-gray-700">
{[
{ icon: "🍼", label: "Feeds", count: counts.feed, last: lastFeed?.loggedAt },
{ icon: "🚼", label: "Diapers", count: counts.diaper, last: lastDiaper?.loggedAt },
{ icon: "😴", label: "Sleep", count: counts.sleep, last: lastSleep?.loggedAt },
].map(item => (
<div key={item.label} className="flex flex-col items-center py-3 px-1">
<div key={item.label} className="flex flex-col items-center py-2 px-1">
<span className="text-xl mb-0.5">{item.icon}</span>
<span className="text-xl font-bold text-gray-800 dark:text-white leading-tight">{item.count}</span>
<span className="text-xs text-gray-400 dark:text-gray-500">{item.label}</span>
@ -75,7 +74,7 @@ function TodaySummary({ logs }: { logs: Log[] }) {
);
}
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 AI_CHIP_FALLBACK = ["How much should baby eat?", "Sleep schedule tips?", "Development milestones?"];
export default function HomePage() {
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
@ -88,6 +87,7 @@ export default function HomePage() {
const [recentLogs, setRecentLogs] = useState<Log[]>([]);
const [logsLoading, setLogsLoading] = useState(true);
const [vaccineReminders, setVaccineReminders] = useState<any[]>([]);
const [aiChips, setAiChips] = useState<string[]>([]);
const { theme, toggle: toggleTheme } = useTheme();
const { childId, child, familyId, loading } = useFamily();
const stage = useStageCheck(child?.birthDate ?? null);
@ -130,8 +130,40 @@ export default function HomePage() {
fetchRecentLogs();
}, [childId]);
useEffect(() => {
if (!childId || !child) return;
const age = calculateAge(child.birthDate);
fetch("/api/ai", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
childId,
messages: [{
role: "user",
content: `Generate exactly 3 short question chips (max 7 words each) a parent might want to ask right now about their baby ${child.name}, age ${age}. Reply ONLY with a JSON array of 3 strings, no other text.`,
}],
}),
})
.then(r => r.json())
.then(d => {
const chips = JSON.parse(d.reply);
if (Array.isArray(chips) && chips.length > 0) setAiChips(chips.slice(0, 3));
else setAiChips(AI_CHIP_FALLBACK);
})
.catch(() => setAiChips(AI_CHIP_FALLBACK));
}, [childId, child?.id]);
if (loading) {
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
return (
<div className="min-h-screen flex flex-col items-center justify-center gap-4 bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
<div className="flex gap-3 text-4xl">
{["🍼", "😴", "🚼", "👶"].map((e, i) => (
<span key={i} className="animate-bounce" style={{ animationDelay: `${i * 120}ms` }}>{e}</span>
))}
</div>
<p className="text-sm text-gray-400">Loading Tia</p>
</div>
);
}
if (!familyId) {
@ -199,7 +231,7 @@ export default function HomePage() {
};
return (
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800 pb-24">
<div className="p-4 flex justify-between items-center">
<button className="p-2"><Link href="/menu"><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></Link></button>
<div className="flex items-center gap-1">
@ -215,13 +247,24 @@ export default function HomePage() {
<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="w-16 h-16 bg-rose-100 dark:bg-rose-900 rounded-full flex items-center justify-center text-2xl">👶</div>
{child?.imageUrl
? <img src={child.imageUrl} alt={child.name} className="w-16 h-16 rounded-full object-cover" />
: <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="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>
</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 && (
<button
onClick={() => { processOfflineQueue(); setPendingCount(0); }}
className="mx-4 mb-3 w-[calc(100%-2rem)] bg-amber-100 text-amber-800 px-4 py-2 rounded-xl flex items-center justify-between"
>
<span> {pendingCount} pending log{pendingCount > 1 ? "s" : ""}</span>
<span className="text-sm font-medium">Retry now </span>
</button>
)}
{vaccineReminders.length > 0 && (
<div className="mx-4 mb-4 bg-red-50 border border-red-200 px-4 py-3 rounded-xl">
@ -241,27 +284,35 @@ export default function HomePage() {
<TodaySummary logs={recentLogs} />
{stage && (() => {
const h = new Date().getHours();
type Suggestion = { label: string; type: "feed" | "sleep" | "diaper" };
const matrix: Record<BabyStage, Suggestion> =
h >= 5 && h < 9 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Feed", type: "feed" }, sitter: { label: "Feed", type: "feed" }, crawler: { label: "Feed", type: "feed" }, toddler: { label: "Feed", type: "feed" }, walker: { label: "Feed", type: "feed" } } :
h >= 9 && h < 12 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Feed + Diaper", type: "feed" }, sitter: { label: "Solids", type: "feed" }, crawler: { label: "Solids", type: "feed" }, toddler: { label: "Solids", type: "feed" }, walker: { label: "Solids", type: "feed" } } :
h >= 12 && h < 15 ? { newborn: { label: "Nap time", type: "sleep" }, infant: { label: "Nap time", type: "sleep" }, sitter: { label: "Nap time", type: "sleep" }, crawler: { label: "Nap time", type: "sleep" }, toddler: { label: "Nap time", type: "sleep" }, walker: { label: "Rest time", type: "sleep" } } :
h >= 15 && h < 18 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Feed", type: "feed" }, sitter: { label: "Snack time", type: "feed" }, crawler: { label: "Snack time", type: "feed" }, toddler: { label: "Snack time", type: "feed" }, walker: { label: "Snack time", type: "feed" } } :
h >= 18 && h < 21 ? { newborn: { label: "Evening feed", type: "feed" }, infant: { label: "Evening feed", type: "feed" }, sitter: { label: "Dinner feed", type: "feed" }, crawler: { label: "Dinner feed", type: "feed" }, toddler: { label: "Dinner feed", type: "feed" }, walker: { label: "Dinner feed", type: "feed" } } :
{ newborn: { label: "Night feed", type: "feed" }, infant: { label: "Night feed", type: "feed" }, sitter: { label: "Bedtime", type: "sleep" }, crawler: { label: "Bedtime", type: "sleep" }, toddler: { label: "Bedtime", type: "sleep" }, walker: { label: "Bedtime", type: "sleep" } };
const s = matrix[stage.stage];
return (
<div className="mx-4 mb-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-2xl px-4 py-3 flex items-center justify-between">
<div>
<p className="text-xs font-semibold text-amber-600 dark:text-amber-400 uppercase tracking-wide">Suggested now</p>
<p className="text-base font-bold text-amber-900 dark:text-amber-100">{s.label}</p>
</div>
<button
onClick={() => setModalType(s.type)}
className="px-4 py-2 bg-amber-400 text-white rounded-xl text-sm font-semibold"
>
Log it
</button>
</div>
);
})()}
<div className="px-4 mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold ml-1">Quick Log</h2>
{stage && (() => {
const h = new Date().getHours();
type Suggestion = { label: string; type: string };
const matrix: Record<BabyStage, Suggestion> =
h >= 5 && h < 9 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Feed", type: "feed" }, sitter: { label: "Feed", type: "feed" }, crawler: { label: "Feed", type: "feed" }, toddler: { label: "Feed", type: "feed" }, walker: { label: "Feed", type: "feed" } } :
h >= 9 && h < 12 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Feed + Diaper", type: "feed" }, sitter: { label: "Solids", type: "feed" }, crawler: { label: "Solids", type: "feed" }, toddler: { label: "Solids", type: "feed" }, walker: { label: "Solids", type: "feed" } } :
h >= 12 && h < 15 ? { newborn: { label: "Sleep", type: "sleep" }, infant: { label: "Sleep", type: "sleep" }, sitter: { label: "Nap", type: "sleep" }, crawler: { label: "Nap", type: "sleep" }, toddler: { label: "Nap", type: "sleep" }, walker: { label: "Rest", type: "sleep" } } :
h >= 15 && h < 18 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Feed", type: "feed" }, sitter: { label: "Snack", type: "feed" }, crawler: { label: "Snack", type: "feed" }, toddler: { label: "Snack", type: "feed" }, walker: { label: "Snack", type: "feed" } } :
h >= 18 && h < 21 ? { newborn: { label: "Feed", type: "feed" }, infant: { label: "Bath + Feed", type: "feed" }, sitter: { label: "Bath", type: "feed" }, crawler: { label: "Bath", type: "feed" }, toddler: { label: "Bath", type: "feed" }, walker: { label: "Bath", type: "feed" } } :
{ newborn: { label: "Night feed", type: "feed" }, infant: { label: "Night feed", type: "feed" }, sitter: { label: "Sleep", type: "sleep" }, crawler: { label: "Sleep", type: "sleep" }, toddler: { label: "Sleep", type: "sleep" }, walker: { label: "Sleep", type: "sleep" } };
const s = matrix[stage.stage];
return (
<span className="text-xs font-medium px-2.5 py-1 rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
Suggested now {s.label}
</span>
);
})()}
</div>
<h2 className="font-semibold mb-3 ml-1">Quick Log</h2>
<div className="flex gap-3 overflow-x-auto scrollbar-hide pb-1 -mx-4 px-4">
<button onClick={() => setModalType("feed")} className="flex-shrink-0 flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm w-[72px]"><span className="text-3xl">🍼</span><span className="text-sm mt-1">Feed</span></button>
<button onClick={() => setModalType("sleep")} className="flex-shrink-0 flex flex-col items-center p-4 bg-white dark:bg-gray-800 rounded-xl shadow-sm w-[72px]"><span className="text-3xl">😴</span><span className="text-sm mt-1">Sleep</span></button>
@ -281,16 +332,21 @@ export default function HomePage() {
<input type="text" value={aiInput} onChange={e => setAiInput(e.target.value)} onKeyDown={e => e.key === "Enter" && handleAiChat()} placeholder="Ask anything..." className="flex-1 p-2 border dark:border-gray-600 rounded-xl text-sm bg-white dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" disabled={aiLoading} />
<button onClick={() => handleAiChat()} disabled={aiLoading || !aiInput.trim()} className="px-4 bg-rose-400 text-white rounded-xl text-sm">{aiLoading ? "..." : "Ask"}</button>
</div>
<div className="flex flex-wrap gap-2">
{QUICK_QUESTIONS.map((q, i) => <button key={i} onClick={() => handleAiChat(q)} className="px-3 py-1 bg-rose-50 dark:bg-gray-700 text-rose-600 dark:text-rose-400 rounded-full text-sm">{q}</button>)}
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
{(aiChips.length > 0 ? aiChips : AI_CHIP_FALLBACK).map((q, i) => (
<button key={i} onClick={() => handleAiChat(q)} className="flex-shrink-0 px-3 py-1.5 bg-rose-50 dark:bg-gray-700 text-rose-600 dark:text-rose-400 rounded-full text-sm whitespace-nowrap">{q}</button>
))}
</div>
</div>
</div>
<div className="px-4 mt-4">
<h2 className="font-semibold mb-3 ml-1">Recent Activity</h2>
<div className="flex justify-between items-center mb-3">
<h2 className="font-semibold ml-1">Recent Activity</h2>
<Link href="/activity" className="text-rose-500 text-sm">See all </Link>
</div>
<div className="space-y-2">
{logsLoading ? <p className="text-gray-400 text-sm">Loading...</p> : recentLogs.length === 0 ? <p className="text-gray-400 text-sm">No logs yet today</p> : recentLogs.slice(0, 5).map(log => (
{logsLoading ? <p className="text-gray-400 text-sm">Loading...</p> : recentLogs.length === 0 ? <p className="text-gray-400 text-sm">No logs yet today</p> : recentLogs.slice(0, 3).map(log => (
<div key={log.id} className="flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-xl">
<div className="flex items-center gap-3">
<span className="text-xl">{log.type === "feed" && "🍼"}{log.type === "sleep" && "😴"}{log.type === "diaper" && "🚼"}</span>

View file

@ -0,0 +1,39 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const TABS = [
{ href: "/", icon: "🏠", label: "Home" },
{ href: "/activity", icon: "📋", label: "Activity" },
{ href: "/ai", icon: "🤖", label: "Ask AI" },
{ href: "/menu", icon: "☰", label: "Menu" },
] as const;
const HIDDEN_PREFIXES = ["/login", "/onboarding", "/admin"];
export function BottomNav() {
const pathname = usePathname();
if (HIDDEN_PREFIXES.some(p => pathname?.startsWith(p))) return null;
return (
<nav className="fixed bottom-0 inset-x-0 bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800 flex justify-around items-center py-2 z-40">
{TABS.map(tab => {
const isActive = tab.href === "/" ? pathname === "/" : pathname?.startsWith(tab.href);
return (
<Link
key={tab.href}
href={tab.href}
className={`flex flex-col items-center gap-0.5 px-4 py-1 rounded-xl transition-colors ${
isActive ? "text-rose-500" : "text-gray-400 dark:text-gray-500"
}`}
>
<span className="text-xl leading-none">{tab.icon}</span>
<span className="text-xs font-medium">{tab.label}</span>
</Link>
);
})}
</nav>
);
}

View file

@ -5,6 +5,7 @@ export interface Child {
name: string;
birthDate: string;
sex: string;
imageUrl?: string | null;
}
export type LogType = "feed" | "diaper" | "sleep";