diff --git a/src/components/server/PresetPickerModal.tsx b/src/components/server/PresetPickerModal.tsx new file mode 100644 index 0000000..a59c96e --- /dev/null +++ b/src/components/server/PresetPickerModal.tsx @@ -0,0 +1,187 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { Modal } from '@/components/ui/Modal'; +import { Button } from '@/components/ui/Button'; +import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; +import { listPresetsAction, applyPresetAction } from '@/lib/actions/presets'; +import type { ConfigPreset } from '@/lib/schemas/preset'; +import { PRESET_SECTION_LABELS, getPresetSections } from '@/lib/schemas/preset'; + +interface PresetPickerModalProps { + isOpen: boolean; + onClose: () => void; + serverId: string; + onApplied: () => void; +} + +export function PresetPickerModal({ isOpen, onClose, serverId, onApplied }: PresetPickerModalProps) { + const [presets, setPresets] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [loadError, setLoadError] = useState(null); + const [selected, setSelected] = useState(null); + const [restart, setRestart] = useState(false); + const [isApplying, setIsApplying] = useState(false); + const [applyError, setApplyError] = useState(null); + const [search, setSearch] = useState(''); + + const loadPresets = useCallback(async () => { + setIsLoading(true); + setLoadError(null); + const result = await listPresetsAction(); + setIsLoading(false); + if (!result.success) { + setLoadError(result.message); + return; + } + setPresets(result.data); + }, []); + + useEffect(() => { + if (isOpen) { + setSelected(null); + setApplyError(null); + setSearch(''); + loadPresets(); + } + }, [isOpen, loadPresets]); + + const filtered = presets.filter((p) => + p.name.toLowerCase().includes(search.toLowerCase()) || + p.description.toLowerCase().includes(search.toLowerCase()) + ); + + const handleApply = async () => { + if (!selected) return; + setIsApplying(true); + setApplyError(null); + const result = await applyPresetAction(serverId, selected, restart); + setIsApplying(false); + if (!result.success) { + setApplyError(result.message); + return; + } + onApplied(); + onClose(); + }; + + const selectedPreset = presets.find((p) => p.id === selected); + + return ( + +
+ setSearch(e.target.value)} + className="form-input text-sm" + /> + + {loadError &&

{loadError}

} + +
+ {isLoading ? ( +
+ +
+ ) : filtered.length === 0 ? ( +
+ {presets.length === 0 ? 'No presets saved yet.' : 'No presets match your search.'} +
+ ) : ( +
    + {filtered.map((preset) => { + const sections = getPresetSections(preset); + const isSelected = selected === preset.id; + return ( +
  • setSelected(preset.id)} + className={`cursor-pointer border-b border-border-muted px-4 py-3 transition-colors last:border-0 ${ + isSelected + ? 'bg-orange/10' + : 'hover:bg-hover' + }`} + > +
    +
    +

    + {preset.name} +

    + {preset.description && ( +

    {preset.description}

    + )} +
    +
    + {sections.length > 0 ? ( +

    {sections.length} section{sections.length !== 1 ? 's' : ''}

    + ) : ( +

    Empty

    + )} +
    +
    +
  • + ); + })} +
+ )} +
+ + {selectedPreset && ( +
+

Sections in this preset:

+
+ {getPresetSections(selectedPreset).map((key) => ( + + {PRESET_SECTION_LABELS[key] ?? key} + + ))} + {getPresetSections(selectedPreset).length === 0 && ( + No sections — nothing will be applied. + )} +
+
+ )} + + {applyError &&

{applyError}

} + +
+ + +
+ + +
+
+
+
+ ); +} diff --git a/src/components/server/SavePresetModal.tsx b/src/components/server/SavePresetModal.tsx new file mode 100644 index 0000000..a6a41cd --- /dev/null +++ b/src/components/server/SavePresetModal.tsx @@ -0,0 +1,149 @@ +'use client'; + +import { useState } from 'react'; +import { Modal } from '@/components/ui/Modal'; +import { Button } from '@/components/ui/Button'; +import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; +import { createPresetAction } from '@/lib/actions/presets'; +import type { Configurations } from '@/lib/schemas/config'; +import { PRESET_SECTION_LABELS } from '@/lib/schemas/preset'; + +type SectionKey = keyof Configurations; + +const ALL_SECTIONS: SectionKey[] = ['configuration', 'assistRules', 'event', 'eventRules', 'settings']; + +interface SavePresetModalProps { + isOpen: boolean; + onClose: () => void; + configurations: Configurations; +} + +export function SavePresetModal({ isOpen, onClose, configurations }: SavePresetModalProps) { + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [selectedSections, setSelectedSections] = useState>( + new Set(ALL_SECTIONS) + ); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const [saved, setSaved] = useState(false); + + const toggleSection = (key: SectionKey) => + setSelectedSections((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + + const handleClose = () => { + if (isSaving) return; + setName(''); + setDescription(''); + setSelectedSections(new Set(ALL_SECTIONS)); + setError(null); + setSaved(false); + onClose(); + }; + + const handleSave = async () => { + if (!name.trim() || selectedSections.size === 0) return; + setIsSaving(true); + setError(null); + + const result = await createPresetAction({ + name: name.trim(), + description: description.trim() || undefined, + configuration: selectedSections.has('configuration') ? (configurations.configuration as Record) : null, + assistRules: selectedSections.has('assistRules') ? (configurations.assistRules as Record) : null, + event: selectedSections.has('event') ? (configurations.event as Record) : null, + eventRules: selectedSections.has('eventRules') ? (configurations.eventRules as Record) : null, + settings: selectedSections.has('settings') ? (configurations.settings as Record) : null + }); + + setIsSaving(false); + + if (!result.success) { + setError(result.message); + return; + } + + setSaved(true); + setTimeout(() => handleClose(), 1200); + }; + + return ( + +
+
+ + setName(e.target.value)} + placeholder="e.g. Sprint Race — Spa" + className="form-input text-sm" + disabled={isSaving || saved} + /> +
+ +
+ + setDescription(e.target.value)} + placeholder="Optional description..." + className="form-input text-sm" + disabled={isSaving || saved} + /> +
+ +
+ +
+ {ALL_SECTIONS.map((key) => ( + + ))} +
+
+ + {error &&

{error}

} + {saved &&

Preset saved!

} + +
+ + +
+
+
+ ); +} diff --git a/src/components/server/ServerConfigurationTabs.tsx b/src/components/server/ServerConfigurationTabs.tsx index e5d1e2a..94d83a3 100755 --- a/src/components/server/ServerConfigurationTabs.tsx +++ b/src/components/server/ServerConfigurationTabs.tsx @@ -10,10 +10,15 @@ import { StatisticsDashboard } from '@/components/statistics/StatisticsDashboard import { LeaderboardManager } from '@/components/leaderboard/LeaderboardManager'; import { ActivityLogTable } from '@/components/activityLog/ActivityLogTable'; import { ServerLogViewer } from '@/components/server/ServerLogViewer'; +import { PresetPickerModal } from '@/components/server/PresetPickerModal'; +import { SavePresetModal } from '@/components/server/SavePresetModal'; import { useState } from 'react'; import { StateHistoryStats } from '@/lib/schemas'; import { Leaderboard } from '@/lib/schemas/leaderboard'; -import { bulkUpdateConfigurationsAction } from '@/lib/actions/configuration'; +import { + bulkUpdateConfigurationsAction, + getConfigurationsAction +} from '@/lib/actions/configuration'; interface ServerConfigurationTabsProps { serverId: string; @@ -56,10 +61,23 @@ export function ServerConfigurationTabs({ const [settingsData, setSettingsData] = useState(configurations.settings); const [dirtyConfigs, setDirtyConfigs] = useState>(new Set()); - const [restart, setRestart] = useState(true); + const [restart, setRestart] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [saveError, setSaveError] = useState(null); const [saveSuccess, setSaveSuccess] = useState(false); + const [isPresetModalOpen, setIsPresetModalOpen] = useState(false); + const [isSavePresetModalOpen, setIsSavePresetModalOpen] = useState(false); + + const reloadConfigs = async () => { + const result = await getConfigurationsAction(serverId); + if (!result.success) return; + setConfigData(result.data.configuration); + setAssistData(result.data.assistRules); + setEventData(result.data.event); + setEventRulesData(result.data.eventRules); + setSettingsData(result.data.settings); + setDirtyConfigs(new Set()); + }; const markDirty = (key: keyof Configurations) => setDirtyConfigs((prev) => new Set(prev).add(key)); @@ -103,7 +121,10 @@ export function ServerConfigurationTabs({ { setConfigData(data); markDirty('configuration'); }} + onFormDataChange={(data) => { + setConfigData(data); + markDirty('configuration'); + }} /> ); @@ -112,7 +133,10 @@ export function ServerConfigurationTabs({ { setAssistData(data); markDirty('assistRules'); }} + onFormDataChange={(data) => { + setAssistData(data); + markDirty('assistRules'); + }} /> ); @@ -121,7 +145,10 @@ export function ServerConfigurationTabs({ { setEventData(data); markDirty('event'); }} + onFormDataChange={(data) => { + setEventData(data); + markDirty('event'); + }} /> ); @@ -130,7 +157,10 @@ export function ServerConfigurationTabs({ { setEventRulesData(data); markDirty('eventRules'); }} + onFormDataChange={(data) => { + setEventRulesData(data); + markDirty('eventRules'); + }} /> ); @@ -139,7 +169,10 @@ export function ServerConfigurationTabs({ { setSettingsData(data); markDirty('settings'); }} + onFormDataChange={(data) => { + setSettingsData(data); + markDirty('settings'); + }} /> ); @@ -165,7 +198,10 @@ export function ServerConfigurationTabs({ return (
-