mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 18:46:07 +00:00
feat: Add package icons using Simple Icons and remove sorting functionality
- Integrate Simple Icons via CDN with SVG mask technique for theme-aware icons - Add icon mapping for 150+ packages across all categories (development, design, multimedia, system tools, gaming, productivity, education) - Implement fallback to default PackageIcon when icon unavailable or fails to load - Add icon error handling with hidden img element to detect load failures - Update RecommendationCard and RecommendationListItem to display
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { Package as PackageIcon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
@@ -12,6 +13,7 @@ interface RecommendationCardProps {
|
||||
|
||||
export function RecommendationCard({ pkg, isSelected, onToggle }: RecommendationCardProps) {
|
||||
const { t } = useLocale()
|
||||
const [iconError, setIconError] = useState(false)
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -22,8 +24,33 @@ export function RecommendationCard({ pkg, isSelected, onToggle }: Recommendation
|
||||
|
||||
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<PackageIcon className="h-8 w-8 text-primary flex-shrink-0" />
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{pkg.icon && !iconError ? (
|
||||
<>
|
||||
<div
|
||||
className="h-8 w-8 flex-shrink-0 bg-foreground"
|
||||
style={{
|
||||
maskImage: `url(https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${pkg.icon}.svg)`,
|
||||
WebkitMaskImage: `url(https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${pkg.icon}.svg)`,
|
||||
maskRepeat: 'no-repeat',
|
||||
WebkitMaskRepeat: 'no-repeat',
|
||||
maskSize: 'contain',
|
||||
WebkitMaskSize: 'contain',
|
||||
maskPosition: 'center',
|
||||
WebkitMaskPosition: 'center'
|
||||
}}
|
||||
/>
|
||||
{/* Hidden image to detect load errors */}
|
||||
<img
|
||||
src={`https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${pkg.icon}.svg`}
|
||||
alt=""
|
||||
className="hidden"
|
||||
onError={() => setIconError(true)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<PackageIcon className="h-8 w-8 text-foreground flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold truncate">{pkg.name}</h3>
|
||||
<p className="text-xs text-muted-foreground">{pkg.version}</p>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { Package as PackageIcon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RecommendedPackage } from '@/types/recommendations'
|
||||
@@ -9,6 +10,8 @@ interface RecommendationListItemProps {
|
||||
}
|
||||
|
||||
export function RecommendationListItem({ pkg, isSelected, onToggle }: RecommendationListItemProps) {
|
||||
const [iconError, setIconError] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-3 p-3 rounded-lg border-2 transition-all cursor-pointer hover:shadow-md ${isSelected
|
||||
@@ -18,7 +21,32 @@ export function RecommendationListItem({ pkg, isSelected, onToggle }: Recommenda
|
||||
onClick={() => onToggle(pkg)}
|
||||
>
|
||||
{/* Package Icon */}
|
||||
<PackageIcon className="h-6 w-6 text-primary flex-shrink-0" />
|
||||
{pkg.icon && !iconError ? (
|
||||
<>
|
||||
<div
|
||||
className="h-6 w-6 flex-shrink-0 bg-foreground"
|
||||
style={{
|
||||
maskImage: `url(https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${pkg.icon}.svg)`,
|
||||
WebkitMaskImage: `url(https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${pkg.icon}.svg)`,
|
||||
maskRepeat: 'no-repeat',
|
||||
WebkitMaskRepeat: 'no-repeat',
|
||||
maskSize: 'contain',
|
||||
WebkitMaskSize: 'contain',
|
||||
maskPosition: 'center',
|
||||
WebkitMaskPosition: 'center'
|
||||
}}
|
||||
/>
|
||||
{/* Hidden image to detect load errors */}
|
||||
<img
|
||||
src={`https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${pkg.icon}.svg`}
|
||||
alt=""
|
||||
className="hidden"
|
||||
onError={() => setIconError(true)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<PackageIcon className="h-6 w-6 text-foreground flex-shrink-0" />
|
||||
)}
|
||||
|
||||
{/* Package Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
@@ -41,7 +41,6 @@ export function RecommendationsSection({
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid')
|
||||
const [sortMode, setSortMode] = useState<SortMode>('recommended')
|
||||
const [filterCategory, setFilterCategory] = useState<FilterCategory>('all')
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
@@ -116,8 +115,8 @@ export function RecommendationsSection({
|
||||
return recommendations.filter(pkg => pkg.matchedCategory === category).length
|
||||
}
|
||||
|
||||
// Filter and sort recommendations
|
||||
const filteredAndSortedRecommendations = useMemo(() => {
|
||||
// Filter recommendations
|
||||
const filteredRecommendations = useMemo(() => {
|
||||
let result = [...recommendations]
|
||||
|
||||
// Filter by category
|
||||
@@ -125,26 +124,8 @@ export function RecommendationsSection({
|
||||
result = result.filter(pkg => pkg.matchedCategory === filterCategory)
|
||||
}
|
||||
|
||||
// Sort
|
||||
switch (sortMode) {
|
||||
case 'popularity':
|
||||
result.sort((a, b) => (b.popularity || 0) - (a.popularity || 0))
|
||||
break
|
||||
case 'preset':
|
||||
result.sort((a, b) => {
|
||||
if (a.presetMatch && !b.presetMatch) return -1
|
||||
if (!a.presetMatch && b.presetMatch) return 1
|
||||
return b.recommendationScore - a.recommendationScore
|
||||
})
|
||||
break
|
||||
case 'recommended':
|
||||
default:
|
||||
result.sort((a, b) => b.recommendationScore - a.recommendationScore)
|
||||
break
|
||||
}
|
||||
|
||||
return result
|
||||
}, [recommendations, filterCategory, sortMode])
|
||||
}, [recommendations, filterCategory])
|
||||
|
||||
if (!isProfileComplete()) {
|
||||
return null
|
||||
@@ -294,46 +275,6 @@ export function RecommendationsSection({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
{/* Sort Dropdown */}
|
||||
<div className="flex items-center gap-1 border rounded-md">
|
||||
<Button
|
||||
variant={sortMode === 'recommended' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSortMode('recommended')
|
||||
}}
|
||||
className="h-8 text-xs rounded-r-none"
|
||||
>
|
||||
<Award className="h-3 w-3 mr-1" />
|
||||
{t('recommendations.sort.recommended') || 'Best Match'}
|
||||
</Button>
|
||||
<Button
|
||||
variant={sortMode === 'popularity' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSortMode('popularity')
|
||||
}}
|
||||
className="h-8 text-xs rounded-none border-x"
|
||||
>
|
||||
<TrendingUp className="h-3 w-3 mr-1" />
|
||||
{t('recommendations.sort.popular') || 'Popular'}
|
||||
</Button>
|
||||
<Button
|
||||
variant={sortMode === 'preset' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSortMode('preset')
|
||||
}}
|
||||
className="h-8 text-xs rounded-l-none"
|
||||
>
|
||||
<Star className="h-3 w-3 mr-1" />
|
||||
{t('recommendations.sort.preset') || 'Essential'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex items-center border rounded-md">
|
||||
<Button
|
||||
@@ -397,7 +338,7 @@ export function RecommendationsSection({
|
||||
|
||||
{!loading && !error && recommendations.length > 0 && viewMode === 'grid' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredAndSortedRecommendations.map(pkg => (
|
||||
{filteredRecommendations.map(pkg => (
|
||||
<RecommendationCard
|
||||
key={pkg.id}
|
||||
pkg={pkg}
|
||||
@@ -411,7 +352,7 @@ export function RecommendationsSection({
|
||||
{/* Compact View Mode */}
|
||||
{!loading && !error && recommendations.length > 0 && viewMode === 'compact' && (
|
||||
<div className="space-y-2">
|
||||
{filteredAndSortedRecommendations.map(pkg => (
|
||||
{filteredRecommendations.map(pkg => (
|
||||
<RecommendationListItem
|
||||
key={pkg.id}
|
||||
pkg={pkg}
|
||||
|
||||
Reference in New Issue
Block a user