mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36: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 { Package as PackageIcon } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
@@ -12,6 +13,7 @@ interface RecommendationCardProps {
|
|||||||
|
|
||||||
export function RecommendationCard({ pkg, isSelected, onToggle }: RecommendationCardProps) {
|
export function RecommendationCard({ pkg, isSelected, onToggle }: RecommendationCardProps) {
|
||||||
const { t } = useLocale()
|
const { t } = useLocale()
|
||||||
|
const [iconError, setIconError] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -22,8 +24,33 @@ export function RecommendationCard({ pkg, isSelected, onToggle }: Recommendation
|
|||||||
|
|
||||||
|
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-start gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<PackageIcon className="h-8 w-8 text-primary flex-shrink-0" />
|
{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">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-semibold truncate">{pkg.name}</h3>
|
<h3 className="font-semibold truncate">{pkg.name}</h3>
|
||||||
<p className="text-xs text-muted-foreground">{pkg.version}</p>
|
<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 { Package as PackageIcon } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { RecommendedPackage } from '@/types/recommendations'
|
import { RecommendedPackage } from '@/types/recommendations'
|
||||||
@@ -9,6 +10,8 @@ interface RecommendationListItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RecommendationListItem({ pkg, isSelected, onToggle }: RecommendationListItemProps) {
|
export function RecommendationListItem({ pkg, isSelected, onToggle }: RecommendationListItemProps) {
|
||||||
|
const [iconError, setIconError] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-3 p-3 rounded-lg border-2 transition-all cursor-pointer hover:shadow-md ${isSelected
|
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)}
|
onClick={() => onToggle(pkg)}
|
||||||
>
|
>
|
||||||
{/* Package Icon */}
|
{/* 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 */}
|
{/* Package Info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export function RecommendationsSection({
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('grid')
|
const [viewMode, setViewMode] = useState<ViewMode>('grid')
|
||||||
const [sortMode, setSortMode] = useState<SortMode>('recommended')
|
|
||||||
const [filterCategory, setFilterCategory] = useState<FilterCategory>('all')
|
const [filterCategory, setFilterCategory] = useState<FilterCategory>('all')
|
||||||
const [isExpanded, setIsExpanded] = useState(false)
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
|
|
||||||
@@ -116,8 +115,8 @@ export function RecommendationsSection({
|
|||||||
return recommendations.filter(pkg => pkg.matchedCategory === category).length
|
return recommendations.filter(pkg => pkg.matchedCategory === category).length
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter and sort recommendations
|
// Filter recommendations
|
||||||
const filteredAndSortedRecommendations = useMemo(() => {
|
const filteredRecommendations = useMemo(() => {
|
||||||
let result = [...recommendations]
|
let result = [...recommendations]
|
||||||
|
|
||||||
// Filter by category
|
// Filter by category
|
||||||
@@ -125,26 +124,8 @@ export function RecommendationsSection({
|
|||||||
result = result.filter(pkg => pkg.matchedCategory === filterCategory)
|
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
|
return result
|
||||||
}, [recommendations, filterCategory, sortMode])
|
}, [recommendations, filterCategory])
|
||||||
|
|
||||||
if (!isProfileComplete()) {
|
if (!isProfileComplete()) {
|
||||||
return null
|
return null
|
||||||
@@ -294,46 +275,6 @@ export function RecommendationsSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-auto">
|
<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 */}
|
{/* View Mode Toggle */}
|
||||||
<div className="flex items-center border rounded-md">
|
<div className="flex items-center border rounded-md">
|
||||||
<Button
|
<Button
|
||||||
@@ -397,7 +338,7 @@ export function RecommendationsSection({
|
|||||||
|
|
||||||
{!loading && !error && recommendations.length > 0 && viewMode === 'grid' && (
|
{!loading && !error && recommendations.length > 0 && viewMode === 'grid' && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{filteredAndSortedRecommendations.map(pkg => (
|
{filteredRecommendations.map(pkg => (
|
||||||
<RecommendationCard
|
<RecommendationCard
|
||||||
key={pkg.id}
|
key={pkg.id}
|
||||||
pkg={pkg}
|
pkg={pkg}
|
||||||
@@ -411,7 +352,7 @@ export function RecommendationsSection({
|
|||||||
{/* Compact View Mode */}
|
{/* Compact View Mode */}
|
||||||
{!loading && !error && recommendations.length > 0 && viewMode === 'compact' && (
|
{!loading && !error && recommendations.length > 0 && viewMode === 'compact' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{filteredAndSortedRecommendations.map(pkg => (
|
{filteredRecommendations.map(pkg => (
|
||||||
<RecommendationListItem
|
<RecommendationListItem
|
||||||
key={pkg.id}
|
key={pkg.id}
|
||||||
pkg={pkg}
|
pkg={pkg}
|
||||||
|
|||||||
@@ -583,6 +583,191 @@ export const PRESET_DESCRIPTIONS: Record<string, string> = {
|
|||||||
"zotero": "Your personal research assistant"
|
"zotero": "Your personal research assistant"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PACKAGE_ICONS: Record<string, string> = {
|
||||||
|
// Development
|
||||||
|
"git": "git",
|
||||||
|
"Git.Git": "git",
|
||||||
|
"curl": "curl",
|
||||||
|
"wget": "gnu",
|
||||||
|
"nodejs": "nodedotjs",
|
||||||
|
"npm": "npm",
|
||||||
|
"python3": "python",
|
||||||
|
"python3-pip": "pypi",
|
||||||
|
"python-pip": "pypi",
|
||||||
|
"docker.io": "docker",
|
||||||
|
"Docker.DockerDesktop": "docker",
|
||||||
|
"dotnet-sdk-8.0": "dotnet",
|
||||||
|
"dotnet-sdk": "dotnet",
|
||||||
|
"Microsoft.VisualStudioCode": "visualstudiocode",
|
||||||
|
"visual-studio-code": "visualstudiocode",
|
||||||
|
"code": "visualstudiocode",
|
||||||
|
"Postman.Postman": "postman",
|
||||||
|
"postman": "postman",
|
||||||
|
"Microsoft.WindowsTerminal": "windows",
|
||||||
|
"iterm2": "iterm2",
|
||||||
|
"warp": "warp",
|
||||||
|
"sublime-text": "sublimetext",
|
||||||
|
"build-essential": "linux",
|
||||||
|
"base-devel": "archlinux",
|
||||||
|
"java-17-openjdk-devel": "openjdk",
|
||||||
|
"jdk17-openjdk": "openjdk",
|
||||||
|
"temurin": "eclipse",
|
||||||
|
"Anysphere.Cursor": "cursor",
|
||||||
|
"cursor": "cursor",
|
||||||
|
"EclipseFoundation.Eclipse": "eclipse",
|
||||||
|
"WinSCP.WinSCP": "winscp",
|
||||||
|
"PuTTY.PuTTY": "putty",
|
||||||
|
|
||||||
|
// Design
|
||||||
|
"gimp": "gimp",
|
||||||
|
"GIMP.GIMP": "gimp",
|
||||||
|
"inkscape": "inkscape",
|
||||||
|
"Inkscape.Inkscape": "inkscape",
|
||||||
|
"blender": "blender",
|
||||||
|
"BlenderFoundation.Blender": "blender",
|
||||||
|
"krita": "krita",
|
||||||
|
"KDE.Krita": "krita",
|
||||||
|
"darktable": "darktable",
|
||||||
|
"xnviewmp": "xnview",
|
||||||
|
"XnSoft.XnViewMP": "xnview",
|
||||||
|
"IrfanSkiljan.IrfanView": "irfanview",
|
||||||
|
"FastStone.Viewer": "imagej", // Placeholder, no icon
|
||||||
|
"ShareX.ShareX": "sharex",
|
||||||
|
"Greenshot.Greenshot": "greenshot",
|
||||||
|
|
||||||
|
// Multimedia
|
||||||
|
"vlc": "vlcmediaplayer",
|
||||||
|
"VideoLAN.VLC": "vlcmediaplayer",
|
||||||
|
"audacity": "audacity",
|
||||||
|
"Audacity.Audacity": "audacity",
|
||||||
|
"obs-studio": "obsstudio",
|
||||||
|
"obs": "obsstudio",
|
||||||
|
"OBSProject.OBSStudio": "obsstudio",
|
||||||
|
"ffmpeg": "ffmpeg",
|
||||||
|
"mpv": "mpv",
|
||||||
|
"handbrake": "handbrake",
|
||||||
|
"HandBrake.HandBrake": "handbrake",
|
||||||
|
"kdenlive": "kdenlive",
|
||||||
|
"spotify": "spotify",
|
||||||
|
"Spotify.Spotify": "spotify",
|
||||||
|
"Apple.iTunes": "itunes",
|
||||||
|
"foobar2000": "foobar2000",
|
||||||
|
"PeterPawlowski.foobar2000": "foobar2000",
|
||||||
|
"Winamp.Winamp": "winamp",
|
||||||
|
"AIMP.AIMP": "aimp",
|
||||||
|
"iina": "iina",
|
||||||
|
|
||||||
|
// System Tools
|
||||||
|
"htop": "htop",
|
||||||
|
"fastfetch": "linux",
|
||||||
|
"neofetch": "linux",
|
||||||
|
"tmux": "tmux",
|
||||||
|
"zsh": "zsh",
|
||||||
|
"gparted": "gparted",
|
||||||
|
"timeshift": "linux",
|
||||||
|
"stacer": "linux",
|
||||||
|
"keepassxc": "keepassxc",
|
||||||
|
"DominikReichl.KeePass": "keepass",
|
||||||
|
"synaptic": "debian",
|
||||||
|
"7zip.7zip": "7zip",
|
||||||
|
"Microsoft.PowerToys": "windows",
|
||||||
|
"voidtools.Everything": "windows",
|
||||||
|
"RARLab.WinRAR": "winrar",
|
||||||
|
"TeamViewer.TeamViewer": "teamviewer",
|
||||||
|
"teamviewer": "teamviewer",
|
||||||
|
"RealVNC.VNCViewer": "realvnc",
|
||||||
|
"rufus": "rufus",
|
||||||
|
"Rufus.Rufus": "rufus",
|
||||||
|
"bleachbit": "bleachbit",
|
||||||
|
"BleachBit.BleachBit": "bleachbit",
|
||||||
|
"rectangle": "macos",
|
||||||
|
"the-unarchiver": "macos",
|
||||||
|
"keka": "macos",
|
||||||
|
"appcleaner": "macos",
|
||||||
|
"raycast": "raycast",
|
||||||
|
"alfred": "alfred",
|
||||||
|
"qbittorrent": "qbittorrent",
|
||||||
|
"qBittorrent.qBittorrent": "qbittorrent",
|
||||||
|
"NVAccess.NVDA": "nvda",
|
||||||
|
"Malwarebytes.Malwarebytes": "malwarebytes",
|
||||||
|
|
||||||
|
// Gaming
|
||||||
|
"steam": "steam",
|
||||||
|
"Valve.Steam": "steam",
|
||||||
|
"lutris": "lutris",
|
||||||
|
"gamemode": "linux",
|
||||||
|
"mangohud": "opengl",
|
||||||
|
"discord": "discord",
|
||||||
|
"Discord.Discord": "discord",
|
||||||
|
"wine": "wine",
|
||||||
|
"winetricks": "wine",
|
||||||
|
"EpicGames.EpicGamesLauncher": "epicgames",
|
||||||
|
"epic-games": "epicgames",
|
||||||
|
"GOG.Galaxy": "gogdotcom",
|
||||||
|
|
||||||
|
// Productivity
|
||||||
|
"libreoffice": "libreoffice",
|
||||||
|
"TheDocumentFoundation.LibreOffice": "libreoffice",
|
||||||
|
"libreoffice-fresh": "libreoffice",
|
||||||
|
"thunderbird": "thunderbird",
|
||||||
|
"Mozilla.Thunderbird": "thunderbird",
|
||||||
|
"firefox": "firefox",
|
||||||
|
"Mozilla.Firefox": "firefox",
|
||||||
|
"chromium": "chromium",
|
||||||
|
"chromium-browser": "chromium",
|
||||||
|
"Google.Chrome": "googlechrome",
|
||||||
|
"google-chrome": "googlechrome",
|
||||||
|
"Microsoft.Edge": "microsoftedge",
|
||||||
|
"microsoft-edge": "microsoftedge",
|
||||||
|
"Brave.Brave": "brave",
|
||||||
|
"brave-browser": "brave",
|
||||||
|
"Opera.Opera": "opera",
|
||||||
|
"opera": "opera",
|
||||||
|
"zoom": "zoom",
|
||||||
|
"Zoom.Zoom": "zoom",
|
||||||
|
"microsoft-teams": "microsoftteams",
|
||||||
|
"Microsoft.Teams": "microsoftteams",
|
||||||
|
"slack": "slack",
|
||||||
|
"SlackTechnologies.Slack": "slack",
|
||||||
|
"notion": "notion",
|
||||||
|
"Notion.Notion": "notion",
|
||||||
|
"obsidian": "obsidian",
|
||||||
|
"Obsidian.Obsidian": "obsidian",
|
||||||
|
"foxitreader": "foxit",
|
||||||
|
"Foxit.FoxitReader": "foxit",
|
||||||
|
"adobe-acrobat-reader": "adobeacrobatreader",
|
||||||
|
"dropbox": "dropbox",
|
||||||
|
"Dropbox.Dropbox": "dropbox",
|
||||||
|
"onedrive": "microsoftonedrive",
|
||||||
|
"Microsoft.OneDrive": "microsoftonedrive",
|
||||||
|
"google-drive": "googledrive",
|
||||||
|
"evernote": "evernote",
|
||||||
|
"Evernote.Evernote": "evernote",
|
||||||
|
"evolution": "linux",
|
||||||
|
"focuswriter": "linux",
|
||||||
|
|
||||||
|
// Education
|
||||||
|
"anki": "anki",
|
||||||
|
"Anki.Anki": "anki",
|
||||||
|
"zotero": "zotero"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPresetIcon(name: string): string | undefined {
|
||||||
|
// Try exact match
|
||||||
|
if (PACKAGE_ICONS[name]) {
|
||||||
|
return PACKAGE_ICONS[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try case insensitive
|
||||||
|
const lowerName = name.toLowerCase();
|
||||||
|
const key = Object.keys(PACKAGE_ICONS).find(k => k.toLowerCase() === lowerName);
|
||||||
|
if (key) {
|
||||||
|
return PACKAGE_ICONS[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export function getPresetDetails(name: string): { description: string } {
|
export function getPresetDetails(name: string): { description: string } {
|
||||||
// Try exact match
|
// Try exact match
|
||||||
if (PRESET_DESCRIPTIONS[name]) {
|
if (PRESET_DESCRIPTIONS[name]) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
RecommendedPackage,
|
RecommendedPackage,
|
||||||
UserCategory,
|
UserCategory,
|
||||||
} from "@/types/recommendations";
|
} from "@/types/recommendations";
|
||||||
import { getPackagesWithCategories, getPresetDetails } from "@/data/recommendationPresets";
|
import { getPackagesWithCategories, getPresetDetails, getPresetIcon } from "@/data/recommendationPresets";
|
||||||
|
|
||||||
export class RecommendationService {
|
export class RecommendationService {
|
||||||
/**
|
/**
|
||||||
@@ -65,14 +65,15 @@ export class RecommendationService {
|
|||||||
const packages: Package[] = [];
|
const packages: Package[] = [];
|
||||||
|
|
||||||
for (const { name, category } of packagesInfo) {
|
for (const { name, category } of packagesInfo) {
|
||||||
const { description } = getPresetDetails(name);
|
// Get icon slug if available
|
||||||
|
const iconSlug = getPresetIcon(name);
|
||||||
|
|
||||||
// Create a mock package object to avoid database queries
|
// Create a mock package object to avoid database queries
|
||||||
// This ensures instant loading for recommendations
|
// This ensures instant loading for recommendations
|
||||||
const mockPackage: Package = {
|
const mockPackage: Package = {
|
||||||
id: `${platformId}:${name.toLowerCase()}`,
|
id: `${platformId}:${name.toLowerCase()}`,
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: "", // Description removed as requested
|
||||||
version: "latest",
|
version: "latest",
|
||||||
platform_id: platformId,
|
platform_id: platformId,
|
||||||
type: "cli",
|
type: "cli",
|
||||||
@@ -86,9 +87,19 @@ export class RecommendationService {
|
|||||||
id: platformId,
|
id: platformId,
|
||||||
name: platformId.charAt(0).toUpperCase() + platformId.slice(1),
|
name: platformId.charAt(0).toUpperCase() + platformId.slice(1),
|
||||||
package_manager: "unknown"
|
package_manager: "unknown"
|
||||||
}
|
},
|
||||||
|
// We attach the icon slug to the tags temporarily or we can add a custom field if we extend the type
|
||||||
|
// But simpler is to pass it through the system.
|
||||||
|
// Actually, Package interface doesn't have icon.
|
||||||
|
// RecommendedPackage does (we added it).
|
||||||
|
// So we need to handle this in scorePackage or casting.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Hack: Store icon slug in tags so it survives until scorePackage
|
||||||
|
if (iconSlug) {
|
||||||
|
mockPackage.tags = [`icon:${iconSlug}`];
|
||||||
|
}
|
||||||
|
|
||||||
packages.push(mockPackage);
|
packages.push(mockPackage);
|
||||||
categoryMap.set(mockPackage.id, category);
|
categoryMap.set(mockPackage.id, category);
|
||||||
}
|
}
|
||||||
@@ -124,6 +135,15 @@ export class RecommendationService {
|
|||||||
): RecommendedPackage {
|
): RecommendedPackage {
|
||||||
const isPresetMatch = presetPackageNames.includes(pkg.name);
|
const isPresetMatch = presetPackageNames.includes(pkg.name);
|
||||||
|
|
||||||
|
// Extract icon from tags if present
|
||||||
|
let icon: string | undefined;
|
||||||
|
if (pkg.tags) {
|
||||||
|
const iconTag = pkg.tags.find(tag => tag.startsWith('icon:'));
|
||||||
|
if (iconTag) {
|
||||||
|
icon = iconTag.replace('icon:', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Simplified score: just use popularity score (0-100)
|
// Simplified score: just use popularity score (0-100)
|
||||||
// Give a boost to preset packages so they appear first
|
// Give a boost to preset packages so they appear first
|
||||||
let finalScore = pkg.popularity_score || 0;
|
let finalScore = pkg.popularity_score || 0;
|
||||||
@@ -155,6 +175,7 @@ export class RecommendationService {
|
|||||||
recommendationReason: "", // Removed as requested
|
recommendationReason: "", // Removed as requested
|
||||||
presetMatch: isPresetMatch,
|
presetMatch: isPresetMatch,
|
||||||
matchedCategory: matchedCategory,
|
matchedCategory: matchedCategory,
|
||||||
|
icon: icon
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export interface RecommendedPackage {
|
|||||||
recommendationReason: string;
|
recommendationReason: string;
|
||||||
presetMatch?: boolean;
|
presetMatch?: boolean;
|
||||||
matchedCategory?: UserCategory; // Which user category this package matched
|
matchedCategory?: UserCategory; // Which user category this package matched
|
||||||
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user