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

75 lines2,900 bytesCodeBlame
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 });
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>
67 <ProfileSection
68 username={user.username}
69 profile={profile}
70 error={actionData && "profileError" in actionData ? actionData.profileError : undefined}
71 saved={Boolean(actionData && "profileSaved" in actionData)}
72 />
73 </div>
74 );
75}