flagon-io/g1t

public

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

g1t/apps/web/app/components/avatar-field.tsx

73 lines2,687 bytesCodeBlame
1import { ImageUp, Trash2 } from "lucide-react";
2import { useRef } from "react";
3import { Form, useNavigation } from "react-router";
4
5import { AVATAR_ACCEPT } from "../lib/avatar-upload";
6import { Avatar, Button, ErrorText } from "./ui";
7
8/**
9 * Uploading an avatar, as GitHub's settings do: the one shown now, a
10 * button to pick a new image, which is sent as soon as it is picked, and
11 * a way back to the letter. Posts `intent=avatar` with the file in
12 * `avatar`, or `intent=remove-avatar`.
13 */
14export function AvatarField({
15 name,
16 image,
17 square,
18 error,
19 about,
20}: {
21 /** What the letter avatar is drawn from. */
22 name: string;
23 image?: string | null;
24 square?: boolean;
25 error?: string;
26 about: string;
27}) {
28 const navigation = useNavigation();
29 const form = useRef<HTMLFormElement>(null);
30 const intent = navigation.formData?.get("intent");
31 const busy = navigation.state === "submitting" && (intent === "avatar" || intent === "remove-avatar");
32 return (
33 <div className="flex items-start gap-4">
34 <Avatar name={name} image={image} size={64} square={square} />
35 <div className="min-w-0 space-y-2">
36 <p className="text-sm text-muted">{about}</p>
37 <div className="flex flex-wrap items-center gap-2">
38 <Form method="post" encType="multipart/form-data" ref={form}>
39 <input type="hidden" name="intent" value="avatar" />
40 <label
41 className={`inline-flex cursor-pointer items-center gap-2 rounded-md px-3.5 py-2 text-sm font-medium ring-1 ring-line transition-colors hover:ring-line-strong ${
42 busy ? "pointer-events-none opacity-50" : ""
43 }`}
44 >
45 <ImageUp size={15} />
46 {busy && intent === "avatar" ? "Uploading…" : image ? "Upload a new image" : "Upload an image"}
47 <input
48 type="file"
49 name="avatar"
50 accept={AVATAR_ACCEPT}
51 className="sr-only"
52 onChange={(event) => {
53 if (event.currentTarget.files?.length) form.current?.requestSubmit();
54 }}
55 />
56 </label>
57 </Form>
58 {image && (
59 <Form method="post">
60 <input type="hidden" name="intent" value="remove-avatar" />
61 <Button type="submit" variant="quiet" disabled={busy}>
62 <Trash2 size={15} />
63 Remove
64 </Button>
65 </Form>
66 )}
67 </div>
68 <p className="text-xs text-faint">PNG, JPEG, WebP or GIF, at most 1 MB. Square images look best.</p>
69 <ErrorText>{error}</ErrorText>
70 </div>
71 </div>
72 );
73}