ZeroToShip

Docs/Extra

// extra

Componenti bonus

Oltre ai componenti della landing standard, nel repo c'è un secondo strato di roba: illustrazioni disegnate a mano, una sezione founder, un terminale animato che nessuna pagina usa ancora, e tre cartelle vuote che servono a provare varianti senza rompere la home. Nessuno di questi pezzi è obbligatorio — sono lì perché ti servano il giorno in cui la landing di default non ti basta più.

1. FounderNote

components/FounderNote.tsx è la sezione “due parole dal founder”: ritratto disegnato a sinistra, lettera in serif a destra, tre numeri sotto. È già montata in app/page.tsx tra <FileExplorer /> e <Pricing />, con id="proof" così puoi puntarci un link.

Tutto il contenuto sta in cima al file, in due array. I numeri:

components/FounderNote.tsx
tsx
const stats = [
  { value: "1h30", label: "build più veloce con questo repo — in diretta davanti alla telecamera" },
  { value: "200k", label: "persone hanno visto quella build sui social" },
  { value: "4 giorni", label: "dall'idea al primo ricavo, più di una volta" },
];

// Add real quotes from early buyers here — the section stays hidden while empty.
const testimonials: { quote: string; name: string; role: string }[] = [];

Il dettaglio furbo è testimonials: la griglia delle citazioni è avvolta in un testimonials.length > 0, quindi finché l'array è vuoto la sezione semplicemente non esiste. Il giorno che hai le prime recensioni vere le aggiungi lì e compaiono da sole — niente placeholder finti in produzione.

Il volto è <FounderFace /> da components/drawn: è un disegno generico, sostituiscilo con una tua foto dentro un <Image /> quando ne hai voglia.

2. HeroTerminal e TerminalWindow

Questi due non sono importati da nessuna pagina: sono un visual alternativo per l'hero, pronto ma in panchina. La home oggi usa <FolderOrbit />.

components/TerminalWindow.tsx è il guscio: un server component senza logica, con la barra dei tre pallini e il titolo. Prende tre prop:

components/TerminalWindow.tsx
tsx
const TerminalWindow = ({
  title = "zsh — zerotoship",
  children,
  className = "",
}: {
  title?: string;
  children: ReactNode;
  className?: string;
}) => { /* ... */ };

components/HeroTerminal.tsx è il contenuto animato, ed è un client component perché usa useState e useEffect. Scrive a macchina il comando git clone zerotoship, poi fa comparire una riga alla volta i cinque moduli, poi la riga finale. Il contenuto è tutto in due costanti in cima:

components/HeroTerminal.tsx
tsx
const COMMAND = "git clone zerotoship";

const LINES = [
  { label: "auth", note: "(Supabase + Google login)" },
  { label: "payments", note: "(Stripe checkout + webhooks)" },
  { label: "database", note: "(Supabase, ready to go)" },
  { label: "emails", note: "(Resend)" },
  { label: "SEO", note: "(meta tags + sitemap + blog)" },
] as const;

Due cose fatte bene che vale la pena non rompere quando ci metti mano: controlla prefers-reduced-motion e, se l'utente ha chiesto meno animazioni, salta direttamente allo stato finale; e la cleanup dello useEffect cancella tutti i setTimeout, quindi non ti restano timer appesi quando cambi pagina.

Un avvertimento onesto: TerminalWindow è stilizzato con le classi DaisyUI (bg-base-200, text-primary, border-base-content/10) e non con i token del tema brutalist (--cream, --ink, .card-brutal) che usa il resto della landing. Se lo infili nella home così com'è, stona. Cambia le classi prima.

3. components/drawn/

Dieci illustrazioni SVG scritte a mano come componenti React, più un index.ts che fa da barrel: le importi tutte da @/components/drawn. Nessuna dipendenza esterna, nessun file immagine, niente Figma.

components/MiaSezione.tsx
tsx
import { DrawnArrow, Squiggle, WobbleBorder } from "@/components/drawn";

<WobbleBorder className="p-8">
  <h2>Contenuto dentro un bordo tremolante</h2>
</WobbleBorder>

<Squiggle className="w-40 h-3" color="var(--brick)" />
<DrawnArrow direction="down-right" className="w-14 h-20" />

Si dividono in due famiglie. Le primitive, che prendono className, color e strokeWidth e usano currentColor di default, quindi ereditano l'inchiostro dal genitore:

  • Squiggle — la linea ondulata da mettere sotto una headline. Per la sottolineatura inline conviene la utility .squiggle di globals.css: questo componente serve quando la vuoi isolata.
  • DrawnArrow — freccia storta. La prop direction accetta down, up, left, right, down-left, down-right e ruota l'SVG di conseguenza.
  • FloppyIcon — floppy disk in linea, buono per un microcopy nostalgico.
  • FounderFace — il volto usato da FounderNote.
  • TiredMaker — il maker esausto, pensato come centro della sezione Problem.

E i contenitori, che prendono dei figli o dei dati e costruiscono una scena. Questi usano direttamente i token del tema (--ink, --cream, --kraft, --mustard, --brick):

  • WobbleBorder — avvolge i figli in un bordo disegnato. Prop stroke (default var(--ink)) e strokeWidth. Padding e sfondo li gestisci tu con className.
  • MacFrame — un Macintosh vintage. I figli finiscono dentro lo “schermo”, posizionato in percentuale sopra l'SVG.
  • CardboardBox — scatola di cartone aperta con delle etichette che spuntano fuori. Vuole tags: { icon, label, color? }[], fino a otto, più un toLine (default "your saas").
  • AllInOne — cerchio storto che racchiude sei etichette, con una caption (default "one repo").
  • FolderOrbit — cartella al centro e otto etichette che le orbitano intorno con ritmi diversi. È quello che vedi nell'hero: components/Hero.tsx gli passa l'array routes.

Nei tre contenitori a etichette (CardboardBox, AllInOne, FolderOrbit) ogni tag accetta un color tra cream, mustard e brick; il default è cream. Le posizioni sono array fissi dentro ogni file: se ti serve una disposizione diversa, quello è il punto da toccare.

components/Hero.tsx
tsx
import { FolderOrbit } from "@/components/drawn";

const routes = [
  { icon: "🔐", label: "/auth" },
  { icon: "💳", label: "/payments", color: "mustard" as const },
  { icon: "🗄️", label: "/database" },
  { icon: "🤖", label: ".cursorrules", color: "brick" as const },
];

<FolderOrbit routes={routes} />

4. components/heroes/ e components/problem-variants/

Occhio, perché su questo la documentazione di solito mente: queste due cartelle esistono ma sono vuote. Non contengono nessun file, e siccome git non traccia le cartelle vuote potresti non vederle nemmeno dopo un clone pulito.

Sono uno slot di lavoro, non una libreria. L'idea è: quando vuoi provare un hero diverso non riscrivi components/Hero.tsx — ne fai una copia lì dentro, la monti in app/page.tsx al posto dell'originale, e se non ti convince cancelli il file invece di fare git revert. Stessa cosa per components/Problem.tsx e problem-variants/.

terminal
bash
cp components/Hero.tsx components/heroes/HeroTerminale.tsx
# poi sostituisci <FolderOrbit /> con <HeroTerminal /> dentro la copia
app/page.tsx
tsx
// import Hero from "@/components/Hero";
import Hero from "@/components/heroes/HeroTerminale";

Se una variante vince, spostala su components/Hero.tsx e svuota la cartella. Non lasciarci dentro sei versioni: fra un mese non ricordi quale è quella buona.

5. La route /preview/heroes

Anche app/preview/heroes/ è una cartella vuota: senza un page.tsx dentro, la rotta /preview/heroes oggi risponde 404. È il posto previsto per una pagina di confronto — tutte le varianti impilate una sotto l'altra, così le guardi affiancate invece di ricaricare la home ogni volta.

app/preview/heroes/page.tsx
tsx
import Hero from "@/components/Hero";
import HeroTerminale from "@/components/heroes/HeroTerminale";

const VARIANTI = [
  { nome: "A — FolderOrbit (attuale)", Component: Hero },
  { nome: "B — Terminale animato", Component: HeroTerminale },
];

export default function PreviewHeroes() {
  return (
    <main style={{ background: "var(--cream)" }}>
      {VARIANTI.map(({ nome, Component }) => (
        <section key={nome}>
          <p className="font-mono text-sm px-6 py-3" style={{ color: "var(--brick)" }}>
            {"//"} {nome}
          </p>
          <Component />
          <div className="divider-drawn my-6" />
        </section>
      ))}
    </main>
  );
}

Due note prima di dimenticartela online. Il middleware.ts intercetta anche questa rotta — rinfresca solo la sessione Supabase, non la blocca — quindi la pagina resta pubblica: chiunque indovini l'URL la vede. E finisce nel sitemap come qualsiasi altra pagina, a meno che tu non la aggiunga all'array exclude di next-sitemap.config.js. La soluzione più pulita è cancellare la cartella prima del deploy.