feat(21-22): agente AI generazione preventivo + pagina pubblica deck

- Migration 0010: tabella proposals (id, slug, lead_id, client_id,
  offer_macro_id, content jsonb, state, selected_tier, accepted_at)
  applicata a prod via SSH tunnel
- @anthropic-ai/sdk@0.105.0 installato; ANTHROPIC_API_KEY in .env.local
- src/lib/proposal/: schema Zod ProposalContent, agente Claude Opus 4.8,
  assemble (AI + offerta DB + config consulente), queries, profile.ts
- Admin: /admin/preventivi lista + /genera (pre-fill ?lead_id=X) + /[id] review
- Sidebar: voce Preventivi + CTA globale lime "Genera preventivo"
- LeadDetail: pulsante "Genera preventivo" → /admin/preventivi/genera?lead_id=X
- Pagina pubblica /preventivo/[slug]: deck 20+ slide light-mode iamcavalli,
  navigazione frecce + dot + keyboard, accept/reject con guard immutabilità
- STATE.md aggiornato (80%), 21-PLAN.md scritto nel formato GSD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 14:37:05 +02:00
parent 86c86cd420
commit fdcc938252
41 changed files with 2818 additions and 19 deletions
+20 -7
View File
@@ -12,16 +12,18 @@ import {
Settings,
LogOut,
Zap,
FileText,
} from "lucide-react";
const NAV_ITEMS = [
{ href: "/admin", label: "Dashboard", icon: LayoutDashboard, exact: true },
{ href: "/admin/clients", label: "Clienti", icon: Users },
{ href: "/admin/leads", label: "Lead", icon: Zap },
{ href: "/admin/projects", label: "Progetti", icon: FolderOpen },
{ href: "/admin/offers", label: "Offerte", icon: Tag },
{ href: "/admin/catalog", label: "Catalogo", icon: BookOpen },
{ href: "/admin/impostazioni", label: "Impostazioni", icon: Settings },
{ href: "/admin", label: "Dashboard", icon: LayoutDashboard, exact: true },
{ href: "/admin/clients", label: "Clienti", icon: Users },
{ href: "/admin/leads", label: "Lead", icon: Zap },
{ href: "/admin/projects", label: "Progetti", icon: FolderOpen },
{ href: "/admin/preventivi", label: "Preventivi", icon: FileText },
{ href: "/admin/offers", label: "Offerte", icon: Tag },
{ href: "/admin/catalog", label: "Catalogo", icon: BookOpen },
{ href: "/admin/impostazioni", label: "Impostazioni", icon: Settings },
];
export function AdminSidebar() {
@@ -37,6 +39,17 @@ export function AdminSidebar() {
<span className="font-bold text-white tracking-tight text-sm">iamcavalli</span>
</div>
{/* CTA globale — genera preventivo */}
<div className="px-3 py-3 border-b border-white/10">
<Link
href="/admin/preventivi/genera"
className="flex items-center justify-center gap-2 w-full px-3 py-2 rounded-md text-xs font-semibold bg-[#DEF168] text-[#1A463C] hover:bg-[#d4e85e] transition-colors"
>
<FileText size={13} />
Genera preventivo
</Link>
</div>
{/* Nav links */}
<nav className="flex-1 px-3 py-4 flex flex-col gap-0.5">
{NAV_ITEMS.map(({ href, label, icon: Icon, exact }) => {
@@ -10,6 +10,7 @@ import { OptionMultiSelect } from "@/components/ui/option-multi-select";
import { addLeadTag, removeLeadTag, renameLeadTag, deleteTranscript } from "@/app/admin/leads/actions";
import { formatDistanceToNow, format } from "date-fns";
import { it } from "date-fns/locale";
import Link from "next/link";
import { LogActivityModal } from "./LogActivityModal";
import { SendQuoteModal } from "./SendQuoteModal";
import { EditLeadModal } from "./LeadForm";
@@ -99,6 +100,12 @@ export function LeadDetail({
<LogActivityModal leadId={lead.id} />
<TranscriptModal leadId={lead.id} />
<SendQuoteModal leadId={lead.id} />
<Link
href={`/admin/preventivi/genera?lead_id=${lead.id}`}
className="inline-flex items-center gap-1.5 px-3 py-2 bg-[#DEF168] text-[#1A463C] rounded-md text-sm font-semibold hover:bg-[#d4e85e] transition-colors"
>
Genera preventivo
</Link>
<EditLeadModal lead={lead} />
</div>
</div>
@@ -0,0 +1,193 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import type { ProposalDetail } from "@/lib/proposal/queries";
import { CoverSection } from "./sections/CoverSection";
import { VisionSection } from "./sections/VisionSection";
import { IndexSection } from "./sections/IndexSection";
import { ChapterDivider } from "./sections/ChapterDivider";
import { StrategistSection } from "./sections/StrategistSection";
import { FactsSection } from "./sections/FactsSection";
import { TestimonialsSection } from "./sections/TestimonialsSection";
import { ProblemNodeSection } from "./sections/ProblemNodeSection";
import { SynthesisDiagramSection } from "./sections/SynthesisDiagramSection";
import { SolutionNodeSection } from "./sections/SolutionNodeSection";
import { SolutionSynthesisSection } from "./sections/SolutionSynthesisSection";
import { ScopeSection } from "./sections/ScopeSection";
import { DeliverablesSection } from "./sections/DeliverablesSection";
import { TimelineSection } from "./sections/TimelineSection";
import { PricingSection } from "./sections/PricingSection";
import { StagesRecapSection } from "./sections/StagesRecapSection";
import { ComparisonMatrixSection } from "./sections/ComparisonMatrixSection";
import { NextStepsSection } from "./sections/NextStepsSection";
import { ClosingSection } from "./sections/ClosingSection";
import { AcceptSection } from "./sections/AcceptSection";
type Props = {
proposal: ProposalDetail;
onAccept: (slug: string, tier: string, email?: string, notes?: string) => Promise<void>;
onReject: (slug: string, notes?: string) => Promise<void>;
};
export function ProposalDeck({ proposal, onAccept, onReject }: Props) {
const { content, state, selectedTier, acceptedAt } = proposal;
const { ai, consultant, offer, header } = content;
const problems = ai.problems;
const solutions = ai.solutions;
// Costruisce la lista di slide dinamicamente
const testimonialPages = Math.ceil(consultant.testimonials.length / 9);
type Slide = { id: string; component: React.ReactNode };
const slides: Slide[] = [
// 01 — Cover
{ id: "cover", component: <CoverSection header={header} /> },
// 02 — Vision
{ id: "vision", component: <VisionSection vision={ai.vision} header={header} /> },
// 03 — Sommario
{ id: "index", component: <IndexSection /> },
// Cap.01 — Lo strategist
{ id: "ch01", component: <ChapterDivider number="01" title="Lo strategist." subtitle="Chi vi affianca in questo lavoro, con quali credenziali, con quale metodo e con quali precedenti dimostrabili." /> },
{ id: "strategist", component: <StrategistSection consultant={consultant} /> },
{ id: "facts", component: <FactsSection consultant={consultant} /> },
// Testimonianze (1N pagine da 9)
...Array.from({ length: testimonialPages }, (_, i) => ({
id: `testimonials-${i + 1}`,
component: (
<TestimonialsSection
testimonials={consultant.testimonials}
page={i + 1}
totalPages={testimonialPages}
/>
),
})),
// Cap.02 — Il problema
{ id: "ch02", component: <ChapterDivider number="02" title="Il problema." subtitle={`${problems.length} nodi rilevati nella discovery. Li chiamiamo per nome, uno per uno.`} /> },
// Nodi problema
...problems.map((p) => ({
id: `problem-${p.id}`,
component: <ProblemNodeSection problem={p} />,
})),
// Sintesi problema
{ id: "problem-synthesis", component: <SynthesisDiagramSection synthesis={ai.problemSynthesis} type="problem" /> },
// Cap.03 — La soluzione
{ id: "ch03", component: <ChapterDivider number="03" title="La soluzione." subtitle="Per ogni nodo, un'elevazione speculare. La struttura riprende l'ordine dei problemi e mostra dove la trasformazione vi porta." /> },
// Nodi soluzione
...solutions.map((s) => ({
id: `solution-${s.id}`,
component: <SolutionNodeSection solution={s} />,
})),
// Sintesi soluzione
{ id: "solution-synthesis", component: <SolutionSynthesisSection synthesis={ai.solutionSynthesis} /> },
// Cap.04 — L'esecuzione
{ id: "ch04", component: <ChapterDivider number="04" title="L'esecuzione." subtitle="Lo scope del lavoro, gli obiettivi, i deliverable, ciò che è incluso e ciò che non lo è. La timeline." /> },
{ id: "scope", component: <ScopeSection scope={ai.scope} /> },
{ id: "deliverables", component: <DeliverablesSection deliverables={ai.deliverables} /> },
{ id: "timeline", component: <TimelineSection timeline={ai.timeline} /> },
// Cap.05 — Le opzioni
{ id: "ch05", component: <ChapterDivider number="05" title="Le opzioni." subtitle="Il valore di mercato delle attività, le tre opzioni progressive con il loro pricing, e il processo per partire." /> },
{ id: "pricing", component: <PricingSection tiers={offer.tiers} /> },
// Stages recap + comparison
{ id: "stages", component: <StagesRecapSection stages={ai.stagesRecap.stages} /> },
{ id: "comparison", component: <ComparisonMatrixSection matrix={ai.comparisonMatrix} tiers={offer.tiers} /> },
// Come procedere / Next steps
{ id: "nextsteps", component: <NextStepsSection consultant={consultant} header={header} offer={offer} /> },
// Accept/Reject (solo se published)
...(state === "published"
? [{
id: "accept",
component: (
<AcceptSection
slug={proposal.slug}
tiers={offer.tiers}
header={header}
onAccept={onAccept}
onReject={onReject}
/>
),
}]
: []),
// Closing
{ id: "closing", component: <ClosingSection consultant={consultant} header={header} state={state} selectedTier={selectedTier} acceptedAt={acceptedAt} /> },
];
const total = slides.length;
const [current, setCurrent] = useState(0);
const goTo = useCallback((i: number) => setCurrent(Math.max(0, Math.min(total - 1, i))), [total]);
const prev = useCallback(() => goTo(current - 1), [current, goTo]);
const next = useCallback(() => goTo(current + 1), [current, goTo]);
// Navigazione da tastiera
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "ArrowRight" || e.key === "ArrowDown") next();
if (e.key === "ArrowLeft" || e.key === "ArrowUp") prev();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [next, prev]);
return (
<div className="relative min-h-screen bg-white font-sans select-none">
{/* Slide corrente */}
<div className="min-h-screen flex flex-col">
{/* Topbar */}
<div className="fixed top-0 left-0 right-0 z-20 flex items-center justify-between px-8 py-4 bg-white/90 backdrop-blur-sm border-b border-border">
<span className="text-xs font-mono text-muted-foreground tracking-widest uppercase">
Documento di Soluzione · {header.clientName} · {header.date}
</span>
<span className="text-xs font-mono text-muted-foreground">
{String(current + 1).padStart(2, "0")} / {String(total).padStart(2, "0")}
</span>
</div>
{/* Contenuto slide */}
<div className="flex-1 pt-16 pb-20">
{slides[current].component}
</div>
{/* Bottom nav */}
<div className="fixed bottom-0 left-0 right-0 z-20 flex items-center justify-between px-8 py-4 bg-white/90 backdrop-blur-sm border-t border-border">
{/* Frecce */}
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<button
onClick={prev}
disabled={current === 0}
className="disabled:opacity-30 hover:text-foreground transition-colors"
aria-label="Precedente"
>
</button>
<button
onClick={next}
disabled={current === total - 1}
className="disabled:opacity-30 hover:text-foreground transition-colors"
aria-label="Prossimo"
>
</button>
<span className="ml-2 hidden sm:inline">CLICK DOTS</span>
</div>
{/* Dot navigator */}
<div className="flex items-center gap-1 overflow-x-auto max-w-[60vw] py-1">
{slides.map((s, i) => (
<button
key={s.id}
onClick={() => goTo(i)}
aria-label={`Vai alla slide ${i + 1}`}
className={`shrink-0 rounded-full transition-all ${
i === current
? "w-6 h-2 bg-primary"
: "w-2 h-2 bg-border hover:bg-muted-foreground"
}`}
/>
))}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,137 @@
"use client";
import { useState, useTransition } from "react";
import { Loader2 } from "lucide-react";
import type { AssembledProposal } from "@/lib/proposal/assemble";
type Props = {
slug: string;
tiers: AssembledProposal["offer"]["tiers"];
header: AssembledProposal["header"];
onAccept: (slug: string, tier: string, email?: string, notes?: string) => Promise<void>;
onReject: (slug: string, notes?: string) => Promise<void>;
};
export function AcceptSection({ slug, tiers, header, onAccept, onReject }: Props) {
const [selectedTier, setSelectedTier] = useState<string | null>(null);
const [email, setEmail] = useState("");
const [notes, setNotes] = useState("");
const [action, setAction] = useState<"accept" | "reject" | null>(null);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
function handleAccept() {
if (!selectedTier) { setError("Seleziona un tier per procedere"); return; }
setError(null);
startTransition(async () => {
try {
await onAccept(slug, selectedTier, email || undefined, notes || undefined);
} catch (e) {
setError(e instanceof Error ? e.message : "Errore");
}
});
}
function handleReject() {
setError(null);
startTransition(async () => {
try {
await onReject(slug, notes || undefined);
} catch (e) {
setError(e instanceof Error ? e.message : "Errore");
}
});
}
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-10">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">
Accettazione formale
</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Scegli il tuo <span className="text-primary">tier.</span>
</h2>
<p className="text-base text-muted-foreground mt-4">
Seleziona l&apos;opzione che preferisci. La conferma via questo modulo attiva il lavoro.
</p>
</div>
{/* Selezione tier */}
<div className="grid grid-cols-3 gap-4">
{tiers.map((tier) => (
<button
key={tier.tierLetter}
onClick={() => setSelectedTier(tier.tierLetter)}
className={`rounded-xl border-2 p-6 text-left transition-all ${
selectedTier === tier.tierLetter
? "border-primary bg-primary/5"
: "border-border hover:border-primary/40"
}`}
>
<p className="text-2xl font-light text-primary">{tier.tierLetter}</p>
<p className="font-medium text-foreground mt-1">{tier.publicName}</p>
<p className="text-sm text-muted-foreground mt-2">
{tier.publicPrice ? `${parseFloat(tier.publicPrice).toLocaleString("it-IT")}` : `${parseFloat(tier.servicesTotal).toLocaleString("it-IT")}`}
</p>
</button>
))}
</div>
{/* Email e note */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-xs font-mono text-muted-foreground uppercase tracking-widest">
Email (opzionale)
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="tua@email.com"
className="w-full border border-border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-mono text-muted-foreground uppercase tracking-widest">
Note (opzionale)
</label>
<input
type="text"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Note per il consulente…"
className="w-full border border-border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
/>
</div>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
{/* CTA */}
<div className="flex items-center gap-4">
<button
onClick={handleAccept}
disabled={isPending}
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-xl font-medium hover:bg-primary/90 disabled:opacity-60 transition-colors"
>
{isPending && action === "accept" ? <Loader2 size={16} className="animate-spin" /> : null}
Accetto {selectedTier ? `Opzione ${selectedTier}` : "scegli tier"}
</button>
<button
onClick={() => { setAction("reject"); handleReject(); }}
disabled={isPending}
className="px-6 py-3 border border-border rounded-xl text-muted-foreground hover:text-destructive hover:border-destructive/40 disabled:opacity-60 transition-colors text-sm"
>
Non procedo
</button>
</div>
<p className="text-xs font-mono text-muted-foreground">
{header.consultantName} · Accettazione formale valida {header.validityDays} giorni dalla data di invio.
</p>
</div>
);
}
@@ -0,0 +1,15 @@
type Props = { number: string; title: string; subtitle: string };
export function ChapterDivider({ number, title, subtitle }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-6">
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Capitolo</p>
<div className="space-y-2">
<p className="text-8xl font-light text-primary">{number}</p>
<div className="w-12 h-px bg-primary" />
</div>
<h2 className="text-5xl font-light text-foreground">{title}</h2>
<p className="text-lg text-muted-foreground max-w-lg">{subtitle}</p>
</div>
);
}
@@ -0,0 +1,69 @@
import type { ConsultantProfile } from "@/lib/proposal/profile";
import type { AssembledProposal } from "@/lib/proposal/assemble";
type Props = {
consultant: ConsultantProfile;
header: AssembledProposal["header"];
state: string;
selectedTier: string | null;
acceptedAt: Date | null;
};
export function ClosingSection({ consultant, header, state, selectedTier, acceptedAt }: Props) {
return (
<div className="min-h-screen flex flex-col justify-between px-16 py-24">
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Come si parte</p>
<div className="space-y-6">
{state === "accepted" && acceptedAt && selectedTier ? (
<div className="space-y-4">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-green-100 text-green-800 rounded-full text-sm font-medium">
Proposta accettata Opzione {selectedTier}
</div>
<h2 className="text-5xl font-light text-foreground">
Il kickoff è già <span className="text-primary">in calendario.</span>
</h2>
<p className="text-lg text-muted-foreground max-w-xl">
Hai scelto l&apos;Opzione {selectedTier} il{" "}
{acceptedAt.toLocaleDateString("it-IT", { day: "2-digit", month: "long", year: "numeric" })}.
Il consulente ti contatterà a breve per definire la data di kickoff.
</p>
</div>
) : state === "rejected" ? (
<div className="space-y-4">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-amber-100 text-amber-800 rounded-full text-sm font-medium">
Proposta non accettata
</div>
<h2 className="text-5xl font-light text-foreground">Grazie per il tuo tempo.</h2>
<p className="text-lg text-muted-foreground">
Se cambierai idea o vorrai discutere di altri progetti, siamo qui.
</p>
</div>
) : (
<div className="space-y-4">
<h2 className="text-5xl font-light text-foreground">
Il kickoff è già <span className="text-primary">in calendario.</span>
</h2>
<p className="text-lg text-muted-foreground max-w-xl">
Una volta confermata l&apos;opzione, la prima sessione viene pianificata entro la settimana successiva.
Tutto parte da .
</p>
</div>
)}
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">
{consultant.name} · {consultant.contact.email}
{consultant.contact.phone ? ` · ${consultant.contact.phone}` : ""}
</p>
{consultant.contact.website && (
<p className="text-xs font-mono text-muted-foreground">{consultant.contact.website}</p>
)}
<p className="text-xs font-mono text-muted-foreground mt-4">
Documento di Soluzione · {header.clientName} · {header.date}
</p>
</div>
</div>
);
}
@@ -0,0 +1,65 @@
import type { ComparisonRow } from "@/lib/proposal/schema";
import type { AssembledProposal } from "@/lib/proposal/assemble";
type Props = {
matrix: ComparisonRow[];
tiers: AssembledProposal["offer"]["tiers"];
};
export function ComparisonMatrixSection({ matrix, tiers }: Props) {
const tierLetters = tiers.map((t) => t.tierLetter);
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-8">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Riepilogo del lavoro</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Le voci che <span className="text-primary">compongono</span> ogni opzione.
</h2>
</div>
<div className="border border-border rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-muted border-b border-border">
<tr>
<th className="px-6 py-4 text-left font-mono text-xs tracking-widest text-muted-foreground uppercase">
Componente del lavoro
</th>
{tierLetters.map((l) => (
<th key={l} className="px-6 py-4 text-center font-mono text-xs tracking-widest text-muted-foreground uppercase w-24">
{l}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-border">
{matrix.map((row, i) => {
const values = [row.inA, row.inB, row.inC];
return (
<tr key={i} className="hover:bg-muted/30 transition-colors">
<td className="px-6 py-4">
<p className="font-medium text-foreground">{row.component}</p>
<p className="text-xs text-muted-foreground mt-0.5">{row.description}</p>
</td>
{values.slice(0, tierLetters.length).map((included, j) => (
<td key={j} className="px-6 py-4 text-center">
{included ? (
<span className="inline-block w-3 h-3 rounded-full bg-primary" />
) : (
<span className="inline-block w-3 h-3 rounded-full border-2 border-border" />
)}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
<p className="text-xs font-mono text-muted-foreground">
Le tre opzioni si compongono progressivamente. La C eredita tutto da B, che a sua volta eredita tutto da A.
</p>
</div>
);
}
@@ -0,0 +1,39 @@
import type { AssembledProposal } from "@/lib/proposal/assemble";
type Props = { header: AssembledProposal["header"] };
export function CoverSection({ header }: Props) {
return (
<div className="min-h-screen flex flex-col justify-between px-16 py-24">
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">
Documento di Soluzione
</p>
<div className="space-y-6">
<h1 className="text-6xl font-light leading-tight tracking-tight text-foreground">
{header.offerTitle.split(" ").map((word, i, arr) => {
const isLast = i === arr.length - 1;
return (
<span key={i}>
{isLast ? <span className="text-primary font-normal">{word}</span> : `${word} `}
</span>
);
})}
</h1>
{header.offerDescription && (
<p className="text-lg text-muted-foreground max-w-2xl">{header.offerDescription}</p>
)}
<p className="text-base text-foreground">
{header.clientName} × {header.consultantName}
</p>
<div className="flex items-center gap-6 text-xs font-mono text-muted-foreground">
<span>Data: {header.date}</span>
<span>·</span>
<span>Validità {header.validityDays} giorni</span>
</div>
</div>
<div />
</div>
);
}
@@ -0,0 +1,51 @@
import type { ProposalContent } from "@/lib/proposal/schema";
import { X } from "lucide-react";
type Props = { deliverables: ProposalContent["deliverables"] };
export function DeliverablesSection({ deliverables }: Props) {
return (
<div className="min-h-screen flex items-center px-16 py-24">
<div className="grid grid-cols-2 gap-8 w-full">
{/* Deliverable */}
<div className="border border-primary/20 bg-primary/5 rounded-xl p-8 space-y-4">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<div className="w-5 h-5 rounded-full bg-primary" />
</div>
<p className="text-xs font-mono tracking-widest text-primary uppercase">Deliverable · Output finali</p>
<h3 className="text-lg font-medium text-foreground">Quattro asset progressivi, uno per opzione.</h3>
<ul className="space-y-2">
{deliverables.deliverables.map((d, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-foreground">
<span className="text-primary mt-0.5"></span>
<span dangerouslySetInnerHTML={{ __html: d }} />
</li>
))}
</ul>
</div>
{/* Out of scope */}
<div className="border border-border rounded-xl p-8 space-y-4">
<div className="w-10 h-10 rounded-full border-2 border-border flex items-center justify-center text-muted-foreground">
</div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Confini · Ciò che non è incluso</p>
<div className="border border-border rounded-lg p-4 bg-muted/30 space-y-1">
<p className="text-sm font-medium text-foreground">
Ciò che non è esplicitamente indicato è da considerarsi escluso.{" "}
<span className="italic">A titolo di esempio:</span>
</p>
</div>
<ul className="space-y-2">
{deliverables.outOfScope.map((d, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
<X size={14} className="mt-0.5 shrink-0" />
<span>{d}</span>
</li>
))}
</ul>
</div>
</div>
</div>
);
}
@@ -0,0 +1,26 @@
import type { ConsultantProfile } from "@/lib/proposal/profile";
type Props = { consultant: ConsultantProfile };
export function FactsSection({ consultant }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-12">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Tre fatti</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Cosa porto al <span className="text-primary">tavolo.</span>
</h2>
</div>
<div className="grid grid-cols-3 gap-6">
{consultant.facts.map((fact, i) => (
<div key={i} className="border border-border rounded-xl p-8 space-y-4">
<p className="text-5xl font-light text-primary">{fact.value}</p>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">{fact.label}</p>
<p className="text-sm text-foreground font-medium">{fact.description}</p>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,30 @@
const CHAPTERS = [
{ n: "01", title: "Lo strategist", subtitle: "Chi vi affianca, con quale metodo e con quali precedenti." },
{ n: "02", title: "Il problema", subtitle: "I nodi rilevati nella discovery, ognuno chiamato per nome." },
{ n: "03", title: "La soluzione", subtitle: "L'elevazione speculare a ciascun nodo — con il risultato concreto." },
{ n: "04", title: "L'esecuzione", subtitle: "Scope, deliverable, ciò che non è incluso, timeline." },
{ n: "05", title: "Le opzioni", subtitle: "Tre intensità progressive con valore di mercato e pricing." },
];
export function IndexSection() {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-8">
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Sommario</p>
<h2 className="text-5xl font-light text-foreground">
Cinque <span className="text-primary">capitoli.</span>
</h2>
<div className="divide-y divide-border">
{CHAPTERS.map((c) => (
<div key={c.n} className="flex items-baseline justify-between py-5 gap-8">
<div className="flex items-baseline gap-6">
<span className="text-2xl font-mono text-primary">{c.n}</span>
<span className="text-xl text-foreground">{c.title}</span>
</div>
<span className="text-sm text-muted-foreground text-right max-w-xs">{c.subtitle}</span>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,68 @@
import type { ConsultantProfile } from "@/lib/proposal/profile";
import type { AssembledProposal } from "@/lib/proposal/assemble";
type Props = {
consultant: ConsultantProfile;
header: AssembledProposal["header"];
offer: AssembledProposal["offer"];
};
export function NextStepsSection({ consultant, header }: Props) {
// Calcola la data di scadenza (validityDays dalla data di generazione)
const expiryNote = `Valida ${header.validityDays} giorni dalla data di invio.`;
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-10">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Come procedere ora</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Tre passaggi <br />
<span className="text-primary">per attivare il lavoro.</span>
</h2>
</div>
<div className="space-y-4">
{consultant.nextSteps.map((step) => (
<div key={step.number} className="flex items-start gap-6">
<span className="text-xl font-mono text-primary shrink-0">{step.number}</span>
<p className="text-base text-foreground leading-relaxed">
{step.highlightedPart ? (
<>
{step.text.split(step.highlightedPart)[0]}
<strong>{step.highlightedPart}</strong>
{step.text.split(step.highlightedPart)[1]}
</>
) : (
step.text
)}
</p>
</div>
))}
</div>
<div className="border-t border-border pt-6 space-y-1">
<p className="text-sm font-medium text-foreground">
{consultant.name} · {consultant.contact.email}
{consultant.contact.phone ? ` · ${consultant.contact.phone}` : ""}
</p>
<p className="text-xs font-mono text-muted-foreground">{expiryNote}</p>
</div>
{/* Legale + accettazione */}
<div className="grid grid-cols-2 gap-4 text-xs text-muted-foreground">
<div className="border border-border rounded-lg p-4 space-y-1">
<p className="font-mono uppercase tracking-widest text-foreground text-[10px]">Termini legali</p>
<p>{consultant.legal.revisions}</p>
<p>{consultant.legal.ip}</p>
<p>{consultant.legal.nda}</p>
<p>Foro competente: {consultant.legal.forum}</p>
</div>
<div className="border border-border rounded-lg p-4 space-y-1">
<p className="font-mono uppercase tracking-widest text-foreground text-[10px]">Accettazione</p>
<p>{consultant.legal.acceptanceInstructions}</p>
<p className="mt-2 font-medium text-foreground">{expiryNote}</p>
</div>
</div>
</div>
);
}
@@ -0,0 +1,76 @@
import type { AssembledProposal } from "@/lib/proposal/assemble";
type Props = { tiers: AssembledProposal["offer"]["tiers"] };
function formatEur(val: string | null | undefined): string {
if (!val) return "—";
const n = parseFloat(val);
return `${n.toLocaleString("it-IT")}`;
}
export function PricingSection({ tiers }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-10">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Le tre opzioni</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Tre tappe, <span className="text-primary">un solo risultato.</span>
</h2>
</div>
<div className="grid grid-cols-3 gap-6">
{tiers.map((tier, i) => {
const isHighlighted = i === 1; // tier B highlighted di default
const price = tier.publicPrice ?? tier.servicesTotal;
const durationLabel = `${tier.durationMonths} ${tier.durationMonths === 1 ? "mese" : "mesi"}`;
return (
<div
key={tier.id}
className={`rounded-xl p-8 space-y-6 border ${
isHighlighted
? "border-primary bg-primary/5"
: "border-border bg-white"
}`}
>
<div className="flex items-start justify-between">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">
Opzione {tier.tierLetter} / {durationLabel}
</p>
<h3 className="text-xl font-medium text-foreground mt-1">{tier.publicName}</h3>
</div>
<span className="text-xs font-mono bg-primary/10 text-primary px-2 py-1 rounded">
{tier.tierLetter}
</span>
</div>
<div className="space-y-1">
<p className="text-3xl font-light text-primary">{formatEur(price)}</p>
<p className="text-xs text-muted-foreground">
{tier.durationMonths > 1 ? `${tier.durationMonths} mesi` : "pagamento unico"}
</p>
</div>
{/* Servizi inclusi */}
{tier.services.length > 0 && (
<ul className="space-y-1.5">
{tier.services.map((s) => (
<li key={s.id} className="flex items-start gap-2 text-sm text-foreground">
<span className="text-primary mt-0.5"></span>
<span>{s.name}</span>
</li>
))}
</ul>
)}
</div>
);
})}
</div>
<p className="text-xs font-mono text-muted-foreground">
Le tre opzioni si compongono progressivamente. La C eredita tutto da B, che a sua volta eredita tutto da A.
</p>
</div>
);
}
@@ -0,0 +1,34 @@
import type { ProblemNode } from "@/lib/proposal/schema";
type Props = { problem: ProblemNode };
export function ProblemNodeSection({ problem }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-8 max-w-4xl">
<div>
<p className="text-xs font-mono tracking-widest text-destructive uppercase">
Problema {problem.id} · {problem.subtitle}
</p>
<h2 className="text-5xl font-light text-foreground mt-4 leading-tight">
{problem.title.split(problem.title.split(" ").slice(-2).join(" "))[0]}
<span className="text-destructive">{problem.title.split(" ").slice(-2).join(" ")}</span>
</h2>
<p className="text-base text-muted-foreground mt-4 leading-relaxed max-w-2xl">
{problem.body}
</p>
</div>
<div className="border border-destructive/30 bg-destructive/5 rounded-xl p-6 space-y-2">
<p className="text-xs font-mono tracking-widest text-destructive uppercase">Il rischio</p>
<p className="text-sm text-foreground leading-relaxed">{problem.risk}</p>
</div>
<div className="border-l-2 border-border pl-6 space-y-1">
<p className="text-sm text-muted-foreground italic leading-relaxed">
&ldquo;{problem.quote}&rdquo;
</p>
<p className="text-xs font-mono text-muted-foreground">{problem.quoteAttribution}</p>
</div>
</div>
);
}
@@ -0,0 +1,39 @@
import type { ProposalContent } from "@/lib/proposal/schema";
import { CheckCircle2 } from "lucide-react";
type Props = { scope: ProposalContent["scope"] };
export function ScopeSection({ scope }: Props) {
return (
<div className="min-h-screen flex items-center px-16 py-24">
<div className="grid grid-cols-2 gap-12 w-full">
{/* Scope */}
<div className="border border-primary/20 bg-primary/5 rounded-xl p-8 space-y-4">
<div className="w-10 h-10 rounded-full border border-primary flex items-center justify-center">
<div className="w-5 h-5 rounded-full bg-primary/30" />
</div>
<p className="text-xs font-mono tracking-widest text-primary uppercase">Scope · Capitolo del lavoro</p>
<h3 className="text-xl font-medium text-foreground">{scope.scopeTitle}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{scope.scopeBody}</p>
</div>
{/* Obiettivi */}
<div className="border border-border bg-muted/30 rounded-xl p-8 space-y-4">
<div className="w-10 h-10 rounded-full border border-border flex items-center justify-center">
<div className="w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-b-[10px] border-b-foreground" />
</div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Obiettivi · Punti d&apos;arrivo</p>
<h3 className="text-xl font-medium text-foreground">Quattro risultati concreti, misurabili al termine.</h3>
<ul className="space-y-2">
{scope.objectives.map((obj, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-foreground">
<CheckCircle2 size={14} className="text-primary mt-0.5 shrink-0" />
<span dangerouslySetInnerHTML={{ __html: obj }} />
</li>
))}
</ul>
</div>
</div>
</div>
);
}
@@ -0,0 +1,35 @@
import type { SolutionNode } from "@/lib/proposal/schema";
type Props = { solution: SolutionNode };
export function SolutionNodeSection({ solution }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-8 max-w-4xl">
<div>
<p className="text-xs font-mono tracking-widest text-primary uppercase">
{solution.subtitle}
</p>
<h2 className="text-5xl font-light text-foreground mt-4 leading-tight">
{solution.title}
</h2>
</div>
<div className="border border-primary/20 bg-primary/5 rounded-xl p-6 space-y-2">
<p className="text-xs font-mono tracking-widest text-primary uppercase">La trasformazione</p>
<p className="text-sm text-foreground leading-relaxed">{solution.transformation}</p>
</div>
<div className="border border-border rounded-xl p-6 space-y-3">
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Attraverso cosa</p>
<ul className="space-y-2">
{solution.throughWhat.map((item, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-foreground">
<span className="text-primary mt-0.5"></span>
<span dangerouslySetInnerHTML={{ __html: item }} />
</li>
))}
</ul>
</div>
</div>
);
}
@@ -0,0 +1,42 @@
import type { ProposalContent } from "@/lib/proposal/schema";
type Props = { synthesis: ProposalContent["solutionSynthesis"] };
export function SolutionSynthesisSection({ synthesis }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-10">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Sintesi della soluzione</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Una direzione, <span className="text-success">cinque elevazioni.</span>
</h2>
<p className="text-base text-muted-foreground mt-4 max-w-2xl">
Da un&apos;unica decisione strategica a monte si declinano cinque movimenti specifici. Il sistema operativo che resta al team interno.
</p>
</div>
<div className="space-y-6">
{/* Direzione */}
<div className="border border-primary/20 bg-primary/5 rounded-xl p-6 text-center mx-auto max-w-lg">
<p className="text-xs font-mono tracking-widest text-primary uppercase mb-2">La direzione</p>
<p className="text-sm font-medium text-foreground">{synthesis.direction}</p>
</div>
{/* Connettori */}
<div className="flex justify-center">
<div className="w-px h-8 bg-primary/30" />
</div>
{/* Elevazioni */}
<div className="flex items-center justify-between gap-4">
{synthesis.elevations.map((e, i) => (
<div key={e.id} className="flex-1 border border-border rounded-lg px-4 py-3 text-center">
<p className="text-xs font-mono text-muted-foreground mb-1">0{i + 1} </p>
<p className="text-sm font-medium text-foreground">{e.label}</p>
</div>
))}
</div>
</div>
</div>
);
}
@@ -0,0 +1,43 @@
import type { ProposalContent } from "@/lib/proposal/schema";
type Props = { stages: ProposalContent["stagesRecap"]["stages"] };
export function StagesRecapSection({ stages }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-10">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Recap del progresso</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Tre tappe verso <span className="text-primary">la trasformazione.</span>
</h2>
<p className="text-base text-muted-foreground mt-4 max-w-3xl">
Le tre opzioni non sono pacchetti scollegati: sono{" "}
<strong>tre tappe progressive</strong> dello stesso percorso. Ognuna costruisce sopra la precedente.
Insieme portano alla <strong>trasformazione totale</strong>.
</p>
</div>
<div className="flex items-start gap-4">
{stages.map((stage, i) => (
<div key={i} className="flex-1">
<div className="border border-primary/20 rounded-xl p-6 space-y-3">
<p className="text-4xl font-light text-primary">{stage.number}</p>
<p className="text-xs font-mono tracking-widest text-primary uppercase">{stage.label}</p>
<h3 className="text-xl font-medium text-foreground">{stage.headline}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{stage.body}</p>
<div className="border border-border rounded-md px-3 py-2 bg-muted/30">
<p className="text-xs font-mono text-muted-foreground">
{stage.deliverable}
</p>
</div>
</div>
{i < stages.length - 1 && (
<div className="hidden" /* arrow between stages — gestito dal grid */ />
)}
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,50 @@
import type { ConsultantProfile } from "@/lib/proposal/profile";
import { CheckCircle2 } from "lucide-react";
type Props = { consultant: ConsultantProfile };
export function StrategistSection({ consultant }: Props) {
return (
<div className="min-h-screen flex items-center px-16 py-24">
<div className="grid grid-cols-3 gap-12 w-full">
{/* Foto */}
<div className="flex flex-col items-center justify-center">
<div className="w-48 h-64 rounded-xl border-2 border-primary bg-muted flex items-center justify-center overflow-hidden">
{consultant.photoUrl ? (
<img src={consultant.photoUrl} alt={consultant.name} className="w-full h-full object-cover" />
) : (
<span className="text-muted-foreground text-sm">{consultant.name[0]}</span>
)}
</div>
</div>
{/* Bio */}
<div className="col-span-2 space-y-6">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">
Strategist · Brand &amp; Business
</p>
<h2 className="text-4xl font-light text-foreground mt-2">{consultant.name}</h2>
</div>
<p className="text-base font-medium text-foreground leading-relaxed">{consultant.bio}</p>
<ul className="space-y-2">
{consultant.credentials.map((c, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-muted-foreground">
<CheckCircle2 size={14} className="text-primary mt-0.5 shrink-0" />
<span dangerouslySetInnerHTML={{ __html: c }} />
</li>
))}
</ul>
<div className="p-4 bg-muted rounded-lg text-sm text-foreground leading-relaxed border-l-2 border-primary">
{consultant.contact.website && (
<span className="font-mono text-xs text-muted-foreground">{consultant.contact.website}</span>
)}
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,51 @@
import type { ProposalContent } from "@/lib/proposal/schema";
type Props = {
synthesis: ProposalContent["problemSynthesis"];
type: "problem";
};
export function SynthesisDiagramSection({ synthesis }: Props) {
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-10">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Sintesi del problema</p>
<h2 className="text-5xl font-light text-foreground mt-4">
Cinque nodi, <span className="text-destructive">una sola radice.</span>
</h2>
<p className="text-base text-muted-foreground mt-4 max-w-2xl">
I cinque nodi convergono su un&apos;unica domanda strategica non ancora risolta. Sciolto quel nodo, gli altri trovano il proprio posto.
</p>
</div>
{/* Diagramma semplice: nodi → radice comune */}
<div className="space-y-8">
{/* Nodi */}
<div className="flex items-center justify-between gap-4">
{synthesis.nodeLabels.map((label, i) => (
<div
key={i}
className="flex-1 border border-border rounded-lg px-4 py-3 text-center"
>
<p className="text-xs font-mono text-muted-foreground mb-1">0{i + 1} </p>
<p className="text-sm font-medium text-foreground">{label}</p>
</div>
))}
</div>
{/* Connettori */}
<div className="flex justify-center">
<div className="w-px h-8 bg-destructive/30" />
</div>
{/* Radice */}
<div className="mx-auto max-w-lg border border-destructive/30 bg-destructive/5 rounded-xl p-6 text-center">
<p className="text-xs font-mono tracking-widest text-destructive uppercase mb-2">
La radice comune
</p>
<p className="text-sm font-medium text-foreground">{synthesis.rootCause}</p>
</div>
</div>
</div>
);
}
@@ -0,0 +1,44 @@
import type { Testimonial } from "@/lib/proposal/profile";
type Props = {
testimonials: Testimonial[];
page: number;
totalPages: number;
};
export function TestimonialsSection({ testimonials, page, totalPages }: Props) {
const perPage = 9;
const slice = testimonials.slice((page - 1) * perPage, page * perPage);
return (
<div className="min-h-screen flex flex-col px-16 py-24 space-y-8">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">
Pagina {page} di {totalPages} · Cosa dicono di me
</p>
<h2 className="text-4xl font-light text-foreground mt-4">
Una selezione dalle <span className="text-primary">testimonianze</span> di chi vi ha preceduto.
</h2>
</div>
<div className="grid grid-cols-3 gap-4 flex-1">
{slice.map((t, i) => (
<div key={i} className="border border-border rounded-lg p-5 space-y-3 flex flex-col">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-sm font-medium text-primary shrink-0">
{t.name[0]}
</div>
<div>
<p className="text-sm font-medium text-foreground leading-tight">{t.name}</p>
<p className="text-xs font-mono text-primary">{t.role}</p>
</div>
</div>
<p className="text-sm text-muted-foreground italic leading-relaxed flex-1">
&ldquo;{t.quote}&rdquo;
</p>
</div>
))}
</div>
</div>
);
}
@@ -0,0 +1,88 @@
import type { ProposalContent } from "@/lib/proposal/schema";
type Props = { timeline: ProposalContent["timeline"] };
const COLOR_MAP = {
cyan: { bg: "bg-cyan-400", text: "text-cyan-900" },
purple: { bg: "bg-violet-400", text: "text-violet-900" },
green: { bg: "bg-emerald-400",text: "text-emerald-900" },
};
export function TimelineSection({ timeline }: Props) {
const weeks = Array.from({ length: timeline.totalWeeks }, (_, i) => i + 1);
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-10">
<div>
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">Timeline</p>
<h2 className="text-5xl font-light text-foreground mt-4">
{timeline.totalWeeks} settimane di <span className="text-primary">progressione.</span>
</h2>
</div>
<div className="space-y-4">
{/* Header settimane */}
<div className="grid gap-2" style={{ gridTemplateColumns: `140px repeat(${timeline.totalWeeks}, 1fr)` }}>
<div />
{weeks.map((w) => (
<div key={w} className="text-center text-xs font-mono text-muted-foreground">W{w}</div>
))}
</div>
{/* Righe fase */}
{timeline.phases.map((phase, i) => {
const colors = COLOR_MAP[phase.color];
return (
<div
key={i}
className="grid gap-2 items-center"
style={{ gridTemplateColumns: `140px repeat(${timeline.totalWeeks}, 1fr)` }}
>
<div className="text-xs text-muted-foreground font-mono truncate pr-2">
{i === 0 ? "OPZIONE A" : i === 1 ? "OPZIONE B" : "OPZIONE C"}
<br />
<span className="text-[10px] text-muted-foreground/60">
{phase.endWeek - phase.startWeek + 1} settimane
</span>
</div>
{weeks.map((w) => {
const active = w >= phase.startWeek && w <= phase.endWeek;
const isFirst = w === phase.startWeek;
const isLast = w === phase.endWeek;
return (
<div
key={w}
className={`h-10 ${active ? `${colors.bg} ${isFirst ? "rounded-l-md" : ""} ${isLast ? "rounded-r-md" : ""}` : ""} flex items-center ${isFirst ? "justify-start pl-3" : ""}`}
>
{isFirst && (
<span className={`text-xs font-medium ${colors.text} whitespace-nowrap`}>
{phase.label}
</span>
)}
</div>
);
})}
</div>
);
})}
</div>
{/* Legenda */}
<div className="flex items-center gap-6">
{timeline.phases.map((phase, i) => {
const colors = COLOR_MAP[phase.color];
return (
<div key={i} className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-sm ${colors.bg}`} />
<span className="text-xs text-muted-foreground">{phase.label}</span>
</div>
);
})}
</div>
<p className="text-xs font-mono text-muted-foreground">
{timeline.disclaimer}
</p>
</div>
);
}
@@ -0,0 +1,33 @@
import type { AssembledProposal } from "@/lib/proposal/assemble";
import type { ProposalContent } from "@/lib/proposal/schema";
type Props = {
vision: ProposalContent["vision"];
header: AssembledProposal["header"];
};
export function VisionSection({ vision, header }: Props) {
const words = vision.headline.split(" ");
const mid = Math.floor(words.length / 2);
return (
<div className="min-h-screen flex flex-col justify-center px-16 py-24 space-y-8">
<p className="text-xs font-mono tracking-widest text-muted-foreground uppercase">
Dove andiamo
</p>
<h2 className="text-5xl font-light leading-tight tracking-tight text-foreground">
{words.slice(0, mid).join(" ")}{" "}
<span className="text-primary">{words.slice(mid).join(" ")}</span>
</h2>
<p className="text-lg text-muted-foreground max-w-2xl leading-relaxed">
{vision.body}
</p>
<p className="text-sm text-muted-foreground">
{header.clientName} × {header.consultantName}
</p>
</div>
);
}