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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
181 lines · 6.0 KB Raw History
import { useQuery } from '@tanstack/react-query';
import { CheckIcon, CopyIcon, PackageIcon } from 'lucide-react';
import { useState } from 'react';
import { Badge } from '#/components/atoms/badge.jsx';
import { Notice } from '#/components/atoms/notice.jsx';
import { RowIcon } from '#/components/atoms/row-icon.jsx';
import { EmptyState } from '#/components/molecules/empty-state.jsx';
import { ListRow } from '#/components/molecules/list-row.jsx';
import { TextLink } from '#/components/molecules/text-link.jsx';
import { get } from '#/lib/api.js';
import { bytes, timeAgo } from '#/lib/format.js';
import { queryKeys } from '#/lib/query.js';

/** One line summarizing a package's shape. */
function packageSummary(pkg) {
  if (pkg.versions.length === 0) return 'nothing published yet';
  const parts = [
    `${pkg.versions.length} ${pkg.versions.length === 1 ? 'version' : 'versions'}`,
  ];
  parts.push(bytes(pkg.size));
  if (pkg.updatedAt) parts.push(`published ${timeAgo(pkg.updatedAt)}`);
  return parts.join(' · ');
}

/** A command with a copy button beside it, shown in full where room allows. */
function CopyCommand({ command, compact }) {
  const [copied, setCopied] = useState(false);

  const copy = (event) => {
    event.preventDefault();
    event.stopPropagation();
    navigator.clipboard.writeText(command).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    });
  };

  return (
    <button
      type="button"
      onClick={copy}
      title={`Copy: ${command}`}
      className="flex shrink-0 items-center gap-1.5 rounded-lg border border-border bg-background px-2 py-1 font-mono text-[12px] text-muted-foreground hover:text-foreground"
    >
      {!compact && <span className="hidden lg:inline">{command}</span>}
      {copied ? (
        <CheckIcon className="size-3.5" />
      ) : (
        <CopyIcon className="size-3.5" />
      )}
    </button>
  );
}

/** Every version of one package: number, size, when, its install command. */
function VersionList({ pkg, registryUrl }) {
  const rows = [...pkg.versions].reverse();
  return (
    <div className="max-h-72 overflow-y-auto rounded-2xl border border-border bg-card px-5 py-2">
      {rows.map((entry) => (
        <div
          key={entry.version}
          className="flex items-center gap-3 border-border border-b py-2 text-[12.5px] last:border-b-0"
        >
          <span className="min-w-0 shrink-0 font-mono">{entry.version}</span>
          <span className="min-w-0 flex-1 truncate text-muted-foreground">
            {bytes(entry.size)}
            {entry.publishedAt
              ? ` · published ${timeAgo(entry.publishedAt)}`
              : ''}
          </span>
          <CopyCommand
            command={`npm install ${pkg.name}@${entry.version} --registry ${registryUrl}`}
            compact
          />
        </div>
      ))}
    </div>
  );
}

/**
 * @param {{data: Object}} props
 */
export function PackagesPage({ data: account }) {
  const [openVersions, setOpenVersions] = useState(
    /** @type {string|null} */ (null),
  );
  const { data, error } = useQuery({
    queryKey: queryKeys.npmPackages,
    queryFn: () => get('/npm/packages'),
  });

  if (error) {
    return <Notice variant="error">{error.message}</Notice>;
  }
  if (!data) return null;

  if (!data.packages.length) {
    return (
      <EmptyState
        icon={<PackageIcon />}
        title="No packages yet"
        description={
          <>
            Your account is an npm registry. Point npm at{' '}
            <span className="font-mono text-[12px]">
              https://{account.hostname}/npm/
            </span>
            , sign in with{' '}
            <span className="font-mono text-[12px]">npm login</span> using an
            app password, and{' '}
            <span className="font-mono text-[12px]">npm publish</span>. Tarballs
            are stored in your repo as blobs, so packages ride along in backups
            and migrate with your account. Anyone can install.
          </>
        }
      />
    );
  }

  return (
    <div className="flex flex-col gap-3">
      {data.packages.map((pkg) => {
        const latest = pkg.distTags.find((t) => t.tag === 'latest');
        const otherTags = pkg.distTags.filter((t) => t.tag !== 'latest');
        return (
          <div key={pkg.name} className="flex flex-col gap-2">
            <ListRow
              className="px-5 py-4"
              media={
                <RowIcon>
                  <PackageIcon />
                </RowIcon>
              }
              title={
                <div className="flex min-w-0 items-center gap-2">
                  <span className="min-w-0 truncate text-[16px]">
                    {pkg.name}
                  </span>
                  {latest && (
                    <Badge variant="scope" className="shrink-0">
                      v{latest.version}
                    </Badge>
                  )}
                  {otherTags.slice(0, 3).map((tag) => (
                    <Badge variant="scope" className="shrink-0" key={tag.tag}>
                      {tag.tag}
                    </Badge>
                  ))}
                </div>
              }
              description={packageSummary(pkg)}
              actions={
                <div className="flex shrink-0 items-center gap-3">
                  {pkg.versions.length > 0 && (
                    <TextLink
                      onClick={() =>
                        setOpenVersions(
                          openVersions === pkg.name ? null : pkg.name,
                        )
                      }
                    >
                      {openVersions === pkg.name ? 'Hide versions' : 'Versions'}
                    </TextLink>
                  )}
                  <CopyCommand
                    command={`npm install ${pkg.name} --registry ${data.registryUrl}`}
                  />
                </div>
              }
            />
            {openVersions === pkg.name && (
              <VersionList pkg={pkg} registryUrl={data.registryUrl} />
            )}
          </div>
        );
      })}
    </div>
  );
}