@rapidmx/web-client 0.3.0 → 0.4.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/apps/admin/branding/index.tsx +39 -404
- package/apps/admin/domains/[uid].tsx +92 -259
- package/apps/admin/encryption-policy/index.tsx +19 -0
- package/apps/admin/index.tsx +100 -82
- package/apps/admin/mailbox-policy/index.tsx +19 -0
- package/apps/admin/mailboxes/new/index.tsx +28 -291
- package/apps/admin/plugins/index.tsx +15 -0
- package/apps/admin/retention-policy/index.tsx +39 -132
- package/apps/admin/setup/index.tsx +15 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +249 -210
- package/apps/shared/components/admin/settings/BrandingForm.tsx +374 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +176 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +104 -0
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +302 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +119 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +595 -0
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +98 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +254 -0
- package/apps/shared/components/admin/setup/SetupWizard.tsx +300 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
- package/apps/shared/components/calendar/EventModal.tsx +40 -4
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
- package/apps/shared/components/contacts/ContactForm.tsx +38 -4
- package/apps/shared/components/layout/AppShell.tsx +198 -169
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +298 -265
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +128 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +630 -595
- package/apps/shared/components/mail/compose/ComposeContext.tsx +8 -3
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +930 -765
- package/apps/shared/components/mail/layout/MailShell.tsx +408 -302
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +89 -0
- package/apps/shared/search/localIndexBlockCipher.ts +83 -0
- package/apps/shared/search/localIndexBuilder.ts +179 -0
- package/apps/shared/search/localIndexKey.ts +27 -0
- package/apps/shared/search/localIndexRpcClient.ts +116 -0
- package/apps/shared/search/localIndexSchema.ts +227 -0
- package/apps/shared/search/localIndexSizePreference.ts +88 -0
- package/apps/shared/search/localIndexVFS.ts +235 -0
- package/apps/shared/search/localIndexWorker.ts +464 -0
- package/apps/shared/search/searchTier2.ts +79 -0
- package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
- package/apps/www/calendar/index.tsx +31 -21
- package/apps/www/contacts/index.tsx +33 -6
- package/apps/www/index.tsx +661 -106
- package/apps/www/messages/[uid].tsx +5 -1
- package/apps/www/settings/encryption/index.tsx +63 -1
- package/apps/www/settings/sharing/index.tsx +271 -0
- package/apps/www/tasks/index.tsx +53 -4
- package/dist/apps/admin/branding/index.js +4 -98
- package/dist/apps/admin/domains/[uid].js +5 -68
- package/dist/apps/admin/encryption-policy/index.js +8 -0
- package/dist/apps/admin/index.js +14 -1
- package/dist/apps/admin/mailbox-policy/index.js +8 -0
- package/dist/apps/admin/mailboxes/new/index.js +4 -89
- package/dist/apps/admin/plugins/index.js +6 -0
- package/dist/apps/admin/retention-policy/index.js +3 -36
- package/dist/apps/admin/setup/index.js +6 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +34 -3
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +105 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +77 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +57 -0
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +106 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +52 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +260 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +44 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +131 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +142 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
- package/dist/apps/shared/components/calendar/EventModal.js +16 -4
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
- package/dist/apps/shared/components/contacts/ContactForm.js +16 -5
- package/dist/apps/shared/components/layout/AppShell.js +30 -7
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +15 -3
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +77 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +27 -2
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +2 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +134 -14
- package/dist/apps/shared/components/mail/layout/MailShell.js +106 -40
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +76 -0
- package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
- package/dist/apps/shared/search/localIndexBuilder.js +153 -0
- package/dist/apps/shared/search/localIndexKey.js +25 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +78 -0
- package/dist/apps/shared/search/localIndexSchema.js +179 -0
- package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
- package/dist/apps/shared/search/localIndexVFS.js +230 -0
- package/dist/apps/shared/search/localIndexWorker.js +341 -0
- package/dist/apps/shared/search/searchTier2.js +38 -0
- package/dist/apps/www/calendar/index.js +14 -12
- package/dist/apps/www/contacts/index.js +15 -6
- package/dist/apps/www/index.js +496 -94
- package/dist/apps/www/messages/[uid].js +5 -1
- package/dist/apps/www/settings/encryption/index.js +30 -2
- package/dist/apps/www/settings/sharing/index.js +128 -0
- package/dist/apps/www/tasks/index.js +27 -5
- package/package.json +3 -2
|
@@ -1,210 +1,249 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import "../../../styles/app.css";
|
|
6
|
-
import React, { PropsWithChildren, ReactNode, useEffect, useState } from "react";
|
|
7
|
-
import {
|
|
8
|
-
HiOutlineClipboardDocumentList,
|
|
9
|
-
HiOutlineClock,
|
|
10
|
-
HiOutlineDocumentArrowDown,
|
|
11
|
-
HiOutlineGlobeAlt,
|
|
12
|
-
HiOutlineInboxStack,
|
|
13
|
-
HiOutlineKey,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| "
|
|
35
|
-
| "
|
|
36
|
-
| "
|
|
37
|
-
| "
|
|
38
|
-
| "
|
|
39
|
-
| "
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import "../../../styles/app.css";
|
|
6
|
+
import React, { PropsWithChildren, ReactNode, useEffect, useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
HiOutlineClipboardDocumentList,
|
|
9
|
+
HiOutlineClock,
|
|
10
|
+
HiOutlineDocumentArrowDown,
|
|
11
|
+
HiOutlineGlobeAlt,
|
|
12
|
+
HiOutlineInboxStack,
|
|
13
|
+
HiOutlineKey,
|
|
14
|
+
HiOutlineLockClosed,
|
|
15
|
+
HiOutlinePaintBrush,
|
|
16
|
+
HiOutlinePuzzlePiece,
|
|
17
|
+
HiOutlineQueueList,
|
|
18
|
+
HiOutlineRocketLaunch,
|
|
19
|
+
HiOutlineShieldCheck,
|
|
20
|
+
HiOutlineShieldExclamation,
|
|
21
|
+
HiOutlineUserGroup,
|
|
22
|
+
HiOutlineWrenchScrewdriver,
|
|
23
|
+
} from "react-icons/hi2";
|
|
24
|
+
import { apiFetch, ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
25
|
+
import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
26
|
+
import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
|
|
27
|
+
import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
|
|
28
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
29
|
+
import BottomTabBar, { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
30
|
+
import { BrandingFooter, BrandingHeader } from "../../layout/BrandingChrome.js";
|
|
31
|
+
import UserMenu from "../../layout/UserMenu.js";
|
|
32
|
+
|
|
33
|
+
export type AdminSection =
|
|
34
|
+
| "mailboxes"
|
|
35
|
+
| "quarantine"
|
|
36
|
+
| "ingestQueue"
|
|
37
|
+
| "domains"
|
|
38
|
+
| "auditLog"
|
|
39
|
+
| "distributionLists"
|
|
40
|
+
| "transportRules"
|
|
41
|
+
| "escrowScopes"
|
|
42
|
+
| "retentionPolicy"
|
|
43
|
+
| "encryptionPolicy"
|
|
44
|
+
| "mailboxPolicy"
|
|
45
|
+
| "setup"
|
|
46
|
+
| "dataRequests"
|
|
47
|
+
| "plugins"
|
|
48
|
+
| "branding";
|
|
49
|
+
|
|
50
|
+
export interface AdminShellProps {
|
|
51
|
+
/** Which icon in the rail is highlighted as the current section. */
|
|
52
|
+
active: AdminSection;
|
|
53
|
+
/** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
|
|
54
|
+
userUid?: string;
|
|
55
|
+
/** auth-server's base URL, injected via the route's `fetchProps` — see `src/mongo/routes/AdminConsoleRoute.ts`. */
|
|
56
|
+
authServerUrl?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Where `mailApi.ts`'s `impersonateUser()`/`stopImpersonating()` should call: the real auth-server in
|
|
59
|
+
* production, or `""` under `yarn dev` to call this app's own local dev-only impersonation endpoint
|
|
60
|
+
* instead — see `AdminConsoleRoute`'s `fetchProps` and `src/dev/DevImpersonationRoute.ts`.
|
|
61
|
+
*/
|
|
62
|
+
impersonationBaseUrl?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type Status = "checking" | "denied" | "error" | "authorized";
|
|
66
|
+
|
|
67
|
+
/** Sections shown in the persistent icon rail / mobile tab bar — every admin area reachable from
|
|
68
|
+
* anywhere in the console. Deliberately excludes `quarantine`/`ingestQueue`: those are scoped to a
|
|
69
|
+
* single mailbox and only ever reached via links on that mailbox's own detail page (see
|
|
70
|
+
* `apps/admin/mailboxes/[uid].tsx`), not global navigation destinations. */
|
|
71
|
+
const NAV_ITEMS: NavItem[] = [
|
|
72
|
+
{ id: "mailboxes", href: "/admin", label: "Mailboxes", icon: HiOutlineInboxStack },
|
|
73
|
+
{ id: "domains", href: "/admin/domains", label: "Domains", icon: HiOutlineGlobeAlt },
|
|
74
|
+
{ id: "auditLog", href: "/admin/audit-log", label: "Audit Log", icon: HiOutlineClipboardDocumentList },
|
|
75
|
+
{
|
|
76
|
+
id: "distributionLists",
|
|
77
|
+
href: "/admin/distribution-lists",
|
|
78
|
+
label: "Distribution Lists",
|
|
79
|
+
icon: HiOutlineUserGroup,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "transportRules",
|
|
83
|
+
href: "/admin/transport-rules",
|
|
84
|
+
label: "Transport Rules",
|
|
85
|
+
icon: HiOutlineShieldCheck,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "escrowScopes",
|
|
89
|
+
href: "/admin/escrow-scopes",
|
|
90
|
+
label: "Escrow Scopes",
|
|
91
|
+
icon: HiOutlineKey,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "encryptionPolicy",
|
|
95
|
+
href: "/admin/encryption-policy",
|
|
96
|
+
label: "Encryption Policy",
|
|
97
|
+
icon: HiOutlineLockClosed,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "mailboxPolicy",
|
|
101
|
+
href: "/admin/mailbox-policy",
|
|
102
|
+
label: "Mailbox Policy",
|
|
103
|
+
icon: HiOutlineWrenchScrewdriver,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "retentionPolicy",
|
|
107
|
+
href: "/admin/retention-policy",
|
|
108
|
+
label: "Retention Policy",
|
|
109
|
+
icon: HiOutlineClock,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "dataRequests",
|
|
113
|
+
href: "/admin/data-requests",
|
|
114
|
+
label: "Data Requests",
|
|
115
|
+
icon: HiOutlineDocumentArrowDown,
|
|
116
|
+
},
|
|
117
|
+
{ id: "plugins", href: "/admin/plugins", label: "Plugins", icon: HiOutlinePuzzlePiece },
|
|
118
|
+
{ id: "branding", href: "/admin/branding", label: "Branding", icon: HiOutlinePaintBrush },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
/** Mailbox-scoped sections — not part of the global nav rail/tab bar (see `NAV_ITEMS` above), but
|
|
122
|
+
* still need an entry here so the header can resolve a label for them when they're the active page. */
|
|
123
|
+
const MAILBOX_SCOPED_ITEMS: NavItem[] = [
|
|
124
|
+
{ id: "quarantine", href: "/admin/quarantine", label: "Quarantine", icon: HiOutlineShieldExclamation },
|
|
125
|
+
{ id: "ingestQueue", href: "/admin/ingest-queue", label: "Ingest Queue", icon: HiOutlineQueueList },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
/** The setup wizard - reached by redirect or from the Mailboxes page, not from the rail. */
|
|
129
|
+
const SETUP_ITEM: NavItem = { id: "setup", href: "/admin/setup", label: "Setup", icon: HiOutlineRocketLaunch };
|
|
130
|
+
|
|
131
|
+
const ALL_ITEMS: NavItem[] = [...NAV_ITEMS, ...MAILBOX_SCOPED_ITEMS, SETUP_ITEM];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Gates every `apps/admin` page behind the `admin` trusted role. Uses `GET /api/admin/release-notes` (any
|
|
135
|
+
* `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a canary: a 200 means the
|
|
136
|
+
* caller's JWT carries a trusted role, a 403 means it doesn't. There is no local step-up/elevation flow
|
|
137
|
+
* (that would need a cross-origin call to auth-server's own elevation endpoint — not wired up yet).
|
|
138
|
+
*/
|
|
139
|
+
export default function AdminShell({ active, userUid, authServerUrl, children }: PropsWithChildren<AdminShellProps>) {
|
|
140
|
+
const [status, setStatus] = useState<Status>("checking");
|
|
141
|
+
const [error, setError] = useState<string | null>(null);
|
|
142
|
+
const { branding, iconSrc } = useBranding();
|
|
143
|
+
|
|
144
|
+
useRedirectIfUnauthenticated(userUid, authServerUrl);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!userUid) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
apiFetch("/admin/release-notes")
|
|
151
|
+
.then(async () => {
|
|
152
|
+
// Until first-run setup is finished, every other admin page sends the admin to the wizard. A failed
|
|
153
|
+
// check never blocks the console - the admin can still reach setup from the Mailboxes page.
|
|
154
|
+
if (active !== "setup") {
|
|
155
|
+
try {
|
|
156
|
+
if ((await getSetupStatus()).required) {
|
|
157
|
+
window.location.href = SETUP_ITEM.href;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Fall through to the page.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
setStatus("authorized");
|
|
165
|
+
})
|
|
166
|
+
.catch((err) => {
|
|
167
|
+
if (err instanceof ApiRequestError && (err.status === 403 || err.status === 401)) {
|
|
168
|
+
setStatus("denied");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not verify administrator access.");
|
|
172
|
+
setStatus("error");
|
|
173
|
+
});
|
|
174
|
+
}, [userUid]);
|
|
175
|
+
|
|
176
|
+
function handleSignOut() {
|
|
177
|
+
window.location.href = authServerUrl ?? "/";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let content: ReactNode;
|
|
181
|
+
if (!userUid || status === "checking") {
|
|
182
|
+
content = <div className="min-h-screen" />;
|
|
183
|
+
} else if (status === "denied") {
|
|
184
|
+
content = (
|
|
185
|
+
<div className="min-h-screen flex items-center justify-center p-8">
|
|
186
|
+
<div className="w-full max-w-md">
|
|
187
|
+
<Alert>You do not have administrator access.</Alert>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
} else if (status === "error") {
|
|
192
|
+
content = (
|
|
193
|
+
<div className="min-h-screen flex items-center justify-center p-8">
|
|
194
|
+
<div className="w-full max-w-md">
|
|
195
|
+
<Alert>{error}</Alert>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
const activeItem = ALL_ITEMS.find((item) => item.id === active);
|
|
201
|
+
content = (
|
|
202
|
+
<div className="min-h-screen flex flex-col bg-surface-alt">
|
|
203
|
+
<div className="flex-1 flex min-h-0">
|
|
204
|
+
<nav
|
|
205
|
+
aria-label="Admin sections"
|
|
206
|
+
className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1"
|
|
207
|
+
>
|
|
208
|
+
<img src={iconSrc} width="96" height="96" alt="" className="mb-3" />
|
|
209
|
+
{NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (
|
|
210
|
+
<a
|
|
211
|
+
key={id}
|
|
212
|
+
href={href}
|
|
213
|
+
aria-label={label}
|
|
214
|
+
aria-current={id === active ? "page" : undefined}
|
|
215
|
+
title={label}
|
|
216
|
+
className={[
|
|
217
|
+
"w-10 h-10 flex items-center justify-center rounded-sm",
|
|
218
|
+
id === active
|
|
219
|
+
? "bg-primary/10 text-primary-dark"
|
|
220
|
+
: "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
221
|
+
].join(" ")}
|
|
222
|
+
>
|
|
223
|
+
<Icon size={20} aria-hidden="true" />
|
|
224
|
+
</a>
|
|
225
|
+
))}
|
|
226
|
+
</nav>
|
|
227
|
+
<BottomTabBar apps={NAV_ITEMS} active={active} />
|
|
228
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
229
|
+
<header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
|
|
230
|
+
<span className="font-display font-bold text-lg uppercase tracking-wide">{activeItem?.label}</span>
|
|
231
|
+
<UserMenu userUid={userUid} authServerUrl={authServerUrl} onSignOut={handleSignOut} />
|
|
232
|
+
</header>
|
|
233
|
+
<div className="flex-1 pb-14 md:pb-0">
|
|
234
|
+
<main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<>
|
|
244
|
+
<BrandingHeader branding={branding} />
|
|
245
|
+
{content}
|
|
246
|
+
<BrandingFooter branding={branding} />
|
|
247
|
+
</>
|
|
248
|
+
);
|
|
249
|
+
}
|