style: format code with prettier
Local Build and Deploy / deploy (push) Successful in 49s

This commit is contained in:
openhands committed 2026-07-12 21:07:34 +02:00
1 parent e85e4d74ea
commit 2e4ed76121
378 files changed
+22404 -20686

No files matched your search

@@ -1,4 +1,4 @@
'use client'
"use client";
import {
AlertTriangle,
@@ -11,46 +11,39 @@ import {
Save,
ShoppingCart,
Sparkles,
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useCallback, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { translateCatalogItems } from '@/actions/catalog-items'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { useServerAction } from '@/hooks/use-server-action'
import { FurniIcon } from './catalog-items-table'
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner";
import { translateCatalogItems } from "@/actions/catalog-items";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useServerAction } from "@/hooks/use-server-action";
import { FurniIcon } from "./catalog-items-table";
interface BaseItemForTranslation {
id: number
publicName: string
itemName: string
spriteId: number
type: string
id: number;
publicName: string;
itemName: string;
spriteId: number;
type: string;
}
interface CatalogTranslateTabProps {
baseItems: BaseItemForTranslation[]
catalogNameMap: Record<number, string>
canEdit: boolean
furniDataIdList: number[]
furniDescriptionMap: Record<number, string>
furniRevisionMap?: Record<number, { classname: string; revision: number }>
baseItems: BaseItemForTranslation[];
catalogNameMap: Record<number, string>;
canEdit: boolean;
furniDataIdList: number[];
furniDescriptionMap: Record<number, string>;
furniRevisionMap?: Record<number, { classname: string; revision: number }>;
}
type FilterMode = 'all' | 'missing' | 'modified'
type FilterMode = "all" | "missing" | "modified";
export function CatalogTranslateTab({
baseItems,
@@ -60,164 +53,164 @@ export function CatalogTranslateTab({
furniDescriptionMap,
furniRevisionMap = {},
}: CatalogTranslateTabProps) {
const router = useRouter()
const { run, isPending } = useServerAction()
const [isSuggesting, setIsSuggesting] = useState(false)
const [filterMode, setFilterMode] = useState<FilterMode>('all')
const router = useRouter();
const { run, isPending } = useServerAction();
const [isSuggesting, setIsSuggesting] = useState(false);
const [filterMode, setFilterMode] = useState<FilterMode>("all");
const furniDataIdSet = useMemo(() => new Set(furniDataIdList), [furniDataIdList])
const [selected, setSelected] = useState<Set<number>>(new Set())
const furniDataIdSet = useMemo(() => new Set(furniDataIdList), [furniDataIdList]);
const [selected, setSelected] = useState<Set<number>>(new Set());
// ── Editable state: names + descriptions ─────────────────────────
const [editedNames, setEditedNames] = useState<Record<number, string>>(
Object.fromEntries(baseItems.map((item) => [item.id, item.publicName])),
)
);
const [editedDescs, setEditedDescs] = useState<Record<number, string>>(
Object.fromEntries(baseItems.map((item) => [item.id, furniDescriptionMap[item.id] ?? ''])),
)
Object.fromEntries(baseItems.map((item) => [item.id, furniDescriptionMap[item.id] ?? ""])),
);
// Track which items have name or description changes
const isNameChanged = useCallback(
(item: BaseItemForTranslation) => editedNames[item.id] !== item.publicName,
[editedNames],
)
);
const isDescChanged = useCallback(
(item: BaseItemForTranslation) => editedDescs[item.id] !== (furniDescriptionMap[item.id] ?? ''),
(item: BaseItemForTranslation) => editedDescs[item.id] !== (furniDescriptionMap[item.id] ?? ""),
[editedDescs, furniDescriptionMap],
)
);
const changedItems = useMemo(
() => baseItems.filter((item) => isNameChanged(item) || isDescChanged(item)),
[baseItems, isNameChanged, isDescChanged],
)
);
const missingItems = useMemo(
() => baseItems.filter((item) => !furniDataIdSet.has(item.id)),
[baseItems, furniDataIdSet],
)
);
const filteredItems = useMemo(() => {
switch (filterMode) {
case 'missing':
return baseItems.filter((item) => !furniDataIdSet.has(item.id))
case 'modified':
return baseItems.filter((item) => isNameChanged(item) || isDescChanged(item))
case "missing":
return baseItems.filter((item) => !furniDataIdSet.has(item.id));
case "modified":
return baseItems.filter((item) => isNameChanged(item) || isDescChanged(item));
default:
return baseItems
return baseItems;
}
}, [baseItems, filterMode, furniDataIdSet, isNameChanged, isDescChanged])
}, [baseItems, filterMode, furniDataIdSet, isNameChanged, isDescChanged]);
const toggleOne = useCallback((id: number) => {
setSelected((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}, [])
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
}, []);
const toggleAll = useCallback(() => {
setSelected((prev) =>
prev.size === filteredItems.length ? new Set() : new Set(filteredItems.map((i) => i.id)),
)
}, [filteredItems])
);
}, [filteredItems]);
// Items to act on: selected ones if any, otherwise all
const targetItems = useMemo(() => {
if (selected.size === 0) return baseItems
return baseItems.filter((i) => selected.has(i.id))
}, [baseItems, selected])
if (selected.size === 0) return baseItems;
return baseItems.filter((i) => selected.has(i.id));
}, [baseItems, selected]);
function handleNameChange(id: number, value: string) {
setEditedNames((prev) => ({ ...prev, [id]: value }))
setEditedNames((prev) => ({ ...prev, [id]: value }));
}
function handleDescChange(id: number, value: string) {
setEditedDescs((prev) => ({ ...prev, [id]: value }))
setEditedDescs((prev) => ({ ...prev, [id]: value }));
}
function handleReset() {
setEditedNames(Object.fromEntries(baseItems.map((item) => [item.id, item.publicName])))
setEditedNames(Object.fromEntries(baseItems.map((item) => [item.id, item.publicName])));
setEditedDescs(
Object.fromEntries(baseItems.map((item) => [item.id, furniDescriptionMap[item.id] ?? ''])),
)
Object.fromEntries(baseItems.map((item) => [item.id, furniDescriptionMap[item.id] ?? ""])),
);
}
async function handleSuggestFromHabboFurni() {
if (targetItems.length === 0) return
setIsSuggesting(true)
if (targetItems.length === 0) return;
setIsSuggesting(true);
try {
// Collect classnames from selected items (or all if none selected)
const classnames = targetItems.map((item) => item.itemName).filter(Boolean)
const classnames = targetItems.map((item) => item.itemName).filter(Boolean);
if (classnames.length === 0) {
toast.info('No classnames found to look up.')
setIsSuggesting(false)
return
toast.info("No classnames found to look up.");
setIsSuggesting(false);
return;
}
// Batch in chunks of 50 to avoid too-long URLs
const CHUNK_SIZE = 50
const allSuggestions: Record<string, { name: string; description: string }> = {}
const CHUNK_SIZE = 50;
const allSuggestions: Record<string, { name: string; description: string }> = {};
for (let i = 0; i < classnames.length; i += CHUNK_SIZE) {
const chunk = classnames.slice(i, i + CHUNK_SIZE)
const chunk = classnames.slice(i, i + CHUNK_SIZE);
const res = await fetch(
`/api/admin/catalog/suggest?classnames=${encodeURIComponent(chunk.join(','))}`,
)
`/api/admin/catalog/suggest?classnames=${encodeURIComponent(chunk.join(","))}`,
);
if (res.ok) {
const data = await res.json()
Object.assign(allSuggestions, data.suggestions || {})
const data = await res.json();
Object.assign(allSuggestions, data.suggestions || {});
}
}
// Apply suggestions to items that have a match (names + descriptions)
let nameCount = 0
let descCount = 0
const newNames = { ...editedNames }
const newDescs = { ...editedDescs }
let nameCount = 0;
let descCount = 0;
const newNames = { ...editedNames };
const newDescs = { ...editedDescs };
for (const item of targetItems) {
const suggestion = allSuggestions[item.itemName]
if (!suggestion) continue
const suggestion = allSuggestions[item.itemName];
if (!suggestion) continue;
if (suggestion.name && suggestion.name !== item.publicName) {
newNames[item.id] = suggestion.name
nameCount++
newNames[item.id] = suggestion.name;
nameCount++;
}
const currentDesc = furniDescriptionMap[item.id] ?? ''
const currentDesc = furniDescriptionMap[item.id] ?? "";
if (suggestion.description && suggestion.description !== currentDesc) {
newDescs[item.id] = suggestion.description
descCount++
newDescs[item.id] = suggestion.description;
descCount++;
}
}
setEditedNames(newNames)
setEditedDescs(newDescs)
setEditedNames(newNames);
setEditedDescs(newDescs);
const total = nameCount + descCount
const total = nameCount + descCount;
if (total > 0) {
const parts = []
if (nameCount > 0) parts.push(`${nameCount} name(s)`)
if (descCount > 0) parts.push(`${descCount} description(s)`)
toast.success(`HabboFurni: ${parts.join(' + ')} suggested from habbo.it. Review and save.`)
const parts = [];
if (nameCount > 0) parts.push(`${nameCount} name(s)`);
if (descCount > 0) parts.push(`${descCount} description(s)`);
toast.success(`HabboFurni: ${parts.join(" + ")} suggested from habbo.it. Review and save.`);
} else {
toast.info(
`HabboFurni: No new suggestions found (${Object.keys(allSuggestions).length} looked up, all already match).`,
)
);
}
} catch (err) {
toast.error(`HabboFurni error: ${(err as Error).message}`)
toast.error(`HabboFurni error: ${(err as Error).message}`);
} finally {
setIsSuggesting(false)
setIsSuggesting(false);
}
}
async function handleSaveAll() {
if (changedItems.length === 0) return
if (changedItems.length === 0) return;
run(
() =>
@@ -229,26 +222,26 @@ export function CatalogTranslateTab({
})),
}),
{
successMessage: 'Translations saved.',
errorMessage: 'Failed to save translations.',
successMessage: "Translations saved.",
errorMessage: "Failed to save translations.",
onSuccess: (data) => {
const d = data as Record<string, number | boolean>
const parts = []
const d = data as Record<string, number | boolean>;
const parts = [];
if (d.namesUpdated && (d.namesUpdated as number) > 0)
parts.push(`${d.namesUpdated} name(s) updated`)
parts.push(`${d.namesUpdated} name(s) updated`);
if (d.descriptionsUpdated && (d.descriptionsUpdated as number) > 0)
parts.push(`${d.descriptionsUpdated} description(s) updated`)
if (parts.length === 0) parts.push(`${d.updated ?? 0} item(s) processed`)
if (d.furniDataUpdated) parts.push('FurnitureData.json synced')
parts.push(`${d.descriptionsUpdated} description(s) updated`);
if (parts.length === 0) parts.push(`${d.updated ?? 0} item(s) processed`);
if (d.furniDataUpdated) parts.push("FurnitureData.json synced");
if (d.furniDataInserted && (d.furniDataInserted as number) > 0) {
parts.push(`${d.furniDataInserted} new item(s) added to FurnitureData.json`)
parts.push(`${d.furniDataInserted} new item(s) added to FurnitureData.json`);
}
parts.push('emulator cache refreshed')
toast.success(`${parts.join(' · ')}.`)
router.refresh()
parts.push("emulator cache refreshed");
toast.success(`${parts.join(" · ")}.`);
router.refresh();
},
},
)
);
}
return (
@@ -274,8 +267,7 @@ export function CatalogTranslateTab({
<div className="flex items-center gap-1.5 mt-2 text-xs text-[var(--admin-warning)] dark:text-[var(--admin-warning)]">
<AlertTriangle className="h-3.5 w-3.5" />
<span>
{missingItems.length} item(s) missing from FurnitureData.json — will be
auto-inserted on save
{missingItems.length} item(s) missing from FurnitureData.json — will be auto-inserted on save
</span>
</div>
)}
@@ -294,7 +286,7 @@ export function CatalogTranslateTab({
) : (
<Sparkles className="mr-2 h-4 w-4" />
)}
Suggest IT{selected.size > 0 ? ` (${selected.size})` : ''}
Suggest IT{selected.size > 0 ? ` (${selected.size})` : ""}
</Button>
<Button
variant="outline"
@@ -305,11 +297,7 @@ export function CatalogTranslateTab({
<RotateCcw className="mr-2 h-4 w-4" />
Reset
</Button>
<Button
size="sm"
onClick={handleSaveAll}
disabled={isPending || changedItems.length === 0}
>
<Button size="sm" onClick={handleSaveAll} disabled={isPending || changedItems.length === 0}>
{isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
@@ -327,28 +315,28 @@ export function CatalogTranslateTab({
<Filter className="h-4 w-4 text-muted-foreground" />
<div className="flex gap-1">
<Button
variant={filterMode === 'all' ? 'default' : 'outline'}
variant={filterMode === "all" ? "default" : "outline"}
size="sm"
className="h-7 text-xs"
onClick={() => setFilterMode('all')}
onClick={() => setFilterMode("all")}
>
All ({baseItems.length})
</Button>
<Button
variant={filterMode === 'missing' ? 'default' : 'outline'}
variant={filterMode === "missing" ? "default" : "outline"}
size="sm"
className="h-7 text-xs"
onClick={() => setFilterMode('missing')}
onClick={() => setFilterMode("missing")}
disabled={missingItems.length === 0}
>
<AlertTriangle className="mr-1 h-3 w-3" />
Missing ({missingItems.length})
</Button>
<Button
variant={filterMode === 'modified' ? 'default' : 'outline'}
variant={filterMode === "modified" ? "default" : "outline"}
size="sm"
className="h-7 text-xs"
onClick={() => setFilterMode('modified')}
onClick={() => setFilterMode("modified")}
disabled={changedItems.length === 0}
>
Modified ({changedItems.length})
@@ -362,9 +350,7 @@ export function CatalogTranslateTab({
No items to translate in this page.
</p>
) : filteredItems.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
No items match the current filter.
</p>
<p className="text-sm text-muted-foreground text-center py-8">No items match the current filter.</p>
) : (
<TooltipProvider>
<div className="rounded-md border overflow-auto max-h-[600px]">
@@ -389,19 +375,19 @@ export function CatalogTranslateTab({
</TableHeader>
<TableBody>
{filteredItems.map((item) => {
const nameChanged = isNameChanged(item)
const descChanged = isDescChanged(item)
const isChanged = nameChanged || descChanged
const isMissing = !furniDataIdSet.has(item.id)
const currentCatalogName = catalogNameMap[item.id]
const nameChanged = isNameChanged(item);
const descChanged = isDescChanged(item);
const isChanged = nameChanged || descChanged;
const isMissing = !furniDataIdSet.has(item.id);
const currentCatalogName = catalogNameMap[item.id];
return (
<TableRow
key={item.id}
className={
isChanged
? 'bg-[var(--admin-warning-subtle)] dark:bg-[var(--admin-warning-subtle)]'
? "bg-[var(--admin-warning-subtle)] dark:bg-[var(--admin-warning-subtle)]"
: isMissing
? 'bg-[var(--admin-warning-subtle)] dark:bg-[var(--admin-warning-subtle)]'
? "bg-[var(--admin-warning-subtle)] dark:bg-[var(--admin-warning-subtle)]"
: undefined
}
>
@@ -414,24 +400,18 @@ export function CatalogTranslateTab({
</TableCell>
<TableCell className="px-1">
{(() => {
const revInfo = furniRevisionMap[item.id]
const revInfo = furniRevisionMap[item.id];
return revInfo ? (
<FurniIcon
classname={revInfo.classname}
revision={revInfo.revision}
/>
<FurniIcon classname={revInfo.classname} revision={revInfo.revision} />
) : (
<FurniIcon classname={item.itemName} revision={0} />
)
);
})()}
</TableCell>
<TableCell className="font-mono text-xs">{item.id}</TableCell>
<TableCell>
<Badge
variant={item.type === 's' ? 'default' : 'secondary'}
className="text-xs"
>
{item.type === 's' ? 'Floor' : item.type === 'i' ? 'Wall' : item.type}
<Badge variant={item.type === "s" ? "default" : "secondary"} className="text-xs">
{item.type === "s" ? "Floor" : item.type === "i" ? "Wall" : item.type}
</Badge>
</TableCell>
<TableCell className="text-xs text-muted-foreground font-mono">
@@ -439,10 +419,10 @@ export function CatalogTranslateTab({
</TableCell>
<TableCell>
<Input
value={editedNames[item.id] || ''}
value={editedNames[item.id] || ""}
onChange={(e) => handleNameChange(item.id, e.target.value)}
disabled={!canEdit}
className={`h-8 text-sm ${nameChanged ? 'border-[var(--admin-warning-border)] dark:border-[var(--admin-warning-border)]' : ''}`}
className={`h-8 text-sm ${nameChanged ? "border-[var(--admin-warning-border)] dark:border-[var(--admin-warning-border)]" : ""}`}
/>
{currentCatalogName && currentCatalogName !== item.publicName && (
<p className="text-[11px] text-muted-foreground mt-0.5 truncate">
@@ -452,11 +432,11 @@ export function CatalogTranslateTab({
</TableCell>
<TableCell>
<Input
value={editedDescs[item.id] || ''}
value={editedDescs[item.id] || ""}
onChange={(e) => handleDescChange(item.id, e.target.value)}
disabled={!canEdit}
placeholder="No description"
className={`h-8 text-sm ${descChanged ? 'border-[var(--admin-warning-border)] dark:border-[var(--admin-warning-border)]' : ''}`}
className={`h-8 text-sm ${descChanged ? "border-[var(--admin-warning-border)] dark:border-[var(--admin-warning-border)]" : ""}`}
/>
</TableCell>
<TableCell>
@@ -464,7 +444,7 @@ export function CatalogTranslateTab({
{isChanged ? (
<Badge
variant="secondary"
className="text-xs bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]"
className="text-xs bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]"
>
Modified
</Badge>
@@ -494,7 +474,7 @@ export function CatalogTranslateTab({
</div>
</TableCell>
</TableRow>
)
);
})}
</TableBody>
</Table>
@@ -503,5 +483,5 @@ export function CatalogTranslateTab({
)}
</CardContent>
</Card>
)
);
}