"use client"; import { HardDriveUpload, LoaderCircle, Package, PackageSearch, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { ConfirmDialog } from "@/components/confirm-dialog"; import { PageHeader, PageShell, StatePanel } from "@/components/page-layout"; import { SectionCard } from "@/components/section-card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/lib/api-client"; type Project = { provider: "MODRINTH" | "HANGAR"; projectId: string; name: string; description: string; iconUrl: string | null; downloads: number; license: string | null; author: string; categories: string[]; minecraftVersions: string[]; updatedAt: string | null; projectUrl: string; }; type Version = { provider: "MODRINTH" | "HANGAR"; projectId: string; projectName?: string; versionId: string; version: string; platform: "PAPER" | "FOLIA" | "VELOCITY"; minecraftVersions: string[]; filename: string; size: number; sha256: string | null; downloadUrl: string; license: string | null; description?: string; author?: string; iconUrl?: string | null; projectUrl?: string; categories?: string[]; updatedAt?: string | null; }; type Artifact = { id: string; provider: string; name: string; version: string; filename: string; description: string | null; author: string | null; icon_url: string | null; project_url: string | null; categories: string[]; server_plugins: Array<{ server_id: string }>; }; export function PluginRegistry() { const [query, setQuery] = useState(""); const [projects, setProjects] = useState([]); const [versions, setVersions] = useState([]); const [selectedProject, setSelectedProject] = useState(null); const [artifacts, setArtifacts] = useState([]); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const platform = "PAPER" as const; const loadArtifacts = async () => { const response = await api.api.registry.artifacts.get(); if (response.error) throw response.error; setArtifacts((response.data ?? []) as Artifact[]); }; useEffect(() => { void api.api.registry.artifacts .get() .then(({ data, error: responseError }) => { if (responseError) throw responseError; setArtifacts((data ?? []) as Artifact[]); }) .catch((loadError) => setError(loadError instanceof Error ? loadError.message : "Failed to load registry") ); }, []); useEffect(() => { if (projects.length > 0) return; void api.api.registry.search .get({ query: { query: "", platform, }, }) .then(({ data, error: responseError }) => { if (responseError) throw responseError; setProjects((data ?? []) as Project[]); }) .catch((loadError) => setError(loadError instanceof Error ? loadError.message : "Failed to load top plugins") ); }, [platform, projects.length]); const search = async () => { if (!query.trim()) return; setBusy(true); setError(null); setSelectedProject(null); setVersions([]); try { const response = await api.api.registry.search.get({ query: { query, platform, }, }); if (response.error) throw response.error; setProjects((response.data ?? []) as Project[]); } catch (searchError) { setError(searchError instanceof Error ? searchError.message : "Search failed"); } finally { setBusy(false); } }; const selectProject = async (project: Project) => { setBusy(true); setError(null); setSelectedProject(project); try { const response = await api.api.registry.versions.get({ query: { provider: project.provider, projectId: project.projectId, platform, }, }); if (response.error) throw response.error; setVersions((response.data ?? []) as Version[]); } catch (versionError) { setError(versionError instanceof Error ? versionError.message : "Failed to load versions"); } finally { setBusy(false); } }; const store = async (version: Version) => { setBusy(true); setError(null); try { const response = await api.api.registry.artifacts.post({ ...version, projectName: selectedProject?.name, description: selectedProject?.description, author: selectedProject?.author, iconUrl: selectedProject?.iconUrl, projectUrl: selectedProject?.projectUrl, categories: selectedProject?.categories, updatedAt: selectedProject?.updatedAt, }); if (response.error) throw response.error; await loadArtifacts(); } catch (installError) { setError(installError instanceof Error ? installError.message : "Registry import failed"); } finally { setBusy(false); } }; const upload = async (file: File | undefined) => { if (!file) return; setBusy(true); setError(null); try { const response = await api.api.registry.artifacts.upload.post({ file }); if (response.error) throw response.error; await loadArtifacts(); } catch (uploadError) { setError(uploadError instanceof Error ? uploadError.message : "Upload failed"); } finally { setBusy(false); } }; const deleteArtifact = async () => { if (!deleteTarget) return; setBusy(true); setError(null); try { const response = await api.api.registry.library({ artifactId: deleteTarget.id }).delete(); if (response.error) throw response.error; setDeleteTarget(null); await loadArtifacts(); } catch (deleteError) { setError(deleteError instanceof Error ? deleteError.message : "Failed to delete artifact"); } finally { setBusy(false); } }; return ( Discover Library {artifacts.length} } >
setQuery(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); void search(); } }} placeholder="Search ViaVersion, LuckPerms, CoreProtect..." />
{error && (

{error}

)} {selectedProject ? (
{selectedProject.iconUrl ? ( ) : (
)}

{selectedProject.name}

by {selectedProject.author} ·{" "} {selectedProject.downloads.toLocaleString()} downloads

{selectedProject.description}

{selectedProject.provider} {selectedProject.license && ( {selectedProject.license} )} {selectedProject.categories.slice(0, 4).map((category) => ( {category} ))}
{!busy && versions.length === 0 && ( )} {versions.slice(0, 15).map((version) => (

{version.version}

{version.filename} · {(version.size / 1024 / 1024).toFixed(1)} MiB

))}
) : projects.length > 0 ? (
{!query.trim() && (

Top plugins

Popular projects ranked by downloads

{projects.length} projects
)}
{projects.map((project, index) => ( ))}
) : ( } className="min-h-56" /> )}
} headerAction={ } > {artifacts.length === 0 ? ( } className="min-h-52" /> ) : (
{artifacts.map((artifact) => (
{artifact.icon_url ? ( ) : (
)} {artifact.provider === "UPLOAD" ? "Private" : artifact.provider}

{artifact.name}

{artifact.author && (

by {artifact.author}

)}

{artifact.version} · {artifact.filename}

{artifact.description && (

{artifact.description}

)} {artifact.categories.length > 0 && (
{artifact.categories.slice(0, 3).map((category) => ( {category} ))}
)}

Deployed to {artifact.server_plugins.length}{" "} {artifact.server_plugins.length === 1 ? "server" : "servers"}

Assign from Create/Edit Server {artifact.project_url && ( )}
))}
)}
Delete {deleteTarget?.name} from the global registry? This removes it from every server and permanently deletes private S3 content. } confirmLabel="Delete Artifact" onConfirm={deleteArtifact} onOpenChange={(open) => !open && setDeleteTarget(null)} />
); }