Rebrand product as Manyangles and add Authentik

This commit is contained in:
2026-09-08 09:17:25 -04:00
parent 27e2f196eb
commit f5702caaea
37 changed files with 630 additions and 237 deletions
+157 -52
View File
@@ -1,7 +1,15 @@
import Link from "next/link";
import { CameraIcon } from "lucide-react";
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 {
@@ -12,73 +20,170 @@ import {
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 = await caller.event.listed();
const viewer = await caller.viewer.me();
const [listed, viewer] = await Promise.all([
caller.event.listed(),
caller.viewer.me(),
]);
const demoEvent = listed[0];
return (
<main className="page-pad mx-auto flex min-h-[calc(100dvh-3.5rem)] w-full max-w-5xl flex-col gap-12 py-10 sm:min-h-[calc(100dvh-4rem)] sm:py-16">
<div className="reveal flex max-w-2xl flex-col gap-5">
<p className="text-xs font-medium tracking-[0.22em] text-primary uppercase sm:text-sm">
Photos from the day
</p>
<h1 className="font-display text-4xl leading-[1.1] sm:text-6xl">
Keep the originals. Share the day.
</h1>
<p className="max-w-xl text-base text-muted-foreground sm:text-lg">
Guests upload from a link. You choose what the public sees. Full-quality
files stay yours.
</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">
<CameraIcon data-icon="inline-start" aria-hidden="true" />
Host an event
</Link>
</Button>
) : (
<Button asChild size="lg" className="tap-target w-full sm:w-auto">
<Link href="/sign-up">Have an invite?</Link>
</Button>
)}
<Button asChild variant="outline" size="lg" className="tap-target w-full sm:w-auto">
<Link href="/sign-in">Sign in</Link>
</Button>
<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]">
Every angle.<br /><span className="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>
</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="reveal-2 flex flex-col gap-4">
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">Open galleries</h2>
<div className="grid gap-3 sm:grid-cols-2">
<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"
>
<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">
<CardHeader>
<div className="mb-2 flex size-10 items-center justify-center rounded-full bg-accent text-primary">
<BrandMark className="size-5" />
</div>
<CardTitle className="truncate text-xl font-semibold tracking-tight">
{event.title}
</CardTitle>
<CardDescription>
{formatEventDate(event.startsAt) ?? "Open gallery"}
</CardDescription>
<div className="mb-2 flex size-10 items-center justify-center rounded-lg bg-primary text-primary-foreground"><BrandMark className="size-5" /></div>
<CardTitle className="truncate text-xl font-semibold tracking-tight">{event.title}</CardTitle>
<CardDescription>{formatEventDate(event.startsAt) ?? "Open gallery"}</CardDescription>
</CardHeader>
<CardContent>
<span className="text-sm text-primary">View gallery</span>
</CardContent>
<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>
);
}