···
18
18
import UserProfile from './components/UserProfile/UserProfile';
19
19
import ZenPage from './components/ZenPage';
20
20
import CompareScores from './components/CompareScores/CompareScores';
21
21
+
import AdminRoute from './components/Admin/AdminRoute';
21
22
import "./App.css";
22
23
23
24
const App = () => {
···
27
28
<div className="app-container" style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
28
29
<Navbar />
29
30
<div className="main-container" style={{ flex: 1 }}>
30
30
-
<Routes>
31
31
-
{/* All routes are now public */}
32
32
-
<Route path="/home" element={<Home />} />
33
33
-
<Route path="/compare/:username1/:username2" element={<CompareScores />} />
34
34
-
<Route path="/compare" element={<CompareScores />} />
35
35
-
<Route path="/alt-text" element={<AltTextRatingTool />} />
36
36
-
<Route path="/about" element={<About />} />
37
37
-
<Route path="/privacy" element={<Privacy />} />
38
38
-
<Route path="/terms" element={<Terms />} />
39
39
-
<Route path="/newsletter" element={<Newsletter />} />
40
40
-
<Route path="/supporter" element={<Supporter />} />
41
41
-
<Route path="/definitions" element={<Definitions />} />
42
42
-
<Route path="/leaderboard" element={<Leaderboard />} />
43
43
-
<Route path="/resources" element={<Resources />} />
44
44
-
<Route path="/shortcut" element={<Shortcut />} />
45
45
-
<Route path="/zen" element={<ZenPage />} />
46
46
-
<Route path="/methodology" element={<ScoringMethodology />} />
47
47
-
{/* Handle both DIDs and regular usernames */}
48
48
-
<Route path="/:username" element={<UserProfile />} />
49
49
-
{/* Default routes */}
50
50
-
<Route path="/" element={<Navigate to="/home" replace />} />
51
51
-
<Route path="*" element={<Navigate to="/home" replace />} />
52
52
-
</Routes>
31
31
+
<Routes>
32
32
+
{/* All routes are now public */}
33
33
+
<Route path="/home" element={<Home />} />
34
34
+
<Route path="/compare/:username1/:username2" element={<CompareScores />} />
35
35
+
<Route path="/compare" element={<CompareScores />} />
36
36
+
<Route path="/alt-text" element={<AltTextRatingTool />} />
37
37
+
<Route path="/about" element={<About />} />
38
38
+
<Route path="/privacy" element={<Privacy />} />
39
39
+
<Route path="/terms" element={<Terms />} />
40
40
+
<Route path="/newsletter" element={<Newsletter />} />
41
41
+
<Route path="/supporter" element={<Supporter />} />
42
42
+
<Route path="/definitions" element={<Definitions />} />
43
43
+
<Route path="/leaderboard" element={<Leaderboard />} />
44
44
+
<Route path="/resources" element={<Resources />} />
45
45
+
<Route path="/shortcut" element={<Shortcut />} />
46
46
+
<Route path="/zen" element={<ZenPage />} />
47
47
+
<Route path="/methodology" element={<ScoringMethodology />} />
48
48
+
49
49
+
{/* Admin Route */}
50
50
+
<Route path="/admin" element={<AdminRoute />} />
51
51
+
52
52
+
{/* Handle both DIDs and regular usernames */}
53
53
+
<Route path="/:username" element={<UserProfile />} />
54
54
+
55
55
+
{/* Default routes */}
56
56
+
<Route path="/" element={<Navigate to="/home" replace />} />
57
57
+
<Route path="*" element={<Navigate to="/home" replace />} />
58
58
+
</Routes>
53
59
</div>
54
60
<Footer />
55
61
</div>
···
1
1
+
/* src/components/Admin/AdminPanel.css */
2
2
+
.admin-panel {
3
3
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
4
4
+
max-width: 1200px;
5
5
+
margin: 0 auto;
6
6
+
padding: 20px;
7
7
+
color: #333;
8
8
+
}
9
9
+
10
10
+
/* Header */
11
11
+
.admin-header {
12
12
+
display: flex;
13
13
+
justify-content: space-between;
14
14
+
align-items: center;
15
15
+
margin-bottom: 20px;
16
16
+
padding-bottom: 10px;
17
17
+
border-bottom: 1px solid #eee;
18
18
+
}
19
19
+
20
20
+
.admin-header h1 {
21
21
+
margin: 0;
22
22
+
color: #333;
23
23
+
font-size: 24px;
24
24
+
}
25
25
+
26
26
+
/* Container layout */
27
27
+
.admin-container {
28
28
+
display: grid;
29
29
+
grid-template-columns: 300px 1fr;
30
30
+
gap: 20px;
31
31
+
height: calc(100vh - 120px);
32
32
+
}
33
33
+
34
34
+
/* Resources sidebar */
35
35
+
.resources-sidebar {
36
36
+
background-color: #f8f9fa;
37
37
+
border-radius: 8px;
38
38
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
39
39
+
overflow: hidden;
40
40
+
display: flex;
41
41
+
flex-direction: column;
42
42
+
height: 100%;
43
43
+
}
44
44
+
45
45
+
.sidebar-header {
46
46
+
display: flex;
47
47
+
justify-content: space-between;
48
48
+
align-items: center;
49
49
+
padding: 15px;
50
50
+
background-color: #f0f2f5;
51
51
+
border-bottom: 1px solid #ddd;
52
52
+
}
53
53
+
54
54
+
.sidebar-header h2 {
55
55
+
margin: 0;
56
56
+
font-size: 18px;
57
57
+
}
58
58
+
59
59
+
.resources-list {
60
60
+
overflow-y: auto;
61
61
+
flex-grow: 1;
62
62
+
}
63
63
+
64
64
+
.resource-item {
65
65
+
display: flex;
66
66
+
justify-content: space-between;
67
67
+
align-items: center;
68
68
+
padding: 12px 15px;
69
69
+
border-bottom: 1px solid #eee;
70
70
+
cursor: pointer;
71
71
+
transition: background-color 0.2s;
72
72
+
}
73
73
+
74
74
+
.resource-item:hover {
75
75
+
background-color: #f0f2f5;
76
76
+
}
77
77
+
78
78
+
.resource-item.selected {
79
79
+
background-color: #e6f7ff;
80
80
+
border-left: 3px solid #1890ff;
81
81
+
}
82
82
+
83
83
+
.resource-item-name {
84
84
+
overflow: hidden;
85
85
+
text-overflow: ellipsis;
86
86
+
white-space: nowrap;
87
87
+
flex-grow: 1;
88
88
+
}
89
89
+
90
90
+
.resource-item-actions {
91
91
+
display: flex;
92
92
+
gap: 5px;
93
93
+
visibility: hidden;
94
94
+
}
95
95
+
96
96
+
.resource-item:hover .resource-item-actions {
97
97
+
visibility: visible;
98
98
+
}
99
99
+
100
100
+
/* Resource editor */
101
101
+
.resource-editor {
102
102
+
background-color: #fff;
103
103
+
border-radius: 8px;
104
104
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
105
105
+
padding: 20px;
106
106
+
overflow-y: auto;
107
107
+
height: 100%;
108
108
+
}
109
109
+
110
110
+
.resource-editor h2 {
111
111
+
margin-top: 0;
112
112
+
margin-bottom: 20px;
113
113
+
color: #333;
114
114
+
}
115
115
+
116
116
+
/* Form styling */
117
117
+
.form-row {
118
118
+
display: grid;
119
119
+
grid-template-columns: 1fr 1fr;
120
120
+
gap: 15px;
121
121
+
margin-bottom: 15px;
122
122
+
}
123
123
+
124
124
+
.form-group {
125
125
+
margin-bottom: 15px;
126
126
+
}
127
127
+
128
128
+
.form-group label {
129
129
+
display: block;
130
130
+
margin-bottom: 5px;
131
131
+
font-weight: 500;
132
132
+
color: #555;
133
133
+
}
134
134
+
135
135
+
.form-group input[type="text"],
136
136
+
.form-group input[type="url"],
137
137
+
.form-group input[type="number"],
138
138
+
.form-group textarea {
139
139
+
width: 100%;
140
140
+
padding: 10px;
141
141
+
border: 1px solid #ddd;
142
142
+
border-radius: 4px;
143
143
+
font-size: 14px;
144
144
+
}
145
145
+
146
146
+
.form-group textarea {
147
147
+
resize: vertical;
148
148
+
min-height: 100px;
149
149
+
}
150
150
+
151
151
+
.checkbox-group {
152
152
+
display: flex;
153
153
+
align-items: center;
154
154
+
gap: 8px;
155
155
+
}
156
156
+
157
157
+
.checkbox-group input[type="checkbox"] {
158
158
+
margin: 0;
159
159
+
}
160
160
+
161
161
+
.checkbox-group label {
162
162
+
margin-bottom: 0;
163
163
+
}
164
164
+
165
165
+
/* Categories and Tags sections */
166
166
+
.categories-section,
167
167
+
.tags-section {
168
168
+
background-color: #f8f9fa;
169
169
+
border-radius: 6px;
170
170
+
padding: 15px;
171
171
+
border: 1px solid #eee;
172
172
+
}
173
173
+
174
174
+
.section-header {
175
175
+
display: flex;
176
176
+
justify-content: space-between;
177
177
+
align-items: center;
178
178
+
margin-bottom: 10px;
179
179
+
}
180
180
+
181
181
+
.section-header label {
182
182
+
font-weight: 600;
183
183
+
margin-bottom: 0;
184
184
+
}
185
185
+
186
186
+
.checkbox-list {
187
187
+
max-height: 200px;
188
188
+
overflow-y: auto;
189
189
+
}
190
190
+
191
191
+
.checkbox-item {
192
192
+
display: flex;
193
193
+
align-items: center;
194
194
+
margin-bottom: 8px;
195
195
+
gap: 8px;
196
196
+
}
197
197
+
198
198
+
.checkbox-item label {
199
199
+
margin-bottom: 0;
200
200
+
font-weight: normal;
201
201
+
}
202
202
+
203
203
+
/* Form actions */
204
204
+
.form-actions {
205
205
+
display: flex;
206
206
+
justify-content: flex-end;
207
207
+
gap: 10px;
208
208
+
margin-top: 20px;
209
209
+
}
210
210
+
211
211
+
/* Buttons */
212
212
+
button {
213
213
+
cursor: pointer;
214
214
+
border: none;
215
215
+
border-radius: 4px;
216
216
+
font-size: 14px;
217
217
+
transition: background-color 0.2s, opacity 0.2s;
218
218
+
}
219
219
+
220
220
+
.add-new-button {
221
221
+
background-color: #1890ff;
222
222
+
color: white;
223
223
+
padding: 6px 12px;
224
224
+
font-size: 13px;
225
225
+
}
226
226
+
227
227
+
.add-new-button:hover {
228
228
+
background-color: #40a9ff;
229
229
+
}
230
230
+
231
231
+
.delete-button {
232
232
+
background: none;
233
233
+
padding: 3px 6px;
234
234
+
font-size: 16px;
235
235
+
opacity: 0.7;
236
236
+
}
237
237
+
238
238
+
.delete-button:hover {
239
239
+
opacity: 1;
240
240
+
background-color: #ffebee;
241
241
+
}
242
242
+
243
243
+
.add-item-button {
244
244
+
background-color: #f5f5f5;
245
245
+
color: #333;
246
246
+
padding: 4px 8px;
247
247
+
font-size: 12px;
248
248
+
border: 1px solid #ddd;
249
249
+
}
250
250
+
251
251
+
.add-item-button:hover {
252
252
+
background-color: #e0e0e0;
253
253
+
}
254
254
+
255
255
+
.save-button {
256
256
+
background-color: #52c41a;
257
257
+
color: white;
258
258
+
padding: 10px 20px;
259
259
+
}
260
260
+
261
261
+
.save-button:hover {
262
262
+
background-color: #73d13d;
263
263
+
}
264
264
+
265
265
+
.cancel-button {
266
266
+
background-color: #f5f5f5;
267
267
+
color: #333;
268
268
+
padding: 10px 20px;
269
269
+
border: 1px solid #d9d9d9;
270
270
+
}
271
271
+
272
272
+
.cancel-button:hover {
273
273
+
background-color: #e6e6e6;
274
274
+
}
275
275
+
276
276
+
.logout-button {
277
277
+
background-color: #f5f5f5;
278
278
+
color: #333;
279
279
+
padding: 8px 16px;
280
280
+
border: 1px solid #d9d9d9;
281
281
+
}
282
282
+
283
283
+
.logout-button:hover {
284
284
+
background-color: #e6e6e6;
285
285
+
}
286
286
+
287
287
+
/* Alert messages */
288
288
+
.alert {
289
289
+
padding: 12px 15px;
290
290
+
margin-bottom: 20px;
291
291
+
border-radius: 4px;
292
292
+
font-weight: 500;
293
293
+
}
294
294
+
295
295
+
.alert.success {
296
296
+
background-color: #f6ffed;
297
297
+
border: 1px solid #b7eb8f;
298
298
+
color: #52c41a;
299
299
+
}
300
300
+
301
301
+
.alert.error {
302
302
+
background-color: #fff2f0;
303
303
+
border: 1px solid #ffccc7;
304
304
+
color: #f5222d;
305
305
+
}
306
306
+
307
307
+
/* Loading spinner */
308
308
+
.admin-loading {
309
309
+
display: flex;
310
310
+
flex-direction: column;
311
311
+
align-items: center;
312
312
+
justify-content: center;
313
313
+
height: 100vh;
314
314
+
}
315
315
+
316
316
+
.loading-spinner {
317
317
+
border: 4px solid #f3f3f3;
318
318
+
border-top: 4px solid #1890ff;
319
319
+
border-radius: 50%;
320
320
+
width: 40px;
321
321
+
height: 40px;
322
322
+
animation: spin 1s linear infinite;
323
323
+
margin-bottom: 15px;
324
324
+
}
325
325
+
326
326
+
@keyframes spin {
327
327
+
0% { transform: rotate(0deg); }
328
328
+
100% { transform: rotate(360deg); }
329
329
+
}
330
330
+
331
331
+
/* Login form */
332
332
+
.admin-login-container {
333
333
+
display: flex;
334
334
+
justify-content: center;
335
335
+
align-items: center;
336
336
+
height: 100vh;
337
337
+
background-color: #f0f2f5;
338
338
+
}
339
339
+
340
340
+
.admin-login-card {
341
341
+
background-color: white;
342
342
+
border-radius: 8px;
343
343
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
344
344
+
padding: 30px;
345
345
+
width: 100%;
346
346
+
max-width: 400px;
347
347
+
}
348
348
+
349
349
+
.admin-login-card h2 {
350
350
+
margin-top: 0;
351
351
+
margin-bottom: 20px;
352
352
+
text-align: center;
353
353
+
color: #333;
354
354
+
}
355
355
+
356
356
+
.login-button {
357
357
+
background-color: #1890ff;
358
358
+
color: white;
359
359
+
padding: 10px 0;
360
360
+
width: 100%;
361
361
+
font-size: 16px;
362
362
+
margin-top: 10px;
363
363
+
}
364
364
+
365
365
+
.login-button:hover {
366
366
+
background-color: #40a9ff;
367
367
+
}
368
368
+
369
369
+
.auth-error {
370
370
+
background-color: #fff2f0;
371
371
+
border: 1px solid #ffccc7;
372
372
+
color: #f5222d;
373
373
+
padding: 10px;
374
374
+
border-radius: 4px;
375
375
+
margin-bottom: 15px;
376
376
+
font-size: 14px;
377
377
+
}
378
378
+
379
379
+
/* Accessibility */
380
380
+
.sr-only {
381
381
+
position: absolute;
382
382
+
width: 1px;
383
383
+
height: 1px;
384
384
+
padding: 0;
385
385
+
margin: -1px;
386
386
+
overflow: hidden;
387
387
+
clip: rect(0, 0, 0, 0);
388
388
+
white-space: nowrap;
389
389
+
border-width: 0;
390
390
+
}
391
391
+
392
392
+
/* Responsive design */
393
393
+
@media (max-width: 768px) {
394
394
+
.admin-container {
395
395
+
grid-template-columns: 1fr;
396
396
+
}
397
397
+
398
398
+
.form-row {
399
399
+
grid-template-columns: 1fr;
400
400
+
}
401
401
+
402
402
+
.resources-sidebar {
403
403
+
height: 300px;
404
404
+
}
405
405
+
}
···
1
1
+
// src/components/Admin/AdminPanel.jsx
2
2
+
import React, { useState, useEffect } from 'react';
3
3
+
import { supabase } from '../../lib/supabase';
4
4
+
import './AdminPanel.css';
5
5
+
6
6
+
const AdminPanel = () => {
7
7
+
// State management
8
8
+
const [resources, setResources] = useState([]);
9
9
+
const [categories, setCategories] = useState([]);
10
10
+
const [tags, setTags] = useState([]);
11
11
+
const [selectedResource, setSelectedResource] = useState(null);
12
12
+
const [isLoading, setIsLoading] = useState(true);
13
13
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
14
14
+
const [authError, setAuthError] = useState(null);
15
15
+
16
16
+
// Login state
17
17
+
const [email, setEmail] = useState('');
18
18
+
const [password, setPassword] = useState('');
19
19
+
20
20
+
// New/Edit resource form state
21
21
+
const [formData, setFormData] = useState({
22
22
+
name: '',
23
23
+
description: '',
24
24
+
url: '',
25
25
+
domain: '',
26
26
+
featured: false,
27
27
+
position: 0,
28
28
+
selectedCategories: [],
29
29
+
selectedTags: []
30
30
+
});
31
31
+
32
32
+
// Alert state
33
33
+
const [alert, setAlert] = useState({ show: false, message: '', type: '' });
34
34
+
35
35
+
// Check authentication on mount
36
36
+
useEffect(() => {
37
37
+
const checkAuth = async () => {
38
38
+
const { data: { session } } = await supabase.auth.getSession();
39
39
+
setIsAuthenticated(!!session);
40
40
+
41
41
+
if (session) {
42
42
+
fetchAllData();
43
43
+
} else {
44
44
+
setIsLoading(false);
45
45
+
}
46
46
+
};
47
47
+
48
48
+
checkAuth();
49
49
+
}, []);
50
50
+
51
51
+
// Login handler
52
52
+
const handleLogin = async (e) => {
53
53
+
e.preventDefault();
54
54
+
setIsLoading(true);
55
55
+
setAuthError(null);
56
56
+
57
57
+
try {
58
58
+
const { data, error } = await supabase.auth.signInWithPassword({
59
59
+
email,
60
60
+
password
61
61
+
});
62
62
+
63
63
+
if (error) throw error;
64
64
+
65
65
+
setIsAuthenticated(true);
66
66
+
fetchAllData();
67
67
+
} catch (error) {
68
68
+
console.error('Error logging in:', error);
69
69
+
setAuthError(error.message);
70
70
+
setIsLoading(false);
71
71
+
}
72
72
+
};
73
73
+
74
74
+
// Logout handler
75
75
+
const handleLogout = async () => {
76
76
+
await supabase.auth.signOut();
77
77
+
setIsAuthenticated(false);
78
78
+
};
79
79
+
80
80
+
// Fetch all required data from Supabase
81
81
+
const fetchAllData = async () => {
82
82
+
setIsLoading(true);
83
83
+
try {
84
84
+
// Fetch resources
85
85
+
const { data: resourcesData, error: resourcesError } = await supabase
86
86
+
.from('resources')
87
87
+
.select('*')
88
88
+
.order('position');
89
89
+
90
90
+
if (resourcesError) throw resourcesError;
91
91
+
92
92
+
// Fetch categories
93
93
+
const { data: categoriesData, error: categoriesError } = await supabase
94
94
+
.from('categories')
95
95
+
.select('*')
96
96
+
.order('name');
97
97
+
98
98
+
if (categoriesError) throw categoriesError;
99
99
+
100
100
+
// Fetch tags
101
101
+
const { data: tagsData, error: tagsError } = await supabase
102
102
+
.from('tags')
103
103
+
.select('*')
104
104
+
.order('name');
105
105
+
106
106
+
if (tagsError) throw tagsError;
107
107
+
108
108
+
// Fetch resource-category associations
109
109
+
const { data: resourceCategories, error: rcError } = await supabase
110
110
+
.from('resource_categories')
111
111
+
.select('*');
112
112
+
113
113
+
if (rcError) throw rcError;
114
114
+
115
115
+
// Fetch resource-tag associations
116
116
+
const { data: resourceTags, error: rtError } = await supabase
117
117
+
.from('resource_tags')
118
118
+
.select('*');
119
119
+
120
120
+
if (rtError) throw rtError;
121
121
+
122
122
+
// Enhance resources with their associated categories and tags
123
123
+
const enhancedResources = resourcesData.map(resource => {
124
124
+
const resourceCats = resourceCategories
125
125
+
.filter(rc => rc.resource_id === resource.id)
126
126
+
.map(rc => rc.category_id);
127
127
+
128
128
+
const resourceTs = resourceTags
129
129
+
.filter(rt => rt.resource_id === resource.id)
130
130
+
.map(rt => rt.tag_id);
131
131
+
132
132
+
return {
133
133
+
...resource,
134
134
+
categoryIds: resourceCats,
135
135
+
tagIds: resourceTs
136
136
+
};
137
137
+
});
138
138
+
139
139
+
// Update state
140
140
+
setResources(enhancedResources);
141
141
+
setCategories(categoriesData);
142
142
+
setTags(tagsData);
143
143
+
} catch (error) {
144
144
+
console.error('Error fetching data:', error);
145
145
+
showAlert(`Error: ${error.message}`, 'error');
146
146
+
} finally {
147
147
+
setIsLoading(false);
148
148
+
}
149
149
+
};
150
150
+
151
151
+
// Handle resource selection
152
152
+
const handleSelectResource = (resource) => {
153
153
+
setSelectedResource(resource);
154
154
+
setFormData({
155
155
+
name: resource.name || '',
156
156
+
description: resource.description || '',
157
157
+
url: resource.url || '',
158
158
+
domain: resource.domain || '',
159
159
+
featured: resource.featured || false,
160
160
+
position: resource.position || 0,
161
161
+
selectedCategories: resource.categoryIds || [],
162
162
+
selectedTags: resource.tagIds || []
163
163
+
});
164
164
+
};
165
165
+
166
166
+
// Handle form input changes
167
167
+
const handleInputChange = (e) => {
168
168
+
const { name, value, type, checked } = e.target;
169
169
+
setFormData({
170
170
+
...formData,
171
171
+
[name]: type === 'checkbox' ? checked : value
172
172
+
});
173
173
+
};
174
174
+
175
175
+
// Handle category selection changes
176
176
+
const handleCategoryChange = (categoryId) => {
177
177
+
setFormData(prevData => {
178
178
+
const selectedCategories = [...prevData.selectedCategories];
179
179
+
180
180
+
if (selectedCategories.includes(categoryId)) {
181
181
+
// Remove category if already selected
182
182
+
return {
183
183
+
...prevData,
184
184
+
selectedCategories: selectedCategories.filter(id => id !== categoryId)
185
185
+
};
186
186
+
} else {
187
187
+
// Add category if not already selected
188
188
+
return {
189
189
+
...prevData,
190
190
+
selectedCategories: [...selectedCategories, categoryId]
191
191
+
};
192
192
+
}
193
193
+
});
194
194
+
};
195
195
+
196
196
+
// Handle tag selection changes
197
197
+
const handleTagChange = (tagId) => {
198
198
+
setFormData(prevData => {
199
199
+
const selectedTags = [...prevData.selectedTags];
200
200
+
201
201
+
if (selectedTags.includes(tagId)) {
202
202
+
// Remove tag if already selected
203
203
+
return {
204
204
+
...prevData,
205
205
+
selectedTags: selectedTags.filter(id => id !== tagId)
206
206
+
};
207
207
+
} else {
208
208
+
// Add tag if not already selected
209
209
+
return {
210
210
+
...prevData,
211
211
+
selectedTags: [...selectedTags, tagId]
212
212
+
};
213
213
+
}
214
214
+
});
215
215
+
};
216
216
+
217
217
+
// Clear form and selected resource
218
218
+
const handleClearForm = () => {
219
219
+
setSelectedResource(null);
220
220
+
setFormData({
221
221
+
name: '',
222
222
+
description: '',
223
223
+
url: '',
224
224
+
domain: '',
225
225
+
featured: false,
226
226
+
position: resources.length + 1,
227
227
+
selectedCategories: [],
228
228
+
selectedTags: []
229
229
+
});
230
230
+
};
231
231
+
232
232
+
// Show alert message
233
233
+
const showAlert = (message, type = 'success') => {
234
234
+
setAlert({ show: true, message, type });
235
235
+
setTimeout(() => {
236
236
+
setAlert({ show: false, message: '', type: '' });
237
237
+
}, 5000);
238
238
+
};
239
239
+
240
240
+
// Save resource changes
241
241
+
const handleSaveResource = async (e) => {
242
242
+
e.preventDefault();
243
243
+
setIsLoading(true);
244
244
+
245
245
+
try {
246
246
+
const resourceData = {
247
247
+
name: formData.name,
248
248
+
description: formData.description,
249
249
+
url: formData.url,
250
250
+
domain: formData.domain,
251
251
+
featured: formData.featured,
252
252
+
position: formData.position,
253
253
+
updated_at: new Date().toISOString()
254
254
+
};
255
255
+
256
256
+
let resourceId;
257
257
+
258
258
+
if (selectedResource) {
259
259
+
// Update existing resource
260
260
+
const { error } = await supabase
261
261
+
.from('resources')
262
262
+
.update(resourceData)
263
263
+
.eq('id', selectedResource.id);
264
264
+
265
265
+
if (error) throw error;
266
266
+
resourceId = selectedResource.id;
267
267
+
268
268
+
// Delete existing category and tag associations
269
269
+
await supabase
270
270
+
.from('resource_categories')
271
271
+
.delete()
272
272
+
.eq('resource_id', resourceId);
273
273
+
274
274
+
await supabase
275
275
+
.from('resource_tags')
276
276
+
.delete()
277
277
+
.eq('resource_id', resourceId);
278
278
+
279
279
+
showAlert(`Resource "${formData.name}" updated successfully!`);
280
280
+
} else {
281
281
+
// Add new resource
282
282
+
resourceData.created_at = new Date().toISOString();
283
283
+
284
284
+
const { data, error } = await supabase
285
285
+
.from('resources')
286
286
+
.insert(resourceData)
287
287
+
.select();
288
288
+
289
289
+
if (error) throw error;
290
290
+
resourceId = data[0].id;
291
291
+
292
292
+
showAlert(`Resource "${formData.name}" created successfully!`);
293
293
+
}
294
294
+
295
295
+
// Add category associations
296
296
+
if (formData.selectedCategories.length > 0) {
297
297
+
const categoryAssociations = formData.selectedCategories.map(categoryId => ({
298
298
+
resource_id: resourceId,
299
299
+
category_id: categoryId
300
300
+
}));
301
301
+
302
302
+
const { error: categoryError } = await supabase
303
303
+
.from('resource_categories')
304
304
+
.insert(categoryAssociations);
305
305
+
306
306
+
if (categoryError) throw categoryError;
307
307
+
}
308
308
+
309
309
+
// Add tag associations
310
310
+
if (formData.selectedTags.length > 0) {
311
311
+
const tagAssociations = formData.selectedTags.map(tagId => ({
312
312
+
resource_id: resourceId,
313
313
+
tag_id: tagId
314
314
+
}));
315
315
+
316
316
+
const { error: tagError } = await supabase
317
317
+
.from('resource_tags')
318
318
+
.insert(tagAssociations);
319
319
+
320
320
+
if (tagError) throw tagError;
321
321
+
}
322
322
+
323
323
+
// Refresh data
324
324
+
fetchAllData();
325
325
+
handleClearForm();
326
326
+
} catch (error) {
327
327
+
console.error('Error saving resource:', error);
328
328
+
showAlert(`Error: ${error.message}`, 'error');
329
329
+
} finally {
330
330
+
setIsLoading(false);
331
331
+
}
332
332
+
};
333
333
+
334
334
+
// Delete resource
335
335
+
const handleDeleteResource = async (resourceId, resourceName) => {
336
336
+
if (!window.confirm(`Are you sure you want to delete "${resourceName}"?`)) {
337
337
+
return;
338
338
+
}
339
339
+
340
340
+
setIsLoading(true);
341
341
+
342
342
+
try {
343
343
+
// Delete associated records first (foreign key constraints)
344
344
+
await supabase
345
345
+
.from('resource_categories')
346
346
+
.delete()
347
347
+
.eq('resource_id', resourceId);
348
348
+
349
349
+
await supabase
350
350
+
.from('resource_tags')
351
351
+
.delete()
352
352
+
.eq('resource_id', resourceId);
353
353
+
354
354
+
// Delete the resource
355
355
+
const { error } = await supabase
356
356
+
.from('resources')
357
357
+
.delete()
358
358
+
.eq('id', resourceId);
359
359
+
360
360
+
if (error) throw error;
361
361
+
362
362
+
showAlert(`Resource "${resourceName}" deleted successfully!`);
363
363
+
364
364
+
// Refresh data
365
365
+
fetchAllData();
366
366
+
367
367
+
// Clear form if the deleted resource was selected
368
368
+
if (selectedResource && selectedResource.id === resourceId) {
369
369
+
handleClearForm();
370
370
+
}
371
371
+
} catch (error) {
372
372
+
console.error('Error deleting resource:', error);
373
373
+
showAlert(`Error: ${error.message}`, 'error');
374
374
+
} finally {
375
375
+
setIsLoading(false);
376
376
+
}
377
377
+
};
378
378
+
379
379
+
// Create new category
380
380
+
const handleCreateCategory = async () => {
381
381
+
const categoryName = prompt('Enter the new category name:');
382
382
+
if (!categoryName) return;
383
383
+
384
384
+
const emoji = prompt('Enter an emoji for this category:');
385
385
+
if (!emoji) return;
386
386
+
387
387
+
setIsLoading(true);
388
388
+
389
389
+
try {
390
390
+
const { data, error } = await supabase
391
391
+
.from('categories')
392
392
+
.insert({
393
393
+
name: categoryName,
394
394
+
emoji: emoji,
395
395
+
created_at: new Date().toISOString()
396
396
+
})
397
397
+
.select();
398
398
+
399
399
+
if (error) throw error;
400
400
+
401
401
+
showAlert(`Category "${categoryName}" created successfully!`);
402
402
+
fetchAllData();
403
403
+
} catch (error) {
404
404
+
console.error('Error creating category:', error);
405
405
+
showAlert(`Error: ${error.message}`, 'error');
406
406
+
} finally {
407
407
+
setIsLoading(false);
408
408
+
}
409
409
+
};
410
410
+
411
411
+
// Create new tag
412
412
+
const handleCreateTag = async () => {
413
413
+
const tagName = prompt('Enter the new tag name:');
414
414
+
if (!tagName) return;
415
415
+
416
416
+
setIsLoading(true);
417
417
+
418
418
+
try {
419
419
+
const { data, error } = await supabase
420
420
+
.from('tags')
421
421
+
.insert({
422
422
+
name: tagName,
423
423
+
created_at: new Date().toISOString()
424
424
+
})
425
425
+
.select();
426
426
+
427
427
+
if (error) throw error;
428
428
+
429
429
+
showAlert(`Tag "${tagName}" created successfully!`);
430
430
+
fetchAllData();
431
431
+
} catch (error) {
432
432
+
console.error('Error creating tag:', error);
433
433
+
showAlert(`Error: ${error.message}`, 'error');
434
434
+
} finally {
435
435
+
setIsLoading(false);
436
436
+
}
437
437
+
};
438
438
+
439
439
+
// Render loading spinner
440
440
+
if (isLoading) {
441
441
+
return (
442
442
+
<div className="admin-loading">
443
443
+
<div className="loading-spinner"></div>
444
444
+
<p>Loading...</p>
445
445
+
</div>
446
446
+
);
447
447
+
}
448
448
+
449
449
+
// Render login form if not authenticated
450
450
+
if (!isAuthenticated) {
451
451
+
return (
452
452
+
<div className="admin-login-container">
453
453
+
<div className="admin-login-card">
454
454
+
<h2>Admin Login</h2>
455
455
+
{authError && <div className="auth-error">{authError}</div>}
456
456
+
<form onSubmit={handleLogin}>
457
457
+
<div className="form-group">
458
458
+
<label htmlFor="email">Email</label>
459
459
+
<input
460
460
+
type="email"
461
461
+
id="email"
462
462
+
value={email}
463
463
+
onChange={(e) => setEmail(e.target.value)}
464
464
+
required
465
465
+
/>
466
466
+
</div>
467
467
+
<div className="form-group">
468
468
+
<label htmlFor="password">Password</label>
469
469
+
<input
470
470
+
type="password"
471
471
+
id="password"
472
472
+
value={password}
473
473
+
onChange={(e) => setPassword(e.target.value)}
474
474
+
required
475
475
+
/>
476
476
+
</div>
477
477
+
<button type="submit" className="login-button">Login</button>
478
478
+
</form>
479
479
+
</div>
480
480
+
</div>
481
481
+
);
482
482
+
}
483
483
+
484
484
+
// Main admin panel UI
485
485
+
return (
486
486
+
<div className="admin-panel">
487
487
+
{/* Header */}
488
488
+
<header className="admin-header">
489
489
+
<h1>Resources Admin Panel</h1>
490
490
+
<button onClick={handleLogout} className="logout-button">Logout</button>
491
491
+
</header>
492
492
+
493
493
+
{/* Alert message */}
494
494
+
{alert.show && (
495
495
+
<div className={`alert ${alert.type}`}>
496
496
+
{alert.message}
497
497
+
</div>
498
498
+
)}
499
499
+
500
500
+
<div className="admin-container">
501
501
+
{/* Resources list sidebar */}
502
502
+
<div className="resources-sidebar">
503
503
+
<div className="sidebar-header">
504
504
+
<h2>Resources</h2>
505
505
+
<button onClick={handleClearForm} className="add-new-button">
506
506
+
+ New Resource
507
507
+
</button>
508
508
+
</div>
509
509
+
<div className="resources-list">
510
510
+
{resources.map(resource => (
511
511
+
<div
512
512
+
key={resource.id}
513
513
+
className={`resource-item ${selectedResource && selectedResource.id === resource.id ? 'selected' : ''}`}
514
514
+
onClick={() => handleSelectResource(resource)}
515
515
+
>
516
516
+
<div className="resource-item-name">{resource.name}</div>
517
517
+
<div className="resource-item-actions">
518
518
+
<button
519
519
+
onClick={(e) => {
520
520
+
e.stopPropagation();
521
521
+
handleDeleteResource(resource.id, resource.name);
522
522
+
}}
523
523
+
className="delete-button"
524
524
+
title="Delete resource"
525
525
+
>
526
526
+
🗑️
527
527
+
</button>
528
528
+
</div>
529
529
+
</div>
530
530
+
))}
531
531
+
</div>
532
532
+
</div>
533
533
+
534
534
+
{/* Resource edit form */}
535
535
+
<div className="resource-editor">
536
536
+
<h2>{selectedResource ? 'Edit Resource' : 'Add New Resource'}</h2>
537
537
+
<form onSubmit={handleSaveResource}>
538
538
+
<div className="form-row">
539
539
+
<div className="form-group">
540
540
+
<label htmlFor="name">Name *</label>
541
541
+
<input
542
542
+
type="text"
543
543
+
id="name"
544
544
+
name="name"
545
545
+
value={formData.name}
546
546
+
onChange={handleInputChange}
547
547
+
required
548
548
+
/>
549
549
+
</div>
550
550
+
<div className="form-group">
551
551
+
<label htmlFor="domain">Domain</label>
552
552
+
<input
553
553
+
type="text"
554
554
+
id="domain"
555
555
+
name="domain"
556
556
+
value={formData.domain}
557
557
+
onChange={handleInputChange}
558
558
+
/>
559
559
+
</div>
560
560
+
</div>
561
561
+
562
562
+
<div className="form-group">
563
563
+
<label htmlFor="url">URL *</label>
564
564
+
<input
565
565
+
type="url"
566
566
+
id="url"
567
567
+
name="url"
568
568
+
value={formData.url}
569
569
+
onChange={handleInputChange}
570
570
+
required
571
571
+
/>
572
572
+
</div>
573
573
+
574
574
+
<div className="form-group">
575
575
+
<label htmlFor="description">Description *</label>
576
576
+
<textarea
577
577
+
id="description"
578
578
+
name="description"
579
579
+
value={formData.description}
580
580
+
onChange={handleInputChange}
581
581
+
rows="4"
582
582
+
required
583
583
+
></textarea>
584
584
+
</div>
585
585
+
586
586
+
<div className="form-row">
587
587
+
<div className="form-group">
588
588
+
<label htmlFor="position">Position</label>
589
589
+
<input
590
590
+
type="number"
591
591
+
id="position"
592
592
+
name="position"
593
593
+
value={formData.position}
594
594
+
onChange={handleInputChange}
595
595
+
min="0"
596
596
+
/>
597
597
+
</div>
598
598
+
<div className="form-group checkbox-group">
599
599
+
<input
600
600
+
type="checkbox"
601
601
+
id="featured"
602
602
+
name="featured"
603
603
+
checked={formData.featured}
604
604
+
onChange={handleInputChange}
605
605
+
/>
606
606
+
<label htmlFor="featured">Featured Resource</label>
607
607
+
</div>
608
608
+
</div>
609
609
+
610
610
+
<div className="form-row">
611
611
+
{/* Categories selection */}
612
612
+
<div className="form-group categories-section">
613
613
+
<div className="section-header">
614
614
+
<label>Categories</label>
615
615
+
<button
616
616
+
type="button"
617
617
+
onClick={handleCreateCategory}
618
618
+
className="add-item-button"
619
619
+
>
620
620
+
+ Add Category
621
621
+
</button>
622
622
+
</div>
623
623
+
<div className="checkbox-list">
624
624
+
{categories.map(category => (
625
625
+
<div key={category.id} className="checkbox-item">
626
626
+
<input
627
627
+
type="checkbox"
628
628
+
id={`category-${category.id}`}
629
629
+
checked={formData.selectedCategories.includes(category.id)}
630
630
+
onChange={() => handleCategoryChange(category.id)}
631
631
+
/>
632
632
+
<label htmlFor={`category-${category.id}`}>
633
633
+
{category.emoji} {category.name}
634
634
+
</label>
635
635
+
</div>
636
636
+
))}
637
637
+
</div>
638
638
+
</div>
639
639
+
640
640
+
{/* Tags selection */}
641
641
+
<div className="form-group tags-section">
642
642
+
<div className="section-header">
643
643
+
<label>Tags</label>
644
644
+
<button
645
645
+
type="button"
646
646
+
onClick={handleCreateTag}
647
647
+
className="add-item-button"
648
648
+
>
649
649
+
+ Add Tag
650
650
+
</button>
651
651
+
</div>
652
652
+
<div className="checkbox-list">
653
653
+
{tags.map(tag => (
654
654
+
<div key={tag.id} className="checkbox-item">
655
655
+
<input
656
656
+
type="checkbox"
657
657
+
id={`tag-${tag.id}`}
658
658
+
checked={formData.selectedTags.includes(tag.id)}
659
659
+
onChange={() => handleTagChange(tag.id)}
660
660
+
/>
661
661
+
<label htmlFor={`tag-${tag.id}`}>
662
662
+
#{tag.name}
663
663
+
</label>
664
664
+
</div>
665
665
+
))}
666
666
+
</div>
667
667
+
</div>
668
668
+
</div>
669
669
+
670
670
+
<div className="form-actions">
671
671
+
<button type="button" onClick={handleClearForm} className="cancel-button">
672
672
+
Cancel
673
673
+
</button>
674
674
+
<button type="submit" className="save-button">
675
675
+
{selectedResource ? 'Update Resource' : 'Create Resource'}
676
676
+
</button>
677
677
+
</div>
678
678
+
</form>
679
679
+
</div>
680
680
+
</div>
681
681
+
</div>
682
682
+
);
683
683
+
};
684
684
+
685
685
+
export default AdminPanel;
···
1
1
+
// src/components/Admin/AdminRoute.jsx
2
2
+
import React, { useState, useEffect } from 'react';
3
3
+
import { Navigate } from 'react-router-dom';
4
4
+
import { supabase } from '../../lib/supabase';
5
5
+
import AdminPanel from './AdminPanel';
6
6
+
7
7
+
// This component protects the admin route by checking authentication
8
8
+
const AdminRoute = () => {
9
9
+
const [isAuthenticated, setIsAuthenticated] = useState(null);
10
10
+
const [isLoading, setIsLoading] = useState(true);
11
11
+
12
12
+
useEffect(() => {
13
13
+
const checkAuth = async () => {
14
14
+
try {
15
15
+
const { data: { session } } = await supabase.auth.getSession();
16
16
+
setIsAuthenticated(!!session);
17
17
+
} catch (error) {
18
18
+
console.error('Error checking auth status:', error);
19
19
+
setIsAuthenticated(false);
20
20
+
} finally {
21
21
+
setIsLoading(false);
22
22
+
}
23
23
+
};
24
24
+
25
25
+
checkAuth();
26
26
+
27
27
+
// Listen for auth changes
28
28
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(
29
29
+
(_event, session) => {
30
30
+
setIsAuthenticated(!!session);
31
31
+
}
32
32
+
);
33
33
+
34
34
+
return () => {
35
35
+
subscription.unsubscribe();
36
36
+
};
37
37
+
}, []);
38
38
+
39
39
+
if (isLoading) {
40
40
+
return (
41
41
+
<div className="admin-loading">
42
42
+
<div className="loading-spinner"></div>
43
43
+
<p>Loading...</p>
44
44
+
</div>
45
45
+
);
46
46
+
}
47
47
+
48
48
+
// If not authenticated, redirect to home page
49
49
+
if (isAuthenticated === false) {
50
50
+
return <Navigate to="/" replace />;
51
51
+
}
52
52
+
53
53
+
// If authenticated, render the admin panel
54
54
+
return <AdminPanel />;
55
55
+
};
56
56
+
57
57
+
export default AdminRoute;