Skip to content

g1t/apps/web/app/components/pin-button.tsx

62 lines2,381 bytesCodeBlame
1/**
2 * Pinning a project, so it stays at the top of the workspace's sidebar:
3 * a button on the project's header and on each row of the Projects page.
4 * Posts to the workspace's pins route (routes/workspace/pins.ts); while it
5 * is on its way, it shows as done.
6 */
7import { Pin } from "lucide-react";
8import { useFetcher } from "react-router";
9
10import { cn } from "../lib/cn";
11
12export function PinButton({
13 workspace,
14 slug,
15 name,
16 pinned,
17 compact,
18 small,
19 className,
20}: {
21 workspace: string;
22 slug: string;
23 /** For what the button says to a screen reader. */
24 name: string;
25 pinned: boolean;
26 /** Only the pin, as on a row; the header says it in words too. */
27 compact?: boolean;
28 /** Row-sized, for the sidebar. */
29 small?: boolean;
30 className?: string;
31}) {
32 const fetcher = useFetcher<{ error: string | null }>({ key: `pin:${workspace}/${slug}` });
33 const asked = fetcher.formData?.get("intent");
34 const shown = asked === "pin" ? true : asked === "unpin" ? false : pinned;
35 const label = shown ? `Unpin ${name}` : `Pin ${name}`;
36 const error = fetcher.state === "idle" ? fetcher.data?.error : null;
37 return (
38 <fetcher.Form method="post" action={`/${workspace}/-/pins`} className={cn("relative flex", className)}>
39 <input type="hidden" name="intent" value={shown ? "unpin" : "pin"} />
40 <input type="hidden" name="slug" value={slug} />
41 <button
42 type="submit"
43 aria-label={label}
44 aria-pressed={shown}
45 title={error ?? (shown ? "Unpin from the sidebar" : "Pin to the sidebar")}
46 className={cn(
47 "inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md text-[0.8125rem] transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent",
48 small
49 ? "relative z-10 size-6 hover:bg-raised"
50 : compact
51 ? "relative z-10 size-8 hover:bg-raised"
52 : "h-8 border border-line px-2.5 text-fg/80 hover:border-line-strong hover:bg-surface hover:text-fg",
53 shown ? "text-accent" : compact || small ? "text-faint hover:text-fg" : "",
54 error && "text-danger",
55 )}
56 >
57 <Pin size={small ? 12 : 14} className={shown ? "fill-current" : undefined} />
58 {!compact && !small && <span className="hidden sm:inline">{shown ? "Unpin" : "Pin"}</span>}
59 </button>
60 </fetcher.Form>
61 );
62}