Skip to content
96 linesCodeBlameRaw
1import type { Folio } from "@g1t/contracts";
2import { RotateCcw, Trash2 } from "lucide-react";
3import { useState } from "react";
4import { data } from "react-router";
5
6import type { Route } from "./+types/trash";
7import { useFoliosAction } from "../../../components/folios/actions";
8import { KindIcon } from "../../../components/folios/kinds";
9import { EmptyState, ErrorText, TimeAgo } from "../../../components/ui";
10import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "../../../components/ui/alert-dialog";
11import { Button } from "../../../components/ui/button";
12import { Card } from "../../../components/ui/card";
13import { canDo } from "../../../lib/folios";
14import { page } from "../../../lib/meta";
15import { folios } from "../../../lib/services.server";
16import { requireUser, roleIn } from "../../../lib/session.server";
17
18export function meta({ params, ...args }: Route.MetaArgs) {
19 return page(args, { title: `Trash · Artifacts · ${params.owner} · g1t` });
20}
21
22export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{ items: Folio[] | null }> {
23 const viewer = requireUser(context, request);
24 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
25 const found = await folios.trashed(params.owner.toLowerCase(), viewer).catch(() => null);
26 return { items: found?.ok ? found.value : null };
27}
28
29/** Artifacts moved to the trash that the viewer can change: restore them, or delete them for good. After 30 days they go on their own. */
30export default function FoliosTrash({ loaderData, params }: Route.ComponentProps) {
31 const slug = params.owner.toLowerCase();
32 const { send, error } = useFoliosAction(slug);
33 const [forever, setForever] = useState<Folio | null>(null);
34 const items = loaderData.items;
35 return (
36 <div className="mx-auto max-w-3xl">
37 <h1 className="text-xl font-semibold tracking-tight">Trash</h1>
38 <p className="mt-1 text-sm text-muted">Artifacts come back with what was inside them. After 30 days in the trash they are deleted for good.</p>
39 {error && (
40 <div className="mt-3">
41 <ErrorText>{error}</ErrorText>
42 </div>
43 )}
44 <div className="mt-6">
45 {items === null ? (
46 <EmptyState title="Artifacts didn't answer">Try again in a moment.</EmptyState>
47 ) : items.length === 0 ? (
48 <EmptyState title="The trash is empty" />
49 ) : (
50 <Card asChild divided className="overflow-hidden">
51 <ul>
52 {items.map((f) => (
53 <li key={f.id} className="flex items-center gap-3 px-3 py-2.5 sm:px-4">
54 <KindIcon kind={f.kind} />
55 <span className="min-w-0 grow">
56 <span className="block truncate text-sm">{f.title || "Untitled"}</span>
57 <span className="block truncate text-xs text-faint">
58 {f.space?.name ?? "Private"} · trashed {f.trashed_at ? <TimeAgo at={f.trashed_at} /> : null}
59 </span>
60 </span>
61 <Button type="button" onClick={() => send("restore", { folio_id: f.id })} variant="outline" size="sm" className="text-xs text-fg hover:bg-raised font-normal">
62 <RotateCcw size={13} /> Restore
63 </Button>
64 {canDo(f.viewer_role, "manage") && (
65 <Button type="button" onClick={() => setForever(f)} variant="destructive" size="sm" className="text-xs font-normal">
66 <Trash2 size={13} /> <span className="max-sm:hidden">Delete</span>
67 </Button>
68 )}
69 </li>
70 ))}
71 </ul>
72 </Card>
73 )}
74 </div>
75 <AlertDialog open={!!forever} onOpenChange={(open) => !open && setForever(null)}>
76 <AlertDialogContent>
77 <AlertDialogHeader>
78 <AlertDialogTitle>Delete &ldquo;{forever?.title || "Untitled"}&rdquo; for good?</AlertDialogTitle>
79 <AlertDialogDescription>It goes with its history, comments and everything inside it. This can&apos;t be undone.</AlertDialogDescription>
80 </AlertDialogHeader>
81 <AlertDialogFooter>
82 <AlertDialogCancel>Keep it</AlertDialogCancel>
83 <AlertDialogAction
84 onClick={() => {
85 if (forever) void send("delete", { folio_id: forever.id });
86 setForever(null);
87 }}
88 >
89 Delete for good
90 </AlertDialogAction>
91 </AlertDialogFooter>
92 </AlertDialogContent>
93 </AlertDialog>
94 </div>
95 );
96}