@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,87 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { useDispatch, useSelector } from 'react-redux';
5
+ import { useRouter } from 'next/navigation';
6
+ import { clearSession } from '../state/sessionSlice';
7
+ import { Button } from '@heroui/react';
8
+ import Link from 'next/link';
9
+ export function Sidebar() {
10
+ const router = useRouter();
11
+ const dispatch = useDispatch();
12
+ const system = useSelector((s) => s.nextMin.system);
13
+ const systemStatus = useSelector((s) => s.nextMin.status);
14
+ const { items, status, error } = useSelector((s) => s.schemas);
15
+ const logout = React.useCallback(() => {
16
+ try {
17
+ localStorage.removeItem('nextmin.token');
18
+ localStorage.removeItem('nextmin.user');
19
+ }
20
+ catch { }
21
+ dispatch(clearSession());
22
+ router.replace('/admin/auth/sign-in');
23
+ }, [dispatch, router]);
24
+ const isLoading = status === 'idle' || status === 'loading';
25
+ const orderedItems = React.useMemo(() => {
26
+ const normal = [];
27
+ const sysBucket = [];
28
+ const pickAttrString = (obj, prop) => {
29
+ if (obj &&
30
+ typeof obj === 'object' &&
31
+ prop in obj) {
32
+ const v = obj[prop];
33
+ return typeof v === 'string' ? v : null;
34
+ }
35
+ return null;
36
+ };
37
+ const pickTypeValue = (t) => {
38
+ if (typeof t === 'string')
39
+ return t;
40
+ const d = pickAttrString(t, 'default');
41
+ if (d)
42
+ return d;
43
+ const v = pickAttrString(t, 'value');
44
+ if (v)
45
+ return v;
46
+ return '';
47
+ };
48
+ for (const sc of items || []) {
49
+ const t = sc?.attributes?.type;
50
+ const typeString = pickTypeValue(t);
51
+ const isSystem = typeString.toLowerCase() === 'system';
52
+ (isSystem ? sysBucket : normal).push(sc);
53
+ }
54
+ const byName = (a, b) => String(a?.modelName || '').localeCompare(String(b?.modelName || ''));
55
+ normal.sort(byName);
56
+ sysBucket.sort(byName);
57
+ return [...normal, ...sysBucket];
58
+ }, [items]);
59
+ // Read user info from localStorage (client only)
60
+ const [user, setUser] = React.useState(null);
61
+ React.useEffect(() => {
62
+ try {
63
+ const raw = localStorage.getItem('nextmin.user');
64
+ if (raw)
65
+ setUser(JSON.parse(raw));
66
+ }
67
+ catch {
68
+ setUser(null);
69
+ }
70
+ }, []);
71
+ // —— Brand (logo/name) ——
72
+ const siteName = React.useMemo(() => (system?.siteName && system.siteName.trim()) || 'NextMin', [system]);
73
+ const logoUrl = React.useMemo(() => {
74
+ const arr = Array.isArray(system?.siteLogo) ? system.siteLogo : [];
75
+ const first = arr.find((u) => typeof u === 'string' && u.trim().length > 0);
76
+ return first || undefined;
77
+ }, [system]);
78
+ return (_jsxs("aside", { className: "flex h-full min-h-screen w-64 flex-col border-r", children: [_jsx(Link, { href: "/admin/dashboard", className: "h-20 px-3 border-b flex items-center justify-center", children: logoUrl ? (_jsx("img", { src: logoUrl, alt: siteName, className: "h-10 max-w-[160px] object-contain" })) : (_jsx("div", { className: "text-lg font-semibold truncate", children: siteName })) }), _jsxs("div", { className: "flex-1 overflow-y-auto p-2", children: [isLoading && _jsx("div", { children: "Loading\u2026" }), status === 'failed' && error && (_jsx("div", { className: "text-red-600", children: error })), status === 'succeeded' && (_jsxs("ul", { className: "list-none space-y-2", children: [_jsx("li", { children: _jsx(Link, { href: "/admin/dashboard", className: "w-full hover:bg-gray-200 px-3 py-2 rounded-md inline-flex items-center gap-2", children: "Dashboard" }) }, "dashboard"), orderedItems.map((s) => {
79
+ const slug = String(s.modelName || '').toLowerCase();
80
+ // Hide auto Settings model from the list; we link a fixed Settings route below
81
+ if (slug === 'settings')
82
+ return null;
83
+ return (_jsx("li", { children: _jsx(Link, { href: `/admin/${slug}`, className: "w-full hover:bg-gray-200 px-3 py-2 rounded-md inline-flex items-center gap-2", children: s.modelName }) }, s.modelName));
84
+ }), _jsx("li", { children: _jsx(Link, { href: "/admin/system/update", className: "w-full hover:bg-gray-200 px-3 py-2 rounded-md inline-flex items-center gap-2", children: "Settings" }) }, "settings"), !items.length && (_jsx("li", { className: "text-gray-500", children: "No schemas found" }))] }))] }), _jsxs("div", { className: "mt-auto border-t p-4 space-y-2", children: [_jsx(Link, { href: "/admin/profile", className: "w-full block hover:bg-gray-200 px-3 py-2 rounded-md", children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [user && (_jsxs("div", { className: "text-sm text-gray-700 text-left", children: [_jsx("div", { className: "font-semibold", children: `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim() ||
85
+ user.name ||
86
+ '' }), _jsx("div", { className: "text-gray-500", children: user.email })] })), _jsx("svg", { "aria-hidden": "true", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: "text-default-500 flex-shrink-0", children: _jsx("path", { d: "M9 6l6 6-6 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })] }) }), _jsx(Button, { fullWidth: true, variant: "flat", onPress: logout, children: "Log out" })] })] }));
87
+ }
@@ -0,0 +1,16 @@
1
+ export type FilterValue = {
2
+ q?: string;
3
+ searchKey?: string;
4
+ };
5
+ type Props = {
6
+ model: string;
7
+ value: FilterValue;
8
+ onChange: (v: FilterValue) => void;
9
+ busy?: boolean;
10
+ /** NEW: column visibility control */
11
+ columns: string[];
12
+ visibleColumns: Set<string>;
13
+ onVisibleColumnsChange: (v: Set<string>) => void;
14
+ };
15
+ export declare function TableFilters({ model, value, onChange, busy, columns, visibleColumns, onVisibleColumnsChange, }: Props): import("react/jsx-runtime").JSX.Element | null;
16
+ export {};
@@ -0,0 +1,69 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { Form, Input, Select, SelectItem, Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, } from '@heroui/react';
5
+ import { useSelector } from 'react-redux';
6
+ import { inputTypeFor } from '../lib/schemaUtils';
7
+ export function TableFilters({ model, value, onChange, busy, columns, visibleColumns, onVisibleColumnsChange, }) {
8
+ const { items } = useSelector((s) => s.schemas);
9
+ const schema = React.useMemo(() => items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model]);
10
+ const [searchKey, setSearchKey] = React.useState(value.searchKey);
11
+ const [q, setQ] = React.useState(value.q ?? '');
12
+ React.useEffect(() => {
13
+ setSearchKey(value.searchKey);
14
+ setQ(value.q ?? '');
15
+ }, [value]);
16
+ if (!schema)
17
+ return null;
18
+ const keys = React.useMemo(() => Object.entries(schema.attributes)
19
+ .filter(([, a]) => !a?.private)
20
+ .map(([k]) => k), [schema]);
21
+ const selectedAttr = searchKey
22
+ ? schema.attributes[searchKey]
23
+ : undefined;
24
+ const isBoolean = selectedAttr && inputTypeFor(selectedAttr) === 'checkbox';
25
+ const apply = (e) => {
26
+ e.preventDefault();
27
+ onChange({
28
+ q: q?.trim() || undefined,
29
+ searchKey: searchKey || undefined,
30
+ });
31
+ };
32
+ const clear = () => {
33
+ setSearchKey(undefined);
34
+ setQ('');
35
+ onChange({});
36
+ };
37
+ return (_jsxs(Form, { onSubmit: apply, onReset: clear, className: "grid gap-3 md:grid-cols-5 grid-cols-1 items-end", children: [_jsx(Select, { placeholder: "Search Field", "aria-label": "Search Field", variant: "bordered", size: "sm", selectedKeys: searchKey ? new Set([searchKey]) : new Set(), onSelectionChange: (s) => {
38
+ if (s === 'all')
39
+ return;
40
+ const v = Array.from(s)[0];
41
+ setSearchKey(v);
42
+ setQ(''); // reset q when field changes
43
+ }, className: "w-full", selectionMode: "single", isDisabled: busy, children: keys.map((k) => (_jsx(SelectItem, { textValue: String(formatLabel(k)), children: formatLabel(k) }, k))) }), _jsx("div", { className: "w-full md:col-span-2", children: isBoolean ? (_jsxs(Select, { variant: "bordered", size: "sm", placeholder: "Select value", "aria-label": "Select value", selectedKeys: q ? new Set([q]) : new Set(), onSelectionChange: (s) => {
44
+ if (s === 'all')
45
+ return;
46
+ const v = Array.from(s)[0];
47
+ setQ(v === 'true' ? 'true' : v === 'false' ? 'false' : '');
48
+ }, className: "w-full", isDisabled: busy, children: [_jsx(SelectItem, { textValue: "True", children: "True" }, "true"), _jsx(SelectItem, { textValue: "True", children: "False" }, "false")] })) : (_jsx(Input, { size: "sm", placeholder: "Search", variant: "bordered", type: "search", fullWidth: true, classNames: {
49
+ input: 'px-2',
50
+ }, value: q, onChange: (e) => setQ(e.target.value), isDisabled: busy })) }), _jsxs("div", { className: "flex md:col-span-1 gap-2", children: [_jsx(Button, { type: "submit", color: "primary", isDisabled: busy, size: "sm", children: "Apply" }), _jsx(Button, { type: "reset", variant: "flat", isDisabled: busy, size: "sm", children: "Clear" })] }), _jsx("div", { className: "flex justify-end md:col-span-1 gap-2", children: _jsxs(Dropdown, { size: "sm", children: [_jsx(DropdownTrigger, { className: "hidden sm:flex", children: _jsx(Button, { size: "sm", variant: "flat", endContent: _jsx(ChevronDownSmall, {}), children: "Columns" }) }), _jsx(DropdownMenu, { disallowEmptySelection: true, "aria-label": "Table Columns", closeOnSelect: false, selectionMode: "multiple", selectedKeys: visibleColumns, onSelectionChange: (keys) => {
51
+ if (keys === 'all')
52
+ return;
53
+ onVisibleColumnsChange(keys);
54
+ }, children: columns.map((col) => (_jsx(DropdownItem, { className: "capitalize", children: formatLabel(col) }, col))) })] }) })] }));
55
+ }
56
+ /** tiny inline chevron (no icon pkg) */
57
+ function ChevronDownSmall() {
58
+ return (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", className: "text-small", children: _jsx("path", { d: "M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.24 4.24a.75.75 0 01-1.06 0L5.21 8.29a.75.75 0 01.02-1.08z" }) }));
59
+ }
60
+ function formatLabel(raw) {
61
+ const spaced = raw
62
+ .replace(/[_\-]+/g, ' ')
63
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
64
+ .trim();
65
+ return spaced
66
+ .split(/\s+/)
67
+ .map((w) => (w ? w[0].toUpperCase() + w.slice(1).toLowerCase() : w))
68
+ .join(' ');
69
+ }
@@ -0,0 +1,7 @@
1
+ type Props = {
2
+ columns: string[];
3
+ rows?: number;
4
+ showActions?: boolean;
5
+ };
6
+ export declare function TableSkeleton({ columns, rows, showActions, }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ export function TableSkeleton({ columns, rows = 10, showActions = true, }) {
4
+ return (_jsx("div", { className: "overflow-auto border border-default-200 rounded-lg", children: _jsxs("table", { className: "w-full border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { children: [columns.map((c) => (_jsx("th", { className: "text-left text-xs font-medium uppercase tracking-wide text-foreground/60 px-3 py-2 border-b border-default-200", children: c }, c))), showActions && (_jsx("th", { className: "w-[120px] border-b border-default-200" }))] }) }), _jsx("tbody", { children: Array.from({ length: rows }).map((_, i) => (_jsxs("tr", { children: [columns.map((c, idx) => (_jsx("td", { className: "px-3 py-3 border-b border-default-100", children: _jsx("div", { className: "h-3 w-full max-w-[200px] rounded-md bg-foreground/10 animate-pulse" }) }, `${i}-${c}`))), showActions && (_jsx("td", { className: "px-3 py-3 border-b border-default-100", children: _jsx("div", { className: "h-8 w-16 rounded-md bg-foreground/10 animate-pulse" }) }))] }, i))) })] }) }));
5
+ }
@@ -0,0 +1,5 @@
1
+ /** Priority:
2
+ * 1) Redux: s.nextMin.system.googleMapsKey
3
+ * 2) process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY
4
+ */
5
+ export declare function useGoogleMapsKey(): string | undefined;
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { useMemo } from 'react';
3
+ import { useSelector } from 'react-redux';
4
+ const pick = (s) => (s && s.trim() ? s.trim() : undefined);
5
+ /** Priority:
6
+ * 1) Redux: s.nextMin.system.googleMapsKey
7
+ * 2) process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY
8
+ */
9
+ export function useGoogleMapsKey() {
10
+ const system = useSelector((s) => s?.nextMin?.system);
11
+ return useMemo(() => {
12
+ const fromSystem = pick(system?.googleMapsKey);
13
+ const fromEnv = pick(process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY);
14
+ return fromSystem ?? fromEnv;
15
+ }, [system]);
16
+ }
@@ -0,0 +1,2 @@
1
+ export * from './providers/NextMinProvider';
2
+ export * from './components/AdminApp';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './providers/NextMinProvider';
2
+ export * from './components/AdminApp';
@@ -0,0 +1,31 @@
1
+ import { ApiItemResponse, ApiListResponse } from './types';
2
+ export declare class ApiError extends Error {
3
+ status: number;
4
+ info?: unknown;
5
+ constructor(message: string, status: number, info?: unknown);
6
+ }
7
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
8
+ type FetchOpts = {
9
+ method?: HttpMethod;
10
+ body?: unknown;
11
+ token?: string | null;
12
+ json?: boolean;
13
+ auth?: boolean;
14
+ };
15
+ declare function request<T>(path: string, opts?: FetchOpts): Promise<T>;
16
+ export type ListParams = {
17
+ q?: string;
18
+ searchKey?: string;
19
+ dateFrom?: string;
20
+ dateTo?: string;
21
+ dateKey?: string;
22
+ };
23
+ export declare const api: {
24
+ getSchemas: () => Promise<unknown>;
25
+ list: <T>(modelName: string, page?: number, limit?: number, params?: ListParams) => Promise<ApiListResponse<T>>;
26
+ get: <T>(modelName: string, id: string) => Promise<ApiItemResponse<T>>;
27
+ create: <T>(modelName: string, data: unknown) => Promise<ApiItemResponse<T>>;
28
+ update: <T>(modelName: string, id: string, data: unknown) => Promise<ApiItemResponse<T>>;
29
+ remove: <T>(modelName: string, id: string) => Promise<ApiItemResponse<T>>;
30
+ };
31
+ export { request };
@@ -0,0 +1,94 @@
1
+ const RAW_API = process.env.NEXT_PUBLIC_NEXTMIN_API_URL ?? 'http://localhost:8081/rest';
2
+ const API_KEY = process.env.NEXT_PUBLIC_NEXTMIN_API_KEY ?? '';
3
+ function normalizeBase(base) {
4
+ return base.replace(/\/+$/, '');
5
+ }
6
+ const API_BASE = normalizeBase(RAW_API);
7
+ export class ApiError extends Error {
8
+ constructor(message, status, info) {
9
+ super(message);
10
+ this.name = 'ApiError';
11
+ this.status = status;
12
+ this.info = info;
13
+ }
14
+ }
15
+ function tokenFromStorage() {
16
+ try {
17
+ if (typeof window === 'undefined')
18
+ return null;
19
+ return localStorage.getItem('nextmin.token');
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ function modelPath(modelName) {
26
+ return modelName.toLowerCase() === 'user' ? '/auth/user' : `/${modelName}`;
27
+ }
28
+ async function request(path, opts = {}) {
29
+ const { method = 'GET', body, token, json = true, auth = true } = opts;
30
+ const headers = { 'x-api-key': API_KEY };
31
+ if (json)
32
+ headers['Content-Type'] = 'application/json';
33
+ const bearer = token ?? tokenFromStorage();
34
+ if (auth && bearer)
35
+ headers.Authorization = `Bearer ${bearer}`;
36
+ const res = await fetch(`${API_BASE}${path}`, {
37
+ method,
38
+ headers,
39
+ body: body && json ? JSON.stringify(body) : body,
40
+ });
41
+ const raw = await res.text();
42
+ let payload = {};
43
+ try {
44
+ payload = raw ? JSON.parse(raw) : {};
45
+ }
46
+ catch {
47
+ payload = raw || {};
48
+ }
49
+ if (!res.ok) {
50
+ const status = res.status;
51
+ const serverMsg = payload?.message ??
52
+ payload?.error ??
53
+ res.statusText;
54
+ // Normalize common auth/permission errors for the UI
55
+ if (status === 401)
56
+ throw new ApiError('Not authenticated', 401, payload);
57
+ if (status === 403)
58
+ throw new ApiError('Not permitted', 403, payload);
59
+ throw new ApiError(typeof serverMsg === 'string' && serverMsg.trim()
60
+ ? serverMsg
61
+ : 'API error', status, payload);
62
+ }
63
+ return payload;
64
+ }
65
+ function qs(params) {
66
+ const sp = new URLSearchParams();
67
+ for (const [k, v] of Object.entries(params)) {
68
+ if (v === undefined || v === null)
69
+ continue;
70
+ const s = String(v);
71
+ if (s.trim() === '')
72
+ continue;
73
+ sp.append(k, s);
74
+ }
75
+ const q = sp.toString();
76
+ return q ? `?${q}` : '';
77
+ }
78
+ export const api = {
79
+ getSchemas: () => request('/_schemas'),
80
+ list: (modelName, page = 0, limit = 10, params = {}) => request(`${modelPath(modelName)}${qs({ page, limit, ...params })}`),
81
+ get: (modelName, id) => request(`${modelPath(modelName)}/${id}`),
82
+ create: (modelName, data) => request(modelPath(modelName), {
83
+ method: 'POST',
84
+ body: data,
85
+ }),
86
+ update: (modelName, id, data) => request(`${modelPath(modelName)}/${id}`, {
87
+ method: 'PUT',
88
+ body: data,
89
+ }),
90
+ remove: (modelName, id) => request(`${modelPath(modelName)}/${id}`, {
91
+ method: 'DELETE',
92
+ }),
93
+ };
94
+ export { request };
@@ -0,0 +1,23 @@
1
+ export type LoginPayload = {
2
+ email: string;
3
+ password: string;
4
+ };
5
+ export type RegisterPayload = {
6
+ name: string;
7
+ email: string;
8
+ password: string;
9
+ role?: string;
10
+ };
11
+ export declare function login({ email, password }: LoginPayload): Promise<{
12
+ token: string;
13
+ user: any;
14
+ }>;
15
+ export declare function register(payload: RegisterPayload, token?: string): Promise<{
16
+ success?: boolean;
17
+ message?: string;
18
+ data?: any;
19
+ }>;
20
+ export declare function requestPasswordReset(email: string): Promise<{
21
+ success?: boolean;
22
+ message?: string;
23
+ }>;
@@ -0,0 +1,51 @@
1
+ import { request } from './api';
2
+ function normalizeRole(value) {
3
+ try {
4
+ if (!value)
5
+ return null;
6
+ if (typeof value === 'string')
7
+ return value.toLowerCase();
8
+ if (Array.isArray(value)) {
9
+ for (const v of value) {
10
+ const n = typeof v === 'string' ? v : (v && typeof v === 'object' && typeof v.name === 'string' ? v.name : null);
11
+ if (n)
12
+ return String(n).toLowerCase();
13
+ }
14
+ return null;
15
+ }
16
+ if (typeof value === 'object' && typeof value.name === 'string') {
17
+ return String(value.name).toLowerCase();
18
+ }
19
+ }
20
+ catch { }
21
+ return null;
22
+ }
23
+ export async function login({ email, password }) {
24
+ const payload = { email: String(email || '').trim().toLowerCase(), password };
25
+ const res = await request('/auth/users/login', { method: 'POST', body: payload, auth: false });
26
+ const data = res?.data ?? res;
27
+ const token = data?.token;
28
+ const user = data?.user;
29
+ if (!token || !user) {
30
+ throw new Error(res?.message || 'Login failed');
31
+ }
32
+ // Enforce admin-only access here (superadmin/admin)
33
+ const role = normalizeRole(user?.role);
34
+ if (role !== 'admin' && role !== 'superadmin') {
35
+ throw new Error('You are not authorized to access the admin panel');
36
+ }
37
+ return { token, user };
38
+ }
39
+ export async function register(payload, token) {
40
+ // Not used in Admin, but keep for API parity. Use request() client.
41
+ const res = await request('/auth/users/register', { method: 'POST', body: payload, auth: false });
42
+ if (res?.error)
43
+ throw new Error(res?.message || 'Registration failed');
44
+ return res;
45
+ }
46
+ export async function requestPasswordReset(email) {
47
+ const res = await request('/auth/users/forgot-password', { method: 'POST', body: { email }, auth: false });
48
+ if (res?.error)
49
+ throw new Error(res?.message || 'Reset request failed');
50
+ return res;
51
+ }
@@ -0,0 +1 @@
1
+ export declare function loadGoogleMaps(apiKey?: string): Promise<typeof google>;
@@ -0,0 +1,25 @@
1
+ import { Loader } from '@googlemaps/js-api-loader';
2
+ let mapsPromise = null;
3
+ let cachedKey = null;
4
+ export function loadGoogleMaps(apiKey) {
5
+ const key = (apiKey ??
6
+ process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY ??
7
+ '').trim();
8
+ if (!key)
9
+ return Promise.reject(new Error('GOOGLE_MAPS_KEY_MISSING'));
10
+ if (!mapsPromise || cachedKey !== key) {
11
+ cachedKey = key;
12
+ const loader = new Loader({
13
+ apiKey: key,
14
+ version: 'weekly',
15
+ language: 'en',
16
+ region: 'BD',
17
+ });
18
+ mapsPromise = (async () => {
19
+ (await loader.importLibrary('core'));
20
+ (await loader.importLibrary('places'));
21
+ return window.google;
22
+ })();
23
+ }
24
+ return mapsPromise;
25
+ }
@@ -0,0 +1,2 @@
1
+ import { type Socket } from 'socket.io-client';
2
+ export declare function getSchemaService(): Socket | null;
@@ -0,0 +1,39 @@
1
+ import { Manager, } from 'socket.io-client';
2
+ const SOCKET_PATH = '/__nextmin__/schema';
3
+ const NAMESPACE = '/schema';
4
+ let schemaSocket = null;
5
+ export function getSchemaService() {
6
+ if (typeof window === 'undefined')
7
+ return null; // SSR guard
8
+ const urlStr = process.env.NEXT_PUBLIC_NEXTMIN_API_URL || 'http://localhost:8081/rest';
9
+ let origin = 'http://localhost:8081';
10
+ try {
11
+ const u = new URL(urlStr);
12
+ origin = u.origin;
13
+ }
14
+ catch {
15
+ // keep default
16
+ }
17
+ // Keep a single Manager across HMR / route changes
18
+ const MGR_KEY = '__NM_SCHEMA_MGR__';
19
+ let mgr = window[MGR_KEY];
20
+ if (!mgr) {
21
+ const mgrOpts = {
22
+ path: SOCKET_PATH,
23
+ transports: ['websocket'], // avoid polling aborts on nav
24
+ reconnection: true,
25
+ reconnectionAttempts: Infinity,
26
+ reconnectionDelay: 500,
27
+ reconnectionDelayMax: 4000,
28
+ };
29
+ mgr = new Manager(origin, mgrOpts);
30
+ window[MGR_KEY] = mgr;
31
+ }
32
+ if (!schemaSocket) {
33
+ const sockOpts = {
34
+ auth: { apiKey: process.env.NEXT_PUBLIC_NEXTMIN_API_KEY || '' }, // <-- auth goes here
35
+ };
36
+ schemaSocket = mgr.socket(NAMESPACE, sockOpts);
37
+ }
38
+ return schemaSocket;
39
+ }
@@ -0,0 +1,4 @@
1
+ import { Attribute, ArrayAttribute } from './types';
2
+ export declare function isArrayAttr(attr: Attribute): attr is ArrayAttribute;
3
+ export declare function isRef(attr: Attribute): boolean;
4
+ export declare function inputTypeFor(attr: Attribute): 'text' | 'number' | 'checkbox' | 'textarea';
@@ -0,0 +1,18 @@
1
+ export function isArrayAttr(attr) {
2
+ return Array.isArray(attr);
3
+ }
4
+ export function isRef(attr) {
5
+ return isArrayAttr(attr) ? attr.some((a) => !!a.ref) : !!attr.ref;
6
+ }
7
+ export function inputTypeFor(attr) {
8
+ const a = isArrayAttr(attr) ? attr[0] : attr;
9
+ if (!a)
10
+ return 'text';
11
+ if (a.type === 'number')
12
+ return 'number';
13
+ if (a.type === 'boolean')
14
+ return 'checkbox';
15
+ if (a.longtext)
16
+ return 'textarea';
17
+ return 'text';
18
+ }
@@ -0,0 +1,50 @@
1
+ export type AttributeBase = {
2
+ type: string;
3
+ required?: boolean;
4
+ unique?: boolean;
5
+ private?: boolean;
6
+ ref?: string;
7
+ longtext?: boolean;
8
+ show?: string;
9
+ };
10
+ export type ArrayAttribute = AttributeBase[];
11
+ export type Attribute = AttributeBase | ArrayAttribute;
12
+ export type SchemaDef = {
13
+ modelName: string;
14
+ attributes: Record<string, Attribute>;
15
+ allowedMethods: {
16
+ create?: boolean;
17
+ read?: boolean;
18
+ update?: boolean;
19
+ delete?: boolean;
20
+ };
21
+ };
22
+ export type SchemasResponse = {
23
+ success: true;
24
+ data: SchemaDef[];
25
+ };
26
+ export type ApiListResponse<T> = {
27
+ success: boolean;
28
+ data: T[];
29
+ pagination?: {
30
+ totalRows: number;
31
+ page: number;
32
+ limit: number;
33
+ };
34
+ message?: string;
35
+ };
36
+ export type ApiItemResponse<T> = {
37
+ success: boolean;
38
+ data: T;
39
+ message?: string;
40
+ };
41
+ export type LatLng = {
42
+ lat: number;
43
+ lng: number;
44
+ };
45
+ export type AddressAttribute = Attribute & {
46
+ format?: 'address';
47
+ populate?: string;
48
+ countryCodes?: string[];
49
+ limit?: number;
50
+ };
@@ -0,0 +1 @@
1
+ export {};