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.
Files changed (47) hide show
  1. package/README.md +93 -0
  2. package/bin/index.js +112 -0
  3. package/package.json +33 -0
  4. package/templates/README.md +160 -0
  5. package/templates/_gitignore +8 -0
  6. package/templates/client/index.html +13 -0
  7. package/templates/client/package.json +20 -0
  8. package/templates/client/src/App.jsx +72 -0
  9. package/templates/client/src/components/Loading.jsx +14 -0
  10. package/templates/client/src/components/Navbar.jsx +58 -0
  11. package/templates/client/src/context/AuthContext.jsx +109 -0
  12. package/templates/client/src/index.css +244 -0
  13. package/templates/client/src/main.jsx +16 -0
  14. package/templates/client/src/pages/CreateResource.jsx +142 -0
  15. package/templates/client/src/pages/Dashboard.jsx +117 -0
  16. package/templates/client/src/pages/EditResource.jsx +166 -0
  17. package/templates/client/src/pages/Login.jsx +101 -0
  18. package/templates/client/src/pages/NotFound.jsx +21 -0
  19. package/templates/client/src/pages/Profile.jsx +52 -0
  20. package/templates/client/src/pages/Register.jsx +134 -0
  21. package/templates/client/src/pages/ResourceDetails.jsx +156 -0
  22. package/templates/client/src/pages/Resources.jsx +181 -0
  23. package/templates/client/src/routes/ProtectedRoute.jsx +34 -0
  24. package/templates/client/src/services/api.js +71 -0
  25. package/templates/client/vite.config.js +20 -0
  26. package/templates/server/.env.example +5 -0
  27. package/templates/server/config/db.js +13 -0
  28. package/templates/server/controllers/authController.js +131 -0
  29. package/templates/server/controllers/resourceController.js +254 -0
  30. package/templates/server/middleware/adminMiddleware.js +14 -0
  31. package/templates/server/middleware/authMiddleware.js +53 -0
  32. package/templates/server/middleware/errorMiddleware.js +55 -0
  33. package/templates/server/middleware/sessionAuthMiddleware.js +12 -0
  34. package/templates/server/middleware/uploadMiddleware.js +61 -0
  35. package/templates/server/middleware/validateMiddleware.js +61 -0
  36. package/templates/server/models/Resource.js +43 -0
  37. package/templates/server/models/User.js +48 -0
  38. package/templates/server/package.json +25 -0
  39. package/templates/server/routes/adminRoutes.js +138 -0
  40. package/templates/server/routes/authRoutes.js +21 -0
  41. package/templates/server/routes/resourceRoutes.js +27 -0
  42. package/templates/server/server.js +89 -0
  43. package/templates/server/utils/jwt.js +18 -0
  44. package/templates/server/views/dashboard.ejs +128 -0
  45. package/templates/server/views/login.ejs +48 -0
  46. package/templates/server/views/partials/footer.ejs +6 -0
  47. 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 &rarr;
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;