This repository has no description
0

Configure Feed

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

try again

-1124
-1
src/App.jsx
··· 42 42 <Route path="/leaderboard" element={<Leaderboard />} /> 43 43 <Route path="/resources" element={<Resources />} /> 44 44 <Route path="/resources/submit" element={<ResourceSubmission />} /> 45 - <Route path="/admin/resources" element={<ResourcesManager />} /> 46 45 <Route path="/shortcut" element={<Shortcut />} /> 47 46 <Route path="/zen" element={<ZenPage />} /> 48 47 <Route path="/methodology" element={<ScoringMethodology />} />
-456
src/components/Admin/ResourcesManager.css
··· 1 - /* src/components/Admin/ResourcesManager.css */ 2 - 3 - .admin-resources-manager { 4 - padding: 20px; 5 - max-width: 1400px; 6 - margin: 0 auto; 7 - } 8 - 9 - .admin-resources-manager h1 { 10 - margin-bottom: 24px; 11 - color: #333; 12 - } 13 - 14 - .admin-error-alert { 15 - background-color: #ffebee; 16 - color: #c62828; 17 - padding: 12px 16px; 18 - border-radius: 4px; 19 - margin-bottom: 20px; 20 - border-left: 4px solid #c62828; 21 - } 22 - 23 - /* Tabs */ 24 - .admin-tabs { 25 - display: flex; 26 - margin-bottom: 24px; 27 - border-bottom: 1px solid #e0e0e0; 28 - } 29 - 30 - .admin-tabs button { 31 - padding: 12px 24px; 32 - font-size: 16px; 33 - background: none; 34 - border: none; 35 - cursor: pointer; 36 - font-weight: 500; 37 - color: #666; 38 - position: relative; 39 - } 40 - 41 - .admin-tabs button.active { 42 - color: #007aff; 43 - font-weight: 600; 44 - } 45 - 46 - .admin-tabs button.active::after { 47 - content: ''; 48 - position: absolute; 49 - bottom: -1px; 50 - left: 0; 51 - right: 0; 52 - height: 3px; 53 - background-color: #007aff; 54 - } 55 - 56 - .badge { 57 - display: inline-block; 58 - background-color: #007aff; 59 - color: white; 60 - font-size: 12px; 61 - border-radius: 12px; 62 - padding: 2px 8px; 63 - margin-left: 8px; 64 - } 65 - 66 - /* Toolbar */ 67 - .admin-toolbar { 68 - display: flex; 69 - justify-content: space-between; 70 - align-items: center; 71 - margin-bottom: 20px; 72 - flex-wrap: wrap; 73 - gap: 10px; 74 - } 75 - 76 - .admin-search { 77 - flex: 1; 78 - max-width: 300px; 79 - } 80 - 81 - .admin-search input { 82 - width: 100%; 83 - padding: 10px 12px; 84 - border: 1px solid #ddd; 85 - border-radius: 4px; 86 - font-size: 14px; 87 - } 88 - 89 - .admin-filter select { 90 - padding: 10px 12px; 91 - border: 1px solid #ddd; 92 - border-radius: 4px; 93 - font-size: 14px; 94 - min-width: 200px; 95 - } 96 - 97 - .admin-add-button { 98 - background-color: #007aff; 99 - color: white; 100 - border: none; 101 - border-radius: 4px; 102 - padding: 10px 16px; 103 - font-size: 14px; 104 - font-weight: 500; 105 - cursor: pointer; 106 - transition: background-color 0.2s; 107 - } 108 - 109 - .admin-add-button:hover { 110 - background-color: #0062cc; 111 - } 112 - 113 - /* Loading state */ 114 - .admin-loading { 115 - text-align: center; 116 - padding: 40px; 117 - color: #666; 118 - font-size: 16px; 119 - } 120 - 121 - /* Table */ 122 - .admin-table { 123 - width: 100%; 124 - border-collapse: collapse; 125 - font-size: 14px; 126 - } 127 - 128 - .admin-table th { 129 - background-color: #f5f5f5; 130 - color: #333; 131 - text-align: left; 132 - padding: 12px 16px; 133 - font-weight: 600; 134 - border-bottom: 2px solid #e0e0e0; 135 - } 136 - 137 - .admin-table th.sortable { 138 - cursor: pointer; 139 - } 140 - 141 - .admin-table th.sortable:hover { 142 - background-color: #e0e0e0; 143 - } 144 - 145 - .admin-table td { 146 - padding: 12px 16px; 147 - border-bottom: 1px solid #e0e0e0; 148 - vertical-align: middle; 149 - } 150 - 151 - .admin-table tr:hover { 152 - background-color: #f9f9f9; 153 - } 154 - 155 - .admin-table a { 156 - color: #007aff; 157 - text-decoration: none; 158 - } 159 - 160 - .admin-table a:hover { 161 - text-decoration: underline; 162 - } 163 - 164 - .admin-table .subcategory { 165 - color: #666; 166 - font-size: 13px; 167 - } 168 - 169 - /* Highlight new resources */ 170 - .admin-table tr.new-resource { 171 - background-color: #f0f8ff; 172 - } 173 - 174 - .admin-table tr.new-resource:hover { 175 - background-color: #e3f2fd; 176 - } 177 - 178 - /* Action buttons */ 179 - .action-buttons { 180 - display: flex; 181 - gap: 8px; 182 - flex-wrap: wrap; 183 - } 184 - 185 - .action-buttons button { 186 - padding: 6px 12px; 187 - border: none; 188 - border-radius: 4px; 189 - font-size: 13px; 190 - cursor: pointer; 191 - font-weight: 500; 192 - } 193 - 194 - .edit-button { 195 - background-color: #f0f0f0; 196 - color: #333; 197 - } 198 - 199 - .edit-button:hover { 200 - background-color: #e0e0e0; 201 - } 202 - 203 - .delete-button { 204 - background-color: #ffebee; 205 - color: #c62828; 206 - } 207 - 208 - .delete-button:hover { 209 - background-color: #ffcdd2; 210 - } 211 - 212 - .approve-button { 213 - background-color: #e8f5e9; 214 - color: #2e7d32; 215 - } 216 - 217 - .approve-button:hover { 218 - background-color: #c8e6c9; 219 - } 220 - 221 - .reject-button { 222 - background-color: #ffebee; 223 - color: #c62828; 224 - } 225 - 226 - .reject-button:hover { 227 - background-color: #ffcdd2; 228 - } 229 - 230 - .edit-approve-button { 231 - background-color: #e3f2fd; 232 - color: #0277bd; 233 - } 234 - 235 - .edit-approve-button:hover { 236 - background-color: #bbdefb; 237 - } 238 - 239 - /* No results */ 240 - .admin-no-results { 241 - padding: 40px; 242 - text-align: center; 243 - color: #666; 244 - background-color: #f9f9f9; 245 - border-radius: 4px; 246 - } 247 - 248 - /* Quality stars styling */ 249 - .quality-star { 250 - color: #ddd; 251 - font-size: 16px; 252 - } 253 - 254 - .quality-star.filled { 255 - color: #ffc107; 256 - } 257 - 258 - /* Modal for editing/creating resources */ 259 - .admin-modal { 260 - position: fixed; 261 - top: 0; 262 - left: 0; 263 - right: 0; 264 - bottom: 0; 265 - background-color: rgba(0, 0, 0, 0.5); 266 - display: flex; 267 - justify-content: center; 268 - align-items: center; 269 - z-index: 1000; 270 - } 271 - 272 - .admin-modal-content { 273 - background-color: white; 274 - border-radius: 8px; 275 - padding: 24px; 276 - width: 90%; 277 - max-width: 700px; 278 - max-height: 90vh; 279 - overflow-y: auto; 280 - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 281 - } 282 - 283 - .admin-modal-content h2 { 284 - margin-top: 0; 285 - margin-bottom: 24px; 286 - color: #333; 287 - } 288 - 289 - /* Form styling */ 290 - .form-group { 291 - margin-bottom: 16px; 292 - } 293 - 294 - .form-group label { 295 - display: block; 296 - margin-bottom: 8px; 297 - font-weight: 500; 298 - color: #333; 299 - } 300 - 301 - .form-group input[type="text"], 302 - .form-group input[type="url"], 303 - .form-group input[type="email"], 304 - .form-group select, 305 - .form-group textarea { 306 - width: 100%; 307 - padding: 10px 12px; 308 - border: 1px solid #ddd; 309 - border-radius: 4px; 310 - font-size: 14px; 311 - } 312 - 313 - .form-group textarea { 314 - resize: vertical; 315 - min-height: 80px; 316 - } 317 - 318 - .form-group input:disabled { 319 - background-color: #f5f5f5; 320 - cursor: not-allowed; 321 - } 322 - 323 - .form-group small { 324 - display: block; 325 - margin-top: 4px; 326 - color: #666; 327 - font-size: 12px; 328 - } 329 - 330 - .form-row { 331 - display: flex; 332 - gap: 16px; 333 - margin-bottom: 16px; 334 - } 335 - 336 - .form-row .form-group { 337 - flex: 1; 338 - margin-bottom: 0; 339 - } 340 - 341 - .checkbox-group { 342 - display: flex; 343 - align-items: center; 344 - } 345 - 346 - .checkbox-group label { 347 - display: flex; 348 - align-items: center; 349 - cursor: pointer; 350 - margin-bottom: 0; 351 - } 352 - 353 - .checkbox-group input[type="checkbox"] { 354 - margin-right: 8px; 355 - width: 18px; 356 - height: 18px; 357 - } 358 - 359 - .submission-info { 360 - background-color: #f5f5f5; 361 - padding: 12px; 362 - border-radius: 4px; 363 - margin-bottom: 0; 364 - font-size: 14px; 365 - line-height: 1.6; 366 - } 367 - 368 - .form-buttons { 369 - display: flex; 370 - justify-content: flex-end; 371 - gap: 12px; 372 - margin-top: 24px; 373 - } 374 - 375 - .save-button { 376 - background-color: #007aff; 377 - color: white; 378 - border: none; 379 - border-radius: 4px; 380 - padding: 10px 16px; 381 - font-size: 14px; 382 - font-weight: 500; 383 - cursor: pointer; 384 - transition: background-color 0.2s; 385 - } 386 - 387 - .save-button:hover { 388 - background-color: #0062cc; 389 - } 390 - 391 - .cancel-button { 392 - background-color: #f0f0f0; 393 - color: #333; 394 - border: none; 395 - border-radius: 4px; 396 - padding: 10px 16px; 397 - font-size: 14px; 398 - font-weight: 500; 399 - cursor: pointer; 400 - transition: background-color 0.2s; 401 - } 402 - 403 - .cancel-button:hover { 404 - background-color: #e0e0e0; 405 - } 406 - 407 - /* Responsive adjustments */ 408 - @media (max-width: 768px) { 409 - .admin-toolbar { 410 - flex-direction: column; 411 - align-items: stretch; 412 - } 413 - 414 - .admin-search { 415 - max-width: none; 416 - } 417 - 418 - .form-row { 419 - flex-direction: column; 420 - gap: 16px; 421 - } 422 - 423 - .form-buttons { 424 - flex-direction: column; 425 - } 426 - 427 - .form-buttons button { 428 - width: 100%; 429 - } 430 - 431 - .action-buttons { 432 - flex-direction: column; 433 - } 434 - 435 - .action-buttons button { 436 - width: 100%; 437 - } 438 - 439 - .admin-table { 440 - font-size: 13px; 441 - } 442 - 443 - .admin-table th, 444 - .admin-table td { 445 - padding: 8px; 446 - } 447 - } 448 - 449 - /* Optional: Add responsive table for mobile */ 450 - @media (max-width: 576px) { 451 - .admin-table { 452 - display: block; 453 - overflow-x: auto; 454 - white-space: nowrap; 455 - } 456 - }
-667
src/components/Admin/ResourcesManager.js
··· 1 - // src/components/Admin/ResourcesManager.jsx 2 - import React, { useState, useEffect } from 'react'; 3 - import { supabase } from '../../lib/supabase'; 4 - import './ResourcesManager.css'; 5 - 6 - const ResourcesManager = () => { 7 - const [activeTab, setActiveTab] = useState('resources'); 8 - const [resources, setResources] = useState([]); 9 - const [submissions, setSubmissions] = useState([]); 10 - const [categories, setCategories] = useState([]); 11 - const [isLoading, setIsLoading] = useState(true); 12 - const [error, setError] = useState(null); 13 - 14 - // For editing a resource 15 - const [editingResource, setEditingResource] = useState(null); 16 - const [formData, setFormData] = useState({ 17 - name: '', 18 - url: '', 19 - description: '', 20 - domain: '', 21 - category_id: '', 22 - subcategory: '', 23 - quality: 3, 24 - featured: false 25 - }); 26 - 27 - // For filtering and sorting 28 - const [searchTerm, setSearchTerm] = useState(''); 29 - const [categoryFilter, setCategoryFilter] = useState(''); 30 - const [sortField, setSortField] = useState('created_at'); 31 - const [sortDirection, setSortDirection] = useState('desc'); 32 - 33 - // Load data based on active tab 34 - useEffect(() => { 35 - fetchData(); 36 - }, [activeTab, sortField, sortDirection]); 37 - 38 - // Load categories 39 - useEffect(() => { 40 - async function fetchCategories() { 41 - try { 42 - const { data, error } = await supabase 43 - .from('categories') 44 - .select('*') 45 - .order('position'); 46 - 47 - if (error) throw error; 48 - setCategories(data || []); 49 - } catch (error) { 50 - console.error('Error fetching categories:', error); 51 - setError('Failed to load categories. Please refresh the page.'); 52 - } 53 - } 54 - 55 - fetchCategories(); 56 - }, []); 57 - 58 - // Fetch resources or submissions based on active tab 59 - const fetchData = async () => { 60 - setIsLoading(true); 61 - setError(null); 62 - 63 - try { 64 - if (activeTab === 'resources') { 65 - const { data, error } = await supabase 66 - .from('resources') 67 - .select(` 68 - *, 69 - category:categories(id, name, emoji) 70 - `) 71 - .order(sortField, { ascending: sortDirection === 'asc' }); 72 - 73 - if (error) throw error; 74 - setResources(data || []); 75 - } else if (activeTab === 'submissions') { 76 - const { data, error } = await supabase 77 - .from('resource_submissions') 78 - .select(` 79 - *, 80 - category:categories(id, name, emoji) 81 - `) 82 - .eq('status', 'pending') 83 - .order('created_at', { ascending: false }); 84 - 85 - if (error) throw error; 86 - setSubmissions(data || []); 87 - } 88 - } catch (error) { 89 - console.error(`Error fetching ${activeTab}:`, error); 90 - setError(`Failed to load ${activeTab}. Please try again.`); 91 - } finally { 92 - setIsLoading(false); 93 - } 94 - }; 95 - 96 - // Handle approving a submission 97 - const handleApproveSubmission = async (submission) => { 98 - try { 99 - // First, insert the submission as a new resource 100 - const { error: insertError } = await supabase 101 - .from('resources') 102 - .insert([{ 103 - name: submission.name, 104 - url: submission.url, 105 - description: submission.description, 106 - domain: submission.domain, 107 - category_id: submission.category_id, 108 - subcategory: submission.subcategory, 109 - quality: 3, // Default quality 110 - featured: false, 111 - is_new: true, 112 - created_at: new Date().toISOString() 113 - }]); 114 - 115 - if (insertError) throw insertError; 116 - 117 - // Then update the submission status to approved 118 - const { error: updateError } = await supabase 119 - .from('resource_submissions') 120 - .update({ status: 'approved' }) 121 - .eq('id', submission.id); 122 - 123 - if (updateError) throw updateError; 124 - 125 - // Refresh submissions list 126 - fetchData(); 127 - 128 - } catch (error) { 129 - console.error('Error approving submission:', error); 130 - setError('Failed to approve submission. Please try again.'); 131 - } 132 - }; 133 - 134 - // Handle rejecting a submission 135 - const handleRejectSubmission = async (submissionId) => { 136 - try { 137 - const { error } = await supabase 138 - .from('resource_submissions') 139 - .update({ status: 'rejected' }) 140 - .eq('id', submissionId); 141 - 142 - if (error) throw error; 143 - 144 - // Refresh submissions list 145 - fetchData(); 146 - 147 - } catch (error) { 148 - console.error('Error rejecting submission:', error); 149 - setError('Failed to reject submission. Please try again.'); 150 - } 151 - }; 152 - 153 - // Handle editing a resource 154 - const handleEditResource = (resource) => { 155 - setEditingResource(resource); 156 - setFormData({ 157 - name: resource.name, 158 - url: resource.url, 159 - description: resource.description, 160 - domain: resource.domain, 161 - category_id: resource.category_id, 162 - subcategory: resource.subcategory || '', 163 - quality: resource.quality, 164 - featured: resource.featured 165 - }); 166 - }; 167 - 168 - // Handle saving edited resource 169 - const handleSaveResource = async (e) => { 170 - e.preventDefault(); 171 - 172 - try { 173 - if (editingResource.id) { 174 - // Update existing resource 175 - const { error } = await supabase 176 - .from('resources') 177 - .update({ 178 - name: formData.name, 179 - url: formData.url, 180 - description: formData.description, 181 - domain: formData.domain, 182 - category_id: formData.category_id, 183 - subcategory: formData.subcategory || null, 184 - quality: formData.quality, 185 - featured: formData.featured, 186 - updated_at: new Date().toISOString() 187 - }) 188 - .eq('id', editingResource.id); 189 - 190 - if (error) throw error; 191 - } else { 192 - // Create new resource 193 - const { error } = await supabase 194 - .from('resources') 195 - .insert([{ 196 - name: formData.name, 197 - url: formData.url, 198 - description: formData.description, 199 - domain: formData.domain, 200 - category_id: formData.category_id, 201 - subcategory: formData.subcategory || null, 202 - quality: formData.quality, 203 - featured: formData.featured, 204 - is_new: true, 205 - created_at: new Date().toISOString() 206 - }]); 207 - 208 - if (error) throw error; 209 - } 210 - 211 - // Reset form and refresh data 212 - setEditingResource(null); 213 - fetchData(); 214 - 215 - } catch (error) { 216 - console.error('Error saving resource:', error); 217 - setError('Failed to save resource. Please try again.'); 218 - } 219 - }; 220 - 221 - // Handle deleting a resource 222 - const handleDeleteResource = async (resourceId) => { 223 - if (!window.confirm('Are you sure you want to delete this resource?')) { 224 - return; 225 - } 226 - 227 - try { 228 - const { error } = await supabase 229 - .from('resources') 230 - .delete() 231 - .eq('id', resourceId); 232 - 233 - if (error) throw error; 234 - 235 - // Refresh resources list 236 - fetchData(); 237 - 238 - } catch (error) { 239 - console.error('Error deleting resource:', error); 240 - setError('Failed to delete resource. Please try again.'); 241 - } 242 - }; 243 - 244 - // Handle form input changes 245 - const handleInputChange = (e) => { 246 - const { name, value, type, checked } = e.target; 247 - setFormData(prev => ({ 248 - ...prev, 249 - [name]: type === 'checkbox' ? checked : value 250 - })); 251 - }; 252 - 253 - // Auto-extract domain from URL 254 - useEffect(() => { 255 - if (formData.url) { 256 - try { 257 - const url = new URL(formData.url); 258 - setFormData(prev => ({ ...prev, domain: url.hostname })); 259 - } catch (error) { 260 - // Not a valid URL yet, ignore 261 - } 262 - } 263 - }, [formData.url]); 264 - 265 - // Filter resources based on search term and category 266 - const filteredResources = resources.filter(resource => { 267 - const matchesSearch = searchTerm === '' || 268 - resource.name.toLowerCase().includes(searchTerm.toLowerCase()) || 269 - resource.description.toLowerCase().includes(searchTerm.toLowerCase()) || 270 - resource.domain.toLowerCase().includes(searchTerm.toLowerCase()); 271 - 272 - const matchesCategory = categoryFilter === '' || 273 - resource.category_id === categoryFilter; 274 - 275 - return matchesSearch && matchesCategory; 276 - }); 277 - 278 - // Handle sort change 279 - const handleSortChange = (field) => { 280 - if (sortField === field) { 281 - // Toggle direction if clicking the same field 282 - setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); 283 - } else { 284 - // Default to descending for new field 285 - setSortField(field); 286 - setSortDirection('desc'); 287 - } 288 - }; 289 - 290 - // Render quality stars 291 - const renderQualityStars = (quality) => { 292 - const stars = []; 293 - for (let i = 1; i <= 5; i++) { 294 - stars.push( 295 - <span 296 - key={i} 297 - className={`quality-star ${i <= quality ? 'filled' : 'empty'}`} 298 - > 299 - 300 - </span> 301 - ); 302 - } 303 - return stars; 304 - }; 305 - 306 - return ( 307 - <div className="admin-resources-manager"> 308 - <h1>Resources Manager</h1> 309 - 310 - {error && <div className="admin-error-alert">{error}</div>} 311 - 312 - <div className="admin-tabs"> 313 - <button 314 - className={activeTab === 'resources' ? 'active' : ''} 315 - onClick={() => setActiveTab('resources')} 316 - > 317 - Resources 318 - </button> 319 - <button 320 - className={activeTab === 'submissions' ? 'active' : ''} 321 - onClick={() => setActiveTab('submissions')} 322 - > 323 - Submissions {submissions.length > 0 && <span className="badge">{submissions.length}</span>} 324 - </button> 325 - </div> 326 - 327 - {activeTab === 'resources' && ( 328 - <div className="resources-management"> 329 - <div className="admin-toolbar"> 330 - <div className="admin-search"> 331 - <input 332 - type="text" 333 - placeholder="Search resources..." 334 - value={searchTerm} 335 - onChange={(e) => setSearchTerm(e.target.value)} 336 - /> 337 - </div> 338 - 339 - <div className="admin-filter"> 340 - <select 341 - value={categoryFilter} 342 - onChange={(e) => setCategoryFilter(e.target.value)} 343 - > 344 - <option value="">All Categories</option> 345 - {categories.map(category => ( 346 - <option key={category.id} value={category.id}> 347 - {category.emoji} {category.name} 348 - </option> 349 - ))} 350 - </select> 351 - </div> 352 - 353 - <button 354 - className="admin-add-button" 355 - onClick={() => { 356 - setEditingResource({}); 357 - setFormData({ 358 - name: '', 359 - url: '', 360 - description: '', 361 - domain: '', 362 - category_id: '', 363 - subcategory: '', 364 - quality: 3, 365 - featured: false 366 - }); 367 - }} 368 - > 369 - Add New Resource 370 - </button> 371 - </div> 372 - 373 - {isLoading ? ( 374 - <div className="admin-loading">Loading resources...</div> 375 - ) : ( 376 - <> 377 - <table className="admin-table"> 378 - <thead> 379 - <tr> 380 - <th onClick={() => handleSortChange('name')} className="sortable"> 381 - Name {sortField === 'name' && (sortDirection === 'asc' ? '↑' : '↓')} 382 - </th> 383 - <th>Domain</th> 384 - <th onClick={() => handleSortChange('category_id')} className="sortable"> 385 - Category {sortField === 'category_id' && (sortDirection === 'asc' ? '↑' : '↓')} 386 - </th> 387 - <th onClick={() => handleSortChange('quality')} className="sortable"> 388 - Quality {sortField === 'quality' && (sortDirection === 'asc' ? '↑' : '↓')} 389 - </th> 390 - <th onClick={() => handleSortChange('created_at')} className="sortable"> 391 - Added {sortField === 'created_at' && (sortDirection === 'asc' ? '↑' : '↓')} 392 - </th> 393 - <th>Featured</th> 394 - <th>Actions</th> 395 - </tr> 396 - </thead> 397 - <tbody> 398 - {filteredResources.map(resource => ( 399 - <tr key={resource.id} className={resource.is_new ? 'new-resource' : ''}> 400 - <td>{resource.name}</td> 401 - <td> 402 - <a href={resource.url} target="_blank" rel="noopener noreferrer"> 403 - {resource.domain} 404 - </a> 405 - </td> 406 - <td> 407 - {resource.category?.emoji} {resource.category?.name} 408 - {resource.subcategory && <span className="subcategory"> / {resource.subcategory}</span>} 409 - </td> 410 - <td>{renderQualityStars(resource.quality)}</td> 411 - <td>{new Date(resource.created_at).toLocaleDateString()}</td> 412 - <td>{resource.featured ? '✅' : '❌'}</td> 413 - <td className="action-buttons"> 414 - <button className="edit-button" onClick={() => handleEditResource(resource)}>Edit</button> 415 - <button 416 - className="delete-button" 417 - onClick={() => handleDeleteResource(resource.id)} 418 - > 419 - Delete 420 - </button> 421 - </td> 422 - </tr> 423 - ))} 424 - </tbody> 425 - </table> 426 - 427 - {filteredResources.length === 0 && ( 428 - <div className="admin-no-results">No resources found matching your criteria.</div> 429 - )} 430 - </> 431 - )} 432 - </div> 433 - )} 434 - 435 - {activeTab === 'submissions' && ( 436 - <div className="submissions-management"> 437 - <h2>Pending Submissions</h2> 438 - 439 - {isLoading ? ( 440 - <div className="admin-loading">Loading submissions...</div> 441 - ) : ( 442 - <> 443 - {submissions.length === 0 ? ( 444 - <div className="admin-no-results">No pending submissions.</div> 445 - ) : ( 446 - <table className="admin-table"> 447 - <thead> 448 - <tr> 449 - <th>Name</th> 450 - <th>URL</th> 451 - <th>Category</th> 452 - <th>Submitted By</th> 453 - <th>Date</th> 454 - <th>Actions</th> 455 - </tr> 456 - </thead> 457 - <tbody> 458 - {submissions.map(submission => ( 459 - <tr key={submission.id}> 460 - <td>{submission.name}</td> 461 - <td> 462 - <a href={submission.url} target="_blank" rel="noopener noreferrer"> 463 - {submission.domain} 464 - </a> 465 - </td> 466 - <td> 467 - {submission.category?.emoji} {submission.category?.name} 468 - {submission.subcategory && <span className="subcategory"> / {submission.subcategory}</span>} 469 - </td> 470 - <td> 471 - {submission.submitter_handle || submission.submitter_email || 'Anonymous'} 472 - </td> 473 - <td>{new Date(submission.created_at).toLocaleDateString()}</td> 474 - <td className="action-buttons"> 475 - <button 476 - className="approve-button" 477 - onClick={() => handleApproveSubmission(submission)} 478 - > 479 - Approve 480 - </button> 481 - <button 482 - className="reject-button" 483 - onClick={() => handleRejectSubmission(submission.id)} 484 - > 485 - Reject 486 - </button> 487 - <button 488 - className="edit-approve-button" 489 - onClick={() => { 490 - setEditingResource({ 491 - ...submission, 492 - isSubmission: true 493 - }); 494 - setFormData({ 495 - name: submission.name, 496 - url: submission.url, 497 - description: submission.description, 498 - domain: submission.domain, 499 - category_id: submission.category_id, 500 - subcategory: submission.subcategory || '', 501 - quality: 3, 502 - featured: false 503 - }); 504 - }} 505 - > 506 - Edit & Approve 507 - </button> 508 - </td> 509 - </tr> 510 - ))} 511 - </tbody> 512 - </table> 513 - )} 514 - </> 515 - )} 516 - </div> 517 - )} 518 - 519 - {/* Edit/Create Resource Modal */} 520 - {editingResource && ( 521 - <div className="admin-modal"> 522 - <div className="admin-modal-content"> 523 - <h2>{editingResource.id ? 'Edit Resource' : 'Add New Resource'}</h2> 524 - 525 - <form onSubmit={handleSaveResource}> 526 - <div className="form-group"> 527 - <label htmlFor="name">Resource Name*</label> 528 - <input 529 - type="text" 530 - id="name" 531 - name="name" 532 - value={formData.name} 533 - onChange={handleInputChange} 534 - required 535 - /> 536 - </div> 537 - 538 - <div className="form-group"> 539 - <label htmlFor="url">URL*</label> 540 - <input 541 - type="url" 542 - id="url" 543 - name="url" 544 - value={formData.url} 545 - onChange={handleInputChange} 546 - required 547 - /> 548 - </div> 549 - 550 - <div className="form-group"> 551 - <label htmlFor="description">Description*</label> 552 - <textarea 553 - id="description" 554 - name="description" 555 - value={formData.description} 556 - onChange={handleInputChange} 557 - required 558 - rows="3" 559 - /> 560 - </div> 561 - 562 - <div className="form-group"> 563 - <label htmlFor="domain">Domain</label> 564 - <input 565 - type="text" 566 - id="domain" 567 - name="domain" 568 - value={formData.domain} 569 - onChange={handleInputChange} 570 - disabled 571 - /> 572 - <small>This field is auto-filled from the URL.</small> 573 - </div> 574 - 575 - <div className="form-row"> 576 - <div className="form-group"> 577 - <label htmlFor="category_id">Category*</label> 578 - <select 579 - id="category_id" 580 - name="category_id" 581 - value={formData.category_id} 582 - onChange={handleInputChange} 583 - required 584 - > 585 - <option value="">Select a category</option> 586 - {categories.map(category => ( 587 - <option key={category.id} value={category.id}> 588 - {category.emoji} {category.name} 589 - </option> 590 - ))} 591 - </select> 592 - </div> 593 - 594 - <div className="form-group"> 595 - <label htmlFor="subcategory">Subcategory</label> 596 - <input 597 - type="text" 598 - id="subcategory" 599 - name="subcategory" 600 - value={formData.subcategory} 601 - onChange={handleInputChange} 602 - placeholder="e.g., Feed Tools" 603 - /> 604 - </div> 605 - </div> 606 - 607 - <div className="form-row"> 608 - <div className="form-group"> 609 - <label htmlFor="quality">Quality Rating*</label> 610 - <select 611 - id="quality" 612 - name="quality" 613 - value={formData.quality} 614 - onChange={handleInputChange} 615 - required 616 - > 617 - <option value="5">5 - Excellent</option> 618 - <option value="4">4 - Very Good</option> 619 - <option value="3">3 - Good</option> 620 - <option value="2">2 - Fair</option> 621 - <option value="1">1 - Poor</option> 622 - </select> 623 - </div> 624 - 625 - <div className="form-group checkbox-group"> 626 - <label> 627 - <input 628 - type="checkbox" 629 - name="featured" 630 - checked={formData.featured} 631 - onChange={handleInputChange} 632 - /> 633 - Featured Resource 634 - </label> 635 - </div> 636 - </div> 637 - 638 - {editingResource.isSubmission && ( 639 - <div className="form-group"> 640 - <p className="submission-info"> 641 - <strong>Submitted by:</strong> {editingResource.submitter_handle || editingResource.submitter_email || 'Anonymous'}<br /> 642 - <strong>Submitted on:</strong> {new Date(editingResource.created_at).toLocaleString()} 643 - </p> 644 - </div> 645 - )} 646 - 647 - <div className="form-buttons"> 648 - <button type="submit" className="save-button"> 649 - {editingResource.isSubmission ? 'Approve with Changes' : 'Save Resource'} 650 - </button> 651 - <button 652 - type="button" 653 - className="cancel-button" 654 - onClick={() => setEditingResource(null)} 655 - > 656 - Cancel 657 - </button> 658 - </div> 659 - </form> 660 - </div> 661 - </div> 662 - )} 663 - </div> 664 - ); 665 - }; 666 - 667 - export default ResourcesManager;