g1t/apps/web/app/routes/settings-menu-json.ts

36 lines1,626 bytesCodeBlame
1/**
2 * What the account menu shows under the avatar, fetched when it opens so
3 * no page waits on it: the name people see, the primary address, and how
4 * many invites are left when there is a limit. Each part is optional: a
5 * service that cannot answer leaves its part out.
6 */
7import type { Route } from "./+types/settings-menu-json";
8import { accounts, identity } from "../lib/services.server";
9import { getViewer } from "../lib/session.server";
10import { loadInvites } from "../lib/invites.server";
11
12export type AccountMenuData = {
13 name: string | null;
14 email: string | null;
15 /** Invites left, or null when there is no limit (or none can be made yet). */
16 invites_left: number | null;
17};
18
19export async function loader({ context }: Route.LoaderArgs) {
20 const user = getViewer(context);
21 if (!user) return Response.json(null, { status: 401, headers: { "cache-control": "private, no-store" } });
22 const [profile, emails, invites] = await Promise.all([
23 identity.profile(user.username).catch(() => null),
24 accounts.listEmails(user).catch(() => null),
25 loadInvites(user),
26 ]);
27 const primary = emails?.ok ? (emails.value.emails.find((e) => e.primary)?.email ?? null) : null;
28 const data: AccountMenuData = {
29 name: profile?.name?.trim() || null,
30 email: primary,
31 invites_left: invites?.allowance.limit != null ? (invites.allowance.remaining ?? 0) : null,
32 };
33 // Not kept by the browser: the menu asks again after a name, address or
34 // invite changes, and a kept copy would show the old one for a minute.
35 return Response.json(data, { headers: { "cache-control": "private, no-store" } });
36}