From df9193ffe6cc28d40aca616c4b623dc295df3aeb Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 17:50:50 +0000 Subject: [PATCH] refactor: split package manager components --- .../managers/package/PackageManager.tsx | 306 ++++-------------- .../package/package-manager/PackageCard.tsx | 67 ++++ .../package-manager/PackageFilters.tsx | 65 ++++ .../package/package-manager/PackageTabs.tsx | 85 +++++ .../package/package-manager/usePackages.ts | 94 ++++++ 5 files changed, 368 insertions(+), 249 deletions(-) create mode 100644 frontends/nextjs/src/components/managers/package/package-manager/PackageCard.tsx create mode 100644 frontends/nextjs/src/components/managers/package/package-manager/PackageFilters.tsx create mode 100644 frontends/nextjs/src/components/managers/package/package-manager/PackageTabs.tsx create mode 100644 frontends/nextjs/src/components/managers/package/package-manager/usePackages.ts diff --git a/frontends/nextjs/src/components/managers/package/PackageManager.tsx b/frontends/nextjs/src/components/managers/package/PackageManager.tsx index 3d1aeac7b..5bdeda96e 100644 --- a/frontends/nextjs/src/components/managers/package/PackageManager.tsx +++ b/frontends/nextjs/src/components/managers/package/PackageManager.tsx @@ -1,60 +1,44 @@ -import { useState, useEffect } from 'react' -import { Button } from '@/components/ui' -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui' -import { Input } from '@/components/ui' -import { Badge } from '@/components/ui' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui' -import { ScrollArea } from '@/components/ui' -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui' -import { Separator } from '@/components/ui' +import { useState } from 'react' +import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, ScrollArea, Separator } from '@/components/ui' import { toast } from 'sonner' -import { PACKAGE_CATALOG, type PackageCatalogData } from '@/lib/packages/core/package-catalog' -import type { PackageManifest, InstalledPackage } from '@/lib/package-types' -import { installPackage, listInstalledPackages, togglePackageEnabled, uninstallPackage } from '@/lib/api/packages' -import { Package, Download, Trash, Power, MagnifyingGlass, Star, Tag, User, TrendUp, Funnel, Export, ArrowSquareIn } from '@phosphor-icons/react' +import { installPackage, togglePackageEnabled, uninstallPackage } from '@/lib/api/packages' +import type { PackageCatalogData } from '@/lib/packages/core/package-catalog' +import { ArrowSquareIn, Download, Export, Package, Star, Tag, Trash, User } from '@phosphor-icons/react' import { PackageImportExport } from './PackageImportExport' +import { PackageFilters } from './package-manager/PackageFilters' +import { PackageTabs } from './package-manager/PackageTabs' +import { usePackages } from './package-manager/usePackages' interface PackageManagerProps { onClose?: () => void } export function PackageManager({ onClose }: PackageManagerProps) { - const [packages, setPackages] = useState([]) - const [installedPackages, setInstalledPackages] = useState([]) + const { + filteredPackages, + installedList, + availableList, + installedPackages, + categories, + searchQuery, + categoryFilter, + sortBy, + setSearchQuery, + setCategoryFilter, + setSortBy, + loadPackages, + getCatalogEntry, + } = usePackages() const [selectedPackage, setSelectedPackage] = useState(null) - const [searchQuery, setSearchQuery] = useState('') - const [categoryFilter, setCategoryFilter] = useState('all') - const [sortBy, setSortBy] = useState<'name' | 'downloads' | 'rating'>('downloads') const [showDetails, setShowDetails] = useState(false) const [installing, setInstalling] = useState(false) const [showImportExport, setShowImportExport] = useState(false) const [importExportMode, setImportExportMode] = useState<'import' | 'export'>('export') - useEffect(() => { - loadPackages() - }, []) - - const loadPackages = async () => { - const installed = await listInstalledPackages() - setInstalledPackages(installed) - - const allPackages = Object.values(PACKAGE_CATALOG).map(pkg => { - const packageData = pkg() - - return { - ...packageData.manifest, - installed: installed.some(ip => ip.packageId === packageData.manifest.id), - } - }) - - setPackages(allPackages) - } - const handleInstallPackage = async (packageId: string) => { setInstalling(true) try { - const packageEntry = PACKAGE_CATALOG[packageId]?.() + const packageEntry = getCatalogEntry(packageId) if (!packageEntry) { toast.error('Package not found') return @@ -75,7 +59,7 @@ export function PackageManager({ onClose }: PackageManagerProps) { const handleUninstallPackage = async (packageId: string) => { try { - const packageEntry = PACKAGE_CATALOG[packageId]?.() + const packageEntry = getCatalogEntry(packageId) if (!packageEntry) { toast.error('Package not found') return @@ -103,28 +87,18 @@ export function PackageManager({ onClose }: PackageManagerProps) { } } - const filteredPackages = packages - .filter(pkg => { - const matchesSearch = - pkg.name.toLowerCase().includes(searchQuery.toLowerCase()) || - pkg.description.toLowerCase().includes(searchQuery.toLowerCase()) || - pkg.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) + const openPackageDetails = (packageId: string) => { + const catalogEntry = getCatalogEntry(packageId) + if (!catalogEntry) return - const matchesCategory = categoryFilter === 'all' || pkg.category === categoryFilter + const installedPackage = installedPackages.find(pkg => pkg.packageId === packageId) - return matchesSearch && matchesCategory + setSelectedPackage({ + ...catalogEntry, + manifest: { ...catalogEntry.manifest, installed: Boolean(installedPackage) }, }) - .sort((a, b) => { - if (sortBy === 'name') return a.name.localeCompare(b.name) - if (sortBy === 'downloads') return b.downloadCount - a.downloadCount - if (sortBy === 'rating') return b.rating - a.rating - return 0 - }) - - const categories = ['all', ...Array.from(new Set(packages.map(p => p.category)))] - - const installedList = packages.filter(p => p.installed) - const availableList = packages.filter(p => !p.installed) + setShowDetails(true) + } return (
@@ -139,8 +113,8 @@ export function PackageManager({ onClose }: PackageManagerProps) {
- -
+ +
- -
- - All Packages - - Installed ({installedList.length}) - - - Available ({availableList.length}) - - -
- -
-
- - setSearchQuery(e.target.value)} - className="pl-10" - /> -
- -
- - - -
-
- - - -
- {filteredPackages.map(pkg => ( - ip.packageId === pkg.id)} - onViewDetails={() => { - setSelectedPackage(PACKAGE_CATALOG[pkg.id]?.() ?? null) - setShowDetails(true) - }} - onToggle={handleTogglePackage} - /> - ))} -
-
-
- - - -
- {installedList.length === 0 ? ( -
- -

No packages installed yet

-
- ) : ( - installedList.map(pkg => ( - ip.packageId === pkg.id)} - onViewDetails={() => { - setSelectedPackage(PACKAGE_CATALOG[pkg.id]?.() ?? null) - setShowDetails(true) - }} - onToggle={handleTogglePackage} - /> - )) - )} -
-
-
- - - -
- {availableList.map(pkg => ( - { - setSelectedPackage(PACKAGE_CATALOG[pkg.id]?.() ?? null) - setShowDetails(true) - }} - onToggle={handleTogglePackage} - /> - ))} -
-
-
-
+
@@ -416,7 +288,7 @@ export function PackageManager({ onClose }: PackageManagerProps) { - { setShowImportExport(open) @@ -429,67 +301,3 @@ export function PackageManager({ onClose }: PackageManagerProps) { ) } - -interface PackageCardProps { - package: PackageManifest - isInstalled: boolean - installedPackage?: InstalledPackage - onViewDetails: () => void - onToggle: (packageId: string, enabled: boolean) => void -} - -function PackageCard({ package: pkg, isInstalled, installedPackage, onViewDetails, onToggle }: PackageCardProps) { - return ( - - -
-
- {pkg.icon} -
-
- {pkg.name} - {pkg.description} -
-
-
- - -
- {pkg.category} - {isInstalled && ( - - {installedPackage?.enabled ? 'Active' : 'Disabled'} - - )} -
- -
-
- - {pkg.downloadCount.toLocaleString()} -
-
- - {pkg.rating} -
-
-
- - - - {isInstalled && installedPackage && ( - - )} - -
- ) -} diff --git a/frontends/nextjs/src/components/managers/package/package-manager/PackageCard.tsx b/frontends/nextjs/src/components/managers/package/package-manager/PackageCard.tsx new file mode 100644 index 000000000..3bb4b0167 --- /dev/null +++ b/frontends/nextjs/src/components/managers/package/package-manager/PackageCard.tsx @@ -0,0 +1,67 @@ +import { Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui' +import type { InstalledPackage, PackageManifest } from '@/lib/package-types' +import { Download, Power, Star } from '@phosphor-icons/react' + +interface PackageCardProps { + package: PackageManifest + isInstalled: boolean + installedPackage?: InstalledPackage + onViewDetails: () => void + onToggle: (packageId: string, enabled: boolean) => void +} + +export function PackageCard({ package: pkg, isInstalled, installedPackage, onViewDetails, onToggle }: PackageCardProps) { + return ( + + +
+
+ {pkg.icon} +
+
+ {pkg.name} + {pkg.description} +
+
+
+ + +
+ {pkg.category} + {isInstalled && ( + + {installedPackage?.enabled ? 'Active' : 'Disabled'} + + )} +
+ +
+
+ + {pkg.downloadCount.toLocaleString()} +
+
+ + {pkg.rating} +
+
+
+ + + + {isInstalled && installedPackage && ( + + )} + +
+ ) +} diff --git a/frontends/nextjs/src/components/managers/package/package-manager/PackageFilters.tsx b/frontends/nextjs/src/components/managers/package/package-manager/PackageFilters.tsx new file mode 100644 index 000000000..32632686c --- /dev/null +++ b/frontends/nextjs/src/components/managers/package/package-manager/PackageFilters.tsx @@ -0,0 +1,65 @@ +import { Funnel, MagnifyingGlass, TrendUp } from '@phosphor-icons/react' +import { Input } from '@/components/ui' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui' + +interface PackageFiltersProps { + searchQuery: string + categoryFilter: string + sortBy: 'name' | 'downloads' | 'rating' + categories: string[] + onSearchChange: (value: string) => void + onCategoryChange: (value: string) => void + onSortChange: (value: 'name' | 'downloads' | 'rating') => void +} + +export function PackageFilters({ + searchQuery, + categoryFilter, + sortBy, + categories, + onSearchChange, + onCategoryChange, + onSortChange, +}: PackageFiltersProps) { + return ( +
+
+ + onSearchChange(e.target.value)} + className="pl-10" + /> +
+ +
+ + + +
+
+ ) +} diff --git a/frontends/nextjs/src/components/managers/package/package-manager/PackageTabs.tsx b/frontends/nextjs/src/components/managers/package/package-manager/PackageTabs.tsx new file mode 100644 index 000000000..b14140488 --- /dev/null +++ b/frontends/nextjs/src/components/managers/package/package-manager/PackageTabs.tsx @@ -0,0 +1,85 @@ +import { ScrollArea, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui' +import type { InstalledPackage, PackageManifest } from '@/lib/package-types' +import { Package } from '@phosphor-icons/react' +import { PackageCard } from './PackageCard' + +interface PackageTabsProps { + filteredPackages: PackageManifest[] + installedList: PackageManifest[] + availableList: PackageManifest[] + installedPackages: InstalledPackage[] + onSelectPackage: (packageId: string) => void + onTogglePackage: (packageId: string, enabled: boolean) => Promise +} + +export function PackageTabs({ + filteredPackages, + installedList, + availableList, + installedPackages, + onSelectPackage, + onTogglePackage, +}: PackageTabsProps) { + const renderPackageCards = (packages: PackageManifest[], isInstalled: (pkg: PackageManifest) => boolean) => ( +
+ {packages.map(pkg => ( + ip.packageId === pkg.id)} + onViewDetails={() => onSelectPackage(pkg.id)} + onToggle={onTogglePackage} + /> + ))} +
+ ) + + return ( + +
+ + All Packages + Installed ({installedList.length}) + Available ({availableList.length}) + +
+ + + + {renderPackageCards(filteredPackages, (pkg) => pkg.installed)} + + + + + +
+ {installedList.length === 0 ? ( +
+ +

No packages installed yet

+
+ ) : ( + installedList.map(pkg => ( + ip.packageId === pkg.id)} + onViewDetails={() => onSelectPackage(pkg.id)} + onToggle={onTogglePackage} + /> + )) + )} +
+
+
+ + + + {renderPackageCards(availableList, () => false)} + + +
+ ) +} diff --git a/frontends/nextjs/src/components/managers/package/package-manager/usePackages.ts b/frontends/nextjs/src/components/managers/package/package-manager/usePackages.ts new file mode 100644 index 000000000..b8d8d90d5 --- /dev/null +++ b/frontends/nextjs/src/components/managers/package/package-manager/usePackages.ts @@ -0,0 +1,94 @@ +import { useEffect, useMemo, useState } from 'react' +import { PACKAGE_CATALOG, type PackageCatalogData } from '@/lib/packages/core/package-catalog' +import type { InstalledPackage, PackageManifest } from '@/lib/package-types' +import { listInstalledPackages } from '@/lib/api/packages' + +export interface UsePackagesResult { + packages: PackageManifest[] + installedPackages: InstalledPackage[] + filteredPackages: PackageManifest[] + installedList: PackageManifest[] + availableList: PackageManifest[] + categories: string[] + searchQuery: string + categoryFilter: string + sortBy: 'name' | 'downloads' | 'rating' + setSearchQuery: (query: string) => void + setCategoryFilter: (category: string) => void + setSortBy: (sort: 'name' | 'downloads' | 'rating') => void + loadPackages: () => Promise + getCatalogEntry: (packageId: string) => PackageCatalogData | null +} + +export function usePackages(): UsePackagesResult { + const [packages, setPackages] = useState([]) + const [installedPackages, setInstalledPackages] = useState([]) + const [searchQuery, setSearchQuery] = useState('') + const [categoryFilter, setCategoryFilter] = useState('all') + const [sortBy, setSortBy] = useState<'name' | 'downloads' | 'rating'>('downloads') + + useEffect(() => { + loadPackages() + }, []) + + const loadPackages = async () => { + const installed = await listInstalledPackages() + setInstalledPackages(installed) + + const allPackages = Object.values(PACKAGE_CATALOG).map(pkg => { + const packageData = pkg() + + return { + ...packageData.manifest, + installed: installed.some(ip => ip.packageId === packageData.manifest.id), + } + }) + + setPackages(allPackages) + } + + const filteredPackages = useMemo(() => { + const matchesSearch = (pkg: PackageManifest) => + pkg.name.toLowerCase().includes(searchQuery.toLowerCase()) || + pkg.description.toLowerCase().includes(searchQuery.toLowerCase()) || + pkg.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) + + const matchesCategory = (pkg: PackageManifest) => categoryFilter === 'all' || pkg.category === categoryFilter + + return packages + .filter(pkg => matchesSearch(pkg) && matchesCategory(pkg)) + .sort((a, b) => { + if (sortBy === 'name') return a.name.localeCompare(b.name) + if (sortBy === 'downloads') return b.downloadCount - a.downloadCount + if (sortBy === 'rating') return b.rating - a.rating + return 0 + }) + }, [packages, searchQuery, categoryFilter, sortBy]) + + const categories = useMemo( + () => ['all', ...Array.from(new Set(packages.map(p => p.category)))], + [packages], + ) + + const installedList = useMemo(() => packages.filter(p => p.installed), [packages]) + const availableList = useMemo(() => packages.filter(p => !p.installed), [packages]) + + const getCatalogEntry = (packageId: string) => PACKAGE_CATALOG[packageId]?.() ?? null + + return { + packages, + installedPackages, + filteredPackages, + installedList, + availableList, + categories, + searchQuery, + categoryFilter, + sortBy, + setSearchQuery, + setCategoryFilter, + setSortBy, + loadPackages, + getCatalogEntry, + } +}