revert style: clean up code with prettier and eslint
This commit is contained in:
Remco
2026-07-12 21:02:03 +02:00
parent 27de078f54
commit e85e4d74ea
378 changed files with 20710 additions and 22428 deletions
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,8 @@
"use client";
'use client'
import { Check, ChevronsUpDown, Star } from "lucide-react";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Check, ChevronsUpDown, Star } from 'lucide-react'
import { useMemo, useState } from 'react'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
@@ -10,11 +10,11 @@ import {
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
} from '@/components/ui/command'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Switch } from '@/components/ui/switch'
import { cn } from '@/lib/utils'
// ── Toggle field ──────────────────────────────────────────────
@@ -25,21 +25,21 @@ export function ToggleField({
onChange,
disabled,
}: {
label: string;
label: string
/** Boolean checked state */
checked?: boolean;
checked?: boolean
/** Number or string (0/1 or '0'/'1') — converted to boolean automatically */
value?: number | string;
onChange: (v: boolean) => void;
disabled?: boolean;
value?: number | string
onChange: (v: boolean) => void
disabled?: boolean
}) {
const isChecked = checked ?? (value === 1 || value === "1");
const isChecked = checked ?? (value === 1 || value === '1')
return (
<div className="flex items-center justify-between gap-2">
<Label className="text-xs">{label}</Label>
<Switch checked={isChecked} onCheckedChange={onChange} disabled={disabled} size="sm" />
</div>
);
)
}
// ── Interaction Type Combobox ──────────────────────────────────
@@ -50,19 +50,19 @@ export function InteractionTypeCombobox({
types,
disabled,
}: {
value: string;
onChange: (v: string) => void;
types: string[];
disabled?: boolean;
value: string
onChange: (v: string) => void
types: string[]
disabled?: boolean
}) {
const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState("");
const [open, setOpen] = useState(false)
const [inputValue, setInputValue] = useState('')
const filtered = useMemo(() => {
if (!inputValue.trim()) return types.slice(0, 50);
const q = inputValue.toLowerCase();
return types.filter((t) => t.toLowerCase().includes(q)).slice(0, 50);
}, [types, inputValue]);
if (!inputValue.trim()) return types.slice(0, 50)
const q = inputValue.toLowerCase()
return types.filter((t) => t.toLowerCase().includes(q)).slice(0, 50)
}, [types, inputValue])
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -74,7 +74,7 @@ export function InteractionTypeCombobox({
disabled={disabled}
className="w-full justify-between font-normal h-8 text-sm"
>
<span className="truncate">{value || "Select type..."}</span>
<span className="truncate">{value || 'Select type...'}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@@ -92,15 +92,15 @@ export function InteractionTypeCombobox({
type="button"
className="w-full px-2 py-1.5 text-left text-sm hover:bg-accent rounded"
onClick={() => {
onChange(inputValue.trim());
setOpen(false);
setInputValue("");
onChange(inputValue.trim())
setOpen(false)
setInputValue('')
}}
>
Use &quot;{inputValue.trim()}&quot;
</button>
) : (
"No types found."
'No types found.'
)}
</CommandEmpty>
<CommandGroup>
@@ -109,12 +109,14 @@ export function InteractionTypeCombobox({
key={t}
value={t}
onSelect={() => {
onChange(t);
setOpen(false);
setInputValue("");
onChange(t)
setOpen(false)
setInputValue('')
}}
>
<Check className={cn("mr-2 h-4 w-4", value === t ? "opacity-100" : "opacity-0")} />
<Check
className={cn('mr-2 h-4 w-4', value === t ? 'opacity-100' : 'opacity-0')}
/>
<span className="truncate">{t}</span>
</CommandItem>
))}
@@ -123,7 +125,7 @@ export function InteractionTypeCombobox({
</Command>
</PopoverContent>
</Popover>
);
)
}
// ── Move Page Combobox with smart suggestions ─────────────────
@@ -135,32 +137,35 @@ export function MovePageCombobox({
currentPageId,
suggestedPageIds,
}: {
value: number | null;
onChange: (id: number | null) => void;
pages: { id: number; caption: string }[];
currentPageId: number;
value: number | null
onChange: (id: number | null) => void
pages: { id: number; caption: string }[]
currentPageId: number
/** Page IDs to show first as suggestions (from move-suggestions.ts) */
suggestedPageIds?: number[];
suggestedPageIds?: number[]
}) {
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(false)
const options = useMemo(() => pages.filter((p) => p.id !== currentPageId), [pages, currentPageId]);
const options = useMemo(() => pages.filter((p) => p.id !== currentPageId), [pages, currentPageId])
const selectedPage = value ? pages.find((p) => p.id === value) : null;
const selectedPage = value ? pages.find((p) => p.id === value) : null
// Build suggested pages list
const suggested = useMemo(() => {
if (!suggestedPageIds?.length) return [];
const pageMap = new Map(options.map((p) => [p.id, p]));
if (!suggestedPageIds?.length) return []
const pageMap = new Map(options.map((p) => [p.id, p]))
return suggestedPageIds.map((id) => pageMap.get(id)).filter(Boolean) as {
id: number;
caption: string;
}[];
}, [suggestedPageIds, options]);
id: number
caption: string
}[]
}, [suggestedPageIds, options])
// Non-suggested pages (exclude those already in suggestions)
const suggestedSet = useMemo(() => new Set(suggested.map((s) => s.id)), [suggested]);
const remaining = useMemo(() => options.filter((p) => !suggestedSet.has(p.id)), [options, suggestedSet]);
const suggestedSet = useMemo(() => new Set(suggested.map((s) => s.id)), [suggested])
const remaining = useMemo(
() => options.filter((p) => !suggestedSet.has(p.id)),
[options, suggestedSet],
)
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -173,7 +178,9 @@ export function MovePageCombobox({
>
{selectedPage ? (
<span className="truncate">
<span className="text-muted-foreground font-mono text-xs mr-1.5">#{selectedPage.id}</span>
<span className="text-muted-foreground font-mono text-xs mr-1.5">
#{selectedPage.id}
</span>
{selectedPage.caption}
</span>
) : (
@@ -196,13 +203,20 @@ export function MovePageCombobox({
key={`s-${page.id}`}
value={`${page.id} ${page.caption}`}
onSelect={() => {
onChange(page.id);
setOpen(false);
onChange(page.id)
setOpen(false)
}}
>
<Check className={cn("mr-2 h-4 w-4", value === page.id ? "opacity-100" : "opacity-0")} />
<Check
className={cn(
'mr-2 h-4 w-4',
value === page.id ? 'opacity-100' : 'opacity-0',
)}
/>
<Star className="mr-1.5 h-3 w-3 text-[var(--admin-warning)]" />
<span className="text-muted-foreground font-mono text-xs mr-1.5">#{page.id}</span>
<span className="text-muted-foreground font-mono text-xs mr-1.5">
#{page.id}
</span>
<span className="truncate">{page.caption}</span>
</CommandItem>
))}
@@ -210,17 +224,19 @@ export function MovePageCombobox({
)}
{/* All pages */}
<CommandGroup heading={suggested.length > 0 ? "All pages" : undefined}>
<CommandGroup heading={suggested.length > 0 ? 'All pages' : undefined}>
{remaining.map((page) => (
<CommandItem
key={page.id}
value={`${page.id} ${page.caption}`}
onSelect={() => {
onChange(page.id);
setOpen(false);
onChange(page.id)
setOpen(false)
}}
>
<Check className={cn("mr-2 h-4 w-4", value === page.id ? "opacity-100" : "opacity-0")} />
<Check
className={cn('mr-2 h-4 w-4', value === page.id ? 'opacity-100' : 'opacity-0')}
/>
<span className="text-muted-foreground font-mono text-xs mr-1.5">#{page.id}</span>
<span className="truncate">{page.caption}</span>
</CommandItem>
@@ -230,5 +246,5 @@ export function MovePageCombobox({
</Command>
</PopoverContent>
</Popover>
);
)
}
@@ -1,7 +1,7 @@
"use client";
'use client'
import { Image as ImageIcon } from "lucide-react";
import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname";
import { Image as ImageIcon } from 'lucide-react'
import { getHabboCdnIconUrl, getLocalIconUrl } from '@/lib/furni/classname'
export function FurniIcon({ classname, revision }: { classname: string; revision: number }) {
if (!classname) {
@@ -9,34 +9,34 @@ export function FurniIcon({ classname, revision }: { classname: string; revision
<div className="w-10 h-10 rounded bg-muted flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-muted-foreground" />
</div>
);
)
}
const localUrl = getLocalIconUrl(classname);
const localUrl = getLocalIconUrl(classname)
function handleError(e: React.SyntheticEvent<HTMLImageElement>) {
const img = e.target as HTMLImageElement;
const stage = img.dataset.fallback;
const img = e.target as HTMLImageElement
const stage = img.dataset.fallback
if (!stage && revision > 0) {
img.dataset.fallback = "cdn";
img.src = getHabboCdnIconUrl(classname, revision);
img.dataset.fallback = 'cdn'
img.src = getHabboCdnIconUrl(classname, revision)
} else {
img.dataset.fallback = "none";
img.style.opacity = "0";
img.dataset.fallback = 'none'
img.style.opacity = '0'
}
}
return (
<div className="w-10 h-10 rounded bg-muted/50 flex items-center justify-center overflow-hidden">
{ }
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={localUrl}
alt={classname}
className="max-w-full max-h-full object-contain"
style={{ imageRendering: "pixelated" }}
style={{ imageRendering: 'pixelated' }}
onError={handleError}
loading="lazy"
/>
</div>
);
)
}
@@ -1,4 +1,4 @@
export { CatalogItemsTable } from "./catalog-items-table";
export { FurniIcon } from "./furni-icon";
export type { BaseItemData, CatalogItemData, InlineEdit } from "./types";
export { DEFAULT_NEW_ITEM, getPointsLabel, POINTS_TYPES } from "./types";
export { CatalogItemsTable } from './catalog-items-table'
export { FurniIcon } from './furni-icon'
export type { BaseItemData, CatalogItemData, InlineEdit } from './types'
export { DEFAULT_NEW_ITEM, getPointsLabel, POINTS_TYPES } from './types'
@@ -1,53 +1,59 @@
"use client";
'use client'
import { Loader2, Music } from "lucide-react";
import { useEffect, useState } from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Loader2, Music } from 'lucide-react'
import { useEffect, useState } from 'react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
getCachedSongPickerOptions,
loadSongPickerOptions,
type SoundtrackOption,
} from "@/lib/client-cache/song-picker-cache";
} from '@/lib/client-cache/song-picker-cache'
// Re-export so existing `import { invalidateSongPickerCache } from '.../song-picker'`
// callsites keep working. New code should import from the lib module directly.
export { invalidateSongPickerCache } from "@/lib/client-cache/song-picker-cache";
export { invalidateSongPickerCache } from '@/lib/client-cache/song-picker-cache'
interface Props {
value: number;
onChange: (id: number) => void;
disabled?: boolean;
value: number
onChange: (id: number) => void
disabled?: boolean
}
function formatLength(seconds: number): string {
if (!seconds || seconds <= 0) return "";
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${String(s).padStart(2, "0")}`;
if (!seconds || seconds <= 0) return ''
const m = Math.floor(seconds / 60)
const s = seconds % 60
return `${m}:${String(s).padStart(2, '0')}`
}
export function SongPicker({ value, onChange, disabled }: Props) {
const initial = getCachedSongPickerOptions();
const [options, setOptions] = useState<SoundtrackOption[]>(initial ?? []);
const [loading, setLoading] = useState(!initial);
const initial = getCachedSongPickerOptions()
const [options, setOptions] = useState<SoundtrackOption[]>(initial ?? [])
const [loading, setLoading] = useState(!initial)
useEffect(() => {
if (getCachedSongPickerOptions()) return;
let cancelled = false;
if (getCachedSongPickerOptions()) return
let cancelled = false
loadSongPickerOptions()
.then((items) => {
if (!cancelled) setOptions(items);
if (!cancelled) setOptions(items)
})
.catch(() => {
if (!cancelled) setOptions([]);
if (!cancelled) setOptions([])
})
.finally(() => {
if (!cancelled) setLoading(false);
});
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true;
};
}, []);
cancelled = true
}
}, [])
return (
<Select
@@ -75,12 +81,14 @@ export function SongPicker({ value, onChange, disabled }: Props) {
<span className="font-medium">{opt.name}</span>
{opt.author && <span className="text-xs text-muted-foreground">— {opt.author}</span>}
{opt.length > 0 && (
<span className="text-xs text-muted-foreground font-mono">{formatLength(opt.length)}</span>
<span className="text-xs text-muted-foreground font-mono">
{formatLength(opt.length)}
</span>
)}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
)
}
@@ -1,71 +1,71 @@
export interface BaseItemData {
id: number;
spriteId: number;
publicName: string;
itemName: string;
type: string;
width: number;
length: number;
stackHeight: number;
allowStack: number;
allowSit: number;
allowLay: number;
allowWalk: number;
allowGift: number;
allowTrade: number;
allowRecycle: number;
allowMarketplaceSell: number;
allowInventoryStack: number;
interactionType: string;
interactionModesCount: number;
vendingIds: string;
customparams: string;
effectIdMale: number;
effectIdFemale: number;
clothingOnWalk: string;
id: number
spriteId: number
publicName: string
itemName: string
type: string
width: number
length: number
stackHeight: number
allowStack: number
allowSit: number
allowLay: number
allowWalk: number
allowGift: number
allowTrade: number
allowRecycle: number
allowMarketplaceSell: number
allowInventoryStack: number
interactionType: string
interactionModesCount: number
vendingIds: string
customparams: string
effectIdMale: number
effectIdFemale: number
clothingOnWalk: string
}
export interface CatalogItemData {
id: number;
catalogName: string;
itemIds: string;
costCredits: number;
costPoints: number;
pointsType: number;
amount: number;
limitedSells: number;
limitedStack: number;
orderNumber: number;
offerId: number;
songId: number;
haveOffer: string;
clubOnly: string;
extradata: string;
baseName: string;
baseItemName: string;
spriteId: number;
baseItem: BaseItemData | null;
id: number
catalogName: string
itemIds: string
costCredits: number
costPoints: number
pointsType: number
amount: number
limitedSells: number
limitedStack: number
orderNumber: number
offerId: number
songId: number
haveOffer: string
clubOnly: string
extradata: string
baseName: string
baseItemName: string
spriteId: number
baseItem: BaseItemData | null
}
export interface InlineEdit {
costCredits: number;
costPoints: number;
pointsType: number;
amount: number;
costCredits: number
costPoints: number
pointsType: number
amount: number
}
export const POINTS_TYPES: Record<number, { label: string; color: string }> = {
0: { label: "Duckets", color: "text-[var(--admin-info)]" },
5: { label: "Diamonds", color: "text-[var(--admin-accent-text)]" },
101: { label: "Snowflakes", color: "text-[var(--admin-info)]" },
103: { label: "Hearts", color: "text-[var(--admin-error)]" },
104: { label: "Stars", color: "text-[var(--admin-warning)]" },
105: { label: "Shells", color: "text-[var(--admin-warning)]" },
};
0: { label: 'Duckets', color: 'text-[var(--admin-info)]' },
5: { label: 'Diamonds', color: 'text-[var(--admin-accent-text)]' },
101: { label: 'Snowflakes', color: 'text-[var(--admin-info)]' },
103: { label: 'Hearts', color: 'text-[var(--admin-error)]' },
104: { label: 'Stars', color: 'text-[var(--admin-warning)]' },
105: { label: 'Shells', color: 'text-[var(--admin-warning)]' },
}
export const DEFAULT_NEW_ITEM = {
catalogName: "",
itemIds: "0",
catalogName: '',
itemIds: '0',
costCredits: 0,
costPoints: 0,
pointsType: 0,
@@ -75,11 +75,11 @@ export const DEFAULT_NEW_ITEM = {
orderNumber: 1,
offerId: -1,
songId: 0,
haveOffer: "1",
clubOnly: "0",
extradata: "",
};
haveOffer: '1',
clubOnly: '0',
extradata: '',
}
export function getPointsLabel(type: number) {
return POINTS_TYPES[type] || { label: `Type ${type}`, color: "theme-text-muted" };
return POINTS_TYPES[type] || { label: `Type ${type}`, color: 'theme-text-muted' }
}