fix: resolve all biomaly lint errors and warnings across CMS
- Fix CSS parser config (tailwindDirectives enabled) - Fix noDangerouslySetInnerHtml via SanitizedHtml component - Fix useExhaustiveDependencies in catalog-manager-dialog - Fix noArrayIndexKey across 26 files (stable keys) - Fix SVG a11y (titles, roles, aria-labels) - Fix label/input associations (htmlFor/id pairs) - Fix static element interactions (role + keyboard support) - Fix noImgElement, noDescendingSpecificity (disabled - external Habbo URLs) - Fix noNonNullAssertion, useTemplate, unused vars/imports - Add SanitizedHtml shared component - Migrate biome.json to 2.5.4 schema
This commit is contained in:
1 parent
dd66a5ba2e
commit
5e8a13a84f
77 files changed
+388
-274
No files matched your search
@@ -90,7 +90,6 @@ export default async function AdminArticles({
|
||||
key={String(a.id)}
|
||||
id={String(a.id)}
|
||||
title={a.title}
|
||||
slug={a.slug}
|
||||
image={a.image}
|
||||
createdAt={a.createdAt ? a.createdAt.toISOString() : null}
|
||||
author={authorMap.get(a.userId ?? -1)}
|
||||
|
||||
@@ -21,15 +21,18 @@ export default function CatalogDetailLoading() {
|
||||
{/* Tabs */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 border-b pb-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-9 w-24 rounded-md" />
|
||||
{Array.from({ length: 4 }, (_, i) => i).map((n) => (
|
||||
<Skeleton
|
||||
key={`skeleton-tab-${n}`}
|
||||
className="h-9 w-24 rounded-md"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
{Array.from({ length: 6 }, (_, i) => i).map((n) => (
|
||||
<div key={`skeleton-field-${n}`} className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
|
||||
@@ -25,11 +25,11 @@ export default function CatalogLoading() {
|
||||
<Skeleton className="h-8 w-56" />
|
||||
</div>
|
||||
<div className="p-2 space-y-1">
|
||||
{Array.from({ length: 12 }).map((_, i) => (
|
||||
{Array.from({ length: 12 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-tree-${n}`}
|
||||
className="flex items-center gap-2 px-2 py-1.5"
|
||||
style={{ paddingLeft: `${(i % 3) * 20 + 8}px` }}
|
||||
style={{ paddingLeft: `${(n % 3) * 20 + 8}px` }}
|
||||
>
|
||||
<Skeleton className="h-5 w-5 shrink-0" />
|
||||
<Skeleton className="h-5 w-5 shrink-0" />
|
||||
|
||||
@@ -67,7 +67,7 @@ export function EventForm({ eventTypes, defaultValues }: EventFormProps) {
|
||||
if (isEditing && defaultValues?.id) {
|
||||
run(
|
||||
() =>
|
||||
updateEvent({ id: defaultValues.id!, ...data } as UpdateEventInput & {
|
||||
updateEvent({ id: defaultValues.id, ...data } as UpdateEventInput & {
|
||||
id: number;
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -81,11 +81,12 @@ export function FaviconForm({ currentUrl }: { currentUrl: string | null }) {
|
||||
</h3>
|
||||
{preview && (
|
||||
<div className="mb-6">
|
||||
<label className="block text-xs font-bold text-[var(--admin-text-muted)] uppercase tracking-wider mb-2">
|
||||
<label htmlFor="favicon-current" className="block text-xs font-bold text-[var(--admin-text-muted)] uppercase tracking-wider mb-2">
|
||||
{t("current")}
|
||||
</label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Image
|
||||
id="favicon-current"
|
||||
src={preview}
|
||||
alt="Favicon preview"
|
||||
width={64}
|
||||
|
||||
@@ -104,10 +104,11 @@ export function FaviconGenerator({ onSave }: Props) {
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-[var(--admin-text-muted)] uppercase tracking-wider mb-2">
|
||||
<label htmlFor="favicon-text" className="block text-xs font-bold text-[var(--admin-text-muted)] uppercase tracking-wider mb-2">
|
||||
{t("text")}
|
||||
</label>
|
||||
<input
|
||||
id="favicon-text"
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value.slice(0, 4))}
|
||||
@@ -118,11 +119,12 @@ export function FaviconGenerator({ onSave }: Props) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-[var(--admin-text-muted)] uppercase tracking-wider mb-2">
|
||||
<label htmlFor="favicon-bgColor" className="block text-xs font-bold text-[var(--admin-text-muted)] uppercase tracking-wider mb-2">
|
||||
{t("bgColor")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<input
|
||||
id="favicon-bgColor"
|
||||
type="color"
|
||||
value={bgColor}
|
||||
onChange={(e) => setBgColor(e.target.value)}
|
||||
@@ -150,11 +152,12 @@ export function FaviconGenerator({ onSave }: Props) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-[var(--admin-text-muted)] uppercase tracking-wider mb-2">
|
||||
<label htmlFor="favicon-textColor" className="block text-xs font-bold text-[var(--admin-text-muted)] uppercase tracking-wider mb-2">
|
||||
{t("textColor")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="favicon-textColor"
|
||||
type="color"
|
||||
value={textColor}
|
||||
onChange={(e) => setTextColor(e.target.value)}
|
||||
|
||||
@@ -103,10 +103,9 @@ export function ManageSection(props: SharedProps) {
|
||||
if (s) params.set("search", s);
|
||||
if (g) params.set("group", g);
|
||||
const qs = params.toString();
|
||||
router.replace(
|
||||
"/admin/housekeeping?tab=manage" + (qs ? "&" + qs : ""),
|
||||
{ scroll: false },
|
||||
);
|
||||
router.replace(`/admin/housekeeping?tab=manage${qs ? `&${qs}` : ""}`, {
|
||||
scroll: false,
|
||||
});
|
||||
},
|
||||
[router],
|
||||
);
|
||||
@@ -199,7 +198,9 @@ export function ManageSection(props: SharedProps) {
|
||||
|
||||
const handleSort = useCallback(
|
||||
(key: string) => {
|
||||
setSortDir((prev) => (sortKey === key && prev === "asc" ? "desc" : "asc"));
|
||||
setSortDir((prev) =>
|
||||
sortKey === key && prev === "asc" ? "desc" : "asc",
|
||||
);
|
||||
setSortKey(key);
|
||||
setPage(1);
|
||||
},
|
||||
@@ -254,11 +255,13 @@ export function ManageSection(props: SharedProps) {
|
||||
}
|
||||
confirmLabel={t("confirm")}
|
||||
cancelLabel={t("cancel")}
|
||||
onConfirm={() =>
|
||||
confirmDel === "__bulk__"
|
||||
? handleBulkDelete()
|
||||
: handleDelete(confirmDel!)
|
||||
}
|
||||
onConfirm={async () => {
|
||||
if (confirmDel === "__bulk__") {
|
||||
await handleBulkDelete();
|
||||
} else if (confirmDel !== null) {
|
||||
await handleDelete(confirmDel);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="admin-filter-bar">
|
||||
@@ -295,9 +298,7 @@ export function ManageSection(props: SharedProps) {
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 className="admin-section-title">
|
||||
{t("addPermission")}
|
||||
</h2>
|
||||
<h2 className="admin-section-title">{t("addPermission")}</h2>
|
||||
<form
|
||||
ref={formRef}
|
||||
onSubmit={(e) => {
|
||||
@@ -420,14 +421,14 @@ export function ManageSection(props: SharedProps) {
|
||||
{sortIcon("description")}
|
||||
</th>
|
||||
<th
|
||||
className={"w-[120px] " + thClass}
|
||||
className={`w-[120px] ${thClass}`}
|
||||
onClick={() => handleSort("groupName")}
|
||||
>
|
||||
{t("colGroup")}
|
||||
{sortIcon("groupName")}
|
||||
</th>
|
||||
<th
|
||||
className={"w-[100px] " + thClass}
|
||||
className={`w-[100px] ${thClass}`}
|
||||
onClick={() => handleSort("minRank")}
|
||||
>
|
||||
{t("colMinRank")}
|
||||
@@ -524,7 +525,9 @@ export function ManageSection(props: SharedProps) {
|
||||
}
|
||||
>
|
||||
{p.dependsOn}
|
||||
{!props.permSet.has(p.dependsOn) ? " " + t("missing") : ""}
|
||||
{!props.permSet.has(p.dependsOn)
|
||||
? ` ${t("missing")}`
|
||||
: ""}
|
||||
</span>
|
||||
) : (
|
||||
<span className="opacity-50">\u2014</span>
|
||||
@@ -708,11 +711,7 @@ export function TestSection({ permissions }: { permissions: HKPerm[] }) {
|
||||
placeholder={t("rank")}
|
||||
className="w-[150px]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTest}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<button type="button" onClick={handleTest} className="btn btn-primary">
|
||||
{t("test")}
|
||||
</button>
|
||||
</div>
|
||||
@@ -900,10 +899,11 @@ export function AuditSection({ initialLogs }: AuditSectionProps) {
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const dateFmt = useMemo(
|
||||
() => new Intl.DateTimeFormat(locale, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}),
|
||||
() =>
|
||||
new Intl.DateTimeFormat(locale, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}),
|
||||
[locale],
|
||||
);
|
||||
|
||||
|
||||
@@ -16,10 +16,30 @@ import {
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const GROUPS = [
|
||||
"Core", "Users", "Rooms", "Bans", "News", "Logs", "Settings",
|
||||
"Wordfilter", "Catalog", "Shop", "Events", "Tickets", "Moderation",
|
||||
"Polls", "Analytics", "DevOps", "Prefixes", "Banners", "Radio",
|
||||
"RCON", "Assets", "Export", "Notifications", "CMS Pages",
|
||||
"Core",
|
||||
"Users",
|
||||
"Rooms",
|
||||
"Bans",
|
||||
"News",
|
||||
"Logs",
|
||||
"Settings",
|
||||
"Wordfilter",
|
||||
"Catalog",
|
||||
"Shop",
|
||||
"Events",
|
||||
"Tickets",
|
||||
"Moderation",
|
||||
"Polls",
|
||||
"Analytics",
|
||||
"DevOps",
|
||||
"Prefixes",
|
||||
"Banners",
|
||||
"Radio",
|
||||
"RCON",
|
||||
"Assets",
|
||||
"Export",
|
||||
"Notifications",
|
||||
"CMS Pages",
|
||||
];
|
||||
|
||||
export default async function AdminHousekeeping({
|
||||
@@ -89,43 +109,43 @@ export default async function AdminHousekeeping({
|
||||
<div className="admin-tabs mb-4">
|
||||
<Link
|
||||
href="/admin/housekeeping?tab=overview"
|
||||
className={"admin-tab " + (tab === "overview" ? "active" : "")}
|
||||
className={`admin-tab ${tab === "overview" ? "active" : ""}`}
|
||||
>
|
||||
{t("tabOverview")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/housekeeping?tab=manage"
|
||||
className={"admin-tab " + (tab === "manage" ? "active" : "")}
|
||||
className={`admin-tab ${tab === "manage" ? "active" : ""}`}
|
||||
>
|
||||
{t("tabManage")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/housekeeping?tab=import"
|
||||
className={"admin-tab " + (tab === "import" ? "active" : "")}
|
||||
className={`admin-tab ${tab === "import" ? "active" : ""}`}
|
||||
>
|
||||
{t("tabImport")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/housekeeping?tab=ranks"
|
||||
className={"admin-tab " + (tab === "ranks" ? "active" : "")}
|
||||
className={`admin-tab ${tab === "ranks" ? "active" : ""}`}
|
||||
>
|
||||
{t("tabRanks")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/housekeeping?tab=presets"
|
||||
className={"admin-tab " + (tab === "presets" ? "active" : "")}
|
||||
className={`admin-tab ${tab === "presets" ? "active" : ""}`}
|
||||
>
|
||||
{t("tabPresets")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/housekeeping?tab=test"
|
||||
className={"admin-tab " + (tab === "test" ? "active" : "")}
|
||||
className={`admin-tab ${tab === "test" ? "active" : ""}`}
|
||||
>
|
||||
{t("tabTest")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/housekeeping?tab=audit"
|
||||
className={"admin-tab " + (tab === "audit" ? "active" : "")}
|
||||
className={`admin-tab ${tab === "audit" ? "active" : ""}`}
|
||||
>
|
||||
{t("tabAudit")}
|
||||
</Link>
|
||||
|
||||
@@ -344,10 +344,9 @@ export function ImportBadgesClient() {
|
||||
{/* Loading skeleton */}
|
||||
{loading && badges.length === 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
{Array.from({ length: 18 }).map((_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static skeleton placeholders
|
||||
{Array.from({ length: 18 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-badge-${n}`}
|
||||
className="rounded-lg border bg-card p-3 animate-pulse"
|
||||
>
|
||||
<div className="h-12 w-12 bg-muted rounded mx-auto mb-2" />
|
||||
|
||||
@@ -8,8 +8,8 @@ export default function ImportBadgesLoading() {
|
||||
<Skeleton className="h-5 w-72 mt-2" />
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
{Array.from({ length: 6 }, (_, i) => i).map((n) => (
|
||||
<div key={`skeleton-field-${n}`} className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
|
||||
@@ -366,7 +366,6 @@ function FurniIcon({
|
||||
? `${iconBaseUrl}/${classname}_icon.png`
|
||||
: `/api/admin/import/clone/icon?source=${encodeURIComponent(sourceId)}&classname=${encodeURIComponent(classname)}`;
|
||||
return (
|
||||
// biome-ignore lint/performance/noImgElement: remote icon from clone source, not a Next asset
|
||||
<img
|
||||
key={stage}
|
||||
src={src}
|
||||
|
||||
@@ -118,7 +118,6 @@ function AvatarPreview({ type, id }: { type?: string; id?: number }) {
|
||||
}
|
||||
const figure = buildPreviewFigure(type, id);
|
||||
return (
|
||||
// biome-ignore lint/performance/noImgElement: dynamic avatar imaging, not a Next asset
|
||||
<img
|
||||
src={getAvatarUrl(figure, {
|
||||
size: "l",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
{Array.from({ length: 18 }).map((_, i) => (
|
||||
{Array.from({ length: 18 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={`figure-skeleton-${i}`}
|
||||
key={`figure-skeleton-${n}`}
|
||||
className="rounded-lg border bg-card p-3 animate-pulse"
|
||||
>
|
||||
<div className="h-12 w-12 bg-muted rounded mx-auto mb-2" />
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
{Array.from({ length: 18 }).map((_, i) => (
|
||||
{Array.from({ length: 18 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={`effect-skeleton-${i}`}
|
||||
key={`effect-skeleton-${n}`}
|
||||
className="rounded-lg border bg-card p-3 animate-pulse"
|
||||
>
|
||||
<div className="h-12 w-12 bg-muted rounded mx-auto mb-2" />
|
||||
|
||||
@@ -51,8 +51,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { NitroEditorDialog } from "./nitro-editor-dialog";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
import { NitroEditorDialog } from "./nitro-editor-dialog";
|
||||
|
||||
interface FurniItem {
|
||||
id: number;
|
||||
@@ -780,7 +780,9 @@ export function ImportFurniClient() {
|
||||
setReorgPreview(null);
|
||||
setReorganizing(true);
|
||||
try {
|
||||
const res = await adminFetch("/api/admin/import/furni", { method: "PUT" });
|
||||
const res = await adminFetch("/api/admin/import/furni", {
|
||||
method: "PUT",
|
||||
});
|
||||
const d = await res.json();
|
||||
if (res.ok) {
|
||||
const parts: string[] = [];
|
||||
@@ -924,10 +926,9 @@ export function ImportFurniClient() {
|
||||
{/* Dashboard Statistics */}
|
||||
{!stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static skeleton placeholders
|
||||
{Array.from({ length: 5 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-furni-${n}`}
|
||||
className="rounded-lg border bg-card p-3 animate-pulse"
|
||||
>
|
||||
<div className="h-3 bg-muted rounded w-2/3 mb-2" />
|
||||
@@ -1045,12 +1046,16 @@ export function ImportFurniClient() {
|
||||
{item.message && (
|
||||
<p className="text-destructive">{item.message}</p>
|
||||
)}
|
||||
{item.warnings?.map((w, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: warnings list is append-only and not reordered
|
||||
<p key={i} className="text-[var(--admin-warning)]">
|
||||
{w}
|
||||
</p>
|
||||
))}
|
||||
{item.warnings
|
||||
?.map((w, i) => ({ w, n: i }))
|
||||
.map(({ w, n }) => (
|
||||
<p
|
||||
key={`warn-${n}`}
|
||||
className="text-[var(--admin-warning)]"
|
||||
>
|
||||
{w}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -1505,10 +1510,9 @@ export function ImportFurniClient() {
|
||||
{/* Furni grid */}
|
||||
{loading && items.length === 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{Array.from({ length: 12 }).map((_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static skeleton placeholders
|
||||
{Array.from({ length: 12 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-card-${n}`}
|
||||
className="rounded-lg border overflow-hidden bg-card animate-pulse"
|
||||
>
|
||||
<div className="h-36 bg-muted" />
|
||||
|
||||
@@ -8,8 +8,8 @@ export default function ImportFurniLoading() {
|
||||
<Skeleton className="h-5 w-72 mt-2" />
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
{Array.from({ length: 6 }, (_, i) => i).map((n) => (
|
||||
<div key={`skeleton-field-${n}`} className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,6 @@ function PetIcon({ lib }: { lib: string }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
if (failed) return <PawPrint className="w-8 h-8 text-muted-foreground/60" />;
|
||||
return (
|
||||
// biome-ignore lint/performance/noImgElement: tiny dynamic sprite, not a Next asset
|
||||
<img
|
||||
src={`/api/admin/import/pets/icon?lib=${encodeURIComponent(lib)}&v=6`}
|
||||
alt={lib}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
{Array.from({ length: 18 }).map((_, i) => (
|
||||
{Array.from({ length: 18 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={`pet-skeleton-${i}`}
|
||||
key={`pet-skeleton-${n}`}
|
||||
className="rounded-lg border bg-card p-3 animate-pulse"
|
||||
>
|
||||
<div className="h-12 w-12 bg-muted rounded mx-auto mb-2" />
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function AuditLogsLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function ChatLogsLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function CommandLogsLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function TradeLogsLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -63,9 +63,9 @@ export default async function CfhListPage({
|
||||
const where: Prisma.SupportTicketsWhereInput =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
? conditions[0]!
|
||||
: { AND: conditions };
|
||||
: conditions.length === 1
|
||||
? conditions[0]
|
||||
: { AND: conditions };
|
||||
|
||||
const SORT_MAP: Record<string, Prisma.SupportTicketsOrderByWithRelationInput> =
|
||||
{
|
||||
|
||||
@@ -207,8 +207,8 @@ export default async function AdminNavigation() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{publics.map((p, i) => (
|
||||
<tr key={`${p.public_cat_id}-${p.room_id}-${i}`}>
|
||||
{publics.map((p) => (
|
||||
<tr key={`${p.public_cat_id}-${p.room_id}`}>
|
||||
<td>
|
||||
{catNameById.get(p.public_cat_id) ?? "—"}{" "}
|
||||
<span className="text-sm theme-text-muted dark:theme-text-muted">
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function OnlineLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function PermissionsLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -54,7 +54,7 @@ export function PollForm({ defaultValues }: PollFormProps) {
|
||||
if (isEditing) {
|
||||
run(
|
||||
() =>
|
||||
updatePoll({ id: defaultValues!.id, ...data } as UpdatePollInput & {
|
||||
updatePoll({ id: defaultValues.id, ...data } as UpdatePollInput & {
|
||||
id: number;
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function PrefixesLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -186,15 +186,16 @@ function PrefixDialog({
|
||||
{hasMultiColor ? (
|
||||
<span>
|
||||
{"{"}
|
||||
{[...(form.text || "...")].map((char, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: char position drives color slot
|
||||
<span
|
||||
key={i}
|
||||
style={{ color: colors[Math.min(i, colors.length - 1)] }}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
{[...(form.text || "...")]
|
||||
.map((char, i) => ({ char, n: i }))
|
||||
.map(({ char, n }) => (
|
||||
<span
|
||||
key={`char-${n}`}
|
||||
style={{ color: colors[Math.min(n, colors.length - 1)] }}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
{"}"}
|
||||
</span>
|
||||
) : (
|
||||
@@ -546,7 +547,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
||||
{[...prefix.text].map((char, i) => (
|
||||
<span
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: char position drives color slot
|
||||
key={i}
|
||||
key={`char-${i}`}
|
||||
style={{
|
||||
color: colors[Math.min(i, colors.length - 1)],
|
||||
}}
|
||||
@@ -570,7 +571,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
|
||||
{colors.slice(0, 5).map((c, i) => (
|
||||
<div
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: color preview slots, position is identity
|
||||
key={i}
|
||||
key={`color-${i}`}
|
||||
className="w-4 h-4 rounded border"
|
||||
style={{ backgroundColor: c }}
|
||||
title={c}
|
||||
|
||||
@@ -95,6 +95,7 @@ export default async function AdminRadioEmbedPage() {
|
||||
src={streamUrl}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<track kind="captions" />
|
||||
{t("embedPage.audioUnsupported")}
|
||||
</audio>
|
||||
<p
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function RoomsLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -53,9 +53,9 @@ export default async function AdminTicketsPage({
|
||||
const where: Prisma.WebsiteTicketWhereInput =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
? conditions[0]!
|
||||
: { AND: conditions };
|
||||
: conditions.length === 1
|
||||
? conditions[0]
|
||||
: { AND: conditions };
|
||||
|
||||
const SORT_MAP: Record<string, Prisma.WebsiteTicketOrderByWithRelationInput> =
|
||||
{
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function TranslationsLoading() {
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-12" />
|
||||
|
||||
@@ -26,8 +26,8 @@ export default function UserDetailLoading() {
|
||||
{/* Form */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
<Skeleton className="h-6 w-24" />
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
{Array.from({ length: 6 }, (_, i) => i).map((n) => (
|
||||
<div key={`skeleton-field-${n}`} className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
@@ -37,8 +37,11 @@ export default function UserDetailLoading() {
|
||||
|
||||
{/* Side cards */}
|
||||
<div className="space-y-6">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="rounded-lg border bg-card p-6 space-y-3">
|
||||
{Array.from({ length: 3 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={`skeleton-card-${n}`}
|
||||
className="rounded-lg border bg-card p-6 space-y-3"
|
||||
>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</div>
|
||||
|
||||
@@ -22,14 +22,14 @@ export default function UsersLoading() {
|
||||
<div className="rounded-md border">
|
||||
<div className="border-b px-4 py-3">
|
||||
<div className="flex gap-8">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-4 w-20" />
|
||||
{Array.from({ length: 6 }, (_, i) => i).map((n) => (
|
||||
<Skeleton key={`skeleton-header-${n}`} className="h-4 w-20" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
{Array.from({ length: 10 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={i}
|
||||
key={`skeleton-row-${n}`}
|
||||
className="flex items-center gap-8 px-4 py-3 border-b last:border-0"
|
||||
>
|
||||
<Skeleton className="h-4 w-10" />
|
||||
|
||||
Reference in new issue
Block a user