@rapidmx/web-client 0.2.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.
Files changed (168) hide show
  1. package/LICENSE +382 -0
  2. package/apps/admin/_404.tsx +9 -0
  3. package/apps/admin/_500.tsx +9 -0
  4. package/apps/admin/_layout.tsx +41 -0
  5. package/apps/admin/audit-log/index.tsx +187 -0
  6. package/apps/admin/branding/index.tsx +404 -0
  7. package/apps/admin/distribution-lists/[uid].tsx +70 -0
  8. package/apps/admin/distribution-lists/index.tsx +115 -0
  9. package/apps/admin/distribution-lists/new/index.tsx +114 -0
  10. package/apps/admin/domains/[uid].tsx +259 -0
  11. package/apps/admin/domains/index.tsx +125 -0
  12. package/apps/admin/domains/new/index.tsx +84 -0
  13. package/apps/admin/index.tsx +82 -0
  14. package/apps/admin/ingest-queue/index.tsx +116 -0
  15. package/apps/admin/mailboxes/[uid].tsx +126 -0
  16. package/apps/admin/mailboxes/new/index.tsx +291 -0
  17. package/apps/admin/quarantine/index.tsx +171 -0
  18. package/apps/admin/transport-rules/[uid].tsx +143 -0
  19. package/apps/admin/transport-rules/_transportRuleConfig.tsx +75 -0
  20. package/apps/admin/transport-rules/index.tsx +117 -0
  21. package/apps/admin/transport-rules/new/index.tsx +105 -0
  22. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +117 -0
  23. package/apps/shared/components/admin/layout/AdminShell.tsx +186 -0
  24. package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +73 -0
  25. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +154 -0
  26. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +124 -0
  27. package/apps/shared/components/booking/AvailabilityEditor.tsx +118 -0
  28. package/apps/shared/components/calendar/CalendarListSidebar.tsx +119 -0
  29. package/apps/shared/components/calendar/EventModal.tsx +513 -0
  30. package/apps/shared/components/calendar/MonthView.tsx +139 -0
  31. package/apps/shared/components/calendar/RecurrenceEditor.tsx +180 -0
  32. package/apps/shared/components/calendar/ResourcePicker.tsx +80 -0
  33. package/apps/shared/components/calendar/SplitDayView.tsx +137 -0
  34. package/apps/shared/components/calendar/TimeGridView.tsx +198 -0
  35. package/apps/shared/components/calendar/layout/CalendarShell.tsx +211 -0
  36. package/apps/shared/components/contacts/ContactDetailPane.tsx +99 -0
  37. package/apps/shared/components/contacts/ContactForm.tsx +314 -0
  38. package/apps/shared/components/contacts/ContactsSidebar.tsx +242 -0
  39. package/apps/shared/components/contacts/ContactsToolbar.tsx +115 -0
  40. package/apps/shared/components/contacts/layout/ContactsShell.tsx +197 -0
  41. package/apps/shared/components/layout/AppShell.tsx +165 -0
  42. package/apps/shared/components/layout/BrandingChrome.tsx +25 -0
  43. package/apps/shared/components/layout/MailboxProvisioning.tsx +110 -0
  44. package/apps/shared/components/layout/UserMenu.tsx +150 -0
  45. package/apps/shared/components/mail/ConversationList.tsx +61 -0
  46. package/apps/shared/components/mail/ConversationThreadPane.tsx +174 -0
  47. package/apps/shared/components/mail/MessageDetailPane.tsx +390 -0
  48. package/apps/shared/components/mail/compose/ComposeContext.tsx +114 -0
  49. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +418 -0
  50. package/apps/shared/components/mail/compose/ComposeWindow.tsx +518 -0
  51. package/apps/shared/components/mail/compose/EmojiPicker.tsx +64 -0
  52. package/apps/shared/components/mail/compose/GifPicker.tsx +82 -0
  53. package/apps/shared/components/mail/compose/RichTextEditor.tsx +83 -0
  54. package/apps/shared/components/mail/compose/ScheduleSendPicker.tsx +64 -0
  55. package/apps/shared/components/mail/layout/MailShell.tsx +287 -0
  56. package/apps/shared/components/rules/RuleBuilder.tsx +276 -0
  57. package/apps/shared/components/settings/layout/SettingsShell.tsx +223 -0
  58. package/apps/shared/components/tasks/TasksSidebar.tsx +201 -0
  59. package/apps/shared/components/tasks/TasksToolbar.tsx +68 -0
  60. package/apps/shared/components/tasks/layout/TasksShell.tsx +199 -0
  61. package/apps/shared/styles/app.css +265 -0
  62. package/apps/www/_404.tsx +9 -0
  63. package/apps/www/_500.tsx +9 -0
  64. package/apps/www/_layout.tsx +45 -0
  65. package/apps/www/calendar/index.tsx +407 -0
  66. package/apps/www/contacts/[uid].tsx +88 -0
  67. package/apps/www/contacts/index.tsx +436 -0
  68. package/apps/www/index.tsx +375 -0
  69. package/apps/www/messages/[uid].tsx +70 -0
  70. package/apps/www/settings/auto-reply/index.tsx +129 -0
  71. package/apps/www/settings/booking-types/[uid].tsx +300 -0
  72. package/apps/www/settings/booking-types/index.tsx +98 -0
  73. package/apps/www/settings/booking-types/new/index.tsx +212 -0
  74. package/apps/www/settings/filters/[uid].tsx +155 -0
  75. package/apps/www/settings/filters/_mailFilterRuleConfig.tsx +102 -0
  76. package/apps/www/settings/filters/index.tsx +99 -0
  77. package/apps/www/settings/filters/new/index.tsx +126 -0
  78. package/apps/www/settings/focused-inbox/index.tsx +148 -0
  79. package/apps/www/settings/read-receipts/index.tsx +125 -0
  80. package/apps/www/settings/signatures/[uid].tsx +167 -0
  81. package/apps/www/settings/signatures/index.tsx +85 -0
  82. package/apps/www/settings/signatures/new/index.tsx +129 -0
  83. package/apps/www/settings/signatures/signatureDefaults.ts +36 -0
  84. package/apps/www/tasks/index.tsx +530 -0
  85. package/dist/apps/admin/_404.js +4 -0
  86. package/dist/apps/admin/_500.js +4 -0
  87. package/dist/apps/admin/_layout.js +10 -0
  88. package/dist/apps/admin/audit-log/index.js +66 -0
  89. package/dist/apps/admin/branding/index.js +126 -0
  90. package/dist/apps/admin/distribution-lists/[uid].js +34 -0
  91. package/dist/apps/admin/distribution-lists/index.js +31 -0
  92. package/dist/apps/admin/distribution-lists/new/index.js +51 -0
  93. package/dist/apps/admin/domains/[uid].js +102 -0
  94. package/dist/apps/admin/domains/index.js +31 -0
  95. package/dist/apps/admin/domains/new/index.js +41 -0
  96. package/dist/apps/admin/index.js +32 -0
  97. package/dist/apps/admin/ingest-queue/index.js +48 -0
  98. package/dist/apps/admin/mailboxes/[uid].js +59 -0
  99. package/dist/apps/admin/mailboxes/new/index.js +93 -0
  100. package/dist/apps/admin/quarantine/index.js +66 -0
  101. package/dist/apps/admin/transport-rules/[uid].js +87 -0
  102. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +37 -0
  103. package/dist/apps/admin/transport-rules/index.js +32 -0
  104. package/dist/apps/admin/transport-rules/new/index.js +50 -0
  105. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +64 -0
  106. package/dist/apps/shared/components/admin/layout/AdminShell.js +94 -0
  107. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +16 -0
  108. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +59 -0
  109. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +67 -0
  110. package/dist/apps/shared/components/booking/AvailabilityEditor.js +45 -0
  111. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +44 -0
  112. package/dist/apps/shared/components/calendar/EventModal.js +167 -0
  113. package/dist/apps/shared/components/calendar/MonthView.js +38 -0
  114. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +55 -0
  115. package/dist/apps/shared/components/calendar/ResourcePicker.js +36 -0
  116. package/dist/apps/shared/components/calendar/SplitDayView.js +44 -0
  117. package/dist/apps/shared/components/calendar/TimeGridView.js +58 -0
  118. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +96 -0
  119. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -0
  120. package/dist/apps/shared/components/contacts/ContactForm.js +85 -0
  121. package/dist/apps/shared/components/contacts/ContactsSidebar.js +105 -0
  122. package/dist/apps/shared/components/contacts/ContactsToolbar.js +32 -0
  123. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +93 -0
  124. package/dist/apps/shared/components/layout/AppShell.js +68 -0
  125. package/dist/apps/shared/components/layout/BrandingChrome.js +18 -0
  126. package/dist/apps/shared/components/layout/MailboxProvisioning.js +75 -0
  127. package/dist/apps/shared/components/layout/UserMenu.js +59 -0
  128. package/dist/apps/shared/components/mail/ConversationList.js +17 -0
  129. package/dist/apps/shared/components/mail/ConversationThreadPane.js +112 -0
  130. package/dist/apps/shared/components/mail/MessageDetailPane.js +153 -0
  131. package/dist/apps/shared/components/mail/compose/ComposeContext.js +55 -0
  132. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +88 -0
  133. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +261 -0
  134. package/dist/apps/shared/components/mail/compose/EmojiPicker.js +25 -0
  135. package/dist/apps/shared/components/mail/compose/GifPicker.js +36 -0
  136. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +60 -0
  137. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.js +33 -0
  138. package/dist/apps/shared/components/mail/layout/MailShell.js +139 -0
  139. package/dist/apps/shared/components/rules/RuleBuilder.js +78 -0
  140. package/dist/apps/shared/components/settings/layout/SettingsShell.js +100 -0
  141. package/dist/apps/shared/components/tasks/TasksSidebar.js +81 -0
  142. package/dist/apps/shared/components/tasks/TasksToolbar.js +15 -0
  143. package/dist/apps/shared/components/tasks/layout/TasksShell.js +92 -0
  144. package/dist/apps/shared/styles/app.css +265 -0
  145. package/dist/apps/www/_404.js +4 -0
  146. package/dist/apps/www/_500.js +4 -0
  147. package/dist/apps/www/_layout.js +8 -0
  148. package/dist/apps/www/calendar/index.js +228 -0
  149. package/dist/apps/www/contacts/[uid].js +62 -0
  150. package/dist/apps/www/contacts/index.js +262 -0
  151. package/dist/apps/www/index.js +228 -0
  152. package/dist/apps/www/messages/[uid].js +47 -0
  153. package/dist/apps/www/settings/auto-reply/index.js +54 -0
  154. package/dist/apps/www/settings/booking-types/[uid].js +129 -0
  155. package/dist/apps/www/settings/booking-types/index.js +29 -0
  156. package/dist/apps/www/settings/booking-types/new/index.js +86 -0
  157. package/dist/apps/www/settings/filters/[uid].js +94 -0
  158. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.js +65 -0
  159. package/dist/apps/www/settings/filters/index.js +30 -0
  160. package/dist/apps/www/settings/filters/new/index.js +61 -0
  161. package/dist/apps/www/settings/focused-inbox/index.js +69 -0
  162. package/dist/apps/www/settings/read-receipts/index.js +51 -0
  163. package/dist/apps/www/settings/signatures/[uid].js +96 -0
  164. package/dist/apps/www/settings/signatures/index.js +34 -0
  165. package/dist/apps/www/settings/signatures/new/index.js +63 -0
  166. package/dist/apps/www/settings/signatures/signatureDefaults.js +30 -0
  167. package/dist/apps/www/tasks/index.js +280 -0
  168. package/package.json +87 -0
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useRef, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { deleteBrandingIcon, deleteBrandingLogo, deleteBrandingStylesheet, getBranding, updateBranding, uploadBrandingIcon, uploadBrandingLogo, uploadBrandingStylesheet, } from "@rapidmx/react-shared/branding/brandingApi.js";
9
+ import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
+ const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
13
+ const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
14
+ export default function BrandingPage(props) {
15
+ return (_jsx(AdminShell, { ...props, active: "branding", children: _jsx(BrandingContent, {}) }));
16
+ }
17
+ function BrandingContent() {
18
+ const [branding, setBranding] = useState(null);
19
+ const [loading, setLoading] = useState(true);
20
+ const [loadError, setLoadError] = useState(null);
21
+ useEffect(() => {
22
+ getBranding()
23
+ .then(setBranding)
24
+ .catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load branding."))
25
+ .finally(() => setLoading(false));
26
+ }, []);
27
+ if (loading) {
28
+ return _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" });
29
+ }
30
+ if (loadError || !branding) {
31
+ return _jsx(Alert, { children: loadError });
32
+ }
33
+ return _jsx(BrandingForm, { branding: branding, onChange: setBranding });
34
+ }
35
+ function BrandingForm({ branding, onChange }) {
36
+ const [companyName, setCompanyName] = useState(branding.companyName);
37
+ const [title, setTitle] = useState(branding.title);
38
+ const [headerHtml, setHeaderHtml] = useState(branding.headerHtml ?? "");
39
+ const [footerHtml, setFooterHtml] = useState(branding.footerHtml ?? "");
40
+ const [logoUrlInput, setLogoUrlInput] = useState(branding.logoUrl ?? "");
41
+ const [iconUrlInput, setIconUrlInput] = useState(branding.iconUrl ?? "");
42
+ const [stylesheetUrlInput, setStylesheetUrlInput] = useState(branding.stylesheetUrl ?? "");
43
+ const [error, setError] = useState(null);
44
+ const [saving, setSaving] = useState(false);
45
+ const [saved, setSaved] = useState(false);
46
+ const [assetBusy, setAssetBusy] = useState(null);
47
+ const logoFileRef = useRef(null);
48
+ const iconFileRef = useRef(null);
49
+ const stylesheetFileRef = useRef(null);
50
+ function runAsset(name, action) {
51
+ setError(null);
52
+ setAssetBusy(name);
53
+ return action()
54
+ .then((updated) => {
55
+ onChange(updated);
56
+ setLogoUrlInput(updated.logoUrl ?? "");
57
+ setIconUrlInput(updated.iconUrl ?? "");
58
+ setStylesheetUrlInput(updated.stylesheetUrl ?? "");
59
+ })
60
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not update branding."))
61
+ .finally(() => setAssetBusy(null));
62
+ }
63
+ function handleLogoFileChange(e) {
64
+ const file = e.target.files?.[0];
65
+ if (!file)
66
+ return;
67
+ if (file.size > MAX_UPLOAD_BYTES) {
68
+ setError(`"${file.name}" is too large — logos must be 5MB or smaller.`);
69
+ }
70
+ else {
71
+ void runAsset("logo-upload", () => uploadBrandingLogo(file));
72
+ }
73
+ e.target.value = "";
74
+ }
75
+ function handleIconFileChange(e) {
76
+ const file = e.target.files?.[0];
77
+ if (!file)
78
+ return;
79
+ if (file.size > MAX_UPLOAD_BYTES) {
80
+ setError(`"${file.name}" is too large — icons must be 5MB or smaller.`);
81
+ }
82
+ else {
83
+ void runAsset("icon-upload", () => uploadBrandingIcon(file));
84
+ }
85
+ e.target.value = "";
86
+ }
87
+ function handleStylesheetFileChange(e) {
88
+ const file = e.target.files?.[0];
89
+ if (!file)
90
+ return;
91
+ if (file.size > MAX_UPLOAD_BYTES) {
92
+ setError(`"${file.name}" is too large — stylesheets must be 5MB or smaller.`);
93
+ }
94
+ else {
95
+ void runAsset("stylesheet-upload", () => uploadBrandingStylesheet(file));
96
+ }
97
+ e.target.value = "";
98
+ }
99
+ async function handleSubmit(e) {
100
+ e.preventDefault();
101
+ setError(null);
102
+ setSaved(false);
103
+ setSaving(true);
104
+ try {
105
+ const updated = await updateBranding({ companyName, title, headerHtml, footerHtml });
106
+ onChange(updated);
107
+ setSaved(true);
108
+ }
109
+ catch (err) {
110
+ setError(err instanceof ApiRequestError ? err.message : "Could not save branding.");
111
+ }
112
+ finally {
113
+ setSaving(false);
114
+ }
115
+ }
116
+ return (_jsxs("div", { className: "max-w-2xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Branding" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Customize the logo, nav-header icon, product name, and chrome shown to every visitor of the webmail and admin console \u2014 including anonymous booking-page visitors." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Logo" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.logoUrl || "/images/logo.svg", alt: "Current logo", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "logo-upload", disabled: assetBusy !== null, onClick: () => logoFileRef.current?.click(), children: "Upload logo" }), _jsx("input", { ref: logoFileRef, type: "file", accept: "image/*", "aria-label": "Upload logo", className: "hidden", onChange: handleLogoFileChange }), branding.logoUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "logo-delete", onClick: () => void runAsset("logo-delete", async () => {
117
+ await deleteBrandingLogo();
118
+ return { ...branding, logoUrl: undefined };
119
+ }), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Icon" }), _jsx("p", { className: "text-xs text-text-muted -mt-1", children: "A compact mark for navigation headers, independent of the full logo above. Falls back to the logo, then a default asset, when not set." }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.iconUrl || branding.logoUrl || "/images/logo.svg", alt: "Current icon", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "icon-upload", disabled: assetBusy !== null, onClick: () => iconFileRef.current?.click(), children: "Upload icon" }), _jsx("input", { ref: iconFileRef, type: "file", accept: "image/*", "aria-label": "Upload icon", className: "hidden", onChange: handleIconFileChange }), branding.iconUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "icon-delete", onClick: () => void runAsset("icon-delete", async () => {
120
+ await deleteBrandingIcon();
121
+ return { ...branding, iconUrl: undefined };
122
+ }), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
123
+ await deleteBrandingStylesheet();
124
+ return { ...branding, stylesheetUrl: undefined };
125
+ }), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
126
+ }
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { getDistributionList } from "@rapidmx/react-shared/admin/distributionListsApi.js";
9
+ import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import MemberListCard from "../../shared/components/admin/distributionLists/MemberListCard.js";
11
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
+ export default function DistributionListDetailPage(props) {
13
+ return (_jsx(AdminShell, { ...props, active: "distributionLists", children: _jsx(DistributionListDetailContent, { uid: props.params.uid }) }));
14
+ }
15
+ function DistributionListDetailContent({ uid }) {
16
+ const [list, setList] = useState(null);
17
+ const [loading, setLoading] = useState(true);
18
+ const [error, setError] = useState(null);
19
+ useEffect(() => {
20
+ setLoading(true);
21
+ setError(null);
22
+ getDistributionList(uid)
23
+ .then(setList)
24
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this distribution list."))
25
+ .finally(() => setLoading(false));
26
+ }, [uid]);
27
+ if (loading) {
28
+ return _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" });
29
+ }
30
+ if (error || !list) {
31
+ return _jsx(Alert, { children: error ?? "Distribution list not found." });
32
+ }
33
+ return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/distribution-lists", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All distribution lists" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: list.primarySmtpAddress })] }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Name" }), _jsx("dd", { children: list.name }), _jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: list.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: list.ownerUserUid ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: list.aliasAddresses && list.aliasAddresses.length > 0 ? list.aliasAddresses.join(", ") : "None" }), _jsx("dt", { className: "text-text-muted", children: "Restrict senders" }), _jsx("dd", { children: list.restrictSenders ? "Only members may send" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Created" }), _jsx("dd", { children: new Date(list.dateCreated).toLocaleString() })] }) }), _jsx(MemberListCard, { list: list, onUpdate: setList })] }));
34
+ }
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { listDistributionLists } from "@rapidmx/react-shared/admin/distributionListsApi.js";
9
+ import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
+ const PAGE_SIZE = 25;
13
+ export default function DistributionListsPage(props) {
14
+ return (_jsx(AdminShell, { ...props, active: "distributionLists", children: _jsx(DistributionListsContent, {}) }));
15
+ }
16
+ function DistributionListsContent() {
17
+ const [page, setPage] = useState(0);
18
+ const [lists, setLists] = useState([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [error, setError] = useState(null);
21
+ useEffect(() => {
22
+ setLoading(true);
23
+ setError(null);
24
+ listDistributionLists({ page, limit: PAGE_SIZE })
25
+ .then(setLists)
26
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load distribution lists."))
27
+ .finally(() => setLoading(false));
28
+ }, [page]);
29
+ const hasNextPage = lists.length === PAGE_SIZE;
30
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Distribution lists" }), _jsx("a", { href: "/admin/distribution-lists/new", children: _jsx(Button, { type: "button", className: "!w-auto", children: "+ New distribution list" }) })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : lists.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No distribution lists yet." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Address", "Name", "Members", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: lists.map((list) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: list.primarySmtpAddress }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: list.name }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: list.memberAddresses.length }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-right", children: _jsx("a", { href: `/admin/distribution-lists/${encodeURIComponent(list.uid)}`, className: "text-primary-dark hover:underline font-medium", children: "View" }) })] }, list.uid))) })] }) })), _jsxs("div", { className: "flex gap-3 items-center mt-4", children: [_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: page === 0 || loading, onClick: () => setPage((p) => Math.max(0, p - 1)), children: "Previous" }), _jsxs("span", { className: "text-sm text-text-muted", children: ["Page ", page + 1] }), _jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: !hasNextPage || loading, onClick: () => setPage((p) => p + 1), children: "Next" })] })] }));
31
+ }
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { createDistributionList } from "@rapidmx/react-shared/admin/distributionListsApi.js";
9
+ import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
+ import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
13
+ const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
14
+ export default function NewDistributionListPage(props) {
15
+ return (_jsx(AdminShell, { ...props, active: "distributionLists", children: _jsx(NewDistributionListForm, {}) }));
16
+ }
17
+ function NewDistributionListForm() {
18
+ const [primarySmtpAddress, setPrimarySmtpAddress] = useState("");
19
+ const [name, setName] = useState("");
20
+ const [description, setDescription] = useState("");
21
+ const [error, setError] = useState(null);
22
+ const [saving, setSaving] = useState(false);
23
+ async function handleSubmit(e) {
24
+ e.preventDefault();
25
+ setError(null);
26
+ if (!primarySmtpAddress.trim()) {
27
+ setError("A primary SMTP address is required.");
28
+ return;
29
+ }
30
+ if (!name.trim()) {
31
+ setError("A name is required.");
32
+ return;
33
+ }
34
+ setSaving(true);
35
+ try {
36
+ const list = await createDistributionList({
37
+ primarySmtpAddress: primarySmtpAddress.trim(),
38
+ name: name.trim(),
39
+ description: description.trim() || undefined,
40
+ });
41
+ window.location.href = `/admin/distribution-lists/${encodeURIComponent(list.uid)}`;
42
+ }
43
+ catch (err) {
44
+ setError(err instanceof ApiRequestError ? err.message : "Could not create the distribution list.");
45
+ }
46
+ finally {
47
+ setSaving(false);
48
+ }
49
+ }
50
+ return (_jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "New distribution list" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Members can be added once the list is created." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Primary SMTP address", htmlFor: "primarySmtpAddress", children: _jsx("input", { id: "primarySmtpAddress", type: "email", className: INPUT_CLASS, value: primarySmtpAddress, onChange: (e) => setPrimarySmtpAddress(e.target.value), placeholder: "team@example.com" }) }), _jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "Team" }) }), _jsx(FormField, { label: "Description (optional)", htmlFor: "description", children: _jsx("input", { id: "description", type: "text", className: INPUT_CLASS, value: description, onChange: (e) => setDescription(e.target.value) }) }), _jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create distribution list" }), _jsx("a", { href: "/admin/distribution-lists", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", children: "Cancel" }) })] })] })] }));
51
+ }
@@ -0,0 +1,102 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { deleteDomain, getDnsSetup, getDomain, verifyDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
9
+ import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
13
+ const RECORD_TYPE_LABELS = {
14
+ ownership: "Ownership (TXT)",
15
+ mx: "MX",
16
+ spf: "SPF",
17
+ dkim: "DKIM",
18
+ dmarc: "DMARC",
19
+ };
20
+ export default function DomainDetailPage(props) {
21
+ return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(DomainDetailContent, { uid: props.params.uid }) }));
22
+ }
23
+ function DomainDetailContent({ uid }) {
24
+ const [domain, setDomain] = useState(null);
25
+ const [dnsSetup, setDnsSetup] = useState([]);
26
+ const [loading, setLoading] = useState(true);
27
+ const [error, setError] = useState(null);
28
+ const [verifying, setVerifying] = useState(false);
29
+ const [copied, setCopied] = useState(false);
30
+ const [confirmingDelete, setConfirmingDelete] = useState(false);
31
+ const [deleting, setDeleting] = useState(false);
32
+ const [deleteError, setDeleteError] = useState(null);
33
+ function reload(id) {
34
+ setLoading(true);
35
+ setError(null);
36
+ Promise.all([getDomain(id), getDnsSetup(id)])
37
+ .then(([d, checks]) => {
38
+ setDomain(d);
39
+ setDnsSetup(checks);
40
+ })
41
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this domain."))
42
+ .finally(() => setLoading(false));
43
+ }
44
+ useEffect(() => {
45
+ reload(uid);
46
+ }, [uid]);
47
+ // Only ever invoked from the "Verify now" button below, which itself only renders once `domain` is
48
+ // resolved (children only render once loaded — see the early returns above) — the non-null assertion
49
+ // reflects that real invariant, not an unchecked assumption. Matches `QuarantineContent.handleRelease`'s
50
+ // identical pattern.
51
+ async function handleVerify() {
52
+ setVerifying(true);
53
+ setError(null);
54
+ try {
55
+ await verifyDomain(domain.uid);
56
+ reload(domain.uid);
57
+ }
58
+ catch (err) {
59
+ setError(err instanceof ApiRequestError ? err.message : "Could not verify this domain.");
60
+ }
61
+ finally {
62
+ setVerifying(false);
63
+ }
64
+ }
65
+ // Only ever invoked from the "Delete domain" confirmation modal below, which itself only renders once
66
+ // `domain` is resolved (children only render once loaded — see the early returns above) — the non-null
67
+ // assertion reflects that real invariant, not an unchecked assumption. Matches `handleVerify`'s identical
68
+ // pattern above.
69
+ function closeDeleteModal() {
70
+ setConfirmingDelete(false);
71
+ }
72
+ async function handleDelete() {
73
+ setDeleting(true);
74
+ setDeleteError(null);
75
+ try {
76
+ await deleteDomain(domain.uid, domain.version);
77
+ window.location.href = "/admin/domains";
78
+ }
79
+ catch (err) {
80
+ setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this domain.");
81
+ setDeleting(false);
82
+ }
83
+ }
84
+ async function handleCopy(value) {
85
+ try {
86
+ await navigator.clipboard.writeText(value);
87
+ setCopied(true);
88
+ setTimeout(() => setCopied(false), 2000);
89
+ }
90
+ catch {
91
+ // Clipboard access can be denied by the browser — the value is still selectable/copyable by hand.
92
+ }
93
+ }
94
+ if (loading) {
95
+ return _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" });
96
+ }
97
+ if (error || !domain) {
98
+ return _jsx(Alert, { children: error ?? "Domain not found." });
99
+ }
100
+ const ownershipCheck = dnsSetup.find((c) => c.type === "ownership");
101
+ return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/domains", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All domains" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: domain.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete domain" })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "flex-1 text-xs bg-surface-alt border border-border rounded-sm py-2 px-3 overflow-x-auto whitespace-nowrap", children: ownershipCheck?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}` }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => handleCopy(ownershipCheck?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`), children: copied ? "Copied" : "Copy" })] }), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Recommended value"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: RECORD_TYPE_LABELS[check.type] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: _jsx("code", { className: "text-xs", children: check.recommendedValue ?? "—" }) })] }, check.type))) })] }) })] })), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete domain", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: domain.name }), "? This cannot be undone."] }), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: closeDeleteModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] })] }));
102
+ }
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { listDomains } from "@rapidmx/react-shared/admin/domainsApi.js";
9
+ import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
+ const PAGE_SIZE = 25;
13
+ export default function DomainsListPage(props) {
14
+ return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(DomainsListContent, {}) }));
15
+ }
16
+ function DomainsListContent() {
17
+ const [page, setPage] = useState(0);
18
+ const [domains, setDomains] = useState([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [error, setError] = useState(null);
21
+ useEffect(() => {
22
+ setLoading(true);
23
+ setError(null);
24
+ listDomains({ page, limit: PAGE_SIZE })
25
+ .then(setDomains)
26
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load domains."))
27
+ .finally(() => setLoading(false));
28
+ }, [page]);
29
+ const hasNextPage = domains.length === PAGE_SIZE;
30
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Domains" }), _jsx("a", { href: "/admin/domains/new", children: _jsx(Button, { type: "button", className: "!w-auto", children: "+ New domain" }) })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : domains.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No domains configured yet." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Name", "Enabled", "Verified", ""].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: domains.map((domain) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: domain.name }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: domain.enabled ? "Yes" : "No" }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-right", children: _jsx("a", { href: `/admin/domains/${encodeURIComponent(domain.uid)}`, className: "text-primary-dark hover:underline font-medium", children: "View" }) })] }, domain.uid))) })] }) })), _jsxs("div", { className: "flex gap-3 items-center mt-4", children: [_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: page === 0 || loading, onClick: () => setPage((p) => Math.max(0, p - 1)), children: "Previous" }), _jsxs("span", { className: "text-sm text-text-muted", children: ["Page ", page + 1] }), _jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: !hasNextPage || loading, onClick: () => setPage((p) => p + 1), children: "Next" })] })] }));
31
+ }
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { createDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
9
+ import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
+ import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
13
+ const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
14
+ export default function NewDomainPage(props) {
15
+ return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(NewDomainForm, {}) }));
16
+ }
17
+ function NewDomainForm() {
18
+ const [name, setName] = useState("");
19
+ const [error, setError] = useState(null);
20
+ const [saving, setSaving] = useState(false);
21
+ async function handleSubmit(e) {
22
+ e.preventDefault();
23
+ setError(null);
24
+ if (!name.trim()) {
25
+ setError("A domain name is required.");
26
+ return;
27
+ }
28
+ setSaving(true);
29
+ try {
30
+ const domain = await createDomain({ name: name.trim() });
31
+ window.location.href = `/admin/domains/${encodeURIComponent(domain.uid)}`;
32
+ }
33
+ catch (err) {
34
+ setError(err instanceof ApiRequestError ? err.message : "Could not create the domain.");
35
+ }
36
+ finally {
37
+ setSaving(false);
38
+ }
39
+ }
40
+ return (_jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "New domain" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "A new domain starts unverified. Once created, its detail page shows the DNS TXT record to publish to prove ownership before mail can be accepted on it." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "bg-surface border border-border rounded-md p-6", children: [_jsx(FormField, { label: "Domain name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "example.com" }) }), _jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create domain" }), _jsx("a", { href: "/admin/domains", children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", children: "Cancel" }) })] })] })] }));
41
+ }
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
9
+ import AdminShell from "../shared/components/admin/layout/AdminShell.js";
10
+ import MailboxTable from "../shared/components/admin/mailboxes/MailboxTable.js";
11
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
13
+ const PAGE_SIZE = 25;
14
+ export default function MailboxesListPage(props) {
15
+ return (_jsx(AdminShell, { ...props, active: "mailboxes", children: _jsx(MailboxesListContent, {}) }));
16
+ }
17
+ function MailboxesListContent() {
18
+ const [page, setPage] = useState(0);
19
+ const [mailboxes, setMailboxes] = useState([]);
20
+ const [loading, setLoading] = useState(true);
21
+ const [error, setError] = useState(null);
22
+ useEffect(() => {
23
+ setLoading(true);
24
+ setError(null);
25
+ listMailboxes({ page, limit: PAGE_SIZE })
26
+ .then(setMailboxes)
27
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load mailboxes."))
28
+ .finally(() => setLoading(false));
29
+ }, [page]);
30
+ const hasNextPage = mailboxes.length === PAGE_SIZE;
31
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Mailboxes" }), _jsx("a", { href: "/admin/mailboxes/new", children: _jsx(Button, { type: "button", className: "!w-auto", children: "+ New mailbox" }) })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(MailboxTable, { mailboxes: mailboxes })), _jsxs("div", { className: "flex gap-3 items-center mt-4", children: [_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: page === 0 || loading, onClick: () => setPage((p) => Math.max(0, p - 1)), children: "Previous" }), _jsxs("span", { className: "text-sm text-text-muted", children: ["Page ", page + 1] }), _jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: !hasNextPage || loading, onClick: () => setPage((p) => p + 1), children: "Next" })] })] }));
32
+ }
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import { listIngestQueue } from "@rapidmx/react-shared/mail/mailApi.js";
9
+ import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ export function readMailboxUid() {
12
+ if (typeof window === "undefined")
13
+ return null;
14
+ return new URLSearchParams(window.location.search).get("mailboxUid");
15
+ }
16
+ const STATUS_STYLES = {
17
+ pending: "bg-surface-alt text-text-muted",
18
+ scanning: "bg-surface-alt text-text-muted",
19
+ delivered: "bg-success text-white",
20
+ failed: "bg-danger text-white",
21
+ };
22
+ export default function IngestQueuePage(props) {
23
+ return (_jsx(AdminShell, { ...props, active: "ingestQueue", children: _jsx(IngestQueueContent, {}) }));
24
+ }
25
+ function IngestQueueContent() {
26
+ const [mailboxUid, setMailboxUid] = useState(null);
27
+ const [entries, setEntries] = useState([]);
28
+ const [loading, setLoading] = useState(true);
29
+ const [error, setError] = useState(null);
30
+ useEffect(() => {
31
+ setMailboxUid(readMailboxUid());
32
+ }, []);
33
+ useEffect(() => {
34
+ if (!mailboxUid) {
35
+ return;
36
+ }
37
+ setLoading(true);
38
+ setError(null);
39
+ listIngestQueue(mailboxUid)
40
+ .then(setEntries)
41
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load the ingest queue."))
42
+ .finally(() => setLoading(false));
43
+ }, [mailboxUid]);
44
+ if (!mailboxUid) {
45
+ return _jsx(Alert, { children: "No mailbox specified. Open a mailbox's detail page and choose \"View ingest queue\"." });
46
+ }
47
+ return (_jsxs("div", { className: "max-w-4xl flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: `/admin/mailboxes/${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 Back to mailbox" }), _jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mt-1", children: "Ingest queue" })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "Nothing pending or failed for this mailbox." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["From", "To", "Received", "Status", "Error"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: entries.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.envelopeFrom }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.envelopeTo.join(", ") }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: new Date(entry.dateCreated).toLocaleString() }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: _jsx("span", { className: `inline-block text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${STATUS_STYLES[entry.status]}`, children: entry.status }) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: entry.errorMessage ?? "" })] }, entry.uid))) })] }) }))] }));
48
+ }