create-next-structure 1.0.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 +7 -0
- package/README.md +51 -0
- package/bin/index.js +90 -0
- package/package.json +44 -0
- package/templates/.env.template +36 -0
- package/templates/app/(dashboard)/dashboard/page.jsx +40 -0
- package/templates/app/(dashboard)/layout.jsx +24 -0
- package/templates/app/(dashboard)/users/page.jsx +64 -0
- package/templates/app/globals.css +14 -0
- package/templates/app/layout.jsx +25 -0
- package/templates/app/login/page.jsx +69 -0
- package/templates/app/page.jsx +10 -0
- package/templates/components/auth/withAuth.jsx +50 -0
- package/templates/components/auth/withPublic.jsx +50 -0
- package/templates/components/layout/Header.jsx +29 -0
- package/templates/components/layout/Sidebar.jsx +35 -0
- package/templates/components/ui/Button.jsx +36 -0
- package/templates/components/ui/ErrorDisplay.jsx +19 -0
- package/templates/components/ui/Input.jsx +30 -0
- package/templates/components/ui/Loading.jsx +16 -0
- package/templates/components/ui/Modal.jsx +33 -0
- package/templates/components/ui/index.js +10 -0
- package/templates/contexts/AuthContext.jsx +112 -0
- package/templates/docs/README.md +128 -0
- package/templates/hooks/useAsync.js +38 -0
- package/templates/hooks/useAuth.js +93 -0
- package/templates/hooks/useForm.js +67 -0
- package/templates/jsconfig.json +27 -0
- package/templates/lib/api/apiClient.js +105 -0
- package/templates/lib/api/auth.api.js +36 -0
- package/templates/lib/api/user.api.js +43 -0
- package/templates/next.config.js +18 -0
- package/templates/package.json +23 -0
- package/templates/store/ReduxProvider.jsx +34 -0
- package/templates/store/api/apiSlice.js +108 -0
- package/templates/store/api/authApi.js +155 -0
- package/templates/store/api/exampleApi.js +108 -0
- package/templates/store/api/userApi.js +114 -0
- package/templates/store/slices/authSlice.js +86 -0
- package/templates/store/store.js +27 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Component
|
|
3
|
+
* Responsibility: Display error state with retry option
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Button } from "./Button";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error display component
|
|
10
|
+
*/
|
|
11
|
+
export const ErrorDisplay = ({ error, onRetry }) => {
|
|
12
|
+
return (
|
|
13
|
+
<div className="error-container">
|
|
14
|
+
<h3>Something went wrong</h3>
|
|
15
|
+
<p>{error || "An unexpected error occurred"}</p>
|
|
16
|
+
{onRetry && <Button onClick={onRetry}>Try Again</Button>}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Component
|
|
3
|
+
* Responsibility: Reusable form input with validation display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base input component
|
|
8
|
+
* @param {string} label - Input label
|
|
9
|
+
* @param {string} error - Error message
|
|
10
|
+
* @param {string} type - Input type
|
|
11
|
+
*/
|
|
12
|
+
export const Input = ({
|
|
13
|
+
label,
|
|
14
|
+
error,
|
|
15
|
+
type = "text",
|
|
16
|
+
className = "",
|
|
17
|
+
...props
|
|
18
|
+
}) => {
|
|
19
|
+
return (
|
|
20
|
+
<div className={`input-wrapper ${className}`}>
|
|
21
|
+
{label && <label className="input-label">{label}</label>}
|
|
22
|
+
<input
|
|
23
|
+
type={type}
|
|
24
|
+
className={`input ${error ? "input-error" : ""}`}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
{error && <span className="error-message">{error}</span>}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading Component
|
|
3
|
+
* Responsibility: Display loading state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Loading spinner component
|
|
8
|
+
*/
|
|
9
|
+
export const Loading = ({ message = "Loading..." }) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className="loading-container">
|
|
12
|
+
<div className="spinner"></div>
|
|
13
|
+
<p>{message}</p>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Component
|
|
3
|
+
* Responsibility: Reusable modal/dialog wrapper
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use client";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base modal component
|
|
10
|
+
* @param {boolean} isOpen - Modal visibility
|
|
11
|
+
* @param {function} onClose - Close handler
|
|
12
|
+
* @param {string} title - Modal title
|
|
13
|
+
*/
|
|
14
|
+
export const Modal = ({ isOpen, onClose, title, children, className = "" }) => {
|
|
15
|
+
if (!isOpen) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="modal-overlay" onClick={onClose}>
|
|
19
|
+
<div
|
|
20
|
+
className={`modal-content ${className}`}
|
|
21
|
+
onClick={(e) => e.stopPropagation()}
|
|
22
|
+
>
|
|
23
|
+
<div className="modal-header">
|
|
24
|
+
<h2>{title}</h2>
|
|
25
|
+
<button onClick={onClose} className="modal-close">
|
|
26
|
+
×
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="modal-body">{children}</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Components Export
|
|
3
|
+
* Responsibility: Centralized export for UI primitives
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { Button } from "./Button";
|
|
7
|
+
export { Input } from "./Input";
|
|
8
|
+
export { Modal } from "./Modal";
|
|
9
|
+
export { Loading } from "./Loading";
|
|
10
|
+
export { ErrorDisplay } from "./ErrorDisplay";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Context
|
|
3
|
+
* Responsibility: Manage authentication state and provide auth methods
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use client";
|
|
7
|
+
|
|
8
|
+
import { createContext, useContext, useState, useEffect } from "react";
|
|
9
|
+
import { useRouter } from "next/navigation";
|
|
10
|
+
import { authApi } from "@/lib/api/auth.api";
|
|
11
|
+
|
|
12
|
+
const AuthContext = createContext(null);
|
|
13
|
+
|
|
14
|
+
export const useAuth = () => {
|
|
15
|
+
const context = useContext(AuthContext);
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw new Error("useAuth must be used within AuthProvider");
|
|
18
|
+
}
|
|
19
|
+
return context;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const AuthProvider = ({ children }) => {
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
const [user, setUser] = useState(null);
|
|
25
|
+
const [loading, setLoading] = useState(true);
|
|
26
|
+
const [error, setError] = useState(null);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize auth state from token
|
|
30
|
+
*/
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const initAuth = async () => {
|
|
33
|
+
const token = localStorage.getItem("token");
|
|
34
|
+
if (token) {
|
|
35
|
+
try {
|
|
36
|
+
const response = await authApi.getCurrentUser();
|
|
37
|
+
setUser(response.data);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
localStorage.removeItem("token");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
setLoading(false);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
initAuth();
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Login user
|
|
50
|
+
*/
|
|
51
|
+
const login = async (credentials) => {
|
|
52
|
+
try {
|
|
53
|
+
setError(null);
|
|
54
|
+
const response = await authApi.login(credentials);
|
|
55
|
+
const { token, user: userData } = response.data;
|
|
56
|
+
|
|
57
|
+
localStorage.setItem("token", token);
|
|
58
|
+
setUser(userData);
|
|
59
|
+
|
|
60
|
+
return userData;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
setError(err.message);
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register user
|
|
69
|
+
*/
|
|
70
|
+
const register = async (userData) => {
|
|
71
|
+
try {
|
|
72
|
+
setError(null);
|
|
73
|
+
const response = await authApi.register(userData);
|
|
74
|
+
const { token, user: newUser } = response.data;
|
|
75
|
+
|
|
76
|
+
localStorage.setItem("token", token);
|
|
77
|
+
setUser(newUser);
|
|
78
|
+
|
|
79
|
+
return newUser;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setError(err.message);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Logout user
|
|
88
|
+
*/
|
|
89
|
+
const logout = async () => {
|
|
90
|
+
try {
|
|
91
|
+
await authApi.logout();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error("Logout error:", err);
|
|
94
|
+
} finally {
|
|
95
|
+
localStorage.removeItem("token");
|
|
96
|
+
setUser(null);
|
|
97
|
+
router.push("/login");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const value = {
|
|
102
|
+
user,
|
|
103
|
+
loading,
|
|
104
|
+
error,
|
|
105
|
+
login,
|
|
106
|
+
register,
|
|
107
|
+
logout,
|
|
108
|
+
isAuthenticated: !!user,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
112
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Template Guide (Docs)
|
|
2
|
+
|
|
3
|
+
This document explains how to use the **Next.js + Redux template** in the `templates/` folder. It focuses only on the template files and how to work with them.
|
|
4
|
+
|
|
5
|
+
## ✅ What this template includes
|
|
6
|
+
|
|
7
|
+
- Next.js App Router setup (`app/`)
|
|
8
|
+
- Redux Toolkit + RTK Query (`store/`)
|
|
9
|
+
- Auth helpers (`components/auth/` + `hooks/useAuth.js`)
|
|
10
|
+
- Layout shell (`components/layout/`)
|
|
11
|
+
- Reusable UI primitives (`components/ui/`)
|
|
12
|
+
- Docs for API integration, architecture, and examples (`docs/`)
|
|
13
|
+
|
|
14
|
+
## 🚀 How to use the template
|
|
15
|
+
|
|
16
|
+
### 1) Copy or scaffold the template
|
|
17
|
+
|
|
18
|
+
- Use this folder as your project base, or copy its contents into a fresh Next.js project.
|
|
19
|
+
- The template already has `package.json`, `next.config.js`, and `jsconfig.json`.
|
|
20
|
+
|
|
21
|
+
### 2) Install dependencies
|
|
22
|
+
|
|
23
|
+
Use the `package.json` that lives inside `templates/`.
|
|
24
|
+
|
|
25
|
+
### 3) Configure environment variables
|
|
26
|
+
|
|
27
|
+
1. Copy `.env.template` into `.env.local`.
|
|
28
|
+
2. Update the API base URL to your backend:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
NEXT_PUBLIC_API_URL=http://localhost:5000/api/v1
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 4) Start building pages
|
|
35
|
+
|
|
36
|
+
- Public pages go under `app/` (ex: `app/page.jsx`).
|
|
37
|
+
- Protected pages should live inside the `(dashboard)` route group and be wrapped by `withAuth`.
|
|
38
|
+
|
|
39
|
+
## 🔧 How it works (high level)
|
|
40
|
+
|
|
41
|
+
- **Redux Provider** is mounted in `app/layout.jsx` via `store/ReduxProvider.jsx`.
|
|
42
|
+
- **Auth** flows use RTK Query and `authSlice` to manage tokens.
|
|
43
|
+
- **API calls** are handled by `store/api/apiSlice.js` with automatic token refresh.
|
|
44
|
+
- **Protected routing** uses `components/auth/withAuth.jsx` and `components/auth/withPublic.jsx`.
|
|
45
|
+
|
|
46
|
+
For deeper guides, see:
|
|
47
|
+
- `docs/API_INTEGRATION.md`
|
|
48
|
+
- `docs/ARCHITECTURE.md`
|
|
49
|
+
- `docs/EXAMPLES.md`
|
|
50
|
+
- `docs/PROJECT_STRUCTURE.md`
|
|
51
|
+
|
|
52
|
+
## 📁 Template folder structure
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
templates/
|
|
56
|
+
├── app/
|
|
57
|
+
│ ├── layout.jsx
|
|
58
|
+
│ ├── page.jsx
|
|
59
|
+
│ ├── globals.css
|
|
60
|
+
│ ├── login/
|
|
61
|
+
│ │ └── page.jsx
|
|
62
|
+
│ └── (dashboard)/
|
|
63
|
+
│ ├── layout.jsx
|
|
64
|
+
│ ├── dashboard/
|
|
65
|
+
│ │ └── page.jsx
|
|
66
|
+
│ └── users/
|
|
67
|
+
│ └── page.jsx
|
|
68
|
+
├── components/
|
|
69
|
+
│ ├── auth/
|
|
70
|
+
│ │ ├── withAuth.jsx
|
|
71
|
+
│ │ └── withPublic.jsx
|
|
72
|
+
│ ├── layout/
|
|
73
|
+
│ │ ├── Header.jsx
|
|
74
|
+
│ │ └── Sidebar.jsx
|
|
75
|
+
│ └── ui/
|
|
76
|
+
│ ├── Button.jsx
|
|
77
|
+
│ ├── ErrorDisplay.jsx
|
|
78
|
+
│ ├── Input.jsx
|
|
79
|
+
│ ├── Loading.jsx
|
|
80
|
+
│ ├── Modal.jsx
|
|
81
|
+
│ └── index.js
|
|
82
|
+
├── contexts/
|
|
83
|
+
│ └── AuthContext.jsx
|
|
84
|
+
├── docs/
|
|
85
|
+
│ ├── API_INTEGRATION.md
|
|
86
|
+
│ ├── ARCHITECTURE.md
|
|
87
|
+
│ ├── EXAMPLES.md
|
|
88
|
+
│ ├── PROJECT_STRUCTURE.md
|
|
89
|
+
│ └── README.md
|
|
90
|
+
├── hooks/
|
|
91
|
+
│ ├── useAsync.js
|
|
92
|
+
│ ├── useAuth.js
|
|
93
|
+
│ └── useForm.js
|
|
94
|
+
├── lib/
|
|
95
|
+
│ └── api/
|
|
96
|
+
│ ├── apiClient.js
|
|
97
|
+
│ ├── auth.api.js
|
|
98
|
+
│ └── user.api.js
|
|
99
|
+
├── store/
|
|
100
|
+
│ ├── api/
|
|
101
|
+
│ │ ├── apiSlice.js
|
|
102
|
+
│ │ ├── authApi.js
|
|
103
|
+
│ │ ├── exampleApi.js
|
|
104
|
+
│ │ └── userApi.js
|
|
105
|
+
│ ├── slices/
|
|
106
|
+
│ │ └── authSlice.js
|
|
107
|
+
│ ├── ReduxProvider.jsx
|
|
108
|
+
│ └── store.js
|
|
109
|
+
├── .env.template
|
|
110
|
+
├── jsconfig.json
|
|
111
|
+
├── next.config.js
|
|
112
|
+
└── package.json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 🧩 Common customization points
|
|
116
|
+
|
|
117
|
+
- **API base URL** → `store/api/apiSlice.js`
|
|
118
|
+
- **Auth payload shape** → `store/api/authApi.js`
|
|
119
|
+
- **App layout UI** → `components/layout/`
|
|
120
|
+
- **Route protection** → `components/auth/withAuth.jsx`
|
|
121
|
+
- **New RTK Query resources** → copy `store/api/exampleApi.js`
|
|
122
|
+
|
|
123
|
+
## ✅ Quick sanity checklist
|
|
124
|
+
|
|
125
|
+
- `.env.local` exists and has `NEXT_PUBLIC_API_URL`
|
|
126
|
+
- `app/layout.jsx` wraps the app in `ReduxProvider`
|
|
127
|
+
- Protected pages use `withAuth`
|
|
128
|
+
- You reference APIs via RTK Query hooks (not manual fetch)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Hook: useAsync
|
|
3
|
+
* Responsibility: Handle async operations with loading and error states
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use client";
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback } from "react";
|
|
9
|
+
|
|
10
|
+
export const useAsync = () => {
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const [data, setData] = useState(null);
|
|
14
|
+
|
|
15
|
+
const execute = useCallback(async (asyncFunction) => {
|
|
16
|
+
setLoading(true);
|
|
17
|
+
setError(null);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const result = await asyncFunction();
|
|
21
|
+
setData(result);
|
|
22
|
+
return result;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
setError(err.message);
|
|
25
|
+
throw err;
|
|
26
|
+
} finally {
|
|
27
|
+
setLoading(false);
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const reset = useCallback(() => {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
setError(null);
|
|
34
|
+
setData(null);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
return { loading, error, data, execute, reset };
|
|
38
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Hook: useAuth
|
|
3
|
+
* Responsibility: Provide authentication state and methods
|
|
4
|
+
*
|
|
5
|
+
* USAGE GUIDE:
|
|
6
|
+
* - Import: import { useAuth } from '@/hooks/useAuth'
|
|
7
|
+
* - In component: const { user, isAuthenticated, login, logout, register } = useAuth()
|
|
8
|
+
*
|
|
9
|
+
* EXAMPLE:
|
|
10
|
+
* ```jsx
|
|
11
|
+
* const { user, isAuthenticated, login, logout, isLoading } = useAuth();
|
|
12
|
+
*
|
|
13
|
+
* const handleLogin = async (credentials) => {
|
|
14
|
+
* try {
|
|
15
|
+
* await login(credentials);
|
|
16
|
+
* // Success - automatically redirects
|
|
17
|
+
* } catch (error) {
|
|
18
|
+
* console.error('Login failed:', error);
|
|
19
|
+
* }
|
|
20
|
+
* };
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
"use client";
|
|
25
|
+
|
|
26
|
+
import { useSelector, useDispatch } from "react-redux";
|
|
27
|
+
import { useRouter } from "next/navigation";
|
|
28
|
+
import {
|
|
29
|
+
selectCurrentUser,
|
|
30
|
+
selectIsAuthenticated,
|
|
31
|
+
selectToken,
|
|
32
|
+
logout as logoutAction,
|
|
33
|
+
} from "@/store/slices/authSlice";
|
|
34
|
+
import {
|
|
35
|
+
useLoginMutation,
|
|
36
|
+
useRegisterMutation,
|
|
37
|
+
useLogoutMutation,
|
|
38
|
+
} from "@/store/api/authApi";
|
|
39
|
+
|
|
40
|
+
export const useAuth = () => {
|
|
41
|
+
const dispatch = useDispatch();
|
|
42
|
+
const router = useRouter();
|
|
43
|
+
|
|
44
|
+
const user = useSelector(selectCurrentUser);
|
|
45
|
+
const isAuthenticated = useSelector(selectIsAuthenticated);
|
|
46
|
+
const token = useSelector(selectToken);
|
|
47
|
+
|
|
48
|
+
const [loginMutation, { isLoading: isLoggingIn }] = useLoginMutation();
|
|
49
|
+
const [registerMutation, { isLoading: isRegistering }] =
|
|
50
|
+
useRegisterMutation();
|
|
51
|
+
const [logoutMutation, { isLoading: isLoggingOut }] = useLogoutMutation();
|
|
52
|
+
|
|
53
|
+
const login = async (credentials) => {
|
|
54
|
+
try {
|
|
55
|
+
const result = await loginMutation(credentials).unwrap();
|
|
56
|
+
router.push("/dashboard");
|
|
57
|
+
return result;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const register = async (userData) => {
|
|
64
|
+
try {
|
|
65
|
+
const result = await registerMutation(userData).unwrap();
|
|
66
|
+
router.push("/dashboard");
|
|
67
|
+
return result;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const logout = async () => {
|
|
74
|
+
try {
|
|
75
|
+
await logoutMutation().unwrap();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error("Logout error:", error);
|
|
78
|
+
} finally {
|
|
79
|
+
dispatch(logoutAction());
|
|
80
|
+
router.push("/login");
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
user,
|
|
86
|
+
isAuthenticated,
|
|
87
|
+
token,
|
|
88
|
+
login,
|
|
89
|
+
register,
|
|
90
|
+
logout,
|
|
91
|
+
isLoading: isLoggingIn || isRegistering || isLoggingOut,
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Hook: useForm
|
|
3
|
+
* Responsibility: Handle form state and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use client";
|
|
7
|
+
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
|
|
10
|
+
export const useForm = (initialValues = {}, validate) => {
|
|
11
|
+
const [values, setValues] = useState(initialValues);
|
|
12
|
+
const [errors, setErrors] = useState({});
|
|
13
|
+
const [touched, setTouched] = useState({});
|
|
14
|
+
|
|
15
|
+
const handleChange = (e) => {
|
|
16
|
+
const { name, value } = e.target;
|
|
17
|
+
setValues((prev) => ({ ...prev, [name]: value }));
|
|
18
|
+
|
|
19
|
+
// Clear error when user starts typing
|
|
20
|
+
if (errors[name]) {
|
|
21
|
+
setErrors((prev) => ({ ...prev, [name]: "" }));
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const handleBlur = (e) => {
|
|
26
|
+
const { name } = e.target;
|
|
27
|
+
setTouched((prev) => ({ ...prev, [name]: true }));
|
|
28
|
+
|
|
29
|
+
// Validate on blur if validation function provided
|
|
30
|
+
if (validate) {
|
|
31
|
+
const validationErrors = validate(values);
|
|
32
|
+
setErrors(validationErrors);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleSubmit = (onSubmit) => async (e) => {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
|
|
39
|
+
// Validate all fields
|
|
40
|
+
if (validate) {
|
|
41
|
+
const validationErrors = validate(values);
|
|
42
|
+
setErrors(validationErrors);
|
|
43
|
+
|
|
44
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await onSubmit(values);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const reset = () => {
|
|
53
|
+
setValues(initialValues);
|
|
54
|
+
setErrors({});
|
|
55
|
+
setTouched({});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
values,
|
|
60
|
+
errors,
|
|
61
|
+
touched,
|
|
62
|
+
handleChange,
|
|
63
|
+
handleBlur,
|
|
64
|
+
handleSubmit,
|
|
65
|
+
reset,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client Configuration
|
|
3
|
+
* Responsibility: Centralized HTTP client with interceptors for Next.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const API_BASE_URL =
|
|
7
|
+
process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1";
|
|
8
|
+
|
|
9
|
+
class ApiClient {
|
|
10
|
+
constructor(baseURL = API_BASE_URL) {
|
|
11
|
+
this.baseURL = baseURL;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get auth token from storage (client-side only)
|
|
16
|
+
*/
|
|
17
|
+
getToken() {
|
|
18
|
+
if (typeof window !== "undefined") {
|
|
19
|
+
return localStorage.getItem("token");
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build headers with auth token
|
|
26
|
+
*/
|
|
27
|
+
getHeaders() {
|
|
28
|
+
const headers = {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const token = this.getToken();
|
|
33
|
+
if (token) {
|
|
34
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generic request handler
|
|
42
|
+
*/
|
|
43
|
+
async request(endpoint, options = {}) {
|
|
44
|
+
const url = `${this.baseURL}${endpoint}`;
|
|
45
|
+
const config = {
|
|
46
|
+
...options,
|
|
47
|
+
headers: {
|
|
48
|
+
...this.getHeaders(),
|
|
49
|
+
...options.headers,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(url, config);
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new Error(data.message || "Something went wrong");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return data;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("API Error:", error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* GET request
|
|
70
|
+
*/
|
|
71
|
+
async get(endpoint, params = {}) {
|
|
72
|
+
const queryString = new URLSearchParams(params).toString();
|
|
73
|
+
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
|
74
|
+
return this.request(url, { method: "GET" });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* POST request
|
|
79
|
+
*/
|
|
80
|
+
async post(endpoint, body) {
|
|
81
|
+
return this.request(endpoint, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify(body),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* PUT request
|
|
89
|
+
*/
|
|
90
|
+
async put(endpoint, body) {
|
|
91
|
+
return this.request(endpoint, {
|
|
92
|
+
method: "PUT",
|
|
93
|
+
body: JSON.stringify(body),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* DELETE request
|
|
99
|
+
*/
|
|
100
|
+
async delete(endpoint) {
|
|
101
|
+
return this.request(endpoint, { method: "DELETE" });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default new ApiClient();
|