@surajprasad/create-starterkit 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/README.md +30 -0
- package/index.js +327 -0
- package/package.json +29 -0
- package/templates/mern/backend/.env.example +11 -0
- package/templates/mern/backend/Dockerfile +13 -0
- package/templates/mern/backend/package.json +23 -0
- package/templates/mern/backend/server.js +25 -0
- package/templates/mern/backend/src/app.js +48 -0
- package/templates/mern/backend/src/config/db.js +17 -0
- package/templates/mern/backend/src/config/env.js +38 -0
- package/templates/mern/backend/src/middleware/authMiddleware.js +30 -0
- package/templates/mern/backend/src/middleware/errorMiddleware.js +17 -0
- package/templates/mern/backend/src/middleware/notFound.middleware.js +9 -0
- package/templates/mern/backend/src/modules/auth/auth.controller.js +45 -0
- package/templates/mern/backend/src/modules/auth/auth.model.js +20 -0
- package/templates/mern/backend/src/modules/auth/auth.routes.js +55 -0
- package/templates/mern/backend/src/modules/auth/auth.service.js +185 -0
- package/templates/mern/backend/src/modules/auth/dto/login.dto.js +12 -0
- package/templates/mern/backend/src/modules/auth/dto/register.dto.js +14 -0
- package/templates/mern/backend/src/modules/email/dto/sendEmail.dto.js +16 -0
- package/templates/mern/backend/src/modules/email/email.controller.js +33 -0
- package/templates/mern/backend/src/modules/email/email.routes.js +13 -0
- package/templates/mern/backend/src/modules/email/email.service.js +88 -0
- package/templates/mern/backend/src/modules/email/templates/resetPassword.html +47 -0
- package/templates/mern/backend/src/modules/email/templates/verifyEmail.html +45 -0
- package/templates/mern/backend/src/modules/email/templates/welcome.html +47 -0
- package/templates/mern/backend/src/utils/apiResponse.util.js +9 -0
- package/templates/mern/backend/src/utils/asyncHandler.util.js +6 -0
- package/templates/mern/backend/src/utils/generateOTP.util.js +10 -0
- package/templates/mern/backend/src/utils/generateResetToken.util.js +8 -0
- package/templates/mern/backend/src/utils/generateToken.util.js +17 -0
- package/templates/mern/backend/src/utils/hashPassword.util.js +11 -0
- package/templates/mern/backend/src/utils/validateDto.util.js +18 -0
- package/templates/mern/frontend/.env.example +1 -0
- package/templates/mern/frontend/Dockerfile +13 -0
- package/templates/mern/frontend/index.html +13 -0
- package/templates/mern/frontend/package.json +23 -0
- package/templates/mern/frontend/src/App.jsx +102 -0
- package/templates/mern/frontend/src/main.jsx +14 -0
- package/templates/mern/frontend/src/modules/auth/components/ProtectedRoute.jsx +10 -0
- package/templates/mern/frontend/src/modules/auth/index.js +6 -0
- package/templates/mern/frontend/src/modules/auth/pages/ForgotPasswordPage.jsx +64 -0
- package/templates/mern/frontend/src/modules/auth/pages/LoginPage.jsx +82 -0
- package/templates/mern/frontend/src/modules/auth/pages/RegisterPage.jsx +81 -0
- package/templates/mern/frontend/src/modules/auth/pages/ResetPasswordPage.jsx +78 -0
- package/templates/mern/frontend/src/modules/auth/pages/VerifyEmailPage.jsx +69 -0
- package/templates/mern/frontend/src/modules/auth/services/auth.service.js +34 -0
- package/templates/mern/frontend/src/modules/auth/store/authStore.js +37 -0
- package/templates/mern/frontend/src/modules/dashboard/index.js +2 -0
- package/templates/mern/frontend/src/modules/dashboard/pages/DashboardPage.jsx +41 -0
- package/templates/mern/frontend/src/shared/components/Button.jsx +31 -0
- package/templates/mern/frontend/src/shared/components/Input.jsx +23 -0
- package/templates/mern/frontend/src/shared/components/Toast.jsx +52 -0
- package/templates/mern/frontend/src/shared/services/api.js +20 -0
- package/templates/mern/frontend/src/shared/utils/formatError.util.js +8 -0
- package/templates/mern/frontend/src/shared/utils/storage.util.js +25 -0
- package/templates/mern/frontend/vite.config.js +13 -0
- package/templates/mern/frontend-next/.env.example +1 -0
- package/templates/mern/frontend-next/app/forgot-password/page.js +8 -0
- package/templates/mern/frontend-next/app/layout.js +15 -0
- package/templates/mern/frontend-next/app/login/page.js +8 -0
- package/templates/mern/frontend-next/app/page.js +22 -0
- package/templates/mern/frontend-next/app/register/page.js +8 -0
- package/templates/mern/frontend-next/app/reset-password/page.js +8 -0
- package/templates/mern/frontend-next/app/verify-email/page.js +8 -0
- package/templates/mern/frontend-next/jsconfig.json +6 -0
- package/templates/mern/frontend-next/next.config.mjs +7 -0
- package/templates/mern/frontend-next/package.json +18 -0
- package/templates/mern/frontend-next/src/modules/auth/components/ProtectedRoute.jsx +19 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/ForgotPasswordPage.jsx +66 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/LoginPage.jsx +88 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/RegisterPage.jsx +84 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/ResetPasswordPage.jsx +76 -0
- package/templates/mern/frontend-next/src/modules/auth/pages/VerifyEmailPage.jsx +71 -0
- package/templates/mern/frontend-next/src/modules/auth/services/auth.service.js +29 -0
- package/templates/mern/frontend-next/src/modules/auth/store/authStore.js +37 -0
- package/templates/mern/frontend-next/src/modules/dashboard/pages/DashboardPage.jsx +46 -0
- package/templates/mern/frontend-next/src/shared/components/Button.jsx +31 -0
- package/templates/mern/frontend-next/src/shared/components/Input.jsx +24 -0
- package/templates/mern/frontend-next/src/shared/components/Toast.jsx +52 -0
- package/templates/mern/frontend-next/src/shared/services/api.js +25 -0
- package/templates/mern/frontend-next/src/shared/utils/formatError.util.js +8 -0
- package/templates/mern/frontend-next/src/shared/utils/storage.util.js +28 -0
- package/templates/mern/package.json +6 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
2
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { useAuthStore } from "../../auth/store/authStore.js";
|
|
6
|
+
|
|
7
|
+
export const DashboardPage = () => {
|
|
8
|
+
const user = useAuthStore((s) => s.user);
|
|
9
|
+
const clearAuth = useAuthStore((s) => s.clearAuth);
|
|
10
|
+
const [toast, setToast] = useState(null);
|
|
11
|
+
|
|
12
|
+
const onLogout = () => {
|
|
13
|
+
clearAuth();
|
|
14
|
+
setToast({ type: "success", message: "Logged out" });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div style={{ padding: 24, maxWidth: 860, margin: "0 auto" }}>
|
|
19
|
+
<div
|
|
20
|
+
style={{
|
|
21
|
+
background: "white",
|
|
22
|
+
border: "1px solid #E5E7EB",
|
|
23
|
+
borderRadius: 16,
|
|
24
|
+
padding: 18
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Dashboard</h1>
|
|
28
|
+
<p style={{ marginTop: 8, color: "#6B7280", fontSize: 13 }}>
|
|
29
|
+
You are logged in as <strong>{user?.email || "unknown"}</strong>
|
|
30
|
+
</p>
|
|
31
|
+
<div style={{ marginTop: 14 }}>
|
|
32
|
+
<Button onClick={onLogout} variant="secondary">
|
|
33
|
+
Logout
|
|
34
|
+
</Button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const Button = ({ children, variant = "primary", ...props }) => {
|
|
2
|
+
const styles =
|
|
3
|
+
variant === "primary"
|
|
4
|
+
? {
|
|
5
|
+
background: "#4F46E5",
|
|
6
|
+
color: "white",
|
|
7
|
+
border: "1px solid #4F46E5"
|
|
8
|
+
}
|
|
9
|
+
: {
|
|
10
|
+
background: "white",
|
|
11
|
+
color: "#111827",
|
|
12
|
+
border: "1px solid #E5E7EB"
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<button
|
|
17
|
+
{...props}
|
|
18
|
+
style={{
|
|
19
|
+
...styles,
|
|
20
|
+
padding: "10px 14px",
|
|
21
|
+
borderRadius: 10,
|
|
22
|
+
fontWeight: 700,
|
|
23
|
+
cursor: "pointer",
|
|
24
|
+
opacity: props.disabled ? 0.6 : 1
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</button>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const Input = ({ label, ...props }) => {
|
|
2
|
+
return (
|
|
3
|
+
<label style={{ display: "block" }}>
|
|
4
|
+
{label ? (
|
|
5
|
+
<div style={{ marginBottom: 6, fontSize: 12, color: "#374151", fontWeight: 700 }}>
|
|
6
|
+
{label}
|
|
7
|
+
</div>
|
|
8
|
+
) : null}
|
|
9
|
+
<input
|
|
10
|
+
{...props}
|
|
11
|
+
style={{
|
|
12
|
+
width: "100%",
|
|
13
|
+
padding: "10px 12px",
|
|
14
|
+
borderRadius: 10,
|
|
15
|
+
border: "1px solid #E5E7EB",
|
|
16
|
+
outline: "none",
|
|
17
|
+
fontSize: 14
|
|
18
|
+
}}
|
|
19
|
+
/>
|
|
20
|
+
</label>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export const Toast = ({ message, type = "info", onClose }) => {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (!message) return;
|
|
6
|
+
const t = setTimeout(() => onClose?.(), 3500);
|
|
7
|
+
return () => clearTimeout(t);
|
|
8
|
+
}, [message, onClose]);
|
|
9
|
+
|
|
10
|
+
if (!message) return null;
|
|
11
|
+
|
|
12
|
+
const bg =
|
|
13
|
+
type === "error" ? "#FEE2E2" : type === "success" ? "#DCFCE7" : "#E0E7FF";
|
|
14
|
+
const border =
|
|
15
|
+
type === "error" ? "#FCA5A5" : type === "success" ? "#86EFAC" : "#A5B4FC";
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
style={{
|
|
20
|
+
position: "fixed",
|
|
21
|
+
bottom: 16,
|
|
22
|
+
right: 16,
|
|
23
|
+
maxWidth: 360,
|
|
24
|
+
background: bg,
|
|
25
|
+
border: `1px solid ${border}`,
|
|
26
|
+
padding: "10px 12px",
|
|
27
|
+
borderRadius: 12,
|
|
28
|
+
color: "#111827",
|
|
29
|
+
boxShadow: "0 10px 20px rgba(0,0,0,0.08)"
|
|
30
|
+
}}
|
|
31
|
+
role="status"
|
|
32
|
+
>
|
|
33
|
+
<div style={{ display: "flex", gap: 10, alignItems: "start" }}>
|
|
34
|
+
<div style={{ flex: 1, fontSize: 13, lineHeight: 1.4 }}>{message}</div>
|
|
35
|
+
<button
|
|
36
|
+
onClick={onClose}
|
|
37
|
+
style={{
|
|
38
|
+
background: "transparent",
|
|
39
|
+
border: "none",
|
|
40
|
+
cursor: "pointer",
|
|
41
|
+
fontWeight: 900,
|
|
42
|
+
color: "#111827"
|
|
43
|
+
}}
|
|
44
|
+
aria-label="Close"
|
|
45
|
+
>
|
|
46
|
+
×
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
import { storage } from "../utils/storage.util.js";
|
|
4
|
+
|
|
5
|
+
const baseURL = import.meta.env.VITE_API_URL || "http://localhost:5000";
|
|
6
|
+
|
|
7
|
+
export const api = axios.create({
|
|
8
|
+
baseURL,
|
|
9
|
+
withCredentials: false
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
api.interceptors.request.use((config) => {
|
|
13
|
+
const token = storage.get("token");
|
|
14
|
+
if (token) {
|
|
15
|
+
config.headers = config.headers || {};
|
|
16
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
17
|
+
}
|
|
18
|
+
return config;
|
|
19
|
+
});
|
|
20
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const storage = {
|
|
2
|
+
get(key) {
|
|
3
|
+
try {
|
|
4
|
+
const raw = localStorage.getItem(key);
|
|
5
|
+
return raw ? JSON.parse(raw) : null;
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
set(key, value) {
|
|
11
|
+
try {
|
|
12
|
+
localStorage.setItem(key, JSON.stringify(value));
|
|
13
|
+
} catch {
|
|
14
|
+
// ignore
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
remove(key) {
|
|
18
|
+
try {
|
|
19
|
+
localStorage.removeItem(key);
|
|
20
|
+
} catch {
|
|
21
|
+
// ignore
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NEXT_PUBLIC_API_URL=http://localhost:5000
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const metadata = {
|
|
2
|
+
title: "StarterKit",
|
|
3
|
+
description: "Production-ready StarterKit"
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export default function RootLayout({ children }) {
|
|
7
|
+
return (
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<body style={{ margin: 0, background: "#F9FAFB", fontFamily: "Arial, Helvetica, sans-serif" }}>
|
|
10
|
+
{children}
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
import { DashboardPage } from "../src/modules/dashboard/pages/DashboardPage.jsx";
|
|
6
|
+
import { useAuthStore } from "../src/modules/auth/store/authStore.js";
|
|
7
|
+
import { ProtectedRoute } from "../src/modules/auth/components/ProtectedRoute.jsx";
|
|
8
|
+
|
|
9
|
+
export default function HomePage() {
|
|
10
|
+
const refreshMe = useAuthStore((s) => s.refreshMe);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
refreshMe().catch(() => {});
|
|
14
|
+
}, [refreshMe]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<ProtectedRoute>
|
|
18
|
+
<DashboardPage />
|
|
19
|
+
</ProtectedRoute>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starterkit-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"next": "^15.2.2",
|
|
13
|
+
"react": "^19.0.0",
|
|
14
|
+
"react-dom": "^19.0.0",
|
|
15
|
+
"zustand": "^5.0.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
import { useAuthStore } from "../store/authStore.js";
|
|
7
|
+
|
|
8
|
+
export const ProtectedRoute = ({ children }) => {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const token = useAuthStore((s) => s.token);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!token) router.replace("/login");
|
|
14
|
+
}, [token, router]);
|
|
15
|
+
|
|
16
|
+
if (!token) return null;
|
|
17
|
+
return children;
|
|
18
|
+
};
|
|
19
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
|
|
6
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
7
|
+
import { Input } from "../../../shared/components/Input.jsx";
|
|
8
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
9
|
+
import { formatError } from "../../../shared/utils/formatError.util.js";
|
|
10
|
+
import { forgotPasswordApi } from "../services/auth.service.js";
|
|
11
|
+
|
|
12
|
+
export const ForgotPasswordPage = () => {
|
|
13
|
+
const [email, setEmail] = useState("");
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const [toast, setToast] = useState(null);
|
|
16
|
+
|
|
17
|
+
const onSubmit = async (e) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
setLoading(true);
|
|
20
|
+
try {
|
|
21
|
+
await forgotPasswordApi({ email });
|
|
22
|
+
setToast({ type: "success", message: "If the email exists, a reset link was sent." });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
setToast({ type: "error", message: formatError(err) });
|
|
25
|
+
} finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
32
|
+
<form
|
|
33
|
+
onSubmit={onSubmit}
|
|
34
|
+
style={{
|
|
35
|
+
width: "100%",
|
|
36
|
+
maxWidth: 420,
|
|
37
|
+
background: "white",
|
|
38
|
+
border: "1px solid #E5E7EB",
|
|
39
|
+
borderRadius: 16,
|
|
40
|
+
padding: 20
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Forgot password</h1>
|
|
44
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
45
|
+
We will email you a reset link.
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
49
|
+
<Input label="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
50
|
+
<Button disabled={loading} type="submit">
|
|
51
|
+
{loading ? "Please wait..." : "Send reset link"}
|
|
52
|
+
</Button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div style={{ marginTop: 14 }}>
|
|
56
|
+
<Link href="/login" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
57
|
+
Back to login
|
|
58
|
+
</Link>
|
|
59
|
+
</div>
|
|
60
|
+
</form>
|
|
61
|
+
|
|
62
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
8
|
+
import { Input } from "../../../shared/components/Input.jsx";
|
|
9
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
10
|
+
import { formatError } from "../../../shared/utils/formatError.util.js";
|
|
11
|
+
import { loginApi } from "../services/auth.service.js";
|
|
12
|
+
import { useAuthStore } from "../store/authStore.js";
|
|
13
|
+
|
|
14
|
+
export const LoginPage = () => {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const setAuth = useAuthStore((s) => s.setAuth);
|
|
17
|
+
|
|
18
|
+
const [email, setEmail] = useState("");
|
|
19
|
+
const [password, setPassword] = useState("");
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const [toast, setToast] = useState(null);
|
|
22
|
+
|
|
23
|
+
const onSubmit = async (e) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
setLoading(true);
|
|
26
|
+
try {
|
|
27
|
+
const res = await loginApi({ email, password });
|
|
28
|
+
const token = res?.data?.token;
|
|
29
|
+
const user = res?.data?.user;
|
|
30
|
+
setAuth({ token, user });
|
|
31
|
+
setToast({ type: "success", message: "Logged in" });
|
|
32
|
+
router.replace("/");
|
|
33
|
+
} catch (err) {
|
|
34
|
+
setToast({ type: "error", message: formatError(err) });
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
42
|
+
<form
|
|
43
|
+
onSubmit={onSubmit}
|
|
44
|
+
style={{
|
|
45
|
+
width: "100%",
|
|
46
|
+
maxWidth: 420,
|
|
47
|
+
background: "white",
|
|
48
|
+
border: "1px solid #E5E7EB",
|
|
49
|
+
borderRadius: 16,
|
|
50
|
+
padding: 20
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Login</h1>
|
|
54
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
55
|
+
Sign in to your account.
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
59
|
+
<Input label="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
60
|
+
<Input
|
|
61
|
+
label="Password"
|
|
62
|
+
type="password"
|
|
63
|
+
value={password}
|
|
64
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
65
|
+
/>
|
|
66
|
+
<Button disabled={loading} type="submit">
|
|
67
|
+
{loading ? "Please wait..." : "Login"}
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 14 }}>
|
|
72
|
+
<Link href="/register" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
73
|
+
Create account
|
|
74
|
+
</Link>
|
|
75
|
+
<Link
|
|
76
|
+
href="/forgot-password"
|
|
77
|
+
style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}
|
|
78
|
+
>
|
|
79
|
+
Forgot password?
|
|
80
|
+
</Link>
|
|
81
|
+
</div>
|
|
82
|
+
</form>
|
|
83
|
+
|
|
84
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useRouter } from "next/navigation";
|
|
6
|
+
|
|
7
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
8
|
+
import { Input } from "../../../shared/components/Input.jsx";
|
|
9
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
10
|
+
import { formatError } from "../../../shared/utils/formatError.util.js";
|
|
11
|
+
import { registerApi } from "../services/auth.service.js";
|
|
12
|
+
import { useAuthStore } from "../store/authStore.js";
|
|
13
|
+
|
|
14
|
+
export const RegisterPage = () => {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const setAuth = useAuthStore((s) => s.setAuth);
|
|
17
|
+
|
|
18
|
+
const [name, setName] = useState("");
|
|
19
|
+
const [email, setEmail] = useState("");
|
|
20
|
+
const [password, setPassword] = useState("");
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [toast, setToast] = useState(null);
|
|
23
|
+
|
|
24
|
+
const onSubmit = async (e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
setLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
const res = await registerApi({ name, email, password });
|
|
29
|
+
const token = res?.data?.token;
|
|
30
|
+
const user = res?.data?.user;
|
|
31
|
+
setAuth({ token, user });
|
|
32
|
+
setToast({ type: "success", message: "Registered. Verify your email with OTP." });
|
|
33
|
+
router.replace("/verify-email");
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setToast({ type: "error", message: formatError(err) });
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
43
|
+
<form
|
|
44
|
+
onSubmit={onSubmit}
|
|
45
|
+
style={{
|
|
46
|
+
width: "100%",
|
|
47
|
+
maxWidth: 420,
|
|
48
|
+
background: "white",
|
|
49
|
+
border: "1px solid #E5E7EB",
|
|
50
|
+
borderRadius: 16,
|
|
51
|
+
padding: 20
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Create account</h1>
|
|
55
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
56
|
+
Get started with StarterKit.
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
60
|
+
<Input label="Name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
61
|
+
<Input label="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
62
|
+
<Input
|
|
63
|
+
label="Password"
|
|
64
|
+
type="password"
|
|
65
|
+
value={password}
|
|
66
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
67
|
+
/>
|
|
68
|
+
<Button disabled={loading} type="submit">
|
|
69
|
+
{loading ? "Please wait..." : "Register"}
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div style={{ marginTop: 14 }}>
|
|
74
|
+
<Link href="/login" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
75
|
+
Already have an account? Login
|
|
76
|
+
</Link>
|
|
77
|
+
</div>
|
|
78
|
+
</form>
|
|
79
|
+
|
|
80
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|