Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
209 lines
7.5 KiB
TypeScript
209 lines
7.5 KiB
TypeScript
"use client"
|
|
|
|
import { useRef, useState } from "react"
|
|
import Link from "next/link"
|
|
import { Icon } from "@iconify/react"
|
|
import { ProductCrossPlatformSection } from "@/components/landing/product/product-cross-platform-section"
|
|
import { ProductCta } from "@/components/landing/product/product-cta"
|
|
import { ProductFeatureGrid } from "@/components/landing/product/product-feature-grid"
|
|
import { ProductInteropSection } from "@/components/landing/product/product-interop-section"
|
|
import { ProductShowcases } from "@/components/landing/product/product-showcases"
|
|
import { ProductHero } from "@/components/landing/product/product-hero"
|
|
import { ProductHighlights } from "@/components/landing/product/product-highlights"
|
|
import { ProductIntegrations } from "@/components/landing/product/product-integrations"
|
|
import type { ProductPageData } from "@/components/landing/product/product-data"
|
|
import { useChromeIdentity } from "@/lib/hooks/use-chrome-identity"
|
|
import { getAuthentikEnrollmentUrl } from "@/lib/auth/oidc-config"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
function ProductHeader({
|
|
data,
|
|
scrolled,
|
|
}: {
|
|
data: ProductPageData
|
|
scrolled: boolean
|
|
}) {
|
|
const identity = useChromeIdentity()
|
|
|
|
return (
|
|
<header
|
|
className={cn(
|
|
"sticky top-0 z-40 transition-[background-color,border-color,box-shadow,backdrop-filter] duration-300",
|
|
scrolled
|
|
? "landing-glass-strong shadow-[0_8px_30px_-18px_rgba(0,0,0,0.35)]"
|
|
: "border-b border-transparent"
|
|
)}
|
|
>
|
|
<div className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between gap-4 px-4 sm:px-6">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
<Link
|
|
href="/"
|
|
className="flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-1.5 text-sm font-medium text-[var(--landing-muted)] transition-colors hover:bg-[var(--landing-chip)] hover:text-[var(--landing-fg)]"
|
|
>
|
|
<Icon icon="mdi:arrow-left" className="size-4" aria-hidden />
|
|
<span className="hidden sm:inline">UltiSuite</span>
|
|
</Link>
|
|
<span className="text-[var(--landing-line)]" aria-hidden>
|
|
/
|
|
</span>
|
|
<Link
|
|
href="/"
|
|
className="flex min-w-0 items-center gap-2 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
>
|
|
<img
|
|
src={data.icon}
|
|
alt=""
|
|
className="size-7 shrink-0 object-contain"
|
|
draggable={false}
|
|
aria-hidden
|
|
/>
|
|
<span className="truncate text-lg font-semibold tracking-tight">
|
|
{data.name}
|
|
</span>
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
{identity ? (
|
|
<Link
|
|
href={data.ctas.primary.href}
|
|
className="landing-cta landing-cta--primary h-9 px-4 text-sm"
|
|
style={
|
|
{
|
|
"--landing-glow-a": data.accent,
|
|
"--landing-glow-b": data.accent,
|
|
"--landing-glow-c": data.accent,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
{data.ctas.primary.label}
|
|
<Icon icon="mdi:arrow-right" className="size-4" aria-hidden />
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<a
|
|
href={getAuthentikEnrollmentUrl()}
|
|
className="landing-cta landing-cta--ghost hidden h-9 px-4 text-sm sm:inline-flex"
|
|
>
|
|
Créer un compte
|
|
</a>
|
|
<Link
|
|
href="/login"
|
|
className="landing-cta landing-cta--primary h-9 px-4 text-sm"
|
|
style={
|
|
{
|
|
"--landing-glow-a": data.accent,
|
|
"--landing-glow-b": data.accent,
|
|
"--landing-glow-c": data.accent,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
Se connecter
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|
|
|
|
function ProductFooter({ data }: { data: ProductPageData }) {
|
|
return (
|
|
<footer className="border-t border-[var(--landing-line)] px-4 py-8 sm:px-6">
|
|
<div className="mx-auto flex w-full max-w-6xl flex-col items-center justify-between gap-4 text-sm text-[var(--landing-muted)] sm:flex-row">
|
|
<div className="flex items-center gap-2.5">
|
|
<img
|
|
src={data.icon}
|
|
alt=""
|
|
className="h-5 w-5 object-contain"
|
|
draggable={false}
|
|
aria-hidden
|
|
/>
|
|
<span className="font-semibold text-[var(--landing-fg)]">
|
|
{data.name}
|
|
</span>
|
|
<span aria-hidden>·</span>
|
|
<span>{data.tagline} — UltiSuite</span>
|
|
</div>
|
|
<nav className="flex items-center gap-4" aria-label="Liens">
|
|
<Link href="/" className="transition-colors hover:text-[var(--landing-fg)]">
|
|
Accueil
|
|
</Link>
|
|
<Link
|
|
href={data.ctas.primary.href}
|
|
className="transition-colors hover:text-[var(--landing-fg)]"
|
|
>
|
|
Ouvrir l'app
|
|
</Link>
|
|
{data.ctas.secondary ? (
|
|
<Link
|
|
href={data.ctas.secondary.href}
|
|
className="transition-colors hover:text-[var(--landing-fg)]"
|
|
>
|
|
Démo
|
|
</Link>
|
|
) : null}
|
|
</nav>
|
|
</div>
|
|
</footer>
|
|
)
|
|
}
|
|
|
|
export function ProductPageShell({ data }: { data: ProductPageData }) {
|
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
const [scrolled, setScrolled] = useState(false)
|
|
|
|
const accentStyle = {
|
|
"--landing-glow-a": data.accent,
|
|
"--landing-glow-b": data.accent,
|
|
"--landing-glow-c": data.accent,
|
|
"--landing-chip": `${data.accent}1a`,
|
|
"--landing-chip-fg": data.accent,
|
|
} as React.CSSProperties
|
|
|
|
return (
|
|
<div
|
|
ref={scrollRef}
|
|
className="landing-root relative h-dvh overflow-y-auto overflow-x-hidden"
|
|
style={accentStyle}
|
|
onScroll={() => {
|
|
const top = scrollRef.current?.scrollTop ?? 0
|
|
setScrolled((prev) => (top > 8 ? true : top <= 2 ? false : prev))
|
|
}}
|
|
>
|
|
<div className="landing-backdrop" aria-hidden>
|
|
<div className="landing-orb landing-orb--a" />
|
|
<div className="landing-orb landing-orb--b" />
|
|
<div className="landing-orb landing-orb--c" />
|
|
</div>
|
|
|
|
<div className="relative z-10 flex min-h-full flex-col">
|
|
<ProductHeader data={data} scrolled={scrolled} />
|
|
<main className="flex-1">
|
|
<ProductHero data={data} />
|
|
<ProductHighlights section={data.highlightsSection} accent={data.accent} />
|
|
{data.showcases.length > 0 ? (
|
|
<ProductShowcases showcases={data.showcases} accent={data.accent} />
|
|
) : data.featureGroups ? (
|
|
<ProductFeatureGrid groups={data.featureGroups} accent={data.accent} />
|
|
) : null}
|
|
{data.crossPlatformSection ? (
|
|
<ProductCrossPlatformSection
|
|
section={data.crossPlatformSection}
|
|
accent={data.accent}
|
|
demoApp={data.crossPlatformDemo}
|
|
/>
|
|
) : null}
|
|
{data.interopSection ? (
|
|
<ProductInteropSection section={data.interopSection} accent={data.accent} />
|
|
) : null}
|
|
<ProductIntegrations integrations={data.integrations} accent={data.accent} />
|
|
<ProductCta section={data.ctaSection} accent={data.accent} />
|
|
</main>
|
|
<ProductFooter data={data} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|