This repository has no description
0

Configure Feed

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

add admin panel

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