import { useState, useEffect, useCallback } from 'react'; import { useParams, Link, useSearchParams, useNavigate } from 'react-router-dom'; import { Project, Package, PaginatedResponse } from '../types'; import { getProject, listPackages, createPackage } from '../api'; import { Breadcrumb } from '../components/Breadcrumb'; import { Badge } from '../components/Badge'; import { SearchInput } from '../components/SearchInput'; import { SortDropdown, SortOption } from '../components/SortDropdown'; import { FilterChip, FilterChipGroup } from '../components/FilterChip'; import { Pagination } from '../components/Pagination'; import './Home.css'; const SORT_OPTIONS: SortOption[] = [ { value: 'name', label: 'Name' }, { value: 'created_at', label: 'Created' }, { value: 'updated_at', label: 'Updated' }, ]; const FORMAT_OPTIONS = ['generic', 'npm', 'pypi', 'docker', 'deb', 'rpm', 'maven', 'nuget', 'helm']; function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } function ProjectPage() { const { projectName } = useParams<{ projectName: string }>(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [project, setProject] = useState(null); const [packagesData, setPackagesData] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showForm, setShowForm] = useState(false); const [newPackage, setNewPackage] = useState({ name: '', description: '', format: 'generic', platform: 'any' }); const [creating, setCreating] = useState(false); // Get params from URL const page = parseInt(searchParams.get('page') || '1', 10); const search = searchParams.get('search') || ''; const sort = searchParams.get('sort') || 'name'; const order = (searchParams.get('order') || 'asc') as 'asc' | 'desc'; const format = searchParams.get('format') || ''; const updateParams = useCallback( (updates: Record) => { const newParams = new URLSearchParams(searchParams); Object.entries(updates).forEach(([key, value]) => { if (value === undefined || value === '' || (key === 'page' && value === '1')) { newParams.delete(key); } else { newParams.set(key, value); } }); setSearchParams(newParams); }, [searchParams, setSearchParams] ); const loadData = useCallback(async () => { if (!projectName) return; try { setLoading(true); const [projectData, packagesResult] = await Promise.all([ getProject(projectName), listPackages(projectName, { page, search, sort, order, format: format || undefined }), ]); setProject(projectData); setPackagesData(packagesResult); setError(null); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load data'); } finally { setLoading(false); } }, [projectName, page, search, sort, order, format]); useEffect(() => { loadData(); }, [loadData]); // Keyboard navigation - go back with backspace useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Backspace' && !['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) { e.preventDefault(); navigate('/'); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [navigate]); async function handleCreatePackage(e: React.FormEvent) { e.preventDefault(); try { setCreating(true); await createPackage(projectName!, newPackage); setNewPackage({ name: '', description: '', format: 'generic', platform: 'any' }); setShowForm(false); loadData(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create package'); } finally { setCreating(false); } } const handleSearchChange = (value: string) => { updateParams({ search: value, page: '1' }); }; const handleSortChange = (newSort: string, newOrder: 'asc' | 'desc') => { updateParams({ sort: newSort, order: newOrder, page: '1' }); }; const handleFormatChange = (value: string) => { updateParams({ format: value, page: '1' }); }; const handlePageChange = (newPage: number) => { updateParams({ page: String(newPage) }); }; const clearFilters = () => { setSearchParams({}); }; const hasActiveFilters = search !== '' || format !== ''; const packages = packagesData?.items || []; const pagination = packagesData?.pagination; if (loading && !packagesData) { return
Loading...
; } if (!project) { return
Project not found
; } return (

{project.name}

{project.is_public ? 'Public' : 'Private'}
{project.description &&

{project.description}

}
Created {new Date(project.created_at).toLocaleDateString()} {project.updated_at !== project.created_at && ( Updated {new Date(project.updated_at).toLocaleDateString()} )} by {project.created_by}
{error &&
{error}
} {showForm && (

Create New Package

setNewPackage({ ...newPackage, name: e.target.value })} placeholder="releases" required />
setNewPackage({ ...newPackage, description: e.target.value })} placeholder="Optional description" />
)}
{hasActiveFilters && ( {search && handleSearchChange('')} />} {format && handleFormatChange('')} />} )} {packages.length === 0 ? (
{hasActiveFilters ? (

No packages match your filters. Try adjusting your search.

) : (

No packages yet. Create your first package to start uploading artifacts!

)}
) : ( <>
{packages.map((pkg) => (

{pkg.name}

{pkg.format}
{pkg.description &&

{pkg.description}

} {(pkg.tag_count !== undefined || pkg.artifact_count !== undefined) && (
{pkg.tag_count !== undefined && (
{pkg.tag_count} Tags
)} {pkg.artifact_count !== undefined && (
{pkg.artifact_count} Artifacts
)} {pkg.total_size !== undefined && pkg.total_size > 0 && (
{formatBytes(pkg.total_size)} Size
)}
)}
{pkg.latest_tag && ( Latest: {pkg.latest_tag} )} Created {new Date(pkg.created_at).toLocaleDateString()}
))}
{pagination && pagination.total_pages > 1 && ( )} )}
); } export default ProjectPage;