Skip to content
98 linesCodeBlameRaw
1import type { DocSpace } from "@g1t/contracts";
2import { Check, Plus } from "lucide-react";
3import { Link, data } from "react-router";
4
5import type { Route } from "./+types/spaces";
6import { useFoliosAction, useFoliosData } from "../../../components/folios/actions";
7import { SpaceIcon, spaceKindLabel } from "../../../components/folios/parts";
8import { EmptyState, ErrorText } from "../../../components/ui";
9import { spacePath } from "../../../lib/folios";
10import { page } from "../../../lib/meta";
11import { docs } from "../../../lib/services.server";
12import { requireUser, roleIn } from "../../../lib/session.server";
13
14export function meta({ params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `Spaces · Artifacts · ${params.owner} · g1t` });
16}
17
18export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{ spaces: DocSpace[] | null }> {
19 const viewer = requireUser(context, request);
20 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
21 const found = await docs.sidebar(params.owner.toLowerCase(), viewer).catch(() => null);
22 return { spaces: found?.ok ? found.value.spaces.filter((s) => !s.archived_at).map(({ pages: _pages, ...space }) => space) : null };
23}
24
25/**
26 * Every space the viewer can open. Open spaces are joined to show in the
27 * sidebar, so it doesn't fill up; team and members-only spaces show
28 * there already.
29 */
30export default function BrowseSpaces({ loaderData, params }: Route.ComponentProps) {
31 const slug = params.owner.toLowerCase();
32 const layout = useFoliosData();
33 const { send, busy, error } = useFoliosAction(slug);
34 const shown = new Set((layout?.sidebar?.spaces ?? []).map((s) => s.id));
35 const spaces = loaderData.spaces;
36 return (
37 <div className="mx-auto max-w-4xl">
38 <div className="flex flex-wrap items-end justify-between gap-4">
39 <div>
40 <h1 className="text-xl font-semibold tracking-tight">Spaces</h1>
41 <p className="mt-1 text-sm text-muted">A space holds artifacts for a team, a project or a topic. Join an open space to see it in your sidebar.</p>
42 </div>
43 <Link to={`/${slug}/-/artifacts/spaces/new`} className="inline-flex h-9 items-center gap-1.5 rounded-md border border-line px-3 text-sm text-fg/80 hover:border-line-strong hover:bg-surface hover:text-fg">
44 <Plus size={15} /> New space
45 </Link>
46 </div>
47 {error && (
48 <div className="mt-3">
49 <ErrorText>{error}</ErrorText>
50 </div>
51 )}
52 <div className="mt-6">
53 {spaces === null ? (
54 <EmptyState title="Artifacts didn't answer">Try again in a moment.</EmptyState>
55 ) : spaces.length === 0 ? (
56 <EmptyState title="No spaces yet">Make one for a team, a project or a topic.</EmptyState>
57 ) : (
58 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
59 {spaces.map((s) => {
60 const joined = shown.has(s.id);
61 const joinable = s.kind === "workspace" && !s.is_default;
62 return (
63 <li key={s.id} className="flex items-center gap-3 px-4 py-3">
64 <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-raised text-base">
65 <SpaceIcon space={s} size={16} />
66 </span>
67 <span className="min-w-0 grow">
68 <Link to={spacePath(slug, s.slug)} className="block truncate text-sm font-medium text-fg hover:text-accent">
69 {s.name}
70 </Link>
71 <span className="block truncate text-xs text-faint">
72 {spaceKindLabel(s.kind)}
73 {s.description ? ` · ${s.description}` : ""}
74 </span>
75 </span>
76 {joinable ? (
77 <button
78 type="button"
79 disabled={busy}
80 onClick={() => send(joined ? "leave_space" : "join_space", { space_id: s.id })}
81 className={`inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md px-3 text-xs font-medium disabled:opacity-60 ${joined ? "border border-line text-muted hover:border-danger/40 hover:text-danger" : "bg-accent text-bg hover:bg-accent-hover"}`}
82 >
83 {joined ? "Leave" : "Join"}
84 </button>
85 ) : (
86 <span className="inline-flex shrink-0 items-center gap-1 text-xs text-faint">
87 <Check size={13} /> In your sidebar
88 </span>
89 )}
90 </li>
91 );
92 })}
93 </ul>
94 )}
95 </div>
96 </div>
97 );
98}