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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
92 lines · 3.0 KB Raw History
import { Badge } from '#/components/atoms/badge.jsx';
import { Link } from '#/components/atoms/link.jsx';
import { TextLink } from '#/components/molecules/text-link.jsx';
import { bytes, dateTime } from '#/lib/format.js';
import { parseAtUri } from '#/lib/record-link.js';

/**
 * The records this file appears in, each opening in the records browser. A
 * file no record points at is flagged instead: the link table is this server's
 * bookkeeping, so it is phrased as what is known, not a promise.
 */
function UsedBy({ records, recordHref, onNavigate }) {
  if (!records.length) {
    return (
      <div className="mt-2.5">
        <Badge>Not used by any record</Badge>
        <p className="mt-1.5 text-[12px] text-faint leading-relaxed">
          No record points at this file. Unused files are cleaned up
          automatically.
        </p>
      </div>
    );
  }
  return (
    <div className="mt-2.5">
      <div className="text-[12px] text-faint">Used by</div>
      <div className="mt-1 flex flex-col items-start gap-1">
        {records.map((uri) => {
          const target = parseAtUri(uri);
          if (!target) return null;
          return (
            <Link
              key={uri}
              href={recordHref(target.collection, target.rkey)}
              onNavigate={onNavigate}
              className="break-all font-mono text-[12px] text-key underline-offset-2 hover:text-foreground hover:underline"
            >
              {target.collection}/{target.rkey}
            </Link>
          );
        })}
      </div>
    </div>
  );
}

/** One file's full details; the body of both the card and the tile dialog. */
export function FileDetails({ blob, blobBase, recordHref, onNavigate }) {
  return (
    <div className="min-w-0 flex-1">
      <div className="flex flex-wrap items-baseline gap-x-3 gap-y-0.5">
        <span className="min-w-0 break-all font-medium text-[14px] text-foreground">
          {blob.mimeType || 'File'}
        </span>
        <span className="text-[12.5px] text-muted-foreground">
          {bytes(blob.size)}
        </span>
        {(blob.time || blob.createdAt) && (
          <span
            className="ml-auto text-[12px] text-faint"
            title={
              blob.timeFromRecord && blob.createdAt
                ? `Stored here ${dateTime(blob.createdAt)}`
                : undefined
            }
          >
            {blob.timeFromRecord
              ? dateTime(blob.time)
              : `Stored ${dateTime(blob.time || blob.createdAt)}`}
          </span>
        )}
      </div>
      <div className="mt-0.5 break-all font-mono text-[11.5px] text-faint">
        {blob.cid}
      </div>
      <UsedBy
        records={blob.records}
        recordHref={recordHref}
        onNavigate={onNavigate}
      />
      {blobBase && (
        <TextLink
          href={`${blobBase}&cid=${encodeURIComponent(blob.cid)}`}
          external
          className="mt-3 text-[12.5px]"
        >
          Download
        </TextLink>
      )}
    </div>
  );
}