From 70ff02c930affc250ca0cd188a02a4a084b7942c Mon Sep 17 00:00:00 2001 From: Mannu Date: Sat, 23 May 2026 19:08:58 +0530 Subject: [PATCH] feat(home): overhaul home screen with bottom nav and UX improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/app/FamilyProvider.tsx | 1 + src/app/activity/page.tsx | 2 +- src/app/api/children/route.ts | 2 +- src/app/layout.tsx | 2 + src/app/page.tsx | 126 ++++++++++++++++++++++++---------- src/components/BottomNav.tsx | 39 +++++++++++ src/types/index.ts | 1 + 7 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 src/components/BottomNav.tsx diff --git a/src/app/FamilyProvider.tsx b/src/app/FamilyProvider.tsx index 5bc3b4d..f9efa7b 100644 --- a/src/app/FamilyProvider.tsx +++ b/src/app/FamilyProvider.tsx @@ -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); diff --git a/src/app/activity/page.tsx b/src/app/activity/page.tsx index 582dda9..22ae5ce 100644 --- a/src/app/activity/page.tsx +++ b/src/app/activity/page.tsx @@ -153,7 +153,7 @@ export default function ActivityPage() { })()} {/* Content */} -
+
{loading ? (
Loading…
) : view === "calendar" ? ( diff --git a/src/app/api/children/route.ts b/src/app/api/children/route.ts index b9ea8a5..bcb29d6 100644 --- a/src/app/api/children/route.ts +++ b/src/app/api/children/route.ts @@ -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 || [] }); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4417222..745e08e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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({ {children} + diff --git a/src/app/page.tsx b/src/app/page.tsx index 42056a3..2e60bb3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 ( -
+
{[ { 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 => ( -
+
{item.icon} {item.count} {item.label} @@ -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([]); const [logsLoading, setLogsLoading] = useState(true); const [vaccineReminders, setVaccineReminders] = useState([]); + const [aiChips, setAiChips] = useState([]); 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
Loading...
; + return ( +
+
+ {["🍼", "😴", "🚼", "πŸ‘Ά"].map((e, i) => ( + {e} + ))} +
+

Loading Tia…

+
+ ); } if (!familyId) { @@ -199,7 +231,7 @@ export default function HomePage() { }; return ( -
+
@@ -215,13 +247,24 @@ export default function HomePage() {
-
πŸ‘Ά
+ {child?.imageUrl + ? {child.name} + :
πŸ‘Ά
+ }
{child?.name || "Baby"}
{calculateAge(child?.birthDate || "")}
β†’
- {pendingCount > 0 &&
{pendingCount} pending log{pendingCount > 1 ? "s" : ""}
} + {pendingCount > 0 && ( + + )} {vaccineReminders.length > 0 && (
@@ -241,27 +284,35 @@ export default function HomePage() { + {stage && (() => { + const h = new Date().getHours(); + type Suggestion = { label: string; type: "feed" | "sleep" | "diaper" }; + const matrix: Record = + 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 ( +
+
+

Suggested now

+

{s.label}

+
+ +
+ ); + })()} +
-
-

Quick Log

- {stage && (() => { - const h = new Date().getHours(); - type Suggestion = { label: string; type: string }; - const matrix: Record = - 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 ( - - Suggested now β†’ {s.label} - - ); - })()} -
+

Quick Log

@@ -281,16 +332,21 @@ export default function HomePage() { 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} />
-
- {QUICK_QUESTIONS.map((q, i) => )} +
+ {(aiChips.length > 0 ? aiChips : AI_CHIP_FALLBACK).map((q, i) => ( + + ))}
-

Recent Activity

+
+

Recent Activity

+ See all β†’ +
- {logsLoading ?

Loading...

: recentLogs.length === 0 ?

No logs yet today

: recentLogs.slice(0, 5).map(log => ( + {logsLoading ?

Loading...

: recentLogs.length === 0 ?

No logs yet today

: recentLogs.slice(0, 3).map(log => (
{log.type === "feed" && "🍼"}{log.type === "sleep" && "😴"}{log.type === "diaper" && "🚼"} diff --git a/src/components/BottomNav.tsx b/src/components/BottomNav.tsx new file mode 100644 index 0000000..be889ec --- /dev/null +++ b/src/components/BottomNav.tsx @@ -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 ( + + ); +} diff --git a/src/types/index.ts b/src/types/index.ts index d80c779..a114e3c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,7 @@ export interface Child { name: string; birthDate: string; sex: string; + imageUrl?: string | null; } export type LogType = "feed" | "diaper" | "sleep";