| 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 | */ |
| 7 | import type { Route } from "./+types/settings-menu-json"; |
| 8 | import { accounts, identity } from "../lib/services.server"; |
| 9 | import { getViewer } from "../lib/session.server"; |
| 10 | import { loadInvites } from "../lib/invites.server"; |
| 11 | |
| 12 | export 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 | |
| 19 | export 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 | return Response.json(data, { headers: { "cache-control": "private, max-age=60" } }); |
| 34 | } |