import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Plus, UploadCloud, Puzzle, Rocket, Copy } 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 { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; interface Game { id: string; slug: string; name: string; } interface GamesResponse { data: Game[]; } interface GlobalPlugin { id: string; gameId: string; gameName: string; gameSlug: string; name: string; slug: string; description: string | null; source: 'manual' | 'spiget'; isGlobal: boolean; } interface GlobalPluginsResponse { data: GlobalPlugin[]; } interface PluginRelease { id: string; pluginId: string; version: string; channel: 'stable' | 'beta' | 'alpha'; artifactType: 'file' | 'zip'; artifactUrl: string; destination: string | null; fileName: string | null; changelog: string | null; installSchema: unknown[]; configTemplates: unknown[]; isPublished: boolean; createdAt: string; updatedAt: string; } interface PluginReleaseResponse { plugin: GlobalPlugin; releases: PluginRelease[]; } type ReleaseInputMode = 'url' | 'upload'; 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 prettyJson(input: unknown): string { try { return JSON.stringify(input, null, 2); } catch { return '[]'; } } function parseJsonArray(raw: string): unknown[] { if (raw.trim() === '') return []; const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) { throw new Error('JSON value must be an array'); } return parsed; } async function parseJsonArrayFile(file: File, label: string): Promise { let raw = await file.text(); if (raw.charCodeAt(0) === 0xfeff) { raw = raw.slice(1); } try { const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) { throw new Error('JSON value must be an array'); } return parsed; } catch (error) { const message = error instanceof Error ? error.message : 'Invalid JSON'; throw new Error(`${label}: ${message}`); } } export function AdminPluginsPage() { const queryClient = useQueryClient(); const [selectedGameId, setSelectedGameId] = useState(''); const [selectedPluginId, setSelectedPluginId] = useState(null); const [createPluginOpen, setCreatePluginOpen] = useState(false); const [createPluginName, setCreatePluginName] = useState(''); const [createPluginSlug, setCreatePluginSlug] = useState(''); const [createPluginDescription, setCreatePluginDescription] = useState(''); const [createReleaseOpen, setCreateReleaseOpen] = useState(false); const [releaseInputMode, setReleaseInputMode] = useState('upload'); const [releaseVersion, setReleaseVersion] = useState(''); const [releaseChannel, setReleaseChannel] = useState<'stable' | 'beta' | 'alpha'>('stable'); const [releaseArtifactType, setReleaseArtifactType] = useState<'file' | 'zip'>('file'); const [releaseArtifactUrl, setReleaseArtifactUrl] = useState(''); const [releaseDestination, setReleaseDestination] = useState(''); const [releaseFileName, setReleaseFileName] = useState(''); const [releaseChangelog, setReleaseChangelog] = useState(''); const [releaseInstallSchemaJson, setReleaseInstallSchemaJson] = useState('[]'); const [releaseTemplatesJson, setReleaseTemplatesJson] = useState('[]'); const [releaseInstallSchemaFile, setReleaseInstallSchemaFile] = useState(null); const [releaseTemplatesFile, setReleaseTemplatesFile] = useState(null); const [releaseInstallSchemaFileInputKey, setReleaseInstallSchemaFileInputKey] = useState(0); const [releaseTemplatesFileInputKey, setReleaseTemplatesFileInputKey] = useState(0); const [releaseArtifactFiles, setReleaseArtifactFiles] = useState([]); const { data: gamesData } = useQuery({ queryKey: ['admin-games'], queryFn: () => api.get('/admin/games'), }); const games = gamesData?.data ?? []; const { data: pluginsData } = useQuery({ queryKey: ['admin-plugins', selectedGameId], queryFn: () => api.get( '/admin/plugins', selectedGameId ? { gameId: selectedGameId } : undefined, ), }); const plugins = pluginsData?.data ?? []; const selectedPlugin = useMemo( () => plugins.find((plugin) => plugin.id === selectedPluginId) ?? null, [plugins, selectedPluginId], ); const { data: releaseData } = useQuery({ queryKey: ['admin-plugin-releases', selectedPluginId], enabled: Boolean(selectedPluginId), queryFn: () => api.get(`/admin/plugins/${selectedPluginId}/releases`), }); const releases = releaseData?.releases ?? []; const resetReleaseForm = () => { setCreateReleaseOpen(false); setReleaseInputMode('upload'); setReleaseVersion(''); setReleaseChannel('stable'); setReleaseArtifactType('file'); setReleaseArtifactUrl(''); setReleaseDestination(''); setReleaseFileName(''); setReleaseChangelog(''); setReleaseInstallSchemaJson('[]'); setReleaseTemplatesJson('[]'); setReleaseInstallSchemaFile(null); setReleaseTemplatesFile(null); setReleaseInstallSchemaFileInputKey((prev) => prev + 1); setReleaseTemplatesFileInputKey((prev) => prev + 1); setReleaseArtifactFiles([]); }; const appendReleaseFiles = (incoming: FileList | null) => { if (!incoming || incoming.length === 0) return; setReleaseArtifactFiles((prev) => { const map = new Map(); for (const item of prev) { const relative = (item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name; map.set(`${relative}::${item.size}::${item.lastModified}`, item); } for (const item of Array.from(incoming)) { const relative = (item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name; map.set(`${relative}::${item.size}::${item.lastModified}`, item); } return Array.from(map.values()); }); }; const createPluginMutation = useMutation({ mutationFn: (body: { gameId: string; name: string; slug?: string; description?: string; }) => api.post('/admin/plugins', body), onSuccess: () => { toast.success('Global plugin created'); setCreatePluginOpen(false); setCreatePluginName(''); setCreatePluginSlug(''); setCreatePluginDescription(''); queryClient.invalidateQueries({ queryKey: ['admin-plugins'] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to create plugin')); }, }); const createReleaseMutation = useMutation({ mutationFn: (body: { version: string; channel: 'stable' | 'beta' | 'alpha'; artifactType: 'file' | 'zip'; artifactUrl: string; destination?: string; fileName?: string; changelog?: string; installSchema?: unknown[]; configTemplates?: unknown[]; }) => { if (!selectedPluginId) { throw new Error('No plugin selected'); } return api.post(`/admin/plugins/${selectedPluginId}/releases`, body); }, onSuccess: () => { toast.success('Release published'); resetReleaseForm(); queryClient.invalidateQueries({ queryKey: ['admin-plugin-releases', selectedPluginId] }); queryClient.invalidateQueries({ queryKey: ['admin-plugins'] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to publish release')); }, }); const createUploadReleaseMutation = useMutation({ mutationFn: (formData: FormData) => { if (!selectedPluginId) { throw new Error('No plugin selected'); } return api.post(`/admin/plugins/${selectedPluginId}/releases/upload`, formData); }, onSuccess: () => { toast.success('Release uploaded and published'); resetReleaseForm(); queryClient.invalidateQueries({ queryKey: ['admin-plugin-releases', selectedPluginId] }); queryClient.invalidateQueries({ queryKey: ['admin-plugins'] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to upload release')); }, }); const togglePublishedMutation = useMutation({ mutationFn: ({ releaseId, isPublished }: { releaseId: string; isPublished: boolean }) => { if (!selectedPluginId) { throw new Error('No plugin selected'); } return api.patch(`/admin/plugins/${selectedPluginId}/releases/${releaseId}`, { isPublished }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-plugin-releases', selectedPluginId] }); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to update release')); }, }); const openReleaseDialogFrom = (release?: PluginRelease) => { setReleaseVersion(''); setReleaseChannel('stable'); setReleaseArtifactType('file'); setReleaseArtifactUrl(''); setReleaseDestination(''); setReleaseFileName(''); setReleaseChangelog(''); setReleaseInstallSchemaJson('[]'); setReleaseTemplatesJson('[]'); setReleaseInstallSchemaFile(null); setReleaseTemplatesFile(null); setReleaseInstallSchemaFileInputKey((prev) => prev + 1); setReleaseTemplatesFileInputKey((prev) => prev + 1); setReleaseArtifactFiles([]); setReleaseInputMode(release ? 'url' : 'upload'); if (release) { setReleaseChannel(release.channel); setReleaseArtifactType(release.artifactType); setReleaseArtifactUrl(release.artifactUrl); setReleaseDestination(release.destination ?? ''); setReleaseFileName(release.fileName ?? ''); setReleaseChangelog(release.changelog ?? ''); setReleaseInstallSchemaJson(prettyJson(release.installSchema)); setReleaseTemplatesJson(prettyJson(release.configTemplates)); } setCreateReleaseOpen(true); }; return (

Global Plugins

Oyun bazında global plugin tanımla, release yayınla, install ayar şemasını yönet.

Create Global Plugin
{ event.preventDefault(); if (!selectedGameId) { toast.error('Select a game first'); return; } createPluginMutation.mutate({ gameId: selectedGameId, name: createPluginName, slug: createPluginSlug || undefined, description: createPluginDescription || undefined, }); }} >
game.id === selectedGameId)?.name ?? ''} readOnly placeholder="Select game from filter above" />
setCreatePluginName(e.target.value)} required />
setCreatePluginSlug(e.target.value)} />
setCreatePluginDescription(e.target.value)} />
Plugins {plugins.length === 0 && (

No plugins found for this filter.

)} {plugins.map((plugin) => ( ))}
Releases
{!selectedPlugin && (

Select a plugin to manage releases.

)} {selectedPlugin && releases.length === 0 && (

No releases published yet.

)} {releases.map((release) => (
v{release.version} {release.channel} {release.artifactType} {!release.isPublished && Unpublished}

{release.artifactUrl}

Schema: {Array.isArray(release.installSchema) ? release.installSchema.length : 0} fields • Templates:{' '} {Array.isArray(release.configTemplates) ? release.configTemplates.length : 0}

))}
{ if (open) { setCreateReleaseOpen(true); return; } resetReleaseForm(); }} > Publish Release{selectedPlugin ? ` - ${selectedPlugin.name}` : ''}
{ event.preventDefault(); try { const installSchema = releaseInstallSchemaFile ? await parseJsonArrayFile(releaseInstallSchemaFile, 'Install schema file') : parseJsonArray(releaseInstallSchemaJson); const configTemplates = releaseTemplatesFile ? await parseJsonArrayFile(releaseTemplatesFile, 'Config templates file') : parseJsonArray(releaseTemplatesJson); if (releaseInputMode === 'upload') { if (releaseArtifactFiles.length === 0) { toast.error('Select at least one file or folder'); return; } const formData = new FormData(); formData.append('version', releaseVersion); formData.append('channel', releaseChannel); if (releaseDestination.trim()) formData.append('destination', releaseDestination.trim()); if (releaseFileName.trim()) formData.append('fileName', releaseFileName.trim()); if (releaseChangelog.trim()) formData.append('changelog', releaseChangelog); if (releaseInstallSchemaFile) { formData.append( 'installSchemaFile', releaseInstallSchemaFile, releaseInstallSchemaFile.name, ); } else { formData.append('installSchema', JSON.stringify(installSchema)); } if (releaseTemplatesFile) { formData.append( 'configTemplatesFile', releaseTemplatesFile, releaseTemplatesFile.name, ); } else { formData.append('configTemplates', JSON.stringify(configTemplates)); } for (const file of releaseArtifactFiles) { const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath; formData.append('relativePath', relativePath && relativePath.length > 0 ? relativePath : file.name); formData.append('files', file, file.name); } createUploadReleaseMutation.mutate(formData); return; } createReleaseMutation.mutate({ version: releaseVersion, channel: releaseChannel, artifactType: releaseArtifactType, artifactUrl: releaseArtifactUrl, destination: releaseDestination || undefined, fileName: releaseFileName || undefined, changelog: releaseChangelog || undefined, installSchema, configTemplates, }); } catch (error) { const message = error instanceof Error ? error.message : 'Invalid JSON'; toast.error(`Release JSON error: ${message}`); } }} >
setReleaseVersion(e.target.value)} required />
{releaseInputMode === 'url' && (
setReleaseArtifactUrl(e.target.value)} required={releaseInputMode === 'url'} />
)} {releaseInputMode === 'upload' && (

Tek dosya secersen tekil upload olur. Birden fazla dosya veya klasor secersen otomatik zip yapilip CDN'e yuklenir.

appendReleaseFiles(e.target.files)} />
)} className="block w-full text-sm" onChange={(e) => appendReleaseFiles(e.target.files)} />

Selected: {releaseArtifactFiles.length} file(s)

{releaseArtifactFiles.length > 0 && ( )}
{releaseArtifactFiles.length > 0 && (
{releaseArtifactFiles.map((file, index) => { const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath; return (

{relativePath || file.name}

); })}
)}
)}
setReleaseDestination(e.target.value)} placeholder="/game/csgo/addons" />
setReleaseFileName(e.target.value)} placeholder="plugin.dll" />