This repository has no description
1// src/components/Admin/AdminPanel.jsx
2import React, { useState, useEffect, useCallback } from 'react';
3import { supabase } from '../../lib/supabase';
4import './AdminPanel.css';
5
6const AdminPanel = () => {
7 // State management
8 const [resources, setResources] = useState([]);
9 const [categories, setCategories] = useState([]);
10 const [tags, setTags] = useState([]);
11 const [selectedResource, setSelectedResource] = useState(null);
12 const [isLoading, setIsLoading] = useState(true);
13 const [isAuthenticated, setIsAuthenticated] = useState(false);
14 const [authError, setAuthError] = useState(null);
15 const [statusFilter, setStatusFilter] = useState('all');
16 const [searchQuery, setSearchQuery] = useState('');
17 const [completenessFilter, setCompletenessFilter] = useState('all');
18 const [categoryFilter, setCategoryFilter] = useState('all');
19 const [tagFilter, setTagFilter] = useState('all');
20 const [featuredFilter, setFeaturedFilter] = useState('all');
21
22 // View management
23 const [activeView, setActiveView] = useState('resources'); // 'resources', 'reorder'
24 const [reorderMode, setReorderMode] = useState('featured'); // 'featured', 'category'
25 const [selectedCategoryForReorder, setSelectedCategoryForReorder] = useState(null);
26 const [updatingPositions, setUpdatingPositions] = useState(false);
27
28 // Login state
29 const [email, setEmail] = useState('');
30 const [password, setPassword] = useState('');
31
32 // New/Edit resource form state
33 const [formData, setFormData] = useState({
34 name: '',
35 description: '',
36 url: '',
37 domain: '',
38 featured: false,
39 position: 0,
40 selectedCategories: [],
41 selectedTags: [],
42 status: 'draft'
43 });
44
45 // Alert state
46 const [alert, setAlert] = useState({ show: false, message: '', type: '' });
47
48 // Fetch all required data from Supabase
49 const fetchAllData = useCallback(async () => {
50 setIsLoading(true);
51 try {
52 // Fetch resources
53 const { data: resourcesData, error: resourcesError } = await supabase
54 .from('resources')
55 .select('*')
56 .order('position');
57
58 if (resourcesError) throw resourcesError;
59
60 // Fetch categories
61 const { data: categoriesData, error: categoriesError } = await supabase
62 .from('categories')
63 .select('*')
64 .order('name');
65
66 if (categoriesError) throw categoriesError;
67
68 // Fetch tags
69 const { data: tagsData, error: tagsError } = await supabase
70 .from('tags')
71 .select('*')
72 .order('name');
73
74 if (tagsError) throw tagsError;
75
76 // Fetch resource-category associations
77 const { data: resourceCategories, error: rcError } = await supabase
78 .from('resource_categories')
79 .select('*');
80
81 if (rcError) throw rcError;
82
83 // Fetch resource-tag associations
84 const { data: resourceTags, error: rtError } = await supabase
85 .from('resource_tags')
86 .select('*');
87
88 if (rtError) throw rtError;
89
90 // Enhance resources with their associated categories and tags
91 const enhancedResources = resourcesData.map(resource => {
92 const resourceCats = resourceCategories
93 .filter(rc => rc.resource_id === resource.id)
94 .map(rc => rc.category_id);
95
96 const resourceTs = resourceTags
97 .filter(rt => rt.resource_id === resource.id)
98 .map(rt => rt.tag_id);
99
100 // Calculate completeness for UI
101 const completeness = calculateCompleteness({
102 ...resource,
103 categoryIds: resourceCats,
104 tagIds: resourceTs
105 });
106
107 return {
108 ...resource,
109 categoryIds: resourceCats,
110 tagIds: resourceTs,
111 completeness,
112 status: resource.status || 'draft'
113 };
114 });
115
116 // Update state
117 setResources(enhancedResources);
118 setCategories(categoriesData);
119 setTags(tagsData);
120 } catch (error) {
121 console.error('Error fetching data:', error);
122 showAlert(`Error: ${error.message}`, 'error');
123 } finally {
124 setIsLoading(false);
125 }
126 }, []);
127
128 // Check authentication on mount
129 useEffect(() => {
130 const checkAuth = async () => {
131 const { data: { session } } = await supabase.auth.getSession();
132 setIsAuthenticated(!!session);
133
134 if (session) {
135 fetchAllData();
136 } else {
137 setIsLoading(false);
138 }
139 };
140
141 checkAuth();
142 }, [fetchAllData]);
143
144 // Handle resource selection
145 const handleSelectResource = (resource) => {
146 setSelectedResource(resource);
147 setFormData({
148 name: resource.name || '',
149 description: resource.description || '',
150 url: resource.url || '',
151 domain: resource.domain || '',
152 featured: resource.featured || false,
153 position: resource.position || 0,
154 selectedCategories: resource.categoryIds || [],
155 selectedTags: resource.tagIds || [],
156 status: resource.status || 'draft'
157 });
158 };
159
160 // Handle keyboard navigation
161 const handleKeyNavigation = useCallback((e) => {
162 if (!selectedResource || resources.length === 0) return;
163
164 const currentIndex = resources.findIndex(r => r.id === selectedResource.id);
165 let newIndex;
166
167 switch(e.key) {
168 case "ArrowDown":
169 newIndex = (currentIndex + 1) % resources.length;
170 handleSelectResource(resources[newIndex]);
171 break;
172 case "ArrowUp":
173 newIndex = (currentIndex - 1 + resources.length) % resources.length;
174 handleSelectResource(resources[newIndex]);
175 break;
176 default:
177 break;
178 }
179 }, [selectedResource, resources]);
180
181 // Add event listener for keyboard navigation
182 useEffect(() => {
183 document.addEventListener('keydown', handleKeyNavigation);
184 return () => {
185 document.removeEventListener('keydown', handleKeyNavigation);
186 };
187 }, [handleKeyNavigation]);
188
189 // Reset filters
190 const resetFilters = () => {
191 setStatusFilter('all');
192 setCompletenessFilter('all');
193 setCategoryFilter('all');
194 setTagFilter('all');
195 setFeaturedFilter('all');
196 setSearchQuery('');
197 };
198
199 // Calculate resource completeness percentage
200 const calculateCompleteness = (resource) => {
201 let total = 4; // Required fields: name, description, url
202 let filled = 0;
203
204 if (resource.name) filled++;
205 if (resource.description) filled++;
206 if (resource.url) filled++;
207 if (resource.domain) filled++;
208
209 // Categories and tags are optional but contribute to completeness
210 if (resource.categoryIds && resource.categoryIds.length > 0) filled++;
211 total++;
212
213 if (resource.tagIds && resource.tagIds.length > 0) filled++;
214 total++;
215
216 return Math.round((filled / total) * 100);
217 };
218
219 // Login handler
220 const handleLogin = async (e) => {
221 e.preventDefault();
222 setIsLoading(true);
223 setAuthError(null);
224
225 try {
226 const { error } = await supabase.auth.signInWithPassword({
227 email,
228 password
229 });
230
231 if (error) throw error;
232
233 setIsAuthenticated(true);
234 fetchAllData();
235 } catch (error) {
236 console.error('Error logging in:', error);
237 setAuthError(error.message);
238 setIsLoading(false);
239 }
240 };
241
242 // Logout handler
243 const handleLogout = async () => {
244 await supabase.auth.signOut();
245 setIsAuthenticated(false);
246 };
247
248 // Handle form input changes
249 const handleInputChange = (e) => {
250 const { name, value, type, checked } = e.target;
251 setFormData({
252 ...formData,
253 [name]: type === 'checkbox' ? checked : value
254 });
255 };
256
257 // Handle status change
258 const handleStatusChange = (status) => {
259 setFormData({
260 ...formData,
261 status
262 });
263 };
264
265 // Update a single resource position without full page refresh
266 const updateResourcePosition = async (resourceId, newPosition) => {
267 if (updatingPositions) return;
268 setUpdatingPositions(true);
269
270 try {
271 // Update the resource position in the database
272 const { error } = await supabase
273 .from('resources')
274 .update({ position: newPosition })
275 .eq('id', resourceId);
276
277 if (error) throw error;
278
279 // Update local state without fetching all data again
280 setResources(prevResources => {
281 return prevResources.map(resource => {
282 if (resource.id === resourceId) {
283 return { ...resource, position: newPosition };
284 }
285 return resource;
286 });
287 });
288
289 showAlert(`Position updated successfully!`);
290 } catch (error) {
291 console.error('Error updating position:', error);
292 showAlert(`Error: ${error.message}`, 'error');
293 } finally {
294 setUpdatingPositions(false);
295 }
296 };
297
298 // Handle position input change and update
299 const handlePositionChange = (resourceId, e) => {
300 const newPosition = parseInt(e.target.value);
301 if (isNaN(newPosition) || newPosition < 0) return;
302
303 updateResourcePosition(resourceId, newPosition);
304 };
305
306 // Reorder resources (move up or down in list)
307 const handleReorderResource = async (resourceId, direction) => {
308 if (updatingPositions) return;
309
310 // Find the resource to update
311 const resource = resources.find(r => r.id === resourceId);
312 if (!resource) return;
313
314 // Calculate new position - decrement for up, increment for down
315 const newPosition = direction === 'up'
316 ? resource.position - 1
317 : resource.position + 1;
318
319 setUpdatingPositions(true);
320
321 try {
322 // Update position in database
323 await supabase
324 .from('resources')
325 .update({ position: newPosition })
326 .eq('id', resourceId);
327
328 // Update local state
329 setResources(prevResources => {
330 return prevResources.map(r => {
331 if (r.id === resourceId) {
332 return { ...r, position: newPosition };
333 }
334 return r;
335 });
336 });
337
338 showAlert(`Resource moved ${direction}!`);
339 } catch (error) {
340 console.error(`Error moving resource ${direction}:`, error);
341 showAlert(`Error: ${error.message}`, 'error');
342 } finally {
343 setUpdatingPositions(false);
344 }
345 };
346
347 // Save all positions at once
348 const saveAllPositions = async (orderedResources) => {
349 setIsLoading(true);
350
351 try {
352 // Create an array of update operations
353 const updates = orderedResources.map((resource, index) => ({
354 id: resource.id,
355 position: index + 1
356 }));
357
358 // Execute updates in parallel
359 const promises = updates.map(update =>
360 supabase
361 .from('resources')
362 .update({ position: update.position })
363 .eq('id', update.id)
364 );
365
366 await Promise.all(promises);
367
368 // Refresh data
369 await fetchAllData();
370
371 showAlert(`Resource positions updated successfully!`);
372 } catch (error) {
373 console.error('Error updating positions:', error);
374 showAlert(`Error: ${error.message}`, 'error');
375 } finally {
376 setIsLoading(false);
377 }
378 };
379
380 // Filter resources based on status, search query, completeness, category, tag, and featured status
381 const filteredResources = resources.filter(resource => {
382 // Status filter
383 if (statusFilter !== 'all' && resource.status !== statusFilter) return false;
384
385 // Search query filter
386 if (searchQuery && !resource.name.toLowerCase().includes(searchQuery.toLowerCase())) return false;
387
388 // Completeness filter
389 if (completenessFilter === 'incomplete' && resource.completeness === 100) return false;
390 if (completenessFilter === 'complete' && resource.completeness < 100) return false;
391 if (completenessFilter.startsWith('min-') && resource.completeness < parseInt(completenessFilter.substring(4))) return false;
392 if (completenessFilter.startsWith('max-') && resource.completeness > parseInt(completenessFilter.substring(4))) return false;
393
394 // Category filter
395 if (categoryFilter !== 'all' && (!resource.categoryIds || !resource.categoryIds.includes(parseInt(categoryFilter)))) return false;
396
397 // Tag filter
398 if (tagFilter !== 'all' && (!resource.tagIds || !resource.tagIds.includes(parseInt(tagFilter)))) return false;
399
400 // Featured filter
401 if (featuredFilter === 'featured' && !resource.featured) return false;
402 if (featuredFilter === 'not-featured' && resource.featured) return false;
403
404 return true;
405 });
406
407 // Handle category selection changes
408 const handleCategoryChange = (categoryId) => {
409 setFormData(prevData => {
410 const selectedCategories = [...prevData.selectedCategories];
411
412 if (selectedCategories.includes(categoryId)) {
413 // Remove category if already selected
414 return {
415 ...prevData,
416 selectedCategories: selectedCategories.filter(id => id !== categoryId)
417 };
418 } else {
419 // Add category if not already selected
420 return {
421 ...prevData,
422 selectedCategories: [...selectedCategories, categoryId]
423 };
424 }
425 });
426 };
427
428 // Handle tag selection changes
429 const handleTagChange = (tagId) => {
430 setFormData(prevData => {
431 const selectedTags = [...prevData.selectedTags];
432
433 if (selectedTags.includes(tagId)) {
434 // Remove tag if already selected
435 return {
436 ...prevData,
437 selectedTags: selectedTags.filter(id => id !== tagId)
438 };
439 } else {
440 // Add tag if not already selected
441 return {
442 ...prevData,
443 selectedTags: [...selectedTags, tagId]
444 };
445 }
446 });
447 };
448
449 // Clear form and selected resource
450 const handleClearForm = () => {
451 setSelectedResource(null);
452 setFormData({
453 name: '',
454 description: '',
455 url: '',
456 domain: '',
457 featured: false,
458 position: resources.length + 1,
459 selectedCategories: [],
460 selectedTags: [],
461 status: 'draft'
462 });
463 };
464
465 // Show alert message
466 const showAlert = (message, type = 'success') => {
467 setAlert({ show: true, message, type });
468 setTimeout(() => {
469 setAlert({ show: false, message: '', type: '' });
470 }, 5000);
471 };
472
473 // Save resource changes
474 const handleSaveResource = async (e) => {
475 if (e && e.preventDefault) e.preventDefault();
476 setIsLoading(true);
477
478 try {
479 const resourceData = {
480 name: formData.name,
481 description: formData.description,
482 url: formData.url,
483 domain: formData.domain,
484 featured: formData.featured,
485 position: formData.position,
486 status: formData.status,
487 updated_at: new Date().toISOString()
488 };
489
490 let resourceId;
491
492 if (selectedResource) {
493 // Update existing resource
494 const { error } = await supabase
495 .from('resources')
496 .update(resourceData)
497 .eq('id', selectedResource.id);
498
499 if (error) throw error;
500 resourceId = selectedResource.id;
501
502 // Delete existing category and tag associations
503 await supabase
504 .from('resource_categories')
505 .delete()
506 .eq('resource_id', resourceId);
507
508 await supabase
509 .from('resource_tags')
510 .delete()
511 .eq('resource_id', resourceId);
512
513 showAlert(`Resource "${formData.name}" updated successfully!`);
514 } else {
515 // Add new resource
516 resourceData.created_at = new Date().toISOString();
517
518 const { data, error } = await supabase
519 .from('resources')
520 .insert(resourceData)
521 .select();
522
523 if (error) throw error;
524 resourceId = data[0].id;
525
526 showAlert(`Resource "${formData.name}" created successfully!`);
527 }
528
529 // Add category associations
530 if (formData.selectedCategories.length > 0) {
531 const categoryAssociations = formData.selectedCategories.map(categoryId => ({
532 resource_id: resourceId,
533 category_id: categoryId
534 }));
535
536 const { error: categoryError } = await supabase
537 .from('resource_categories')
538 .insert(categoryAssociations);
539
540 if (categoryError) throw categoryError;
541 }
542
543 // Add tag associations
544 if (formData.selectedTags.length > 0) {
545 const tagAssociations = formData.selectedTags.map(tagId => ({
546 resource_id: resourceId,
547 tag_id: tagId
548 }));
549
550 const { error: tagError } = await supabase
551 .from('resource_tags')
552 .insert(tagAssociations);
553
554 if (tagError) throw tagError;
555 }
556
557 // Refresh data
558 fetchAllData();
559 handleClearForm();
560 } catch (error) {
561 console.error('Error saving resource:', error);
562 showAlert(`Error: ${error.message}`, 'error');
563 } finally {
564 setIsLoading(false);
565 }
566 };
567
568 // Delete resource
569 const handleDeleteResource = async (resourceId, resourceName) => {
570 if (!window.confirm(`Are you sure you want to delete "${resourceName}"?`)) {
571 return;
572 }
573
574 setIsLoading(true);
575
576 try {
577 // Delete associated records first (foreign key constraints)
578 await supabase
579 .from('resource_categories')
580 .delete()
581 .eq('resource_id', resourceId);
582
583 await supabase
584 .from('resource_tags')
585 .delete()
586 .eq('resource_id', resourceId);
587
588 // Delete the resource
589 const { error } = await supabase
590 .from('resources')
591 .delete()
592 .eq('id', resourceId);
593
594 if (error) throw error;
595
596 showAlert(`Resource "${resourceName}" deleted successfully!`);
597
598 // Refresh data
599 fetchAllData();
600
601 // Clear form if the deleted resource was selected
602 if (selectedResource && selectedResource.id === resourceId) {
603 handleClearForm();
604 }
605 } catch (error) {
606 console.error('Error deleting resource:', error);
607 showAlert(`Error: ${error.message}`, 'error');
608 } finally {
609 setIsLoading(false);
610 }
611 };
612
613 // Create new category
614 const handleCreateCategory = async () => {
615 const categoryName = prompt('Enter the new category name:');
616 if (!categoryName) return;
617
618 const emoji = prompt('Enter an emoji for this category:');
619 if (!emoji) return;
620
621 setIsLoading(true);
622
623 try {
624 const { error } = await supabase
625 .from('categories')
626 .insert({
627 name: categoryName,
628 emoji: emoji,
629 created_at: new Date().toISOString()
630 });
631
632 if (error) throw error;
633
634 showAlert(`Category "${categoryName}" created successfully!`);
635 fetchAllData();
636 } catch (error) {
637 console.error('Error creating category:', error);
638 showAlert(`Error: ${error.message}`, 'error');
639 } finally {
640 setIsLoading(false);
641 }
642 };
643
644 // Create new tag
645 const handleCreateTag = async () => {
646 const tagName = prompt('Enter the new tag name:');
647 if (!tagName) return;
648
649 setIsLoading(true);
650
651 try {
652 const { error } = await supabase
653 .from('tags')
654 .insert({
655 name: tagName,
656 created_at: new Date().toISOString()
657 });
658
659 if (error) throw error;
660
661 showAlert(`Tag "${tagName}" created successfully!`);
662 fetchAllData();
663 } catch (error) {
664 console.error('Error creating tag:', error);
665 showAlert(`Error: ${error.message}`, 'error');
666 } finally {
667 setIsLoading(false);
668 }
669 };
670
671 // Render loading spinner
672 if (isLoading) {
673 return (
674 <div className="admin-loading">
675 <div className="loading-spinner"></div>
676 <p>Loading...</p>
677 </div>
678 );
679 }
680
681 // Render login form if not authenticated
682 if (!isAuthenticated) {
683 return (
684 <div className="admin-login-container">
685 <div className="admin-login-card">
686 <h2>Admin Login</h2>
687 {authError && <div className="auth-error">{authError}</div>}
688 <form onSubmit={handleLogin}>
689 <div className="form-group">
690 <label htmlFor="email">Email</label>
691 <input
692 type="email"
693 id="email"
694 value={email}
695 onChange={(e) => setEmail(e.target.value)}
696 required
697 />
698 </div>
699 <div className="form-group">
700 <label htmlFor="password">Password</label>
701 <input
702 type="password"
703 id="password"
704 value={password}
705 onChange={(e) => setPassword(e.target.value)}
706 required
707 />
708 </div>
709 <button type="submit" className="login-button">Login</button>
710 </form>
711 </div>
712 </div>
713 );
714 }
715
716 // Main admin panel UI
717 return (
718 <div className="admin-panel">
719 {/* Header */}
720 <header className="admin-header">
721 <h1>Resources Admin Panel</h1>
722 <div className="nav-tabs">
723 <button
724 className={`nav-tab ${activeView === 'resources' ? 'active' : ''}`}
725 onClick={() => setActiveView('resources')}
726 >
727 Resources
728 </button>
729 <button
730 className={`nav-tab ${activeView === 'reorder' ? 'active' : ''}`}
731 onClick={() => setActiveView('reorder')}
732 >
733 Reorder
734 </button>
735 </div>
736 <button onClick={handleLogout} className="logout-button">Logout</button>
737 </header>
738
739 {/* Alert message */}
740 {alert.show && (
741 <div className={`alert ${alert.type}`}>
742 {alert.message}
743 </div>
744 )}
745
746 {activeView === 'resources' && (
747 <div className="admin-container">
748 {/* Resources list sidebar */}
749 <div className="resources-sidebar">
750 <div className="sidebar-header">
751 <h2>Resources</h2>
752 <button onClick={handleClearForm} className="add-new-button">
753 + New Resource
754 </button>
755 </div>
756 <div className="sidebar-filters">
757 <div className="search-container">
758 <input
759 type="text"
760 placeholder="Search resources..."
761 value={searchQuery}
762 onChange={(e) => setSearchQuery(e.target.value)}
763 className="search-input"
764 />
765 {(searchQuery || statusFilter !== 'all' || completenessFilter !== 'all' ||
766 categoryFilter !== 'all' || tagFilter !== 'all' || featuredFilter !== 'all') && (
767 <button
768 onClick={resetFilters}
769 className="reset-filters-button"
770 title="Reset all filters"
771 >
772 ✕
773 </button>
774 )}
775 </div>
776 <div className="filter-group">
777 <select
778 value={statusFilter}
779 onChange={(e) => setStatusFilter(e.target.value)}
780 className="status-filter"
781 >
782 <option value="all">All Statuses</option>
783 <option value="draft">Draft</option>
784 <option value="review">Review</option>
785 <option value="published">Published</option>
786 </select>
787 <select
788 value={featuredFilter}
789 onChange={(e) => setFeaturedFilter(e.target.value)}
790 className="featured-filter"
791 >
792 <option value="all">All Resources</option>
793 <option value="featured">Featured Only</option>
794 <option value="not-featured">Not Featured</option>
795 </select>
796 </div>
797 <div className="filter-group">
798 <select
799 value={completenessFilter}
800 onChange={(e) => setCompletenessFilter(e.target.value)}
801 className="completeness-filter"
802 >
803 <option value="all">All Completeness</option>
804 <option value="incomplete">Incomplete Only</option>
805 <option value="complete">100% Complete Only</option>
806 <option value="min-25">At least 25%</option>
807 <option value="min-50">At least 50%</option>
808 <option value="min-75">At least 75%</option>
809 <option value="max-25">Less than 25%</option>
810 <option value="max-50">Less than 50%</option>
811 <option value="max-75">Less than 75%</option>
812 </select>
813 </div>
814 <div className="filter-group">
815 <select
816 value={categoryFilter}
817 onChange={(e) => setCategoryFilter(e.target.value)}
818 className="category-filter"
819 >
820 <option value="all">All Categories</option>
821 {categories.map(category => (
822 <option key={category.id} value={category.id}>
823 {category.emoji} {category.name}
824 </option>
825 ))}
826 </select>
827 <select
828 value={tagFilter}
829 onChange={(e) => setTagFilter(e.target.value)}
830 className="tag-filter"
831 >
832 <option value="all">All Tags</option>
833 {tags.map(tag => (
834 <option key={tag.id} value={tag.id}>
835 #{tag.name}
836 </option>
837 ))}
838 </select>
839 </div>
840 </div>
841 <div className="resources-summary">
842 <span className="resources-count">
843 Showing {filteredResources.length} of {resources.length} resources
844 </span>
845 </div>
846 <div className="resources-list">
847 {filteredResources.length > 0 ? (
848 filteredResources.map(resource => (
849 <div
850 key={resource.id}
851 className={`resource-item ${selectedResource && selectedResource.id === resource.id ? 'selected' : ''} status-${resource.status}`}
852 onClick={() => handleSelectResource(resource)}
853 >
854 <div className="resource-completeness-indicator">
855 <div
856 className="completeness-bar"
857 style={{ width: `${resource.completeness}%` }}
858 title={`${resource.completeness}% complete`}
859 ></div>
860 </div>
861 <div className="resource-item-content">
862 <div className="resource-item-name">{resource.name}</div>
863 <div className="resource-item-meta">
864 {resource.featured && <span className="featured-badge">Featured</span>}
865 <span className="completeness-badge" title="Completeness">
866 {resource.completeness}%
867 </span>
868 </div>
869 </div>
870 <div className="resource-item-actions">
871 <button
872 onClick={(e) => {
873 e.stopPropagation();
874 handleDeleteResource(resource.id, resource.name);
875 }}
876 className="delete-button"
877 title="Delete resource"
878 >
879 🗑️
880 </button>
881 </div>
882 </div>
883 ))
884 ) : (
885 <div className="no-resources-message">
886 <p>No resources match your filters.</p>
887 </div>
888 )}
889 </div>
890 </div>
891
892 {/* Resource edit form */}
893 <div className="resource-editor">
894 <div className="editor-header">
895 <h2>{selectedResource ? 'Edit Resource' : 'Add New Resource'}</h2>
896 <div className="floating-actions">
897 <div className="status-selector">
898 <span>Status:</span>
899 <div className="status-buttons">
900 <button
901 type="button"
902 className={`status-button ${formData.status === 'draft' ? 'active' : ''}`}
903 onClick={() => handleStatusChange('draft')}
904 >
905 Draft
906 </button>
907 <button
908 type="button"
909 className={`status-button ${formData.status === 'review' ? 'active' : ''}`}
910 onClick={() => handleStatusChange('review')}
911 >
912 Review
913 </button>
914 <button
915 type="button"
916 className={`status-button ${formData.status === 'published' ? 'active' : ''}`}
917 onClick={() => handleStatusChange('published')}
918 >
919 Published
920 </button>
921 </div>
922 </div>
923 <button
924 type="button"
925 onClick={handleSaveResource}
926 className="floating-save-button"
927 >
928 {selectedResource ? 'Update Resource' : 'Create Resource'}
929 </button>
930 </div>
931 </div>
932 <form onSubmit={handleSaveResource}>
933 <div className="form-row">
934 <div className="form-group">
935 <label htmlFor="name">Name *</label>
936 <input
937 type="text"
938 id="name"
939 name="name"
940 value={formData.name}
941 onChange={handleInputChange}
942 required
943 placeholder="Resource name"
944 />
945 </div>
946 <div className="form-group">
947 <label htmlFor="domain">Domain</label>
948 <input
949 type="text"
950 id="domain"
951 name="domain"
952 value={formData.domain}
953 onChange={handleInputChange}
954 placeholder="e.g., design, development, marketing"
955 />
956 </div>
957 </div>
958
959 <div className="form-group">
960 <label htmlFor="url">URL *</label>
961 <input
962 type="url"
963 id="url"
964 name="url"
965 value={formData.url}
966 onChange={handleInputChange}
967 required
968 placeholder="https://example.com"
969 />
970 </div>
971
972 <div className="form-group">
973 <label htmlFor="description">Description *</label>
974 <textarea
975 id="description"
976 name="description"
977 value={formData.description}
978 onChange={handleInputChange}
979 rows="4"
980 required
981 placeholder="Brief description of the resource..."
982 ></textarea>
983 </div>
984
985 <div className="form-row">
986 <div className="form-group">
987 <label htmlFor="position">Position</label>
988 <input
989 type="number"
990 id="position"
991 name="position"
992 value={formData.position}
993 onChange={handleInputChange}
994 min="0"
995 />
996 </div>
997 <div className="form-group checkbox-group">
998 <input
999 type="checkbox"
1000 id="featured"
1001 name="featured"
1002 checked={formData.featured}
1003 onChange={handleInputChange}
1004 />
1005 <label htmlFor="featured">Featured Resource</label>
1006 </div>
1007 </div>
1008
1009 <div className="form-row">
1010 {/* Categories selection */}
1011 <div className="form-group categories-section">
1012 <div className="section-header">
1013 <label>Categories</label>
1014 <button
1015 type="button"
1016 onClick={handleCreateCategory}
1017 className="add-item-button"
1018 >
1019 + Add Category
1020 </button>
1021 </div>
1022 <div className="checkbox-list">
1023 {categories.length > 0 ? (
1024 categories.map(category => (
1025 <div key={category.id} className="checkbox-item">
1026 <input
1027 type="checkbox"
1028 id={`category-${category.id}`}
1029 checked={formData.selectedCategories.includes(category.id)}
1030 onChange={() => handleCategoryChange(category.id)}
1031 />
1032 <label htmlFor={`category-${category.id}`}>
1033 {category.emoji} {category.name}
1034 </label>
1035 </div>
1036 ))
1037 ) : (
1038 <p className="no-items-message">No categories available. Create one!</p>
1039 )}
1040 </div>
1041 </div>
1042
1043 {/* Tags selection */}
1044 <div className="form-group tags-section">
1045 <div className="section-header">
1046 <label>Tags</label>
1047 <button
1048 type="button"
1049 onClick={handleCreateTag}
1050 className="add-item-button"
1051 >
1052 + Add Tag
1053 </button>
1054 </div>
1055 <div className="checkbox-list">
1056 {tags.length > 0 ? (
1057 tags.map(tag => (
1058 <div key={tag.id} className="checkbox-item">
1059 <input
1060 type="checkbox"
1061 id={`tag-${tag.id}`}
1062 checked={formData.selectedTags.includes(tag.id)}
1063 onChange={() => handleTagChange(tag.id)}
1064 />
1065 <label htmlFor={`tag-${tag.id}`}>
1066 #{tag.name}
1067 </label>
1068 </div>
1069 ))
1070 ) : (
1071 <p className="no-items-message">No tags available. Create one!</p>
1072 )}
1073 </div>
1074 </div>
1075 </div>
1076
1077 <div className="form-actions">
1078 <button type="button" onClick={handleClearForm} className="cancel-button">
1079 Cancel
1080 </button>
1081 <button type="submit" className="save-button">
1082 {selectedResource ? 'Update Resource' : 'Create Resource'}
1083 </button>
1084 </div>
1085 </form>
1086 </div>
1087 </div>
1088 )}
1089
1090 {activeView === 'reorder' && (
1091 <div className="reorder-container">
1092 <div className="reorder-header">
1093 <h2>Reorder Resources</h2>
1094 <div className="reorder-controls">
1095 <div className="reorder-mode-selector">
1096 <button
1097 className={`mode-button ${reorderMode === 'featured' ? 'active' : ''}`}
1098 onClick={() => {
1099 setReorderMode('featured');
1100 setSelectedCategoryForReorder(null);
1101 }}
1102 >
1103 Featured Resources
1104 </button>
1105 <button
1106 className={`mode-button ${reorderMode === 'category' ? 'active' : ''}`}
1107 onClick={() => setReorderMode('category')}
1108 >
1109 By Category
1110 </button>
1111 </div>
1112
1113 {reorderMode === 'category' && (
1114 <div className="category-selector">
1115 <select
1116 value={selectedCategoryForReorder || ''}
1117 onChange={(e) => setSelectedCategoryForReorder(parseInt(e.target.value))}
1118 className="category-select"
1119 >
1120 <option value="">Select Category...</option>
1121 {categories.map(category => (
1122 <option key={category.id} value={category.id}>
1123 {category.emoji} {category.name}
1124 </option>
1125 ))}
1126 </select>
1127 </div>
1128 )}
1129 </div>
1130 </div>
1131
1132 <div className="reorder-content">
1133 {reorderMode === 'featured' ? (
1134 <div className="reorder-list">
1135 <h3>Featured Resources</h3>
1136 {resources.filter(r => r.featured).length === 0 ? (
1137 <p className="no-resources-message">No featured resources found.</p>
1138 ) : (
1139 <div className="sortable-resources">
1140 {resources
1141 .filter(r => r.featured)
1142 .sort((a, b) => a.position - b.position)
1143 .map(resource => (
1144 <div key={resource.id} className="sortable-resource-item">
1145 <div className="resource-info">
1146 <div className="resource-name">{resource.name}</div>
1147 <div className="resource-meta">
1148 <span className={`status-badge status-${resource.status}`}>
1149 {resource.status}
1150 </span>
1151 <div className="position-control">
1152 <span>Position:</span>
1153 <input
1154 type="number"
1155 value={resource.position}
1156 onChange={(e) => handlePositionChange(resource.id, e)}
1157 className="position-input"
1158 min="1"
1159 />
1160 </div>
1161 </div>
1162 </div>
1163 <div className="reorder-actions">
1164 <button
1165 onClick={() => handleReorderResource(resource.id, 'up')}
1166 className="move-button move-up"
1167 title="Move up"
1168 disabled={updatingPositions}
1169 >
1170 ↑
1171 </button>
1172 <button
1173 onClick={() => handleReorderResource(resource.id, 'down')}
1174 className="move-button move-down"
1175 title="Move down"
1176 disabled={updatingPositions}
1177 >
1178 ↓
1179 </button>
1180 </div>
1181 </div>
1182 ))
1183 }
1184 </div>
1185 )}
1186 </div>
1187 ) : (
1188 <div className="reorder-list">
1189 {!selectedCategoryForReorder ? (
1190 <p className="select-category-message">Please select a category from the dropdown above.</p>
1191 ) : (
1192 <>
1193 <h3>
1194 {categories.find(c => c.id === selectedCategoryForReorder)?.emoji} {' '}
1195 {categories.find(c => c.id === selectedCategoryForReorder)?.name} Resources
1196 </h3>
1197 {resources.filter(r =>
1198 r.categoryIds && r.categoryIds.includes(selectedCategoryForReorder)
1199 ).length === 0 ? (
1200 <p className="no-resources-message">No resources found in this category.</p>
1201 ) : (
1202 <div className="sortable-resources">
1203 {resources
1204 .filter(r => r.categoryIds && r.categoryIds.includes(selectedCategoryForReorder))
1205 .sort((a, b) => a.position - b.position)
1206 .map(resource => (
1207 <div key={resource.id} className="sortable-resource-item">
1208 <div className="resource-info">
1209 <div className="resource-name">{resource.name}</div>
1210 <div className="resource-meta">
1211 <span className={`status-badge status-${resource.status}`}>
1212 {resource.status}
1213 </span>
1214 {resource.featured && <span className="featured-badge">Featured</span>}
1215 <div className="position-control">
1216 <span>Position:</span>
1217 <input
1218 type="number"
1219 value={resource.position}
1220 onChange={(e) => handlePositionChange(resource.id, e)}
1221 className="position-input"
1222 min="1"
1223 />
1224 </div>
1225 </div>
1226 </div>
1227 <div className="reorder-actions">
1228 <button
1229 onClick={() => handleReorderResource(resource.id, 'up')}
1230 className="move-button move-up"
1231 title="Move up"
1232 disabled={updatingPositions}
1233 >
1234 ↑
1235 </button>
1236 <button
1237 onClick={() => handleReorderResource(resource.id, 'down')}
1238 className="move-button move-down"
1239 title="Move down"
1240 disabled={updatingPositions}
1241 >
1242 ↓
1243 </button>
1244 </div>
1245 </div>
1246 ))
1247 }
1248 </div>
1249 )}
1250 </>
1251 )}
1252 </div>
1253 )}
1254 </div>
1255 </div>
1256 )}
1257 </div>
1258 );
1259};
1260
1261export default AdminPanel;