Skip to content
78 linesCodeBlameRaw
1import { 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 timezone: text("timezone"),
35 });
36 return result.ok ? { profileSaved: true } : { profileError: result.error.message };
37 }
38 // The picture: identity checks the image's bytes again.
39 case "avatar": {
40 const upload = await readAvatarUpload(form);
41 if ("error" in upload) return { avatarError: upload.error };
42 const result = await identity.setUserAvatar(user, upload.image);
43 return result.ok ? null : { avatarError: result.error.message };
44 }
45 case "remove-avatar": {
46 const result = await identity.setUserAvatar(user, null);
47 return result.ok ? null : { avatarError: result.error.message };
48 }
49 }
50 return null;
51}
52
53export default function ProfileSettings({ loaderData, actionData }: Route.ComponentProps) {
54 const { user, profile } = loaderData;
55 return (
56 <div className="space-y-12">
57 <section id="picture" className="scroll-mt-20">
58 <h2 className="font-medium">Picture</h2>
59 <div className="mt-4">
60 <AvatarField
61 name={user.username}
62 image={user.avatar}
63 error={actionData && "avatarError" in actionData ? actionData.avatarError : undefined}
64 about="Shown beside your username in the sidebar and on mission control. Without one, g1t draws your first letter."
65 />
66 </div>
67 </section>
68 {/* Keyed on what was saved, so the fields show identity's copy once it answers. */}
69 <ProfileSection
70 key={JSON.stringify(profile)}
71 username={user.username}
72 profile={profile}
73 error={actionData && "profileError" in actionData ? actionData.profileError : undefined}
74 saved={Boolean(actionData && "profileSaved" in actionData)}
75 />
76 </div>
77 );
78}