@roboticela/devkit 3.0.0 → 4.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 (40) hide show
  1. package/README.md +12 -0
  2. package/dist/commands/add.js +103 -4
  3. package/dist/commands/eject.d.ts +1 -0
  4. package/dist/commands/eject.js +60 -0
  5. package/dist/index.js +10 -19
  6. package/dist/lib/bundled-registry.d.ts +18 -0
  7. package/dist/lib/bundled-registry.js +82 -0
  8. package/dist/lib/component-entry.d.ts +16 -0
  9. package/dist/lib/component-entry.js +1 -0
  10. package/dist/lib/config.d.ts +2 -0
  11. package/dist/lib/config.js +3 -1
  12. package/dist/lib/installer.js +9 -7
  13. package/dist/lib/registry.d.ts +2 -16
  14. package/dist/lib/registry.js +10 -0
  15. package/package.json +4 -2
  16. package/registry/components/auth/component.json +29 -0
  17. package/registry/components/auth/nextjs-compact/v1/files/app/api/auth/[...route]/route.ts +140 -0
  18. package/registry/components/auth/nextjs-compact/v1/files/app/auth/forgot-password/page.tsx +150 -0
  19. package/registry/components/auth/nextjs-compact/v1/files/app/auth/login/page.tsx +45 -0
  20. package/registry/components/auth/nextjs-compact/v1/files/app/auth/register/page.tsx +45 -0
  21. package/registry/components/auth/nextjs-compact/v1/files/app/auth/reset-password/page.tsx +139 -0
  22. package/registry/components/auth/nextjs-compact/v1/files/components/auth/AuthProvider.tsx +89 -0
  23. package/registry/components/auth/nextjs-compact/v1/files/components/auth/LoginForm.tsx +123 -0
  24. package/registry/components/auth/nextjs-compact/v1/files/components/auth/RegisterForm.tsx +106 -0
  25. package/registry/components/auth/nextjs-compact/v1/files/lib/auth/authService.ts +43 -0
  26. package/registry/components/auth/nextjs-compact/v1/manifest.json +37 -0
  27. package/registry/components/auth/vite-express-tauri/v1/files/server/middleware/requireAuth.ts +14 -0
  28. package/registry/components/auth/vite-express-tauri/v1/files/server/routes/auth.ts +83 -0
  29. package/registry/components/auth/vite-express-tauri/v1/files/server/services/jwtService.ts +19 -0
  30. package/registry/components/auth/vite-express-tauri/v1/files/src/contexts/AuthContext.tsx +72 -0
  31. package/registry/components/auth/vite-express-tauri/v1/manifest.json +47 -0
  32. package/registry/components/hero-section/component.json +28 -0
  33. package/registry/components/hero-section/nextjs-compact/v1/variants/centered/files/components/hero/HeroSection.tsx +97 -0
  34. package/registry/components/hero-section/nextjs-compact/v1/variants/centered/manifest.json +17 -0
  35. package/registry/components/hero-section/nextjs-compact/v1/variants/gradient-mesh/files/components/hero/HeroSection.tsx +146 -0
  36. package/registry/components/hero-section/nextjs-compact/v1/variants/gradient-mesh/manifest.json +17 -0
  37. package/registry/components/hero-section/nextjs-compact/v1/variants/split-image/files/components/hero/HeroSection.tsx +110 -0
  38. package/registry/components/hero-section/nextjs-compact/v1/variants/split-image/manifest.json +17 -0
  39. package/registry/components/registry.json +31 -0
  40. package/schemas/devkit-config.json +63 -0
@@ -0,0 +1,140 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { signToken, verifyToken } from "@/lib/auth/authService";
3
+
4
+ // ── Helpers ─────────────────────────────────────────────────────────────────
5
+
6
+ function json(data: unknown, status = 200) {
7
+ return NextResponse.json(data, { status });
8
+ }
9
+
10
+ function err(message: string, status = 400) {
11
+ return json({ message }, status);
12
+ }
13
+
14
+ // ── Route dispatcher ─────────────────────────────────────────────────────────
15
+
16
+ export async function POST(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) {
17
+ const { route } = await params;
18
+ const action = route.join("/");
19
+
20
+ if (action === "login") return handleLogin(req);
21
+ if (action === "register") return handleRegister(req);
22
+ if (action === "logout") return handleLogout();
23
+ if (action === "refresh") return handleRefresh(req);
24
+ if (action === "forgot-password") return handleForgotPassword(req);
25
+ if (action === "reset-password") return handleResetPassword(req);
26
+
27
+ return err("Not found", 404);
28
+ }
29
+
30
+ export async function GET(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) {
31
+ const { route } = await params;
32
+ const action = route.join("/");
33
+
34
+ if (action === "me") return handleMe(req);
35
+
36
+ return err("Not found", 404);
37
+ }
38
+
39
+ // ── Handlers ─────────────────────────────────────────────────────────────────
40
+
41
+ async function handleLogin(req: NextRequest) {
42
+ const { email, password } = await req.json();
43
+ if (!email || !password) return err("Email and password are required");
44
+
45
+ // Replace with your real user lookup + bcrypt.compare
46
+ const user = await findUserByCredentials(email, password);
47
+ if (!user) return err("Invalid email or password", 401);
48
+
49
+ const token = await signToken({ sub: user.id, email: user.email });
50
+ return json({ user, token });
51
+ }
52
+
53
+ async function handleRegister(req: NextRequest) {
54
+ const { name, email, password } = await req.json();
55
+ if (!name || !email || !password) return err("Name, email, and password are required");
56
+ if (password.length < 8) return err("Password must be at least 8 characters");
57
+
58
+ // Replace with your real user creation logic
59
+ const user = await createUser(name, email, password);
60
+ const token = await signToken({ sub: user.id, email: user.email });
61
+ return json({ user, token }, 201);
62
+ }
63
+
64
+ async function handleLogout() {
65
+ const res = json({ ok: true });
66
+ res.cookies.delete("refresh_token");
67
+ return res;
68
+ }
69
+
70
+ async function handleRefresh(req: NextRequest) {
71
+ const refreshToken = req.cookies.get("refresh_token")?.value;
72
+ if (!refreshToken) return err("No refresh token", 401);
73
+ try {
74
+ const payload = await verifyToken(refreshToken);
75
+ const accessToken = await signToken({ sub: payload.sub, email: payload.email });
76
+ return json({ token: accessToken });
77
+ } catch {
78
+ return err("Invalid refresh token", 401);
79
+ }
80
+ }
81
+
82
+ async function handleForgotPassword(req: NextRequest) {
83
+ const { email } = await req.json();
84
+ if (!email) return err("Email is required");
85
+ // Replace: look up user, generate reset token, send email
86
+ await sendPasswordResetEmail(email);
87
+ return json({ ok: true });
88
+ }
89
+
90
+ async function handleResetPassword(req: NextRequest) {
91
+ const { token, password } = await req.json();
92
+ if (!token || !password) return err("Token and password are required");
93
+ // Replace: verify token, update user password
94
+ await resetUserPassword(token, password);
95
+ return json({ ok: true });
96
+ }
97
+
98
+ async function handleMe(req: NextRequest) {
99
+ const authHeader = req.headers.get("authorization");
100
+ const token = authHeader?.replace("Bearer ", "");
101
+ if (!token) return err("Unauthorized", 401);
102
+ try {
103
+ const payload = await verifyToken(token);
104
+ const user = await findUserById(payload.sub);
105
+ if (!user) return err("User not found", 404);
106
+ return json({ user });
107
+ } catch {
108
+ return err("Invalid token", 401);
109
+ }
110
+ }
111
+
112
+ // ── Stubs (replace with real DB calls) ───────────────────────────────────────
113
+
114
+ async function findUserByCredentials(email: string, _password: string) {
115
+ // TODO: look up user in DB, compare password with bcrypt
116
+ void email;
117
+ return null as { id: string; name: string; email: string } | null;
118
+ }
119
+
120
+ async function createUser(name: string, email: string, _password: string) {
121
+ // TODO: hash password with bcrypt, insert into DB
122
+ void name; void email;
123
+ return { id: "stub", name, email };
124
+ }
125
+
126
+ async function findUserById(id: string) {
127
+ // TODO: look up user in DB by id
128
+ void id;
129
+ return null as { id: string; name: string; email: string } | null;
130
+ }
131
+
132
+ async function sendPasswordResetEmail(email: string) {
133
+ // TODO: generate token, store in DB, send via nodemailer
134
+ void email;
135
+ }
136
+
137
+ async function resetUserPassword(token: string, password: string) {
138
+ // TODO: verify token from DB, hash new password, update user
139
+ void token; void password;
140
+ }
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ export default function ForgotPasswordPage() {
6
+ const [email, setEmail] = useState("");
7
+ const [submitted, setSubmitted] = useState(false);
8
+ const [loading, setLoading] = useState(false);
9
+ const [error, setError] = useState("");
10
+
11
+ async function handleSubmit(e: React.FormEvent) {
12
+ e.preventDefault();
13
+ setLoading(true);
14
+ setError("");
15
+ try {
16
+ const res = await fetch("/api/auth/forgot-password", {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ body: JSON.stringify({ email }),
20
+ });
21
+ if (!res.ok) {
22
+ const d = await res.json();
23
+ throw new Error(d.message || "Failed to send reset email");
24
+ }
25
+ setSubmitted(true);
26
+ } catch (err) {
27
+ setError(err instanceof Error ? err.message : "Something went wrong");
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ }
32
+
33
+ return (
34
+ <main
35
+ style={{
36
+ minHeight: "100vh",
37
+ display: "flex",
38
+ alignItems: "center",
39
+ justifyContent: "center",
40
+ background: "var(--color-bg-subtle)",
41
+ padding: "var(--space-4)",
42
+ }}
43
+ >
44
+ <div
45
+ style={{
46
+ width: "100%",
47
+ maxWidth: "420px",
48
+ background: "var(--color-bg)",
49
+ borderRadius: "var(--radius-xl)",
50
+ border: "1px solid var(--color-border)",
51
+ padding: "var(--space-8)",
52
+ boxShadow: "var(--shadow-lg)",
53
+ }}
54
+ >
55
+ {submitted ? (
56
+ <div style={{ textAlign: "center" }}>
57
+ <div style={{ fontSize: "3rem", marginBottom: "var(--space-4)" }}>📬</div>
58
+ <h1 style={{ fontSize: "var(--text-xl)", fontWeight: "var(--weight-bold)", color: "var(--color-text)" }}>
59
+ Check your email
60
+ </h1>
61
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
62
+ We sent a password reset link to <strong>{email}</strong>
63
+ </p>
64
+ <a
65
+ href="/auth/login"
66
+ style={{
67
+ display: "inline-block",
68
+ marginTop: "var(--space-6)",
69
+ color: "var(--color-primary)",
70
+ fontSize: "var(--text-sm)",
71
+ }}
72
+ >
73
+ ← Back to sign in
74
+ </a>
75
+ </div>
76
+ ) : (
77
+ <>
78
+ <div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
79
+ <h1 style={{ fontSize: "var(--text-2xl)", fontWeight: "var(--weight-bold)", color: "var(--color-text)" }}>
80
+ Forgot password?
81
+ </h1>
82
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
83
+ Enter your email and we&apos;ll send you a reset link.
84
+ </p>
85
+ </div>
86
+
87
+ {error && (
88
+ <div
89
+ style={{
90
+ marginBottom: "var(--space-4)",
91
+ padding: "var(--space-3) var(--space-4)",
92
+ borderRadius: "var(--radius-md)",
93
+ background: "var(--color-error-subtle)",
94
+ color: "var(--color-error-text)",
95
+ fontSize: "var(--text-sm)",
96
+ }}
97
+ >
98
+ {error}
99
+ </div>
100
+ )}
101
+
102
+ <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)" }}>
103
+ <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-2)" }}>
104
+ <label htmlFor="email" style={{ fontSize: "var(--text-sm)", fontWeight: "var(--weight-medium)", color: "var(--color-text)" }}>
105
+ Email
106
+ </label>
107
+ <input
108
+ id="email"
109
+ type="email"
110
+ required
111
+ value={email}
112
+ onChange={(e) => setEmail(e.target.value)}
113
+ style={{
114
+ padding: "var(--space-3) var(--space-4)",
115
+ borderRadius: "var(--radius-md)",
116
+ border: "1px solid var(--color-border)",
117
+ background: "var(--color-bg)",
118
+ color: "var(--color-text)",
119
+ fontSize: "var(--text-base)",
120
+ }}
121
+ />
122
+ </div>
123
+ <button
124
+ type="submit"
125
+ disabled={loading}
126
+ style={{
127
+ padding: "var(--space-3) var(--space-4)",
128
+ borderRadius: "var(--radius-md)",
129
+ background: loading ? "var(--color-bg-muted)" : "var(--color-primary)",
130
+ color: "var(--color-primary-text)",
131
+ fontWeight: "var(--weight-semibold)",
132
+ border: "none",
133
+ cursor: loading ? "not-allowed" : "pointer",
134
+ }}
135
+ >
136
+ {loading ? "Sending…" : "Send reset link"}
137
+ </button>
138
+ <a
139
+ href="/auth/login"
140
+ style={{ textAlign: "center", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}
141
+ >
142
+ ← Back to sign in
143
+ </a>
144
+ </form>
145
+ </>
146
+ )}
147
+ </div>
148
+ </main>
149
+ );
150
+ }
@@ -0,0 +1,45 @@
1
+ import { LoginForm } from "@/components/auth/LoginForm";
2
+
3
+ export default function LoginPage() {
4
+ return (
5
+ <main
6
+ style={{
7
+ minHeight: "100vh",
8
+ display: "flex",
9
+ alignItems: "center",
10
+ justifyContent: "center",
11
+ background: "var(--color-bg-subtle)",
12
+ padding: "var(--space-4)",
13
+ }}
14
+ >
15
+ <div
16
+ style={{
17
+ width: "100%",
18
+ maxWidth: "420px",
19
+ background: "var(--color-bg)",
20
+ borderRadius: "var(--radius-xl)",
21
+ border: "1px solid var(--color-border)",
22
+ padding: "var(--space-8)",
23
+ boxShadow: "var(--shadow-lg)",
24
+ }}
25
+ >
26
+ <div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
27
+ <h1
28
+ style={{
29
+ fontSize: "var(--text-2xl)",
30
+ fontWeight: "var(--weight-bold)",
31
+ color: "var(--color-text)",
32
+ fontFamily: "var(--font-display)",
33
+ }}
34
+ >
35
+ Welcome back
36
+ </h1>
37
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
38
+ Sign in to your account
39
+ </p>
40
+ </div>
41
+ <LoginForm />
42
+ </div>
43
+ </main>
44
+ );
45
+ }
@@ -0,0 +1,45 @@
1
+ import { RegisterForm } from "@/components/auth/RegisterForm";
2
+
3
+ export default function RegisterPage() {
4
+ return (
5
+ <main
6
+ style={{
7
+ minHeight: "100vh",
8
+ display: "flex",
9
+ alignItems: "center",
10
+ justifyContent: "center",
11
+ background: "var(--color-bg-subtle)",
12
+ padding: "var(--space-4)",
13
+ }}
14
+ >
15
+ <div
16
+ style={{
17
+ width: "100%",
18
+ maxWidth: "420px",
19
+ background: "var(--color-bg)",
20
+ borderRadius: "var(--radius-xl)",
21
+ border: "1px solid var(--color-border)",
22
+ padding: "var(--space-8)",
23
+ boxShadow: "var(--shadow-lg)",
24
+ }}
25
+ >
26
+ <div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
27
+ <h1
28
+ style={{
29
+ fontSize: "var(--text-2xl)",
30
+ fontWeight: "var(--weight-bold)",
31
+ color: "var(--color-text)",
32
+ fontFamily: "var(--font-display)",
33
+ }}
34
+ >
35
+ Create an account
36
+ </h1>
37
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
38
+ Get started for free
39
+ </p>
40
+ </div>
41
+ <RegisterForm />
42
+ </div>
43
+ </main>
44
+ );
45
+ }
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import { useState, Suspense } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+
6
+ function ResetForm() {
7
+ const router = useRouter();
8
+ const params = useSearchParams();
9
+ const token = params.get("token") ?? "";
10
+ const [password, setPassword] = useState("");
11
+ const [confirm, setConfirm] = useState("");
12
+ const [loading, setLoading] = useState(false);
13
+ const [error, setError] = useState("");
14
+
15
+ async function handleSubmit(e: React.FormEvent) {
16
+ e.preventDefault();
17
+ if (password !== confirm) { setError("Passwords do not match"); return; }
18
+ setLoading(true);
19
+ setError("");
20
+ try {
21
+ const res = await fetch("/api/auth/reset-password", {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({ token, password }),
25
+ });
26
+ if (!res.ok) {
27
+ const d = await res.json();
28
+ throw new Error(d.message || "Failed to reset password");
29
+ }
30
+ router.push("/auth/login?reset=success");
31
+ } catch (err) {
32
+ setError(err instanceof Error ? err.message : "Something went wrong");
33
+ } finally {
34
+ setLoading(false);
35
+ }
36
+ }
37
+
38
+ if (!token) {
39
+ return (
40
+ <p style={{ color: "var(--color-error)", textAlign: "center" }}>
41
+ Invalid or missing reset token.
42
+ </p>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)" }}>
48
+ {error && (
49
+ <div
50
+ style={{
51
+ padding: "var(--space-3) var(--space-4)",
52
+ borderRadius: "var(--radius-md)",
53
+ background: "var(--color-error-subtle)",
54
+ color: "var(--color-error-text)",
55
+ fontSize: "var(--text-sm)",
56
+ }}
57
+ >
58
+ {error}
59
+ </div>
60
+ )}
61
+ {(["New password", "Confirm password"] as const).map((label) => (
62
+ <div key={label} style={{ display: "flex", flexDirection: "column", gap: "var(--space-2)" }}>
63
+ <label style={{ fontSize: "var(--text-sm)", fontWeight: "var(--weight-medium)", color: "var(--color-text)" }}>
64
+ {label}
65
+ </label>
66
+ <input
67
+ type="password"
68
+ required
69
+ minLength={8}
70
+ value={label === "New password" ? password : confirm}
71
+ onChange={(e) => label === "New password" ? setPassword(e.target.value) : setConfirm(e.target.value)}
72
+ style={{
73
+ padding: "var(--space-3) var(--space-4)",
74
+ borderRadius: "var(--radius-md)",
75
+ border: "1px solid var(--color-border)",
76
+ background: "var(--color-bg)",
77
+ color: "var(--color-text)",
78
+ fontSize: "var(--text-base)",
79
+ }}
80
+ />
81
+ </div>
82
+ ))}
83
+ <button
84
+ type="submit"
85
+ disabled={loading}
86
+ style={{
87
+ padding: "var(--space-3) var(--space-4)",
88
+ borderRadius: "var(--radius-md)",
89
+ background: loading ? "var(--color-bg-muted)" : "var(--color-primary)",
90
+ color: "var(--color-primary-text)",
91
+ fontWeight: "var(--weight-semibold)",
92
+ border: "none",
93
+ cursor: loading ? "not-allowed" : "pointer",
94
+ }}
95
+ >
96
+ {loading ? "Resetting…" : "Reset password"}
97
+ </button>
98
+ </form>
99
+ );
100
+ }
101
+
102
+ export default function ResetPasswordPage() {
103
+ return (
104
+ <main
105
+ style={{
106
+ minHeight: "100vh",
107
+ display: "flex",
108
+ alignItems: "center",
109
+ justifyContent: "center",
110
+ background: "var(--color-bg-subtle)",
111
+ padding: "var(--space-4)",
112
+ }}
113
+ >
114
+ <div
115
+ style={{
116
+ width: "100%",
117
+ maxWidth: "420px",
118
+ background: "var(--color-bg)",
119
+ borderRadius: "var(--radius-xl)",
120
+ border: "1px solid var(--color-border)",
121
+ padding: "var(--space-8)",
122
+ boxShadow: "var(--shadow-lg)",
123
+ }}
124
+ >
125
+ <div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
126
+ <h1 style={{ fontSize: "var(--text-2xl)", fontWeight: "var(--weight-bold)", color: "var(--color-text)" }}>
127
+ Reset password
128
+ </h1>
129
+ <p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
130
+ Choose a new password for your account.
131
+ </p>
132
+ </div>
133
+ <Suspense fallback={<p>Loading…</p>}>
134
+ <ResetForm />
135
+ </Suspense>
136
+ </div>
137
+ </main>
138
+ );
139
+ }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useEffect, useState } from "react";
4
+
5
+ interface User {
6
+ id: string;
7
+ email: string;
8
+ name: string;
9
+ avatar?: string;
10
+ }
11
+
12
+ interface AuthContextValue {
13
+ user: User | null;
14
+ loading: boolean;
15
+ login: (email: string, password: string) => Promise<void>;
16
+ register: (name: string, email: string, password: string) => Promise<void>;
17
+ logout: () => Promise<void>;
18
+ refresh: () => Promise<void>;
19
+ }
20
+
21
+ const AuthContext = createContext<AuthContextValue | null>(null);
22
+
23
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
24
+ const [user, setUser] = useState<User | null>(null);
25
+ const [loading, setLoading] = useState(true);
26
+
27
+ useEffect(() => {
28
+ refresh().finally(() => setLoading(false));
29
+ }, []);
30
+
31
+ async function refresh() {
32
+ try {
33
+ const res = await fetch("/api/auth/me");
34
+ if (res.ok) {
35
+ const data = await res.json();
36
+ setUser(data.user);
37
+ } else {
38
+ setUser(null);
39
+ }
40
+ } catch {
41
+ setUser(null);
42
+ }
43
+ }
44
+
45
+ async function login(email: string, password: string) {
46
+ const res = await fetch("/api/auth/login", {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify({ email, password }),
50
+ });
51
+ if (!res.ok) {
52
+ const err = await res.json();
53
+ throw new Error(err.message || "Login failed");
54
+ }
55
+ const data = await res.json();
56
+ setUser(data.user);
57
+ }
58
+
59
+ async function register(name: string, email: string, password: string) {
60
+ const res = await fetch("/api/auth/register", {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({ name, email, password }),
64
+ });
65
+ if (!res.ok) {
66
+ const err = await res.json();
67
+ throw new Error(err.message || "Registration failed");
68
+ }
69
+ const data = await res.json();
70
+ setUser(data.user);
71
+ }
72
+
73
+ async function logout() {
74
+ await fetch("/api/auth/logout", { method: "POST" });
75
+ setUser(null);
76
+ }
77
+
78
+ return (
79
+ <AuthContext.Provider value={{ user, loading, login, register, logout, refresh }}>
80
+ {children}
81
+ </AuthContext.Provider>
82
+ );
83
+ }
84
+
85
+ export function useAuth() {
86
+ const ctx = useContext(AuthContext);
87
+ if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
88
+ return ctx;
89
+ }