Skip to content
130 linesCodeBlameRaw
1/**
2 * Artifacts as lists show them: rows
3 * grouped by the day they were last edited, in the viewer's time zone, or
4 * cards with a picture drawn from each one's preview. Each has its kind,
5 * who can see it, its space, when it was edited and by whom, and its ⋯
6 * menu.
7 */
8import type { Folio } from "@g1t/contracts";
9import { useMemo } from "react";
10import { Link } from "react-router";
11
12import { dayGroups } from "../../lib/folios";
13import { TimeAgo } from "../ui";
14import { Card } from "../ui/card";
15import { Hint } from "../ui/hint";
16import { useFoliosData } from "./actions";
17import { FolioThumbnail, KindIcon } from "./kinds";
18import { FolioMenu } from "./menu";
19import { AccessMark } from "./parts";
20
21/** Where it sits, in a word: its space; else your Private, or whose it is when it's someone else's. */
22function placeOf(folio: Folio, me: string | undefined): string {
23 if (folio.space) return folio.space.name;
24 return `user:${folio.owner.id}` === me ? "Private" : folio.owner.display_name;
25}
26
27function Edited({ folio }: { folio: Folio }) {
28 const by = folio.edited_by?.display_name;
29 return (
30 <Hint label={by ? `Edited by ${by}` : "Edited"}>
31 <span className="shrink-0 text-xs whitespace-nowrap text-faint" tabIndex={-1}>
32 Edited <TimeAgo at={folio.edited_at} />
33 </span>
34 </Hint>
35 );
36}
37
38export function FolioRow({ slug, folio, onError }: { slug: string; folio: Folio; onError?: (message: string) => void }) {
39 const me = useFoliosData()?.me.key;
40 return (
41 <li className="group relative flex items-center gap-3 px-3 py-2 transition-colors hover:bg-raised/60 sm:px-4">
42 <KindIcon kind={folio.kind} />
43 <span className="flex min-w-0 grow items-center gap-2">
44 <Link to={folio.path} prefetch="intent" className="min-w-0 truncate text-sm text-fg after:absolute after:inset-0 hover:text-accent">
45 {folio.icon && <span className="mr-1.5">{folio.icon}</span>}
46 {folio.title || "Untitled"}
47 </Link>
48 {folio.stale && (
49 <Hint label="Possibly out of date: code it cites changed">
50 <span className="relative size-1.5 shrink-0 rounded-full bg-warn" aria-label="Possibly out of date" />
51 </Hint>
52 )}
53 </span>
54 <span className="relative hidden shrink-0 sm:flex">
55 <AccessMark folio={folio} />
56 </span>
57 <span className="hidden w-32 shrink-0 truncate text-xs text-faint md:block">{placeOf(folio, me)}</span>
58 <span className="relative w-28 shrink-0 text-right max-sm:w-auto">
59 <Edited folio={folio} />
60 </span>
61 <span className="relative">
62 <FolioMenu slug={slug} folio={folio} onError={onError} />
63 </span>
64 </li>
65 );
66}
67
68/** Rows under Today, Yesterday, then dates. */
69export function FolioDays({ slug, items, zone, onError }: { slug: string; items: Folio[]; zone: string; onError?: (message: string) => void }) {
70 const days = useMemo(() => dayGroups(items, (f) => f.edited_at, zone), [items, zone]);
71 return (
72 <div className="space-y-6">
73 {days.map((day) => (
74 <section key={day.key} aria-label={day.label}>
75 <h3 className="mb-1.5 px-1 text-xs font-medium text-faint">{day.label}</h3>
76 <Card asChild divided className="overflow-hidden">
77 <ul>
78 {day.items.map((f) => (
79 <FolioRow key={f.id} slug={slug} folio={f} onError={onError} />
80 ))}
81 </ul>
82 </Card>
83 </section>
84 ))}
85 </div>
86 );
87}
88
89export function FolioCard({ slug, folio, onError }: { slug: string; folio: Folio; onError?: (message: string) => void }) {
90 const me = useFoliosData()?.me.key;
91 return (
92 <Card className="group relative flex flex-col overflow-hidden transition-colors hover:border-line-strong">
93 <div className="pointer-events-none h-32 shrink-0 border-b border-line">
94 <FolioThumbnail folio={folio} />
95 </div>
96 <div className="flex items-start gap-2.5 px-3.5 pt-3 pb-3">
97 <KindIcon kind={folio.kind} size={14} box={24} />
98 <span className="min-w-0 grow">
99 <Link to={folio.path} prefetch="intent" className="line-clamp-1 text-sm font-medium text-fg after:absolute after:inset-0 group-hover:text-accent">
100 {folio.icon && <span className="mr-1">{folio.icon}</span>}
101 {folio.title || "Untitled"}
102 </Link>
103 <span className="mt-0.5 flex items-center gap-1.5 text-[0.6875rem] text-faint">
104 <span className="truncate">{placeOf(folio, me)}</span>
105 <span aria-hidden="true">·</span>
106 <span className="relative">
107 <Edited folio={folio} />
108 </span>
109 </span>
110 </span>
111 <span className="relative -mt-1 -mr-1.5 flex items-center">
112 <span className="relative">
113 <AccessMark folio={folio} />
114 </span>
115 <FolioMenu slug={slug} folio={folio} onError={onError} />
116 </span>
117 </div>
118 </Card>
119 );
120}
121
122export function FolioGrid({ slug, items, onError }: { slug: string; items: Folio[]; onError?: (message: string) => void }) {
123 return (
124 <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
125 {items.map((f) => (
126 <FolioCard key={f.id} slug={slug} folio={f} onError={onError} />
127 ))}
128 </div>
129 );
130}