mern-template-tool 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/README.md +93 -0
- package/bin/index.js +112 -0
- package/package.json +33 -0
- package/templates/README.md +160 -0
- package/templates/_gitignore +8 -0
- package/templates/client/index.html +13 -0
- package/templates/client/package.json +20 -0
- package/templates/client/src/App.jsx +72 -0
- package/templates/client/src/components/Loading.jsx +14 -0
- package/templates/client/src/components/Navbar.jsx +58 -0
- package/templates/client/src/context/AuthContext.jsx +109 -0
- package/templates/client/src/index.css +244 -0
- package/templates/client/src/main.jsx +16 -0
- package/templates/client/src/pages/CreateResource.jsx +142 -0
- package/templates/client/src/pages/Dashboard.jsx +117 -0
- package/templates/client/src/pages/EditResource.jsx +166 -0
- package/templates/client/src/pages/Login.jsx +101 -0
- package/templates/client/src/pages/NotFound.jsx +21 -0
- package/templates/client/src/pages/Profile.jsx +52 -0
- package/templates/client/src/pages/Register.jsx +134 -0
- package/templates/client/src/pages/ResourceDetails.jsx +156 -0
- package/templates/client/src/pages/Resources.jsx +181 -0
- package/templates/client/src/routes/ProtectedRoute.jsx +34 -0
- package/templates/client/src/services/api.js +71 -0
- package/templates/client/vite.config.js +20 -0
- package/templates/server/.env.example +5 -0
- package/templates/server/config/db.js +13 -0
- package/templates/server/controllers/authController.js +131 -0
- package/templates/server/controllers/resourceController.js +254 -0
- package/templates/server/middleware/adminMiddleware.js +14 -0
- package/templates/server/middleware/authMiddleware.js +53 -0
- package/templates/server/middleware/errorMiddleware.js +55 -0
- package/templates/server/middleware/sessionAuthMiddleware.js +12 -0
- package/templates/server/middleware/uploadMiddleware.js +61 -0
- package/templates/server/middleware/validateMiddleware.js +61 -0
- package/templates/server/models/Resource.js +43 -0
- package/templates/server/models/User.js +48 -0
- package/templates/server/package.json +25 -0
- package/templates/server/routes/adminRoutes.js +138 -0
- package/templates/server/routes/authRoutes.js +21 -0
- package/templates/server/routes/resourceRoutes.js +27 -0
- package/templates/server/server.js +89 -0
- package/templates/server/utils/jwt.js +18 -0
- package/templates/server/views/dashboard.ejs +128 -0
- package/templates/server/views/login.ejs +48 -0
- package/templates/server/views/partials/footer.ejs +6 -0
- package/templates/server/views/partials/header.ejs +60 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
import { api } from '../services/api';
|
|
3
|
+
|
|
4
|
+
const AuthContext = createContext();
|
|
5
|
+
|
|
6
|
+
export const AuthProvider = ({ children }) => {
|
|
7
|
+
const [user, setUser] = useState(null);
|
|
8
|
+
const [token, setToken] = useState(() => localStorage.getItem('token') || null);
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
|
|
11
|
+
// Check auth state on mount or token change
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const checkAuthentication = async () => {
|
|
14
|
+
const storedToken = localStorage.getItem('token');
|
|
15
|
+
if (!storedToken) {
|
|
16
|
+
setUser(null);
|
|
17
|
+
setLoading(false);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const response = await api.getMe();
|
|
23
|
+
if (response.success && response.user) {
|
|
24
|
+
setUser(response.user);
|
|
25
|
+
} else {
|
|
26
|
+
logout();
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('Failed to verify token:', err);
|
|
30
|
+
logout();
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
checkAuthentication();
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
// Login handler
|
|
40
|
+
const login = async (email, password) => {
|
|
41
|
+
const response = await api.login({ email, password });
|
|
42
|
+
if (response.success && response.token) {
|
|
43
|
+
localStorage.setItem('token', response.token);
|
|
44
|
+
setToken(response.token);
|
|
45
|
+
setUser(response.user);
|
|
46
|
+
return response;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(response.message || 'Login failed');
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Register handler
|
|
52
|
+
const register = async (name, email, password, role = 'student') => {
|
|
53
|
+
const response = await api.register({ name, email, password, role });
|
|
54
|
+
if (response.success && response.token) {
|
|
55
|
+
localStorage.setItem('token', response.token);
|
|
56
|
+
setToken(response.token);
|
|
57
|
+
setUser(response.user);
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
throw new Error(response.message || 'Registration failed');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Logout handler
|
|
64
|
+
const logout = () => {
|
|
65
|
+
localStorage.removeItem('token');
|
|
66
|
+
setToken(null);
|
|
67
|
+
setUser(null);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Refresh user state
|
|
71
|
+
const refreshUser = async () => {
|
|
72
|
+
try {
|
|
73
|
+
const response = await api.getMe();
|
|
74
|
+
if (response.success) {
|
|
75
|
+
setUser(response.user);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('Error refreshing user profile:', err);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<AuthContext.Provider
|
|
84
|
+
value={{
|
|
85
|
+
user,
|
|
86
|
+
token,
|
|
87
|
+
isAuthenticated: !!user,
|
|
88
|
+
loading,
|
|
89
|
+
login,
|
|
90
|
+
register,
|
|
91
|
+
logout,
|
|
92
|
+
refreshUser,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</AuthContext.Provider>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Custom hook to use AuthContext
|
|
101
|
+
export const useAuth = () => {
|
|
102
|
+
const context = useContext(AuthContext);
|
|
103
|
+
if (!context) {
|
|
104
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
105
|
+
}
|
|
106
|
+
return context;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default AuthContext;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/* Base CSS Reset and Variables */
|
|
2
|
+
:root {
|
|
3
|
+
--primary: #6366f1;
|
|
4
|
+
--primary-hover: #4f46e5;
|
|
5
|
+
--primary-light: #eef2ff;
|
|
6
|
+
--bg-color: #0f172a;
|
|
7
|
+
--card-bg: #1e293b;
|
|
8
|
+
--card-border: #334155;
|
|
9
|
+
--text-primary: #f8fafc;
|
|
10
|
+
--text-secondary: #94a3b8;
|
|
11
|
+
--danger: #ef4444;
|
|
12
|
+
--success: #10b981;
|
|
13
|
+
--warning: #f59e0b;
|
|
14
|
+
--radius: 8px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
background-color: var(--bg-color);
|
|
26
|
+
color: var(--text-primary);
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#root {
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.container {
|
|
39
|
+
max-width: 1100px;
|
|
40
|
+
margin: 0 auto;
|
|
41
|
+
padding: 2rem 1rem;
|
|
42
|
+
flex: 1;
|
|
43
|
+
width: 100%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Typography */
|
|
47
|
+
h1, h2, h3, h4 {
|
|
48
|
+
color: var(--text-primary);
|
|
49
|
+
font-weight: 700;
|
|
50
|
+
margin-bottom: 0.75rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
p {
|
|
54
|
+
color: var(--text-secondary);
|
|
55
|
+
line-height: 1.6;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Navigation */
|
|
59
|
+
.navbar {
|
|
60
|
+
background-color: var(--card-bg);
|
|
61
|
+
border-bottom: 1px solid var(--card-border);
|
|
62
|
+
padding: 1rem 2rem;
|
|
63
|
+
display: flex;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
align-items: center;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.nav-brand {
|
|
69
|
+
font-size: 1.25rem;
|
|
70
|
+
font-weight: 700;
|
|
71
|
+
color: #fff;
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
gap: 0.5rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.nav-links {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 1.25rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.nav-links a {
|
|
85
|
+
color: var(--text-secondary);
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
font-weight: 500;
|
|
88
|
+
font-size: 0.95rem;
|
|
89
|
+
transition: color 0.2s;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.nav-links a:hover,
|
|
93
|
+
.nav-links a.active {
|
|
94
|
+
color: #fff;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Cards & Layout */
|
|
98
|
+
.card {
|
|
99
|
+
background-color: var(--card-bg);
|
|
100
|
+
border: 1px solid var(--card-border);
|
|
101
|
+
border-radius: var(--radius);
|
|
102
|
+
padding: 1.5rem;
|
|
103
|
+
margin-bottom: 1.5rem;
|
|
104
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.grid-cols-3 {
|
|
108
|
+
display: grid;
|
|
109
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
110
|
+
gap: 1.5rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Buttons */
|
|
114
|
+
.btn {
|
|
115
|
+
display: inline-flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
padding: 0.6rem 1.2rem;
|
|
119
|
+
border-radius: var(--radius);
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
font-size: 0.9rem;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
border: none;
|
|
124
|
+
transition: all 0.2s ease;
|
|
125
|
+
text-decoration: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.btn-primary {
|
|
129
|
+
background-color: var(--primary);
|
|
130
|
+
color: white;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.btn-primary:hover {
|
|
134
|
+
background-color: var(--primary-hover);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.btn-secondary {
|
|
138
|
+
background-color: transparent;
|
|
139
|
+
border: 1px solid var(--card-border);
|
|
140
|
+
color: var(--text-primary);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.btn-secondary:hover {
|
|
144
|
+
background-color: var(--card-border);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.btn-danger {
|
|
148
|
+
background-color: var(--danger);
|
|
149
|
+
color: white;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.btn-danger:hover {
|
|
153
|
+
opacity: 0.9;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.btn-sm {
|
|
157
|
+
padding: 0.35rem 0.75rem;
|
|
158
|
+
font-size: 0.8rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Forms */
|
|
162
|
+
.form-group {
|
|
163
|
+
margin-bottom: 1.25rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.form-group label {
|
|
167
|
+
display: block;
|
|
168
|
+
font-size: 0.875rem;
|
|
169
|
+
font-weight: 500;
|
|
170
|
+
color: var(--text-secondary);
|
|
171
|
+
margin-bottom: 0.5rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.form-control {
|
|
175
|
+
width: 100%;
|
|
176
|
+
padding: 0.7rem 0.9rem;
|
|
177
|
+
background-color: #0b1120;
|
|
178
|
+
border: 1px solid var(--card-border);
|
|
179
|
+
border-radius: var(--radius);
|
|
180
|
+
color: var(--text-primary);
|
|
181
|
+
font-size: 0.95rem;
|
|
182
|
+
outline: none;
|
|
183
|
+
transition: border-color 0.2s;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.form-control:focus {
|
|
187
|
+
border-color: var(--primary);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Alerts */
|
|
191
|
+
.alert {
|
|
192
|
+
padding: 0.8rem 1rem;
|
|
193
|
+
border-radius: var(--radius);
|
|
194
|
+
font-size: 0.9rem;
|
|
195
|
+
margin-bottom: 1.25rem;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.alert-error {
|
|
199
|
+
background-color: rgba(239, 68, 68, 0.15);
|
|
200
|
+
border: 1px solid var(--danger);
|
|
201
|
+
color: #fca5a5;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.alert-success {
|
|
205
|
+
background-color: rgba(16, 185, 129, 0.15);
|
|
206
|
+
border: 1px solid var(--success);
|
|
207
|
+
color: #6ee7b7;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Badges */
|
|
211
|
+
.badge {
|
|
212
|
+
display: inline-block;
|
|
213
|
+
padding: 0.2rem 0.6rem;
|
|
214
|
+
border-radius: 9999px;
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
font-weight: 600;
|
|
217
|
+
text-transform: uppercase;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.badge-primary {
|
|
221
|
+
background: rgba(99, 102, 241, 0.2);
|
|
222
|
+
color: #a5b4fc;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Spinner */
|
|
226
|
+
.spinner-container {
|
|
227
|
+
display: flex;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
align-items: center;
|
|
230
|
+
padding: 3rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.spinner {
|
|
234
|
+
width: 40px;
|
|
235
|
+
height: 40px;
|
|
236
|
+
border: 4px solid var(--card-border);
|
|
237
|
+
border-top-color: var(--primary);
|
|
238
|
+
border-radius: 50%;
|
|
239
|
+
animation: spin 0.8s linear infinite;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@keyframes spin {
|
|
243
|
+
to { transform: rotate(360deg); }
|
|
244
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
+
import App from './App.jsx';
|
|
5
|
+
import { AuthProvider } from './context/AuthContext.jsx';
|
|
6
|
+
import './index.css';
|
|
7
|
+
|
|
8
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
9
|
+
<React.StrictMode>
|
|
10
|
+
<BrowserRouter>
|
|
11
|
+
<AuthProvider>
|
|
12
|
+
<App />
|
|
13
|
+
</AuthProvider>
|
|
14
|
+
</BrowserRouter>
|
|
15
|
+
</React.StrictMode>
|
|
16
|
+
);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { api } from '../services/api';
|
|
4
|
+
|
|
5
|
+
const CreateResource = () => {
|
|
6
|
+
const [title, setTitle] = useState('');
|
|
7
|
+
const [description, setDescription] = useState('');
|
|
8
|
+
const [category, setCategory] = useState('General');
|
|
9
|
+
const [file, setFile] = useState(null);
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
|
|
15
|
+
const handleFileChange = (e) => {
|
|
16
|
+
if (e.target.files && e.target.files[0]) {
|
|
17
|
+
setFile(e.target.files[0]);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleSubmit = async (e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Use FormData to support multipart/form-data with file upload
|
|
28
|
+
const formData = new FormData();
|
|
29
|
+
formData.append('title', title);
|
|
30
|
+
formData.append('description', description);
|
|
31
|
+
formData.append('category', category);
|
|
32
|
+
if (file) {
|
|
33
|
+
formData.append('file', file);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = await api.createResource(formData);
|
|
37
|
+
if (response.success) {
|
|
38
|
+
navigate(`/resources/${response.data._id}`);
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err.data && err.data.errors) {
|
|
42
|
+
setError(err.data.errors.map((e) => e.message).join(', '));
|
|
43
|
+
} else {
|
|
44
|
+
setError(err.message || 'Failed to create resource');
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="container" style={{ maxWidth: '650px' }}>
|
|
53
|
+
<div className="card">
|
|
54
|
+
<h2>Create New Resource</h2>
|
|
55
|
+
<p style={{ marginBottom: '1.5rem', fontSize: '0.9rem' }}>
|
|
56
|
+
Demonstrates Multer file upload, JWT-protected POST endpoint, and validation.
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
{error && <div className="alert alert-error">{error}</div>}
|
|
60
|
+
|
|
61
|
+
<form onSubmit={handleSubmit}>
|
|
62
|
+
<div className="form-group">
|
|
63
|
+
<label htmlFor="title">Title *</label>
|
|
64
|
+
<input
|
|
65
|
+
id="title"
|
|
66
|
+
type="text"
|
|
67
|
+
required
|
|
68
|
+
className="form-control"
|
|
69
|
+
placeholder="e.g. Node.js Architecture Notes"
|
|
70
|
+
value={title}
|
|
71
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="form-group">
|
|
76
|
+
<label htmlFor="category">Category</label>
|
|
77
|
+
<select
|
|
78
|
+
id="category"
|
|
79
|
+
className="form-control"
|
|
80
|
+
value={category}
|
|
81
|
+
onChange={(e) => setCategory(e.target.value)}
|
|
82
|
+
>
|
|
83
|
+
<option value="General">General</option>
|
|
84
|
+
<option value="Documentation">Documentation</option>
|
|
85
|
+
<option value="Assignment">Assignment</option>
|
|
86
|
+
<option value="Project">Project</option>
|
|
87
|
+
<option value="Exam">Exam</option>
|
|
88
|
+
</select>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="form-group">
|
|
92
|
+
<label htmlFor="description">Description</label>
|
|
93
|
+
<textarea
|
|
94
|
+
id="description"
|
|
95
|
+
rows={4}
|
|
96
|
+
className="form-control"
|
|
97
|
+
placeholder="Provide context or details about this item..."
|
|
98
|
+
value={description}
|
|
99
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="form-group">
|
|
104
|
+
<label htmlFor="file">File Upload (Optional - PDF, Images, DOCX, max 10MB)</label>
|
|
105
|
+
<input
|
|
106
|
+
id="file"
|
|
107
|
+
type="file"
|
|
108
|
+
className="form-control"
|
|
109
|
+
onChange={handleFileChange}
|
|
110
|
+
accept=".pdf,.png,.jpg,.jpeg,.doc,.docx"
|
|
111
|
+
/>
|
|
112
|
+
{file && (
|
|
113
|
+
<small style={{ display: 'block', marginTop: '0.4rem', color: 'var(--text-secondary)' }}>
|
|
114
|
+
Selected: {file.name} ({(file.size / 1024).toFixed(1)} KB)
|
|
115
|
+
</small>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
|
|
120
|
+
<button
|
|
121
|
+
type="submit"
|
|
122
|
+
className="btn btn-primary"
|
|
123
|
+
disabled={loading}
|
|
124
|
+
style={{ flex: 1 }}
|
|
125
|
+
>
|
|
126
|
+
{loading ? 'Creating...' : 'Create Resource'}
|
|
127
|
+
</button>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={() => navigate('/resources')}
|
|
131
|
+
className="btn btn-secondary"
|
|
132
|
+
>
|
|
133
|
+
Cancel
|
|
134
|
+
</button>
|
|
135
|
+
</div>
|
|
136
|
+
</form>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default CreateResource;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../context/AuthContext';
|
|
4
|
+
import { api } from '../services/api';
|
|
5
|
+
import Loading from '../components/Loading';
|
|
6
|
+
|
|
7
|
+
const Dashboard = () => {
|
|
8
|
+
const { user } = useAuth();
|
|
9
|
+
const [userResources, setUserResources] = useState([]);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const fetchUserData = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const response = await api.getResources();
|
|
16
|
+
if (response.success) {
|
|
17
|
+
// Filter resources created by current user
|
|
18
|
+
const myResources = response.data.filter(
|
|
19
|
+
(r) => r.createdBy && (r.createdBy._id === user?.id || r.createdBy === user?.id)
|
|
20
|
+
);
|
|
21
|
+
setUserResources(myResources);
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error('Failed to load dashboard data:', err);
|
|
25
|
+
} finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
fetchUserData();
|
|
31
|
+
}, [user]);
|
|
32
|
+
|
|
33
|
+
if (loading) return <Loading message="Loading dashboard..." />;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="container">
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
display: 'flex',
|
|
40
|
+
justifyContent: 'space-between',
|
|
41
|
+
alignItems: 'center',
|
|
42
|
+
marginBottom: '2rem',
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<div>
|
|
46
|
+
<h2>Welcome, {user?.name}!</h2>
|
|
47
|
+
<p>You are logged in with role: <span className="badge badge-primary">{user?.role}</span></p>
|
|
48
|
+
</div>
|
|
49
|
+
<Link to="/resources/new" className="btn btn-primary">
|
|
50
|
+
+ Create New Resource
|
|
51
|
+
</Link>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: '1rem', marginBottom: '2rem' }}>
|
|
55
|
+
<div className="card">
|
|
56
|
+
<p style={{ fontSize: '0.85rem' }}>Your Email</p>
|
|
57
|
+
<h3 style={{ fontSize: '1.1rem', marginTop: '0.25rem' }}>{user?.email}</h3>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="card">
|
|
60
|
+
<p style={{ fontSize: '0.85rem' }}>Resources Created</p>
|
|
61
|
+
<h3 style={{ fontSize: '1.8rem', color: 'var(--primary)', marginTop: '0.25rem' }}>
|
|
62
|
+
{userResources.length}
|
|
63
|
+
</h3>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="card">
|
|
66
|
+
<p style={{ fontSize: '0.85rem' }}>Account Status</p>
|
|
67
|
+
<h3 style={{ fontSize: '1.2rem', color: 'var(--success)', marginTop: '0.25rem' }}>
|
|
68
|
+
Active
|
|
69
|
+
</h3>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="card">
|
|
74
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
|
75
|
+
<h3>Your Recent Items</h3>
|
|
76
|
+
<Link to="/resources" style={{ color: 'var(--primary)', fontSize: '0.9rem', textDecoration: 'none' }}>
|
|
77
|
+
View All Resources →
|
|
78
|
+
</Link>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{userResources.length === 0 ? (
|
|
82
|
+
<p style={{ padding: '1.5rem 0', textAlign: 'center' }}>
|
|
83
|
+
You haven't created any resources yet. Click "+ Create New Resource" to add one!
|
|
84
|
+
</p>
|
|
85
|
+
) : (
|
|
86
|
+
<div className="grid-cols-3">
|
|
87
|
+
{userResources.slice(0, 3).map((item) => (
|
|
88
|
+
<div
|
|
89
|
+
key={item._id}
|
|
90
|
+
style={{
|
|
91
|
+
background: 'rgba(255,255,255,0.03)',
|
|
92
|
+
padding: '1rem',
|
|
93
|
+
borderRadius: '6px',
|
|
94
|
+
border: '1px solid var(--card-border)',
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<h4>{item.title}</h4>
|
|
98
|
+
<p style={{ fontSize: '0.85rem', margin: '0.5rem 0' }}>
|
|
99
|
+
Category: <span className="badge badge-primary">{item.category}</span>
|
|
100
|
+
</p>
|
|
101
|
+
<Link
|
|
102
|
+
to={`/resources/${item._id}`}
|
|
103
|
+
className="btn btn-secondary btn-sm"
|
|
104
|
+
style={{ marginTop: '0.5rem' }}
|
|
105
|
+
>
|
|
106
|
+
View Details
|
|
107
|
+
</Link>
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default Dashboard;
|