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

78 lines2,927 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'worktree-agent-a985c01f43538af01'1import { ImageUp, LoaderCircle, Trash2 } from "lucide-react";
Workspace names and icons, and a component kit for every control2import { useRef } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'3import { Form } from "react-router";
Workspace names and icons, and a component kit for every control4
5import { AVATAR_ACCEPT } from "../lib/avatar-upload";
Merge branch 'worktree-agent-a985c01f43538af01'6import { Avatar, ErrorText, SubmitButton, usePending } from "./ui";
Workspace names and icons, and a component kit for every control7
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 form = useRef<HTMLFormElement>(null);
Merge branch 'worktree-agent-a985c01f43538af01'29 // Working until the new picture (or the letter) is the one shown.
30 const uploading = usePending({ intent: "avatar" });
31 const removing = usePending({ intent: "remove-avatar" });
32 const busy = uploading || removing;
Workspace names and icons, and a component kit for every control33 return (
34 <div className="flex items-start gap-4">
35 <Avatar name={name} image={image} size={64} square={square} />
36 <div className="min-w-0 space-y-2">
37 <p className="text-sm text-muted">{about}</p>
38 <div className="flex flex-wrap items-center gap-2">
39 <Form method="post" encType="multipart/form-data" ref={form}>
40 <input type="hidden" name="intent" value="avatar" />
41 <label
42 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 ${
43 busy ? "pointer-events-none opacity-50" : ""
44 }`}
45 >
Merge branch 'worktree-agent-a985c01f43538af01'46 {uploading ? (
47 <LoaderCircle size={15} aria-hidden="true" className="animate-spin" />
48 ) : (
49 <ImageUp size={15} />
50 )}
51 {uploading ? "Uploading…" : image ? "Upload a new image" : "Upload an image"}
Workspace names and icons, and a component kit for every control52 <input
53 type="file"
54 name="avatar"
55 accept={AVATAR_ACCEPT}
56 className="sr-only"
57 onChange={(event) => {
58 if (event.currentTarget.files?.length) form.current?.requestSubmit();
59 }}
60 />
61 </label>
62 </Form>
63 {image && (
64 <Form method="post">
65 <input type="hidden" name="intent" value="remove-avatar" />
Merge branch 'worktree-agent-a985c01f43538af01'66 <SubmitButton variant="quiet" pending="Removing…" match={{ intent: "remove-avatar" }} disabled={uploading}>
Workspace names and icons, and a component kit for every control67 <Trash2 size={15} />
68 Remove
Merge branch 'worktree-agent-a985c01f43538af01'69 </SubmitButton>
Workspace names and icons, and a component kit for every control70 </Form>
71 )}
72 </div>
73 <p className="text-xs text-faint">PNG, JPEG, WebP or GIF, at most 1 MB. Square images look best.</p>
74 <ErrorText>{error}</ErrorText>
75 </div>
76 </div>
77 );
78}

This file's history is long; its oldest lines are credited to the oldest commit read.