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:
Yusuf İpek
2025-11-23 14:38:39 +03:00
parent d66c02b7fa
commit 55e9e3b97d
6 changed files with 274 additions and 71 deletions
+29 -2
View File
@@ -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>
+29 -1
View File
@@ -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">
+5 -64
View File
@@ -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}