Files
manyangles/apps/web/src/app/page.tsx
T

195 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 whats 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>
);
}