import { useState } from 'react'; import { useOutletContext, useParams } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Download, Puzzle, Search, Star, ToggleLeft, ToggleRight, Trash2, Upload, Store, Plus, Pencil, } from 'lucide-react'; import { toast } from 'sonner'; import { api, ApiError } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; interface PluginInstallField { key: string; label: string; type: 'text' | 'number' | 'boolean' | 'select'; description?: string; required?: boolean; defaultValue?: unknown; options?: Array<{ label: string; value: string }>; min?: number; max?: number; pattern?: string; secret?: boolean; } interface InstalledPluginRelease { id: string; version: string; installSchema: PluginInstallField[]; } interface InstalledPlugin { id: string; pluginId: string; releaseId: string | null; name: string; slug: string; description: string | null; source: 'spiget' | 'manual'; externalId: string | null; installedVersion: string | null; isActive: boolean; installOptions: Record; autoUpdateChannel: 'stable' | 'beta' | 'alpha'; isPinned: boolean; status: 'installed' | 'updating' | 'failed'; lastError: string | null; updateAvailable: boolean; latestReleaseId: string | null; latestVersion: string | null; latestChannel: 'stable' | 'beta' | 'alpha' | null; installedAt: string; currentRelease: InstalledPluginRelease | null; } interface SpigetResult { id: number; name: string; tag: string; downloads: number; rating: { average: number; count: number }; updateDate: number; external: boolean; } interface MarketplacePlugin { id: string; name: string; slug: string; description: string | null; source: 'spiget' | 'manual'; externalId: string | null; downloadUrl: string | null; version: string | null; updatedAt: string; isInstalled: boolean; installId: string | null; installedVersion: string | null; isActive: boolean; isPinned: boolean; autoUpdateChannel: 'stable' | 'beta' | 'alpha'; installedAt: string | null; releaseId: string | null; updateAvailable: boolean; latestRelease: { id: string; version: string; channel: 'stable' | 'beta' | 'alpha'; artifactType: 'file' | 'zip'; artifactUrl: string; installSchema: PluginInstallField[]; } | null; } interface MarketplaceResponse { game: { id: string; slug: string; name: string; }; plugins: MarketplacePlugin[]; } function extractApiMessage(error: unknown, fallback: string): string { if (error instanceof ApiError && error.data && typeof error.data === 'object') { const maybeMessage = (error.data as { message?: unknown }).message; if (typeof maybeMessage === 'string' && maybeMessage.trim()) { return maybeMessage; } } return fallback; } function buildInstallOptionsState( fields: PluginInstallField[], current: Record = {}, ): Record { const next: Record = {}; for (const field of fields) { if (field.defaultValue !== undefined) { next[field.key] = field.defaultValue; } } for (const [key, value] of Object.entries(current)) { next[key] = value; } return next; } function PluginInstallSchemaFields({ fields, values, onChange, }: { fields: PluginInstallField[]; values: Record; onChange: (next: Record) => void; }) { return ( <> {fields.map((field) => (
{field.type === 'select' ? ( ) : field.type === 'boolean' ? ( ) : ( onChange({ ...values, [field.key]: field.type === 'number' ? e.target.value === '' ? '' : Number(e.target.value) : e.target.value, }) } min={field.type === 'number' ? field.min : undefined} max={field.type === 'number' ? field.max : undefined} pattern={field.type === 'text' ? field.pattern : undefined} required={Boolean(field.required)} /> )} {field.description && (

{field.description}

)}
))} ); } export function PluginsPage() { const { orgId, serverId } = useParams(); const { server } = useOutletContext<{ server?: { gameSlug: string } }>(); const isMinecraft = server?.gameSlug === 'minecraft-java'; const { data: pluginsData } = useQuery({ queryKey: ['plugins', orgId, serverId], queryFn: () => api.get<{ plugins: InstalledPlugin[] }>( `/organizations/${orgId}/servers/${serverId}/plugins`, ), }); const installed = pluginsData?.plugins ?? []; return ( Marketplace Installed ({installed.length}) {isMinecraft && ( Spiget Search )} Manual Install {isMinecraft && ( )} ); } function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: string }) { const queryClient = useQueryClient(); const [search, setSearch] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [createOpen, setCreateOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [editingPluginId, setEditingPluginId] = useState(null); const [name, setName] = useState(''); const [slug, setSlug] = useState(''); const [downloadUrl, setDownloadUrl] = useState(''); const [version, setVersion] = useState(''); const [description, setDescription] = useState(''); const [installDialogOpen, setInstallDialogOpen] = useState(false); const [installTarget, setInstallTarget] = useState(null); const [installOptions, setInstallOptions] = useState>({}); const [installPinVersion, setInstallPinVersion] = useState(false); const [installAutoUpdateChannel, setInstallAutoUpdateChannel] = useState< 'stable' | 'beta' | 'alpha' >('stable'); const { data, isLoading } = useQuery({ queryKey: ['plugin-marketplace', orgId, serverId, searchTerm], queryFn: () => api.get( `/organizations/${orgId}/servers/${serverId}/plugins/marketplace`, searchTerm ? { q: searchTerm } : undefined, ), }); const installMutation = useMutation({ mutationFn: ({ pluginId, payload, }: { pluginId: string; payload?: { releaseId?: string; options?: Record; pinVersion?: boolean; autoUpdateChannel?: 'stable' | 'beta' | 'alpha'; }; }) => api.post( `/organizations/${orgId}/servers/${serverId}/plugins/install/${pluginId}`, payload ?? {}, ), onSuccess: () => { toast.success('Plugin installed'); setInstallDialogOpen(false); setInstallTarget(null); setInstallOptions({}); queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Plugin install failed')); }, }); const uninstallMutation = useMutation({ mutationFn: (installId: string) => api.delete(`/organizations/${orgId}/servers/${serverId}/plugins/${installId}`), onSuccess: () => { toast.success('Plugin uninstalled'); queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Plugin uninstall failed')); }, }); const updateInstallMutation = useMutation({ mutationFn: ({ installId }: { installId: string }) => api.post(`/organizations/${orgId}/servers/${serverId}/plugins/${installId}/update`), onSuccess: () => { toast.success('Plugin updated'); queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Plugin update failed')); }, }); const createMutation = useMutation({ mutationFn: (body: { name: string; slug?: string; description?: string; downloadUrl: string; version?: string; }) => api.post(`/organizations/${orgId}/servers/${serverId}/plugins/marketplace`, body), onSuccess: () => { toast.success('Marketplace plugin added'); setCreateOpen(false); setName(''); setSlug(''); setDownloadUrl(''); setVersion(''); setDescription(''); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to add marketplace plugin')); }, }); const deleteMutation = useMutation({ mutationFn: (pluginId: string) => api.delete(`/organizations/${orgId}/servers/${serverId}/plugins/marketplace/${pluginId}`), onSuccess: () => { toast.success('Marketplace plugin removed'); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to remove marketplace plugin')); }, }); const updateMutation = useMutation({ mutationFn: (body: { pluginId: string; name: string; slug?: string; description?: string; downloadUrl?: string; version?: string; }) => api.patch( `/organizations/${orgId}/servers/${serverId}/plugins/marketplace/${body.pluginId}`, { name: body.name, slug: body.slug, description: body.description, downloadUrl: body.downloadUrl, version: body.version, }, ), onSuccess: () => { toast.success('Marketplace plugin updated'); setEditOpen(false); setEditingPluginId(null); setName(''); setSlug(''); setDownloadUrl(''); setVersion(''); setDescription(''); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to update marketplace plugin')); }, }); const openEditDialog = (plugin: MarketplacePlugin) => { setEditingPluginId(plugin.id); setName(plugin.name); setSlug(plugin.slug); setDownloadUrl(plugin.downloadUrl ?? ''); setVersion(plugin.version ?? ''); setDescription(plugin.description ?? ''); setEditOpen(true); }; const openInstallDialog = (plugin: MarketplacePlugin) => { const fields = plugin.latestRelease?.installSchema ?? []; setInstallTarget(plugin); setInstallOptions(buildInstallOptionsState(fields)); setInstallPinVersion(false); setInstallAutoUpdateChannel('stable'); setInstallDialogOpen(true); }; const installDirect = (plugin: MarketplacePlugin) => { installMutation.mutate({ pluginId: plugin.id, payload: { releaseId: plugin.latestRelease?.id ?? undefined, pinVersion: false, autoUpdateChannel: 'stable', }, }); }; const plugins = data?.plugins ?? []; const gameName = data?.game.name ?? 'Game'; return (

{gameName} Marketplace

Oyununuza uygun eklentileri tek tıkla kur/kaldırın.

Marketplace Plugin Ekle
{ e.preventDefault(); createMutation.mutate({ name, slug: slug || undefined, description: description || undefined, downloadUrl, version: version || undefined, }); }} >
setName(e.target.value)} required />
setSlug(e.target.value)} />
setDownloadUrl(e.target.value)} placeholder="https://example.com/plugin.jar" required />
setVersion(e.target.value)} />
setDescription(e.target.value)} />
{ setEditOpen(open); if (!open) { setEditingPluginId(null); setName(''); setSlug(''); setDownloadUrl(''); setVersion(''); setDescription(''); } }} > Marketplace Plugin Düzenle
{ e.preventDefault(); if (!editingPluginId) return; updateMutation.mutate({ pluginId: editingPluginId, name, slug: slug || undefined, description: description || undefined, downloadUrl: downloadUrl || undefined, version: version || undefined, }); }} >
setName(e.target.value)} required />
setSlug(e.target.value)} />
setDownloadUrl(e.target.value)} placeholder="https://example.com/plugin.jar" />
setVersion(e.target.value)} />
setDescription(e.target.value)} />
setSearch(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') setSearchTerm(search.trim()); }} />
{isLoading &&

Marketplace yükleniyor...

} {!isLoading && plugins.length === 0 && (

Bu oyun için plugin bulunamadı

Yetkili kullanıcılar yukarıdan marketplace plugin ekleyebilir.

)}
{plugins.map((plugin) => (

{plugin.name}

{plugin.source} {plugin.latestRelease?.version && ( v{plugin.latestRelease.version} )} {plugin.isInstalled && Installed} {plugin.updateAvailable && Update Available}
{plugin.description && (

{plugin.description}

)} {plugin.latestRelease?.artifactUrl && (

{plugin.latestRelease.artifactUrl}

)}
{plugin.isInstalled && plugin.installId ? ( <> {plugin.updateAvailable && ( )} ) : ( <> {(plugin.latestRelease?.installSchema?.length ?? 0) > 0 ? ( ) : ( )} )}
))}
Install Plugin {installTarget ? ` - ${installTarget.name}` : ''}
{ event.preventDefault(); if (!installTarget) return; installMutation.mutate({ pluginId: installTarget.id, payload: { releaseId: installTarget.latestRelease?.id ?? undefined, options: installOptions, pinVersion: installPinVersion, autoUpdateChannel: installAutoUpdateChannel, }, }); }} >
); } function InstalledPlugins({ installed, orgId, serverId, }: { installed: InstalledPlugin[]; orgId: string; serverId: string; }) { const queryClient = useQueryClient(); const [configureDialogOpen, setConfigureDialogOpen] = useState(false); const [configureTarget, setConfigureTarget] = useState(null); const [configureOptions, setConfigureOptions] = useState>({}); const toggleMutation = useMutation({ mutationFn: (id: string) => api.patch(`/organizations/${orgId}/servers/${serverId}/plugins/${id}/toggle`), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }), onError: (error) => { toast.error(extractApiMessage(error, 'Plugin durumu güncellenemedi')); }, }); const uninstallMutation = useMutation({ mutationFn: (id: string) => api.delete(`/organizations/${orgId}/servers/${serverId}/plugins/${id}`), onSuccess: () => { toast.success('Plugin kaldırıldı'); queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Plugin kaldırılamadı')); }, }); const updateMutation = useMutation({ mutationFn: (id: string) => api.post(`/organizations/${orgId}/servers/${serverId}/plugins/${id}/update`), onSuccess: () => { toast.success('Plugin güncellendi'); queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Plugin güncellenemedi')); }, }); const configureMutation = useMutation({ mutationFn: ({ id, payload, }: { id: string; payload: { releaseId: string; options: Record; }; }) => api.post(`/organizations/${orgId}/servers/${serverId}/plugins/${id}/update`, payload), onSuccess: () => { toast.success('Plugin ayarları güncellendi'); setConfigureDialogOpen(false); setConfigureTarget(null); setConfigureOptions({}); queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Plugin ayarları güncellenemedi')); }, }); const openConfigureDialog = (plugin: InstalledPlugin) => { const fields = plugin.currentRelease?.installSchema ?? []; setConfigureTarget(plugin); setConfigureOptions(buildInstallOptionsState(fields, plugin.installOptions)); setConfigureDialogOpen(true); }; if (installed.length === 0) { return (

No plugins installed

Marketplace sekmesinden tek tıkla kurulum yapabilirsiniz.

); } return ( <>
{installed.map((plugin) => (

{plugin.name}

{plugin.source} {plugin.installedVersion && ( v{plugin.installedVersion} )} {!plugin.isActive && Disabled} {plugin.status !== 'installed' && ( {plugin.status} )} {plugin.updateAvailable && ( Update Available )}
{plugin.description && (

{plugin.description}

)} {plugin.latestVersion && (

Latest: v{plugin.latestVersion} {plugin.latestChannel ? ` (${plugin.latestChannel})` : ''}

)} {plugin.lastError && (

{plugin.lastError}

)}
{plugin.currentRelease && plugin.currentRelease.installSchema.length > 0 && ( )} {plugin.updateAvailable && ( )}
))}
{ setConfigureDialogOpen(open); if (!open) { setConfigureTarget(null); setConfigureOptions({}); } }} > Plugin Ayarları {configureTarget ? ` - ${configureTarget.name}` : ''}
{ event.preventDefault(); if (!configureTarget?.currentRelease) return; configureMutation.mutate({ id: configureTarget.id, payload: { releaseId: configureTarget.currentRelease.id, options: configureOptions, }, }); }} >
); } function SpigetSearch({ orgId, serverId }: { orgId: string; serverId: string }) { const queryClient = useQueryClient(); const [query, setQuery] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const { data: results, isLoading } = useQuery({ queryKey: ['spiget-search', orgId, serverId, searchTerm], queryFn: () => api.get<{ results: SpigetResult[] }>( `/organizations/${orgId}/servers/${serverId}/plugins/search`, { q: searchTerm }, ), enabled: searchTerm.length >= 2, }); const installMutation = useMutation({ mutationFn: (resourceId: number) => api.post(`/organizations/${orgId}/servers/${serverId}/plugins/install/spiget`, { resourceId, }), onSuccess: () => { toast.success('Spiget plugin installed'); queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Spiget install failed')); }, }); const handleSearch = () => { if (query.length >= 2) setSearchTerm(query); }; return (
setQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} />
{isLoading &&

Searching...

} {results?.results && results.results.length === 0 && (

No results found

)}
{results?.results?.map((r) => (

{r.name}

{r.tag}

{r.rating.average.toFixed(1)} ({r.rating.count}) {r.downloads.toLocaleString()}
))}
); } function ManualInstall({ orgId, serverId }: { orgId: string; serverId: string }) { const queryClient = useQueryClient(); const [name, setName] = useState(''); const [filePath, setFilePath] = useState(''); const [version, setVersion] = useState(''); const installMutation = useMutation({ mutationFn: (body: { name: string; filePath: string; version?: string }) => api.post(`/organizations/${orgId}/servers/${serverId}/plugins/install/manual`, body), onSuccess: () => { toast.success('Plugin registered'); queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }); queryClient.invalidateQueries({ queryKey: ['plugin-marketplace', orgId, serverId] }); setName(''); setFilePath(''); setVersion(''); }, onError: (error) => { toast.error(extractApiMessage(error, 'Plugin registration failed')); }, }); return ( Manual Plugin Install
{ e.preventDefault(); installMutation.mutate({ name, filePath, version: version || undefined, }); }} >
setName(e.target.value)} required />
setFilePath(e.target.value)} placeholder="plugins/plugin.jar" required />

Files sekmesinden dosyayı önce sunucuya yükleyin. Relative path girerseniz oyunun varsayılan plugin dizinine göre çözülür.

setVersion(e.target.value)} />
); }