~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
203 lines · 7.6 KB Raw History
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 &rarr;
              </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 &rarr;
              </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 &amp; host
          </Button>
        </Panel>
      </div>
    </div>
  );
}