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
40 changes: 23 additions & 17 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { motion } from "framer-motion";
import { Calendar, Package, Sparkles } from "lucide-react";
import { AuthResponse, clearToken, getAuthConfig, getMe, storeToken } from "./api/client";
import AccountMenu from "./components/AccountMenu";
import LanguageSwitcher from "./components/LanguageSwitcher";
import { AboutModal, ChangePasswordModal, DeleteAccountModal, FeedbackModal, InviteHouseholdModal } from "./components/modals";
import { useRecommendationsQuery } from "./hooks/queries";
import { useI18n } from "./i18n";
import { topRecommendation } from "./lib/recommendation";
import DashboardPage from "./pages/Dashboard";
import MealPlannerPage from "./pages/MealPlanner";
Expand All @@ -14,6 +16,7 @@ import { AuthUser } from "./types";

export default function App() {
const queryClient = useQueryClient();
const { t } = useI18n();
const [authUser, setAuthUser] = useState<AuthUser | null>(null);
const [authChecked, setAuthChecked] = useState(false);
const [page, setPage] = useState<"home" | "planner">("home");
Expand Down Expand Up @@ -99,7 +102,7 @@ export default function App() {
if (!authChecked) {
return (
<div className="flex min-h-screen items-center justify-center text-slate-500 dark:text-slate-400">
Loading...
{t("common.loading")}
</div>
);
}
Expand Down Expand Up @@ -140,36 +143,39 @@ export default function App() {
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[0.2em] text-brand-600 dark:text-brand-100">ONS Inventory</p>
<h1 className="mt-2 text-3xl font-semibold text-slate-900 dark:text-white">Welcome, {authUser.firstName}!</h1>
<h1 className="mt-2 text-3xl font-semibold text-slate-900 dark:text-white">{t("header.welcome", { name: authUser.firstName })}</h1>
<p className="mt-2 text-slate-600 dark:text-slate-300">
Track pantry, fridge, and freezer stock and get recipe ideas before ingredients expire.
{t("header.tagline")}
</p>
{authUser.householdName && (
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">{authUser.householdName}</p>
)}
</div>
<div className="flex flex-col items-end gap-3">
<AccountMenu
dark={dark}
onToggleDark={() => setDark((d) => !d)}
onInvite={() => setShowInviteHousehold(true)}
onChangePassword={() => setShowChangePassword(true)}
onFeedback={() => setShowFeedback(true)}
onAbout={() => setShowAbout(true)}
onLogout={handleLogout}
onDeleteAccount={() => setShowDeleteAccount(true)}
/>
<div className="flex items-center gap-2">
<LanguageSwitcher />
<AccountMenu
dark={dark}
onToggleDark={() => setDark((d) => !d)}
onInvite={() => setShowInviteHousehold(true)}
onChangePassword={() => setShowChangePassword(true)}
onFeedback={() => setShowFeedback(true)}
onAbout={() => setShowAbout(true)}
onLogout={handleLogout}
onDeleteAccount={() => setShowDeleteAccount(true)}
/>
</div>
<div className="rounded-xl bg-brand-100 dark:bg-brand-600/20 px-4 py-3 text-sm text-brand-800 dark:text-brand-50">
{topMatch ? (
<>
<div className="font-medium">Top suggestion</div>
<div className="font-medium">{t("header.topSuggestion")}</div>
<div className="mt-1 flex items-center gap-2">
<Sparkles className="h-4 w-4" />
{topMatch.recipe.name} ({topMatch.matchPercentage}%)
</div>
</>
) : (
<div className="font-medium">Add inventory to unlock recommendations</div>
<div className="font-medium">{t("header.noSuggestion")}</div>
)}
</div>
</div>
Expand All @@ -185,7 +191,7 @@ export default function App() {
}`}
>
<Package className="h-4 w-4" />
Inventory
{t("nav.inventory")}
</button>
<button
type="button"
Expand All @@ -195,7 +201,7 @@ export default function App() {
}`}
>
<Calendar className="h-4 w-4" />
Meal Planner
{t("nav.mealPlanner")}
</button>
</nav>

Expand Down
26 changes: 14 additions & 12 deletions frontend/src/components/AccountMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useEffect, useRef, useState, type ReactNode } from "react";
import { CircleUser, Info, KeyRound, LogOut, Menu, MessageSquare, Moon, Sun, Trash2, UserPlus, X } from "lucide-react";
import { useI18n } from "../i18n";

const itemClass =
export const itemClass =
"flex w-full items-center gap-2 px-4 py-2.5 text-left text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800";

function Dropdown({
export function Dropdown({
label,
icon,
openIcon,
Expand Down Expand Up @@ -41,7 +42,7 @@ function Dropdown({
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-label={open ? `Close ${label}` : `Open ${label}`}
aria-label={label}
aria-expanded={open}
className="inline-flex items-center rounded-lg border border-slate-300 dark:border-slate-700 p-2 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800"
>
Expand Down Expand Up @@ -75,19 +76,20 @@ export default function AccountMenu({
onLogout: () => void;
onDeleteAccount: () => void;
}) {
const { t } = useI18n();
const select = (close: () => void, action: () => void) => () => {
close();
action();
};

return (
<div className="flex items-center gap-2">
<Dropdown label="profile menu" icon={<CircleUser className="h-5 w-5" />}>
<Dropdown label={t("menu.profile")} icon={<CircleUser className="h-5 w-5" />}>
{(close) => (
<>
<button type="button" onClick={select(close, onChangePassword)} className={itemClass}>
<KeyRound className="h-3.5 w-3.5" />
Change password
{t("menu.changePassword")}
</button>
<div className="border-t border-slate-200 dark:border-slate-700" />
<button
Expand All @@ -96,33 +98,33 @@ export default function AccountMenu({
className="flex w-full items-center gap-2 px-4 py-2.5 text-left text-sm text-rose-600 dark:text-rose-300 hover:bg-rose-50 dark:hover:bg-rose-950/30"
>
<Trash2 className="h-3.5 w-3.5" />
Delete account
{t("menu.deleteAccount")}
</button>
</>
)}
</Dropdown>
<Dropdown label="menu" icon={<Menu className="h-5 w-5" />} openIcon={<X className="h-5 w-5" />}>
<Dropdown label={t("menu.menu")} icon={<Menu className="h-5 w-5" />} openIcon={<X className="h-5 w-5" />}>
{(close) => (
<>
<button type="button" onClick={select(close, onToggleDark)} className={itemClass}>
{dark ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
{dark ? "Light mode" : "Dark mode"}
{dark ? t("menu.lightMode") : t("menu.darkMode")}
</button>
<button type="button" onClick={select(close, onInvite)} className={itemClass}>
<UserPlus className="h-3.5 w-3.5" />
Invite to household
{t("menu.invite")}
</button>
<button type="button" onClick={select(close, onFeedback)} className={itemClass}>
<MessageSquare className="h-3.5 w-3.5" />
Send feedback
{t("menu.feedback")}
</button>
<button type="button" onClick={select(close, onAbout)} className={itemClass}>
<Info className="h-3.5 w-3.5" />
About
{t("menu.about")}
</button>
<button type="button" onClick={select(close, onLogout)} className={itemClass}>
<LogOut className="h-3.5 w-3.5" />
Logout
{t("menu.logout")}
</button>
</>
)}
Expand Down
Loading
Loading