src
components
Admin
Resources
UserProfile
···
56
56
font-size: 18px;
57
57
}
58
58
59
59
+
.sidebar-filters {
60
60
+
padding: 10px 15px;
61
61
+
border-bottom: 1px solid #ddd;
62
62
+
background-color: #f8f9fa;
63
63
+
}
64
64
+
65
65
+
.filter-group {
66
66
+
margin-bottom: 8px;
67
67
+
}
68
68
+
69
69
+
.search-input {
70
70
+
width: 100%;
71
71
+
padding: 8px;
72
72
+
border: 1px solid #ddd;
73
73
+
border-radius: 4px;
74
74
+
font-size: 14px;
75
75
+
margin-bottom: 8px;
76
76
+
}
77
77
+
78
78
+
.status-filter,
79
79
+
.completeness-filter {
80
80
+
width: 49%;
81
81
+
padding: 6px;
82
82
+
border: 1px solid #ddd;
83
83
+
border-radius: 4px;
84
84
+
font-size: 13px;
85
85
+
margin-right: 2%;
86
86
+
}
87
87
+
88
88
+
.completeness-filter {
89
89
+
margin-right: 0;
90
90
+
}
91
91
+
59
92
.resources-list {
60
93
overflow-y: auto;
61
94
flex-grow: 1;
···
63
96
64
97
.resource-item {
65
98
display: flex;
66
66
-
justify-content: space-between;
67
67
-
align-items: center;
68
68
-
padding: 12px 15px;
99
99
+
flex-direction: column;
100
100
+
padding: 0;
69
101
border-bottom: 1px solid #eee;
70
102
cursor: pointer;
71
103
transition: background-color 0.2s;
104
104
+
position: relative;
105
105
+
}
106
106
+
107
107
+
.resource-completeness-indicator {
108
108
+
height: 3px;
109
109
+
width: 100%;
110
110
+
background-color: #f0f0f0;
111
111
+
}
112
112
+
113
113
+
.completeness-bar {
114
114
+
height: 100%;
115
115
+
background-color: #52c41a;
116
116
+
transition: width 0.3s;
117
117
+
}
118
118
+
119
119
+
.resource-item-content {
120
120
+
display: flex;
121
121
+
justify-content: space-between;
122
122
+
padding: 12px 15px;
72
123
}
73
124
74
125
.resource-item:hover {
···
78
129
.resource-item.selected {
79
130
background-color: #e6f7ff;
80
131
border-left: 3px solid #1890ff;
132
132
+
}
133
133
+
134
134
+
.resource-item.status-draft .completeness-bar {
135
135
+
background-color: #faad14;
136
136
+
}
137
137
+
138
138
+
.resource-item.status-review .completeness-bar {
139
139
+
background-color: #1890ff;
140
140
+
}
141
141
+
142
142
+
.resource-item.status-published .completeness-bar {
143
143
+
background-color: #52c41a;
81
144
}
82
145
83
146
.resource-item-name {
···
87
150
flex-grow: 1;
88
151
}
89
152
153
153
+
.resource-item-meta {
154
154
+
display: flex;
155
155
+
gap: 5px;
156
156
+
font-size: 12px;
157
157
+
margin-top: 4px;
158
158
+
}
159
159
+
160
160
+
.status-badge {
161
161
+
padding: 2px 6px;
162
162
+
border-radius: 10px;
163
163
+
font-size: 11px;
164
164
+
text-transform: uppercase;
165
165
+
font-weight: bold;
166
166
+
}
167
167
+
168
168
+
.status-badge.status-draft {
169
169
+
background-color: #fff7e6;
170
170
+
color: #faad14;
171
171
+
border: 1px solid #faad14;
172
172
+
}
173
173
+
174
174
+
.status-badge.status-review {
175
175
+
background-color: #e6f7ff;
176
176
+
color: #1890ff;
177
177
+
border: 1px solid #1890ff;
178
178
+
}
179
179
+
180
180
+
.status-badge.status-published {
181
181
+
background-color: #f6ffed;
182
182
+
color: #52c41a;
183
183
+
border: 1px solid #52c41a;
184
184
+
}
185
185
+
186
186
+
.featured-badge {
187
187
+
background-color: #f9f0ff;
188
188
+
color: #722ed1;
189
189
+
border: 1px solid #722ed1;
190
190
+
padding: 2px 6px;
191
191
+
border-radius: 10px;
192
192
+
font-size: 11px;
193
193
+
text-transform: uppercase;
194
194
+
font-weight: bold;
195
195
+
}
196
196
+
90
197
.resource-item-actions {
91
198
display: flex;
92
199
gap: 5px;
···
105
212
padding: 20px;
106
213
overflow-y: auto;
107
214
height: 100%;
215
215
+
}
216
216
+
217
217
+
.editor-header {
218
218
+
display: flex;
219
219
+
justify-content: space-between;
220
220
+
align-items: center;
221
221
+
margin-bottom: 20px;
222
222
+
padding-bottom: 15px;
223
223
+
border-bottom: 1px solid #eee;
224
224
+
}
225
225
+
226
226
+
.editor-header h2 {
227
227
+
margin: 0;
228
228
+
color: #333;
229
229
+
}
230
230
+
231
231
+
.floating-actions {
232
232
+
display: flex;
233
233
+
align-items: center;
234
234
+
gap: 15px;
235
235
+
}
236
236
+
237
237
+
.floating-save-button {
238
238
+
background-color: #52c41a;
239
239
+
color: white;
240
240
+
padding: 8px 16px;
241
241
+
border-radius: 4px;
242
242
+
font-weight: 500;
243
243
+
}
244
244
+
245
245
+
.floating-save-button:hover {
246
246
+
background-color: #73d13d;
247
247
+
}
248
248
+
249
249
+
.status-selector {
250
250
+
display: flex;
251
251
+
align-items: center;
252
252
+
gap: 10px;
253
253
+
}
254
254
+
255
255
+
.status-selector span {
256
256
+
font-weight: 500;
257
257
+
color: #555;
258
258
+
}
259
259
+
260
260
+
.status-buttons {
261
261
+
display: flex;
262
262
+
border: 1px solid #ddd;
263
263
+
border-radius: 4px;
264
264
+
overflow: hidden;
265
265
+
}
266
266
+
267
267
+
.status-button {
268
268
+
padding: 6px 10px;
269
269
+
background-color: #f5f5f5;
270
270
+
border: none;
271
271
+
border-right: 1px solid #ddd;
272
272
+
cursor: pointer;
273
273
+
font-size: 13px;
274
274
+
}
275
275
+
276
276
+
.status-button:last-child {
277
277
+
border-right: none;
278
278
+
}
279
279
+
280
280
+
.status-button.active {
281
281
+
background-color: #1890ff;
282
282
+
color: white;
108
283
}
109
284
110
285
.resource-editor h2 {
···
1
1
// src/components/Admin/AdminPanel.jsx
2
2
-
import React, { useState, useEffect } from 'react';
2
2
+
import React, { useState, useEffect, useCallback } from 'react';
3
3
import { supabase } from '../../lib/supabase';
4
4
import './AdminPanel.css';
5
5
···
12
12
const [isLoading, setIsLoading] = useState(true);
13
13
const [isAuthenticated, setIsAuthenticated] = useState(false);
14
14
const [authError, setAuthError] = useState(null);
15
15
+
const [statusFilter, setStatusFilter] = useState('all');
16
16
+
const [searchQuery, setSearchQuery] = useState('');
17
17
+
const [completenessFilter, setCompletenessFilter] = useState(0);
15
18
16
19
// Login state
17
20
const [email, setEmail] = useState('');
···
26
29
featured: false,
27
30
position: 0,
28
31
selectedCategories: [],
29
29
-
selectedTags: []
32
32
+
selectedTags: [],
33
33
+
status: 'draft'
30
34
});
31
35
32
36
// Alert state
33
37
const [alert, setAlert] = useState({ show: false, message: '', type: '' });
34
38
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
39
// Fetch all required data from Supabase
81
81
-
const fetchAllData = async () => {
40
40
+
const fetchAllData = useCallback(async () => {
82
41
setIsLoading(true);
83
42
try {
84
43
// Fetch resources
···
129
88
.filter(rt => rt.resource_id === resource.id)
130
89
.map(rt => rt.tag_id);
131
90
91
91
+
// Calculate completeness for UI
92
92
+
const completeness = calculateCompleteness({
93
93
+
...resource,
94
94
+
categoryIds: resourceCats,
95
95
+
tagIds: resourceTs
96
96
+
});
97
97
+
132
98
return {
133
99
...resource,
134
100
categoryIds: resourceCats,
135
135
-
tagIds: resourceTs
101
101
+
tagIds: resourceTs,
102
102
+
completeness,
103
103
+
status: resource.status || 'draft'
136
104
};
137
105
});
138
106
···
146
114
} finally {
147
115
setIsLoading(false);
148
116
}
149
149
-
};
117
117
+
}, []);
118
118
+
119
119
+
// Check authentication on mount
120
120
+
useEffect(() => {
121
121
+
const checkAuth = async () => {
122
122
+
const { data: { session } } = await supabase.auth.getSession();
123
123
+
setIsAuthenticated(!!session);
124
124
+
125
125
+
if (session) {
126
126
+
fetchAllData();
127
127
+
} else {
128
128
+
setIsLoading(false);
129
129
+
}
130
130
+
};
131
131
+
132
132
+
checkAuth();
133
133
+
}, [fetchAllData]);
150
134
151
135
// Handle resource selection
152
136
const handleSelectResource = (resource) => {
···
159
143
featured: resource.featured || false,
160
144
position: resource.position || 0,
161
145
selectedCategories: resource.categoryIds || [],
162
162
-
selectedTags: resource.tagIds || []
146
146
+
selectedTags: resource.tagIds || [],
147
147
+
status: resource.status || 'draft'
163
148
});
164
149
};
150
150
+
151
151
+
// Handle keyboard navigation
152
152
+
const handleKeyNavigation = useCallback((e) => {
153
153
+
if (!selectedResource || resources.length === 0) return;
154
154
+
155
155
+
const currentIndex = resources.findIndex(r => r.id === selectedResource.id);
156
156
+
let newIndex;
157
157
+
158
158
+
switch(e.key) {
159
159
+
case "ArrowDown":
160
160
+
newIndex = (currentIndex + 1) % resources.length;
161
161
+
handleSelectResource(resources[newIndex]);
162
162
+
break;
163
163
+
case "ArrowUp":
164
164
+
newIndex = (currentIndex - 1 + resources.length) % resources.length;
165
165
+
handleSelectResource(resources[newIndex]);
166
166
+
break;
167
167
+
default:
168
168
+
break;
169
169
+
}
170
170
+
}, [selectedResource, resources]);
171
171
+
172
172
+
// Add event listener for keyboard navigation
173
173
+
useEffect(() => {
174
174
+
document.addEventListener('keydown', handleKeyNavigation);
175
175
+
return () => {
176
176
+
document.removeEventListener('keydown', handleKeyNavigation);
177
177
+
};
178
178
+
}, [handleKeyNavigation]);
179
179
+
180
180
+
// Calculate resource completeness percentage
181
181
+
const calculateCompleteness = (resource) => {
182
182
+
let total = 4; // Required fields: name, description, url
183
183
+
let filled = 0;
184
184
+
185
185
+
if (resource.name) filled++;
186
186
+
if (resource.description) filled++;
187
187
+
if (resource.url) filled++;
188
188
+
if (resource.domain) filled++;
189
189
+
190
190
+
// Categories and tags are optional but contribute to completeness
191
191
+
if (resource.categoryIds && resource.categoryIds.length > 0) filled++;
192
192
+
total++;
193
193
+
194
194
+
if (resource.tagIds && resource.tagIds.length > 0) filled++;
195
195
+
total++;
196
196
+
197
197
+
return Math.round((filled / total) * 100);
198
198
+
};
199
199
+
200
200
+
// Login handler
201
201
+
const handleLogin = async (e) => {
202
202
+
e.preventDefault();
203
203
+
setIsLoading(true);
204
204
+
setAuthError(null);
205
205
+
206
206
+
try {
207
207
+
const { error } = await supabase.auth.signInWithPassword({
208
208
+
email,
209
209
+
password
210
210
+
});
211
211
+
212
212
+
if (error) throw error;
213
213
+
214
214
+
setIsAuthenticated(true);
215
215
+
fetchAllData();
216
216
+
} catch (error) {
217
217
+
console.error('Error logging in:', error);
218
218
+
setAuthError(error.message);
219
219
+
setIsLoading(false);
220
220
+
}
221
221
+
};
222
222
+
223
223
+
// Logout handler
224
224
+
const handleLogout = async () => {
225
225
+
await supabase.auth.signOut();
226
226
+
setIsAuthenticated(false);
227
227
+
};
165
228
166
229
// Handle form input changes
167
230
const handleInputChange = (e) => {
···
171
234
[name]: type === 'checkbox' ? checked : value
172
235
});
173
236
};
237
237
+
238
238
+
// Handle status change
239
239
+
const handleStatusChange = (status) => {
240
240
+
setFormData({
241
241
+
...formData,
242
242
+
status
243
243
+
});
244
244
+
};
245
245
+
246
246
+
// Filter resources based on status, search query, and completeness
247
247
+
const filteredResources = resources.filter(resource => {
248
248
+
// Status filter
249
249
+
if (statusFilter !== 'all' && resource.status !== statusFilter) return false;
250
250
+
251
251
+
// Search query filter
252
252
+
if (searchQuery && !resource.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
253
253
+
254
254
+
// Completeness filter
255
255
+
if (completenessFilter > 0 && resource.completeness < completenessFilter) return false;
256
256
+
257
257
+
return true;
258
258
+
});
174
259
175
260
// Handle category selection changes
176
261
const handleCategoryChange = (categoryId) => {
···
225
310
featured: false,
226
311
position: resources.length + 1,
227
312
selectedCategories: [],
228
228
-
selectedTags: []
313
313
+
selectedTags: [],
314
314
+
status: 'draft'
229
315
});
230
316
};
231
317
···
239
325
240
326
// Save resource changes
241
327
const handleSaveResource = async (e) => {
242
242
-
e.preventDefault();
328
328
+
if (e && e.preventDefault) e.preventDefault();
243
329
setIsLoading(true);
244
330
245
331
try {
···
250
336
domain: formData.domain,
251
337
featured: formData.featured,
252
338
position: formData.position,
339
339
+
status: formData.status,
253
340
updated_at: new Date().toISOString()
254
341
};
255
342
···
387
474
setIsLoading(true);
388
475
389
476
try {
390
390
-
const { data, error } = await supabase
477
477
+
const { error } = await supabase
391
478
.from('categories')
392
479
.insert({
393
480
name: categoryName,
394
481
emoji: emoji,
395
482
created_at: new Date().toISOString()
396
396
-
})
397
397
-
.select();
483
483
+
});
398
484
399
485
if (error) throw error;
400
486
···
416
502
setIsLoading(true);
417
503
418
504
try {
419
419
-
const { data, error } = await supabase
505
505
+
const { error } = await supabase
420
506
.from('tags')
421
507
.insert({
422
508
name: tagName,
423
509
created_at: new Date().toISOString()
424
424
-
})
425
425
-
.select();
510
510
+
});
426
511
427
512
if (error) throw error;
428
513
···
506
591
+ New Resource
507
592
</button>
508
593
</div>
594
594
+
<div className="sidebar-filters">
595
595
+
<div className="filter-group">
596
596
+
<input
597
597
+
type="text"
598
598
+
placeholder="Search resources..."
599
599
+
value={searchQuery}
600
600
+
onChange={(e) => setSearchQuery(e.target.value)}
601
601
+
className="search-input"
602
602
+
/>
603
603
+
</div>
604
604
+
<div className="filter-group">
605
605
+
<select
606
606
+
value={statusFilter}
607
607
+
onChange={(e) => setStatusFilter(e.target.value)}
608
608
+
className="status-filter"
609
609
+
>
610
610
+
<option value="all">All Statuses</option>
611
611
+
<option value="draft">Draft</option>
612
612
+
<option value="review">Review</option>
613
613
+
<option value="published">Published</option>
614
614
+
</select>
615
615
+
<select
616
616
+
value={completenessFilter}
617
617
+
onChange={(e) => setCompletenessFilter(Number(e.target.value))}
618
618
+
className="completeness-filter"
619
619
+
>
620
620
+
<option value="0">All Completeness</option>
621
621
+
<option value="25">At least 25%</option>
622
622
+
<option value="50">At least 50%</option>
623
623
+
<option value="75">At least 75%</option>
624
624
+
<option value="100">100% Complete</option>
625
625
+
</select>
626
626
+
</div>
627
627
+
</div>
509
628
<div className="resources-list">
510
510
-
{resources.map(resource => (
629
629
+
{filteredResources.map(resource => (
511
630
<div
512
631
key={resource.id}
513
513
-
className={`resource-item ${selectedResource && selectedResource.id === resource.id ? 'selected' : ''}`}
632
632
+
className={`resource-item ${selectedResource && selectedResource.id === resource.id ? 'selected' : ''} status-${resource.status}`}
514
633
onClick={() => handleSelectResource(resource)}
515
634
>
516
516
-
<div className="resource-item-name">{resource.name}</div>
635
635
+
<div className="resource-completeness-indicator">
636
636
+
<div
637
637
+
className="completeness-bar"
638
638
+
style={{ width: `${resource.completeness}%` }}
639
639
+
title={`${resource.completeness}% complete`}
640
640
+
></div>
641
641
+
</div>
642
642
+
<div className="resource-item-content">
643
643
+
<div className="resource-item-name">{resource.name}</div>
644
644
+
<div className="resource-item-meta">
645
645
+
<span className={`status-badge status-${resource.status}`}>
646
646
+
{resource.status}
647
647
+
</span>
648
648
+
{resource.featured && <span className="featured-badge">Featured</span>}
649
649
+
</div>
650
650
+
</div>
517
651
<div className="resource-item-actions">
518
652
<button
519
653
onClick={(e) => {
···
533
667
534
668
{/* Resource edit form */}
535
669
<div className="resource-editor">
536
536
-
<h2>{selectedResource ? 'Edit Resource' : 'Add New Resource'}</h2>
670
670
+
<div className="editor-header">
671
671
+
<h2>{selectedResource ? 'Edit Resource' : 'Add New Resource'}</h2>
672
672
+
<div className="floating-actions">
673
673
+
<div className="status-selector">
674
674
+
<span>Status:</span>
675
675
+
<div className="status-buttons">
676
676
+
<button
677
677
+
type="button"
678
678
+
className={`status-button ${formData.status === 'draft' ? 'active' : ''}`}
679
679
+
onClick={() => handleStatusChange('draft')}
680
680
+
>
681
681
+
Draft
682
682
+
</button>
683
683
+
<button
684
684
+
type="button"
685
685
+
className={`status-button ${formData.status === 'review' ? 'active' : ''}`}
686
686
+
onClick={() => handleStatusChange('review')}
687
687
+
>
688
688
+
Review
689
689
+
</button>
690
690
+
<button
691
691
+
type="button"
692
692
+
className={`status-button ${formData.status === 'published' ? 'active' : ''}`}
693
693
+
onClick={() => handleStatusChange('published')}
694
694
+
>
695
695
+
Published
696
696
+
</button>
697
697
+
</div>
698
698
+
</div>
699
699
+
<button
700
700
+
type="button"
701
701
+
onClick={handleSaveResource}
702
702
+
className="floating-save-button"
703
703
+
>
704
704
+
{selectedResource ? 'Update Resource' : 'Create Resource'}
705
705
+
</button>
706
706
+
</div>
707
707
+
</div>
537
708
<form onSubmit={handleSaveResource}>
538
709
<div className="form-row">
539
710
<div className="form-group">
···
1
1
-
import React, { useState, useEffect } from 'react';
2
2
-
import {
3
3
-
getResources,
4
4
-
getPendingSubmissions,
5
5
-
approveSubmission
6
6
-
} from '../lib/supabase';
7
7
-
8
8
-
const ResourceAdmin = () => {
9
9
-
const [resources, setResources] = useState([]);
10
10
-
const [submissions, setSubmissions] = useState([]);
11
11
-
const [activeTab, setActiveTab] = useState('resources');
12
12
-
const [isLoading, setIsLoading] = useState(true);
13
13
-
14
14
-
useEffect(() => {
15
15
-
fetchData();
16
16
-
}, [activeTab]);
17
17
-
18
18
-
async function fetchData() {
19
19
-
setIsLoading(true);
20
20
-
try {
21
21
-
if (activeTab === 'resources') {
22
22
-
const data = await getResources();
23
23
-
setResources(data);
24
24
-
} else if (activeTab === 'submissions') {
25
25
-
const data = await getPendingSubmissions();
26
26
-
setSubmissions(data);
27
27
-
}
28
28
-
} catch (error) {
29
29
-
console.error('Error fetching data:', error);
30
30
-
} finally {
31
31
-
setIsLoading(false);
32
32
-
}
33
33
-
}
34
34
-
35
35
-
const handleApproveSubmission = async (id) => {
36
36
-
try {
37
37
-
await approveSubmission(id);
38
38
-
// Refresh the submissions list
39
39
-
const data = await getPendingSubmissions();
40
40
-
setSubmissions(data);
41
41
-
} catch (error) {
42
42
-
console.error('Error approving submission:', error);
43
43
-
}
44
44
-
};
45
45
-
46
46
-
// More admin functions here...
47
47
-
48
48
-
return (
49
49
-
<div className="admin-dashboard">
50
50
-
<h1>Resource Admin</h1>
51
51
-
52
52
-
<div className="admin-tabs">
53
53
-
<button
54
54
-
className={activeTab === 'resources' ? 'active' : ''}
55
55
-
onClick={() => setActiveTab('resources')}
56
56
-
>
57
57
-
Manage Resources
58
58
-
</button>
59
59
-
<button
60
60
-
className={activeTab === 'submissions' ? 'active' : ''}
61
61
-
onClick={() => setActiveTab('submissions')}
62
62
-
>
63
63
-
Review Submissions
64
64
-
</button>
65
65
-
</div>
66
66
-
67
67
-
{isLoading ? (
68
68
-
<p>Loading...</p>
69
69
-
) : (
70
70
-
<div className="admin-content">
71
71
-
{activeTab === 'resources' && (
72
72
-
<div className="resources-management">
73
73
-
<h2>Resources ({resources.length})</h2>
74
74
-
{/* Resource management table */}
75
75
-
<table className="admin-table">
76
76
-
<thead>
77
77
-
<tr>
78
78
-
<th>Name</th>
79
79
-
<th>Category</th>
80
80
-
<th>Quality</th>
81
81
-
<th>Featured</th>
82
82
-
<th>Actions</th>
83
83
-
</tr>
84
84
-
</thead>
85
85
-
<tbody>
86
86
-
{resources.map(resource => (
87
87
-
<tr key={resource.id}>
88
88
-
<td>{resource.name}</td>
89
89
-
<td>{resource.category.name}</td>
90
90
-
<td>{resource.quality}</td>
91
91
-
<td>{resource.featured ? 'Yes' : 'No'}</td>
92
92
-
<td>
93
93
-
<button>Edit</button>
94
94
-
<button>Delete</button>
95
95
-
</td>
96
96
-
</tr>
97
97
-
))}
98
98
-
</tbody>
99
99
-
</table>
100
100
-
</div>
101
101
-
)}
102
102
-
103
103
-
{activeTab === 'submissions' && (
104
104
-
<div className="submissions-review">
105
105
-
<h2>Pending Submissions ({submissions.length})</h2>
106
106
-
{/* Submission review table */}
107
107
-
{submissions.length === 0 ? (
108
108
-
<p>No pending submissions.</p>
109
109
-
) : (
110
110
-
<table className="admin-table">
111
111
-
<thead>
112
112
-
<tr>
113
113
-
<th>Name</th>
114
114
-
<th>URL</th>
115
115
-
<th>Category</th>
116
116
-
<th>Submitted</th>
117
117
-
<th>Actions</th>
118
118
-
</tr>
119
119
-
</thead>
120
120
-
<tbody>
121
121
-
{submissions.map(submission => (
122
122
-
<tr key={submission.id}>
123
123
-
<td>{submission.name}</td>
124
124
-
<td>
125
125
-
<a href={submission.url} target="_blank" rel="noopener noreferrer">
126
126
-
{submission.domain}
127
127
-
</a>
128
128
-
</td>
129
129
-
<td>{submission.category.name}</td>
130
130
-
<td>{new Date(submission.created_at).toLocaleDateString()}</td>
131
131
-
<td>
132
132
-
<button onClick={() => handleApproveSubmission(submission.id)}>
133
133
-
Approve
134
134
-
</button>
135
135
-
<button>Reject</button>
136
136
-
<button>View Details</button>
137
137
-
</td>
138
138
-
</tr>
139
139
-
))}
140
140
-
</tbody>
141
141
-
</table>
142
142
-
)}
143
143
-
</div>
144
144
-
)}
145
145
-
</div>
146
146
-
)}
147
147
-
</div>
148
148
-
);
149
149
-
};
150
150
-
151
151
-
export default ResourceAdmin;
···
15
15
import ActivityCard from "./components/ActivityCard";
16
16
import ScoreBreakdownCard from "./components/ScoreBreakdownCard";
17
17
import ErrorPage from "../ErrorPage/ErrorPage";
18
18
-
import { supabase } from '../../lib/supabase';
19
18
import _ from 'lodash';
20
19
21
20
import "react-grid-layout/css/styles.css";