import { useState, useEffect, useCallback } from 'react'; import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom'; import { Project, Package, PaginatedResponse, AccessLevel } from '../types'; import { getProject, listPackages, createPackage, getMyProjectAccess, UnauthorizedError, ForbiddenError } from '../api'; import { Breadcrumb } from '../components/Breadcrumb'; import { Badge } from '../components/Badge'; import { DataTable } from '../components/DataTable'; import { SearchInput } from '../components/SearchInput'; import { FilterChip, FilterChipGroup } from '../components/FilterChip'; import { Pagination } from '../components/Pagination'; import { useAuth } from '../contexts/AuthContext'; import './Home.css'; 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 location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); const { user } = useAuth(); const [project, setProject] = useState(null); const [packagesData, setPackagesData] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [accessDenied, setAccessDenied] = useState(false); const [showForm, setShowForm] = useState(false); const [newPackage, setNewPackage] = useState({ name: '', description: '', format: 'generic', platform: 'any' }); const [creating, setCreating] = useState(false); const [accessLevel, setAccessLevel] = useState(null); const [isOwner, setIsOwner] = useState(false); // Derived permissions const canWrite = accessLevel === 'write' || accessLevel === 'admin'; const canAdmin = accessLevel === 'admin'; // 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); setAccessDenied(false); const [projectData, packagesResult, accessResult] = await Promise.all([ getProject(projectName), listPackages(projectName, { page, search, sort, order, format: format || undefined }), getMyProjectAccess(projectName), ]); setProject(projectData); setPackagesData(packagesResult); setAccessLevel(accessResult.access_level); setIsOwner(accessResult.is_owner); setError(null); } catch (err) { if (err instanceof UnauthorizedError) { navigate('/login', { state: { from: location.pathname } }); return; } if (err instanceof ForbiddenError) { setAccessDenied(true); setError('You do not have access to this project'); setLoading(false); return; } setError(err instanceof Error ? err.message : 'Failed to load data'); } finally { setLoading(false); } }, [projectName, page, search, sort, order, format, navigate, location.pathname]); 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 = (columnKey: string) => { const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc'; updateParams({ sort: columnKey, 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 (accessDenied) { return (

Access Denied

You do not have permission to view this project.

{!user && (

Sign in

)}
); } if (!project) { return
Project not found
; } return (

{project.name}

{project.is_public ? 'Public' : 'Private'} {project.is_system && ( System Cache )} {accessLevel && ( {isOwner ? 'Owner' : accessLevel.charAt(0).toUpperCase() + accessLevel.slice(1)} )}
{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}
{canAdmin && !project.team_id && !project.is_system && ( )} {canWrite && !project.is_system ? ( ) : user && !project.is_system ? ( Read-only access ) : null}
{error &&
{error}
} {showForm && canWrite && (

Create New Package

setNewPackage({ ...newPackage, name: e.target.value })} placeholder="releases" required />
setNewPackage({ ...newPackage, description: e.target.value })} placeholder="Optional description" />
)}
{!project?.is_system && ( )}
{hasActiveFilters && ( {search && handleSearchChange('')} />} {format && handleFormatChange('')} />} )}
pkg.id} onRowClick={(pkg) => navigate(`/project/${projectName}/${pkg.name}`)} onSort={handleSortChange} sortKey={sort} sortOrder={order} emptyMessage={ hasActiveFilters ? 'No packages match your filters. Try adjusting your search.' : 'No packages yet. Create your first package to start uploading artifacts!' } columns={[ { key: 'name', header: 'Name', sortable: true, render: (pkg) => {pkg.name}, }, { key: 'description', header: 'Description', className: 'cell-description', render: (pkg) => pkg.description || '—', }, ...(!project?.is_system ? [{ key: 'format', header: 'Format', render: (pkg: Package) => {pkg.format}, }] : []), ...(!project?.is_system ? [{ key: 'version_count', header: 'Versions', render: (pkg: Package) => pkg.version_count ?? '—', }] : []), { key: 'artifact_count', header: project?.is_system ? 'Versions' : 'Artifacts', render: (pkg) => pkg.artifact_count ?? '—', }, { key: 'total_size', header: 'Size', render: (pkg) => pkg.total_size !== undefined && pkg.total_size > 0 ? formatBytes(pkg.total_size) : '—', }, ...(!project?.is_system ? [{ key: 'latest_version', header: 'Latest', render: (pkg: Package) => pkg.latest_version ? {pkg.latest_version} : '—', }] : []), { key: 'created_at', header: 'Created', sortable: true, className: 'cell-date', render: (pkg) => new Date(pkg.created_at).toLocaleDateString(), }, ]} />
{pagination && pagination.total_pages > 1 && ( )}
); } export default ProjectPage;