+
+ {t("product")}
+
+
-
+
{t("product-name")}
@@ -199,19 +202,12 @@ export const ViewProduct = (props: ViewProductProps) => {
size="lg"
variant="outlined"
className="rounded-2xl"
- sx={{
- bgcolor: "var(--joy-palette-background-surface)",
- boxShadow: "0 10px 24px var(--joy-palette-divider)",
- }}
/>
)}
-
+
{t("description")}
@@ -224,44 +220,13 @@ export const ViewProduct = (props: ViewProductProps) => {
size="lg"
variant="outlined"
className="rounded-2xl"
- sx={{
- bgcolor: "var(--joy-palette-background-surface)",
- boxShadow: "0 10px 24px var(--joy-palette-divider)",
- }}
/>
)}
-
-
-
-
- {t("inventory")}
+
+
+
+
+ {t("inventory")}
+
+
+
+
+ {t("amount")}
-
-
-
-
+ {(field) => (
+
+ )}
+
+
+
+
+ {t("price")}
+
+
+ {(field) => (
+ field.handleChange(e.target.value)}
+ onBlur={field.handleBlur}
+ size="lg"
+ variant="outlined"
+ className="rounded-2xl"
+ endDecorator={
+
+ {Cookies.get("currency")}
+
+ }
+ />
+ )}
+
+
+
+
+ {t("storage-place")}
+
+
+ {(field) => (
+
-
-
- {t("price")}
-
-
- {(field) => (
- field.handleChange(e.target.value)}
- onBlur={field.handleBlur}
- size="lg"
- variant="outlined"
- className="rounded-2xl"
- sx={{
- bgcolor: "var(--joy-palette-background-surface)",
- }}
- />
- )}
-
-
- {Cookies.get("currency")}
-
-
-
-
- {t("storage-place")}
-
-
- {(field) => (
-
- )}
-
-
-
+ {storages?.map((storage) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+ {t("jar-label")}
+
+
+ {t("jar-label-hint")}
+
-
-
-
- {t("product-details")}
-
-
-
}
- loading={isPending}
- onClick={() => downloadQRcode()}
- size="lg"
- color="primary"
- variant="solid"
- className="rounded-2xl transition hover:-translate-y-0.5"
- sx={{
- boxShadow:
- "0 16px 36px color-mix(in srgb, var(--joy-palette-primary-solidBg) 35%, transparent)",
- }}
- >
- {t("download-qr-code")}
-
-
-
-
+
- {alert.isAlert && (
-
- )}
-
-
+
+ }
+ onClick={() => downloadQRcode()}
+ size="lg"
+ variant="outlined"
+ color="neutral"
+ className="rounded-2xl"
+ >
+ {t("download-qr-code")}
+
+
+
+
+ {alert.isAlert && (
+
+ )}
+
)}
- >
+
);
};
diff --git a/frontend/src/routes/app/_hiddenLayout.tsx b/frontend/src/routes/app/_hiddenLayout.tsx
index 20d04e5..5a8eb2b 100644
--- a/frontend/src/routes/app/_hiddenLayout.tsx
+++ b/frontend/src/routes/app/_hiddenLayout.tsx
@@ -1,4 +1,4 @@
-import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router";
import { Sidebar } from "../../components/Sidebar";
export const Route = createFileRoute("/app/_hiddenLayout")({
@@ -6,6 +6,8 @@ export const Route = createFileRoute("/app/_hiddenLayout")({
});
function AppLayout() {
+ const pathname = useRouterState({ select: (state) => state.location.pathname });
+
return (
);
diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts
index 571704d..74af22a 100644
--- a/frontend/src/theme.ts
+++ b/frontend/src/theme.ts
@@ -1,6 +1,6 @@
import { extendTheme } from "@mui/joy/styles";
-// I generated this theme with AI (Claude Opus 4.6)
+// Palette tokens sourced from the v0.9 design mockups (canvas/surface/ink/brand/etc.)
export const theme = extendTheme({
cssVarPrefix: "joy",
@@ -8,23 +8,23 @@ export const theme = extendTheme({
light: {
palette: {
primary: {
- 50: "#eef7ff",
- 100: "#dcedff",
- 200: "#b3d9ff",
- 300: "#7fbdff",
- 400: "#469cf5",
- 500: "#0b6bcb",
- 600: "#0958a8",
- 700: "#08478a",
- 800: "#0a3a6e",
- 900: "#0c2f59",
- solidBg: "#0b6bcb",
- solidHoverBg: "#095aa7",
- solidActiveBg: "#08478a",
- outlinedBorder: "#7fbdff",
- outlinedColor: "#08478a",
- softBg: "#dcedff", // Color in sidebar for selected view
- softColor: "#08478a",
+ 50: "#eef3fe",
+ 100: "#dbe6fc",
+ 200: "#b0c8f8",
+ 300: "#7fa3f0",
+ 400: "#4c7de0",
+ 500: "#2159c9",
+ 600: "#1c48a3",
+ 700: "#163a82",
+ 800: "#122e68",
+ 900: "#0e2453",
+ solidBg: "#2159c9",
+ solidHoverBg: "#1c48a3",
+ solidActiveBg: "#163a82",
+ outlinedBorder: "#7fa3f0",
+ outlinedColor: "#163a82",
+ softBg: "#eef3fe", // Color in sidebar for selected view
+ softColor: "#163a82",
},
success: {
50: "#ecfdf5",
@@ -39,88 +39,100 @@ export const theme = extendTheme({
900: "#0a423f",
solidBg: "#0d9488",
solidHoverBg: "#0b7c73",
+ outlinedBorder: "#a6f4dc",
+ outlinedColor: "#0b7c73",
softBg: "#d1faee",
softColor: "#0a655e",
},
warning: {
- 50: "#fffaeb",
- 100: "#fef0c7",
- 200: "#fedf89",
- 300: "#fdc74a",
- 400: "#fbaf24",
- 500: "#f59e0b",
- 600: "#d67e06",
- 700: "#b25f08",
- 800: "#8f4a0e",
- 900: "#763d10",
- solidBg: "#f59e0b",
- solidHoverBg: "#d67e06",
- softBg: "#fef0c7",
- softColor: "#8f4a0e",
+ 50: "#fdf1e0",
+ 100: "#fbe3c1",
+ 200: "#f6c98a",
+ 300: "#eeab54",
+ 400: "#e2932c",
+ 500: "#d98014",
+ 600: "#b46810",
+ 700: "#92530f",
+ 800: "#734111",
+ 900: "#5c350f",
+ solidBg: "#d98014",
+ solidHoverBg: "#b46810",
+ outlinedBorder: "#f6c98a",
+ outlinedColor: "#b46810",
+ softBg: "#fdf1e0",
+ softColor: "#734111",
},
danger: {
- 50: "#fff1f2",
- 100: "#ffe1e4",
- 200: "#ffc7cd",
- 300: "#ff9ba7",
- 400: "#fb6b7d",
- 500: "#e11d48",
- 600: "#be123c",
- 700: "#9b1038",
- 800: "#7f1230",
- 900: "#6a122b",
- solidBg: "#e11d48",
- solidHoverBg: "#be123c",
- softBg: "#ffe1e4",
- softColor: "#9b1038",
+ 50: "#fbeae9",
+ 100: "#f6d2cf",
+ 200: "#eaa39c",
+ 300: "#dc746a",
+ 400: "#cd4f42",
+ 500: "#c4342c",
+ 600: "#a32a24",
+ 700: "#84221f",
+ 800: "#691c1b",
+ 900: "#551818",
+ solidBg: "#c4342c",
+ solidHoverBg: "#a32a24",
+ outlinedBorder: "#eaa39c",
+ outlinedColor: "#a32a24",
+ softBg: "#fbeae9",
+ softColor: "#84221f",
},
neutral: {
- 50: "#f8fafc",
- 100: "#f1f5f9",
- 200: "#e2e8f0",
- 300: "#cbd5e1",
- 400: "#94a3b8",
- 500: "#64748b",
- 600: "#475569",
- 700: "#334155",
- 800: "#1e293b",
- 900: "#0f172a",
+ 50: "#f6f7f9",
+ 100: "#f2f4f6",
+ 200: "#e4e7ec",
+ 300: "#dfe3e8",
+ 400: "#7b8494",
+ 500: "#667283",
+ 600: "#4a5364",
+ 700: "#39414f",
+ 800: "#232833",
+ 900: "#10151f",
+ plainColor: "#4a5364",
+ outlinedColor: "#4a5364",
+ outlinedBorder: "#e4e7ec",
+ outlinedHoverBg: "#fbfcfd",
+ softColor: "#4a5364",
+ softBg: "#f2f4f6",
},
background: {
- body: "#f7f9fc",
+ body: "#f6f7f9",
surface: "#ffffff",
- level1: "#f1f5f9",
- level2: "#e2e8f0",
- level3: "#cbd5e1",
+ level1: "#fbfcfd",
+ level2: "#f2f4f6",
+ level3: "#e4e7ec",
},
text: {
- primary: "#0f172a",
- secondary: "#475569",
- tertiary: "#64748b",
+ primary: "#10151f",
+ secondary: "#4a5364",
+ tertiary: "#667283",
},
- divider: "rgba(15, 23, 42, 0.08)",
+ divider: "#e4e7ec",
},
},
dark: {
palette: {
primary: {
- 50: "#0c2f59",
- 100: "#0a3a6e",
- 200: "#08478a",
- 300: "#0958a8",
- 400: "#0b6bcb",
- 500: "#3b8ce0",
- 600: "#6ba9ea",
- 700: "#9cc6f2",
- 800: "#c7defa",
- 900: "#eef7ff",
- solidBg: "#3b8ce0",
- solidHoverBg: "#6ba9ea",
- solidActiveBg: "#0958a8",
- outlinedBorder: "#0958a8",
- outlinedColor: "#9cc6f2",
- softBg: "rgba(59, 140, 224, 0.16)", // Color in sidebar for selected view
- softColor: "#9cc6f2",
+ 50: "#0e2453",
+ 100: "#122e68",
+ 200: "#163a82",
+ 300: "#1c48a3",
+ 400: "#2159c9",
+ 500: "#5b8def",
+ 600: "#82a8f2",
+ 700: "#a9c3f6",
+ 800: "#cddcfa",
+ 900: "#eef3fe",
+ solidBg: "#5b8def",
+ solidHoverBg: "#82a8f2",
+ solidActiveBg: "#1c48a3",
+ outlinedBorder: "#1c48a3",
+ outlinedColor: "#a9c3f6",
+ softBg: "#16233d", // Color in sidebar for selected view
+ softColor: "#a9c3f6",
},
success: {
50: "#0a423f",
@@ -135,72 +147,85 @@ export const theme = extendTheme({
900: "#ecfdf5",
solidBg: "#0d9488",
solidHoverBg: "#2dc8b1",
+ outlinedBorder: "#0a655e",
+ outlinedColor: "#6ee7c9",
softBg: "rgba(13, 148, 136, 0.18)",
softColor: "#6ee7c9",
},
warning: {
- 50: "#763d10",
- 100: "#8f4a0e",
- 200: "#b25f08",
- 300: "#d67e06",
- 400: "#f59e0b",
- 500: "#fbaf24",
- 600: "#fdc74a",
- 700: "#fedf89",
- 800: "#fef0c7",
- 900: "#fffaeb",
- solidBg: "#f59e0b",
- solidHoverBg: "#fbaf24",
- softBg: "rgba(245, 158, 11, 0.16)",
- softColor: "#fdc74a",
+ 50: "#5c350f",
+ 100: "#734111",
+ 200: "#92530f",
+ 300: "#b46810",
+ 400: "#d98014",
+ 500: "#e3a054",
+ 600: "#edbb82",
+ 700: "#f4d3ac",
+ 800: "#f9e6d3",
+ 900: "#fdf1e0",
+ solidBg: "#e3a054",
+ solidHoverBg: "#edbb82",
+ outlinedBorder: "#92530f",
+ outlinedColor: "#edbb82",
+ softBg: "rgba(227, 160, 84, 0.16)",
+ softColor: "#f4d3ac",
},
danger: {
- 50: "#6a122b",
- 100: "#7f1230",
- 200: "#9b1038",
- 300: "#be123c",
- 400: "#e11d48",
- 500: "#f04463",
- 600: "#fb6b7d",
- 700: "#ff9ba7",
- 800: "#ffc7cd",
- 900: "#fff1f2",
- solidBg: "#e11d48",
- solidHoverBg: "#f04463",
- softBg: "rgba(225, 29, 72, 0.18)",
- softColor: "#fb6b7d",
+ 50: "#551818",
+ 100: "#691c1b",
+ 200: "#84221f",
+ 300: "#a32a24",
+ 400: "#c4342c",
+ 500: "#f07168",
+ 600: "#f4948d",
+ 700: "#f8b7b2",
+ 800: "#fbd6d3",
+ 900: "#fdeceb",
+ solidBg: "#f07168",
+ solidHoverBg: "#f4948d",
+ outlinedBorder: "#84221f",
+ outlinedColor: "#f4948d",
+ softBg: "rgba(240, 113, 104, 0.16)",
+ softColor: "#f8b7b2",
},
neutral: {
- 50: "#0b1220",
- 100: "#0f172a",
- 200: "#1e293b",
- 300: "#334155",
- 400: "#475569",
- 500: "#64748b",
- 600: "#94a3b8",
- 700: "#cbd5e1",
- 800: "#e2e8f0",
- 900: "#f1f5f9",
+ 50: "#0e1116",
+ 100: "#161b22",
+ 200: "#1b212a",
+ 300: "#20262f",
+ 400: "#272e39",
+ 500: "#8590a0",
+ 600: "#aab4c2",
+ 700: "#c7cfd9",
+ 800: "#e8ecf2",
+ 900: "#f6f8fa",
+ plainColor: "#aab4c2",
+ outlinedColor: "#aab4c2",
+ outlinedBorder: "#272e39",
+ outlinedHoverBg: "#1b212a",
+ softColor: "#aab4c2",
+ softBg: "#20262f",
},
background: {
- body: "#0b1220",
- surface: "#111a2e",
- level1: "#16213a",
- level2: "#1e293b",
- level3: "#334155",
+ body: "#0e1116",
+ surface: "#161b22",
+ level1: "#1b212a",
+ level2: "#20262f",
+ level3: "#272e39",
},
text: {
- primary: "#f1f5f9",
- secondary: "#cbd5e1",
- tertiary: "#94a3b8",
+ primary: "#e8ecf2",
+ secondary: "#aab4c2",
+ tertiary: "#8590a0",
},
- divider: "rgba(226, 232, 240, 0.08)",
+ divider: "#272e39",
},
},
},
fontFamily: {
- body: "'Inter', 'Segoe UI', system-ui, sans-serif",
- display: "'Inter', 'Segoe UI', system-ui, sans-serif",
+ body: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
+ display: "'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif",
+ code: "'IBM Plex Mono', ui-monospace, monospace",
},
radius: {
xs: "6px",
@@ -209,6 +234,45 @@ export const theme = extendTheme({
lg: "20px",
xl: "28px",
},
+ typography: {
+ h1: {
+ fontSize: "30px",
+ fontWeight: 700,
+ letterSpacing: "-0.03em",
+ lineHeight: 1.2,
+ },
+ h2: {
+ fontSize: "22px",
+ fontWeight: 700,
+ letterSpacing: "-0.02em",
+ lineHeight: 1.25,
+ },
+ "title-lg": {
+ fontSize: "15px",
+ fontWeight: 600,
+ lineHeight: 1.4,
+ },
+ "title-md": {
+ fontSize: "13px",
+ fontWeight: 600,
+ lineHeight: 1.4,
+ },
+ "body-lg": {
+ fontSize: "13.5px",
+ fontWeight: 400,
+ lineHeight: 1.5,
+ },
+ "body-md": {
+ fontSize: "13.5px",
+ fontWeight: 400,
+ lineHeight: 1.5,
+ },
+ "body-sm": {
+ fontSize: "11.5px",
+ fontWeight: 600,
+ lineHeight: 1.4,
+ },
+ },
components: {
JoyInput: {
styleOverrides: {
diff --git a/frontend/src/utils/i18n/locales/de/de.json b/frontend/src/utils/i18n/locales/de/de.json
index 4e2dbb7..76abdff 100644
--- a/frontend/src/utils/i18n/locales/de/de.json
+++ b/frontend/src/utils/i18n/locales/de/de.json
@@ -86,5 +86,48 @@
"EP005": "Das Produkt konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut.",
"EP006": "Das Produkt konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
"EP007": "Bitte geben Sie mindestens einen Produktnamen ein.",
- "EP008": "Einige erforderliche Produktinformationen fehlen. Bitte überprüfen Sie Ihre Eingabe."
+ "EP008": "Einige erforderliche Produktinformationen fehlen. Bitte überprüfen Sie Ihre Eingabe.",
+ "stat-expired": "Abgelaufen",
+ "stat-expiring-soon": "Läuft in 90 Tagen ab",
+ "stat-low-stock": "Niedriger Bestand",
+ "stat-units-stored": "Gelagerte Einheiten",
+ "days-since-expired": "Vor {{count}} Tagen abgelaufen",
+ "days-until-expiry": "in {{count}} Tagen",
+ "expires-in-days-badge": "Läuft in {{count}} Tagen ab",
+ "general": "Allgemein",
+ "appearance-language": "Darstellung & Sprache",
+ "theme": "Design",
+ "theme-light": "Hell",
+ "theme-dark": "Dunkel",
+ "theme-system": "System",
+ "language": "Sprache",
+ "account": "Konto",
+ "sign-out": "Abmelden",
+ "sign-out-sub": "Beendet diese Sitzung auf diesem Gerät.",
+ "login-tagline-1": "Alles, was du verstaut hast, im Januar noch auffindbar.",
+ "login-tagline-2": "Gefrierfach, Vorrat, Keller. Eine Liste, ein Ablaufdatum pro Glas.",
+ "storages-sub": "{{count}} Orte, {{items}} Artikel gelagert.",
+ "new-storage": "Neuer Lagerort",
+ "product": "Produkt",
+ "product-name-placeholder": "z. B. Tomatensauce",
+ "description-placeholder": "Kurze Notiz zu dieser Charge",
+ "choose-storage": "Lagerort wählen",
+ "amount-price-optional-hint": "Menge und Preis sind optional. Der Lagerort bestimmt, wo der Artikel im Inventar erscheint.",
+ "all-fields-editable-later": "Alle Felder können später bearbeitet werden",
+ "save-product": "Produkt speichern",
+ "jar-label": "Glas-Etikett",
+ "jar-label-hint": "Ausdrucken und auf den Deckel kleben. Scannen öffnet die Schnellansicht.",
+ "scanned-badge": "Gescannt",
+ "opened-from-jar-label": "Über das Glas-Etikett geöffnet",
+ "amount-in-storage": "Menge im Lager",
+ "set-to-zero-hint": "Auf 0 setzen, um den Eintrag zu behalten, aber das Fach zu leeren.",
+ "open-full-details": "Vollständige Produktdetails öffnen",
+ "inventory-summary": "{{count}} Artikel in {{storages}} Lagerorten",
+ "search-products": "Produkte durchsuchen",
+ "filter-all": "Alle",
+ "filter-expired": "Abgelaufen",
+ "filter-soon": "Bald",
+ "filter-low": "Niedrig",
+ "products": "Produkte",
+ "units": "Einheiten"
}
diff --git a/frontend/src/utils/i18n/locales/en/en.json b/frontend/src/utils/i18n/locales/en/en.json
index 8abd620..8327d6f 100644
--- a/frontend/src/utils/i18n/locales/en/en.json
+++ b/frontend/src/utils/i18n/locales/en/en.json
@@ -86,5 +86,48 @@
"EP005": "The product could not be updated. Please try again.",
"EP006": "The product could not be deleted. Please try again.",
"EP007": "Please enter at least a product name.",
- "EP008": "Some required product information is missing. Please check your input."
+ "EP008": "Some required product information is missing. Please check your input.",
+ "stat-expired": "Expired",
+ "stat-expiring-soon": "Expiring in 90 days",
+ "stat-low-stock": "Low stock",
+ "stat-units-stored": "Units stored",
+ "days-since-expired": "Expired {{count}} days ago",
+ "days-until-expiry": "in {{count}} days",
+ "expires-in-days-badge": "Expires in {{count}} days",
+ "general": "General",
+ "appearance-language": "Appearance & language",
+ "theme": "Theme",
+ "theme-light": "Light",
+ "theme-dark": "Dark",
+ "theme-system": "System",
+ "language": "Language",
+ "account": "Account",
+ "sign-out": "Sign out",
+ "sign-out-sub": "Ends this session on this device.",
+ "login-tagline-1": "Everything you put away, still findable in January.",
+ "login-tagline-2": "Freezer, pantry, cellar. One list, one expiry date per jar.",
+ "storages-sub": "{{count}} places, {{items}} items stored.",
+ "new-storage": "New storage",
+ "product": "Product",
+ "product-name-placeholder": "e.g. Tomato Sauce",
+ "description-placeholder": "Short note about this batch",
+ "choose-storage": "Choose a storage",
+ "amount-price-optional-hint": "Amount and price are optional. Storage place decides where the item shows up in the inventory list.",
+ "all-fields-editable-later": "All fields can be edited later",
+ "save-product": "Save product",
+ "jar-label": "Jar label",
+ "jar-label-hint": "Print and stick it on the lid. Scanning opens fast details.",
+ "scanned-badge": "Scanned",
+ "opened-from-jar-label": "Opened from the jar label",
+ "amount-in-storage": "Amount in storage",
+ "set-to-zero-hint": "Set to 0 to keep the record but empty the shelf.",
+ "open-full-details": "Open full product details",
+ "inventory-summary": "{{count}} items across {{storages}} storages",
+ "search-products": "Search products",
+ "filter-all": "All",
+ "filter-expired": "Expired",
+ "filter-soon": "Soon",
+ "filter-low": "Low",
+ "products": "products",
+ "units": "units"
}
diff --git a/frontend/src/utils/productStatus.ts b/frontend/src/utils/productStatus.ts
new file mode 100644
index 0000000..bee4ceb
--- /dev/null
+++ b/frontend/src/utils/productStatus.ts
@@ -0,0 +1,53 @@
+const EXPIRING_SOON_DAYS = 90;
+const LOW_STOCK_MAX = 2;
+
+const daysUntil = (value?: string | null) => {
+ if (!value) {
+ return null;
+ }
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return null;
+ }
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ date.setHours(0, 0, 0, 0);
+ return Math.round((date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
+};
+
+export const isExpired = (expiryDate?: string | null) => {
+ const days = daysUntil(expiryDate);
+ return days !== null && days < 0;
+};
+
+export const isExpiringSoon = (expiryDate?: string | null) => {
+ const days = daysUntil(expiryDate);
+ return days !== null && days >= 0 && days <= EXPIRING_SOON_DAYS;
+};
+
+export const isLowStock = (amount: number) => amount > 0 && amount <= LOW_STOCK_MAX;
+
+export type ProductStatus = "expired" | "expiring-soon" | "ok";
+
+export const getProductStatus = (expiryDate?: string | null): ProductStatus => {
+ if (isExpired(expiryDate)) {
+ return "expired";
+ }
+ if (isExpiringSoon(expiryDate)) {
+ return "expiring-soon";
+ }
+ return "ok";
+};
+
+// Days remaining until expiry (negative = days since it expired), plus the
+// derived status. Components format this into copy via i18next (t()) so the
+// wording stays translatable.
+export const getExpiryDays = (
+ expiryDate?: string | null,
+): { days: number; status: ProductStatus } | null => {
+ const days = daysUntil(expiryDate);
+ if (days === null) {
+ return null;
+ }
+ return { days, status: getProductStatus(expiryDate) };
+};
diff --git a/package-lock.json b/package-lock.json
index 45eaf48..9ed1b4a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,8 @@
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"concurrently": "^10.0.3",
- "globals": "^17.7.0"
+ "globals": "^17.7.0",
+ "prettier": "^3.9.6"
}
},
"backend": {
@@ -45,8 +46,8 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
- "@fontsource/inter": "^5.2.8",
- "@mui/icons-material": "^9.0.1",
+ "@fontsource/ibm-plex-mono": "^5.3.0",
+ "@fontsource/ibm-plex-sans": "^5.3.0",
"@mui/joy": "^5.0.0-beta.52",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-form": "^1.32.0",
@@ -1103,10 +1104,19 @@
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
- "node_modules/@fontsource/inter": {
- "version": "5.2.8",
- "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
- "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==",
+ "node_modules/@fontsource/ibm-plex-mono": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",
+ "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource/ibm-plex-sans": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz",
+ "integrity": "sha512-CbE4CbbEEZJX860XyUiRpsksXIQR8Rp2XDva2VO53NJox9tVNtusrysd2x5YkUEY3ErQ66W1IiiQL8/wihhw5w==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
@@ -1266,32 +1276,6 @@
"url": "https://opencollective.com/mui-org"
}
},
- "node_modules/@mui/icons-material": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.2.0.tgz",
- "integrity": "sha512-VgBd3z7Qc3vd/thcNSMC03nHRh/U4DzMUd+1dRyJTbm/hGo7+N6N4GDuJZDNHa6LZhhwG6Cu1X3DNvrVv8sNag==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.29.2"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@mui/material": "^9.2.0",
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@mui/joy": {
"version": "5.0.0-beta.52",
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.52.tgz",
@@ -1333,220 +1317,6 @@
}
}
},
- "node_modules/@mui/material": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.2.0.tgz",
- "integrity": "sha512-+YTRSgGKGrrRo2XJZXs7JRA6qHoHWvNtxyqxnrRJTBmIuLOUpxxh7m4G9lF4tWberxGFY+EqkkRPgJCl+fSMJg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.29.2",
- "@mui/core-downloads-tracker": "^9.2.0",
- "@mui/system": "^9.2.0",
- "@mui/types": "^9.1.1",
- "@mui/utils": "^9.2.0",
- "@popperjs/core": "^2.11.8",
- "@types/react-transition-group": "^4.4.12",
- "clsx": "^2.1.1",
- "csstype": "^3.2.3",
- "prop-types": "^15.8.1",
- "react-is": "^19.2.6",
- "react-transition-group": "^4.4.5"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@emotion/react": "^11.5.0",
- "@emotion/styled": "^11.3.0",
- "@mui/material-pigment-css": "^9.2.0",
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/react": {
- "optional": true
- },
- "@emotion/styled": {
- "optional": true
- },
- "@mui/material-pigment-css": {
- "optional": true
- },
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/material/node_modules/@mui/core-downloads-tracker": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.2.0.tgz",
- "integrity": "sha512-+XMav+ZaXkZKUFUgzjrfMEedfyJKxxviAske2q8N8CWDMeqZdDU2lWMkiUPiB388hGaDqhwvOAwkrsc/pUyp8g==",
- "license": "MIT",
- "peer": true,
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- }
- },
- "node_modules/@mui/material/node_modules/@mui/private-theming": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.2.0.tgz",
- "integrity": "sha512-w9wpyDxGPGnAACPB2hKhCDmILJIAvQxrfjUbIAEa0AznX1rOjaz5N+yB1uuw8ixnJcpEh/tPbD9oEe19wcWPHw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.29.2",
- "@mui/utils": "^9.2.0",
- "prop-types": "^15.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/material/node_modules/@mui/styled-engine": {
- "version": "9.1.1",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.1.1.tgz",
- "integrity": "sha512-neaYKdJfvEG54q8efHLJR7swpHG/gfSv9xGqW5iTSMsubD7yPCPFrhVBt284j1DOF3uZaaDJSHQL7gz6jGF21Q==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.29.2",
- "@emotion/cache": "^11.14.0",
- "@emotion/serialize": "^1.3.3",
- "@emotion/sheet": "^1.4.0",
- "csstype": "^3.2.3",
- "prop-types": "^15.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@emotion/react": "^11.4.1",
- "@emotion/styled": "^11.3.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/react": {
- "optional": true
- },
- "@emotion/styled": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/material/node_modules/@mui/system": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.2.0.tgz",
- "integrity": "sha512-YvUJwKoGVtbnOm2PyPi5TvX2d1rOA6sqSpEWVs4WmXNIaFTuYmNUaVdU2o1NKUEe31URnD3E8ZVUMcsLQXwcYg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.29.2",
- "@mui/private-theming": "^9.2.0",
- "@mui/styled-engine": "^9.1.1",
- "@mui/types": "^9.1.1",
- "@mui/utils": "^9.2.0",
- "clsx": "^2.1.1",
- "csstype": "^3.2.3",
- "prop-types": "^15.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@emotion/react": "^11.5.0",
- "@emotion/styled": "^11.3.0",
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/react": {
- "optional": true
- },
- "@emotion/styled": {
- "optional": true
- },
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/material/node_modules/@mui/types": {
- "version": "9.1.1",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.1.1.tgz",
- "integrity": "sha512-Zjt7u8wNvDg40rPTGoL+TnfkpuSKjwubsNSFRH1KAVZLcaV4I3AFNHIFbvH7p4F3alEibSbdd90xAgn5Rnfndg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.29.2"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/@mui/material/node_modules/@mui/utils": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.2.0.tgz",
- "integrity": "sha512-OsUH5zhlSOM4xmLl53+agug1M1UyWb4zxFxWQCqwKTKUeQPvTENtg3JhrroBD2qpCLKsX5W/DYGERJ4mBUbc8g==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.29.2",
- "@mui/types": "^9.1.1",
- "@types/prop-types": "^15.7.15",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "react-is": "^19.2.6"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@mui/private-theming": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz",
@@ -2830,6 +2600,7 @@
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -2845,16 +2616,6 @@
"@types/react": "^19.2.0"
}
},
- "node_modules/@types/react-transition-group": {
- "version": "4.4.12",
- "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
- "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
- "license": "MIT",
- "peer": true,
- "peerDependencies": {
- "@types/react": "*"
- }
- },
"node_modules/@types/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
@@ -3510,16 +3271,16 @@
}
},
"node_modules/brace-expansion": {
- "version": "5.0.7",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
- "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/browserslist": {
@@ -3705,15 +3466,15 @@
"license": "MIT"
},
"node_modules/concurrently": {
- "version": "10.0.3",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz",
- "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==",
+ "version": "10.0.5",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz",
+ "integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "5.6.2",
"rxjs": "7.8.2",
- "shell-quote": "1.8.4",
+ "shell-quote": "1.9.0",
"supports-color": "10.2.2",
"tree-kill": "1.2.2",
"yargs": "18.0.0"
@@ -3921,17 +3682,6 @@
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
- "node_modules/dom-helpers": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
- "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.8.7",
- "csstype": "^3.0.2"
- }
- },
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
@@ -4451,9 +4201,9 @@
"license": "MIT"
},
"node_modules/filelist/node_modules/brace-expansion": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
- "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
+ "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -5590,9 +5340,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.15",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
- "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
@@ -5949,9 +5699,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.16",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
- "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"funding": [
{
"type": "opencollective",
@@ -5968,7 +5718,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.12",
+ "nanoid": "^3.3.17",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -5987,9 +5737,9 @@
}
},
"node_modules/prettier": {
- "version": "3.9.4",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz",
- "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==",
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
+ "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
"dev": true,
"license": "MIT",
"bin": {
@@ -6331,23 +6081,6 @@
"integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
"license": "MIT"
},
- "node_modules/react-transition-group": {
- "version": "4.4.5",
- "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
- "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
- "license": "BSD-3-Clause",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.5.5",
- "dom-helpers": "^5.0.1",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.6.2"
- },
- "peerDependencies": {
- "react": ">=16.6.0",
- "react-dom": ">=16.6.0"
- }
- },
"node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
@@ -6590,9 +6323,9 @@
}
},
"node_modules/shell-quote": {
- "version": "1.8.4",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
- "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
+ "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index 73b8d65..83fe89d 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"concurrently": "^10.0.3",
- "globals": "^17.7.0"
+ "globals": "^17.7.0",
+ "prettier": "^3.9.6"
}
}