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
+190
View File
@@ -0,0 +1,190 @@
import { useState } from 'react';
import { useParams } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Settings2, FileText, Save } 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, CardDescription } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
interface ConfigFile {
index: number;
path: string;
parser: string;
editableKeys: string[] | null;
}
interface ConfigEntry {
key: string;
value: string;
}
interface ConfigDetail {
path: string;
parser: string;
editableKeys: string[] | null;
entries: ConfigEntry[];
raw: string;
}
export function ConfigPage() {
const { orgId, serverId } = useParams();
const queryClient = useQueryClient();
const { data: configsData } = useQuery({
queryKey: ['configs', orgId, serverId],
queryFn: () =>
api.get<{ configs: ConfigFile[] }>(
`/organizations/${orgId}/servers/${serverId}/config`,
),
});
const configs = configsData?.configs ?? [];
if (configs.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Settings2 className="mb-4 h-12 w-12 text-muted-foreground/50" />
<p className="text-muted-foreground">No config files available for this game</p>
</CardContent>
</Card>
);
}
return (
<Tabs defaultValue="0" className="space-y-4">
<TabsList>
{configs.map((cf) => (
<TabsTrigger key={cf.index} value={String(cf.index)}>
<FileText className="mr-1.5 h-3.5 w-3.5" />
{cf.path.split('/').pop()}
</TabsTrigger>
))}
</TabsList>
{configs.map((cf) => (
<TabsContent key={cf.index} value={String(cf.index)}>
<ConfigEditor
orgId={orgId!}
serverId={serverId!}
configIndex={cf.index}
configFile={cf}
/>
</TabsContent>
))}
</Tabs>
);
}
function ConfigEditor({
orgId,
serverId,
configIndex,
configFile,
}: {
orgId: string;
serverId: string;
configIndex: number;
configFile: ConfigFile;
}) {
const queryClient = useQueryClient();
const { data: detail } = useQuery({
queryKey: ['config-detail', orgId, serverId, configIndex],
queryFn: () =>
api.get<ConfigDetail>(
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
),
});
const [entries, setEntries] = useState<ConfigEntry[]>([]);
const [initialized, setInitialized] = useState(false);
// Initialize entries from server data
if (detail && !initialized) {
setEntries(detail.entries);
setInitialized(true);
}
const saveMutation = useMutation({
mutationFn: (data: { entries: ConfigEntry[] }) =>
api.patch(
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
data,
),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['config-detail', orgId, serverId, configIndex],
});
},
});
const updateEntry = (key: string, value: string) => {
setEntries((prev) =>
prev.map((e) => (e.key === key ? { ...e, value } : e)),
);
};
const displayEntries = configFile.editableKeys
? entries.filter((e) => configFile.editableKeys!.includes(e.key))
: entries;
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
{configFile.path}
<Badge variant="outline">{configFile.parser}</Badge>
</CardTitle>
<CardDescription>
{configFile.editableKeys
? `${configFile.editableKeys.length} editable keys`
: 'All keys editable'}
</CardDescription>
</div>
<Button
size="sm"
onClick={() => saveMutation.mutate({ entries })}
disabled={saveMutation.isPending}
>
<Save className="h-4 w-4" />
{saveMutation.isPending ? 'Saving...' : 'Save'}
</Button>
</CardHeader>
<CardContent>
{displayEntries.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
{detail ? 'No entries found. The server may need to be started first to generate config files.' : 'Loading...'}
</p>
) : (
<div className="space-y-3">
{displayEntries.map((entry) => (
<div key={entry.key} className="grid gap-1.5">
<Label className="font-mono text-xs text-muted-foreground">
{entry.key}
</Label>
<Input
value={entry.value}
onChange={(e) => updateEntry(entry.key, e.target.value)}
className="font-mono text-sm"
/>
</div>
))}
</div>
)}
{saveMutation.isSuccess && (
<p className="mt-4 text-sm text-green-500">Config saved successfully</p>
)}
{saveMutation.isError && (
<p className="mt-4 text-sm text-destructive">Failed to save config</p>
)}
</CardContent>
</Card>
);
}