Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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