import Link from "next/link";
import { Card, Surface } from "@unlocalhosted/metalui";
import { BRAND } from "@/components/brand";
import { HomeTerminal } from "@/components/home-terminal";
import {
ArrowRightIcon,
BranchIcon,
CodeIcon,
CommitIcon,
CompassIcon,
IssueOpenedIcon,
KeyIcon,
PencilIcon,
PeopleIcon,
PlusIcon,
ShieldLockIcon,
SparkleIcon,
} from "@/components/icons";
import { CardDescription, CardTitle } from "@/components/metal";
import { RepoFilterList } from "@/components/repo-filter-list";
import { Avatar, Blankslate, Box, ButtonLink, Counter, Label, RefChip, RepoAvatar } from "@/components/ui";
import { getViewer } from "@/lib/auth";
import { shortSha, timeAgo } from "@/lib/format";
import { recentActivity, withActivity } from "@/lib/listing";
import { listReposForUser, searchRepos } from "@/lib/repos";
import { serverHost } from "@/lib/request-origin";
import { SIGNUPS_ENABLED } from "@/lib/signups";
import { sshCloneUrl } from "@/lib/ssh-config";
import type { User } from "@/lib/users";
export default async function Home() {
const viewer = await getViewer();
if (viewer) return <Dashboard viewer={viewer} />;
return <Landing sshUrl={sshCloneUrl(await serverHost(), "you/project.git")} />;
}
const features = [
{ icon: CodeIcon, title: "Beautiful code browsing", text: "Syntax highlighting, rendered READMEs, full history and crisp diffs.", wide: true },
{ icon: PencilIcon, title: "Edit in the browser", text: "A real editor. Create, rename, upload and delete files in one commit." },
{ icon: KeyIcon, title: "SSH keys", text: "Push with git@ URLs, exactly the way you're used to." },
{ icon: ShieldLockIcon, title: "Scoped tokens", text: "Read-only or read & write, per repository, with expiry." },
{ icon: PeopleIcon, title: "Built for teams", text: "Accounts, collaborators, public and private repositories." },
{ icon: IssueOpenedIcon, title: "Issues", text: "Track bugs and ideas with markdown discussions.", wide: true },
{ icon: SparkleIcon, title: "Light & dark", text: "A calm interface that follows your system theme." },
];
/** A section heading on the landing page: tight display type in ink. */
const displayHeading = "font-semibold tracking-[-0.04em] text-ink";
function Landing({ sshUrl }: { sshUrl: string }) {
return (
<main className="flex-1">
<section className="mx-auto max-w-6xl px-6 pt-20 pb-16 text-center md:pt-28">
<div className="flex flex-wrap items-center justify-center gap-3">
<Label tone="success">Self-hosted</Label>
<span className="type-label engraved">Your code stays yours</span>
</div>
<h1 className={`mx-auto mt-7 max-w-4xl text-[44px] leading-[1.02] md:text-[72px] md:tracking-[-0.05em] ${displayHeading}`}>
A calm, beautiful home <br className="hidden md:block" />
for your code.
</h1>
<p className="mx-auto mt-6 max-w-xl text-[17px] leading-[26px] font-medium tracking-[-0.012em] text-ink2">
{BRAND.name} hosts git repositories for your whole team. Push over SSH or HTTPS, edit in the browser, and keep private work private.
</p>
<div className="mt-9 flex flex-wrap justify-center gap-3">
{SIGNUPS_ENABLED ? (
<ButtonLink href="/signup" variant="primary" icon={<PlusIcon />}>
Create Your Account
</ButtonLink>
) : (
<ButtonLink href="/login" variant="primary" icon={<ArrowRightIcon />}>
Sign In
</ButtonLink>
)}
<ButtonLink href="/explore" icon={<CompassIcon />}>
Explore Repositories
</ButtonLink>
</div>
<div className="mt-16">
<HomeTerminal sshUrl={sshUrl} />
</div>
</section>
<section className="mx-auto max-w-6xl px-6 py-20">
<p className="text-center type-label engraved">Everything you need</p>
<h2 className={`mt-3 text-center text-[34px] leading-[1.1] ${displayHeading}`}>Ship without the noise</h2>
<p className="mx-auto mt-3 max-w-xl text-center text-[15px] leading-[22px] font-medium tracking-[-0.01em] text-ink2">
The workflow you use every day, running on your own machine.
</p>
<div className="mt-12 grid gap-4 md:grid-cols-3">
{features.map((f) => (
<Card key={f.title} className={f.wide ? "md:col-span-2" : ""}>
<span className="icon-tile mb-3 size-11 rounded-[14px]">
<f.icon width={20} height={20} />
</span>
<CardTitle>{f.title}</CardTitle>
<CardDescription className="max-w-md">{f.text}</CardDescription>
</Card>
))}
</div>
</section>
<section className="mx-auto max-w-6xl px-6">
<Surface material="raise" radius="hero" className="px-8 py-14 text-center">
<p className="type-label engraved">Get started</p>
<h2 className={`mt-3 text-[30px] leading-[1.1] ${displayHeading}`}>Plant your first repository</h2>
<p className="mt-2 type-lead text-ink2">It takes less than a minute.</p>
<ButtonLink href={SIGNUPS_ENABLED ? "/signup" : "/login"} variant="primary" icon={<ArrowRightIcon />} className="mt-7">
{SIGNUPS_ENABLED ? "Get Started for Free" : "Sign In"}
</ButtonLink>
</Surface>
</section>
</main>
);
}
function greeting() {
const h = new Date().getHours();
return h < 5 ? "Good evening" : h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening";
}
/** A row in a list that lifts out of its plate on hover (MetalUI's list row). */
const liftRow = "rounded-row transition-row outline-none hover:recipe-row-list-hover focus-visible:recipe-row-list-hover focus-visible:focus-ring-flush";
async function Dashboard({ viewer }: { viewer: User }) {
const [mine, recent] = await Promise.all([listReposForUser(viewer.id), searchRepos("", viewer.id, 30)]);
const [activity, explore] = await Promise.all([
recentActivity(mine),
withActivity(recent.filter((r) => r.ownerId !== viewer.id && r.visibility === "public").slice(0, 5)),
]);
return (
<div className="mx-auto grid w-full max-w-[1400px] flex-1 gap-6 px-4 py-8 md:grid-cols-[280px_1fr] md:px-6 xl:grid-cols-[280px_1fr_300px]">
<aside className="space-y-4">
<Box className="p-4">
<div className="mb-3 flex items-center justify-between gap-2">
<h2 className="flex items-center gap-2 type-label engraved">
Your repositories <Counter>{mine.length}</Counter>
</h2>
<ButtonLink href="/new" variant="invisible" icon={<PlusIcon />}>
New
</ButtonLink>
</div>
<RepoFilterList repos={mine.map((r) => ({ owner: r.owner, name: r.name, visibility: r.visibility, imageVersion: r.imageVersion }))} />
</Box>
</aside>
<main className="min-w-0">
<div className="mb-6 flex items-center gap-4">
<Avatar name={viewer.username} size={44} />
<div className="min-w-0">
<h1 className="text-[22px] leading-7 font-semibold tracking-[-0.025em] text-ink">
{greeting()}, {viewer.name?.split(" ")[0] || viewer.username}
</h1>
<p className="mt-0.5 type-lead text-ink2">Here’s what’s been happening across your repositories.</p>
</div>
</div>
{activity.length === 0 ? (
<Box>
<Blankslate
icon={<SparkleIcon />}
title={mine.length ? "No commits yet" : "Create your first repository"}
action={
<ButtonLink
href={mine.length ? `/${mine[0].owner}/${mine[0].name}` : "/new"}
variant="primary"
icon={mine.length ? <ArrowRightIcon /> : <PlusIcon />}
>
{mine.length ? "Open Repository" : "Create Repository"}
</ButtonLink>
}
>
{mine.length ? "Push some code or add a file in the browser to see activity here." : "Repositories hold your project’s files and history."}
</Blankslate>
</Box>
) : (
<Box className="p-2">
<h2 className="flex items-center gap-2 px-3 pt-2.5 pb-2 type-label engraved">
Recent activity <Counter>{activity.length}</Counter>
</h2>
<ol className="flex flex-col gap-0.5">
{activity.map(({ repo, commit }) => (
<li key={`${repo.id}-${commit.sha}`} className="flex gap-3 rounded-row px-3 py-2.5 transition-row hover:recipe-row-list-hover">
<span className="icon-tile mt-0.5 size-8 shrink-0 rounded-full">
<CommitIcon width={15} height={15} />
</span>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-baseline gap-x-1.5 type-body text-ink2">
<span className="font-medium text-ink">{commit.authorName}</span> pushed to
<Link href={`/${repo.owner}/${repo.name}`} className="font-medium text-ink hover:underline">
{repo.owner}/{repo.name}
</Link>
<span className="ml-auto type-readout text-ink3">{timeAgo(commit.date)}</span>
</div>
<Link href={`/${repo.owner}/${repo.name}/commit/${commit.sha}`} className="group mt-1.5 flex min-w-0 items-center gap-2.5">
<RefChip className="shrink-0">{shortSha(commit.sha)}</RefChip>
<span className="truncate type-ui text-ink group-hover:underline">{commit.subject}</span>
</Link>
</div>
</li>
))}
</ol>
</Box>
)}
</main>
<aside className="hidden space-y-4 xl:block">
<Box className="p-4">
<h2 className="mb-3 flex items-center gap-2 type-label engraved">
<CompassIcon width={12} height={12} /> Discover
</h2>
{explore.length === 0 && <p className="type-body text-ink3">No public repositories from other users yet.</p>}
<ul className="-mx-2 flex flex-col gap-0.5">
{explore.map((r) => (
<li key={r.id}>
<Link href={`/${r.owner}/${r.name}`} className={`flex gap-3 p-2 ${liftRow}`}>
<RepoAvatar repo={r} size={32} />
<span className="min-w-0">
<span className="block truncate type-title text-ink">
<span className="font-normal text-ink3">{r.owner}/</span>
{r.name}
</span>
{r.description && <span className="mt-0.5 line-clamp-2 type-meta text-ink2">{r.description}</span>}
</span>
</Link>
</li>
))}
</ul>
<ButtonLink href="/explore" variant="invisible" icon={<ArrowRightIcon />} className="mt-2 -ml-2">
Explore More
</ButtonLink>
</Box>
<Box className="p-4">
<div className="flex items-start gap-3">
<span className="icon-tile size-9 shrink-0 rounded-[12px]">
<BranchIcon />
</span>
<div className="min-w-0">
<h2 className="type-title text-ink">Push access</h2>
<p className="mt-0.5 type-meta text-ink2">Authenticate git with an SSH key or an API token.</p>
</div>
</div>
<div className="rule-t -mx-2 mt-4 flex flex-col gap-0.5 pt-2">
<Link href="/settings/keys" className={`flex items-center gap-2.5 px-2 py-1.5 type-ui text-ink ${liftRow}`}>
<KeyIcon className="text-ink2" /> Manage SSH Keys
</Link>
<Link href="/settings/tokens" className={`flex items-center gap-2.5 px-2 py-1.5 type-ui text-ink ${liftRow}`}>
<ShieldLockIcon className="text-ink2" /> Manage API Tokens
</Link>
</div>
</Box>
</aside>
</div>
);
}