195 lines
9.7 KiB
TypeScript
195 lines
9.7 KiB
TypeScript
import Link from "next/link";
|
||
import {
|
||
ArrowRightIcon,
|
||
CheckCircle2Icon,
|
||
DownloadIcon,
|
||
ImagesIcon,
|
||
ShieldCheckIcon,
|
||
UploadIcon,
|
||
} from "lucide-react";
|
||
import { createServerCaller } from "@/trpc/server";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { BrandMark } from "@/components/brand-mark";
|
||
import { formatEventDate } from "@/lib/utils";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
|
||
const features = [
|
||
{
|
||
number: "01",
|
||
icon: UploadIcon,
|
||
title: "One link for everyone",
|
||
body: "Guests upload from any phone. No account, app, or shared camera roll required.",
|
||
},
|
||
{
|
||
number: "02",
|
||
icon: ShieldCheckIcon,
|
||
title: "You decide what’s public",
|
||
body: "Every photo lands in a private review queue before it reaches the shared gallery.",
|
||
},
|
||
{
|
||
number: "03",
|
||
icon: DownloadIcon,
|
||
title: "Originals stay original",
|
||
body: "Keep the full-quality files while Manyangles makes the gallery fast for everyone else.",
|
||
},
|
||
] as const;
|
||
|
||
function AlbumPreview() {
|
||
return (
|
||
<div className="reveal-2 relative mx-auto w-full max-w-xl pb-8 sm:pb-10">
|
||
<Card className="album-preview overflow-hidden rounded-2xl py-0">
|
||
<CardHeader className="flex min-h-18 grid-cols-none flex-row items-center justify-between gap-4 border-b px-5 py-4 sm:px-6">
|
||
<div className="flex min-w-0 items-center gap-3">
|
||
<span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary-foreground/15 text-primary-foreground ring-1 ring-primary-foreground/25">
|
||
<ImagesIcon aria-hidden="true" />
|
||
</span>
|
||
<div className="min-w-0">
|
||
<CardTitle className="truncate text-base font-semibold">Saturday album</CardTitle>
|
||
<CardDescription>Brooklyn · 84 photos</CardDescription>
|
||
</div>
|
||
</div>
|
||
<Badge variant="secondary" className="gap-1.5">
|
||
<span className="size-1.5 rounded-full bg-current" aria-hidden="true" />
|
||
Live
|
||
</Badge>
|
||
</CardHeader>
|
||
<CardContent className="p-2.5 sm:p-3">
|
||
<div className="grid h-[20rem] grid-cols-3 grid-rows-3 gap-2 sm:h-[25rem] sm:gap-2.5" aria-hidden="true">
|
||
<div className="memory-photo memory-photo-one row-span-2" />
|
||
<div className="memory-photo memory-photo-two col-span-2" />
|
||
<div className="memory-photo memory-photo-three" />
|
||
<div className="memory-photo memory-photo-four row-span-2" />
|
||
<div className="memory-photo memory-photo-five" />
|
||
<div className="memory-photo memory-photo-six" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<div className="absolute right-3 bottom-0 left-3 flex items-center gap-3 rounded-xl border border-border/80 bg-card/90 p-3 shadow-xl backdrop-blur-xl sm:right-8 sm:left-auto sm:min-w-64 sm:p-4">
|
||
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
||
<CheckCircle2Icon aria-hidden="true" />
|
||
</span>
|
||
<span className="min-w-0">
|
||
<span className="block text-sm font-semibold">12 photos added</span>
|
||
<span className="block truncate text-xs text-muted-foreground">Original quality · ready to review</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default async function HomePage() {
|
||
const caller = await createServerCaller();
|
||
const [listed, viewer] = await Promise.all([
|
||
caller.event.listed(),
|
||
caller.viewer.me(),
|
||
]);
|
||
const demoEvent = listed[0];
|
||
|
||
return (
|
||
<main>
|
||
<section className="page-pad mx-auto grid min-h-[calc(100svh-4rem)] w-full max-w-6xl items-center gap-12 py-14 sm:py-20 lg:grid-cols-[0.88fr_1.12fr] lg:gap-16 lg:py-24">
|
||
<div className="reveal flex max-w-xl flex-col gap-6">
|
||
<div className="flex items-center gap-3 text-xs font-bold tracking-[0.16em] text-primary uppercase">
|
||
<span className="h-px w-9 bg-primary" aria-hidden="true" />
|
||
Shared albums for real life
|
||
</div>
|
||
<h1 className="font-display text-[clamp(3.4rem,7vw,5.8rem)] leading-[0.92] font-semibold tracking-[-0.055em]">
|
||
<span className="block whitespace-nowrap">Every angle.</span>
|
||
<span className="block text-primary">One shared album.</span>
|
||
</h1>
|
||
<p className="max-w-lg text-lg leading-7 text-muted-foreground sm:text-xl sm:leading-8">
|
||
Give guests one link to add their photos. Review everything in one place, share the best, and keep every original.
|
||
</p>
|
||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
||
{viewer.openSignup ? (
|
||
<Button asChild size="lg" className="tap-target w-full sm:w-auto">
|
||
<Link href="/sign-up">Create an event<ArrowRightIcon data-icon="inline-end" aria-hidden="true" /></Link>
|
||
</Button>
|
||
) : (
|
||
<Button asChild size="lg" className="tap-target w-full sm:w-auto"><Link href="/sign-up">Use an invite</Link></Button>
|
||
)}
|
||
{demoEvent ? (
|
||
<Button asChild variant="outline" size="lg" className="tap-target w-full sm:w-auto"><Link href={`/e/${demoEvent.slug}`}>Open a gallery</Link></Button>
|
||
) : (
|
||
<Button asChild variant="outline" size="lg" className="tap-target w-full sm:w-auto"><Link href="/sign-in">Sign in</Link></Button>
|
||
)}
|
||
</div>
|
||
<div className="flex flex-wrap gap-x-4 gap-y-2 border-t border-border/80 pt-4 text-[0.6875rem] font-bold tracking-[0.08em] text-muted-foreground uppercase">
|
||
<span>No app required</span><span>Original quality</span><span>Private by default</span>
|
||
</div>
|
||
</div>
|
||
<AlbumPreview />
|
||
</section>
|
||
|
||
<section className="border-y border-border/80 bg-card/80 backdrop-blur-sm" aria-labelledby="how-it-works">
|
||
<div className="page-pad mx-auto w-full max-w-6xl py-14 sm:py-20">
|
||
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">The whole handoff, handled</p>
|
||
<h2 id="how-it-works" className="mt-3 max-w-2xl text-4xl font-semibold tracking-[-0.035em] sm:text-5xl">
|
||
Built for the photos that never make it out of the group chat.
|
||
</h2>
|
||
<div className="mt-8 grid gap-3 md:grid-cols-3">
|
||
{features.map((feature) => {
|
||
const Icon = feature.icon;
|
||
return (
|
||
<Card key={feature.title} className="feature-panel overflow-hidden bg-background/60 shadow-none">
|
||
<CardHeader>
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<span className="flex size-10 items-center justify-center rounded-lg bg-primary text-primary-foreground"><Icon aria-hidden="true" /></span>
|
||
<span className="font-mono text-xs font-bold tracking-widest text-muted-foreground">{feature.number}</span>
|
||
</div>
|
||
<CardTitle className="text-xl font-semibold">{feature.title}</CardTitle>
|
||
<CardDescription className="leading-6">{feature.body}</CardDescription>
|
||
</CardHeader>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{listed.length > 0 ? (
|
||
<section className="page-pad mx-auto flex w-full max-w-6xl flex-col gap-6 py-14 sm:py-20">
|
||
<div className="flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
|
||
<div>
|
||
<p className="text-xs font-bold tracking-[0.14em] text-primary uppercase">Open now</p>
|
||
<h2 className="mt-2 text-4xl font-semibold tracking-[-0.035em]">Public galleries</h2>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground">Join the day from wherever you are.</p>
|
||
</div>
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{listed.map((event) => (
|
||
<Link key={event.id} href={`/e/${event.slug}`} className="group block min-w-0 rounded-xl focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none">
|
||
<Card className="h-full transition-transform duration-200 ease-out group-hover:-translate-y-0.5 group-hover:shadow-md motion-reduce:transition-none motion-reduce:group-hover:translate-y-0">
|
||
{event.bannerUrl ? (
|
||
<img src={event.bannerUrl} alt="" width={800} height={300} loading="lazy" decoding="async"
|
||
className="aspect-[8/3] w-full object-cover" />
|
||
) : null}
|
||
<CardHeader>
|
||
{!event.bannerUrl ? <div className="mb-2 flex size-10 items-center justify-center rounded-lg bg-primary text-primary-foreground"><BrandMark className="size-5" /></div> : null}
|
||
<CardTitle className="truncate text-xl font-semibold tracking-tight">{event.title}</CardTitle>
|
||
<CardDescription>{formatEventDate(event.startsAt) ?? "Open gallery"}</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="flex items-center gap-1 text-sm font-medium text-primary">View gallery<ArrowRightIcon aria-hidden="true" /></CardContent>
|
||
</Card>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
) : null}
|
||
|
||
<footer className="border-t border-border/80 bg-foreground text-background">
|
||
<div className="page-pad mx-auto flex w-full max-w-6xl items-center justify-between gap-4 py-7 text-xs text-background/65">
|
||
<span className="font-heading text-lg font-semibold text-background">Manyangles</span><span>Keep the moment, not the compression.</span>
|
||
</div>
|
||
</footer>
|
||
</main>
|
||
);
|
||
}
|