'use client' import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Loader2, RotateCcw, Save, Search, } from 'lucide-react' import { useRouter, useSearchParams } from 'next/navigation' import { useEffect, useMemo, useState, useTransition } from 'react' import { toast } from 'sonner' import { saveEmulatorSettings } from '@/actions/emulator' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { useServerAction } from '@/hooks/use-server-action' interface Setting { key: string value: string } interface GroupSummary { name: string count: number } interface EmulatorSettingsFormProps { settings: Setting[] pagination: { total: number; page: number; perPage: number; lastPage: number } search: string allGroups?: GroupSummary[] activeGroup?: string } const PER_PAGE_OPTIONS = [25, 50, 100, 200] as const function getSettingGroup(key: string): string { const parts = key.split('.') return parts.length > 1 ? parts[0] : 'general' } const GROUP_BORDER_COLORS: Record = { hotel: 'border-l-blue-500', navigator: 'border-l-green-500', catalog: 'border-l-purple-500', room: 'border-l-orange-500', camera: 'border-l-pink-500', commands: 'border-l-cyan-500', wordfilter: 'border-l-red-500', pet: 'border-l-yellow-500', matchmaking: 'border-l-indigo-500', message: 'border-l-emerald-500', notification: 'border-l-fuchsia-500', general: 'border-l-gray-400', } export function EmulatorSettingsForm({ settings, pagination, search, allGroups, activeGroup, }: EmulatorSettingsFormProps) { const router = useRouter() const searchParams = useSearchParams() // `values` only contains overrides typed by the user. Display falls back // to `setting.value`. `originalValues` accumulates the on-disk values for // every setting we have ever rendered (across pages) so we can detect when // an edit returns to its original state. const [values, setValues] = useState>({}) const [originalValues, setOriginalValues] = useState>({}) const [changedKeys, setChangedKeys] = useState>(new Set()) const [isNavigating, startTransition] = useTransition() const { run, isPending } = useServerAction() const [searchValue, setSearchValue] = useState(search) useEffect(() => { setOriginalValues((prev) => { let dirty = false const next = { ...prev } for (const s of settings) { if (!(s.key in next)) { next[s.key] = s.value dirty = true } } return dirty ? next : prev }) }, [settings]) // Prefer the canonical list passed by the server (covers all groups, not // just those visible on the current page). Fall back to deriving from the // current page when the prop is omitted. const groupSummary = useMemo(() => { if (allGroups && allGroups.length > 0) return allGroups const map = new Map() for (const s of settings) { const g = getSettingGroup(s.key) map.set(g, (map.get(g) ?? 0) + 1) } return Array.from(map.entries()) .map(([name, count]) => ({ name, count })) .sort((a, b) => a.name.localeCompare(b.name)) }, [allGroups, settings]) function toggleGroup(name: string) { updateParams({ group: activeGroup === name ? '' : name, page: '1', }) } function getDisplayValue(setting: Setting): string { return values[setting.key] ?? setting.value } function updateValue(key: string, val: string) { setValues((prev) => ({ ...prev, [key]: val })) setChangedKeys((prev) => { const next = new Set(prev) if (originalValues[key] !== undefined && val === originalValues[key]) { next.delete(key) } else { next.add(key) } return next }) } function revertChange(key: string) { setValues((prev) => { const next = { ...prev } delete next[key] return next }) setChangedKeys((prev) => { const next = new Set(prev) next.delete(key) return next }) } function updateParams(updates: Record) { const params = new URLSearchParams(searchParams.toString()) for (const [key, value] of Object.entries(updates)) { if (value) params.set(key, value) else params.delete(key) } startTransition(() => { router.push(`?${params.toString()}`) }) } function handleSearch(e: React.FormEvent) { e.preventDefault() updateParams({ search: searchValue, page: '1' }) } function handleSaveAll() { if (changedKeys.size === 0) { toast.info('No changes to save.') return } const changedSettings: Record = {} for (const key of changedKeys) { changedSettings[key] = values[key] } const count = changedKeys.size run(() => saveEmulatorSettings({ settings: changedSettings }), { successMessage: `${count} setting(s) saved and synced with emulator.`, onSuccess: () => { setChangedKeys(new Set()) setValues({}) // Refresh server data so the on-disk values reflect the save and the // originalValues snapshot rebuilds on next render. setOriginalValues({}) router.refresh() }, }) } function jumpToPage(raw: string) { const n = parseInt(raw, 10) if (!Number.isFinite(n)) return const clamped = Math.max(1, Math.min(pagination.lastPage, n)) if (clamped !== pagination.page) updateParams({ page: String(clamped) }) } return (
{/* Toolbar */}
setSearchValue(e.target.value)} className="pl-9" />
{pagination.total} settings {changedKeys.size > 0 && ( {changedKeys.size} changed )}
{/* Group summary chips — click to toggle group filter */} {groupSummary.length > 1 && (
{activeGroup && ( )} {groupSummary.map(({ name, count }) => { const isActive = activeGroup === name const color = (GROUP_BORDER_COLORS[name] || GROUP_BORDER_COLORS.general).replace( 'border-l-', 'bg-', ) return ( ) })}
)} {/* Settings list — flat rows with always-on inline editing */}
{settings.length === 0 ? (
No settings found.
) : ( settings.map((setting) => { const group = getSettingGroup(setting.key) const borderColor = GROUP_BORDER_COLORS[group] || GROUP_BORDER_COLORS.general const isChanged = changedKeys.has(setting.key) const display = getDisplayValue(setting) return (
{setting.key} updateValue(setting.key, e.target.value)} className={`flex-1 h-8 text-xs font-mono ${ isChanged ? 'border-yellow-500/60' : '' }`} /> {isChanged && ( )}
) }) )}
{/* Pagination + per-page selector */}
Per page {pagination.total === 0 ? '0 results' : `${(pagination.page - 1) * pagination.perPage + 1}–${Math.min( pagination.page * pagination.perPage, pagination.total, )} of ${pagination.total}`}
jumpToPage(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') jumpToPage((e.target as HTMLInputElement).value) }} className="h-8 w-14 text-center text-xs" /> of {pagination.lastPage}
) }