chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useQuery } from '@tanstack/react-query';
import { ServerIcon } from 'lucide-react';
import { Avatar } from '#/components/atoms/avatar.jsx';
import { Button } from '#/components/atoms/button.jsx';
import { Heading } from '#/components/atoms/heading.jsx';
import { Notice } from '#/components/atoms/notice.jsx';
import { Panel } from '#/components/atoms/panel.jsx';
import { ActivityList } from '#/components/molecules/activity-list.jsx';
import { AppCard } from '#/components/molecules/app-card.jsx';
import { Section } from '#/components/molecules/section.jsx';
import { StatTile } from '#/components/molecules/stat-tile.jsx';
import { TextLink } from '#/components/molecules/text-link.jsx';
import { updateHref } from '#/components/organisms/version-card.jsx';
import { get } from '#/lib/api.js';
import { groupByApp, mergeApps, num } from '#/lib/format.js';
import { queryKeys } from '#/lib/query.js';
export function HomePage({ data, onNavigate, appHref, recordHref }) {
const { handle, did, hostname, displayName, avatarUrl, stats, collections } =
data;
const go = onNavigate || (() => {});
// Only for its alias map, so a combined app shows as one card here too. The
// cards render from the record buckets alone before it arrives.
const { data: appsData } = useQuery({
queryKey: queryKeys.apps,
queryFn: () => get('/apps'),
});
const apps = appsData?.aliases
? mergeApps(collections, [], appsData.aliases).sort(
(a, b) => b.count - a.count,
)
: groupByApp(collections);
const { data: activity } = useQuery({
queryKey: ['activity', { limit: 6 }],
queryFn: () => get('/activity?limit=6'),
});
// Answered from the server's cached registry check, so this costs one
// same-origin request and stays quiet unless there is news.
const { data: version } = useQuery({
queryKey: queryKeys.version,
queryFn: () => get('/version'),
});
return (
<div className="space-y-5">
{version?.updateAvailable && (
<Notice variant="info">
<span className="font-bold text-foreground">
Server update available.
</span>{' '}
This server runs pds.js {version.current}, and {version.latest} is
out.{' '}
{version.updateUrl ? (
<>
<TextLink href={updateHref(version.updateUrl, version)} external>
Update this server
</TextLink>{' '}
to install it.{' '}
</>
) : (
<>Update the server deployment to install it. </>
)}
<TextLink href={version.releaseNotesUrl} external>
Release notes
</TextLink>
</Notice>
)}
<Panel className="flex flex-col gap-6 xl:flex-row xl:items-center">
<div className="flex min-w-0 flex-1 items-center gap-4 sm:gap-5">
<Avatar
className="size-16 border-0 bg-gradient-to-br from-beam-from to-beam-to shadow-[inset_0_2px_0_rgba(255,255,255,.28)] sm:size-[76px]"
src={avatarUrl}
fallback={
<span className="font-extrabold text-[22px] text-white sm:text-[26px]">
{(displayName || handle || did).slice(0, 1).toUpperCase()}
</span>
}
/>
<div className="min-w-0">
<div className="truncate font-extrabold text-[22px] text-foreground tracking-tight sm:text-[26px]">
{displayName || (handle ? `@${handle}` : 'Your account')}
</div>
<div className="mt-1 truncate font-mono text-[13px] text-muted-foreground">
{handle ? `@${handle}` : did}
</div>
<div className="mt-2.5 text-[12.5px] text-faint">
Hosted by{' '}
<span className="text-muted-foreground">{hostname}</span>
</div>
</div>
</div>
<div className="grid shrink-0 grid-cols-3 gap-2">
<StatTile
value={num(stats.records)}
label="Things saved"
href="/account/apps"
onNavigate={go}
/>
<StatTile
value={num(stats.apps)}
label="Apps signed in"
href="/account/apps"
onNavigate={go}
/>
<StatTile
value={num(apps.length)}
label="Apps you've used"
href="/account/apps"
onNavigate={go}
/>
</div>
</Panel>
{apps.length > 0 && (
<Panel>
<Section
title="Apps you've used"
description="What each app has stored in your account."
action={
<TextLink href="/account/apps" onNavigate={go}>
See all →
</TextLink>
}
>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{apps.slice(0, 6).map((app) => (
<AppCard
key={app.domain}
app={app}
href={appHref(app.domain)}
onNavigate={go}
/>
))}
</div>
</Section>
</Panel>
)}
<div className="grid grid-cols-1 items-start gap-5 lg:grid-cols-[1fr_360px]">
<Panel>
<Section
title="Recent changes"
description="Things added, changed or removed in your account."
action={
<TextLink href="/account/history" onNavigate={go}>
Full history →
</TextLink>
}
>
{activity?.events?.length ? (
<ActivityList
events={activity.events}
recordHref={recordHref}
onNavigate={go}
/>
) : (
<div className="rounded-2xl border border-border border-dashed px-6 py-10 text-center">
<div className="font-bold text-[14px] text-foreground">
Nothing to show yet
</div>
<p className="mx-auto mt-1.5 max-w-sm text-[13px] text-muted-foreground leading-relaxed">
As apps write to your account, the changes will appear here,
newest first.
</p>
</div>
)}
</Section>
</Panel>
<Panel>
<Heading>Where your account is stored</Heading>
<div className="mt-4 flex items-center gap-3.5 rounded-2xl border border-key/30 bg-key/10 px-4 py-3.5">
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-key/15 text-key">
<ServerIcon className="size-5" />
</div>
<div className="min-w-0">
<div className="truncate font-bold text-[15px] text-key">
{hostname}
</div>
<div className="mt-0.5 text-[12px] text-muted-foreground">
Your home server
</div>
</div>
</div>
<p className="mt-4 text-[13px] text-muted-foreground leading-relaxed">
Someone has to keep your account online. Yours is kept by {hostname}
, which holds your data and hands it to any app you sign in to.
</p>
<p className="mt-2 text-[12.5px] text-faint leading-relaxed">
Hosts can be run by an app maker, by a community, or by you.
</p>
<Button
variant="outline"
onClick={() => go('/account/identity')}
className="mt-4 text-[13px] hover:border-ring hover:text-key"
>
Username & host
</Button>
</Panel>
</div>
</div>
);
}