@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,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useSearchParams } 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 { resetPasswordApi } from "../services/auth.service.js";
|
|
12
|
+
|
|
13
|
+
export const ResetPasswordPage = () => {
|
|
14
|
+
const params = useSearchParams();
|
|
15
|
+
const token = useMemo(() => params.get("token") || "", [params]);
|
|
16
|
+
|
|
17
|
+
const [password, setPassword] = useState("");
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [toast, setToast] = useState(null);
|
|
20
|
+
|
|
21
|
+
const onSubmit = async (e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
await resetPasswordApi({ token, password });
|
|
26
|
+
setToast({ type: "success", message: "Password reset successfully. You can login now." });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
setToast({ type: "error", message: formatError(err) });
|
|
29
|
+
} finally {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
36
|
+
<form
|
|
37
|
+
onSubmit={onSubmit}
|
|
38
|
+
style={{
|
|
39
|
+
width: "100%",
|
|
40
|
+
maxWidth: 420,
|
|
41
|
+
background: "white",
|
|
42
|
+
border: "1px solid #E5E7EB",
|
|
43
|
+
borderRadius: 16,
|
|
44
|
+
padding: 20
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Reset password</h1>
|
|
48
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
49
|
+
Set a new password for your account.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
53
|
+
<Input label="Reset token" value={token} readOnly style={{ background: "#F9FAFB" }} />
|
|
54
|
+
<Input
|
|
55
|
+
label="New password"
|
|
56
|
+
type="password"
|
|
57
|
+
value={password}
|
|
58
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
59
|
+
/>
|
|
60
|
+
<Button disabled={loading || !token} type="submit">
|
|
61
|
+
{loading ? "Please wait..." : "Reset password"}
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div style={{ marginTop: 14 }}>
|
|
66
|
+
<Link href="/login" style={{ color: "#4F46E5", fontWeight: 700, fontSize: 13 }}>
|
|
67
|
+
Back to login
|
|
68
|
+
</Link>
|
|
69
|
+
</div>
|
|
70
|
+
</form>
|
|
71
|
+
|
|
72
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
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 { verifyEmailApi } from "../services/auth.service.js";
|
|
11
|
+
import { useAuthStore } from "../store/authStore.js";
|
|
12
|
+
|
|
13
|
+
export const VerifyEmailPage = () => {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const refreshMe = useAuthStore((s) => s.refreshMe);
|
|
16
|
+
|
|
17
|
+
const [otp, setOtp] = useState("");
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [toast, setToast] = useState(null);
|
|
20
|
+
|
|
21
|
+
const onSubmit = async (e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
await verifyEmailApi({ otp });
|
|
26
|
+
await refreshMe();
|
|
27
|
+
setToast({ type: "success", message: "Email verified" });
|
|
28
|
+
router.replace("/");
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setToast({ type: "error", message: formatError(err) });
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
|
|
38
|
+
<form
|
|
39
|
+
onSubmit={onSubmit}
|
|
40
|
+
style={{
|
|
41
|
+
width: "100%",
|
|
42
|
+
maxWidth: 420,
|
|
43
|
+
background: "white",
|
|
44
|
+
border: "1px solid #E5E7EB",
|
|
45
|
+
borderRadius: 16,
|
|
46
|
+
padding: 20
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Verify Email</h1>
|
|
50
|
+
<p style={{ marginTop: 6, marginBottom: 16, color: "#6B7280", fontSize: 13 }}>
|
|
51
|
+
Enter the 6-digit OTP sent to your email.
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<div style={{ display: "grid", gap: 12 }}>
|
|
55
|
+
<Input
|
|
56
|
+
label="OTP"
|
|
57
|
+
value={otp}
|
|
58
|
+
onChange={(e) => setOtp(e.target.value)}
|
|
59
|
+
placeholder="123456"
|
|
60
|
+
/>
|
|
61
|
+
<Button disabled={loading} type="submit">
|
|
62
|
+
{loading ? "Please wait..." : "Verify"}
|
|
63
|
+
</Button>
|
|
64
|
+
</div>
|
|
65
|
+
</form>
|
|
66
|
+
|
|
67
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { apiFetch } from "../../../shared/services/api.js";
|
|
2
|
+
|
|
3
|
+
export const registerApi = async ({ name, email, password }) => {
|
|
4
|
+
return apiFetch("/api/auth/register", { method: "POST", body: { name, email, password } });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const loginApi = async ({ email, password }) => {
|
|
8
|
+
return apiFetch("/api/auth/login", { method: "POST", body: { email, password } });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const meApi = async () => {
|
|
12
|
+
return apiFetch("/api/auth/me");
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const verifyEmailApi = async ({ otp }) => {
|
|
16
|
+
return apiFetch("/api/auth/verify-email", { method: "POST", body: { otp } });
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const forgotPasswordApi = async ({ email }) => {
|
|
20
|
+
return apiFetch("/api/auth/forgot-password", { method: "POST", body: { email } });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const resetPasswordApi = async ({ token, password }) => {
|
|
24
|
+
return apiFetch(`/api/auth/reset-password?token=${encodeURIComponent(token)}`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
body: { password }
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
import { storage } from "../../../shared/utils/storage.util.js";
|
|
4
|
+
import { meApi } from "../services/auth.service.js";
|
|
5
|
+
|
|
6
|
+
export const useAuthStore = create((set, get) => ({
|
|
7
|
+
user: storage.get("user"),
|
|
8
|
+
token: storage.get("token"),
|
|
9
|
+
loading: false,
|
|
10
|
+
|
|
11
|
+
setAuth({ user, token }) {
|
|
12
|
+
storage.set("user", user);
|
|
13
|
+
storage.set("token", token);
|
|
14
|
+
set({ user, token });
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
clearAuth() {
|
|
18
|
+
storage.remove("user");
|
|
19
|
+
storage.remove("token");
|
|
20
|
+
set({ user: null, token: null });
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async refreshMe() {
|
|
24
|
+
const token = get().token;
|
|
25
|
+
if (!token) return;
|
|
26
|
+
set({ loading: true });
|
|
27
|
+
try {
|
|
28
|
+
const res = await meApi();
|
|
29
|
+
const user = res?.data?.user || null;
|
|
30
|
+
storage.set("user", user);
|
|
31
|
+
set({ user });
|
|
32
|
+
} finally {
|
|
33
|
+
set({ loading: false });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
import { Button } from "../../../shared/components/Button.jsx";
|
|
7
|
+
import { Toast } from "../../../shared/components/Toast.jsx";
|
|
8
|
+
import { useAuthStore } from "../../auth/store/authStore.js";
|
|
9
|
+
|
|
10
|
+
export const DashboardPage = () => {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const user = useAuthStore((s) => s.user);
|
|
13
|
+
const clearAuth = useAuthStore((s) => s.clearAuth);
|
|
14
|
+
const [toast, setToast] = useState(null);
|
|
15
|
+
|
|
16
|
+
const onLogout = () => {
|
|
17
|
+
clearAuth();
|
|
18
|
+
setToast({ type: "success", message: "Logged out" });
|
|
19
|
+
router.replace("/login");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div style={{ padding: 24, maxWidth: 860, margin: "0 auto" }}>
|
|
24
|
+
<div
|
|
25
|
+
style={{
|
|
26
|
+
background: "white",
|
|
27
|
+
border: "1px solid #E5E7EB",
|
|
28
|
+
borderRadius: 16,
|
|
29
|
+
padding: 18
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<h1 style={{ margin: 0, fontSize: 22 }}>Dashboard</h1>
|
|
33
|
+
<p style={{ marginTop: 8, color: "#6B7280", fontSize: 13 }}>
|
|
34
|
+
You are logged in as <strong>{user?.email || "unknown"}</strong>
|
|
35
|
+
</p>
|
|
36
|
+
<div style={{ marginTop: 14 }}>
|
|
37
|
+
<Button onClick={onLogout} variant="secondary">
|
|
38
|
+
Logout
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<Toast message={toast?.message} type={toast?.type} onClose={() => setToast(null)} />
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
@@ -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,24 @@
|
|
|
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
|
+
...(props.style || {})
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
</label>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
@@ -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,25 @@
|
|
|
1
|
+
import { storage } from "../utils/storage.util.js";
|
|
2
|
+
|
|
3
|
+
const baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000";
|
|
4
|
+
|
|
5
|
+
export const apiFetch = async (path, { method = "GET", body, headers } = {}) => {
|
|
6
|
+
const token = storage.get("token");
|
|
7
|
+
const res = await fetch(`${baseURL}${path}`, {
|
|
8
|
+
method,
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
12
|
+
...(headers || {})
|
|
13
|
+
},
|
|
14
|
+
body: body ? JSON.stringify(body) : undefined
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const data = await res.json().catch(() => ({}));
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const err = new Error(data?.message || "Request failed");
|
|
20
|
+
err.response = { data };
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
};
|
|
25
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const storage = {
|
|
2
|
+
get(key) {
|
|
3
|
+
try {
|
|
4
|
+
if (typeof window === "undefined") return null;
|
|
5
|
+
const raw = window.localStorage.getItem(key);
|
|
6
|
+
return raw ? JSON.parse(raw) : null;
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
set(key, value) {
|
|
12
|
+
try {
|
|
13
|
+
if (typeof window === "undefined") return;
|
|
14
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
15
|
+
} catch {
|
|
16
|
+
// ignore
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
remove(key) {
|
|
20
|
+
try {
|
|
21
|
+
if (typeof window === "undefined") return;
|
|
22
|
+
window.localStorage.removeItem(key);
|
|
23
|
+
} catch {
|
|
24
|
+
// ignore
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|