@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.
- package/LICENSE +49 -0
- package/README.md +133 -0
- package/dist/auth/AuthPage.d.ts +1 -0
- package/dist/auth/AuthPage.js +23 -0
- package/dist/auth/ForgotPasswordForm.d.ts +1 -0
- package/dist/auth/ForgotPasswordForm.js +28 -0
- package/dist/auth/SignInForm.d.ts +6 -0
- package/dist/auth/SignInForm.js +38 -0
- package/dist/auth/SignUpForm.d.ts +3 -0
- package/dist/auth/SignUpForm.js +30 -0
- package/dist/components/AddressAutocomplete.d.ts +21 -0
- package/dist/components/AddressAutocomplete.js +182 -0
- package/dist/components/AdminApp.d.ts +1 -0
- package/dist/components/AdminApp.js +134 -0
- package/dist/components/ConfirmDialog.d.ts +12 -0
- package/dist/components/ConfirmDialog.js +6 -0
- package/dist/components/FileUploader.d.ts +32 -0
- package/dist/components/FileUploader.js +480 -0
- package/dist/components/NoAccess.d.ts +3 -0
- package/dist/components/NoAccess.js +5 -0
- package/dist/components/PasswordInput.d.ts +19 -0
- package/dist/components/PasswordInput.js +11 -0
- package/dist/components/PhoneInput.d.ts +23 -0
- package/dist/components/PhoneInput.js +147 -0
- package/dist/components/RefMultiSelect.d.ts +14 -0
- package/dist/components/RefMultiSelect.js +76 -0
- package/dist/components/RefSingleSelect.d.ts +17 -0
- package/dist/components/RefSingleSelect.js +52 -0
- package/dist/components/SchemaForm.d.ts +13 -0
- package/dist/components/SchemaForm.js +592 -0
- package/dist/components/SectionLoader.d.ts +3 -0
- package/dist/components/SectionLoader.js +7 -0
- package/dist/components/Sidebar.d.ts +1 -0
- package/dist/components/Sidebar.js +87 -0
- package/dist/components/TableFilters.d.ts +16 -0
- package/dist/components/TableFilters.js +69 -0
- package/dist/components/TableSkeleton.d.ts +7 -0
- package/dist/components/TableSkeleton.js +5 -0
- package/dist/hooks/useGoogleMapsKey.d.ts +5 -0
- package/dist/hooks/useGoogleMapsKey.js +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/api.d.ts +31 -0
- package/dist/lib/api.js +94 -0
- package/dist/lib/auth.d.ts +23 -0
- package/dist/lib/auth.js +51 -0
- package/dist/lib/googleLoader.d.ts +1 -0
- package/dist/lib/googleLoader.js +25 -0
- package/dist/lib/schemaService.d.ts +2 -0
- package/dist/lib/schemaService.js +39 -0
- package/dist/lib/schemaUtils.d.ts +4 -0
- package/dist/lib/schemaUtils.js +18 -0
- package/dist/lib/types.d.ts +50 -0
- package/dist/lib/types.js +1 -0
- package/dist/nextmin.css +1 -0
- package/dist/providers/NextMinProvider.d.ts +5 -0
- package/dist/providers/NextMinProvider.js +30 -0
- package/dist/router/AdminRouteNormalizer.d.ts +1 -0
- package/dist/router/AdminRouteNormalizer.js +32 -0
- package/dist/router/NextMinRouter.d.ts +1 -0
- package/dist/router/NextMinRouter.js +99 -0
- package/dist/state/nextMinSlice.d.ts +14 -0
- package/dist/state/nextMinSlice.js +34 -0
- package/dist/state/schemaLive.d.ts +2 -0
- package/dist/state/schemaLive.js +19 -0
- package/dist/state/schemasSlice.d.ts +20 -0
- package/dist/state/schemasSlice.js +43 -0
- package/dist/state/sessionSlice.d.ts +10 -0
- package/dist/state/sessionSlice.js +18 -0
- package/dist/state/store.d.ts +28 -0
- package/dist/state/store.js +7 -0
- package/dist/views/CreateEditPage.d.ts +4 -0
- package/dist/views/CreateEditPage.js +64 -0
- package/dist/views/DashboardPage.d.ts +1 -0
- package/dist/views/DashboardPage.js +107 -0
- package/dist/views/ListPage.d.ts +5 -0
- package/dist/views/ListPage.js +76 -0
- package/dist/views/NextNotFound.d.ts +1 -0
- package/dist/views/NextNotFound.js +6 -0
- package/dist/views/ProfilePage.d.ts +1 -0
- package/dist/views/ProfilePage.js +193 -0
- package/dist/views/SettingsEdit.d.ts +2 -0
- package/dist/views/SettingsEdit.js +87 -0
- package/dist/views/list/DataTableHero.d.ts +22 -0
- package/dist/views/list/DataTableHero.js +350 -0
- package/dist/views/list/ListHeader.d.ts +8 -0
- package/dist/views/list/ListHeader.js +7 -0
- package/dist/views/list/Pagination.d.ts +8 -0
- package/dist/views/list/Pagination.js +5 -0
- package/dist/views/list/formatters.d.ts +2 -0
- package/dist/views/list/formatters.js +62 -0
- package/dist/views/list/useListData.d.ts +10 -0
- package/dist/views/list/useListData.js +79 -0
- package/package.json +51 -0
- 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,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,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,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 @@
|
|
|
1
|
+
export default function ProfilePage(): import("react/jsx-runtime").JSX.Element;
|