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:
parent
8a75adb94e
commit
70ff02c930
7 changed files with 136 additions and 37 deletions
|
|
@ -86,6 +86,7 @@ export function FamilyProvider({ children: providerChildren }: { children: React
|
||||||
name: c.name,
|
name: c.name,
|
||||||
birthDate: c.birthDate,
|
birthDate: c.birthDate,
|
||||||
sex: c.sex,
|
sex: c.sex,
|
||||||
|
imageUrl: c.imageUrl ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setChildren(childList);
|
setChildren(childList);
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ export default function ActivityPage() {
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="px-4 pb-20">
|
<div className="px-4 pb-24">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-20 text-gray-400">Loading…</div>
|
<div className="text-center py-20 text-gray-400">Loading…</div>
|
||||||
) : view === "calendar" ? (
|
) : view === "calendar" ? (
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export async function GET(request: Request) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const children = await sql.unsafe(
|
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]
|
[familyId]
|
||||||
);
|
);
|
||||||
return NextResponse.json({ children: children || [] });
|
return NextResponse.json({ children: children || [] });
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Geist, Geist_Mono, Caveat } from "next/font/google";
|
||||||
import { ThemeProvider } from "./ThemeProvider";
|
import { ThemeProvider } from "./ThemeProvider";
|
||||||
import { FamilyProvider } from "./FamilyProvider";
|
import { FamilyProvider } from "./FamilyProvider";
|
||||||
import { PageTransition } from "@/components/PageTransition";
|
import { PageTransition } from "@/components/PageTransition";
|
||||||
|
import { BottomNav } from "@/components/BottomNav";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
|
|
@ -41,6 +42,7 @@ export default function RootLayout({
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<FamilyProvider>
|
<FamilyProvider>
|
||||||
<PageTransition>{children}</PageTransition>
|
<PageTransition>{children}</PageTransition>
|
||||||
|
<BottomNav />
|
||||||
</FamilyProvider>
|
</FamilyProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
126
src/app/page.tsx
126
src/app/page.tsx
|
|
@ -5,7 +5,6 @@ import Link from "next/link";
|
||||||
import { useTheme } from "./ThemeProvider";
|
import { useTheme } from "./ThemeProvider";
|
||||||
import { useFamily } from "./FamilyProvider";
|
import { useFamily } from "./FamilyProvider";
|
||||||
import { useStageCheck, type BabyStage } from "@/hooks/useStageCheck";
|
import { useStageCheck, type BabyStage } from "@/hooks/useStageCheck";
|
||||||
import { Button } from "@/components/ui";
|
|
||||||
import { LogModal, type LogType } from "@/components/LogModal";
|
import { LogModal, type LogType } from "@/components/LogModal";
|
||||||
import { getOfflineQueue, processOfflineQueue } from "@/lib/offline-queue";
|
import { getOfflineQueue, processOfflineQueue } from "@/lib/offline-queue";
|
||||||
import { calculateAge, formatTimeAgo } from "@/lib/formatting";
|
import { calculateAge, formatTimeAgo } from "@/lib/formatting";
|
||||||
|
|
@ -47,21 +46,21 @@ function TodaySummary({ logs }: { logs: Log[] }) {
|
||||||
diaper: today.filter(l => l.type === "diaper").length,
|
diaper: today.filter(l => l.type === "diaper").length,
|
||||||
sleep: today.filter(l => l.type === "sleep").length,
|
sleep: today.filter(l => l.type === "sleep").length,
|
||||||
};
|
};
|
||||||
const lastFeed = logs.find(l => l.type === "feed");
|
const lastFeed = today.find(l => l.type === "feed");
|
||||||
const lastDiaper = logs.find(l => l.type === "diaper");
|
const lastDiaper = today.find(l => l.type === "diaper");
|
||||||
const lastSleep = logs.find(l => l.type === "sleep");
|
const lastSleep = today.find(l => l.type === "sleep");
|
||||||
|
|
||||||
if (!lastFeed && !lastDiaper && !lastSleep) return null;
|
if (!lastFeed && !lastDiaper && !lastSleep) return null;
|
||||||
|
|
||||||
return (
|
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">
|
<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: "Feeds", count: counts.feed, last: lastFeed?.loggedAt },
|
||||||
{ icon: "🚼", label: "Diapers", count: counts.diaper, last: lastDiaper?.loggedAt },
|
{ icon: "🚼", label: "Diapers", count: counts.diaper, last: lastDiaper?.loggedAt },
|
||||||
{ icon: "😴", label: "Sleep", count: counts.sleep, last: lastSleep?.loggedAt },
|
{ icon: "😴", label: "Sleep", count: counts.sleep, last: lastSleep?.loggedAt },
|
||||||
].map(item => (
|
].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 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-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>
|
<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() {
|
export default function HomePage() {
|
||||||
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
|
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
|
||||||
|
|
@ -88,6 +87,7 @@ export default function HomePage() {
|
||||||
const [recentLogs, setRecentLogs] = useState<Log[]>([]);
|
const [recentLogs, setRecentLogs] = useState<Log[]>([]);
|
||||||
const [logsLoading, setLogsLoading] = useState(true);
|
const [logsLoading, setLogsLoading] = useState(true);
|
||||||
const [vaccineReminders, setVaccineReminders] = useState<any[]>([]);
|
const [vaccineReminders, setVaccineReminders] = useState<any[]>([]);
|
||||||
|
const [aiChips, setAiChips] = useState<string[]>([]);
|
||||||
const { theme, toggle: toggleTheme } = useTheme();
|
const { theme, toggle: toggleTheme } = useTheme();
|
||||||
const { childId, child, familyId, loading } = useFamily();
|
const { childId, child, familyId, loading } = useFamily();
|
||||||
const stage = useStageCheck(child?.birthDate ?? null);
|
const stage = useStageCheck(child?.birthDate ?? null);
|
||||||
|
|
@ -130,8 +130,40 @@ export default function HomePage() {
|
||||||
fetchRecentLogs();
|
fetchRecentLogs();
|
||||||
}, [childId]);
|
}, [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) {
|
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) {
|
if (!familyId) {
|
||||||
|
|
@ -199,7 +231,7 @@ export default function HomePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<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>
|
<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">
|
<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">
|
<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>
|
{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="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 className="text-2xl">→</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</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 && (
|
{vaccineReminders.length > 0 && (
|
||||||
<div className="mx-4 mb-4 bg-red-50 border border-red-200 px-4 py-3 rounded-xl">
|
<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} />
|
<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="px-4 mb-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<h2 className="font-semibold mb-3 ml-1">Quick Log</h2>
|
||||||
<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>
|
|
||||||
<div className="flex gap-3 overflow-x-auto scrollbar-hide pb-1 -mx-4 px-4">
|
<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("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>
|
<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} />
|
<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>
|
<button onClick={() => handleAiChat()} disabled={aiLoading || !aiInput.trim()} className="px-4 bg-rose-400 text-white rounded-xl text-sm">{aiLoading ? "..." : "Ask"}</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
||||||
{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>)}
|
{(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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 mt-4">
|
<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">
|
<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 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">
|
<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>
|
||||||
|
|
|
||||||
39
src/components/BottomNav.tsx
Normal file
39
src/components/BottomNav.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ export interface Child {
|
||||||
name: string;
|
name: string;
|
||||||
birthDate: string;
|
birthDate: string;
|
||||||
sex: string;
|
sex: string;
|
||||||
|
imageUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LogType = "feed" | "diaper" | "sleep";
|
export type LogType = "feed" | "diaper" | "sleep";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue