fix: replace emoji flags with SVG dropdown, complete Home's third row

Language switcher was two inline emoji-flag buttons -- emoji flag
rendering is inconsistent across OS/browsers (some render as two-letter
country codes instead of a flag). Replaced with hand-rolled inline SVG
flags (no new dependency for two locales) inside a proper dropdown
menu, trigger showing the current language's flag.

Also added a 9th Home highlight (personalized recommendations, same
copy already written for the Features page) so the lg:grid-cols-3
feature grid's last row has 3 items instead of 2.

v0.48.2
This commit is contained in:
Arnaud
2026-07-18 11:04:37 +02:00
parent 8dccd8898b
commit 9ea1f90ec4
9 changed files with 74 additions and 26 deletions
+5
View File
@@ -2,6 +2,11 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
## 0.48.2 — 2026-07-18 11:15
### Fixed
- Marketing site's language switcher no longer uses emoji flags (inconsistent rendering across OS/browsers) — now inline SVG flags in a proper dropdown menu. Home page's feature grid also got a 9th highlight (personalized recommendations) so the last row isn't left half-empty.
## 0.48.1 — 2026-07-18 11:00
### Added
+2 -1
View File
@@ -7,7 +7,7 @@ import { getEffectiveMarketingLocale } from "@/lib/marketing-locale";
import { isSignupsDisabled } from "@/lib/site-settings";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Sparkles, Calendar, Package, MessageCircle, Users, ChefHat, Utensils, Wand2 } from "lucide-react";
import { Sparkles, Calendar, Package, MessageCircle, Users, ChefHat, Utensils, Wand2, Sparkle } from "lucide-react";
const HIGHLIGHTS = [
{ icon: Sparkles, key: "ai" },
@@ -18,6 +18,7 @@ const HIGHLIGHTS = [
{ icon: Wand2, key: "variations" },
{ icon: Users, key: "social" },
{ icon: ChefHat, key: "batchCook" },
{ icon: Sparkle, key: "recommendations" },
] as const;
export default async function MarketingHomePage() {
@@ -0,0 +1,24 @@
// Minimal inline SVG flags — no emoji (inconsistent rendering across
// OS/browsers) and no new dependency for just two supported locales.
export function FlagGB({ className }: { className?: string }) {
return (
<svg viewBox="0 0 20 14" className={className} aria-hidden="true">
<rect width="20" height="14" fill="#00247d" />
<path d="M0 0 20 14M20 0 0 14" stroke="#fff" strokeWidth="2.4" />
<path d="M0 0 20 14M20 0 0 14" stroke="#cf142b" strokeWidth="1.2" />
<path d="M10 0V14M0 7H20" stroke="#fff" strokeWidth="4" />
<path d="M10 0V14M0 7H20" stroke="#cf142b" strokeWidth="2.4" />
</svg>
);
}
export function FlagFR({ className }: { className?: string }) {
return (
<svg viewBox="0 0 20 14" className={className} aria-hidden="true">
<rect width="20" height="14" fill="#fff" />
<rect width="6.67" height="14" fill="#0055a4" />
<rect x="13.33" width="6.67" height="14" fill="#ef4135" />
</svg>
);
}
@@ -1,12 +1,20 @@
"use client";
import { useRouter } from "next/navigation";
import { Check, Globe } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MARKETING_LOCALE_COOKIE } from "@/lib/marketing-locale-cookie";
import type { Locale } from "@/lib/i18n/provider";
import { FlagGB, FlagFR } from "./flag-icons";
const OPTIONS: { code: Locale; flag: string; label: string }[] = [
{ code: "en", flag: "🇬🇧", label: "English" },
{ code: "fr", flag: "🇫🇷", label: "Français" },
const OPTIONS: { code: Locale; Flag: typeof FlagGB; label: string }[] = [
{ code: "en", Flag: FlagGB, label: "English" },
{ code: "fr", Flag: FlagFR, label: "Français" },
];
// Plain module-level function, not part of the component body — setting a
@@ -26,6 +34,7 @@ function setMarketingLocaleCookie(code: Locale) {
*/
export function LanguageSwitcher({ current }: { current: Locale }) {
const router = useRouter();
const CurrentFlag = OPTIONS.find((o) => o.code === current)?.Flag ?? Globe;
function select(code: Locale) {
if (code === current) return;
@@ -34,22 +43,22 @@ export function LanguageSwitcher({ current }: { current: Locale }) {
}
return (
<div className="flex items-center gap-0.5" role="group" aria-label="Language">
{OPTIONS.map(({ code, flag, label }) => (
<button
key={code}
type="button"
onClick={() => select(code)}
aria-label={label}
aria-pressed={current === code}
title={label}
className={`text-base leading-none p-1.5 rounded-md transition-opacity ${
current === code ? "opacity-100" : "opacity-40 hover:opacity-70"
}`}
<DropdownMenu>
<DropdownMenuTrigger
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
aria-label="Change language"
>
{flag}
</button>
<CurrentFlag className="h-4 w-5 rounded-[2px]" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{OPTIONS.map(({ code, Flag, label }) => (
<DropdownMenuItem key={code} onClick={() => select(code)} className="gap-2">
<Flag className="h-3.5 w-[1.15rem] rounded-[2px] shrink-0" />
<span className="flex-1">{label}</span>
{current === code && <Check className="h-3.5 w-3.5" />}
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
);
}
+8 -1
View File
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together.
export const APP_VERSION = "0.48.1";
export const APP_VERSION = "0.48.2";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.48.2",
date: "2026-07-18 11:15",
fixed: [
"Marketing site's language switcher no longer uses emoji flags (inconsistent rendering across OS/browsers) — now inline SVG flags in a proper dropdown menu. Home page's feature grid also got a 9th highlight (personalized recommendations) so the last row isn't left half-empty.",
],
},
{
version: "0.48.1",
date: "2026-07-18 11:00",
+2 -1
View File
@@ -30,7 +30,8 @@
"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." }
"batchCook": { "title": "Batch cooking", "description": "Cook once, eat all week — dedicated batch-cook planning with fridge/freezer guidance." },
"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." }
}
},
"features": {
+2 -1
View File
@@ -30,7 +30,8 @@
"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." }
"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." },
"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." }
}
},
"features": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@epicure/web",
"version": "0.48.1",
"version": "0.48.2",
"private": true,
"scripts": {
"dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "epicure",
"version": "0.48.1",
"version": "0.48.2",
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",