+
+
{session ? (
{m.marketing.nav.openApp}
@@ -40,7 +50,7 @@ export async function MarketingNav() {
{m.marketing.nav.login}
- {m.marketing.nav.signup}
+ {signupsDisabled ? m.marketing.nav.signupClosed : m.marketing.nav.signup}
>
)}
diff --git a/apps/web/components/marketing/theme-toggle.tsx b/apps/web/components/marketing/theme-toggle.tsx
new file mode 100644
index 0000000..b97de73
--- /dev/null
+++ b/apps/web/components/marketing/theme-toggle.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { useTheme } from "next-themes";
+import { Sun, Moon } from "lucide-react";
+
+/** Simple light/dark toggle for the marketing header — the authenticated
+ * app's Nav has a fuller light/dark/system control buried in a dropdown;
+ * this is a one-click version for a page a visitor hasn't logged into yet. */
+export function ThemeToggle() {
+ const { resolvedTheme, setTheme } = useTheme();
+ const isDark = resolvedTheme === "dark";
+
+ return (
+
+ );
+}
diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts
index 44ee65d..16bacc4 100644
--- a/apps/web/lib/changelog.ts
+++ b/apps/web/lib/changelog.ts
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together.
-export const APP_VERSION = "0.48.0";
+export const APP_VERSION = "0.48.1";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ version: "0.48.1",
+ date: "2026-07-18 11:00",
+ added: [
+ "Marketing site: language switcher (flag toggle, EN/FR) and a dark-mode toggle in the header — both work for logged-out visitors, no account needed. Features/Home now also cover cook mode, recipe variations, personalized recommendations, and ingredient substitution. Signup CTAs now say \"Signups closed\" instead of \"Get started free\" when signups are disabled.",
+ ],
+ },
{
version: "0.48.0",
date: "2026-07-18 09:45",
diff --git a/apps/web/lib/marketing-locale-cookie.ts b/apps/web/lib/marketing-locale-cookie.ts
new file mode 100644
index 0000000..644c737
--- /dev/null
+++ b/apps/web/lib/marketing-locale-cookie.ts
@@ -0,0 +1,6 @@
+// Just the cookie name — split from marketing-locale.ts so client
+// components can import it without pulling in that file's server-only
+// imports (next/headers, lib/auth/server → the DB client → `postgres`,
+// which Next.js then tries to bundle for the browser and fails on Node
+// built-ins like `tls`).
+export const MARKETING_LOCALE_COOKIE = "marketing_locale";
diff --git a/apps/web/lib/marketing-locale.ts b/apps/web/lib/marketing-locale.ts
new file mode 100644
index 0000000..cda4ab7
--- /dev/null
+++ b/apps/web/lib/marketing-locale.ts
@@ -0,0 +1,28 @@
+import { headers, cookies } from "next/headers";
+import { auth } from "@/lib/auth/server";
+import type { Locale } from "@/lib/i18n/provider";
+import { MARKETING_LOCALE_COOKIE } from "@/lib/marketing-locale-cookie";
+
+export { MARKETING_LOCALE_COOKIE };
+
+/**
+ * Locale for the public marketing pages — cookie-based, not tied to a user
+ * account (visitors there have no session). Separate from the authenticated
+ * app's locale system (`lib/i18n/provider.tsx`'s `useLocale`/`setLocale`),
+ * which persists to `users.locale` via an authenticated PATCH that would
+ * just 401 for an anonymous visitor and revert the UI.
+ */
+export async function getMarketingLocale(): Promise {
+ const store = await cookies();
+ const value = store.get(MARKETING_LOCALE_COOKIE)?.value;
+ return value === "fr" ? "fr" : "en";
+}
+
+/** A logged-in visitor's own saved locale wins (matches what they see
+ * everywhere else in the app); otherwise the marketing cookie. */
+export async function getEffectiveMarketingLocale(): Promise {
+ const session = await auth.api.getSession({ headers: await headers() }).catch(() => null);
+ const sessionLocale = (session?.user as { locale?: string } | undefined)?.locale;
+ if (sessionLocale === "en" || sessionLocale === "fr") return sessionLocale;
+ return getMarketingLocale();
+}
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 153ce9f..fae612e 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -5,7 +5,8 @@
"about": "About",
"openApp": "Open app",
"login": "Log in",
- "signup": "Sign up"
+ "signup": "Sign up",
+ "signupClosed": "Signups closed"
},
"footer": {
"about": "About",
@@ -26,6 +27,8 @@
"assistant": { "title": "Cooking assistant", "description": "Ask cooking questions anytime — it can even draft a recipe or shopping list right in the conversation, for you to review and confirm." },
"mealPlan": { "title": "Meal planning", "description": "Plan your week, generate a plan with AI, and turn it straight into a shopping list." },
"pantry": { "title": "Pantry tracking", "description": "Know what you have on hand — scan it in, and let recipes account for it automatically." },
+ "cookMode": { "title": "Hands-free cook mode", "description": "A distraction-free, step-by-step view for actually cooking — large text, timers, no scrolling with messy hands." },
+ "variations": { "title": "Recipe variations", "description": "Ask for a dietary twist, an ingredient swap, or a whole new spin on a recipe — AI proposes a variation, you decide whether to keep it." },
"social": { "title": "Share & discover", "description": "Follow other cooks, rate and comment on recipes, and control exactly who can see yours." },
"batchCook": { "title": "Batch cooking", "description": "Cook once, eat all week — dedicated batch-cook planning with fridge/freezer guidance." }
}
@@ -39,6 +42,10 @@
"photoImport": { "title": "Import from a photo", "description": "A vision model recognizes what's in the photo, then a text model writes the full recipe from that description." },
"assistant": { "title": "Cooking assistant with tool-calling", "description": "A conversational assistant for cooking questions that can also draft a recipe or shopping-list additions for you to confirm — nothing saves without your say." },
"mealPlan": { "title": "AI meal planning", "description": "Generate a week of meals around your preferences, dietary needs, and what's already in your pantry." },
+ "cookMode": { "title": "Hands-free cook mode", "description": "A distraction-free, step-by-step cooking view — large text, built-in timers, made for a kitchen counter, not a desk." },
+ "variations": { "title": "Recipe variations", "description": "Adapt a recipe for a diet, an excluded ingredient, or a creative twist — AI proposes changes, you review and confirm before anything is saved." },
+ "recommendations": { "title": "Personalized recommendations", "description": "A For You feed ranked from what you've favorited and rated, plus meal/drink pairing suggestions for any recipe." },
+ "substitution": { "title": "Ingredient substitution", "description": "Out of something, or avoiding it? Get AI-suggested swaps for any ingredient, right from the recipe." },
"pantry": { "title": "Pantry tracking", "description": "Track what you have — scan barcodes or photos — and let it inform meal planning and shopping lists." },
"shoppingList": { "title": "Shopping lists", "description": "Multiple lists, shared with others, auto-populated from recipes or meal plans." },
"social": { "title": "Follows, ratings & comments", "description": "A social layer for recipes — follow other cooks, rate and review, with granular visibility controls including followers-only." },
diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json
index caccaf4..a58f40a 100644
--- a/apps/web/messages/fr.json
+++ b/apps/web/messages/fr.json
@@ -5,7 +5,8 @@
"about": "À propos",
"openApp": "Ouvrir l'application",
"login": "Connexion",
- "signup": "Inscription"
+ "signup": "Inscription",
+ "signupClosed": "Inscriptions fermées"
},
"footer": {
"about": "À propos",
@@ -26,6 +27,8 @@
"assistant": { "title": "Assistant culinaire", "description": "Posez vos questions de cuisine à tout moment — il peut même rédiger une recette ou une liste de courses directement dans la conversation, pour que vous la validiez." },
"mealPlan": { "title": "Planification de repas", "description": "Planifiez votre semaine, générez un plan avec l'IA, et transformez-le directement en liste de courses." },
"pantry": { "title": "Suivi du garde-manger", "description": "Sachez ce que vous avez sous la main — scannez-le, et laissez les recettes en tenir compte automatiquement." },
+ "cookMode": { "title": "Mode cuisine mains libres", "description": "Une vue étape par étape sans distraction pour cuisiner — grand texte, minuteurs intégrés, pas besoin de scroller les mains sales." },
+ "variations": { "title": "Variations de recette", "description": "Demandez une variante diététique, un remplacement d'ingrédient, ou une toute nouvelle interprétation — l'IA propose une variation, vous décidez de la garder." },
"social": { "title": "Partager & découvrir", "description": "Suivez d'autres cuisiniers, notez et commentez les recettes, et contrôlez précisément qui peut voir les vôtres." },
"batchCook": { "title": "Cuisine en lots", "description": "Cuisinez une fois, mangez toute la semaine — planification dédiée avec conseils de conservation au frigo/congélateur." }
}
@@ -39,6 +42,10 @@
"photoImport": { "title": "Importer depuis une photo", "description": "Un modèle de vision identifie ce qui est sur la photo, puis un modèle de texte rédige la recette complète à partir de cette description." },
"assistant": { "title": "Assistant culinaire avec actions", "description": "Un assistant conversationnel pour vos questions de cuisine, capable aussi de rédiger une recette ou d'ajouter des articles à une liste de courses pour que vous les validiez — rien n'est enregistré sans votre accord." },
"mealPlan": { "title": "Planification de repas par IA", "description": "Générez une semaine de repas selon vos préférences, besoins alimentaires, et ce qui est déjà dans votre garde-manger." },
+ "cookMode": { "title": "Mode cuisine mains libres", "description": "Une vue de cuisine étape par étape sans distraction — grand texte, minuteurs intégrés, pensée pour le plan de travail, pas un bureau." },
+ "variations": { "title": "Variations de recette", "description": "Adaptez une recette pour un régime, un ingrédient exclu, ou une touche créative — l'IA propose des changements, vous validez avant tout enregistrement." },
+ "recommendations": { "title": "Recommandations personnalisées", "description": "Un fil « Pour vous » classé selon vos favoris et vos notes, plus des suggestions d'accords repas/boisson pour chaque recette." },
+ "substitution": { "title": "Substitution d'ingrédients", "description": "Il vous manque un ingrédient, ou vous l'évitez ? Obtenez des remplacements suggérés par l'IA, directement depuis la recette." },
"pantry": { "title": "Suivi du garde-manger", "description": "Suivez ce que vous avez — scannez codes-barres ou photos — pour informer la planification et les listes de courses." },
"shoppingList": { "title": "Listes de courses", "description": "Plusieurs listes, partageables, remplies automatiquement à partir des recettes ou des plans de repas." },
"social": { "title": "Abonnements, notes & commentaires", "description": "Une couche sociale pour les recettes — suivez d'autres cuisiniers, notez et commentez, avec un contrôle de visibilité précis, y compris réservé aux abonnés." },
diff --git a/apps/web/package.json b/apps/web/package.json
index 4e11ffb..8589260 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@epicure/web",
- "version": "0.48.0",
+ "version": "0.48.1",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/package.json b/package.json
index a7379a1..c83edbc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "epicure",
- "version": "0.48.0",
+ "version": "0.48.1",
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",