This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 44 kB View raw
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;