@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
package/LICENSE
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Nextmin Proprietary License v1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 GSCodes
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
1. Definitions
|
|
7
|
+
“Software” means the package and all accompanying files under the package name(s) @airoom/nextmin-react and @airoom/nextmin-node, including any updates provided by Licensor.
|
|
8
|
+
“Licensor” means GSCodes.
|
|
9
|
+
“Licensee” means the person or entity that obtains the Software.
|
|
10
|
+
|
|
11
|
+
2. Grant of License
|
|
12
|
+
Subject to full compliance with this License, Licensor grants Licensee a limited, non-exclusive, non-transferable, non-sublicensable license to:
|
|
13
|
+
a) install and use the Software internally, solely for evaluating or running Licensee’s own applications; and
|
|
14
|
+
b) make a reasonable number of copies solely for internal backup and archival purposes.
|
|
15
|
+
|
|
16
|
+
3. Restrictions
|
|
17
|
+
Except as expressly permitted in Section 2, Licensee must NOT:
|
|
18
|
+
a) copy, publish, distribute, sell, rent, lease, lend, sublicense, or otherwise make the Software available to any third party;
|
|
19
|
+
b) modify, translate, adapt, create derivative works of, or merge the Software with other software;
|
|
20
|
+
c) reverse engineer, decompile, disassemble, or otherwise attempt to derive source code or underlying ideas, except to the extent such restrictions are expressly prohibited by applicable law;
|
|
21
|
+
d) remove, obscure, or alter any proprietary notices or markings; or
|
|
22
|
+
e) use the Software to train, fine-tune, or improve any artificial intelligence or machine learning model.
|
|
23
|
+
|
|
24
|
+
4. Ownership
|
|
25
|
+
The Software is licensed, not sold. Licensor retains all right, title, and interest in and to the Software, including all intellectual property rights and all copies.
|
|
26
|
+
|
|
27
|
+
5. Confidentiality
|
|
28
|
+
The Software and any non-public information provided by Licensor shall be treated as confidential and not disclosed to any third party without Licensor’s prior written consent, except as required by law.
|
|
29
|
+
|
|
30
|
+
6. Term and Termination
|
|
31
|
+
This License is effective until terminated. Licensor may terminate this License immediately upon notice if Licensee breaches any term. Upon termination, Licensee must cease all use and destroy all copies of the Software. Sections 3–10 survive termination.
|
|
32
|
+
|
|
33
|
+
7. Updates; No Support Obligation
|
|
34
|
+
Licensor may, but is not obligated to, provide updates, bug fixes, or support. Any updates are governed by this License unless accompanied by a different license.
|
|
35
|
+
|
|
36
|
+
8. No Warranty
|
|
37
|
+
THE SOFTWARE IS PROVIDED “AS IS” AND “AS AVAILABLE,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. USE IS AT LICENSEE’S SOLE RISK.
|
|
38
|
+
|
|
39
|
+
9. Limitation of Liability
|
|
40
|
+
TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA, GOODWILL, OR BUSINESS INTERRUPTION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LICENSOR’S TOTAL LIABILITY FOR ALL CLAIMS SHALL NOT EXCEED THE AMOUNT PAID BY LICENSEE FOR THE SOFTWARE (IF ANY) IN THE TWELVE (12) MONTHS PRECEDING THE CLAIM.
|
|
41
|
+
|
|
42
|
+
10. Governing Law; Venue
|
|
43
|
+
This License is governed by the laws of State of California, USA, without regard to its conflict of laws principles. The parties submit to the exclusive jurisdiction and venue of the courts located in San Francisco County, California, USA.
|
|
44
|
+
|
|
45
|
+
11. Commercial Licensing
|
|
46
|
+
For commercial use, redistribution, or other rights not expressly granted, contact: tareqaziz0065@gmail.com.
|
|
47
|
+
|
|
48
|
+
12. Entire Agreement
|
|
49
|
+
This License constitutes the entire agreement with respect to the Software and supersedes all prior or contemporaneous agreements or understandings on the subject matter.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @airoom/nextmin-react
|
|
2
|
+
|
|
3
|
+
Plug‑and‑play React Admin Panel that connects to a `@airoom/nextmin-node` backend. Render an authenticated dashboard with lists, forms, file uploads, settings, and live updates — generated from your JSON schemas.
|
|
4
|
+
|
|
5
|
+
- “From JSON schema to REST API + Admin”
|
|
6
|
+
- 1 month → 1 hour
|
|
7
|
+
- Works out of the box with Next.js (App Router and Pages Router)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- `<NextMinProvider>`: initializes store, loads schemas, and opens a live socket
|
|
12
|
+
- `<AdminApp>`: full admin shell with auth, sidebar, dashboard, list/create/edit, profile, settings
|
|
13
|
+
- Built‑in router: `<NextMinRouter>` and `<AdminRouteNormalizer>` for admin routes
|
|
14
|
+
- Components: Sidebar, SchemaForm, FileUploader, reference selectors, phone/password inputs, table & filters
|
|
15
|
+
- Hooks/utils: Google address autocomplete, list data helpers, formatters
|
|
16
|
+
- Styling included; no extra CSS import needed
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# npm
|
|
22
|
+
npm install @airoom/nextmin-react
|
|
23
|
+
|
|
24
|
+
# yarn
|
|
25
|
+
yarn add @airoom/nextmin-react
|
|
26
|
+
|
|
27
|
+
# pnpm
|
|
28
|
+
pnpm add @airoom/nextmin-react
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configure environment
|
|
32
|
+
|
|
33
|
+
Create or update your frontend env file (e.g., `.env.local`) with the server base URL and client API key:
|
|
34
|
+
|
|
35
|
+
```env
|
|
36
|
+
NEXT_PUBLIC_NEXTMIN_API_URL=http://localhost:8081/rest
|
|
37
|
+
# IMPORTANT: Use the API key stored in your database → Settings.apiKey
|
|
38
|
+
NEXT_PUBLIC_NEXTMIN_API_KEY=your_api_key_here
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
|
|
43
|
+
- `NEXT_PUBLIC_NEXTMIN_API_URL` should point to your nextmin-node REST base (e.g., http://host:port/rest)
|
|
44
|
+
- `NEXT_PUBLIC_NEXTMIN_API_KEY` must match the value in your backend DB Settings document’s `apiKey` field
|
|
45
|
+
|
|
46
|
+
Where to find the API key
|
|
47
|
+
|
|
48
|
+
- Start your nextmin-node server once
|
|
49
|
+
- Open the database and locate the Settings collection/table
|
|
50
|
+
- Copy the first Settings document’s `apiKey` value and paste it here
|
|
51
|
+
|
|
52
|
+
## Next.js setup (App Router)
|
|
53
|
+
|
|
54
|
+
Wrap only the admin area with the provider and render the AdminApp. Mark these files as client components.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// app/admin/layout.tsx
|
|
58
|
+
'use client'
|
|
59
|
+
import { NextMinProvider } from '@airoom/nextmin-react';
|
|
60
|
+
|
|
61
|
+
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
62
|
+
return <NextMinProvider>{children}</NextMinProvider>;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// app/admin/page.tsx
|
|
68
|
+
'use client'
|
|
69
|
+
import { AdminApp } from '@airoom/nextmin-react';
|
|
70
|
+
export default function AdminIndex() {
|
|
71
|
+
return <AdminApp />;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// app/admin/[...slug]/page.tsx
|
|
77
|
+
'use client'
|
|
78
|
+
import { AdminApp } from '@airoom/nextmin-react';
|
|
79
|
+
export default function AdminCatchAll() {
|
|
80
|
+
return <AdminApp />;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Behavior
|
|
85
|
+
|
|
86
|
+
- Unauthenticated users are redirected to `/admin/auth/sign-in`
|
|
87
|
+
- Authenticated users visiting `/admin/auth/*` are redirected to `/admin/dashboard`
|
|
88
|
+
- Sidebar and pages are rendered according to the schemas loaded from your backend
|
|
89
|
+
|
|
90
|
+
Pages Router (alternative)
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// pages/admin.tsx
|
|
94
|
+
import dynamic from 'next/dynamic';
|
|
95
|
+
const Admin = dynamic(() => import('@airoom/nextmin-react').then(m => m.AdminApp), { ssr: false });
|
|
96
|
+
export default Admin;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Wrap with `<NextMinProvider>` at a top level where appropriate (e.g., `_app.tsx`) and ensure it runs on the client.
|
|
100
|
+
|
|
101
|
+
## Default admin credentials (after setup)
|
|
102
|
+
|
|
103
|
+
After completing the backend setup, sign in with the default super user and change the password immediately:
|
|
104
|
+
|
|
105
|
+
- Email: super@example.com
|
|
106
|
+
- Username: superadmin
|
|
107
|
+
- Password: supersecurepassword
|
|
108
|
+
|
|
109
|
+
## Usage notes
|
|
110
|
+
|
|
111
|
+
- No extra CSS import required; the package bundles its styles
|
|
112
|
+
- Backend auth/paths are configured via env vars (see above)
|
|
113
|
+
- Add custom links to routes like `/admin/<model>/create` or `/admin/<model>/<id>`
|
|
114
|
+
- The first Settings document (model name "Settings") controls site name, logo, and Google Maps key for address autocomplete
|
|
115
|
+
|
|
116
|
+
## TypeScript
|
|
117
|
+
|
|
118
|
+
Types are bundled with the package.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import type { ApiItemResponse } from '@airoom/nextmin-react/types';
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
- 401 Unauthorized: ensure `NEXT_PUBLIC_NEXTMIN_API_KEY` matches your DB `Settings.apiKey`
|
|
127
|
+
- Cannot load schemas: verify `NEXT_PUBLIC_NEXTMIN_API_URL` points to `/rest` and the backend is reachable
|
|
128
|
+
- Address autocomplete: set `NEXT_PUBLIC_GOOGLE_MAPS_KEY` in your environment and in the Settings document if applicable
|
|
129
|
+
- Blank page on Next.js: ensure admin files are client components (`'use client'`) and dynamic imports have `ssr: false` where necessary
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Licensed under the Nextmin Proprietary License. © 2025 GSCodes. For commercial licensing or extended rights, contact: tareqaziz0065@gmail.com.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function AuthPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
5
|
+
import { useSelector } from 'react-redux';
|
|
6
|
+
import { SignInForm } from './SignInForm';
|
|
7
|
+
import { ForgotPasswordForm } from './ForgotPasswordForm';
|
|
8
|
+
export function AuthPage() {
|
|
9
|
+
const pathname = usePathname();
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const token = useSelector((s) => s?.session?.token ?? null);
|
|
12
|
+
// If already authenticated on /admin/auth/* → go to dashboard
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (token && pathname?.startsWith('/admin/auth')) {
|
|
15
|
+
router.replace('/admin/dashboard');
|
|
16
|
+
}
|
|
17
|
+
}, [token, pathname, router]);
|
|
18
|
+
if (pathname?.startsWith('/admin/auth/forgot-password')) {
|
|
19
|
+
return _jsx(ForgotPasswordForm, {});
|
|
20
|
+
}
|
|
21
|
+
// Default = Sign in; SignInForm already stores token+user and updates Redux.
|
|
22
|
+
return _jsx(SignInForm, { onSuccess: () => router.replace('/admin/dashboard') });
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ForgotPasswordForm(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Card, CardHeader, CardBody, Input, Button, Link, Form, } from '@heroui/react';
|
|
5
|
+
import { requestPasswordReset } from '../lib/auth';
|
|
6
|
+
export function ForgotPasswordForm() {
|
|
7
|
+
const [loading, setLoading] = useState(false);
|
|
8
|
+
const [msg, setMsg] = useState(null);
|
|
9
|
+
const [err, setErr] = useState(null);
|
|
10
|
+
async function submit(formData) {
|
|
11
|
+
setErr(null);
|
|
12
|
+
setMsg(null);
|
|
13
|
+
setLoading(true);
|
|
14
|
+
try {
|
|
15
|
+
await requestPasswordReset(String(formData.get('email') || ''));
|
|
16
|
+
setMsg('If an account exists, reset instructions have been sent.');
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
setErr(e?.message || 'Request failed');
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
setLoading(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return (_jsx("div", { className: "min-h-[100dvh] grid place-items-center p-6 bg-default-50", children: _jsxs(Card, { className: "w-full max-w-md rounded-2xl shadow-lg border border-default-200", children: [_jsxs(CardHeader, { className: "flex flex-col items-center text-center gap-1 pt-6 pb-0", children: [_jsx("h1", { className: "text-3xl font-bold tracking-tight", children: "Reset password" }), _jsx("p", { className: "text-small text-foreground/60", children: "Enter your email to receive a reset link" })] }), _jsx(CardBody, { className: "gap-5 mt-4", children: _jsxs(Form, { className: "flex flex-col gap-4",
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
onSubmit: submit, validationBehavior: "native", children: [_jsx(Input, { variant: "bordered", name: "email", type: "email", label: "Email", labelPlacement: "outside", placeholder: "name@example.com", isRequired: true, autoComplete: "email" }), msg && _jsx("p", { className: "text-sm text-success", children: msg }), err && _jsx("p", { className: "text-sm text-danger", children: err }), _jsx(Button, { type: "submit", isLoading: loading, className: "w-full bg-[#0a35ff] text-white", children: "Send reset link" }), _jsx("p", { className: "text-sm text-center text-foreground/70", children: _jsx(Link, { href: "/admin/auth/sign-in", children: "Back to sign in" }) })] }) })] }) }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Card, CardHeader, CardBody, Input, Button, Link, Form, } from '@heroui/react';
|
|
5
|
+
import { useDispatch } from 'react-redux';
|
|
6
|
+
import { setSession } from '../state/sessionSlice';
|
|
7
|
+
import { login as loginApi } from '../lib/auth';
|
|
8
|
+
export function SignInForm({ onSuccess }) {
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
const [err, setErr] = useState(null);
|
|
11
|
+
const dispatch = useDispatch();
|
|
12
|
+
async function doLogin(email, password) {
|
|
13
|
+
return loginApi({ email, password });
|
|
14
|
+
}
|
|
15
|
+
return (_jsx("div", { className: "min-h-[100dvh] grid place-items-center p-6 bg-default-50", children: _jsxs(Card, { className: "w-full max-w-md rounded-2xl shadow-lg border border-default-200", children: [_jsxs(CardHeader, { className: "flex flex-col items-center text-center gap-1 pt-6 pb-0", children: [_jsx("h1", { className: "text-3xl font-bold tracking-tight", children: "Welcome back" }), _jsx("p", { className: "text-small text-foreground/60", children: "Sign in to your account" })] }), _jsx(CardBody, { className: "gap-5 mt-4", children: _jsxs(Form, { className: "flex flex-col gap-4", validationBehavior: "native", onSubmit: async (e) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setErr(null);
|
|
18
|
+
setLoading(true);
|
|
19
|
+
const form = new FormData(e.currentTarget);
|
|
20
|
+
const email = String(form.get('email') ?? '');
|
|
21
|
+
const password = String(form.get('password') ?? '');
|
|
22
|
+
try {
|
|
23
|
+
const { token, user } = await doLogin(email, password);
|
|
24
|
+
// persist + redux
|
|
25
|
+
localStorage.setItem('nextmin.token', token);
|
|
26
|
+
localStorage.setItem('nextmin.user', JSON.stringify(user));
|
|
27
|
+
dispatch(setSession({ token, user }));
|
|
28
|
+
// optional callback
|
|
29
|
+
onSuccess?.(token, user);
|
|
30
|
+
}
|
|
31
|
+
catch (er) {
|
|
32
|
+
setErr(er?.message || 'Invalid credentials');
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}, children: [_jsx(Input, { isRequired: true, variant: "bordered", label: "Email", labelPlacement: "outside", name: "email", placeholder: "Email", type: "email", autoComplete: "email" }), _jsx(Input, { isRequired: true, variant: "bordered", label: "Password", labelPlacement: "outside", name: "password", placeholder: "Password", type: "password", autoComplete: "current-password" }), err && _jsx("p", { className: "text-sm text-danger", children: err }), _jsx("div", { className: "flex items-center justify-start", children: _jsx(Link, { href: "/admin/auth/forgot-password", className: "text-sm", children: "Forgot Password?" }) }), _jsx(Button, { type: "submit", isLoading: loading, className: "w-full bg-[#0a35ff] text-white", children: "Continue" })] }) })] }) }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Card, CardHeader, CardBody, Input, Button, Checkbox, Link, Form, } from '@heroui/react';
|
|
5
|
+
import { register } from '../lib/auth';
|
|
6
|
+
export function SignUpForm({ onSuccess }) {
|
|
7
|
+
const [loading, setLoading] = useState(false);
|
|
8
|
+
const [err, setErr] = useState(null);
|
|
9
|
+
async function submit(formData) {
|
|
10
|
+
setErr(null);
|
|
11
|
+
setLoading(true);
|
|
12
|
+
try {
|
|
13
|
+
await register({
|
|
14
|
+
name: String(formData.get('name') || ''),
|
|
15
|
+
email: String(formData.get('email') || ''),
|
|
16
|
+
password: String(formData.get('password') || ''),
|
|
17
|
+
});
|
|
18
|
+
onSuccess?.();
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
setErr(e?.message || 'Registration failed');
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return (_jsx("div", { className: "min-h-[100dvh] grid place-items-center p-6", children: _jsxs(Card, { className: "w-full max-w-md", children: [_jsxs(CardHeader, { className: "pb-0", children: [_jsx("h1", { className: "text-2xl font-semibold", children: "Create account" }), _jsx("p", { className: "text-sm text-foreground/70", children: "Start your journey" })] }), _jsx(CardBody, { className: "gap-4 mt-4", children: _jsxs(Form, { className: "flex flex-col gap-4",
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
onSubmit: submit, validationBehavior: "native", children: [_jsx(Input, { variant: "bordered", name: "name", label: "Full Name", isRequired: true, autoComplete: "name" }), _jsx(Input, { variant: "bordered", name: "email", type: "email", label: "Email", isRequired: true, autoComplete: "email" }), _jsx(Input, { variant: "bordered", name: "password", type: "password", label: "Password", isRequired: true, autoComplete: "new-password" }), err && _jsx("p", { className: "text-sm text-danger", children: err }), _jsxs(Checkbox, { required: true, children: ["I agree to the", ' ', _jsx(Link, { href: "/legal/terms", target: "_blank", children: "Terms" }), ' ', "\u00A0and\u00A0", _jsx(Link, { href: "/legal/privacy", target: "_blank", children: "Privacy Policy" })] }), _jsx(Button, { type: "submit", color: "primary", isLoading: loading, children: "Create account" }), _jsxs("p", { className: "text-sm text-center text-foreground/70", children: ["Already have an account? ", _jsx(Link, { href: "/auth/sign-in", children: "Sign in" })] })] }) })] }) }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type LatLng = {
|
|
2
|
+
lat: number;
|
|
3
|
+
lng: number;
|
|
4
|
+
};
|
|
5
|
+
export type AddressAutocompleteGoogleProps = {
|
|
6
|
+
name: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
value: string;
|
|
10
|
+
onChange: (address: string, latlng?: LatLng) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
classNames?: {
|
|
15
|
+
inputWrapper?: string;
|
|
16
|
+
};
|
|
17
|
+
countryCodes?: string[];
|
|
18
|
+
limit?: number;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
};
|
|
21
|
+
export default function AddressAutocompleteGoogle({ name, label, description, value, onChange, disabled, required, className, classNames, countryCodes, limit, apiKey, }: AddressAutocompleteGoogleProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Autocomplete, AutocompleteItem } from '@heroui/react';
|
|
5
|
+
import { loadGoogleMaps } from '../lib/googleLoader';
|
|
6
|
+
function deriveStableKey(s) {
|
|
7
|
+
const pid = s?.placePrediction?.placeId ??
|
|
8
|
+
s?.placePrediction?.id ??
|
|
9
|
+
s?.place_id ??
|
|
10
|
+
s?.id ??
|
|
11
|
+
null;
|
|
12
|
+
const label = s?.placePrediction?.text?.toString?.();
|
|
13
|
+
return pid ? String(pid) : label ? String(label) : null;
|
|
14
|
+
}
|
|
15
|
+
export default function AddressAutocompleteGoogle({ name, label, description, value, onChange, disabled, required, className, classNames, countryCodes, limit = 8, apiKey, }) {
|
|
16
|
+
// Controlled input
|
|
17
|
+
const [q, setQ] = useState(value ?? '');
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if ((value ?? '') !== q)
|
|
20
|
+
setQ(value ?? '');
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
22
|
+
}, [value]);
|
|
23
|
+
const [items, setItems] = useState([]);
|
|
24
|
+
const [keyMissing, setKeyMissing] = useState(false);
|
|
25
|
+
// Google + guards
|
|
26
|
+
const tokenRef = useRef(null);
|
|
27
|
+
const suppressFetchRef = useRef(false);
|
|
28
|
+
const fetchGenRef = useRef(0);
|
|
29
|
+
// Clears
|
|
30
|
+
const manualClearRef = useRef(false);
|
|
31
|
+
const locationRestriction = useMemo(() => {
|
|
32
|
+
const cc = (countryCodes?.[0] ?? '').toLowerCase();
|
|
33
|
+
if (cc === 'bd' || !cc)
|
|
34
|
+
return { west: 88.008, south: 20.67, east: 92.68, north: 26.634 };
|
|
35
|
+
return undefined;
|
|
36
|
+
}, [countryCodes]);
|
|
37
|
+
// Load Google once (and on key change)
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const g = await loadGoogleMaps(apiKey);
|
|
42
|
+
tokenRef.current = new g.maps.places.AutocompleteSessionToken();
|
|
43
|
+
setKeyMissing(false);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
setKeyMissing(true);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
}, [apiKey]);
|
|
50
|
+
if (!apiKey || !apiKey.trim()) {
|
|
51
|
+
return (_jsxs("div", { className: "text-danger text-xs", children: ["Google Maps API key is missing. Set ", _jsx("b", { children: "system.googleMapsKey" }), " or define ", _jsx("b", { children: "NEXT_PUBLIC_GOOGLE_MAPS_KEY" }), "."] }));
|
|
52
|
+
}
|
|
53
|
+
if (keyMissing) {
|
|
54
|
+
return (_jsxs("div", { className: "text-danger text-xs", children: ["Google Maps API key is missing or invalid. Set", ' ', _jsx("b", { children: "system.googleMapsKey" }), " or ", _jsx("b", { children: "NEXT_PUBLIC_GOOGLE_MAPS_KEY" }), "."] }));
|
|
55
|
+
}
|
|
56
|
+
// Debounced suggestions fetch (Places API New)
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (suppressFetchRef.current)
|
|
59
|
+
return;
|
|
60
|
+
const trimmed = (q ?? '').trim();
|
|
61
|
+
if (!trimmed || trimmed.length < 3) {
|
|
62
|
+
setItems([]);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let cancelled = false;
|
|
66
|
+
const myGen = ++fetchGenRef.current;
|
|
67
|
+
const run = async () => {
|
|
68
|
+
try {
|
|
69
|
+
const g = await loadGoogleMaps(apiKey);
|
|
70
|
+
const req = {
|
|
71
|
+
input: trimmed,
|
|
72
|
+
sessionToken: tokenRef.current,
|
|
73
|
+
language: 'en',
|
|
74
|
+
region: (countryCodes?.[0] ?? 'bd').toLowerCase(),
|
|
75
|
+
};
|
|
76
|
+
if (locationRestriction)
|
|
77
|
+
req.locationRestriction = locationRestriction;
|
|
78
|
+
const { suggestions } = await g.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(req);
|
|
79
|
+
if (cancelled || myGen !== fetchGenRef.current)
|
|
80
|
+
return;
|
|
81
|
+
const list = (suggestions ??
|
|
82
|
+
[]);
|
|
83
|
+
const mapped = list
|
|
84
|
+
.slice(0, Math.max(1, limit))
|
|
85
|
+
.map((s) => {
|
|
86
|
+
const key = deriveStableKey(s);
|
|
87
|
+
const label = s.placePrediction?.text?.toString?.() ?? '';
|
|
88
|
+
return key && label ? { key, label, raw: s } : null;
|
|
89
|
+
})
|
|
90
|
+
.filter((r) => !!r);
|
|
91
|
+
setItems(mapped);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
setItems([]);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const t = setTimeout(run, 250);
|
|
98
|
+
return () => {
|
|
99
|
+
cancelled = true;
|
|
100
|
+
clearTimeout(t);
|
|
101
|
+
};
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
+
}, [q, apiKey, countryCodes, locationRestriction, limit]);
|
|
104
|
+
const getLatLng = (loc) => {
|
|
105
|
+
if (!loc)
|
|
106
|
+
return null;
|
|
107
|
+
if (typeof loc.lat === 'function' && typeof loc.lng === 'function')
|
|
108
|
+
return { lat: loc.lat(), lng: loc.lng() };
|
|
109
|
+
if (typeof loc.lat === 'number' && typeof loc.lng === 'number')
|
|
110
|
+
return { lat: loc.lat, lng: loc.lng };
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
// Resolve and commit
|
|
114
|
+
const resolveByKey = async (key) => {
|
|
115
|
+
const rec = items.find((r) => r.key === key);
|
|
116
|
+
if (!rec)
|
|
117
|
+
return;
|
|
118
|
+
suppressFetchRef.current = true;
|
|
119
|
+
// Show EXACT suggestion text immediately, and keep it
|
|
120
|
+
const label = rec.label;
|
|
121
|
+
setQ(label);
|
|
122
|
+
setItems([]);
|
|
123
|
+
try {
|
|
124
|
+
const g = await loadGoogleMaps(apiKey);
|
|
125
|
+
const pp = rec.raw.placePrediction;
|
|
126
|
+
if (!pp)
|
|
127
|
+
return;
|
|
128
|
+
const place = pp.toPlace();
|
|
129
|
+
await place.fetchFields({
|
|
130
|
+
fields: ['location'], // we only need lat/lng; do NOT fetch formattedAddress
|
|
131
|
+
sessionToken: tokenRef.current ?? undefined,
|
|
132
|
+
});
|
|
133
|
+
const ll = getLatLng(place.location);
|
|
134
|
+
// Always commit the SUGGESTION TEXT to the form; populate latLng when available
|
|
135
|
+
onChange(label, ll ?? undefined);
|
|
136
|
+
// fresh session
|
|
137
|
+
tokenRef.current = new g.maps.places.AutocompleteSessionToken();
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
suppressFetchRef.current = false;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
// Normalize HeroUI selection payload
|
|
144
|
+
function extractKey(sel) {
|
|
145
|
+
if (sel === null || sel === undefined || sel === 'all')
|
|
146
|
+
return null;
|
|
147
|
+
if (sel instanceof Set) {
|
|
148
|
+
const first = sel.values().next().value;
|
|
149
|
+
return first != null ? String(first) : null;
|
|
150
|
+
}
|
|
151
|
+
return String(sel);
|
|
152
|
+
}
|
|
153
|
+
return (_jsx("div", { className: className, children: _jsx(Autocomplete, { "aria-label": label ?? name, label: label, labelPlacement: "outside", placeholder: "Type address", name: name, isRequired: required, description: description, variant: "bordered", classNames: classNames, isDisabled: disabled, items: items, inputValue: q, isClearable: true, onClear: () => {
|
|
154
|
+
manualClearRef.current = true;
|
|
155
|
+
setQ('');
|
|
156
|
+
setItems([]);
|
|
157
|
+
onChange('', undefined);
|
|
158
|
+
}, onKeyDown: (e) => {
|
|
159
|
+
if ((e.key === 'Backspace' || e.key === 'Delete') && q.length <= 1) {
|
|
160
|
+
manualClearRef.current = true;
|
|
161
|
+
}
|
|
162
|
+
}, onInputChange: (text) => {
|
|
163
|
+
if (text == null)
|
|
164
|
+
return;
|
|
165
|
+
const trimmed = text.trim();
|
|
166
|
+
// Ignore blur-initiated clears unless it was a real manual clear
|
|
167
|
+
if (trimmed === '' && !manualClearRef.current)
|
|
168
|
+
return;
|
|
169
|
+
if (trimmed === '' && manualClearRef.current) {
|
|
170
|
+
manualClearRef.current = false;
|
|
171
|
+
setQ('');
|
|
172
|
+
setItems([]);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (text !== q)
|
|
176
|
+
setQ(text);
|
|
177
|
+
}, onSelectionChange: (sel) => {
|
|
178
|
+
const key = extractKey(sel);
|
|
179
|
+
if (key)
|
|
180
|
+
void resolveByKey(key);
|
|
181
|
+
}, children: (item) => (_jsx(AutocompleteItem, { textValue: item.label, children: item.label }, item.key)) }) }));
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function AdminApp(): import("react/jsx-runtime").JSX.Element;
|