@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.
Files changed (84) hide show
  1. package/README.md +30 -0
  2. package/index.js +327 -0
  3. package/package.json +29 -0
  4. package/templates/mern/backend/.env.example +11 -0
  5. package/templates/mern/backend/Dockerfile +13 -0
  6. package/templates/mern/backend/package.json +23 -0
  7. package/templates/mern/backend/server.js +25 -0
  8. package/templates/mern/backend/src/app.js +48 -0
  9. package/templates/mern/backend/src/config/db.js +17 -0
  10. package/templates/mern/backend/src/config/env.js +38 -0
  11. package/templates/mern/backend/src/middleware/authMiddleware.js +30 -0
  12. package/templates/mern/backend/src/middleware/errorMiddleware.js +17 -0
  13. package/templates/mern/backend/src/middleware/notFound.middleware.js +9 -0
  14. package/templates/mern/backend/src/modules/auth/auth.controller.js +45 -0
  15. package/templates/mern/backend/src/modules/auth/auth.model.js +20 -0
  16. package/templates/mern/backend/src/modules/auth/auth.routes.js +55 -0
  17. package/templates/mern/backend/src/modules/auth/auth.service.js +185 -0
  18. package/templates/mern/backend/src/modules/auth/dto/login.dto.js +12 -0
  19. package/templates/mern/backend/src/modules/auth/dto/register.dto.js +14 -0
  20. package/templates/mern/backend/src/modules/email/dto/sendEmail.dto.js +16 -0
  21. package/templates/mern/backend/src/modules/email/email.controller.js +33 -0
  22. package/templates/mern/backend/src/modules/email/email.routes.js +13 -0
  23. package/templates/mern/backend/src/modules/email/email.service.js +88 -0
  24. package/templates/mern/backend/src/modules/email/templates/resetPassword.html +47 -0
  25. package/templates/mern/backend/src/modules/email/templates/verifyEmail.html +45 -0
  26. package/templates/mern/backend/src/modules/email/templates/welcome.html +47 -0
  27. package/templates/mern/backend/src/utils/apiResponse.util.js +9 -0
  28. package/templates/mern/backend/src/utils/asyncHandler.util.js +6 -0
  29. package/templates/mern/backend/src/utils/generateOTP.util.js +10 -0
  30. package/templates/mern/backend/src/utils/generateResetToken.util.js +8 -0
  31. package/templates/mern/backend/src/utils/generateToken.util.js +17 -0
  32. package/templates/mern/backend/src/utils/hashPassword.util.js +11 -0
  33. package/templates/mern/backend/src/utils/validateDto.util.js +18 -0
  34. package/templates/mern/frontend/.env.example +1 -0
  35. package/templates/mern/frontend/Dockerfile +13 -0
  36. package/templates/mern/frontend/index.html +13 -0
  37. package/templates/mern/frontend/package.json +23 -0
  38. package/templates/mern/frontend/src/App.jsx +102 -0
  39. package/templates/mern/frontend/src/main.jsx +14 -0
  40. package/templates/mern/frontend/src/modules/auth/components/ProtectedRoute.jsx +10 -0
  41. package/templates/mern/frontend/src/modules/auth/index.js +6 -0
  42. package/templates/mern/frontend/src/modules/auth/pages/ForgotPasswordPage.jsx +64 -0
  43. package/templates/mern/frontend/src/modules/auth/pages/LoginPage.jsx +82 -0
  44. package/templates/mern/frontend/src/modules/auth/pages/RegisterPage.jsx +81 -0
  45. package/templates/mern/frontend/src/modules/auth/pages/ResetPasswordPage.jsx +78 -0
  46. package/templates/mern/frontend/src/modules/auth/pages/VerifyEmailPage.jsx +69 -0
  47. package/templates/mern/frontend/src/modules/auth/services/auth.service.js +34 -0
  48. package/templates/mern/frontend/src/modules/auth/store/authStore.js +37 -0
  49. package/templates/mern/frontend/src/modules/dashboard/index.js +2 -0
  50. package/templates/mern/frontend/src/modules/dashboard/pages/DashboardPage.jsx +41 -0
  51. package/templates/mern/frontend/src/shared/components/Button.jsx +31 -0
  52. package/templates/mern/frontend/src/shared/components/Input.jsx +23 -0
  53. package/templates/mern/frontend/src/shared/components/Toast.jsx +52 -0
  54. package/templates/mern/frontend/src/shared/services/api.js +20 -0
  55. package/templates/mern/frontend/src/shared/utils/formatError.util.js +8 -0
  56. package/templates/mern/frontend/src/shared/utils/storage.util.js +25 -0
  57. package/templates/mern/frontend/vite.config.js +13 -0
  58. package/templates/mern/frontend-next/.env.example +1 -0
  59. package/templates/mern/frontend-next/app/forgot-password/page.js +8 -0
  60. package/templates/mern/frontend-next/app/layout.js +15 -0
  61. package/templates/mern/frontend-next/app/login/page.js +8 -0
  62. package/templates/mern/frontend-next/app/page.js +22 -0
  63. package/templates/mern/frontend-next/app/register/page.js +8 -0
  64. package/templates/mern/frontend-next/app/reset-password/page.js +8 -0
  65. package/templates/mern/frontend-next/app/verify-email/page.js +8 -0
  66. package/templates/mern/frontend-next/jsconfig.json +6 -0
  67. package/templates/mern/frontend-next/next.config.mjs +7 -0
  68. package/templates/mern/frontend-next/package.json +18 -0
  69. package/templates/mern/frontend-next/src/modules/auth/components/ProtectedRoute.jsx +19 -0
  70. package/templates/mern/frontend-next/src/modules/auth/pages/ForgotPasswordPage.jsx +66 -0
  71. package/templates/mern/frontend-next/src/modules/auth/pages/LoginPage.jsx +88 -0
  72. package/templates/mern/frontend-next/src/modules/auth/pages/RegisterPage.jsx +84 -0
  73. package/templates/mern/frontend-next/src/modules/auth/pages/ResetPasswordPage.jsx +76 -0
  74. package/templates/mern/frontend-next/src/modules/auth/pages/VerifyEmailPage.jsx +71 -0
  75. package/templates/mern/frontend-next/src/modules/auth/services/auth.service.js +29 -0
  76. package/templates/mern/frontend-next/src/modules/auth/store/authStore.js +37 -0
  77. package/templates/mern/frontend-next/src/modules/dashboard/pages/DashboardPage.jsx +46 -0
  78. package/templates/mern/frontend-next/src/shared/components/Button.jsx +31 -0
  79. package/templates/mern/frontend-next/src/shared/components/Input.jsx +24 -0
  80. package/templates/mern/frontend-next/src/shared/components/Toast.jsx +52 -0
  81. package/templates/mern/frontend-next/src/shared/services/api.js +25 -0
  82. package/templates/mern/frontend-next/src/shared/utils/formatError.util.js +8 -0
  83. package/templates/mern/frontend-next/src/shared/utils/storage.util.js +28 -0
  84. 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,8 @@
1
+ export const formatError = (err) => {
2
+ const msg =
3
+ err?.response?.data?.message ||
4
+ err?.message ||
5
+ "Something went wrong";
6
+ return String(msg);
7
+ };
8
+
@@ -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,13 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ proxy: {
9
+ "/api": "http://localhost:5000"
10
+ }
11
+ }
12
+ });
13
+
@@ -0,0 +1 @@
1
+ NEXT_PUBLIC_API_URL=http://localhost:5000
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ import { ForgotPasswordPage } from "../../src/modules/auth/pages/ForgotPasswordPage.jsx";
4
+
5
+ export default function Page() {
6
+ return <ForgotPasswordPage />;
7
+ }
8
+
@@ -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,8 @@
1
+ "use client";
2
+
3
+ import { LoginPage } from "../../src/modules/auth/pages/LoginPage.jsx";
4
+
5
+ export default function Page() {
6
+ return <LoginPage />;
7
+ }
8
+
@@ -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,8 @@
1
+ "use client";
2
+
3
+ import { RegisterPage } from "../../src/modules/auth/pages/RegisterPage.jsx";
4
+
5
+ export default function Page() {
6
+ return <RegisterPage />;
7
+ }
8
+
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ import { ResetPasswordPage } from "../../src/modules/auth/pages/ResetPasswordPage.jsx";
4
+
5
+ export default function Page() {
6
+ return <ResetPasswordPage />;
7
+ }
8
+
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ import { VerifyEmailPage } from "../../src/modules/auth/pages/VerifyEmailPage.jsx";
4
+
5
+ export default function Page() {
6
+ return <VerifyEmailPage />;
7
+ }
8
+
@@ -0,0 +1,6 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": "."
4
+ }
5
+ }
6
+
@@ -0,0 +1,7 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true
4
+ };
5
+
6
+ export default nextConfig;
7
+
@@ -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
+