g1t/apps/web/app/routes/settings/profile.tsx

77 lines3,029 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.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1import { identity } from "../../lib/services.server";
2
3import type { Route } from "./+types/profile";
4import { page } from "../../lib/meta";
5import { AvatarField } from "../../components/avatar-field";
6import { readAvatarUpload } from "../../lib/avatar-upload";
7import { assertSameOrigin, requireUser } from "../../lib/session.server";
8import { ProfileSection } from "../../components/profile-form";
9
10export function meta(args: Route.MetaArgs) {
11 return page(args, { title: "Profile · Settings · g1t" });
12}
13
14export async function loader({ request, context }: Route.LoaderArgs) {
15 const user = requireUser(context, request);
16 const profile = await identity.profile(user.username);
17 return { user, profile };
18}
19
20export async function action({ request, context }: Route.ActionArgs) {
21 assertSameOrigin(request);
22 const user = requireUser(context, request);
23 const form = await request.formData();
24 switch (form.get("intent")) {
25 // Identity checks every field again, and the website most of all.
26 case "profile": {
27 const text = (name: string) => String(form.get(name) ?? "");
28 const result = await identity.updateProfile(user, {
29 name: text("name"),
30 bio: text("bio"),
31 location: text("location"),
32 website: text("website"),
33 pronouns: text("pronouns"),
34 });
35 return result.ok ? { profileSaved: true } : { profileError: result.error.message };
36 }
37 // The picture: identity checks the image's bytes again.
38 case "avatar": {
39 const upload = await readAvatarUpload(form);
40 if ("error" in upload) return { avatarError: upload.error };
41 const result = await identity.setUserAvatar(user, upload.image);
42 return result.ok ? null : { avatarError: result.error.message };
43 }
44 case "remove-avatar": {
45 const result = await identity.setUserAvatar(user, null);
46 return result.ok ? null : { avatarError: result.error.message };
47 }
48 }
49 return null;
50}
51
52export default function ProfileSettings({ loaderData, actionData }: Route.ComponentProps) {
53 const { user, profile } = loaderData;
54 return (
55 <div className="space-y-12">
56 <section id="picture" className="scroll-mt-20">
57 <h2 className="font-medium">Picture</h2>
58 <div className="mt-4">
59 <AvatarField
60 name={user.username}
61 image={user.avatar}
62 error={actionData && "avatarError" in actionData ? actionData.avatarError : undefined}
63 about="Shown beside your username in the sidebar and on mission control. Without one, g1t draws your first letter."
64 />
65 </div>
66 </section>
Merge branch 'worktree-agent-a985c01f43538af01'67 {/* Keyed on what was saved, so the fields show identity's copy once it answers. */}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas68 <ProfileSection
Merge branch 'worktree-agent-a985c01f43538af01'69 key={JSON.stringify(profile)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas70 username={user.username}
71 profile={profile}
72 error={actionData && "profileError" in actionData ? actionData.profileError : undefined}
73 saved={Boolean(actionData && "profileSaved" in actionData)}
74 />
75 </div>
76 );
77}

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