This repository has no description
0

Configure Feed

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

enhance admin panel

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