create-ailk 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +4 -1
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +9 -0
- package/dist/lib/scaffold-render-sweep.js +24 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +119 -0
- package/dist/programmatic.js +3 -0
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.js +140 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +60 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/CLAUDE.md +25 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +81 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
- package/templates/apps/api/src/middleware/auth.ts +206 -46
- package/templates/apps/api/src/routes/leads/index.ts +4 -0
- package/templates/apps/api/src/server.ts +107 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
- package/templates/apps/api/src/services/lead-routing.ts +30 -2
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +5 -1
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +24 -0
- package/templates/apps/web/messages/en.json +156 -35
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +90 -15
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use client"; // reads meeting types and slots on demand and posts a controlled form
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ScheduleService — the New menu's Schedule service popup (#8233, spec D5,
|
|
5
|
+
* D8, D10; AC-MAA-10). On open it reads the meeting types the provider
|
|
6
|
+
* offers its clients (`readMeetingTypes`) and lists only those; picking one
|
|
7
|
+
* reads that type's open times for the next 14 days in the browser's time
|
|
8
|
+
* zone (`readSlots`, which computes the range on the server). Book posts the
|
|
9
|
+
* type, the slot's start as served, the time zone and optional notes to
|
|
10
|
+
* `member.book` — no name and no email: the server takes the verified email
|
|
11
|
+
* and the name from the session.
|
|
12
|
+
*
|
|
13
|
+
* With no type offered it says no times are offered and, when Send message
|
|
14
|
+
* is on the menu, offers to switch to it. A taken slot (`slot_unavailable`)
|
|
15
|
+
* re-reads the picked type's times and keeps the type and the notes; every
|
|
16
|
+
* other refusal shows its plain message with everything kept.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
Button,
|
|
21
|
+
Label,
|
|
22
|
+
RadioGroup,
|
|
23
|
+
RadioGroupItem,
|
|
24
|
+
Text,
|
|
25
|
+
Textarea,
|
|
26
|
+
} from "@working-theory/ui/primitives";
|
|
27
|
+
import type { MemberMeetingType, SchedulingSlot } from "@working-theory/validation";
|
|
28
|
+
import {
|
|
29
|
+
startTransition,
|
|
30
|
+
useActionState,
|
|
31
|
+
useId,
|
|
32
|
+
useMemo,
|
|
33
|
+
useRef,
|
|
34
|
+
useState,
|
|
35
|
+
type FormEvent,
|
|
36
|
+
type ReactElement,
|
|
37
|
+
} from "react";
|
|
38
|
+
|
|
39
|
+
import { bookService, readMeetingTypes, readSlots, type NewActionState } from "../new-actions";
|
|
40
|
+
import {
|
|
41
|
+
NewPopupRefusal,
|
|
42
|
+
NewPopupStatus,
|
|
43
|
+
useNewMenuPopup,
|
|
44
|
+
useOpenRead,
|
|
45
|
+
type NewPopupLabels,
|
|
46
|
+
} from "../new-menu";
|
|
47
|
+
|
|
48
|
+
export interface ScheduleServiceLabels extends NewPopupLabels {
|
|
49
|
+
service: string;
|
|
50
|
+
/** Carries a literal `{minutes}`, filled per type here. */
|
|
51
|
+
duration: string;
|
|
52
|
+
time: string;
|
|
53
|
+
notes: string;
|
|
54
|
+
submit: string;
|
|
55
|
+
none: string;
|
|
56
|
+
suggest: string;
|
|
57
|
+
noSlots: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ScheduleServiceProps {
|
|
61
|
+
locale: string;
|
|
62
|
+
labels: ScheduleServiceLabels;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const SEND_MESSAGE = "send-message";
|
|
66
|
+
|
|
67
|
+
export function ScheduleService({ locale, labels }: ScheduleServiceProps): ReactElement {
|
|
68
|
+
const popup = useNewMenuPopup();
|
|
69
|
+
const [read, retry] = useOpenRead(readMeetingTypes);
|
|
70
|
+
if (read === "loading" || read.kind === "error") {
|
|
71
|
+
return <NewPopupStatus failed={read !== "loading"} labels={labels} onRetry={retry} />;
|
|
72
|
+
}
|
|
73
|
+
if (read.items.length === 0) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="flex flex-col gap-4" data-testid="new-schedule-none">
|
|
76
|
+
<Text as="p" variant="body-sm">
|
|
77
|
+
{labels.none}
|
|
78
|
+
</Text>
|
|
79
|
+
{popup.has(SEND_MESSAGE) ? (
|
|
80
|
+
<div>
|
|
81
|
+
<Button
|
|
82
|
+
variant="outline"
|
|
83
|
+
className="max-md:min-h-11"
|
|
84
|
+
onClick={() => popup.open(SEND_MESSAGE)}
|
|
85
|
+
data-testid="new-schedule-suggest"
|
|
86
|
+
>
|
|
87
|
+
{labels.suggest}
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
90
|
+
) : null}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return <BookingForm types={read.items} locale={locale} labels={labels} />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type Slots = { kind: "idle" | "loading" | "error" } | { kind: "ok"; slots: SchedulingSlot[] };
|
|
98
|
+
|
|
99
|
+
function BookingForm({
|
|
100
|
+
types,
|
|
101
|
+
locale,
|
|
102
|
+
labels,
|
|
103
|
+
}: {
|
|
104
|
+
types: readonly MemberMeetingType[];
|
|
105
|
+
locale: string;
|
|
106
|
+
labels: ScheduleServiceLabels;
|
|
107
|
+
}): ReactElement {
|
|
108
|
+
const popup = useNewMenuPopup();
|
|
109
|
+
const id = useId();
|
|
110
|
+
const timeZone = useMemo(() => Intl.DateTimeFormat().resolvedOptions().timeZone, []);
|
|
111
|
+
const [slug, setSlug] = useState("");
|
|
112
|
+
const [start, setStart] = useState("");
|
|
113
|
+
const [notes, setNotes] = useState("");
|
|
114
|
+
const [slots, setSlots] = useState<Slots>({ kind: "idle" });
|
|
115
|
+
// Only the newest slots read lands: a quick second pick never shows the first's times.
|
|
116
|
+
const latestRead = useRef(0);
|
|
117
|
+
|
|
118
|
+
const loadSlots = (picked: string): void => {
|
|
119
|
+
const readId = ++latestRead.current;
|
|
120
|
+
setSlots({ kind: "loading" });
|
|
121
|
+
const land = (next: Slots): void => {
|
|
122
|
+
if (readId === latestRead.current) setSlots(next);
|
|
123
|
+
};
|
|
124
|
+
readSlots({ slug: picked, timeZone }).then(
|
|
125
|
+
(result) => land(result.kind === "ok" ? { kind: "ok", slots: result.slots } : { kind: "error" }),
|
|
126
|
+
() => land({ kind: "error" }),
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const [state, dispatch, pending] = useActionState<NewActionState, FormData>(
|
|
131
|
+
async (prev, formData) => {
|
|
132
|
+
const next = await bookService(prev, formData);
|
|
133
|
+
if (next.kind === "success") popup.close(true);
|
|
134
|
+
if (next.kind === "error" && next.code === "slot_unavailable") {
|
|
135
|
+
const picked = formData.get("slug");
|
|
136
|
+
setStart("");
|
|
137
|
+
if (typeof picked === "string") loadSlots(picked);
|
|
138
|
+
}
|
|
139
|
+
return next;
|
|
140
|
+
},
|
|
141
|
+
{ kind: "idle" },
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const onSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
const formData = new FormData();
|
|
147
|
+
formData.set("slug", slug);
|
|
148
|
+
formData.set("start", start);
|
|
149
|
+
formData.set("timeZone", timeZone);
|
|
150
|
+
formData.set("notes", notes);
|
|
151
|
+
startTransition(() => dispatch(formData));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const dayFormat = new Intl.DateTimeFormat(locale, {
|
|
155
|
+
weekday: "long",
|
|
156
|
+
month: "long",
|
|
157
|
+
day: "numeric",
|
|
158
|
+
timeZone,
|
|
159
|
+
});
|
|
160
|
+
const timeFormat = new Intl.DateTimeFormat(locale, { timeStyle: "short", timeZone });
|
|
161
|
+
const days: { day: string; slots: { slot: SchedulingSlot; index: number }[] }[] = [];
|
|
162
|
+
if (slots.kind === "ok") {
|
|
163
|
+
slots.slots.forEach((slot, index) => {
|
|
164
|
+
const day = dayFormat.format(new Date(slot.start));
|
|
165
|
+
const last = days[days.length - 1];
|
|
166
|
+
if (last?.day === day) last.slots.push({ slot, index });
|
|
167
|
+
else days.push({ day, slots: [{ slot, index }] });
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<form onSubmit={onSubmit} className="flex flex-col gap-4" data-testid="new-schedule-form">
|
|
173
|
+
<div className="flex flex-col gap-2">
|
|
174
|
+
<Text as="p" id={`${id}-service`} variant="body-sm" className="font-semibold">
|
|
175
|
+
{labels.service}
|
|
176
|
+
</Text>
|
|
177
|
+
<RadioGroup
|
|
178
|
+
aria-labelledby={`${id}-service`}
|
|
179
|
+
value={slug}
|
|
180
|
+
onValueChange={(picked) => {
|
|
181
|
+
setSlug(picked);
|
|
182
|
+
setStart("");
|
|
183
|
+
loadSlots(picked);
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{types.map((type) => (
|
|
187
|
+
<div key={type.slug} className="flex min-h-11 items-center gap-3">
|
|
188
|
+
<RadioGroupItem
|
|
189
|
+
id={`${id}-type-${type.slug}`}
|
|
190
|
+
value={type.slug}
|
|
191
|
+
data-testid={`new-schedule-type-${type.slug}`}
|
|
192
|
+
/>
|
|
193
|
+
<Label htmlFor={`${id}-type-${type.slug}`} className="flex flex-wrap gap-x-2">
|
|
194
|
+
{type.title}{" "}
|
|
195
|
+
<Text as="span" variant="caption">
|
|
196
|
+
{labels.duration.replace("{minutes}", String(type.durationMinutes))}
|
|
197
|
+
</Text>
|
|
198
|
+
</Label>
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
201
|
+
</RadioGroup>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{slots.kind === "loading" || slots.kind === "error" ? (
|
|
205
|
+
<NewPopupStatus
|
|
206
|
+
failed={slots.kind === "error"}
|
|
207
|
+
labels={labels}
|
|
208
|
+
onRetry={() => loadSlots(slug)}
|
|
209
|
+
/>
|
|
210
|
+
) : null}
|
|
211
|
+
{slots.kind === "ok" && slots.slots.length === 0 ? (
|
|
212
|
+
<Text as="p" variant="body-sm" data-testid="new-schedule-no-slots">
|
|
213
|
+
{labels.noSlots}
|
|
214
|
+
</Text>
|
|
215
|
+
) : null}
|
|
216
|
+
{days.length > 0 ? (
|
|
217
|
+
<div className="flex flex-col gap-2">
|
|
218
|
+
<Text as="p" id={`${id}-time`} variant="body-sm" className="font-semibold">
|
|
219
|
+
{labels.time}
|
|
220
|
+
</Text>
|
|
221
|
+
<RadioGroup aria-labelledby={`${id}-time`} value={start} onValueChange={setStart}>
|
|
222
|
+
{days.map(({ day, slots: daySlots }) => (
|
|
223
|
+
<div key={day} className="flex flex-col gap-1">
|
|
224
|
+
<Text as="p" variant="caption">
|
|
225
|
+
{day}
|
|
226
|
+
</Text>
|
|
227
|
+
{daySlots.map(({ slot, index }) => (
|
|
228
|
+
<div key={slot.start} className="flex min-h-11 items-center gap-3">
|
|
229
|
+
<RadioGroupItem
|
|
230
|
+
id={`${id}-slot-${index}`}
|
|
231
|
+
value={slot.start}
|
|
232
|
+
data-testid={`new-schedule-slot-${index}`}
|
|
233
|
+
/>
|
|
234
|
+
<Label htmlFor={`${id}-slot-${index}`}>
|
|
235
|
+
{timeFormat.format(new Date(slot.start))}
|
|
236
|
+
</Label>
|
|
237
|
+
</div>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
))}
|
|
241
|
+
</RadioGroup>
|
|
242
|
+
</div>
|
|
243
|
+
) : null}
|
|
244
|
+
|
|
245
|
+
<div className="flex flex-col gap-2">
|
|
246
|
+
<div className="flex items-baseline gap-2">
|
|
247
|
+
<Label htmlFor={`${id}-notes`}>{labels.notes}</Label>
|
|
248
|
+
<Text as="span" variant="caption">
|
|
249
|
+
{labels.optional}
|
|
250
|
+
</Text>
|
|
251
|
+
</div>
|
|
252
|
+
<Textarea
|
|
253
|
+
id={`${id}-notes`}
|
|
254
|
+
name="notes"
|
|
255
|
+
rows={3}
|
|
256
|
+
value={notes}
|
|
257
|
+
onChange={(event) => setNotes(event.target.value)}
|
|
258
|
+
data-testid="new-schedule-notes"
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{state.kind === "error" ? <NewPopupRefusal code={state.code} labels={labels} /> : null}
|
|
263
|
+
<div>
|
|
264
|
+
<Button
|
|
265
|
+
type="submit"
|
|
266
|
+
disabled={pending || slug === "" || start === ""}
|
|
267
|
+
// 44px on a phone (a touch-first context); the 36px primary from md up.
|
|
268
|
+
className="max-md:min-h-11"
|
|
269
|
+
data-testid="new-schedule-submit"
|
|
270
|
+
>
|
|
271
|
+
{pending ? labels.submitting : labels.submit}
|
|
272
|
+
</Button>
|
|
273
|
+
</div>
|
|
274
|
+
</form>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client"; // a controlled form posting through the New menu's server action
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SendMessage — the New menu's Send message popup (#8233, spec D9, D10): a
|
|
5
|
+
* subject, a body (line breaks kept) and an optional link, posted to
|
|
6
|
+
* `member.sendMessage` by `sendMessage` (`../new-actions.ts`). No name and no
|
|
7
|
+
* email: the server takes both from the session.
|
|
8
|
+
*
|
|
9
|
+
* Every field is controlled and the form posts what the state holds, so a
|
|
10
|
+
* refusal — a non-https link above all — shows its plain message with every
|
|
11
|
+
* typed value still in place. The link is sent as typed; the server's https
|
|
12
|
+
* rule is the only one. A success closes the popup and refreshes the Inbox.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Button, Input, Label, Text, Textarea } from "@working-theory/ui/primitives";
|
|
16
|
+
import { MESSAGE_BODY_MAX, MESSAGE_SUBJECT_MAX } from "@working-theory/validation";
|
|
17
|
+
import {
|
|
18
|
+
startTransition,
|
|
19
|
+
useActionState,
|
|
20
|
+
useId,
|
|
21
|
+
useState,
|
|
22
|
+
type FormEvent,
|
|
23
|
+
type ReactElement,
|
|
24
|
+
} from "react";
|
|
25
|
+
|
|
26
|
+
import { sendMessage, type NewActionState } from "../new-actions";
|
|
27
|
+
import { NewPopupRefusal, useNewMenuPopup, type NewPopupLabels } from "../new-menu";
|
|
28
|
+
|
|
29
|
+
export interface SendMessageLabels extends NewPopupLabels {
|
|
30
|
+
subject: string;
|
|
31
|
+
body: string;
|
|
32
|
+
link: string;
|
|
33
|
+
linkHelp: string;
|
|
34
|
+
submit: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SendMessageProps {
|
|
38
|
+
locale: string;
|
|
39
|
+
labels: SendMessageLabels;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SendMessage({ labels }: SendMessageProps): ReactElement {
|
|
43
|
+
const popup = useNewMenuPopup();
|
|
44
|
+
const id = useId();
|
|
45
|
+
const [subject, setSubject] = useState("");
|
|
46
|
+
const [body, setBody] = useState("");
|
|
47
|
+
const [link, setLink] = useState("");
|
|
48
|
+
const [state, dispatch, pending] = useActionState<NewActionState, FormData>(
|
|
49
|
+
async (prev, formData) => {
|
|
50
|
+
const next = await sendMessage(prev, formData);
|
|
51
|
+
if (next.kind === "success") popup.close(true);
|
|
52
|
+
return next;
|
|
53
|
+
},
|
|
54
|
+
{ kind: "idle" },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const onSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
const formData = new FormData();
|
|
60
|
+
formData.set("subject", subject);
|
|
61
|
+
formData.set("body", body);
|
|
62
|
+
formData.set("link", link);
|
|
63
|
+
startTransition(() => dispatch(formData));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<form onSubmit={onSubmit} className="flex flex-col gap-4" data-testid="new-message-form">
|
|
68
|
+
<div className="flex flex-col gap-2">
|
|
69
|
+
<Label htmlFor={`${id}-subject`}>{labels.subject}</Label>
|
|
70
|
+
<Input
|
|
71
|
+
id={`${id}-subject`}
|
|
72
|
+
name="subject"
|
|
73
|
+
value={subject}
|
|
74
|
+
maxLength={MESSAGE_SUBJECT_MAX}
|
|
75
|
+
required
|
|
76
|
+
onChange={(event) => setSubject(event.target.value)}
|
|
77
|
+
data-testid="new-message-subject"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex flex-col gap-2">
|
|
81
|
+
<Label htmlFor={`${id}-body`}>{labels.body}</Label>
|
|
82
|
+
<Textarea
|
|
83
|
+
id={`${id}-body`}
|
|
84
|
+
name="body"
|
|
85
|
+
value={body}
|
|
86
|
+
rows={5}
|
|
87
|
+
maxLength={MESSAGE_BODY_MAX}
|
|
88
|
+
required
|
|
89
|
+
onChange={(event) => setBody(event.target.value)}
|
|
90
|
+
data-testid="new-message-body"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="flex flex-col gap-2">
|
|
94
|
+
<div className="flex items-baseline gap-2">
|
|
95
|
+
<Label htmlFor={`${id}-link`}>{labels.link}</Label>
|
|
96
|
+
<Text as="span" variant="caption">
|
|
97
|
+
{labels.optional}
|
|
98
|
+
</Text>
|
|
99
|
+
</div>
|
|
100
|
+
<Input
|
|
101
|
+
id={`${id}-link`}
|
|
102
|
+
name="link"
|
|
103
|
+
type="text"
|
|
104
|
+
inputMode="url"
|
|
105
|
+
value={link}
|
|
106
|
+
aria-describedby={`${id}-link-help`}
|
|
107
|
+
onChange={(event) => setLink(event.target.value)}
|
|
108
|
+
data-testid="new-message-link"
|
|
109
|
+
/>
|
|
110
|
+
<Text as="p" id={`${id}-link-help`} variant="caption">
|
|
111
|
+
{labels.linkHelp}
|
|
112
|
+
</Text>
|
|
113
|
+
</div>
|
|
114
|
+
{state.kind === "error" ? <NewPopupRefusal code={state.code} labels={labels} /> : null}
|
|
115
|
+
<div>
|
|
116
|
+
<Button
|
|
117
|
+
type="submit"
|
|
118
|
+
disabled={pending}
|
|
119
|
+
// 44px on a phone (a touch-first context); the 36px primary from md up.
|
|
120
|
+
className="max-md:min-h-11"
|
|
121
|
+
data-testid="new-message-submit"
|
|
122
|
+
>
|
|
123
|
+
{pending ? labels.submitting : labels.submit}
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</form>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client"; // reads the URL fragment, which only the browser has
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BillingAnchor — forwards `/account/general#billing` to `/account/billing`
|
|
5
|
+
* (#8232, spec docs/specs/8232-member-settings-account-general-and-spec.md
|
|
6
|
+
* D8, C6).
|
|
7
|
+
*
|
|
8
|
+
* A URL fragment is never sent to the server, so `/account/settings` cannot
|
|
9
|
+
* redirect `#billing` itself: it redirects to `/account/general`, the browser
|
|
10
|
+
* carries the fragment across that redirect, and this part of General sends
|
|
11
|
+
* a `#billing` on to Billing. `#general`, or no fragment, stays on General.
|
|
12
|
+
* It renders nothing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { localeHref } from "@working-theory/ui";
|
|
16
|
+
import { useRouter } from "next/navigation";
|
|
17
|
+
import { useEffect } from "react";
|
|
18
|
+
|
|
19
|
+
export function BillingAnchor({ locale }: { locale: string }): null {
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (window.location.hash === "#billing") {
|
|
23
|
+
router.replace(localeHref(locale, "/account/billing"));
|
|
24
|
+
}
|
|
25
|
+
}, [locale, router]);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client"; // calls the deletion server action on click and shows the answer in place
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DeletionRequest — User settings › General › Privacy's "Request account
|
|
5
|
+
* deletion" (#8232, spec docs/specs/8232-member-settings-account-general-and-spec.md
|
|
6
|
+
* D5; roadmap D10, #8140 disposition 1).
|
|
7
|
+
*
|
|
8
|
+
* Moved here from `settings/deletion-request.tsx` (#7430) rather than edited
|
|
9
|
+
* in place (C3): `sync-routes` never overwrites a file a scaffold already
|
|
10
|
+
* has, so a synced General importing a changed copy would not compile.
|
|
11
|
+
*
|
|
12
|
+
* Who takes it is `via` (General's `loadProfileSource`, D3):
|
|
13
|
+
*
|
|
14
|
+
* - `member` — the member API answers: Request calls the
|
|
15
|
+
* `requestAccountDeletion` server action (`POST /v1/member/deletion-request`)
|
|
16
|
+
* with an empty body, so the API's own subject and message go to the
|
|
17
|
+
* operator. The API files it with the deployment that holds the account
|
|
18
|
+
* and answers which operator that is; the success line follows that
|
|
19
|
+
* answer, so nothing here tests the topology. An operator completes it —
|
|
20
|
+
* nothing is deleted on click.
|
|
21
|
+
* - `session` — a build with no member API: a pointer to Support, which
|
|
22
|
+
* files into this site's own operator inbox.
|
|
23
|
+
* - `none` — a hub-mode spoke whose hub answers no member route: the
|
|
24
|
+
* unavailable line alone. No Support pointer, because Support on a spoke
|
|
25
|
+
* files into the spoke's own inbox, and a request about a hub account
|
|
26
|
+
* must never land there (SEC-7894-11).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { Button, InlineMessage, Link, Text } from "@working-theory/ui/primitives";
|
|
30
|
+
import { useState, useTransition, type ReactElement } from "react";
|
|
31
|
+
|
|
32
|
+
import { requestAccountDeletion, type AccountDeletionResult } from "../profile-actions";
|
|
33
|
+
|
|
34
|
+
export interface DeletionRequestLabels {
|
|
35
|
+
lead: string;
|
|
36
|
+
action: string;
|
|
37
|
+
sentSite: string;
|
|
38
|
+
sentHub: string;
|
|
39
|
+
unverified: string;
|
|
40
|
+
unavailable: string;
|
|
41
|
+
error: string;
|
|
42
|
+
viaSupport: string;
|
|
43
|
+
supportLink: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function DeletionRequest({
|
|
47
|
+
via,
|
|
48
|
+
supportHref,
|
|
49
|
+
labels,
|
|
50
|
+
}: {
|
|
51
|
+
via: "member" | "session" | "none";
|
|
52
|
+
/** This site's own Support page (`/account/support`, localized). */
|
|
53
|
+
supportHref: string;
|
|
54
|
+
labels: DeletionRequestLabels;
|
|
55
|
+
}): ReactElement {
|
|
56
|
+
const [result, setResult] = useState<AccountDeletionResult | null>(null);
|
|
57
|
+
const [pending, startTransition] = useTransition();
|
|
58
|
+
|
|
59
|
+
if (via === "none") {
|
|
60
|
+
return (
|
|
61
|
+
<div data-testid="account-deletion-unavailable">
|
|
62
|
+
<Text as="p" variant="body-sm" tone="muted">
|
|
63
|
+
{labels.unavailable}
|
|
64
|
+
</Text>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (via === "session") {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col items-start gap-2">
|
|
72
|
+
<Text as="p" variant="body-sm" tone="muted">
|
|
73
|
+
{labels.viaSupport}
|
|
74
|
+
</Text>
|
|
75
|
+
<Link
|
|
76
|
+
href={supportHref}
|
|
77
|
+
className="inline-flex min-h-6 items-center"
|
|
78
|
+
data-testid="account-deletion-support"
|
|
79
|
+
>
|
|
80
|
+
{labels.supportLink}
|
|
81
|
+
</Link>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (result?.ok) {
|
|
87
|
+
return (
|
|
88
|
+
<InlineMessage variant="success" role="status" className="text-body-sm">
|
|
89
|
+
{result.sentTo === "hub-operator" ? labels.sentHub : labels.sentSite}
|
|
90
|
+
</InlineMessage>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const error =
|
|
95
|
+
result === null
|
|
96
|
+
? null
|
|
97
|
+
: result.reason === "unverified"
|
|
98
|
+
? labels.unverified
|
|
99
|
+
: result.reason === "unavailable"
|
|
100
|
+
? labels.unavailable
|
|
101
|
+
: labels.error;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex flex-col items-start gap-2">
|
|
105
|
+
<Text as="p" variant="body-sm" tone="muted">
|
|
106
|
+
{labels.lead}
|
|
107
|
+
</Text>
|
|
108
|
+
<Button
|
|
109
|
+
type="button"
|
|
110
|
+
variant="outline"
|
|
111
|
+
// #8339 D8 — the Admin's pill Save shape, now that deletion sits on
|
|
112
|
+
// the shared General composition's pane.
|
|
113
|
+
className="!h-11 md:!h-10 !rounded-full !px-5"
|
|
114
|
+
disabled={pending}
|
|
115
|
+
onClick={() => {
|
|
116
|
+
startTransition(async () => {
|
|
117
|
+
try {
|
|
118
|
+
setResult(await requestAccountDeletion());
|
|
119
|
+
} catch {
|
|
120
|
+
// A network failure throws rather than answering a reason.
|
|
121
|
+
setResult({ ok: false, reason: "failed" });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}}
|
|
125
|
+
data-testid="account-deletion-request"
|
|
126
|
+
>
|
|
127
|
+
{labels.action}
|
|
128
|
+
</Button>
|
|
129
|
+
{error !== null ? <InlineMessage variant="error">{error}</InlineMessage> : null}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /[locale]/account/general — User settings › General (#8339 D8; builds on
|
|
3
|
+
* #8232 D2).
|
|
4
|
+
*
|
|
5
|
+
* RSC, inside the account layout's SignedInShell with the rail visible, in
|
|
6
|
+
* the member's four-tab settings frame (D7). Renders the shared
|
|
7
|
+
* `AccountSettings` composition (@working-theory/templates/account, spec
|
|
8
|
+
* 8339 D4) — the same General form the Admin renders — through the client
|
|
9
|
+
* adapter `GeneralSettings` (`profile-form.tsx`). Below the form:
|
|
10
|
+
*
|
|
11
|
+
* - Request account deletion (`DeletionRequest`, #8232 D5);
|
|
12
|
+
* - any section a module contributes (`accountSettingsSections`);
|
|
13
|
+
* - `BillingAnchor`, which forwards a `#billing` fragment to Billing.
|
|
14
|
+
*
|
|
15
|
+
* Sign-in moved to Security and cookie settings to Notifications (#8339
|
|
16
|
+
* C6) — General no longer renders either.
|
|
17
|
+
*
|
|
18
|
+
* Whether the profile and deletion go through the member API is
|
|
19
|
+
* `loadProfileSource` (D3), never a topology test here.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { AccountSettingsFrame, AccountSettingsPane } from "@working-theory/templates/account";
|
|
23
|
+
import { localeHref } from "@working-theory/ui";
|
|
24
|
+
import { Separator } from "@working-theory/ui/primitives";
|
|
25
|
+
import type { Metadata } from "next";
|
|
26
|
+
import { getTranslations } from "next-intl/server";
|
|
27
|
+
import type { ReactElement } from "react";
|
|
28
|
+
|
|
29
|
+
import { accountSettingsSections } from "../../../../../lib/account-config";
|
|
30
|
+
import { accountSession } from "../account-data";
|
|
31
|
+
import { siteThemeModeCount } from "../settings/theme-modes";
|
|
32
|
+
import { memberSettingsFrame, SettingsSection, type Translate } from "../settings-frame";
|
|
33
|
+
|
|
34
|
+
import { BillingAnchor } from "./billing-anchor";
|
|
35
|
+
import { DeletionRequest } from "./deletion-request";
|
|
36
|
+
import { GeneralSettings } from "./profile-form";
|
|
37
|
+
import { loadProfileSource } from "./profile-source";
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
params: Promise<{ locale: string }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
44
|
+
const { locale } = await params;
|
|
45
|
+
const t = await getTranslations({ locale, namespace: "account" });
|
|
46
|
+
return { title: t("settings.general.title"), description: t("settings.general.lead") };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default async function AccountGeneralPage({ params }: Props): Promise<ReactElement> {
|
|
50
|
+
const { locale } = await params;
|
|
51
|
+
const t = await getTranslations({ locale, namespace: "account" });
|
|
52
|
+
const session = await accountSession(locale);
|
|
53
|
+
|
|
54
|
+
// Unreachable behind the (authed) gate; a session that expired between
|
|
55
|
+
// the layout and the page renders the pane's title alone.
|
|
56
|
+
if (!session) {
|
|
57
|
+
return (
|
|
58
|
+
<AccountSettingsFrame {...memberSettingsFrame(locale, t as unknown as Translate, "general")} name="">
|
|
59
|
+
<AccountSettingsPane
|
|
60
|
+
id="account-settings-general"
|
|
61
|
+
title={t("settings.general.title")}
|
|
62
|
+
lead={t("settings.general.lead")}
|
|
63
|
+
>
|
|
64
|
+
{null}
|
|
65
|
+
</AccountSettingsPane>
|
|
66
|
+
</AccountSettingsFrame>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { ctx } = session;
|
|
71
|
+
const source = await loadProfileSource(ctx);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<GeneralSettings
|
|
75
|
+
frame={memberSettingsFrame(locale, t as unknown as Translate, "general")}
|
|
76
|
+
user={{ name: ctx.user.name ?? "", email: ctx.user.email, image: source.image }}
|
|
77
|
+
labels={{
|
|
78
|
+
title: t("settings.general.title"),
|
|
79
|
+
lead: t("settings.general.lead"),
|
|
80
|
+
colorMode: t("settings.general.colorMode"),
|
|
81
|
+
light: t("settings.general.light"),
|
|
82
|
+
auto: t("settings.general.auto"),
|
|
83
|
+
dark: t("settings.general.dark"),
|
|
84
|
+
emailLabel: t("settings.general.email"),
|
|
85
|
+
nameLabel: t("settings.general.name"),
|
|
86
|
+
photoLabel: t("settings.general.photo"),
|
|
87
|
+
photoHint: t("settings.general.photoHint"),
|
|
88
|
+
photoPlaceholder: t("settings.general.photoPlaceholder"),
|
|
89
|
+
saveButton: t("settings.general.save"),
|
|
90
|
+
saving: t("settings.general.saving"),
|
|
91
|
+
saved: t("settings.general.saved"),
|
|
92
|
+
saveError: t("settings.general.saveError"),
|
|
93
|
+
nameRequired: t("settings.general.nameRequired"),
|
|
94
|
+
photoInvalid: t("settings.general.photoInvalid"),
|
|
95
|
+
unverified: t("settings.general.unverified"),
|
|
96
|
+
unavailable: t("settings.general.unavailable"),
|
|
97
|
+
}}
|
|
98
|
+
colorMode={
|
|
99
|
+
siteThemeModeCount() > 1
|
|
100
|
+
? { cookieName: process.env.NEXT_PUBLIC_THEME_COOKIE_NAME ?? "ailk-theme" }
|
|
101
|
+
: null
|
|
102
|
+
}
|
|
103
|
+
via={source.via}
|
|
104
|
+
>
|
|
105
|
+
<Separator />
|
|
106
|
+
<div data-testid="account-deletion">
|
|
107
|
+
<DeletionRequest
|
|
108
|
+
via={source.via}
|
|
109
|
+
supportHref={localeHref(locale, "/account/support")}
|
|
110
|
+
labels={{
|
|
111
|
+
lead: t("settings.privacy.deletionLead"),
|
|
112
|
+
action: t("settings.privacy.deletion"),
|
|
113
|
+
sentSite: t("settings.privacy.deletionSentSite"),
|
|
114
|
+
sentHub: t("settings.privacy.deletionSentHub"),
|
|
115
|
+
unverified: t("settings.privacy.deletionUnverified"),
|
|
116
|
+
unavailable: t("settings.privacy.deletionUnavailable"),
|
|
117
|
+
error: t("settings.privacy.deletionError"),
|
|
118
|
+
viaSupport: t("settings.privacy.deletionViaSupport"),
|
|
119
|
+
supportLink: t("settings.privacy.deletionSupportLink"),
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
{accountSettingsSections.map((section) => (
|
|
124
|
+
<SettingsSection key={section.id} id={section.id} title={ctx.t(section.title)}>
|
|
125
|
+
{section.render(ctx)}
|
|
126
|
+
</SettingsSection>
|
|
127
|
+
))}
|
|
128
|
+
<BillingAnchor locale={locale} />
|
|
129
|
+
</GeneralSettings>
|
|
130
|
+
);
|
|
131
|
+
}
|