@nubitio/admin 0.5.11 → 0.5.14
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/dist/index.cjs +205 -0
- package/dist/index.d.cts +81 -1
- package/dist/index.d.mts +81 -1
- package/dist/index.mjs +203 -3
- package/dist/style.css +46 -0
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -383,8 +383,213 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
383
383
|
});
|
|
384
384
|
};
|
|
385
385
|
//#endregion
|
|
386
|
+
//#region packages/admin/auth/SessionContext.tsx
|
|
387
|
+
const SessionContext = (0, react.createContext)(null);
|
|
388
|
+
function joinApiPath$1(apiBaseUrl, path) {
|
|
389
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
390
|
+
}
|
|
391
|
+
function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
|
|
392
|
+
const [session, setSession] = (0, react.useState)({ status: "loading" });
|
|
393
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
394
|
+
try {
|
|
395
|
+
const response = await fetch(joinApiPath$1(apiBaseUrl, mePath), { credentials: "include" });
|
|
396
|
+
if (!response.ok) {
|
|
397
|
+
setSession({ status: "anonymous" });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
setSession({
|
|
401
|
+
status: "authenticated",
|
|
402
|
+
profile: await response.json()
|
|
403
|
+
});
|
|
404
|
+
} catch {
|
|
405
|
+
setSession({ status: "anonymous" });
|
|
406
|
+
}
|
|
407
|
+
}, [apiBaseUrl, mePath]);
|
|
408
|
+
(0, react.useEffect)(() => {
|
|
409
|
+
refresh();
|
|
410
|
+
}, [refresh]);
|
|
411
|
+
const logout = (0, react.useCallback)(async () => {
|
|
412
|
+
await fetch(joinApiPath$1(apiBaseUrl, logoutPath), {
|
|
413
|
+
method: "POST",
|
|
414
|
+
credentials: "include"
|
|
415
|
+
});
|
|
416
|
+
setSession({ status: "anonymous" });
|
|
417
|
+
}, [apiBaseUrl, logoutPath]);
|
|
418
|
+
const value = (0, react.useMemo)(() => ({
|
|
419
|
+
session,
|
|
420
|
+
refresh,
|
|
421
|
+
logout,
|
|
422
|
+
roles: session.status === "authenticated" ? session.profile.roles : [],
|
|
423
|
+
username: session.status === "authenticated" ? session.profile.username : null
|
|
424
|
+
}), [
|
|
425
|
+
logout,
|
|
426
|
+
refresh,
|
|
427
|
+
session
|
|
428
|
+
]);
|
|
429
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionContext.Provider, {
|
|
430
|
+
value,
|
|
431
|
+
children
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function useSession() {
|
|
435
|
+
const context = (0, react.useContext)(SessionContext);
|
|
436
|
+
if (!context) throw new Error("useSession must be used within a <SessionProvider>.");
|
|
437
|
+
return context;
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region packages/admin/auth/LoginPage.tsx
|
|
441
|
+
function joinApiPath(apiBaseUrl, path) {
|
|
442
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
443
|
+
}
|
|
444
|
+
function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
|
|
445
|
+
const [username, setUsername] = (0, react.useState)(defaultUsername);
|
|
446
|
+
const [password, setPassword] = (0, react.useState)("");
|
|
447
|
+
const [error, setError] = (0, react.useState)(null);
|
|
448
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
449
|
+
const submit = async (event) => {
|
|
450
|
+
event.preventDefault();
|
|
451
|
+
setBusy(true);
|
|
452
|
+
setError(null);
|
|
453
|
+
try {
|
|
454
|
+
const response = await fetch(joinApiPath(apiBaseUrl, loginPath), {
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers: { "Content-Type": "application/json" },
|
|
457
|
+
credentials: "include",
|
|
458
|
+
body: JSON.stringify({
|
|
459
|
+
username,
|
|
460
|
+
password
|
|
461
|
+
})
|
|
462
|
+
});
|
|
463
|
+
if (!response.ok) {
|
|
464
|
+
setError((await response.json().catch(() => null))?.message ?? "Login failed");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
onLoggedIn();
|
|
468
|
+
} catch {
|
|
469
|
+
setError("Network error");
|
|
470
|
+
} finally {
|
|
471
|
+
setBusy(false);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
475
|
+
style: {
|
|
476
|
+
display: "grid",
|
|
477
|
+
placeItems: "center",
|
|
478
|
+
minHeight: "100vh"
|
|
479
|
+
},
|
|
480
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Card, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
481
|
+
onSubmit: submit,
|
|
482
|
+
style: {
|
|
483
|
+
display: "flex",
|
|
484
|
+
flexDirection: "column",
|
|
485
|
+
gap: 12,
|
|
486
|
+
width: 320,
|
|
487
|
+
padding: 8
|
|
488
|
+
},
|
|
489
|
+
children: [
|
|
490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
491
|
+
style: { margin: 0 },
|
|
492
|
+
children: title
|
|
493
|
+
}),
|
|
494
|
+
hint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
495
|
+
style: {
|
|
496
|
+
margin: 0,
|
|
497
|
+
color: "var(--text-secondary)"
|
|
498
|
+
},
|
|
499
|
+
children: hint
|
|
500
|
+
}),
|
|
501
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.TextField, {
|
|
502
|
+
placeholder: "Email",
|
|
503
|
+
value: username,
|
|
504
|
+
autoComplete: "username",
|
|
505
|
+
onChange: (e) => setUsername(e.target.value)
|
|
506
|
+
}),
|
|
507
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.TextField, {
|
|
508
|
+
placeholder: "Password",
|
|
509
|
+
type: "password",
|
|
510
|
+
value: password,
|
|
511
|
+
autoComplete: "current-password",
|
|
512
|
+
onChange: (e) => setPassword(e.target.value)
|
|
513
|
+
}),
|
|
514
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
515
|
+
style: {
|
|
516
|
+
margin: 0,
|
|
517
|
+
color: "var(--error-color, #dc2626)"
|
|
518
|
+
},
|
|
519
|
+
children: error
|
|
520
|
+
}),
|
|
521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Button, {
|
|
522
|
+
variant: "primary",
|
|
523
|
+
type: "submit",
|
|
524
|
+
disabled: busy,
|
|
525
|
+
children: busy ? "Signing in…" : "Sign in"
|
|
526
|
+
})
|
|
527
|
+
]
|
|
528
|
+
}) })
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region packages/admin/runtime/useAppRuntime.ts
|
|
533
|
+
function useAppRuntime() {
|
|
534
|
+
const [toasts, setToasts] = (0, react.useState)([]);
|
|
535
|
+
const dismiss = (0, react.useCallback)((id) => {
|
|
536
|
+
setToasts((current) => current.filter((toast) => toast.id !== id));
|
|
537
|
+
}, []);
|
|
538
|
+
const notify = (0, react.useCallback)((message, type = "info", durationMs = 4e3) => {
|
|
539
|
+
const id = Date.now() + Math.floor(Math.random() * 1e3);
|
|
540
|
+
setToasts((current) => [...current, {
|
|
541
|
+
id,
|
|
542
|
+
message,
|
|
543
|
+
type
|
|
544
|
+
}]);
|
|
545
|
+
window.setTimeout(() => dismiss(id), durationMs);
|
|
546
|
+
}, [dismiss]);
|
|
547
|
+
const confirm = (0, react.useCallback)((message) => {
|
|
548
|
+
if (typeof window === "undefined") return false;
|
|
549
|
+
return window.confirm(message);
|
|
550
|
+
}, []);
|
|
551
|
+
return {
|
|
552
|
+
runtime: (0, react.useMemo)(() => ({
|
|
553
|
+
notify,
|
|
554
|
+
confirm
|
|
555
|
+
}), [confirm, notify]),
|
|
556
|
+
toasts,
|
|
557
|
+
dismiss
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region packages/admin/runtime/ToastHost.tsx
|
|
562
|
+
const TYPE_CLASS = {
|
|
563
|
+
success: "nb-toast--success",
|
|
564
|
+
error: "nb-toast--error",
|
|
565
|
+
warning: "nb-toast--warning",
|
|
566
|
+
info: "nb-toast--info"
|
|
567
|
+
};
|
|
568
|
+
function ToastHost({ toasts, onDismiss }) {
|
|
569
|
+
if (toasts.length === 0) return null;
|
|
570
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
571
|
+
className: "nb-toast-host",
|
|
572
|
+
"aria-live": "polite",
|
|
573
|
+
children: toasts.map((toast) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
574
|
+
className: `nb-toast ${TYPE_CLASS[toast.type]}`,
|
|
575
|
+
role: "status",
|
|
576
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: toast.message }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
577
|
+
type: "button",
|
|
578
|
+
className: "nb-toast__close",
|
|
579
|
+
onClick: () => onDismiss(toast.id),
|
|
580
|
+
children: "×"
|
|
581
|
+
})]
|
|
582
|
+
}, toast.id))
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
386
586
|
exports.AdminHeader = AdminHeader;
|
|
387
587
|
exports.AdminShell = AdminShell;
|
|
388
588
|
exports.AdminSidebarMenu = AdminSidebarMenu;
|
|
589
|
+
exports.LoginPage = LoginPage;
|
|
590
|
+
exports.SessionProvider = SessionProvider;
|
|
591
|
+
exports.ToastHost = ToastHost;
|
|
592
|
+
exports.useAppRuntime = useAppRuntime;
|
|
389
593
|
exports.useScreenSize = useScreenSize;
|
|
390
594
|
exports.useScreenSizeClass = useScreenSizeClass;
|
|
595
|
+
exports.useSession = useSession;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { CoreRuntime } from "@nubitio/core";
|
|
2
3
|
|
|
3
4
|
//#region packages/admin/AdminHeader.d.ts
|
|
4
5
|
interface AdminHeaderAction {
|
|
@@ -95,4 +96,83 @@ declare const useScreenSize: () => {
|
|
|
95
96
|
};
|
|
96
97
|
declare const useScreenSizeClass: () => "screen-large" | "screen-medium" | "screen-small" | "screen-x-small";
|
|
97
98
|
//#endregion
|
|
98
|
-
|
|
99
|
+
//#region packages/admin/auth/SessionContext.d.ts
|
|
100
|
+
type SessionProfile = {
|
|
101
|
+
username: string;
|
|
102
|
+
roles: string[];
|
|
103
|
+
};
|
|
104
|
+
type SessionState = {
|
|
105
|
+
status: 'loading';
|
|
106
|
+
} | {
|
|
107
|
+
status: 'anonymous';
|
|
108
|
+
} | {
|
|
109
|
+
status: 'authenticated';
|
|
110
|
+
profile: SessionProfile;
|
|
111
|
+
};
|
|
112
|
+
interface SessionProviderConfig {
|
|
113
|
+
/** API base URL, e.g. `/api/`. */
|
|
114
|
+
apiBaseUrl?: string;
|
|
115
|
+
/** Profile endpoint relative to apiBaseUrl. @default `me` */
|
|
116
|
+
mePath?: string;
|
|
117
|
+
/** Logout endpoint relative to apiBaseUrl. @default `auth/logout` */
|
|
118
|
+
logoutPath?: string;
|
|
119
|
+
}
|
|
120
|
+
interface SessionContextValue {
|
|
121
|
+
session: SessionState;
|
|
122
|
+
refresh: () => Promise<void>;
|
|
123
|
+
logout: () => Promise<void>;
|
|
124
|
+
roles: string[];
|
|
125
|
+
username: string | null;
|
|
126
|
+
}
|
|
127
|
+
declare function SessionProvider({
|
|
128
|
+
apiBaseUrl,
|
|
129
|
+
mePath,
|
|
130
|
+
logoutPath,
|
|
131
|
+
children
|
|
132
|
+
}: SessionProviderConfig & {
|
|
133
|
+
children: React.ReactNode;
|
|
134
|
+
}): React.JSX.Element;
|
|
135
|
+
declare function useSession(): SessionContextValue;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region packages/admin/auth/LoginPage.d.ts
|
|
138
|
+
interface LoginPageProps {
|
|
139
|
+
onLoggedIn: () => void;
|
|
140
|
+
apiBaseUrl?: string;
|
|
141
|
+
loginPath?: string;
|
|
142
|
+
title?: string;
|
|
143
|
+
hint?: string;
|
|
144
|
+
defaultUsername?: string;
|
|
145
|
+
}
|
|
146
|
+
declare function LoginPage({
|
|
147
|
+
onLoggedIn,
|
|
148
|
+
apiBaseUrl,
|
|
149
|
+
loginPath,
|
|
150
|
+
title,
|
|
151
|
+
hint,
|
|
152
|
+
defaultUsername
|
|
153
|
+
}: LoginPageProps): import("react").JSX.Element;
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region packages/admin/runtime/useAppRuntime.d.ts
|
|
156
|
+
type NotificationType = 'success' | 'error' | 'warning' | 'info';
|
|
157
|
+
type ToastItem = {
|
|
158
|
+
id: number;
|
|
159
|
+
message: string;
|
|
160
|
+
type: NotificationType;
|
|
161
|
+
};
|
|
162
|
+
declare function useAppRuntime(): {
|
|
163
|
+
runtime: CoreRuntime;
|
|
164
|
+
toasts: ToastItem[];
|
|
165
|
+
dismiss: (id: number) => void;
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region packages/admin/runtime/ToastHost.d.ts
|
|
169
|
+
interface ToastHostProps {
|
|
170
|
+
toasts: ToastItem[];
|
|
171
|
+
onDismiss: (id: number) => void;
|
|
172
|
+
}
|
|
173
|
+
declare function ToastHost({
|
|
174
|
+
toasts,
|
|
175
|
+
onDismiss
|
|
176
|
+
}: ToastHostProps): import("react").JSX.Element | null;
|
|
177
|
+
//#endregion
|
|
178
|
+
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, LoginPage, type LoginPageProps, type NotificationType, type SessionContextValue, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, ToastHost, type ToastHostProps, type ToastItem, useAppRuntime, useScreenSize, useScreenSizeClass, useSession };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { CoreRuntime } from "@nubitio/core";
|
|
2
3
|
|
|
3
4
|
//#region packages/admin/AdminHeader.d.ts
|
|
4
5
|
interface AdminHeaderAction {
|
|
@@ -95,4 +96,83 @@ declare const useScreenSize: () => {
|
|
|
95
96
|
};
|
|
96
97
|
declare const useScreenSizeClass: () => "screen-large" | "screen-medium" | "screen-small" | "screen-x-small";
|
|
97
98
|
//#endregion
|
|
98
|
-
|
|
99
|
+
//#region packages/admin/auth/SessionContext.d.ts
|
|
100
|
+
type SessionProfile = {
|
|
101
|
+
username: string;
|
|
102
|
+
roles: string[];
|
|
103
|
+
};
|
|
104
|
+
type SessionState = {
|
|
105
|
+
status: 'loading';
|
|
106
|
+
} | {
|
|
107
|
+
status: 'anonymous';
|
|
108
|
+
} | {
|
|
109
|
+
status: 'authenticated';
|
|
110
|
+
profile: SessionProfile;
|
|
111
|
+
};
|
|
112
|
+
interface SessionProviderConfig {
|
|
113
|
+
/** API base URL, e.g. `/api/`. */
|
|
114
|
+
apiBaseUrl?: string;
|
|
115
|
+
/** Profile endpoint relative to apiBaseUrl. @default `me` */
|
|
116
|
+
mePath?: string;
|
|
117
|
+
/** Logout endpoint relative to apiBaseUrl. @default `auth/logout` */
|
|
118
|
+
logoutPath?: string;
|
|
119
|
+
}
|
|
120
|
+
interface SessionContextValue {
|
|
121
|
+
session: SessionState;
|
|
122
|
+
refresh: () => Promise<void>;
|
|
123
|
+
logout: () => Promise<void>;
|
|
124
|
+
roles: string[];
|
|
125
|
+
username: string | null;
|
|
126
|
+
}
|
|
127
|
+
declare function SessionProvider({
|
|
128
|
+
apiBaseUrl,
|
|
129
|
+
mePath,
|
|
130
|
+
logoutPath,
|
|
131
|
+
children
|
|
132
|
+
}: SessionProviderConfig & {
|
|
133
|
+
children: React.ReactNode;
|
|
134
|
+
}): React.JSX.Element;
|
|
135
|
+
declare function useSession(): SessionContextValue;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region packages/admin/auth/LoginPage.d.ts
|
|
138
|
+
interface LoginPageProps {
|
|
139
|
+
onLoggedIn: () => void;
|
|
140
|
+
apiBaseUrl?: string;
|
|
141
|
+
loginPath?: string;
|
|
142
|
+
title?: string;
|
|
143
|
+
hint?: string;
|
|
144
|
+
defaultUsername?: string;
|
|
145
|
+
}
|
|
146
|
+
declare function LoginPage({
|
|
147
|
+
onLoggedIn,
|
|
148
|
+
apiBaseUrl,
|
|
149
|
+
loginPath,
|
|
150
|
+
title,
|
|
151
|
+
hint,
|
|
152
|
+
defaultUsername
|
|
153
|
+
}: LoginPageProps): import("react").JSX.Element;
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region packages/admin/runtime/useAppRuntime.d.ts
|
|
156
|
+
type NotificationType = 'success' | 'error' | 'warning' | 'info';
|
|
157
|
+
type ToastItem = {
|
|
158
|
+
id: number;
|
|
159
|
+
message: string;
|
|
160
|
+
type: NotificationType;
|
|
161
|
+
};
|
|
162
|
+
declare function useAppRuntime(): {
|
|
163
|
+
runtime: CoreRuntime;
|
|
164
|
+
toasts: ToastItem[];
|
|
165
|
+
dismiss: (id: number) => void;
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region packages/admin/runtime/ToastHost.d.ts
|
|
169
|
+
interface ToastHostProps {
|
|
170
|
+
toasts: ToastItem[];
|
|
171
|
+
onDismiss: (id: number) => void;
|
|
172
|
+
}
|
|
173
|
+
declare function ToastHost({
|
|
174
|
+
toasts,
|
|
175
|
+
onDismiss
|
|
176
|
+
}: ToastHostProps): import("react").JSX.Element | null;
|
|
177
|
+
//#endregion
|
|
178
|
+
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, LoginPage, type LoginPageProps, type NotificationType, type SessionContextValue, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, ToastHost, type ToastHostProps, type ToastItem, useAppRuntime, useScreenSize, useScreenSizeClass, useSession };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
3
|
-
import { Badge, IconButton, useFloatingPanel } from "@nubitio/ui";
|
|
3
|
+
import { Badge, Button, Card, IconButton, TextField, useFloatingPanel } from "@nubitio/ui";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region packages/admin/AdminHeader.tsx
|
|
6
6
|
function ActionPopover({ action }) {
|
|
@@ -359,4 +359,204 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
|
|
|
359
359
|
});
|
|
360
360
|
};
|
|
361
361
|
//#endregion
|
|
362
|
-
|
|
362
|
+
//#region packages/admin/auth/SessionContext.tsx
|
|
363
|
+
const SessionContext = createContext(null);
|
|
364
|
+
function joinApiPath$1(apiBaseUrl, path) {
|
|
365
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
366
|
+
}
|
|
367
|
+
function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
|
|
368
|
+
const [session, setSession] = useState({ status: "loading" });
|
|
369
|
+
const refresh = useCallback(async () => {
|
|
370
|
+
try {
|
|
371
|
+
const response = await fetch(joinApiPath$1(apiBaseUrl, mePath), { credentials: "include" });
|
|
372
|
+
if (!response.ok) {
|
|
373
|
+
setSession({ status: "anonymous" });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
setSession({
|
|
377
|
+
status: "authenticated",
|
|
378
|
+
profile: await response.json()
|
|
379
|
+
});
|
|
380
|
+
} catch {
|
|
381
|
+
setSession({ status: "anonymous" });
|
|
382
|
+
}
|
|
383
|
+
}, [apiBaseUrl, mePath]);
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
refresh();
|
|
386
|
+
}, [refresh]);
|
|
387
|
+
const logout = useCallback(async () => {
|
|
388
|
+
await fetch(joinApiPath$1(apiBaseUrl, logoutPath), {
|
|
389
|
+
method: "POST",
|
|
390
|
+
credentials: "include"
|
|
391
|
+
});
|
|
392
|
+
setSession({ status: "anonymous" });
|
|
393
|
+
}, [apiBaseUrl, logoutPath]);
|
|
394
|
+
const value = useMemo(() => ({
|
|
395
|
+
session,
|
|
396
|
+
refresh,
|
|
397
|
+
logout,
|
|
398
|
+
roles: session.status === "authenticated" ? session.profile.roles : [],
|
|
399
|
+
username: session.status === "authenticated" ? session.profile.username : null
|
|
400
|
+
}), [
|
|
401
|
+
logout,
|
|
402
|
+
refresh,
|
|
403
|
+
session
|
|
404
|
+
]);
|
|
405
|
+
return /* @__PURE__ */ jsx(SessionContext.Provider, {
|
|
406
|
+
value,
|
|
407
|
+
children
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function useSession() {
|
|
411
|
+
const context = useContext(SessionContext);
|
|
412
|
+
if (!context) throw new Error("useSession must be used within a <SessionProvider>.");
|
|
413
|
+
return context;
|
|
414
|
+
}
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region packages/admin/auth/LoginPage.tsx
|
|
417
|
+
function joinApiPath(apiBaseUrl, path) {
|
|
418
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
419
|
+
}
|
|
420
|
+
function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
|
|
421
|
+
const [username, setUsername] = useState(defaultUsername);
|
|
422
|
+
const [password, setPassword] = useState("");
|
|
423
|
+
const [error, setError] = useState(null);
|
|
424
|
+
const [busy, setBusy] = useState(false);
|
|
425
|
+
const submit = async (event) => {
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
setBusy(true);
|
|
428
|
+
setError(null);
|
|
429
|
+
try {
|
|
430
|
+
const response = await fetch(joinApiPath(apiBaseUrl, loginPath), {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: { "Content-Type": "application/json" },
|
|
433
|
+
credentials: "include",
|
|
434
|
+
body: JSON.stringify({
|
|
435
|
+
username,
|
|
436
|
+
password
|
|
437
|
+
})
|
|
438
|
+
});
|
|
439
|
+
if (!response.ok) {
|
|
440
|
+
setError((await response.json().catch(() => null))?.message ?? "Login failed");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
onLoggedIn();
|
|
444
|
+
} catch {
|
|
445
|
+
setError("Network error");
|
|
446
|
+
} finally {
|
|
447
|
+
setBusy(false);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
return /* @__PURE__ */ jsx("div", {
|
|
451
|
+
style: {
|
|
452
|
+
display: "grid",
|
|
453
|
+
placeItems: "center",
|
|
454
|
+
minHeight: "100vh"
|
|
455
|
+
},
|
|
456
|
+
children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("form", {
|
|
457
|
+
onSubmit: submit,
|
|
458
|
+
style: {
|
|
459
|
+
display: "flex",
|
|
460
|
+
flexDirection: "column",
|
|
461
|
+
gap: 12,
|
|
462
|
+
width: 320,
|
|
463
|
+
padding: 8
|
|
464
|
+
},
|
|
465
|
+
children: [
|
|
466
|
+
/* @__PURE__ */ jsx("h2", {
|
|
467
|
+
style: { margin: 0 },
|
|
468
|
+
children: title
|
|
469
|
+
}),
|
|
470
|
+
hint && /* @__PURE__ */ jsx("p", {
|
|
471
|
+
style: {
|
|
472
|
+
margin: 0,
|
|
473
|
+
color: "var(--text-secondary)"
|
|
474
|
+
},
|
|
475
|
+
children: hint
|
|
476
|
+
}),
|
|
477
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
478
|
+
placeholder: "Email",
|
|
479
|
+
value: username,
|
|
480
|
+
autoComplete: "username",
|
|
481
|
+
onChange: (e) => setUsername(e.target.value)
|
|
482
|
+
}),
|
|
483
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
484
|
+
placeholder: "Password",
|
|
485
|
+
type: "password",
|
|
486
|
+
value: password,
|
|
487
|
+
autoComplete: "current-password",
|
|
488
|
+
onChange: (e) => setPassword(e.target.value)
|
|
489
|
+
}),
|
|
490
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
491
|
+
style: {
|
|
492
|
+
margin: 0,
|
|
493
|
+
color: "var(--error-color, #dc2626)"
|
|
494
|
+
},
|
|
495
|
+
children: error
|
|
496
|
+
}),
|
|
497
|
+
/* @__PURE__ */ jsx(Button, {
|
|
498
|
+
variant: "primary",
|
|
499
|
+
type: "submit",
|
|
500
|
+
disabled: busy,
|
|
501
|
+
children: busy ? "Signing in…" : "Sign in"
|
|
502
|
+
})
|
|
503
|
+
]
|
|
504
|
+
}) })
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region packages/admin/runtime/useAppRuntime.ts
|
|
509
|
+
function useAppRuntime() {
|
|
510
|
+
const [toasts, setToasts] = useState([]);
|
|
511
|
+
const dismiss = useCallback((id) => {
|
|
512
|
+
setToasts((current) => current.filter((toast) => toast.id !== id));
|
|
513
|
+
}, []);
|
|
514
|
+
const notify = useCallback((message, type = "info", durationMs = 4e3) => {
|
|
515
|
+
const id = Date.now() + Math.floor(Math.random() * 1e3);
|
|
516
|
+
setToasts((current) => [...current, {
|
|
517
|
+
id,
|
|
518
|
+
message,
|
|
519
|
+
type
|
|
520
|
+
}]);
|
|
521
|
+
window.setTimeout(() => dismiss(id), durationMs);
|
|
522
|
+
}, [dismiss]);
|
|
523
|
+
const confirm = useCallback((message) => {
|
|
524
|
+
if (typeof window === "undefined") return false;
|
|
525
|
+
return window.confirm(message);
|
|
526
|
+
}, []);
|
|
527
|
+
return {
|
|
528
|
+
runtime: useMemo(() => ({
|
|
529
|
+
notify,
|
|
530
|
+
confirm
|
|
531
|
+
}), [confirm, notify]),
|
|
532
|
+
toasts,
|
|
533
|
+
dismiss
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region packages/admin/runtime/ToastHost.tsx
|
|
538
|
+
const TYPE_CLASS = {
|
|
539
|
+
success: "nb-toast--success",
|
|
540
|
+
error: "nb-toast--error",
|
|
541
|
+
warning: "nb-toast--warning",
|
|
542
|
+
info: "nb-toast--info"
|
|
543
|
+
};
|
|
544
|
+
function ToastHost({ toasts, onDismiss }) {
|
|
545
|
+
if (toasts.length === 0) return null;
|
|
546
|
+
return /* @__PURE__ */ jsx("div", {
|
|
547
|
+
className: "nb-toast-host",
|
|
548
|
+
"aria-live": "polite",
|
|
549
|
+
children: toasts.map((toast) => /* @__PURE__ */ jsxs("div", {
|
|
550
|
+
className: `nb-toast ${TYPE_CLASS[toast.type]}`,
|
|
551
|
+
role: "status",
|
|
552
|
+
children: [/* @__PURE__ */ jsx("span", { children: toast.message }), /* @__PURE__ */ jsx("button", {
|
|
553
|
+
type: "button",
|
|
554
|
+
className: "nb-toast__close",
|
|
555
|
+
onClick: () => onDismiss(toast.id),
|
|
556
|
+
children: "×"
|
|
557
|
+
})]
|
|
558
|
+
}, toast.id))
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
562
|
+
export { AdminHeader, AdminShell, AdminSidebarMenu, LoginPage, SessionProvider, ToastHost, useAppRuntime, useScreenSize, useScreenSizeClass, useSession };
|
package/dist/style.css
CHANGED
|
@@ -369,3 +369,49 @@
|
|
|
369
369
|
opacity: 0;
|
|
370
370
|
pointer-events: none;
|
|
371
371
|
}
|
|
372
|
+
.nb-toast-host {
|
|
373
|
+
position: fixed;
|
|
374
|
+
right: 1rem;
|
|
375
|
+
bottom: 1rem;
|
|
376
|
+
z-index: 1000;
|
|
377
|
+
display: flex;
|
|
378
|
+
flex-direction: column;
|
|
379
|
+
gap: 0.5rem;
|
|
380
|
+
max-width: min(24rem, 100vw - 2rem);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.nb-toast {
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: flex-start;
|
|
386
|
+
justify-content: space-between;
|
|
387
|
+
gap: 0.75rem;
|
|
388
|
+
padding: 0.75rem 1rem;
|
|
389
|
+
border-radius: var(--radius-md, 0.5rem);
|
|
390
|
+
border: 1px solid var(--border-subtle);
|
|
391
|
+
background: var(--surface-2);
|
|
392
|
+
color: var(--text-primary);
|
|
393
|
+
box-shadow: var(--shadow-md, 0 8px 24px rgba(0, 0, 0, 0.12));
|
|
394
|
+
font-size: 0.875rem;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.nb-toast--success {
|
|
398
|
+
border-color: color-mix(in srgb, var(--success-color, #16a34a) 35%, var(--border-subtle));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.nb-toast--error {
|
|
402
|
+
border-color: color-mix(in srgb, var(--error-color, #dc2626) 35%, var(--border-subtle));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.nb-toast--warning {
|
|
406
|
+
border-color: color-mix(in srgb, var(--warning-color, #d97706) 35%, var(--border-subtle));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.nb-toast__close {
|
|
410
|
+
border: 0;
|
|
411
|
+
background: transparent;
|
|
412
|
+
color: var(--text-secondary);
|
|
413
|
+
cursor: pointer;
|
|
414
|
+
font-size: 1.125rem;
|
|
415
|
+
line-height: 1;
|
|
416
|
+
padding: 0;
|
|
417
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubitio/admin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Admin shell layout components: responsive sidebar, header, and screen size utilities for Nubit apps.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"react": "^19.0.0",
|
|
53
53
|
"react-dom": "^19.0.0",
|
|
54
54
|
"react-router-dom": "^6.0.0",
|
|
55
|
-
"@nubitio/
|
|
55
|
+
"@nubitio/core": "^0.5.14",
|
|
56
|
+
"@nubitio/ui": "^0.5.14"
|
|
56
57
|
}
|
|
57
58
|
}
|