From 12efa0affd4af14e50f127be22a43b86bfc47e79 Mon Sep 17 00:00:00 2001 From: damedotblog Date: Fri, 28 Feb 2025 21:34:26 -0500 Subject: [PATCH] enhance admin panel --- src/components/Admin/AdminPanel.css | 181 ++++++++++++- src/components/Admin/AdminPanel.js | 297 +++++++++++++++++----- src/components/Resources/ResourceAdmin.js | 151 ----------- src/components/UserProfile/UserProfile.js | 1 - 4 files changed, 412 insertions(+), 218 deletions(-) delete mode 100644 src/components/Resources/ResourceAdmin.js diff --git a/src/components/Admin/AdminPanel.css b/src/components/Admin/AdminPanel.css index 5e8fc6d..e86bf12 100644 --- a/src/components/Admin/AdminPanel.css +++ b/src/components/Admin/AdminPanel.css @@ -56,6 +56,39 @@ font-size: 18px; } + .sidebar-filters { + padding: 10px 15px; + border-bottom: 1px solid #ddd; + background-color: #f8f9fa; + } + + .filter-group { + margin-bottom: 8px; + } + + .search-input { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + margin-bottom: 8px; + } + + .status-filter, + .completeness-filter { + width: 49%; + padding: 6px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 13px; + margin-right: 2%; + } + + .completeness-filter { + margin-right: 0; + } + .resources-list { overflow-y: auto; flex-grow: 1; @@ -63,12 +96,30 @@ .resource-item { display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 15px; + flex-direction: column; + padding: 0; border-bottom: 1px solid #eee; cursor: pointer; transition: background-color 0.2s; + position: relative; + } + + .resource-completeness-indicator { + height: 3px; + width: 100%; + background-color: #f0f0f0; + } + + .completeness-bar { + height: 100%; + background-color: #52c41a; + transition: width 0.3s; + } + + .resource-item-content { + display: flex; + justify-content: space-between; + padding: 12px 15px; } .resource-item:hover { @@ -80,6 +131,18 @@ border-left: 3px solid #1890ff; } + .resource-item.status-draft .completeness-bar { + background-color: #faad14; + } + + .resource-item.status-review .completeness-bar { + background-color: #1890ff; + } + + .resource-item.status-published .completeness-bar { + background-color: #52c41a; + } + .resource-item-name { overflow: hidden; text-overflow: ellipsis; @@ -87,6 +150,50 @@ flex-grow: 1; } + .resource-item-meta { + display: flex; + gap: 5px; + font-size: 12px; + margin-top: 4px; + } + + .status-badge { + padding: 2px 6px; + border-radius: 10px; + font-size: 11px; + text-transform: uppercase; + font-weight: bold; + } + + .status-badge.status-draft { + background-color: #fff7e6; + color: #faad14; + border: 1px solid #faad14; + } + + .status-badge.status-review { + background-color: #e6f7ff; + color: #1890ff; + border: 1px solid #1890ff; + } + + .status-badge.status-published { + background-color: #f6ffed; + color: #52c41a; + border: 1px solid #52c41a; + } + + .featured-badge { + background-color: #f9f0ff; + color: #722ed1; + border: 1px solid #722ed1; + padding: 2px 6px; + border-radius: 10px; + font-size: 11px; + text-transform: uppercase; + font-weight: bold; + } + .resource-item-actions { display: flex; gap: 5px; @@ -107,6 +214,74 @@ height: 100%; } + .editor-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; + } + + .editor-header h2 { + margin: 0; + color: #333; + } + + .floating-actions { + display: flex; + align-items: center; + gap: 15px; + } + + .floating-save-button { + background-color: #52c41a; + color: white; + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + } + + .floating-save-button:hover { + background-color: #73d13d; + } + + .status-selector { + display: flex; + align-items: center; + gap: 10px; + } + + .status-selector span { + font-weight: 500; + color: #555; + } + + .status-buttons { + display: flex; + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; + } + + .status-button { + padding: 6px 10px; + background-color: #f5f5f5; + border: none; + border-right: 1px solid #ddd; + cursor: pointer; + font-size: 13px; + } + + .status-button:last-child { + border-right: none; + } + + .status-button.active { + background-color: #1890ff; + color: white; + } + .resource-editor h2 { margin-top: 0; margin-bottom: 20px; diff --git a/src/components/Admin/AdminPanel.js b/src/components/Admin/AdminPanel.js index c13afa3..cc272a4 100644 --- a/src/components/Admin/AdminPanel.js +++ b/src/components/Admin/AdminPanel.js @@ -1,5 +1,5 @@ // src/components/Admin/AdminPanel.jsx -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { supabase } from '../../lib/supabase'; import './AdminPanel.css'; @@ -12,6 +12,9 @@ const AdminPanel = () => { const [isLoading, setIsLoading] = useState(true); const [isAuthenticated, setIsAuthenticated] = useState(false); const [authError, setAuthError] = useState(null); + const [statusFilter, setStatusFilter] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + const [completenessFilter, setCompletenessFilter] = useState(0); // Login state const [email, setEmail] = useState(''); @@ -26,59 +29,15 @@ const AdminPanel = () => { featured: false, position: 0, selectedCategories: [], - selectedTags: [] + selectedTags: [], + status: 'draft' }); // Alert state const [alert, setAlert] = useState({ show: false, message: '', type: '' }); - // Check authentication on mount - useEffect(() => { - const checkAuth = async () => { - const { data: { session } } = await supabase.auth.getSession(); - setIsAuthenticated(!!session); - - if (session) { - fetchAllData(); - } else { - setIsLoading(false); - } - }; - - checkAuth(); - }, []); - - // Login handler - const handleLogin = async (e) => { - e.preventDefault(); - setIsLoading(true); - setAuthError(null); - - try { - const { data, error } = await supabase.auth.signInWithPassword({ - email, - password - }); - - if (error) throw error; - - setIsAuthenticated(true); - fetchAllData(); - } catch (error) { - console.error('Error logging in:', error); - setAuthError(error.message); - setIsLoading(false); - } - }; - - // Logout handler - const handleLogout = async () => { - await supabase.auth.signOut(); - setIsAuthenticated(false); - }; - // Fetch all required data from Supabase - const fetchAllData = async () => { + const fetchAllData = useCallback(async () => { setIsLoading(true); try { // Fetch resources @@ -129,10 +88,19 @@ const AdminPanel = () => { .filter(rt => rt.resource_id === resource.id) .map(rt => rt.tag_id); + // Calculate completeness for UI + const completeness = calculateCompleteness({ + ...resource, + categoryIds: resourceCats, + tagIds: resourceTs + }); + return { ...resource, categoryIds: resourceCats, - tagIds: resourceTs + tagIds: resourceTs, + completeness, + status: resource.status || 'draft' }; }); @@ -146,7 +114,23 @@ const AdminPanel = () => { } finally { setIsLoading(false); } - }; + }, []); + + // Check authentication on mount + useEffect(() => { + const checkAuth = async () => { + const { data: { session } } = await supabase.auth.getSession(); + setIsAuthenticated(!!session); + + if (session) { + fetchAllData(); + } else { + setIsLoading(false); + } + }; + + checkAuth(); + }, [fetchAllData]); // Handle resource selection const handleSelectResource = (resource) => { @@ -159,9 +143,88 @@ const AdminPanel = () => { featured: resource.featured || false, position: resource.position || 0, selectedCategories: resource.categoryIds || [], - selectedTags: resource.tagIds || [] + selectedTags: resource.tagIds || [], + status: resource.status || 'draft' }); }; + + // Handle keyboard navigation + const handleKeyNavigation = useCallback((e) => { + if (!selectedResource || resources.length === 0) return; + + const currentIndex = resources.findIndex(r => r.id === selectedResource.id); + let newIndex; + + switch(e.key) { + case "ArrowDown": + newIndex = (currentIndex + 1) % resources.length; + handleSelectResource(resources[newIndex]); + break; + case "ArrowUp": + newIndex = (currentIndex - 1 + resources.length) % resources.length; + handleSelectResource(resources[newIndex]); + break; + default: + break; + } + }, [selectedResource, resources]); + + // Add event listener for keyboard navigation + useEffect(() => { + document.addEventListener('keydown', handleKeyNavigation); + return () => { + document.removeEventListener('keydown', handleKeyNavigation); + }; + }, [handleKeyNavigation]); + + // Calculate resource completeness percentage + const calculateCompleteness = (resource) => { + let total = 4; // Required fields: name, description, url + let filled = 0; + + if (resource.name) filled++; + if (resource.description) filled++; + if (resource.url) filled++; + if (resource.domain) filled++; + + // Categories and tags are optional but contribute to completeness + if (resource.categoryIds && resource.categoryIds.length > 0) filled++; + total++; + + if (resource.tagIds && resource.tagIds.length > 0) filled++; + total++; + + return Math.round((filled / total) * 100); + }; + + // Login handler + const handleLogin = async (e) => { + e.preventDefault(); + setIsLoading(true); + setAuthError(null); + + try { + const { error } = await supabase.auth.signInWithPassword({ + email, + password + }); + + if (error) throw error; + + setIsAuthenticated(true); + fetchAllData(); + } catch (error) { + console.error('Error logging in:', error); + setAuthError(error.message); + setIsLoading(false); + } + }; + + // Logout handler + const handleLogout = async () => { + await supabase.auth.signOut(); + setIsAuthenticated(false); + }; // Handle form input changes const handleInputChange = (e) => { @@ -171,6 +234,28 @@ const AdminPanel = () => { [name]: type === 'checkbox' ? checked : value }); }; + + // Handle status change + const handleStatusChange = (status) => { + setFormData({ + ...formData, + status + }); + }; + + // Filter resources based on status, search query, and completeness + const filteredResources = resources.filter(resource => { + // Status filter + if (statusFilter !== 'all' && resource.status !== statusFilter) return false; + + // Search query filter + if (searchQuery && !resource.name.toLowerCase().includes(searchQuery.toLowerCase())) return false; + + // Completeness filter + if (completenessFilter > 0 && resource.completeness < completenessFilter) return false; + + return true; + }); // Handle category selection changes const handleCategoryChange = (categoryId) => { @@ -225,7 +310,8 @@ const AdminPanel = () => { featured: false, position: resources.length + 1, selectedCategories: [], - selectedTags: [] + selectedTags: [], + status: 'draft' }); }; @@ -239,7 +325,7 @@ const AdminPanel = () => { // Save resource changes const handleSaveResource = async (e) => { - e.preventDefault(); + if (e && e.preventDefault) e.preventDefault(); setIsLoading(true); try { @@ -250,6 +336,7 @@ const AdminPanel = () => { domain: formData.domain, featured: formData.featured, position: formData.position, + status: formData.status, updated_at: new Date().toISOString() }; @@ -387,14 +474,13 @@ const AdminPanel = () => { setIsLoading(true); try { - const { data, error } = await supabase + const { error } = await supabase .from('categories') .insert({ name: categoryName, emoji: emoji, created_at: new Date().toISOString() - }) - .select(); + }); if (error) throw error; @@ -416,13 +502,12 @@ const AdminPanel = () => { setIsLoading(true); try { - const { data, error } = await supabase + const { error } = await supabase .from('tags') .insert({ name: tagName, created_at: new Date().toISOString() - }) - .select(); + }); if (error) throw error; @@ -506,14 +591,63 @@ const AdminPanel = () => { + New Resource +
+
+ setSearchQuery(e.target.value)} + className="search-input" + /> +
+
+ + +
+
- {resources.map(resource => ( + {filteredResources.map(resource => (
handleSelectResource(resource)} > -
{resource.name}
+
+
+
+
+
{resource.name}
+
+ + {resource.status} + + {resource.featured && Featured} +
+
+ + +
+
+ +
+
diff --git a/src/components/Resources/ResourceAdmin.js b/src/components/Resources/ResourceAdmin.js deleted file mode 100644 index 17d7fa2..0000000 --- a/src/components/Resources/ResourceAdmin.js +++ /dev/null @@ -1,151 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - getResources, - getPendingSubmissions, - approveSubmission -} from '../lib/supabase'; - -const ResourceAdmin = () => { - const [resources, setResources] = useState([]); - const [submissions, setSubmissions] = useState([]); - const [activeTab, setActiveTab] = useState('resources'); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - fetchData(); - }, [activeTab]); - - async function fetchData() { - setIsLoading(true); - try { - if (activeTab === 'resources') { - const data = await getResources(); - setResources(data); - } else if (activeTab === 'submissions') { - const data = await getPendingSubmissions(); - setSubmissions(data); - } - } catch (error) { - console.error('Error fetching data:', error); - } finally { - setIsLoading(false); - } - } - - const handleApproveSubmission = async (id) => { - try { - await approveSubmission(id); - // Refresh the submissions list - const data = await getPendingSubmissions(); - setSubmissions(data); - } catch (error) { - console.error('Error approving submission:', error); - } - }; - - // More admin functions here... - - return ( -
-

Resource Admin

- -
- - -
- - {isLoading ? ( -

Loading...

- ) : ( -
- {activeTab === 'resources' && ( -
-

Resources ({resources.length})

- {/* Resource management table */} - - - - - - - - - - - - {resources.map(resource => ( - - - - - - - - ))} - -
NameCategoryQualityFeaturedActions
{resource.name}{resource.category.name}{resource.quality}{resource.featured ? 'Yes' : 'No'} - - -
-
- )} - - {activeTab === 'submissions' && ( -
-

Pending Submissions ({submissions.length})

- {/* Submission review table */} - {submissions.length === 0 ? ( -

No pending submissions.

- ) : ( - - - - - - - - - - - - {submissions.map(submission => ( - - - - - - - - ))} - -
NameURLCategorySubmittedActions
{submission.name} - - {submission.domain} - - {submission.category.name}{new Date(submission.created_at).toLocaleDateString()} - - - -
- )} -
- )} -
- )} -
- ); -}; - -export default ResourceAdmin; \ No newline at end of file diff --git a/src/components/UserProfile/UserProfile.js b/src/components/UserProfile/UserProfile.js index af0e957..fcecb3a 100644 --- a/src/components/UserProfile/UserProfile.js +++ b/src/components/UserProfile/UserProfile.js @@ -15,7 +15,6 @@ import AltTextCard from "./components/AltTextCard"; import ActivityCard from "./components/ActivityCard"; import ScoreBreakdownCard from "./components/ScoreBreakdownCard"; import ErrorPage from "../ErrorPage/ErrorPage"; -import { supabase } from '../../lib/supabase'; import _ from 'lodash'; import "react-grid-layout/css/styles.css"; -- 2.51.2