Files
Epicnabbo-Catalogus-Updated…/src/app/admin/settings/cms-settings-form.tsx
T
simoleo89andCursor d1baaf798a Add multi-hotel Habbo gamedata locale in CMS settings.
Furni name suggestions, import enrichment, and badge texts now follow habbo_gamedata_hotel instead of hardcoded habbo.it.

Co-authored-by: Cursor <[email protected]>
2026-07-17 22:28:18 +02:00

236 lines
5.9 KiB
TypeScript

"use client";
import {
Building2,
Gamepad2,
ImageIcon,
Link as LinkIcon,
Loader2,
Music,
Save,
Shield,
UserPlus,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { saveManagedSettings } from "@/actions/admin-settings";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useServerAction } from "@/hooks/use-server-action";
import {
BOOLEAN_KEYS,
type ManagedField,
SETTINGS_GROUPS,
type SettingsGroup,
} from "./cms-settings-config";
const ICONS = {
building: Building2,
image: ImageIcon,
gamepad: Gamepad2,
music: Music,
shield: Shield,
link: LinkIcon,
"user-plus": UserPlus,
} as const;
export function CmsSettingsForm({
values: initialValues,
canEdit,
}: {
values: Record<string, string>;
canEdit: boolean;
}) {
const [values, setValues] = useState(initialValues);
const { isPending, run } = useServerAction();
const t = useTranslations("pages.admin.settings");
function handleChange(key: string, value: string) {
setValues((prev) => ({ ...prev, [key]: value }));
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!canEdit) return;
const normalized = { ...values };
for (const key of BOOLEAN_KEYS) {
normalized[key] = values[key] === "1" ? "1" : "0";
}
run(() => saveManagedSettings({ settings: normalized }), {
successMessage: t("saved"),
errorMessage: t("saveFailed"),
});
}
return (
<form onSubmit={handleSubmit} className="space-y-5">
{SETTINGS_GROUPS.map((group) => (
<GroupSection
key={group.id}
group={group}
values={values}
canEdit={canEdit}
onChange={handleChange}
/>
))}
{canEdit ? (
<div className="sticky bottom-3 z-10 flex justify-end">
<Button type="submit" disabled={isPending} className="shadow-lg">
{isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
{isPending ? t("form.saving") : t("saveSettings")}
</Button>
</div>
) : (
<p className="text-sm text-muted-foreground">{t("viewOnly")}</p>
)}
</form>
);
}
function GroupSection({
group,
values,
canEdit,
onChange,
}: {
group: SettingsGroup;
values: Record<string, string>;
canEdit: boolean;
onChange: (key: string, value: string) => void;
}) {
const Icon = ICONS[group.icon];
return (
<section className="rounded-2xl border border-[var(--admin-border)] bg-[var(--admin-surface)] p-5 space-y-4 shadow-card">
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center shrink-0">
<Icon size={18} className="text-[var(--admin-accent)]" />
</div>
<div className="min-w-0">
<h2 className="m-0 text-base font-bold text-[var(--admin-text)]">
{group.title}
</h2>
{group.description ? (
<p className="m-0 mt-0.5 text-xs text-[var(--admin-text-muted)]">
{group.description}
</p>
) : null}
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{group.fields.map((field) => (
<FieldControl
key={field.key}
field={field}
value={values[field.key] ?? ""}
canEdit={canEdit}
onChange={onChange}
/>
))}
</div>
</section>
);
}
function FieldControl({
field,
value,
canEdit,
onChange,
}: {
field: ManagedField;
value: string;
canEdit: boolean;
onChange: (key: string, value: string) => void;
}) {
const type = field.type ?? "text";
const wide = type === "textarea" || type === "boolean";
if (type === "boolean") {
const checked = value === "1" || value.toLowerCase() === "true";
return (
<div
className={`${wide ? "sm:col-span-2" : ""} flex items-center justify-between gap-4 rounded-xl border border-[var(--admin-border)] px-4 py-3`}
>
<div className="min-w-0">
<Label htmlFor={field.key} className="text-sm font-semibold">
{field.label}
</Label>
{field.description ? (
<p className="m-0 mt-0.5 text-xs text-muted-foreground">
{field.description}
</p>
) : null}
</div>
<Switch
id={field.key}
checked={checked}
disabled={!canEdit}
onCheckedChange={(next) => onChange(field.key, next ? "1" : "0")}
/>
</div>
);
}
return (
<div className={`space-y-1.5 ${wide ? "sm:col-span-2" : ""}`}>
<Label htmlFor={field.key} className="text-sm font-semibold">
{field.label}
</Label>
{field.description ? (
<p className="m-0 text-xs text-muted-foreground">{field.description}</p>
) : null}
{type === "textarea" ? (
<Textarea
id={field.key}
value={value}
disabled={!canEdit}
placeholder={field.placeholder}
onChange={(e) => onChange(field.key, e.target.value)}
rows={3}
/>
) : type === "select" ? (
<select
id={field.key}
value={value}
disabled={!canEdit}
onChange={(e) => onChange(field.key, e.target.value)}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
{(field.options ?? []).map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
) : (
<Input
id={field.key}
type={
type === "number"
? "number"
: type === "password"
? "password"
: "text"
}
value={value}
disabled={!canEdit}
placeholder={field.placeholder}
onChange={(e) => onChange(field.key, e.target.value)}
/>
)}
<code className="block text-[0.65rem] text-muted-foreground truncate">
{field.key}
</code>
</div>
);
}