ultisuite-client/components/landing/landing-sections.tsx
R3D347HR4Y efaaf16f60
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: update metadata and layout for new product pages
- Refactored metadata for contacts, administration, and Ulticards pages to utilize dynamic app names and descriptions.
- Introduced new product pages for Ultiai, Ultical, Ulticards, Ultidrive, Ultimail, and Ultimeet with appropriate metadata.
- Enhanced layout components to ensure consistent styling and functionality across new product sections.
- Updated various components to replace hardcoded labels with dynamic references to improve maintainability and consistency.
2026-06-19 22:11:42 +02:00

381 lines
14 KiB
TypeScript

"use client"
import { ULTICARDS_APP_NAME } from "@/lib/suite/page-metadata"
import Link from "next/link"
import { Icon } from "@iconify/react"
import { LandingReveal } from "@/components/landing/landing-reveal"
import {
LANDING_APPS,
LANDING_FEATURES,
LANDING_INTEGRATIONS,
landingAppGlowVars,
} from "@/components/landing/landing-data"
import { useChromeIdentity } from "@/lib/hooks/use-chrome-identity"
import { getAuthentikEnrollmentUrl } from "@/lib/auth/oidc-config"
import { cn } from "@/lib/utils"
function SectionHeading({
eyebrow,
title,
description,
}: {
eyebrow: string
title: React.ReactNode
description?: string
}) {
return (
<LandingReveal className="mx-auto flex max-w-2xl flex-col items-center gap-4 text-center">
<span className="rounded-full bg-[var(--landing-chip)] px-3.5 py-1 text-xs font-semibold uppercase tracking-wider text-[var(--landing-chip-fg)]">
{eyebrow}
</span>
<h2 className="text-balance text-3xl font-bold tracking-tight sm:text-4xl">
{title}
</h2>
{description ? (
<p className="text-balance text-base leading-relaxed text-[var(--landing-muted)]">
{description}
</p>
) : null}
</LandingReveal>
)
}
/* ---------- Applications ---------- */
export function LandingAppsSection() {
return (
<section id="applications" className="scroll-mt-20 px-4 py-20 sm:px-6">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-12">
<SectionHeading
eyebrow="Applications"
title={
<>
Une suite <span className="landing-gradient-text">connectée</span>,
pas une collection d'outils
</>
}
description="Chaque application partage la même identité, les mêmes contacts et le même stockage. Ouvrez, l'écosystème suit."
/>
<ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{LANDING_APPS.map((app, index) => {
const card = (
<div
className={cn(
"landing-glass landing-halo-card flex h-full flex-col gap-3 rounded-2xl p-5",
app.soon && "opacity-75"
)}
style={landingAppGlowVars(app.gradient) as React.CSSProperties}
>
<div className="flex items-center justify-between">
<span
className="flex size-11 items-center justify-center rounded-xl"
style={{ backgroundColor: `${app.accent}1a` }}
>
{app.iconDark ? (
<>
<img
src={app.icon}
alt=""
className="size-7 object-contain dark:hidden"
draggable={false}
aria-hidden
/>
<img
src={app.iconDark}
alt=""
className="hidden size-7 object-contain dark:block"
draggable={false}
aria-hidden
/>
</>
) : (
<img
src={app.icon}
alt=""
className="size-7 object-contain"
draggable={false}
aria-hidden
/>
)}
</span>
{app.soon ? (
<span className="rounded-full border border-[var(--landing-line)] px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--landing-muted)]">
Bientôt
</span>
) : (
<Icon
icon="mdi:arrow-top-right"
className="size-4 text-[var(--landing-muted)] transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
aria-hidden
/>
)}
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-wider text-[var(--landing-muted)]">
{app.tagline}
</p>
<h3 className="text-lg font-semibold tracking-tight">
{app.name}
</h3>
</div>
<p className="text-sm leading-relaxed text-[var(--landing-muted)]">
{app.description}
</p>
</div>
)
return (
<LandingReveal as="li" key={app.name} delay={(index % 4) * 0.07}>
{app.href && !app.soon ? (
<Link href={app.productHref ?? app.href} className="group block h-full rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-ring/50">
{card}
</Link>
) : (
card
)}
</LandingReveal>
)
})}
</ul>
</div>
</section>
)
}
/* ---------- Fonctionnalités (bento) ---------- */
export function LandingFeaturesSection() {
return (
<section id="fonctionnalites" className="scroll-mt-20 px-4 py-20 sm:px-6">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-12">
<SectionHeading
eyebrow="Fonctionnalités"
title={
<>
Conçue pour remplacer,
<br className="hidden sm:block" /> pensée pour{" "}
<span className="landing-gradient-text">durer</span>
</>
}
description="Tout ce que vous attendez d'une suite moderne — sans céder vos données pour l'obtenir."
/>
<ul className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{LANDING_FEATURES.map((feature, index) => (
<LandingReveal
as="li"
key={feature.title}
delay={(index % 4) * 0.07}
className={cn(feature.wide && "sm:col-span-2")}
>
<div className="landing-glass landing-halo-card flex h-full flex-col gap-3 rounded-2xl p-6">
<span className="flex size-11 items-center justify-center rounded-xl bg-[var(--landing-chip)] text-[var(--landing-chip-fg)]">
<Icon icon={feature.icon} className="size-6" aria-hidden />
</span>
<h3 className="text-lg font-semibold tracking-tight">
{feature.title}
</h3>
<p className="text-sm leading-relaxed text-[var(--landing-muted)]">
{feature.description}
</p>
</div>
</LandingReveal>
))}
</ul>
</div>
</section>
)
}
/* ---------- Souveraineté + stats ---------- */
const STATS = [
{ value: "100 %", label: "open source, auditable" },
{ value: "0", label: "tracker, télémétrie ou pub" },
{ value: "1", label: "identité SSO pour toute la suite" },
{ value: "", label: "contrôle : c'est votre serveur" },
]
export function LandingSovereigntySection() {
return (
<section id="souverainete" className="scroll-mt-20 px-4 py-20 sm:px-6">
<div className="mx-auto w-full max-w-6xl">
<LandingReveal>
<div className="landing-glass-strong relative overflow-hidden rounded-3xl px-6 py-12 sm:px-12 sm:py-16">
<div
className="pointer-events-none absolute inset-0 opacity-60"
style={{
background:
"radial-gradient(60% 90% at 15% 0%, color-mix(in oklab, var(--landing-glow-a) 22%, transparent), transparent 70%), radial-gradient(50% 80% at 90% 100%, color-mix(in oklab, var(--landing-glow-b) 18%, transparent), transparent 70%)",
}}
aria-hidden
/>
<div className="relative flex flex-col gap-10">
<div className="flex max-w-2xl flex-col gap-4">
<span className="inline-flex w-fit items-center gap-2 rounded-full bg-[var(--landing-chip)] px-3.5 py-1 text-xs font-semibold uppercase tracking-wider text-[var(--landing-chip-fg)]">
<Icon icon="mdi:shield-check-outline" className="size-4" aria-hidden />
Souveraineté
</span>
<h2 className="text-balance text-3xl font-bold tracking-tight sm:text-4xl">
Vos données ne quittent jamais{" "}
<span className="landing-gradient-text">votre territoire</span>
</h2>
<p className="text-base leading-relaxed text-[var(--landing-muted)]">
Déployée chez vous ou chez l'hébergeur de votre choix, la
suite Ulti garde mails, fichiers et identités sous votre
juridiction. Migration progressive : rattachez vos comptes
existants et avancez à votre rythme.
</p>
</div>
<dl className="grid grid-cols-2 gap-6 lg:grid-cols-4">
{STATS.map((stat, index) => (
<LandingReveal key={stat.label} delay={index * 0.08}>
<div className="flex flex-col gap-1 border-l-2 border-[var(--landing-glow-a)] pl-4">
<dt className="order-2 text-sm text-[var(--landing-muted)]">
{stat.label}
</dt>
<dd className="order-1 text-3xl font-bold tracking-tight sm:text-4xl">
{stat.value}
</dd>
</div>
</LandingReveal>
))}
</dl>
</div>
</div>
</LandingReveal>
</div>
</section>
)
}
/* ---------- Intégrations (marquee) ---------- */
function IntegrationsTrack() {
return (
<div className="landing-marquee__track" aria-hidden>
{LANDING_INTEGRATIONS.map((item) => (
<span
key={item.label}
className="flex shrink-0 items-center gap-2 text-sm font-medium text-[var(--landing-muted)]"
>
<Icon icon={item.icon} className="size-5" aria-hidden />
{item.label}
</span>
))}
</div>
)
}
export function LandingIntegrationsSection() {
return (
<section className="px-0 pt-4 pb-10">
<LandingReveal className="mx-auto flex w-full max-w-6xl flex-col gap-6">
<p className="text-center text-xs font-semibold uppercase tracking-widest text-[var(--landing-muted)]">
S'intègre avec vos standards ouverts
</p>
<div
className="landing-marquee"
role="list"
aria-label={LANDING_INTEGRATIONS.map((i) => i.label).join(", ")}
>
<IntegrationsTrack />
<IntegrationsTrack />
</div>
</LandingReveal>
</section>
)
}
/* ---------- CTA final + footer ---------- */
export function LandingFooter() {
const identity = useChromeIdentity()
return (
<footer className="px-4 pb-10 pt-20 sm:px-6">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-14">
<LandingReveal className="flex flex-col items-center gap-6 text-center">
<h2 className="text-balance text-3xl font-bold tracking-tight sm:text-5xl">
Prêt à reprendre{" "}
<span className="landing-gradient-text">le contrôle</span> ?
</h2>
<p className="max-w-xl text-balance text-base text-[var(--landing-muted)]">
{identity
? "Votre suite est déjà prête. Ouvrez une application et continuez là où vous en étiez."
: "Créez votre compte et découvrez une suite complète qui travaille pour vous — pas l'inverse."}
</p>
<div className="flex flex-wrap items-center justify-center gap-3">
{identity ? (
<>
<Link
href="/mail/inbox"
className="landing-cta landing-cta--primary h-12 px-7 text-base"
>
Ouvrir Ultimail
<Icon icon="mdi:arrow-right" className="size-5" aria-hidden />
</Link>
<Link
href="/chat"
className="landing-cta landing-cta--ghost h-12 px-7 text-base"
>
Parler à UltiAI
</Link>
</>
) : (
<>
<a
href={getAuthentikEnrollmentUrl()}
className="landing-cta landing-cta--primary h-12 px-7 text-base"
>
Créer un compte
<Icon icon="mdi:arrow-right" className="size-5" aria-hidden />
</a>
<Link
href="/login"
className="landing-cta landing-cta--ghost h-12 px-7 text-base"
>
Se connecter
</Link>
</>
)}
</div>
</LandingReveal>
<div className="flex flex-col items-center justify-between gap-4 border-t border-[var(--landing-line)] pt-8 text-sm text-[var(--landing-muted)] sm:flex-row">
<div className="flex items-center gap-2.5">
<img
src="/ultisuite-mark.svg"
alt=""
className="h-6 w-6 object-contain"
draggable={false}
aria-hidden
/>
<span className="font-semibold text-[var(--landing-fg)]">
UltiSuite
</span>
<span aria-hidden>·</span>
<span>Suite collaborative souveraine et open source</span>
</div>
<nav className="flex items-center gap-4" aria-label="Liens">
<Link href="/mail" className="transition-colors hover:text-[var(--landing-fg)]">
Ultimail
</Link>
<Link href="/drive" className="transition-colors hover:text-[var(--landing-fg)]">
UltiDrive
</Link>
<Link href="/contacts" className="transition-colors hover:text-[var(--landing-fg)]">
{ULTICARDS_APP_NAME}
</Link>
<Link href="/chat" className="transition-colors hover:text-[var(--landing-fg)]">
UltiAI
</Link>
</nav>
</div>
</div>
</footer>
)
}