Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,12 @@ jobs:
run: npm run typecheck
env:
# Checks app + tests + the .next/types route types the Build step
# just generated; needs the same schema/type heap as before.
NODE_OPTIONS: --max-old-space-size=4096
# just generated. At 4,096 MB this ran out of heap on main at
# fda9dcfc ("Ineffective mark-compacts near heap limit", exit 134)
# after passing at the same cap on the heads before it: the checked
# program has grown to the cap. It runs alone after the build, on a
# 16 GB runner, so it gets 6,144 MB.
NODE_OPTIONS: --max-old-space-size=6144

mobile:
# The Expo app (mobile/) is its own npm project sharing design/tokens.json
Expand Down
36 changes: 18 additions & 18 deletions app/(app)/dashboard/food/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ function ListRow({ label, meta, dot }: { label: string; meta?: string; dot?: str
}

const FEATURES = [
{ icon: Sparkles, tint: 'bg-blue-500/15 text-blue-400', title: 'Smart Meal Planning', desc: 'AI-powered meal suggestions based on preferences, dietary needs and goals.' },
{ icon: BookOpen, tint: 'bg-emerald-500/15 text-emerald-400', title: 'Healthy Recipes', desc: 'Thousands of family-friendly recipes with nutrition info, ratings and cooking tips.' },
{ icon: ShoppingCart, tint: 'bg-amber-500/15 text-amber-400', title: 'Organized Grocery Lists', desc: 'Auto-generate lists from your meal plan or recipes, organized by aisle.' },
{ icon: Boxes, tint: 'bg-orange-500/15 text-orange-400', title: 'Pantry Management', desc: 'Track what you have, get alerts for expiring items and low stock.' },
{ icon: Activity, tint: 'bg-violet-500/15 text-violet-400', title: 'Nutrition Tracking', desc: 'Track calories, macros and nutrients to stay on top of your family’s health.' },
{ icon: Heart, tint: 'bg-rose-500/15 text-rose-400', title: 'Dining Out Made Easy', desc: 'Discover healthy restaurants, save favorites and track your dining-out history.' },
{ icon: Sparkles, tint: 'bg-blue-500/15 text-blue-400', id: 'smartMealPlanning' },
{ icon: BookOpen, tint: 'bg-emerald-500/15 text-emerald-400', id: 'healthyRecipes' },
{ icon: ShoppingCart, tint: 'bg-amber-500/15 text-amber-400', id: 'organizedGroceryLists' },
{ icon: Boxes, tint: 'bg-orange-500/15 text-orange-400', id: 'pantryManagement' },
{ icon: Activity, tint: 'bg-violet-500/15 text-violet-400', id: 'nutritionTracking' },
{ icon: Heart, tint: 'bg-rose-500/15 text-rose-400', id: 'diningOutMadeEasy' },
];

export default async function FoodPage() {
Expand Down Expand Up @@ -131,7 +131,7 @@ export default async function FoodPage() {
type D = { id: string; name: string; cuisine: string | null; rating: number | null };
const score = (scores ?? [])[0] as { overall: number; grade: string } | undefined;

const PAGES = ['Meal Planner', 'Recipes', 'Grocery List', 'Pantry Inventory', 'Family Favorites', 'Nutrition Tracker', 'Dining Out'];
const PAGES = ['mealPlanner', 'recipes', 'groceryList', 'pantryInventory', 'familyFavorites', 'nutritionTracker', 'diningOut'].map((k) => t(`dashboardFood.${k}`));

return (
<div className="space-y-6 pb-28">
Expand All @@ -154,37 +154,37 @@ export default async function FoodPage() {

<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-4">
{/* 1. Meal Planner */}
<FeatureCard index={1} title={t('dashboardFood.mealPlanner')} href="/dashboard/meals" icon={CalendarRange} tint="bg-blue-500/15 text-blue-400" count={planCount ?? 0} countLabel="dinners planned this week">
<FeatureCard index={1} title={t('dashboardFood.mealPlanner')} href="/dashboard/meals" icon={CalendarRange} tint="bg-blue-500/15 text-blue-400" count={planCount ?? 0} countLabel={t('dashboardFood.count.dinnersPlannedThisWeek')}>
{planRows.length === 0 ? <EmptyHint>{t('dashboardFood.noDinnersPlannedYet')}</EmptyHint>
: planRows.slice(0, 4).map((p) => <ListRow key={p.plan_date} label={p.meal_id ? (mealName.get(p.meal_id) ?? 'Planned meal') : 'Planned meal'} meta={p.plan_date === todayIso ? 'Today' : fmtDay(p.plan_date, locale.code)} dot="bg-blue-400" />)}
: planRows.slice(0, 4).map((p) => <ListRow key={p.plan_date} label={p.meal_id ? (mealName.get(p.meal_id) ?? t('dashboardFood.plannedMeal')) : t('dashboardFood.plannedMeal')} meta={p.plan_date === todayIso ? t('dashboardFood.today') : fmtDay(p.plan_date, locale.code)} dot="bg-blue-400" />)}
</FeatureCard>

{/* 2. Recipes */}
<FeatureCard index={2} title={t('dashboardFood.recipes')} href="/dashboard/recipes" icon={BookOpen} tint="bg-emerald-500/15 text-emerald-400" count={recipeCount ?? 0} countLabel="recipes">
<FeatureCard index={2} title={t('dashboardFood.recipes')} href="/dashboard/recipes" icon={BookOpen} tint="bg-emerald-500/15 text-emerald-400" count={recipeCount ?? 0} countLabel={t('dashboardFood.count.recipes')}>
{(recipes ?? []).length === 0 ? <EmptyHint>{t('dashboardFood.noRecipesYet')}</EmptyHint>
: (recipes as R[]).map((r) => <ListRow key={r.id} label={r.name} meta={r.rating ? `★ ${r.rating}` : ''} dot="bg-emerald-400" />)}
</FeatureCard>

{/* 3. Grocery List */}
<FeatureCard index={3} title={t('dashboardFood.groceryList')} href="/dashboard/grocery" icon={ShoppingCart} tint="bg-amber-500/15 text-amber-400" count={groceryCount ?? 0} countLabel="items to buy">
<FeatureCard index={3} title={t('dashboardFood.groceryList')} href="/dashboard/grocery" icon={ShoppingCart} tint="bg-amber-500/15 text-amber-400" count={groceryCount ?? 0} countLabel={t('dashboardFood.count.itemsToBuy')}>
{(grocery ?? []).length === 0 ? <EmptyHint>{t('dashboardFood.yourListIsEmpty')}</EmptyHint>
: (grocery as G[]).map((g) => <ListRow key={g.id} label={g.name} dot="bg-amber-400" />)}
</FeatureCard>

{/* 4. Pantry Inventory */}
<FeatureCard index={4} title={t('dashboardFood.pantryInventory')} href="/dashboard/pantry" icon={Boxes} tint="bg-orange-500/15 text-orange-400" count={pantryCount ?? 0} countLabel="expiring soon">
<FeatureCard index={4} title={t('dashboardFood.pantryInventory')} href="/dashboard/pantry" icon={Boxes} tint="bg-orange-500/15 text-orange-400" count={pantryCount ?? 0} countLabel={t('dashboardFood.count.expiringSoon')}>
{(pantry ?? []).length === 0 ? <EmptyHint>{t('dashboardFood.nothingExpiringSoon')}</EmptyHint>
: (pantry as P[]).map((p) => <ListRow key={p.id} label={p.name} meta={p.expires_at ? fmtDay(p.expires_at, locale.code) : `${p.quantity}${p.unit ? ' ' + p.unit : ''}`} dot="bg-orange-400" />)}
</FeatureCard>

{/* 5. Family Favorites */}
<FeatureCard index={5} title={t('dashboardFood.familyFavorites')} href="/dashboard/recipes" icon={Heart} tint="bg-rose-500/15 text-rose-400" count={favoriteCount ?? 0} countLabel="favorites">
<FeatureCard index={5} title={t('dashboardFood.familyFavorites')} href="/dashboard/recipes" icon={Heart} tint="bg-rose-500/15 text-rose-400" count={favoriteCount ?? 0} countLabel={t('dashboardFood.count.favorites')}>
{(favorites ?? []).length === 0 ? <EmptyHint>{t('dashboardFood.noFavoritesYet')}</EmptyHint>
: (favorites as R[]).map((r) => <ListRow key={r.id} label={r.name} meta={r.rating ? `★ ${r.rating}` : ''} dot="bg-rose-400" />)}
</FeatureCard>

{/* 6. Nutrition Tracker */}
<FeatureCard index={6} title={t('dashboardFood.nutritionTracker')} href="/dashboard/kitchen" icon={Activity} tint="bg-violet-500/15 text-violet-400" count={score?.overall ?? 0} countLabel="family food score">
<FeatureCard index={6} title={t('dashboardFood.nutritionTracker')} href="/dashboard/kitchen" icon={Activity} tint="bg-violet-500/15 text-violet-400" count={score?.overall ?? 0} countLabel={t('dashboardFood.count.familyFoodScore')}>
{score ? (
<div className="flex items-center gap-3">
<span className={cn('grid h-14 w-14 place-items-center rounded-full text-lg font-black',
Expand All @@ -200,7 +200,7 @@ export default async function FoodPage() {
</FeatureCard>

{/* 7. Dining Out */}
<FeatureCard index={7} title={t('dashboardFood.diningOut')} href="/dashboard/dining" icon={Utensils} tint="bg-indigo-500/15 text-indigo-400" count={diningCount ?? 0} countLabel="saved places">
<FeatureCard index={7} title={t('dashboardFood.diningOut')} href="/dashboard/dining" icon={Utensils} tint="bg-indigo-500/15 text-indigo-400" count={diningCount ?? 0} countLabel={t('dashboardFood.count.savedPlaces')}>
{(dining ?? []).length === 0 ? <EmptyHint>{t('dashboardFood.noDiningSpotsSavedYet')}</EmptyHint>
: (dining as D[]).map((d) => <ListRow key={d.id} label={d.name} meta={d.rating ? `★ ${d.rating}` : (d.cuisine ?? '')} dot="bg-indigo-400" />)}
</FeatureCard>
Expand All @@ -213,11 +213,11 @@ export default async function FoodPage() {
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
{FEATURES.map((f) => (
<div key={f.title} className="flex items-start gap-3">
<div key={f.id} className="flex items-start gap-3">
<span className={cn('grid h-9 w-9 shrink-0 place-items-center rounded-xl', f.tint)}><f.icon className="h-5 w-5" /></span>
<div>
<p className="text-sm font-semibold">{f.title}</p>
<p className="mt-0.5 text-xs leading-relaxed text-muted">{f.desc}</p>
<p className="text-sm font-semibold">{t(`dashboardFood.feature.${f.id}.title`)}</p>
<p className="mt-0.5 text-xs leading-relaxed text-muted">{t(`dashboardFood.feature.${f.id}.desc`)}</p>
</div>
</div>
))}
Expand Down
32 changes: 16 additions & 16 deletions components/dashboard/family-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export async function FamilyDashboard({ ctx }: { ctx: UserContext }) {
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
{greeting}, {ctx.active.family.name}! <span>👋</span>
{greeting} <span>👋</span>
</h1>
<p className="mt-1 text-sm text-muted">{tr('familyDashboard.heresWhatsHappeningWithYourFamily')}</p>
</div>
Expand All @@ -188,22 +188,22 @@ export async function FamilyDashboard({ ctx }: { ctx: UserContext }) {

{/* Stat cards */}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-6">
<StatCard href="/dashboard/calendar" label={tr('familyDashboard.eventsToday')} value={todayEvents?.length ?? 0} icon={Calendar} bg="bg-violet-600" linkLabel="View calendar" />
<StatCard href="/dashboard/chores" label={tr('familyDashboard.openTasks')} value={openChores ?? 0} icon={CheckCircle2} bg="bg-emerald-600" linkLabel="View tasks" />
<StatCard href="/dashboard/chores" label={tr('familyDashboard.dueToday')} value={dueTodayCount ?? 0} icon={ListChecks} bg="bg-orange-500" linkLabel="View chores" />
<StatCard href="/dashboard/settings#members" label={tr('familyDashboard.birthdaysSoon')} value={birthdayCount} icon={Cake} bg="bg-rose-500" linkLabel="View all" />
<StatCard href="/dashboard/reminders" label={tr('familyDashboard.overdueAlerts')} value={overdueReminders?.length ?? 0} icon={Bell} bg="bg-amber-500" linkLabel="View reminders" />
<StatCard href="/dashboard/messages" label={tr('familyDashboard.unreadMessages')} value={unreadMessages ?? 0} icon={MessageCircle} bg="bg-blue-600" linkLabel="Open messages" />
<StatCard href="/dashboard/calendar" label={tr('familyDashboard.eventsToday')} value={todayEvents?.length ?? 0} icon={Calendar} bg="bg-violet-600" linkLabel={tr('familyDashboard.cta.viewCalendar')} />
<StatCard href="/dashboard/chores" label={tr('familyDashboard.openTasks')} value={openChores ?? 0} icon={CheckCircle2} bg="bg-emerald-600" linkLabel={tr('familyDashboard.cta.viewTasks')} />
<StatCard href="/dashboard/chores" label={tr('familyDashboard.dueToday')} value={dueTodayCount ?? 0} icon={ListChecks} bg="bg-orange-500" linkLabel={tr('familyDashboard.cta.viewChores')} />
<StatCard href="/dashboard/settings#members" label={tr('familyDashboard.birthdaysSoon')} value={birthdayCount} icon={Cake} bg="bg-rose-500" linkLabel={tr('familyDashboard.cta.viewAll')} />
<StatCard href="/dashboard/reminders" label={tr('familyDashboard.overdueAlerts')} value={overdueReminders?.length ?? 0} icon={Bell} bg="bg-amber-500" linkLabel={tr('familyDashboard.cta.viewReminders')} />
<StatCard href="/dashboard/messages" label={tr('familyDashboard.unreadMessages')} value={unreadMessages ?? 0} icon={MessageCircle} bg="bg-blue-600" linkLabel={tr('familyDashboard.cta.openMessages')} />
</div>

{/* AI tools */}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-5">
{[
{ href: '/dashboard/inbox', label: 'Magic Import', desc: 'Paste anything → organized', icon: Wand2, bg: 'bg-violet-600' },
{ href: '/dashboard/scan', label: 'Scan Flyer', desc: 'Photo → calendar', icon: ScanLine, bg: 'bg-blue-600' },
{ href: '/dashboard/briefing', label: 'Daily Briefing', desc: "Today at a glance", icon: Sun, bg: 'bg-amber-500' },
{ href: '/dashboard/assistant', label: 'AI Assistant', desc: 'Ask anything', icon: Sparkles, bg: 'bg-emerald-600' },
{ href: '/display', label: 'Kitchen Display', desc: 'Full-screen kiosk', icon: Monitor, bg: 'bg-rose-500' },
{ href: '/dashboard/inbox', label: tr('familyDashboard.tool.magicImport.label'), desc: tr('familyDashboard.tool.magicImport.desc'), icon: Wand2, bg: 'bg-violet-600' },
{ href: '/dashboard/scan', label: tr('familyDashboard.tool.scanFlyer.label'), desc: tr('familyDashboard.tool.scanFlyer.desc'), icon: ScanLine, bg: 'bg-blue-600' },
{ href: '/dashboard/briefing', label: tr('familyDashboard.tool.dailyBriefing.label'), desc: tr('familyDashboard.tool.dailyBriefing.desc'), icon: Sun, bg: 'bg-amber-500' },
{ href: '/dashboard/assistant', label: tr('familyDashboard.tool.aiAssistant.label'), desc: tr('familyDashboard.tool.aiAssistant.desc'), icon: Sparkles, bg: 'bg-emerald-600' },
{ href: '/display', label: tr('familyDashboard.tool.kitchenDisplay.label'), desc: tr('familyDashboard.tool.kitchenDisplay.desc'), icon: Monitor, bg: 'bg-rose-500' },
].map((t) => (
<Link key={t.href} href={t.href} className="group flex items-center gap-3 rounded-2xl border border-border bg-surface/40 p-4 transition hover:bg-elevated">
<div className={cn('grid h-10 w-10 shrink-0 place-items-center rounded-xl', t.bg)}>
Expand Down Expand Up @@ -302,7 +302,7 @@ export async function FamilyDashboard({ ctx }: { ctx: UserContext }) {
</div>
</div>
<p className="mb-4 text-sm text-fg/70">
{suggestions.length > 0 ? 'Here are some suggestions for your family:' : 'Everything looks on track. Ask the assistant anything.'}
{suggestions.length > 0 ? tr('familyDashboard.suggestionsIntro') : tr('familyDashboard.allOnTrack')}
</p>
{suggestions.length > 0 && (
<div className="space-y-2.5">
Expand Down Expand Up @@ -382,9 +382,9 @@ export async function FamilyDashboard({ ctx }: { ctx: UserContext }) {
</div>
<div className="space-y-3">
{[
{ color: 'bg-violet-500', label: 'Completed', val: done },
{ color: 'bg-yellow-400', label: 'In Progress', val: Math.max(0, total - done - (openChores ?? 0)) },
{ color: 'bg-elevated', label: 'Remaining', val: openChores ?? 0 },
{ color: 'bg-violet-500', label: tr('familyDashboard.completed'), val: done },
{ color: 'bg-yellow-400', label: tr('familyDashboard.progress.inProgress'), val: Math.max(0, total - done - (openChores ?? 0)) },
{ color: 'bg-elevated', label: tr('familyDashboard.progress.remaining'), val: openChores ?? 0 },
].map(({ color, label, val }) => (
<div key={label} className="flex items-center gap-3">
<div className={cn('h-3 w-3 rounded-full', color)} />
Expand Down
14 changes: 7 additions & 7 deletions components/modules/calendar-module.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ const CATEGORY_DOT: Record<string, string> = {

// "Show" toggles mirror the image's birthday / school / holiday switches.
const SHOW_TOGGLES = [
{ key: 'birthday', label: 'Birthdays', emoji: '🎁' },
{ key: 'school', label: 'School Events', emoji: '🏫' },
{ key: 'holiday', label: 'Holidays', emoji: '🎉' },
{ key: 'birthday', labelKey: 'calendarModule.show.birthdays', emoji: '🎁' },
{ key: 'school', labelKey: 'calendarModule.show.schoolEvents', emoji: '🏫' },
{ key: 'holiday', labelKey: 'calendarModule.show.holidays', emoji: '🎉' },
] as const;

// Sentinel "calendar" for events with no assignee (shared / whole-family).
Expand Down Expand Up @@ -498,8 +498,8 @@ export function CalendarModule() {

// "Calendars" list: every member + a synthetic whole-family entry.
const calendarRows = [
...members.map((m) => ({ key: m.id, label: m.user_id === userId ? `${m.display_name} (Me)` : m.display_name, color: m.color })),
{ key: FAMILY_KEY, label: 'Family', color: null as string | null },
...members.map((m) => ({ key: m.id, label: m.user_id === userId ? tr('calendarModule.nameMe', { name: m.display_name }) : m.display_name, color: m.color })),
{ key: FAMILY_KEY, label: tr('calendarModule.family'), color: null as string | null },
];

return (
Expand Down Expand Up @@ -925,7 +925,7 @@ export function CalendarModule() {
<label key={t.key} className="flex cursor-pointer items-center gap-2 rounded-lg px-1.5 py-1.5 transition hover:bg-elevated">
<input type="checkbox" checked={visible} onChange={() => toggleCategory(t.key)}
className="h-4 w-4 shrink-0 accent-brand" />
<span className="flex-1 text-xs">{t.label}</span>
<span className="flex-1 text-xs">{tr(t.labelKey)}</span>
<span aria-hidden>{t.emoji}</span>
</label>
);
Expand Down Expand Up @@ -996,7 +996,7 @@ function NewEventModal({ existing, onClose, onSaved }: {
if (!parsed.success) { setErrors(fieldErrors(parsed.error)); return; }
// End must be after start when both are provided.
if (parsed.data.ends_at && new Date(parsed.data.ends_at) <= new Date(parsed.data.starts_at)) {
setErrors({ ends_at: 'End time must be after the start time.' });
setErrors({ ends_at: tr('calendarModule.endAfterStart') });
return;
}
setErrors({});
Expand Down
8 changes: 5 additions & 3 deletions components/modules/calm-module.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import { useTranslations } from '@/components/i18n/locale-provider';
const SOURCE_ICON: Record<CalmSource, typeof Bot> = {
agent: Bot, autopilot: Sun, operating_index: Gauge, approval: Inbox, reminder: Bell, graph: Network,
};
const SOURCE_LABEL: Record<CalmSource, string> = {
agent: 'Assistant', autopilot: 'Autopilot', operating_index: 'Operating Index', approval: 'Approval', reminder: 'Reminder', graph: 'Relationships',
const SOURCE_KEY: Record<CalmSource, string> = {
agent: 'calmModule.source.agent', autopilot: 'calmModule.source.autopilot', operating_index: 'calmModule.source.operatingIndex',
approval: 'calmModule.source.approval', reminder: 'calmModule.source.reminder', graph: 'calmModule.source.graph',
};

function Row({ item, urgent }: { item: CalmItem; urgent?: boolean }) {
const t = useTranslations();
const Icon = SOURCE_ICON[item.source];
const inner = (
<div className={cn('group flex items-center gap-3 rounded-xl border p-3 transition',
Expand All @@ -33,7 +35,7 @@ function Row({ item, urgent }: { item: CalmItem; urgent?: boolean }) {
<p className="text-sm font-medium text-fg">{item.title}</p>
{item.detail && <p className="truncate text-xs text-muted">{item.detail}</p>}
</div>
<span className="hidden shrink-0 text-[10px] uppercase tracking-wide text-muted sm:block">{SOURCE_LABEL[item.source]}</span>
<span className="hidden shrink-0 text-[10px] uppercase tracking-wide text-muted sm:block">{t(SOURCE_KEY[item.source])}</span>
{item.href && <ArrowRight className="h-4 w-4 shrink-0 text-muted opacity-0 transition group-hover:opacity-100" />}
</div>
);
Expand Down
Loading
Loading