batchCooking/apps/web/src/pages/landing/LandingPage.tsx
Nicolas da7b077098
All checks were successful
CI / lint (push) Successful in 2m1s
CI / build (push) Successful in 2m16s
CI / intent-service-test (push) Successful in 3m47s
CI / e2e (push) Successful in 7m45s
CI / test (push) Successful in 14m48s
feat(web): page vitrine publique à la racine (#41)
Un visiteur déconnecté sur `/` voit désormais une vraie page de
présentation avant qu'on lui demande de se connecter, au lieu d'être
renvoyé directement vers `/login`.

- `RequireAuth.tsx` devient sensible au chemin : dans sa branche `!user`,
  `pathname === "/"` rend `<LandingPage />` ; toutes les autres routes
  protégées redirigent vers `/login` exactement comme avant. Aucun
  changement de la table de routes — `/` reste `<PlanningPage>` niché sous
  `RequireAuth`+`AppLayout`, et `AppLayout` (donc la sidebar) ne monte
  jamais pour la vitrine.
- `pages/landing/LandingPage.tsx` + `landing-page.scss` : hero (accroche +
  illustration SVG dessinée à la main dans le langage de `lucide-react`,
  zéro dépendance ni asset externe) + deux CTA, grille de 4
  fonctionnalités (icônes lucide), section « aperçu », CTA final. Copie
  entièrement sous le namespace i18n `landing.*`.
- `index.html` : favicon SVG (monogramme « bC ») + `<meta description>` —
  `/` est maintenant une page publique partageable.
- Les 3 captures d'écran réelles de la section « aperçu » sont servies
  statiquement depuis `public/landing/` (un fichier manquant retombe sur
  son `alt`, ne casse pas le build) ; elles restent à produire par un
  humain avec l'app lancée sur des données réelles — cf.
  `public/landing/README.md`.
- Tests Cypress `landing.feature` : vitrine visible déconnecté sur `/`,
  CTA vers `/login` et `/signup`, autre route protégée toujours redirigée,
  visiteur connecté voit bien le planning. `smoke.cy.ts` mis à jour pour
  tester la redirection depuis `/recettes` (puisque `/` ne redirige plus).

Closes #41
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 04:33:41 +02:00

130 lines
5.1 KiB
TypeScript

import { CalendarDays, HeartPulse, Sparkles, Users } from "lucide-react";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import "./landing-page.scss";
/**
* Public marketing page shown at `/` to a logged-out visitor (see
* `RequireAuth` for how it's wired in without touching the route table).
* All copy comes from i18next under the `landing.*` namespace — no
* hardcoded JSX text, same convention as every other page.
*
* Deliberately dependency-free for visuals: the hero illustration is an
* inline stroke-drawn SVG in the same visual language as `lucide-react`
* (the icon set already used everywhere), not an imported illustration
* library or an external asset — same choice `TechStepCorrectionPopover`
* made for its own hand-drawn icons. The three preview images are real
* app screenshots served statically from `public/landing/` (see that
* folder's README); a missing file just falls back to its `alt` text and
* never breaks the build.
*/
export function LandingPage() {
const { t } = useTranslation();
const features: { icon: ReactNode; key: "planning" | "import" | "health" | "household" }[] = [
{ icon: <CalendarDays aria-hidden />, key: "planning" },
{ icon: <Sparkles aria-hidden />, key: "import" },
{ icon: <HeartPulse aria-hidden />, key: "health" },
{ icon: <Users aria-hidden />, key: "household" },
];
const shots: { src: string; key: "planning" | "recipe" | "cookingSession" }[] = [
{ src: "/landing/planning.png", key: "planning" },
{ src: "/landing/recipe.png", key: "recipe" },
{ src: "/landing/cooking-session.png", key: "cookingSession" },
];
return (
<main className="landing">
<section className="landing__hero">
<div className="landing__hero-copy">
<p className="landing__wordmark">batchCooking</p>
<h1>{t("landing.hero.title")}</h1>
<p className="landing__lede">{t("landing.hero.lede")}</p>
<div className="landing__cta-row">
<Link className="landing__cta landing__cta--primary" to="/signup">
{t("landing.hero.ctaSignup")}
</Link>
<Link className="landing__cta landing__cta--secondary" to="/login">
{t("landing.hero.ctaLogin")}
</Link>
</div>
</div>
<HeroIllustration />
</section>
<section className="landing__features" aria-labelledby="landing-features-title">
<h2 id="landing-features-title">{t("landing.features.title")}</h2>
<ul className="landing__feature-grid">
{features.map(({ icon, key }) => (
<li key={key} className="landing__feature">
<span className="landing__feature-icon">{icon}</span>
<h3>{t(`landing.features.${key}.title`)}</h3>
<p>{t(`landing.features.${key}.body`)}</p>
</li>
))}
</ul>
</section>
<section className="landing__preview" aria-labelledby="landing-preview-title">
<h2 id="landing-preview-title">{t("landing.preview.title")}</h2>
<div className="landing__shots">
{shots.map(({ src, key }) => (
<figure key={key} className="landing__shot">
<img src={src} alt={t(`landing.preview.${key}Alt`)} loading="lazy" />
<figcaption>{t(`landing.preview.${key}Caption`)}</figcaption>
</figure>
))}
</div>
</section>
<section className="landing__final-cta">
<h2>{t("landing.finalCta.title")}</h2>
<Link className="landing__cta landing__cta--primary" to="/signup">
{t("landing.finalCta.button")}
</Link>
</section>
</main>
);
}
/**
* Hero illustration — a week grid with a simmering pot, drawn in the same
* 2px round-stroke `currentColor` language as the lucide icon set so it
* sits visually with the rest of the app. No external asset.
*/
function HeroIllustration() {
return (
<svg
className="landing__hero-art"
viewBox="0 0 240 200"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
{/* week grid */}
<rect x="18" y="26" width="150" height="118" rx="10" />
<line x1="18" y1="56" x2="168" y2="56" />
<line x1="63" y1="56" x2="63" y2="144" />
<line x1="108" y1="56" x2="108" y2="144" />
<line x1="138" y1="56" x2="138" y2="144" />
<line x1="18" y1="86" x2="168" y2="86" />
<line x1="18" y1="115" x2="168" y2="115" />
{/* a couple of "planned" chips */}
<rect x="26" y="64" width="30" height="12" rx="6" />
<rect x="71" y="93" width="30" height="12" rx="6" />
<rect x="116" y="123" width="16" height="12" rx="6" />
{/* simmering pot, front-right */}
<path d="M150 150h74a8 8 0 0 1 8 8v18a20 20 0 0 1-20 20h-50a20 20 0 0 1-20-20v-18a8 8 0 0 1 8-8Z" />
<line x1="142" y1="150" x2="128" y2="150" />
<line x1="232" y1="150" x2="246" y2="150" />
<path d="M164 138c-4-6 2-10 0-16" />
<path d="M188 136c-4-6 2-10 0-16" />
<path d="M212 138c-4-6 2-10 0-16" />
</svg>
);
}