chore: initial commit for phase06

This commit is contained in:
hibna
2026-02-21 23:46:01 +03:00
parent 0941a9ba46
commit 5709d8bc10
16 changed files with 1667 additions and 15 deletions
+316 -5
View File
@@ -1,12 +1,323 @@
import { Puzzle } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { useState } from 'react';
import { useParams } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
Puzzle,
Search,
Download,
Trash2,
ToggleLeft,
ToggleRight,
Star,
Upload,
} from 'lucide-react';
import { api } 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, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
interface InstalledPlugin {
id: string;
pluginId: string;
name: string;
slug: string;
description: string | null;
source: 'spiget' | 'manual';
externalId: string | null;
installedVersion: string | null;
isActive: boolean;
installedAt: string;
}
interface SpigetResult {
id: number;
name: string;
tag: string;
downloads: number;
rating: { average: number; count: number };
updateDate: number;
external: boolean;
}
export function PluginsPage() {
const { orgId, serverId } = useParams();
const queryClient = useQueryClient();
const { data: pluginsData } = useQuery({
queryKey: ['plugins', orgId, serverId],
queryFn: () =>
api.get<{ plugins: InstalledPlugin[] }>(
`/organizations/${orgId}/servers/${serverId}/plugins`,
),
});
const installed = pluginsData?.plugins ?? [];
return (
<Tabs defaultValue="installed" className="space-y-4">
<TabsList>
<TabsTrigger value="installed">
<Puzzle className="mr-1.5 h-3.5 w-3.5" />
Installed ({installed.length})
</TabsTrigger>
<TabsTrigger value="search">
<Search className="mr-1.5 h-3.5 w-3.5" />
Search Plugins
</TabsTrigger>
<TabsTrigger value="manual">
<Upload className="mr-1.5 h-3.5 w-3.5" />
Manual Install
</TabsTrigger>
</TabsList>
<TabsContent value="installed">
<InstalledPlugins installed={installed} orgId={orgId!} serverId={serverId!} />
</TabsContent>
<TabsContent value="search">
<SpigetSearch orgId={orgId!} serverId={serverId!} />
</TabsContent>
<TabsContent value="manual">
<ManualInstall orgId={orgId!} serverId={serverId!} />
</TabsContent>
</Tabs>
);
}
function InstalledPlugins({
installed,
orgId,
serverId,
}: {
installed: InstalledPlugin[];
orgId: string;
serverId: string;
}) {
const queryClient = useQueryClient();
const toggleMutation = useMutation({
mutationFn: (id: string) =>
api.patch(`/organizations/${orgId}/servers/${serverId}/plugins/${id}/toggle`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }),
});
const uninstallMutation = useMutation({
mutationFn: (id: string) =>
api.delete(`/organizations/${orgId}/servers/${serverId}/plugins/${id}`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }),
});
if (installed.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Puzzle className="mb-4 h-12 w-12 text-muted-foreground/50" />
<p className="text-muted-foreground">No plugins installed</p>
<p className="mt-1 text-xs text-muted-foreground">
Search for plugins or install manually
</p>
</CardContent>
</Card>
);
}
return (
<div className="space-y-2">
{installed.map((plugin) => (
<Card key={plugin.id}>
<CardContent className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<Puzzle className="h-5 w-5 text-primary" />
<div>
<div className="flex items-center gap-2">
<p className="font-medium">{plugin.name}</p>
<Badge variant="outline">{plugin.source}</Badge>
{!plugin.isActive && <Badge variant="secondary">Disabled</Badge>}
</div>
{plugin.description && (
<p className="text-sm text-muted-foreground">{plugin.description}</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Button
size="icon"
variant="ghost"
onClick={() => toggleMutation.mutate(plugin.id)}
title={plugin.isActive ? 'Disable' : 'Enable'}
>
{plugin.isActive ? (
<ToggleRight className="h-4 w-4 text-green-500" />
) : (
<ToggleLeft className="h-4 w-4 text-muted-foreground" />
)}
</Button>
<Button
size="icon"
variant="ghost"
onClick={() => uninstallMutation.mutate(plugin.id)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
);
}
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: () => queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] }),
});
const handleSearch = () => {
if (query.length >= 2) setSearchTerm(query);
};
return (
<div className="space-y-4">
<div className="flex gap-2">
<Input
placeholder="Search Spiget plugins (Minecraft only)..."
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
<Button onClick={handleSearch} disabled={query.length < 2}>
<Search className="h-4 w-4" />
Search
</Button>
</div>
{isLoading && <p className="text-sm text-muted-foreground">Searching...</p>}
{results?.results && results.results.length === 0 && (
<p className="text-sm text-muted-foreground">No results found</p>
)}
<div className="space-y-2">
{results?.results?.map((r) => (
<Card key={r.id}>
<CardContent className="flex items-center justify-between p-4">
<div>
<p className="font-medium">{r.name}</p>
<p className="text-sm text-muted-foreground">{r.tag}</p>
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Star className="h-3 w-3" />
{r.rating.average.toFixed(1)} ({r.rating.count})
</span>
<span>
<Download className="inline h-3 w-3" /> {r.downloads.toLocaleString()}
</span>
</div>
</div>
<Button
size="sm"
onClick={() => installMutation.mutate(r.id)}
disabled={installMutation.isPending || r.external}
>
<Download className="h-4 w-4" />
{r.external ? 'External' : 'Install'}
</Button>
</CardContent>
</Card>
))}
</div>
</div>
);
}
function ManualInstall({ orgId, serverId }: { orgId: string; serverId: string }) {
const queryClient = useQueryClient();
const [name, setName] = useState('');
const [fileName, setFileName] = useState('');
const [version, setVersion] = useState('');
const installMutation = useMutation({
mutationFn: (body: { name: string; fileName: string; version?: string }) =>
api.post(`/organizations/${orgId}/servers/${serverId}/plugins/install/manual`, body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['plugins', orgId, serverId] });
setName('');
setFileName('');
setVersion('');
},
});
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Puzzle className="mb-4 h-12 w-12 text-muted-foreground/50" />
<p className="text-muted-foreground">Plugin management coming soon</p>
<CardHeader>
<CardTitle>Manual Plugin Install</CardTitle>
</CardHeader>
<CardContent>
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
installMutation.mutate({
name,
fileName,
version: version || undefined,
});
}}
>
<div className="space-y-2">
<Label>Plugin Name</Label>
<Input value={name} onChange={(e) => setName(e.target.value)} required />
</div>
<div className="space-y-2">
<Label>File Name</Label>
<Input
value={fileName}
onChange={(e) => setFileName(e.target.value)}
placeholder="plugin.jar"
required
/>
<p className="text-xs text-muted-foreground">
Upload the file to /plugins/ directory via the Files tab first
</p>
</div>
<div className="space-y-2">
<Label>Version (optional)</Label>
<Input value={version} onChange={(e) => setVersion(e.target.value)} />
</div>
<Button type="submit" disabled={installMutation.isPending}>
{installMutation.isPending ? 'Registering...' : 'Register Plugin'}
</Button>
</form>
</CardContent>
</Card>
);