@airoom/nextmin-react 0.1.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 (95) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +133 -0
  3. package/dist/auth/AuthPage.d.ts +1 -0
  4. package/dist/auth/AuthPage.js +23 -0
  5. package/dist/auth/ForgotPasswordForm.d.ts +1 -0
  6. package/dist/auth/ForgotPasswordForm.js +28 -0
  7. package/dist/auth/SignInForm.d.ts +6 -0
  8. package/dist/auth/SignInForm.js +38 -0
  9. package/dist/auth/SignUpForm.d.ts +3 -0
  10. package/dist/auth/SignUpForm.js +30 -0
  11. package/dist/components/AddressAutocomplete.d.ts +21 -0
  12. package/dist/components/AddressAutocomplete.js +182 -0
  13. package/dist/components/AdminApp.d.ts +1 -0
  14. package/dist/components/AdminApp.js +134 -0
  15. package/dist/components/ConfirmDialog.d.ts +12 -0
  16. package/dist/components/ConfirmDialog.js +6 -0
  17. package/dist/components/FileUploader.d.ts +32 -0
  18. package/dist/components/FileUploader.js +480 -0
  19. package/dist/components/NoAccess.d.ts +3 -0
  20. package/dist/components/NoAccess.js +5 -0
  21. package/dist/components/PasswordInput.d.ts +19 -0
  22. package/dist/components/PasswordInput.js +11 -0
  23. package/dist/components/PhoneInput.d.ts +23 -0
  24. package/dist/components/PhoneInput.js +147 -0
  25. package/dist/components/RefMultiSelect.d.ts +14 -0
  26. package/dist/components/RefMultiSelect.js +76 -0
  27. package/dist/components/RefSingleSelect.d.ts +17 -0
  28. package/dist/components/RefSingleSelect.js +52 -0
  29. package/dist/components/SchemaForm.d.ts +13 -0
  30. package/dist/components/SchemaForm.js +592 -0
  31. package/dist/components/SectionLoader.d.ts +3 -0
  32. package/dist/components/SectionLoader.js +7 -0
  33. package/dist/components/Sidebar.d.ts +1 -0
  34. package/dist/components/Sidebar.js +87 -0
  35. package/dist/components/TableFilters.d.ts +16 -0
  36. package/dist/components/TableFilters.js +69 -0
  37. package/dist/components/TableSkeleton.d.ts +7 -0
  38. package/dist/components/TableSkeleton.js +5 -0
  39. package/dist/hooks/useGoogleMapsKey.d.ts +5 -0
  40. package/dist/hooks/useGoogleMapsKey.js +16 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/lib/api.d.ts +31 -0
  44. package/dist/lib/api.js +94 -0
  45. package/dist/lib/auth.d.ts +23 -0
  46. package/dist/lib/auth.js +51 -0
  47. package/dist/lib/googleLoader.d.ts +1 -0
  48. package/dist/lib/googleLoader.js +25 -0
  49. package/dist/lib/schemaService.d.ts +2 -0
  50. package/dist/lib/schemaService.js +39 -0
  51. package/dist/lib/schemaUtils.d.ts +4 -0
  52. package/dist/lib/schemaUtils.js +18 -0
  53. package/dist/lib/types.d.ts +50 -0
  54. package/dist/lib/types.js +1 -0
  55. package/dist/nextmin.css +1 -0
  56. package/dist/providers/NextMinProvider.d.ts +5 -0
  57. package/dist/providers/NextMinProvider.js +30 -0
  58. package/dist/router/AdminRouteNormalizer.d.ts +1 -0
  59. package/dist/router/AdminRouteNormalizer.js +32 -0
  60. package/dist/router/NextMinRouter.d.ts +1 -0
  61. package/dist/router/NextMinRouter.js +99 -0
  62. package/dist/state/nextMinSlice.d.ts +14 -0
  63. package/dist/state/nextMinSlice.js +34 -0
  64. package/dist/state/schemaLive.d.ts +2 -0
  65. package/dist/state/schemaLive.js +19 -0
  66. package/dist/state/schemasSlice.d.ts +20 -0
  67. package/dist/state/schemasSlice.js +43 -0
  68. package/dist/state/sessionSlice.d.ts +10 -0
  69. package/dist/state/sessionSlice.js +18 -0
  70. package/dist/state/store.d.ts +28 -0
  71. package/dist/state/store.js +7 -0
  72. package/dist/views/CreateEditPage.d.ts +4 -0
  73. package/dist/views/CreateEditPage.js +64 -0
  74. package/dist/views/DashboardPage.d.ts +1 -0
  75. package/dist/views/DashboardPage.js +107 -0
  76. package/dist/views/ListPage.d.ts +5 -0
  77. package/dist/views/ListPage.js +76 -0
  78. package/dist/views/NextNotFound.d.ts +1 -0
  79. package/dist/views/NextNotFound.js +6 -0
  80. package/dist/views/ProfilePage.d.ts +1 -0
  81. package/dist/views/ProfilePage.js +193 -0
  82. package/dist/views/SettingsEdit.d.ts +2 -0
  83. package/dist/views/SettingsEdit.js +87 -0
  84. package/dist/views/list/DataTableHero.d.ts +22 -0
  85. package/dist/views/list/DataTableHero.js +350 -0
  86. package/dist/views/list/ListHeader.d.ts +8 -0
  87. package/dist/views/list/ListHeader.js +7 -0
  88. package/dist/views/list/Pagination.d.ts +8 -0
  89. package/dist/views/list/Pagination.js +5 -0
  90. package/dist/views/list/formatters.d.ts +2 -0
  91. package/dist/views/list/formatters.js +62 -0
  92. package/dist/views/list/useListData.d.ts +10 -0
  93. package/dist/views/list/useListData.js +79 -0
  94. package/package.json +51 -0
  95. package/tsconfig.json +18 -0
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { useEffect } from 'react';
3
+ import { usePathname, useSearchParams, useRouter } from 'next/navigation';
4
+ export function AdminRouteNormalizer() {
5
+ const pathname = usePathname();
6
+ const search = useSearchParams();
7
+ const router = useRouter();
8
+ useEffect(() => {
9
+ if (!pathname)
10
+ return;
11
+ // Only touch /admin paths
12
+ if (!pathname.startsWith('/admin'))
13
+ return;
14
+ // Collapse duplicate slashes and trim trailing slash (except exactly "/admin")
15
+ let normalized = pathname.replace(/\/{2,}/g, '/');
16
+ if (normalized !== '/admin') {
17
+ normalized = normalized.replace(/\/+$/, '');
18
+ }
19
+ // If nothing changed, exit
20
+ if (normalized === pathname)
21
+ return;
22
+ const query = search?.toString();
23
+ const hash = typeof window !== 'undefined' ? window.location.hash : '';
24
+ let target = normalized;
25
+ if (query)
26
+ target += `?${query}`;
27
+ if (hash)
28
+ target += hash;
29
+ router.replace(target);
30
+ }, [pathname, search, router]);
31
+ return null;
32
+ }
@@ -0,0 +1 @@
1
+ export declare function NextMinRouter(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useMemo, useEffect } from 'react';
4
+ import { usePathname, useRouter } from 'next/navigation';
5
+ import { useSelector } from 'react-redux';
6
+ import { ListPage } from '../views/ListPage';
7
+ import { CreateEditPage } from '../views/CreateEditPage';
8
+ import { NextNotFound } from '../views/NextNotFound';
9
+ import { DashboardPage } from '../views/DashboardPage';
10
+ import { SectionLoader } from '../components/SectionLoader';
11
+ import SettingsEdit from '../views/SettingsEdit';
12
+ import ProfilePage from '../views/ProfilePage';
13
+ export function NextMinRouter() {
14
+ const pathname = usePathname();
15
+ const router = useRouter();
16
+ const { items: schemas, status } = useSelector((s) => s.schemas);
17
+ const { state, atAdminRoot } = useMemo(() => {
18
+ const parts = (pathname || '/').split('/').filter(Boolean);
19
+ const idx = parts.indexOf('admin');
20
+ const segs = idx >= 0 ? parts.slice(idx + 1) : [];
21
+ const atRoot = idx >= 0 && segs.length === 0;
22
+ if (atRoot) {
23
+ return {
24
+ atAdminRoot: true,
25
+ state: { model: null, mode: 'dashboard', id: null },
26
+ };
27
+ }
28
+ if (segs[0]?.toLowerCase() === 'auth') {
29
+ return {
30
+ atAdminRoot: false,
31
+ state: { model: null, mode: 'dashboard', id: null },
32
+ };
33
+ }
34
+ if (segs[0]?.toLowerCase() === 'dashboard') {
35
+ return {
36
+ atAdminRoot: false,
37
+ state: { model: null, mode: 'dashboard', id: null },
38
+ };
39
+ }
40
+ if (segs[0]?.toLowerCase() === 'profile') {
41
+ return {
42
+ atAdminRoot: false,
43
+ state: { model: null, mode: 'profile', id: null },
44
+ };
45
+ }
46
+ const model = segs[0]?.toLowerCase() ?? null;
47
+ if (!model)
48
+ return {
49
+ atAdminRoot: false,
50
+ state: { model: null, mode: null, id: null },
51
+ };
52
+ if (segs.length === 1)
53
+ return {
54
+ atAdminRoot: false,
55
+ state: { model, mode: 'list', id: null },
56
+ };
57
+ if (segs[1] === 'create')
58
+ return {
59
+ atAdminRoot: false,
60
+ state: { model, mode: 'create', id: null },
61
+ };
62
+ if (segs[0] === 'system' && segs[1] === 'update')
63
+ return {
64
+ atAdminRoot: false,
65
+ state: { model, mode: 'system', id: null },
66
+ };
67
+ return {
68
+ atAdminRoot: false,
69
+ state: { model, mode: 'edit', id: segs[1] ?? null },
70
+ };
71
+ }, [pathname]);
72
+ // Canonicalize /admin → /admin/dashboard
73
+ useEffect(() => {
74
+ if (atAdminRoot)
75
+ router.replace('/admin/dashboard');
76
+ }, [atAdminRoot, router]);
77
+ // While schemas are loading, show an overlay only in this pane
78
+ if (status !== 'succeeded') {
79
+ return (_jsx("div", { className: "relative min-h-[200px]", children: _jsx(SectionLoader, { label: "Loading schemas\u2026" }) }));
80
+ }
81
+ if (state.mode === 'dashboard')
82
+ return _jsx(DashboardPage, {});
83
+ if (state.mode === 'profile')
84
+ return _jsx(ProfilePage, {});
85
+ if (!state.model)
86
+ return _jsx(DashboardPage, {});
87
+ if (state.mode === 'system')
88
+ return _jsx(SettingsEdit, {});
89
+ const schema = schemas.find((s) => s.modelName.toLowerCase() === state.model);
90
+ if (!schema)
91
+ return _jsx(NextNotFound, {});
92
+ if (state.mode === 'list')
93
+ return _jsx(ListPage, { model: state.model });
94
+ if (state.mode === 'create')
95
+ return _jsx(CreateEditPage, { model: state.model });
96
+ if (state.mode === 'edit' && state.id)
97
+ return _jsx(CreateEditPage, { model: state.model, id: state.id });
98
+ return _jsx(NextNotFound, {});
99
+ }
@@ -0,0 +1,14 @@
1
+ export type SystemSettings = {
2
+ apiKey?: string;
3
+ siteName?: string;
4
+ siteLogo?: string[];
5
+ googleMapsKey: string;
6
+ } | null;
7
+ type NextMinState = {
8
+ system: SystemSettings;
9
+ status: 'idle' | 'loading' | 'succeeded' | 'failed';
10
+ error?: string;
11
+ };
12
+ export declare const systemLoading: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"nextMin/systemLoading">, systemLoaded: import("@reduxjs/toolkit").ActionCreatorWithPayload<SystemSettings, "nextMin/systemLoaded">, systemFailed: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "nextMin/systemFailed">, setSystem: import("@reduxjs/toolkit").ActionCreatorWithPayload<SystemSettings, "nextMin/setSystem">, clearSystem: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"nextMin/clearSystem">;
13
+ declare const _default: import("redux").Reducer<NextMinState>;
14
+ export default _default;
@@ -0,0 +1,34 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ const initialState = {
3
+ system: null,
4
+ status: 'idle',
5
+ };
6
+ const nextMinSlice = createSlice({
7
+ name: 'nextMin',
8
+ initialState,
9
+ reducers: {
10
+ systemLoading(state) {
11
+ state.status = 'loading';
12
+ state.error = undefined;
13
+ },
14
+ systemLoaded(state, action) {
15
+ state.status = 'succeeded';
16
+ state.system = action.payload ?? null;
17
+ state.error = undefined;
18
+ },
19
+ systemFailed(state, action) {
20
+ state.status = 'failed';
21
+ state.error = action.payload;
22
+ },
23
+ setSystem(state, action) {
24
+ state.system = action.payload ?? null;
25
+ },
26
+ clearSystem(state) {
27
+ state.system = null;
28
+ state.status = 'idle';
29
+ state.error = undefined;
30
+ },
31
+ },
32
+ });
33
+ export const { systemLoading, systemLoaded, systemFailed, setSystem, clearSystem, } = nextMinSlice.actions;
34
+ export default nextMinSlice.reducer;
@@ -0,0 +1,2 @@
1
+ import { NextMinStore } from './store';
2
+ export declare function startSchemaLive(store: NextMinStore): void;
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { setSchemas } from './schemasSlice';
3
+ import { getSchemaService } from '../lib/schemaService';
4
+ export function startSchemaLive(store) {
5
+ const sock = getSchemaService();
6
+ if (!sock)
7
+ return;
8
+ if (sock.__nm_bound)
9
+ return;
10
+ sock.__nm_bound = true;
11
+ const push = (all) => {
12
+ const data = Array.isArray(all) ? all : Object.values(all || {});
13
+ store.dispatch(setSchemas(data));
14
+ };
15
+ sock.on('schemasData', push);
16
+ sock.on('schemasUpdated', push);
17
+ sock.on('connect', () => console.log('[nextmin] schema service connected', sock.id));
18
+ sock.on('connect_error', (e) => console.warn('[nextmin] schema service connect_error:', e?.message));
19
+ }
@@ -0,0 +1,20 @@
1
+ import { SchemaDef } from '../lib/types';
2
+ type Status = 'idle' | 'loading' | 'succeeded' | 'failed';
3
+ export type SchemasState = {
4
+ items: SchemaDef[];
5
+ status: Status;
6
+ error?: string;
7
+ };
8
+ export declare const fetchSchemas: import("@reduxjs/toolkit").AsyncThunk<SchemaDef[], void, {
9
+ state?: unknown;
10
+ dispatch?: import("redux-thunk").ThunkDispatch<unknown, unknown, import("redux").UnknownAction>;
11
+ extra?: unknown;
12
+ rejectValue?: unknown;
13
+ serializedErrorType?: unknown;
14
+ pendingMeta?: unknown;
15
+ fulfilledMeta?: unknown;
16
+ rejectedMeta?: unknown;
17
+ }>;
18
+ export declare const setSchemas: import("@reduxjs/toolkit").ActionCreatorWithPayload<SchemaDef[], "schemas/setSchemas">;
19
+ declare const _default: import("redux").Reducer<SchemasState>;
20
+ export default _default;
@@ -0,0 +1,43 @@
1
+ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2
+ import { api } from '../lib/api';
3
+ const initialState = {
4
+ items: [],
5
+ status: 'idle',
6
+ };
7
+ export const fetchSchemas = createAsyncThunk('schemas/fetch', async () => {
8
+ const res = (await api.getSchemas());
9
+ return res.data;
10
+ }, {
11
+ condition: (_arg, { getState }) => {
12
+ const { status } = getState().schemas;
13
+ // skip if already fetched or fetching
14
+ return status === 'idle';
15
+ },
16
+ });
17
+ const schemasSlice = createSlice({
18
+ name: 'schemas',
19
+ initialState,
20
+ reducers: {
21
+ setSchemas(state, action) {
22
+ state.items = action.payload;
23
+ state.status = 'succeeded';
24
+ },
25
+ },
26
+ extraReducers(builder) {
27
+ builder
28
+ .addCase(fetchSchemas.pending, (state) => {
29
+ state.status = 'loading';
30
+ state.error = undefined;
31
+ })
32
+ .addCase(fetchSchemas.fulfilled, (state, action) => {
33
+ state.status = 'succeeded';
34
+ state.items = action.payload;
35
+ })
36
+ .addCase(fetchSchemas.rejected, (state, action) => {
37
+ state.status = 'failed';
38
+ state.error = action.error.message;
39
+ });
40
+ },
41
+ });
42
+ export const { setSchemas } = schemasSlice.actions;
43
+ export default schemasSlice.reducer;
@@ -0,0 +1,10 @@
1
+ type Session = {
2
+ token: string | null;
3
+ user: any | null;
4
+ };
5
+ export declare const setSession: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
6
+ token: string;
7
+ user: any;
8
+ }, "session/setSession">, clearSession: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"session/clearSession">;
9
+ declare const _default: import("redux").Reducer<Session>;
10
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ const initialState = { token: null, user: null };
3
+ const sessionSlice = createSlice({
4
+ name: 'session',
5
+ initialState,
6
+ reducers: {
7
+ setSession: (s, a) => {
8
+ s.token = a.payload.token;
9
+ s.user = a.payload.user;
10
+ },
11
+ clearSession: (s) => {
12
+ s.token = null;
13
+ s.user = null;
14
+ },
15
+ },
16
+ });
17
+ export const { setSession, clearSession } = sessionSlice.actions;
18
+ export default sessionSlice.reducer;
@@ -0,0 +1,28 @@
1
+ export declare const nextminstore: import("@reduxjs/toolkit").EnhancedStore<{
2
+ schemas: import("./schemasSlice").SchemasState;
3
+ nextMin: {
4
+ system: import("./nextMinSlice").SystemSettings;
5
+ status: "idle" | "loading" | "succeeded" | "failed";
6
+ error?: string;
7
+ };
8
+ session: {
9
+ token: string | null;
10
+ user: any | null;
11
+ };
12
+ }, import("redux").UnknownAction, import("@reduxjs/toolkit").Tuple<[import("redux").StoreEnhancer<{
13
+ dispatch: import("redux-thunk").ThunkDispatch<{
14
+ schemas: import("./schemasSlice").SchemasState;
15
+ nextMin: {
16
+ system: import("./nextMinSlice").SystemSettings;
17
+ status: "idle" | "loading" | "succeeded" | "failed";
18
+ error?: string;
19
+ };
20
+ session: {
21
+ token: string | null;
22
+ user: any | null;
23
+ };
24
+ }, undefined, import("redux").UnknownAction>;
25
+ }>, import("redux").StoreEnhancer]>>;
26
+ export type NextMinStore = typeof nextminstore;
27
+ export type NextMinRootState = ReturnType<NextMinStore['getState']>;
28
+ export type NextMinDispatch = NextMinStore['dispatch'];
@@ -0,0 +1,7 @@
1
+ import { configureStore } from '@reduxjs/toolkit';
2
+ import schemasReducer from './schemasSlice';
3
+ import nextMinReducer from './nextMinSlice';
4
+ import sessionReducer from './sessionSlice';
5
+ export const nextminstore = configureStore({
6
+ reducer: { schemas: schemasReducer, nextMin: nextMinReducer, session: sessionReducer },
7
+ });
@@ -0,0 +1,4 @@
1
+ export declare function CreateEditPage({ model, id }: {
2
+ model: string;
3
+ id?: string;
4
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useSelector } from 'react-redux';
6
+ import { api } from '../lib/api';
7
+ import { SchemaForm } from '../components/SchemaForm';
8
+ import Link from 'next/link';
9
+ import { Button, Divider } from '@heroui/react';
10
+ export function CreateEditPage({ model, id }) {
11
+ const router = useRouter();
12
+ const { items } = useSelector((s) => s.schemas);
13
+ const schema = useMemo(() => items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model]);
14
+ const [initialValues, setInitialValues] = useState({});
15
+ const [busy, setBusy] = useState(false);
16
+ const [error, setError] = useState();
17
+ useEffect(() => {
18
+ let cancelled = false;
19
+ if (!id) {
20
+ setInitialValues({});
21
+ setError(undefined);
22
+ return;
23
+ }
24
+ (async () => {
25
+ try {
26
+ const res = await api.get(model, id);
27
+ if (!cancelled) {
28
+ setInitialValues(res.data || {});
29
+ setError(undefined);
30
+ }
31
+ }
32
+ catch (e) {
33
+ if (!cancelled)
34
+ setError(e?.message || 'Failed to load');
35
+ }
36
+ })();
37
+ return () => {
38
+ cancelled = true;
39
+ };
40
+ }, [id, model]);
41
+ const handleSubmit = async (values) => {
42
+ setBusy(true);
43
+ setError(undefined);
44
+ try {
45
+ if (id) {
46
+ await api.update(model, id, values);
47
+ }
48
+ else {
49
+ await api.create(model, values);
50
+ }
51
+ router.push(`/admin/${model}`);
52
+ router.refresh();
53
+ }
54
+ catch (e) {
55
+ setError(e?.message || 'Save failed');
56
+ }
57
+ finally {
58
+ setBusy(false);
59
+ }
60
+ };
61
+ if (!schema)
62
+ return _jsx("div", { className: "text-danger text-sm", children: "Schema not found" });
63
+ return (_jsxs("div", { className: "grid gap-4 px-4", children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsx("h2", { className: "m-0 text-xl font-semibold", children: id ? `Edit ${schema.modelName}` : `Create ${schema.modelName}` }), _jsx("div", { className: "flex items-center gap-3", children: _jsx(Button, { as: Link, href: `/admin/${model}`, size: "sm", variant: "flat", color: "danger", isDisabled: busy, children: "Cancel" }) })] }), _jsx(Divider, { className: "my-3" }), error && _jsx("div", { className: "text-danger text-sm", children: error }), _jsx(SchemaForm, { model: model, schemaOverride: schema, initialValues: initialValues, submitLabel: id ? 'Save' : 'Create', busy: busy, onSubmit: handleSubmit })] }));
64
+ }
@@ -0,0 +1 @@
1
+ export declare function DashboardPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { useSelector } from 'react-redux';
5
+ import { api } from '../lib/api';
6
+ import { Divider, Card, CardBody } from '@heroui/react';
7
+ function ArrowUpRightIcon(props) {
8
+ return (_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": true, ...props, children: _jsx("path", { d: "M7 17L17 7M17 7H9M17 7v8", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
9
+ }
10
+ function UsersIcon(props) {
11
+ return (_jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": true, ...props, children: [_jsx("path", { d: "M16 21v-2a4 4 0 00-4-4H7a4 4 0 00-4 4v2", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("circle", { cx: "9", cy: "7", r: "4", stroke: "currentColor", strokeWidth: "2" }), _jsx("path", { d: "M22 21v-2a3 3 0 00-3-3h-2", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M16 3.13a4 4 0 010 7.75", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })] }));
12
+ }
13
+ function DoctorIcon(props) {
14
+ return (_jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": true, ...props, children: [_jsx("path", { d: "M12 12a5 5 0 100-10 5 5 0 000 10z", stroke: "currentColor", strokeWidth: "2" }), _jsx("path", { d: "M4 22v-1a7 7 0 017-7h2a7 7 0 017 7v1", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("path", { d: "M12 7v3M10.5 8.5h3", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }));
15
+ }
16
+ function HospitalIcon(props) {
17
+ return (_jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": true, ...props, children: [_jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", stroke: "currentColor", strokeWidth: "2" }), _jsx("path", { d: "M12 7v10M7 12h10", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }));
18
+ }
19
+ function CalendarIcon(props) {
20
+ return (_jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": true, ...props, children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "17", rx: "2", stroke: "currentColor", strokeWidth: "2" }), _jsx("path", { d: "M8 2v4M16 2v4M3 10h18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }));
21
+ }
22
+ export function DashboardPage() {
23
+ const schemas = useSelector((s) => s.schemas.items);
24
+ const status = useSelector((s) => s.schemas.status);
25
+ const cards = useMemo(() => {
26
+ const byName = (name) => schemas.some((s) => s.modelName?.toLowerCase() === name);
27
+ return [
28
+ {
29
+ key: 'users',
30
+ label: 'Total users',
31
+ icon: _jsx(UsersIcon, { className: "text-primary" }),
32
+ colorClass: 'bg-primary/10 text-primary',
33
+ model: byName('users') ? 'users' : undefined,
34
+ },
35
+ {
36
+ key: 'doctors',
37
+ label: 'Total doctors',
38
+ icon: _jsx(DoctorIcon, { className: "text-success" }),
39
+ colorClass: 'bg-success/10 text-success',
40
+ model: byName('doctors') ? 'doctors' : undefined,
41
+ },
42
+ {
43
+ key: 'hospitals',
44
+ label: 'Total hospitals',
45
+ icon: _jsx(HospitalIcon, { className: "text-warning" }),
46
+ colorClass: 'bg-warning/10 text-warning',
47
+ model: byName('hospitals') ? 'hospitals' : undefined,
48
+ },
49
+ {
50
+ key: 'appointments',
51
+ label: 'Total appointments',
52
+ icon: _jsx(CalendarIcon, { className: "text-secondary" }),
53
+ colorClass: 'bg-secondary/10 text-secondary',
54
+ model: byName('appointments') ? 'appointments' : undefined,
55
+ },
56
+ ];
57
+ }, [schemas]);
58
+ const [counts, setCounts] = useState({});
59
+ const [loading, setLoading] = useState(false);
60
+ const [error, setError] = useState(null);
61
+ useEffect(() => {
62
+ let cancelled = false;
63
+ async function load() {
64
+ if (status !== 'succeeded')
65
+ return;
66
+ setLoading(true);
67
+ setError(null);
68
+ try {
69
+ const entries = await Promise.all(cards.map(async (c) => {
70
+ if (!c.model)
71
+ return [c.key, null]; // schema missing
72
+ try {
73
+ const res = await api.list(c.model, 0, 1);
74
+ const total = res?.pagination?.totalRows ?? res?.data?.length ?? 0;
75
+ return [c.key, total];
76
+ }
77
+ catch (e) {
78
+ return [c.key, null];
79
+ }
80
+ }));
81
+ if (!cancelled) {
82
+ const map = {};
83
+ for (const [k, v] of entries)
84
+ map[k] = v;
85
+ setCounts(map);
86
+ }
87
+ }
88
+ catch (e) {
89
+ if (!cancelled)
90
+ setError(e?.message || 'Failed to load stats');
91
+ }
92
+ finally {
93
+ if (!cancelled)
94
+ setLoading(false);
95
+ }
96
+ }
97
+ load();
98
+ return () => {
99
+ cancelled = true;
100
+ };
101
+ }, [cards, status]);
102
+ return (_jsxs("div", { className: "grid gap-4 px-4", children: [_jsx("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: _jsx("h2", { className: "m-0 text-xl font-semibold", children: "Dashboard" }) }), _jsx(Divider, { className: "my-3" }), (error || loading) && (_jsx("div", { className: 'rounded-md px-3 py-2 ' +
103
+ (error ? 'bg-red-100 text-red-700' : 'bg-default-100 text-default-700'), children: error || 'Loading summary…' })), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4", children: cards.map((c) => {
104
+ const value = counts[c.key];
105
+ return (_jsx(Card, { className: "shadow-sm border border-default-200", children: _jsxs(CardBody, { className: "flex flex-row items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm text-foreground/60", children: c.label }), _jsx("div", { className: "text-2xl font-semibold mt-1", children: value == null ? '—' : value })] }), _jsx("div", { className: `h-10 w-10 rounded-md flex items-center justify-center ${c.colorClass}`, children: c.icon })] }) }, c.key));
106
+ }) })] }));
107
+ }
@@ -0,0 +1,5 @@
1
+ type Props = {
2
+ model: string;
3
+ };
4
+ export declare function ListPage({ model }: Props): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState, useEffect, useCallback } from 'react';
4
+ import { useSelector } from 'react-redux';
5
+ import { useListData } from './list/useListData';
6
+ import { ListHeader } from './list/ListHeader';
7
+ import { DataTableHero } from './list/DataTableHero';
8
+ import { TableFilters } from '../components/TableFilters';
9
+ import { NoAccess } from '../components/NoAccess';
10
+ export function ListPage({ model }) {
11
+ // paging + filters
12
+ const [page, setPage] = useState(1); // 1-based
13
+ const [pageSize, setPageSize] = useState(10);
14
+ const [filters, setFilters] = useState({});
15
+ // schema
16
+ const { items } = useSelector((s) => s.schemas);
17
+ const schema = useMemo(() => items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model]);
18
+ // reset on model change
19
+ useEffect(() => {
20
+ setPage(1);
21
+ setPageSize(10);
22
+ setFilters({});
23
+ }, [model]);
24
+ // data
25
+ const [{ rows, total, loading, forbidden, err }, refetch] = useListData(model, page, pageSize, {
26
+ q: filters.q,
27
+ searchKey: filters.searchKey,
28
+ });
29
+ // columns (ALWAYS call hooks; derive from schema or empty object)
30
+ const attributes = (schema?.attributes ?? {});
31
+ const allColumns = useMemo(() => Object.entries(attributes)
32
+ .filter(([, a]) => !a?.private)
33
+ .map(([k]) => k), [attributes]);
34
+ // default visible: first 4 + 'createdAt' if present (stable via useMemo)
35
+ const defaultVisible = useMemo(() => {
36
+ const set = new Set();
37
+ // first 4 non-private fields
38
+ allColumns.slice(0, 4).forEach((c) => set.add(c));
39
+ // force-include 'type' if present
40
+ if (allColumns.includes('type'))
41
+ set.add('type');
42
+ // force-include 'createdAt' if present
43
+ if (allColumns.includes('createdAt'))
44
+ set.add('createdAt');
45
+ return Array.from(set);
46
+ }, [allColumns]);
47
+ // visible columns state (init once, update when model/defaultVisible change)
48
+ const [visibleColumns, setVisibleColumns] = useState(() => new Set(defaultVisible));
49
+ useEffect(() => {
50
+ // whenever model or defaultVisible changes, reset visible columns
51
+ setVisibleColumns(new Set(defaultVisible));
52
+ }, [model, defaultVisible]);
53
+ const tableColumns = useMemo(() => allColumns.filter((c) => visibleColumns.has(c)), [allColumns, visibleColumns]);
54
+ const baseHref = useMemo(() => `/admin/${model}`, [model]);
55
+ const handleRefetch = useCallback(async () => {
56
+ await refetch();
57
+ }, [refetch]);
58
+ // Choose content WITHOUT early-returning before hooks
59
+ let content = null;
60
+ if (!schema) {
61
+ content = _jsx("div", { style: { padding: 16 }, children: "Schema not found" });
62
+ }
63
+ else if (forbidden) {
64
+ content = (_jsx(NoAccess, { message: err ?? 'You are not permitted to view this resource.' }));
65
+ }
66
+ else {
67
+ content = (_jsx("div", { className: "overflow-hidden", children: _jsx(DataTableHero, { topContent: _jsx(TableFilters, { model: model, value: filters, busy: loading, onChange: (v) => {
68
+ setFilters(v);
69
+ setPage(1);
70
+ }, columns: allColumns, visibleColumns: visibleColumns, onVisibleColumnsChange: (keys) => setVisibleColumns(new Set([...keys])) }), modelName: model, columns: tableColumns, rows: rows, total: total, page: page, pageSize: pageSize, onPageChange: setPage, onDeleted: handleRefetch, onPageSizeChange: (n) => {
71
+ setPageSize(n);
72
+ setPage(1);
73
+ }, baseHref: baseHref, loading: loading, error: err ?? undefined }) }));
74
+ }
75
+ return (_jsxs("div", { className: "grid gap-3 px-4", children: [_jsx(ListHeader, { title: schema?.modelName ?? model, createHref: `${baseHref}/create`, loading: loading }), content] }));
76
+ }
@@ -0,0 +1 @@
1
+ export declare function NextNotFound(): null;
@@ -0,0 +1,6 @@
1
+ 'use client';
2
+ import { notFound } from 'next/navigation';
3
+ export function NextNotFound() {
4
+ notFound(); // renders app/not-found.tsx if present, or Next's default 404
5
+ return null;
6
+ }
@@ -0,0 +1 @@
1
+ export default function ProfilePage(): import("react/jsx-runtime").JSX.Element;