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,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth API Service
|
|
3
|
+
* Responsibility: Authentication-related API calls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import apiClient from "./apiClient";
|
|
7
|
+
|
|
8
|
+
export const authApi = {
|
|
9
|
+
/**
|
|
10
|
+
* Login user
|
|
11
|
+
*/
|
|
12
|
+
login: async (credentials) => {
|
|
13
|
+
return apiClient.post("/auth/login", credentials);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register user
|
|
18
|
+
*/
|
|
19
|
+
register: async (userData) => {
|
|
20
|
+
return apiClient.post("/auth/register", userData);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get current user
|
|
25
|
+
*/
|
|
26
|
+
getCurrentUser: async () => {
|
|
27
|
+
return apiClient.get("/auth/me");
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Logout user
|
|
32
|
+
*/
|
|
33
|
+
logout: async () => {
|
|
34
|
+
return apiClient.post("/auth/logout");
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User API Service
|
|
3
|
+
* Responsibility: User-related API calls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import apiClient from "./apiClient";
|
|
7
|
+
|
|
8
|
+
export const userApi = {
|
|
9
|
+
/**
|
|
10
|
+
* Get all users with pagination
|
|
11
|
+
*/
|
|
12
|
+
getUsers: async (params = {}) => {
|
|
13
|
+
return apiClient.get("/users", params);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get user by ID
|
|
18
|
+
*/
|
|
19
|
+
getUserById: async (id) => {
|
|
20
|
+
return apiClient.get(`/users/${id}`);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create user
|
|
25
|
+
*/
|
|
26
|
+
createUser: async (userData) => {
|
|
27
|
+
return apiClient.post("/users", userData);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Update user
|
|
32
|
+
*/
|
|
33
|
+
updateUser: async (id, userData) => {
|
|
34
|
+
return apiClient.put(`/users/${id}`, userData);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Delete user
|
|
39
|
+
*/
|
|
40
|
+
deleteUser: async (id) => {
|
|
41
|
+
return apiClient.delete(`/users/${id}`);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
const nextConfig = {
|
|
3
|
+
reactStrictMode: true,
|
|
4
|
+
// API proxy configuration (optional - only if you want to proxy API calls)
|
|
5
|
+
async rewrites() {
|
|
6
|
+
const apiUrl =
|
|
7
|
+
process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1";
|
|
8
|
+
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
source: "/api/:path*",
|
|
12
|
+
destination: `${apiUrl}/:path*`,
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = nextConfig;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-template",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Production-ready Next.js App Router frontend template with Redux & RTK Query",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"lint": "next lint"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"next": "^14.0.0",
|
|
14
|
+
"react": "^18.2.0",
|
|
15
|
+
"react-dom": "^18.2.0",
|
|
16
|
+
"@reduxjs/toolkit": "^2.0.1",
|
|
17
|
+
"react-redux": "^9.0.4"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"eslint": "^8.54.0",
|
|
21
|
+
"eslint-config-next": "^14.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redux Store Provider
|
|
3
|
+
* Responsibility: Wrap the app with Redux store
|
|
4
|
+
*
|
|
5
|
+
* USAGE GUIDE:
|
|
6
|
+
* - Import this in your root layout (app/layout.jsx)
|
|
7
|
+
* - Wrap all components that need access to Redux store
|
|
8
|
+
*
|
|
9
|
+
* EXAMPLE:
|
|
10
|
+
* ```jsx
|
|
11
|
+
* import { ReduxProvider } from '@/store/ReduxProvider';
|
|
12
|
+
*
|
|
13
|
+
* export default function RootLayout({ children }) {
|
|
14
|
+
* return (
|
|
15
|
+
* <html lang="en">
|
|
16
|
+
* <body>
|
|
17
|
+
* <ReduxProvider>
|
|
18
|
+
* {children}
|
|
19
|
+
* </ReduxProvider>
|
|
20
|
+
* </body>
|
|
21
|
+
* </html>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
"use client";
|
|
28
|
+
|
|
29
|
+
import { Provider } from "react-redux";
|
|
30
|
+
import { store } from "./store";
|
|
31
|
+
|
|
32
|
+
export function ReduxProvider({ children }) {
|
|
33
|
+
return <Provider store={store}>{children}</Provider>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTK Query API Slice with Auto Token Refresh
|
|
3
|
+
* Responsibility: Base API configuration with automatic token refresh on 401
|
|
4
|
+
*
|
|
5
|
+
* USAGE GUIDE:
|
|
6
|
+
* - This is the base API slice that handles all API calls
|
|
7
|
+
* - Automatically adds Authorization header with access token
|
|
8
|
+
* - Automatically refreshes token when it expires (401 error)
|
|
9
|
+
* - All other API services extend from this base
|
|
10
|
+
*
|
|
11
|
+
* HOW IT WORKS:
|
|
12
|
+
* 1. Request sent with access token
|
|
13
|
+
* 2. If 401 error received, automatically tries to refresh token
|
|
14
|
+
* 3. If refresh successful, retries original request
|
|
15
|
+
* 4. If refresh fails, logs out user
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
|
19
|
+
import { updateAccessToken, logout } from "../slices/authSlice";
|
|
20
|
+
|
|
21
|
+
const API_BASE_URL =
|
|
22
|
+
process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api/v1";
|
|
23
|
+
|
|
24
|
+
// Base query with auth token
|
|
25
|
+
const baseQuery = fetchBaseQuery({
|
|
26
|
+
baseUrl: API_BASE_URL,
|
|
27
|
+
prepareHeaders: (headers, { getState }) => {
|
|
28
|
+
const token = getState().auth.accessToken;
|
|
29
|
+
|
|
30
|
+
if (token) {
|
|
31
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return headers;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Base query with re-authentication (token refresh)
|
|
39
|
+
const baseQueryWithReauth = async (args, api, extraOptions) => {
|
|
40
|
+
let result = await baseQuery(args, api, extraOptions);
|
|
41
|
+
|
|
42
|
+
// If we get a 401 error, try to refresh the token
|
|
43
|
+
if (result?.error?.status === 401) {
|
|
44
|
+
console.log("Token expired, attempting to refresh...");
|
|
45
|
+
|
|
46
|
+
const refreshToken = api.getState().auth.refreshToken;
|
|
47
|
+
|
|
48
|
+
if (refreshToken) {
|
|
49
|
+
// Try to get a new access token
|
|
50
|
+
const refreshResult = await baseQuery(
|
|
51
|
+
{
|
|
52
|
+
url: "/auth/refresh",
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: { refreshToken },
|
|
55
|
+
},
|
|
56
|
+
api,
|
|
57
|
+
extraOptions
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (refreshResult?.data) {
|
|
61
|
+
// Store the new access token
|
|
62
|
+
const { accessToken } = refreshResult.data;
|
|
63
|
+
api.dispatch(updateAccessToken(accessToken));
|
|
64
|
+
|
|
65
|
+
console.log(
|
|
66
|
+
"Token refreshed successfully, retrying original request..."
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Retry the original request with new token
|
|
70
|
+
result = await baseQuery(args, api, extraOptions);
|
|
71
|
+
} else {
|
|
72
|
+
// Refresh token failed - logout user
|
|
73
|
+
console.log("Token refresh failed, logging out...");
|
|
74
|
+
api.dispatch(logout());
|
|
75
|
+
|
|
76
|
+
// Redirect to login page
|
|
77
|
+
if (typeof window !== "undefined") {
|
|
78
|
+
window.location.href = "/login";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// No refresh token available - logout user
|
|
83
|
+
api.dispatch(logout());
|
|
84
|
+
|
|
85
|
+
// Redirect to login page
|
|
86
|
+
if (typeof window !== "undefined") {
|
|
87
|
+
window.location.href = "/login";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Create the API slice
|
|
96
|
+
export const apiSlice = createApi({
|
|
97
|
+
reducerPath: "api",
|
|
98
|
+
baseQuery: baseQueryWithReauth,
|
|
99
|
+
tagTypes: ["User"
|
|
100
|
+
✓ Starting...
|
|
101
|
+
`destination` does not start with `/`, `http://`, or `https://` for route {"source":"/api/:path*","destination":"undefined/:path*"}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
Error: Invalid rewrite found
|
|
105
|
+
|
|
106
|
+
coder-bird@root-bird:~/Desktop/next-template$ , "Auth"], // Add more tag types as needed
|
|
107
|
+
endpoints: (builder) => ({}), // Endpoints will be injected from other files
|
|
108
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth API Service using RTK Query
|
|
3
|
+
* Responsibility: Handle all authentication-related API calls
|
|
4
|
+
*
|
|
5
|
+
* USAGE GUIDE:
|
|
6
|
+
* - Import: import { useLoginMutation, useRegisterMutation, useLogoutMutation, useGetCurrentUserQuery } from '@/store/api/authApi'
|
|
7
|
+
* - In component: const [login, { isLoading, error }] = useLoginMutation()
|
|
8
|
+
* - Call mutation: await login({ email, password }).unwrap()
|
|
9
|
+
*
|
|
10
|
+
* EXAMPLE:
|
|
11
|
+
* ```jsx
|
|
12
|
+
* const [login, { isLoading }] = useLoginMutation();
|
|
13
|
+
*
|
|
14
|
+
* const handleLogin = async (credentials) => {
|
|
15
|
+
* try {
|
|
16
|
+
* const result = await login(credentials).unwrap();
|
|
17
|
+
* // Success - user is automatically logged in
|
|
18
|
+
* router.push('/dashboard');
|
|
19
|
+
* } catch (error) {
|
|
20
|
+
* // Handle error
|
|
21
|
+
* console.error('Login failed:', error);
|
|
22
|
+
* }
|
|
23
|
+
* };
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { apiSlice } from "./apiSlice";
|
|
28
|
+
import { setCredentials, logout as logoutAction } from "../slices/authSlice";
|
|
29
|
+
|
|
30
|
+
export const authApi = apiSlice.injectEndpoints({
|
|
31
|
+
endpoints: (builder) => ({
|
|
32
|
+
// Login
|
|
33
|
+
login: builder.mutation({
|
|
34
|
+
query: (credentials) => ({
|
|
35
|
+
url: "/auth/login",
|
|
36
|
+
method: "POST",
|
|
37
|
+
body: credentials,
|
|
38
|
+
}),
|
|
39
|
+
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
40
|
+
try {
|
|
41
|
+
const { data } = await queryFulfilled;
|
|
42
|
+
// Store user data and tokens in Redux store
|
|
43
|
+
dispatch(
|
|
44
|
+
setCredentials({
|
|
45
|
+
user: data.user,
|
|
46
|
+
accessToken: data.accessToken,
|
|
47
|
+
refreshToken: data.refreshToken,
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Login error:", error);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
invalidatesTags: ["Auth"],
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
// Register
|
|
58
|
+
register: builder.mutation({
|
|
59
|
+
query: (userData) => ({
|
|
60
|
+
url: "/auth/register",
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: userData,
|
|
63
|
+
}),
|
|
64
|
+
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
65
|
+
try {
|
|
66
|
+
const { data } = await queryFulfilled;
|
|
67
|
+
// Store user data and tokens in Redux store
|
|
68
|
+
dispatch(
|
|
69
|
+
setCredentials({
|
|
70
|
+
user: data.user,
|
|
71
|
+
accessToken: data.accessToken,
|
|
72
|
+
refreshToken: data.refreshToken,
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("Register error:", error);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
invalidatesTags: ["Auth"],
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
// Logout
|
|
83
|
+
logout: builder.mutation({
|
|
84
|
+
query: () => ({
|
|
85
|
+
url: "/auth/logout",
|
|
86
|
+
method: "POST",
|
|
87
|
+
}),
|
|
88
|
+
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
|
89
|
+
try {
|
|
90
|
+
await queryFulfilled;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("Logout error:", error);
|
|
93
|
+
} finally {
|
|
94
|
+
// Clear auth state regardless of API success
|
|
95
|
+
dispatch(logoutAction());
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
invalidatesTags: ["Auth", "User"],
|
|
99
|
+
}),
|
|
100
|
+
|
|
101
|
+
// Get current user
|
|
102
|
+
getCurrentUser: builder.query({
|
|
103
|
+
query: () => "/auth/me",
|
|
104
|
+
providesTags: ["User"],
|
|
105
|
+
}),
|
|
106
|
+
|
|
107
|
+
// Refresh token (usually called automatically by baseQueryWithReauth)
|
|
108
|
+
refreshToken: builder.mutation({
|
|
109
|
+
query: (refreshToken) => ({
|
|
110
|
+
url: "/auth/refresh",
|
|
111
|
+
method: "POST",
|
|
112
|
+
body: { refreshToken },
|
|
113
|
+
}),
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
// Forgot password
|
|
117
|
+
forgotPassword: builder.mutation({
|
|
118
|
+
query: (email) => ({
|
|
119
|
+
url: "/auth/forgot-password",
|
|
120
|
+
method: "POST",
|
|
121
|
+
body: { email },
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
// Reset password
|
|
126
|
+
resetPassword: builder.mutation({
|
|
127
|
+
query: ({ token, password }) => ({
|
|
128
|
+
url: "/auth/reset-password",
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: { token, password },
|
|
131
|
+
}),
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
// Change password
|
|
135
|
+
changePassword: builder.mutation({
|
|
136
|
+
query: ({ oldPassword, newPassword }) => ({
|
|
137
|
+
url: "/auth/change-password",
|
|
138
|
+
method: "POST",
|
|
139
|
+
body: { oldPassword, newPassword },
|
|
140
|
+
}),
|
|
141
|
+
}),
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Export hooks for usage in components
|
|
146
|
+
export const {
|
|
147
|
+
useLoginMutation,
|
|
148
|
+
useRegisterMutation,
|
|
149
|
+
useLogoutMutation,
|
|
150
|
+
useGetCurrentUserQuery,
|
|
151
|
+
useRefreshTokenMutation,
|
|
152
|
+
useForgotPasswordMutation,
|
|
153
|
+
useResetPasswordMutation,
|
|
154
|
+
useChangePasswordMutation,
|
|
155
|
+
} = authApi;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example API Service Template
|
|
3
|
+
* Responsibility: Template for creating new API services
|
|
4
|
+
*
|
|
5
|
+
* USAGE GUIDE:
|
|
6
|
+
* Copy this file and rename it to match your resource (e.g., postsApi.js, productsApi.js)
|
|
7
|
+
* Replace 'Example' with your resource name
|
|
8
|
+
*
|
|
9
|
+
* STEPS TO CREATE NEW API SERVICE:
|
|
10
|
+
* 1. Copy this file
|
|
11
|
+
* 2. Rename file and variables
|
|
12
|
+
* 3. Define your tag types in store/api/apiSlice.js
|
|
13
|
+
* 4. Update endpoints with your API routes
|
|
14
|
+
* 5. Import and use hooks in your components
|
|
15
|
+
*
|
|
16
|
+
* EXAMPLE FOR POSTS API:
|
|
17
|
+
* ```jsx
|
|
18
|
+
* // store/api/postsApi.js
|
|
19
|
+
* import { apiSlice } from './apiSlice';
|
|
20
|
+
*
|
|
21
|
+
* export const postsApi = apiSlice.injectEndpoints({
|
|
22
|
+
* endpoints: (builder) => ({
|
|
23
|
+
* getPosts: builder.query({
|
|
24
|
+
* query: () => '/posts',
|
|
25
|
+
* providesTags: ['Posts'],
|
|
26
|
+
* }),
|
|
27
|
+
* createPost: builder.mutation({
|
|
28
|
+
* query: (postData) => ({
|
|
29
|
+
* url: '/posts',
|
|
30
|
+
* method: 'POST',
|
|
31
|
+
* body: postData,
|
|
32
|
+
* }),
|
|
33
|
+
* invalidatesTags: ['Posts'],
|
|
34
|
+
* }),
|
|
35
|
+
* }),
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* export const { useGetPostsQuery, useCreatePostMutation } = postsApi;
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { apiSlice } from "./apiSlice";
|
|
43
|
+
|
|
44
|
+
export const exampleApi = apiSlice.injectEndpoints({
|
|
45
|
+
endpoints: (builder) => ({
|
|
46
|
+
// GET - Fetch list of items
|
|
47
|
+
getExamples: builder.query({
|
|
48
|
+
query: (params = {}) => {
|
|
49
|
+
const queryString = new URLSearchParams(params).toString();
|
|
50
|
+
return `/examples${queryString ? `?${queryString}` : ""}`;
|
|
51
|
+
},
|
|
52
|
+
providesTags: (result) =>
|
|
53
|
+
result
|
|
54
|
+
? [
|
|
55
|
+
...result.data.map(({ id }) => ({ type: "Example", id })),
|
|
56
|
+
{ type: "Example", id: "LIST" },
|
|
57
|
+
]
|
|
58
|
+
: [{ type: "Example", id: "LIST" }],
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
// GET - Fetch single item by ID
|
|
62
|
+
getExampleById: builder.query({
|
|
63
|
+
query: (id) => `/examples/${id}`,
|
|
64
|
+
providesTags: (result, error, id) => [{ type: "Example", id }],
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
// POST - Create new item
|
|
68
|
+
createExample: builder.mutation({
|
|
69
|
+
query: (data) => ({
|
|
70
|
+
url: "/examples",
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: data,
|
|
73
|
+
}),
|
|
74
|
+
invalidatesTags: [{ type: "Example", id: "LIST" }],
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
// PUT - Update item
|
|
78
|
+
updateExample: builder.mutation({
|
|
79
|
+
query: ({ id, ...data }) => ({
|
|
80
|
+
url: `/examples/${id}`,
|
|
81
|
+
method: "PUT",
|
|
82
|
+
body: data,
|
|
83
|
+
}),
|
|
84
|
+
invalidatesTags: (result, error, { id }) => [
|
|
85
|
+
{ type: "Example", id },
|
|
86
|
+
{ type: "Example", id: "LIST" },
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
// DELETE - Delete item
|
|
91
|
+
deleteExample: builder.mutation({
|
|
92
|
+
query: (id) => ({
|
|
93
|
+
url: `/examples/${id}`,
|
|
94
|
+
method: "DELETE",
|
|
95
|
+
}),
|
|
96
|
+
invalidatesTags: [{ type: "Example", id: "LIST" }],
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Export hooks for usage in components
|
|
102
|
+
export const {
|
|
103
|
+
useGetExamplesQuery,
|
|
104
|
+
useGetExampleByIdQuery,
|
|
105
|
+
useCreateExampleMutation,
|
|
106
|
+
useUpdateExampleMutation,
|
|
107
|
+
useDeleteExampleMutation,
|
|
108
|
+
} = exampleApi;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User API Service using RTK Query
|
|
3
|
+
* Responsibility: Handle all user-related API calls
|
|
4
|
+
*
|
|
5
|
+
* USAGE GUIDE:
|
|
6
|
+
* - Import: import { useGetUsersQuery, useGetUserByIdQuery, useUpdateUserMutation } from '@/store/api/userApi'
|
|
7
|
+
* - In component: const { data: users, isLoading, error } = useGetUsersQuery()
|
|
8
|
+
* - With params: const { data: user } = useGetUserByIdQuery(userId)
|
|
9
|
+
*
|
|
10
|
+
* EXAMPLE:
|
|
11
|
+
* ```jsx
|
|
12
|
+
* const { data: users, isLoading, error, refetch } = useGetUsersQuery({ page: 1, limit: 10 });
|
|
13
|
+
*
|
|
14
|
+
* const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation();
|
|
15
|
+
*
|
|
16
|
+
* const handleUpdate = async (userId, userData) => {
|
|
17
|
+
* try {
|
|
18
|
+
* await updateUser({ id: userId, ...userData }).unwrap();
|
|
19
|
+
* // Success
|
|
20
|
+
* } catch (error) {
|
|
21
|
+
* // Handle error
|
|
22
|
+
* }
|
|
23
|
+
* };
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { apiSlice } from "./apiSlice";
|
|
28
|
+
|
|
29
|
+
export const userApi = apiSlice.injectEndpoints({
|
|
30
|
+
endpoints: (builder) => ({
|
|
31
|
+
// Get all users (with pagination and filters)
|
|
32
|
+
getUsers: builder.query({
|
|
33
|
+
query: (params = {}) => {
|
|
34
|
+
const queryString = new URLSearchParams(params).toString();
|
|
35
|
+
return `/users${queryString ? `?${queryString}` : ""}`;
|
|
36
|
+
},
|
|
37
|
+
providesTags: (result) =>
|
|
38
|
+
result
|
|
39
|
+
? [
|
|
40
|
+
...result.users.map(({ id }) => ({ type: "User", id })),
|
|
41
|
+
{ type: "User", id: "LIST" },
|
|
42
|
+
]
|
|
43
|
+
: [{ type: "User", id: "LIST" }],
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
// Get single user by ID
|
|
47
|
+
getUserById: builder.query({
|
|
48
|
+
query: (id) => `/users/${id}`,
|
|
49
|
+
providesTags: (result, error, id) => [{ type: "User", id }],
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
// Create new user
|
|
53
|
+
createUser: builder.mutation({
|
|
54
|
+
query: (userData) => ({
|
|
55
|
+
url: "/users",
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: userData,
|
|
58
|
+
}),
|
|
59
|
+
invalidatesTags: [{ type: "User", id: "LIST" }],
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
// Update user
|
|
63
|
+
updateUser: builder.mutation({
|
|
64
|
+
query: ({ id, ...userData }) => ({
|
|
65
|
+
url: `/users/${id}`,
|
|
66
|
+
method: "PUT",
|
|
67
|
+
body: userData,
|
|
68
|
+
}),
|
|
69
|
+
invalidatesTags: (result, error, { id }) => [{ type: "User", id }],
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
// Delete user
|
|
73
|
+
deleteUser: builder.mutation({
|
|
74
|
+
query: (id) => ({
|
|
75
|
+
url: `/users/${id}`,
|
|
76
|
+
method: "DELETE",
|
|
77
|
+
}),
|
|
78
|
+
invalidatesTags: [{ type: "User", id: "LIST" }],
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
// Update user profile
|
|
82
|
+
updateProfile: builder.mutation({
|
|
83
|
+
query: (userData) => ({
|
|
84
|
+
url: "/users/profile",
|
|
85
|
+
method: "PUT",
|
|
86
|
+
body: userData,
|
|
87
|
+
}),
|
|
88
|
+
invalidatesTags: ["User"],
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
// Upload user avatar
|
|
92
|
+
uploadAvatar: builder.mutation({
|
|
93
|
+
query: (formData) => ({
|
|
94
|
+
url: "/users/avatar",
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: formData,
|
|
97
|
+
// Don't set Content-Type header, let browser set it for FormData
|
|
98
|
+
headers: {},
|
|
99
|
+
}),
|
|
100
|
+
invalidatesTags: ["User"],
|
|
101
|
+
}),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Export hooks for usage in components
|
|
106
|
+
export const {
|
|
107
|
+
useGetUsersQuery,
|
|
108
|
+
useGetUserByIdQuery,
|
|
109
|
+
useCreateUserMutation,
|
|
110
|
+
useUpdateUserMutation,
|
|
111
|
+
useDeleteUserMutation,
|
|
112
|
+
useUpdateProfileMutation,
|
|
113
|
+
useUploadAvatarMutation,
|
|
114
|
+
} = userApi;
|