diff --git a/src/components/Dashboard/listings/ListingCard.tsx b/src/components/Dashboard/listings/ListingCard.tsx index 3609152d..0c9e4098 100644 --- a/src/components/Dashboard/listings/ListingCard.tsx +++ b/src/components/Dashboard/listings/ListingCard.tsx @@ -91,8 +91,7 @@ const ListingCard: React.FC = ({ listing, onDelete, onEdit }) {/* Status Badge Only */}
- - {statusConfig.text} +
diff --git a/src/components/User/DashboardContent/ListingsContent.tsx b/src/components/User/DashboardContent/ListingsContent.tsx index 703e3467..a1a942da 100644 --- a/src/components/User/DashboardContent/ListingsContent.tsx +++ b/src/components/User/DashboardContent/ListingsContent.tsx @@ -22,7 +22,12 @@ import { Clock, XCircle, TrendingUp, - Calendar + Calendar, + ChevronDown, + BarChart3, + Target, + Activity, + Shield } from 'lucide-react'; interface ListingWithMatchStatus extends SkillListing { @@ -49,6 +54,10 @@ const ListingsContent: React.FC = ({ onNavigateToSkills }) const [statusFilter, setStatusFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all'); + // Custom dropdown states for mobile + const [showStatusDropdown, setShowStatusDropdown] = useState(false); + const [showCategoryDropdown, setShowCategoryDropdown] = useState(false); + // Delete confirmation state const [deletingListingId, setDeletingListingId] = useState(null); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); @@ -71,6 +80,25 @@ const ListingsContent: React.FC = ({ onNavigateToSkills }) updateStats(); }, [listings]); + // Close all dropdowns + const closeAllDropdowns = () => { + setShowStatusDropdown(false); + setShowCategoryDropdown(false); + }; + + // Close dropdowns when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (!target.closest('.custom-dropdown')) { + closeAllDropdowns(); + } + }; + + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + }, []); + // Function to fetch user listings and skills const fetchUserData = async () => { setLoading(true); @@ -240,63 +268,109 @@ const ListingsContent: React.FC = ({ onNavigateToSkills }) } }; - // Render statistics cards + // Custom dropdown component for mobile + const CustomDropdown = ({ + value, + options, + onChange, + placeholder, + isOpen, + setIsOpen, + renderValue, + className = "" + }: { + value: string; + options: { value: string; label: string }[]; + onChange: (value: string) => void; + placeholder: string; + isOpen: boolean; + setIsOpen: (open: boolean) => void; + renderValue?: (value: string) => string; + className?: string; + }) => ( +
+ + + {isOpen && ( +
+ {options.map((option) => ( + + ))} +
+ )} +
+ ); + + // Render enhanced statistics cards const renderStatsCards = () => ( -
-
-
- -
-

Total Listings

-

{stats.total}

-
+
+
+
+
+
{stats.total}
+
Total Listings
-
-
- -
-

Active

-

{stats.active}

-
+
+
+
+
{stats.active}
+
Active
-
-
- -
-

Not Active

-

{stats.notActive}

-
+
+
+
+
{stats.notActive}
+
Not Active
-
-
- -
-

In Matches

-

{stats.inMatches}

-
+
+
+
+
{stats.inMatches}
+
In Matches
); return ( -
+
{/* Header */} -
-
-

My Skill Listings

-

Manage your skill exchange listings and track their status

-
+
+

My Skill Listings

- - )} - {statusFilter !== 'all' && ( - - Status: {statusFilter} - - - )} - {categoryFilter !== 'all' && ( - - Category: {categoryFilter} - - - )} + {/* Compact Info Row */} +
+
+ {listings.length > 0 && ( + Showing {filteredListings.length} of {listings.length} listings + )} +
+
+ {stats.inMatches > 0 && ( +
+ + + {stats.inMatches} protected by matches +
)} - - {/* Results count */} -
- Showing {filteredListings.length} of {listings.length} listings -
- )} +
{/* Loading State */} {loading && ( @@ -459,18 +555,19 @@ const ListingsContent: React.FC = ({ onNavigateToSkills }) {/* No Results State */} {!loading && listings.length > 0 && filteredListings.length === 0 && (
- -

No listings found

-

Try adjusting your search criteria or filters

+ +

No listings match your filters

+

Try adjusting your search or filter criteria

)} diff --git a/src/components/User/DashboardContent/MySkillsContent.tsx b/src/components/User/DashboardContent/MySkillsContent.tsx index 82fbb80d..9185b6ce 100644 --- a/src/components/User/DashboardContent/MySkillsContent.tsx +++ b/src/components/User/DashboardContent/MySkillsContent.tsx @@ -1,7 +1,7 @@ // File: src/components/User/DashboardContent/MySkillsContent.tsx 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { getUserSkills, deleteUserSkill } from '@/services/skillService'; import { getSkillsUsedInMatches } from '@/services/trendingService'; import { UserSkill } from '@/types/userSkill'; @@ -9,7 +9,7 @@ import { useToast } from '@/lib/context/ToastContext'; import AddSkillForm from '@/components/Dashboard/skills/AddSkillForm'; import EditSkillForm from '@/components/Dashboard/skills/EditSkillForm'; import ConfirmationModal from '@/components/Dashboard/listings/ConfirmationModal'; -import { Info, AlertTriangle, Users, Calendar } from 'lucide-react'; +import { Info, AlertTriangle, Users, Calendar, Search, Filter, BarChart3, Award, Target, Activity, BookOpen, Settings, TrendingUp, Layers, Eye, Edit2, Trash2, Lock, ChevronDown } from 'lucide-react'; const SkillsPage = () => { const { showToast } = useToast(); @@ -27,13 +27,21 @@ const SkillsPage = () => { const [usedSkillIds, setUsedSkillIds] = useState([]); const [matchUsedSkills, setMatchUsedSkills] = useState(null); - // Fetch user skills and used skill IDs on component mount - useEffect(() => { - fetchUserData(); - }, []); + // Filter and search states + const [searchTerm, setSearchTerm] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('all'); + const [selectedProficiency, setSelectedProficiency] = useState('all'); + const [selectedUsageStatus, setSelectedUsageStatus] = useState('all'); + const [sortBy, setSortBy] = useState('name'); + + // Custom dropdown states for mobile + const [showCategoryDropdown, setShowCategoryDropdown] = useState(false); + const [showProficiencyDropdown, setShowProficiencyDropdown] = useState(false); + const [showUsageDropdown, setShowUsageDropdown] = useState(false); + const [showSortDropdown, setShowSortDropdown] = useState(false); // Function to fetch user skills and used skill IDs - const fetchUserData = async () => { + const fetchUserData = React.useCallback(async () => { setLoading(true); try { // Fetch skills @@ -69,7 +77,12 @@ const SkillsPage = () => { } finally { setLoading(false); } - }; + }, [showToast]); + + // Fetch user skills and used skill IDs on component mount + useEffect(() => { + fetchUserData(); + }, [fetchUserData]); // Check if a skill is used in a listing const isSkillUsedInListing = (skillId: string) => { @@ -143,8 +156,88 @@ const SkillsPage = () => { } }; - // Group skills by category - const skillsByCategory = skills.reduce((acc, skill) => { + // Calculate overall statistics + const skillStats = useMemo(() => { + const totalSkills = skills.length; + const expertSkills = skills.filter(s => s.proficiencyLevel === 'Expert').length; + const intermediateSkills = skills.filter(s => s.proficiencyLevel === 'Intermediate').length; + const beginnerSkills = skills.filter(s => s.proficiencyLevel === 'Beginner').length; + const usedInListings = skills.filter(s => isSkillUsedInListing(s.id)).length; + const usedInMatches = skills.filter(s => isSkillUsedInMatches(s.id)).length; + const categories = [...new Set(skills.map(s => s.categoryName))].length; + + return { + total: totalSkills, + expert: expertSkills, + intermediate: intermediateSkills, + beginner: beginnerSkills, + usedInListings, + usedInMatches, + categories + }; + }, [skills, usedSkillIds, matchUsedSkills]); + + // Get unique categories for filter dropdown + const categories = useMemo(() => { + return [...new Set(skills.map(skill => skill.categoryName))]; + }, [skills]); + + // Filter and sort skills + const filteredAndSortedSkills = useMemo(() => { + let filtered = skills.filter(skill => { + // Search filter + if (searchTerm && !skill.skillTitle.toLowerCase().includes(searchTerm.toLowerCase())) { + return false; + } + + // Category filter + if (selectedCategory !== 'all' && skill.categoryName !== selectedCategory) { + return false; + } + + // Proficiency filter + if (selectedProficiency !== 'all' && skill.proficiencyLevel !== selectedProficiency) { + return false; + } + + // Usage status filter + if (selectedUsageStatus !== 'all') { + const usedInListing = isSkillUsedInListing(skill.id); + const usedInMatch = isSkillUsedInMatches(skill.id); + + if (selectedUsageStatus === 'used' && !usedInListing && !usedInMatch) return false; + if (selectedUsageStatus === 'unused' && (usedInListing || usedInMatch)) return false; + if (selectedUsageStatus === 'listings' && !usedInListing) return false; + if (selectedUsageStatus === 'matches' && !usedInMatch) return false; + } + + return true; + }); + + // Sort skills + filtered.sort((a, b) => { + switch (sortBy) { + case 'name': + return a.skillTitle.localeCompare(b.skillTitle); + case 'category': + return a.categoryName.localeCompare(b.categoryName); + case 'proficiency': + const proficiencyOrder = { 'Expert': 3, 'Intermediate': 2, 'Beginner': 1 }; + return proficiencyOrder[b.proficiencyLevel] - proficiencyOrder[a.proficiencyLevel]; + case 'usage': + const aUsage = (isSkillUsedInListing(a.id) ? 2 : 0) + (isSkillUsedInMatches(a.id) ? 1 : 0); + const bUsage = (isSkillUsedInListing(b.id) ? 2 : 0) + (isSkillUsedInMatches(b.id) ? 1 : 0); + return bUsage - aUsage; + default: + return 0; + } + }); + + return filtered; + }, [skills, searchTerm, selectedCategory, selectedProficiency, selectedUsageStatus, sortBy, usedSkillIds, matchUsedSkills]); + + // Group filtered skills by category + const skillsByCategory = filteredAndSortedSkills.reduce((acc, skill) => { if (!acc[skill.categoryName]) { acc[skill.categoryName] = []; } @@ -212,6 +305,90 @@ const SkillsPage = () => { return indicators; }; + // Close all dropdowns + const closeAllDropdowns = () => { + setShowCategoryDropdown(false); + setShowProficiencyDropdown(false); + setShowUsageDropdown(false); + setShowSortDropdown(false); + }; + + // Close dropdowns when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (!target.closest('.custom-dropdown')) { + closeAllDropdowns(); + } + }; + + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + }, []); + + // Truncate category names for mobile + const truncateCategory = (category: string, maxLength: number = 20) => { + if (category.length <= maxLength) return category; + return category.slice(0, maxLength) + '...'; + }; + + // Custom dropdown component for mobile + const CustomDropdown = ({ + value, + options, + onChange, + placeholder, + isOpen, + setIsOpen, + renderValue, + className = "" + }: { + value: string; + options: { value: string; label: string }[]; + onChange: (value: string) => void; + placeholder: string; + isOpen: boolean; + setIsOpen: (open: boolean) => void; + renderValue?: (value: string) => string; + className?: string; + }) => ( +
+ + + {isOpen && ( +
+ {options.map((option) => ( + + ))} +
+ )} +
+ ); + // Render skill card - matching your screenshot exactly const renderSkillCard = (skill: UserSkill) => { const isUsedInListing = isSkillUsedInListing(skill.id); @@ -229,36 +406,43 @@ const SkillsPage = () => { }`} >
-
-

{truncateText(skill.skillTitle, 14)}

-
- + {/* Title row with proficiency level always inline */} +
+

+ {skill.skillTitle} +

+ {skill.proficiencyLevel} - - {/* Status indicators */} - {statusIndicators.map((indicator, index) => ( -
- {indicator.type === 'listing' && ( -
- )} - {indicator.type === 'match' && ( - - )} -
- ))}
+ + {/* Status indicators row */} + {statusIndicators.length > 0 && ( +
+ {statusIndicators.map((indicator, index) => ( +
+ {indicator.type === 'listing' && ( +
+ )} + {indicator.type === 'match' && ( + + )} +
+ ))} +
+ )}
{/* Status text */} @@ -279,7 +463,7 @@ const SkillsPage = () => { onClick={() => viewSkillDetails(skill)} className="text-blue-600 hover:text-blue-800 text-sm flex items-center" > - View Details + View Details {canModify ? ( @@ -288,26 +472,18 @@ const SkillsPage = () => { onClick={() => attemptToEditSkill(skill)} className="w-8 h-8 bg-blue-500 text-white rounded flex items-center justify-center hover:bg-blue-600" > - - - - +
) : (
- + Protected
)} @@ -317,9 +493,43 @@ const SkillsPage = () => { ); }; + // Category options for dropdown + const categoryOptions = [ + { value: 'all', label: 'All Categories' }, + ...categories.map(category => ({ + value: category, + label: truncateCategory(category) + })) + ]; + + // Proficiency options + const proficiencyOptions = [ + { value: 'all', label: 'All Levels' }, + { value: 'Expert', label: 'Expert' }, + { value: 'Intermediate', label: 'Intermediate' }, + { value: 'Beginner', label: 'Beginner' } + ]; + + // Usage status options + const usageOptions = [ + { value: 'all', label: 'All Status' }, + { value: 'used', label: 'Used' }, + { value: 'unused', label: 'Unused' }, + { value: 'listings', label: 'In Listings' }, + { value: 'matches', label: 'In Matches' } + ]; + + // Sort options + const sortOptions = [ + { value: 'name', label: 'Sort by Name' }, + { value: 'category', label: 'Sort by Category' }, + { value: 'proficiency', label: 'Sort by Level' }, + { value: 'usage', label: 'Sort by Usage' } + ]; + return ( -
-
+
+

My Skills

- {/* Info banner about skill protection */} - {matchUsedSkills?.totalActiveMatches > 0 && ( -
-
- -
-

Active Skill Matches

-

- You have {matchUsedSkills.totalActiveMatches} active skill match{matchUsedSkills.totalActiveMatches > 1 ? 'es' : ''}. - Skills involved in these matches cannot be modified until the matches are completed or cancelled. -

+ {/* Overall Statistics */} + {skills.length > 0 && ( +
+
+
+ +
+
{skillStats.total}
+
Total Skills
+
+ +
+
+ +
+
{skillStats.expert}
+
Expert
+
+ +
+
+ +
+
{skillStats.intermediate}
+
Intermediate
+
+ +
+
+ +
+
{skillStats.beginner}
+
Beginner
+
+ +
+
+ {/* Match indicator - same as skill cards */} +
+ +
+
+
{skillStats.usedInMatches}
+
In Matches
+
+ +
+
+ {/* Listing indicator - same as skill cards */} +
+
+
+
+
{skillStats.usedInListings}
+
In Listings
+
+ +
+
+ +
+
{skillStats.categories}
+
Categories
+
+
+ )} + + {/* Search and Filters */} + {skills.length > 0 && ( +
+
+ {/* Search - Full width on mobile */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-7 pr-3 py-1.5 border border-gray-200 rounded text-gray-900 placeholder-gray-500 text-xs focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + /> +
+ + {/* Use custom dropdowns for mobile responsiveness */} +
+ value === 'all' ? 'All Categories' : truncateCategory(value, 15)} + /> +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ + {/* Mobile-only additional filters row */} +
+ + +
)} + {/* Compact Info Row */} +
+
+ {skills.length > 0 && ( + Showing {filteredAndSortedSkills.length} of {skills.length} skills + )} +
+
+ {matchUsedSkills?.totalActiveMatches > 0 && ( +
+ + + {matchUsedSkills.totalActiveMatches} active match{matchUsedSkills.totalActiveMatches > 1 ? 'es' : ''} - Skills protected + +
+ )} +
+
+ {loading ? (
+ ) : filteredAndSortedSkills.length === 0 && skills.length > 0 ? ( +
+ +

No skills match your filters

+

Try adjusting your search or filter criteria

+ +
) : skills.length === 0 ? (

You haven't added any skills yet

@@ -361,11 +809,11 @@ const SkillsPage = () => {
) : ( -
+
{Object.entries(skillsByCategory).map(([category, categorySkills]) => (
-

{category}

-
+

{category}

+
{categorySkills.map(skill => renderSkillCard(skill))}
@@ -446,10 +894,7 @@ const SkillsPage = () => { }} className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex items-center" > - - - - + Edit Skill )}