fix: error handling
This commit is contained in:
@@ -39,6 +39,9 @@ function parseDetails(raw: string): string {
|
|||||||
if (obj.driver_count !== undefined && obj.race_count !== undefined) {
|
if (obj.driver_count !== undefined && obj.race_count !== undefined) {
|
||||||
return `${obj.driver_count} drivers, ${obj.race_count} races`;
|
return `${obj.driver_count} drivers, ${obj.race_count} races`;
|
||||||
}
|
}
|
||||||
|
if (obj.preset_id) {
|
||||||
|
return `Preset: ${obj.preset_name} - Sections applied: ${obj.sections_applied}`;
|
||||||
|
}
|
||||||
return raw;
|
return raw;
|
||||||
} catch {
|
} catch {
|
||||||
return raw;
|
return raw;
|
||||||
@@ -50,7 +53,9 @@ const ACTION_BADGE: Record<ActionType, string> = {
|
|||||||
server_start: 'bg-green-bg text-green',
|
server_start: 'bg-green-bg text-green',
|
||||||
server_stop: 'bg-red-bg text-red',
|
server_stop: 'bg-red-bg text-red',
|
||||||
server_restart: 'bg-yellow-bg text-yellow',
|
server_restart: 'bg-yellow-bg text-yellow',
|
||||||
leaderboard_update: 'bg-overlay text-muted'
|
leaderboard_update: 'bg-overlay text-muted',
|
||||||
|
preset_create: 'bg-green-bg text-green',
|
||||||
|
preset_apply: 'bg-blue-bg text-blue'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ActivityLogTable({ serverId, servers }: ActivityLogTableProps) {
|
export function ActivityLogTable({ serverId, servers }: ActivityLogTableProps) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { AssistRules } from '@/lib/schemas/config';
|
import type { AssistRules } from '@/lib/schemas/config';
|
||||||
|
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||||
|
|
||||||
interface AssistRulesEditorProps {
|
interface AssistRulesEditorProps {
|
||||||
formData: AssistRules;
|
formData: AssistRules;
|
||||||
@@ -8,52 +9,16 @@ interface AssistRulesEditorProps {
|
|||||||
onFormDataChange: (data: AssistRules) => void;
|
onFormDataChange: (data: AssistRules) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistFields = [
|
const assistFields: { key: keyof AssistRules; type: 'number' | 'select' }[] = [
|
||||||
{
|
{ key: 'stabilityControlLevelMax', type: 'number' },
|
||||||
key: 'stabilityControlLevelMax' as keyof AssistRules,
|
{ key: 'disableAutosteer', type: 'select' },
|
||||||
label: 'Stability Control Level Max',
|
{ key: 'disableAutoLights', type: 'select' },
|
||||||
type: 'number'
|
{ key: 'disableAutoWiper', type: 'select' },
|
||||||
},
|
{ key: 'disableAutoEngineStart', type: 'select' },
|
||||||
{
|
{ key: 'disableAutoPitLimiter', type: 'select' },
|
||||||
key: 'disableAutosteer' as keyof AssistRules,
|
{ key: 'disableAutoGear', type: 'select' },
|
||||||
label: 'Disable Autosteer',
|
{ key: 'disableAutoClutch', type: 'select' },
|
||||||
type: 'select'
|
{ key: 'disableIdealLine', type: 'select' }
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'disableAutoLights' as keyof AssistRules,
|
|
||||||
label: 'Disable Auto Lights',
|
|
||||||
type: 'select'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'disableAutoWiper' as keyof AssistRules,
|
|
||||||
label: 'Disable Auto Wiper',
|
|
||||||
type: 'select'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'disableAutoEngineStart' as keyof AssistRules,
|
|
||||||
label: 'Disable Auto Engine Start',
|
|
||||||
type: 'select'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'disableAutoPitLimiter' as keyof AssistRules,
|
|
||||||
label: 'Disable Auto Pit Limiter',
|
|
||||||
type: 'select'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'disableAutoGear' as keyof AssistRules,
|
|
||||||
label: 'Disable Auto Gear',
|
|
||||||
type: 'select'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'disableAutoClutch' as keyof AssistRules,
|
|
||||||
label: 'Disable Auto Clutch',
|
|
||||||
type: 'select'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'disableIdealLine' as keyof AssistRules,
|
|
||||||
label: 'Disable Ideal Line',
|
|
||||||
type: 'select'
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AssistRulesEditor({ formData, disabled, onFormDataChange }: AssistRulesEditorProps) {
|
export function AssistRulesEditor({ formData, disabled, onFormDataChange }: AssistRulesEditorProps) {
|
||||||
@@ -67,9 +32,9 @@ export function AssistRulesEditor({ formData, disabled, onFormDataChange }: Assi
|
|||||||
return (
|
return (
|
||||||
<div className="max-w-3xl space-y-6">
|
<div className="max-w-3xl space-y-6">
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
{assistFields.map(({ key, label, type }) => (
|
{assistFields.map(({ key, type }) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
|
||||||
{type === 'number' ? (
|
{type === 'number' ? (
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { Configuration } from '@/lib/schemas/config';
|
import type { Configuration } from '@/lib/schemas/config';
|
||||||
|
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||||
|
|
||||||
interface ConfigurationEditorProps {
|
interface ConfigurationEditorProps {
|
||||||
formData: Configuration;
|
formData: Configuration;
|
||||||
@@ -20,7 +21,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
|||||||
<div className="max-w-3xl space-y-6">
|
<div className="max-w-3xl space-y-6">
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">UDP Port</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.udpPort}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -31,7 +32,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">TCP Port</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.tcpPort}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -42,7 +43,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Max Connections</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.maxConnections}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -53,7 +54,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">LAN Discovery</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.lanDiscovery}</label>
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData.lanDiscovery}
|
value={formData.lanDiscovery}
|
||||||
@@ -66,7 +67,7 @@ export function ConfigurationEditor({ formData, disabled, onFormDataChange }: Co
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Register To Lobby</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.registerToLobby}</label>
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData.registerToLobby}
|
value={formData.registerToLobby}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { EventConfig, Session } from '@/lib/schemas/config';
|
import type { EventConfig, Session } from '@/lib/schemas/config';
|
||||||
|
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||||
import { TRACKS } from '@/lib/constants/tracks';
|
import { TRACKS } from '@/lib/constants/tracks';
|
||||||
|
|
||||||
interface EventConfigEditorProps {
|
interface EventConfigEditorProps {
|
||||||
@@ -63,7 +64,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Track</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.track}</label>
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData.track}
|
value={formData.track}
|
||||||
@@ -79,9 +80,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.preRaceWaitingTimeSeconds}</label>
|
||||||
Pre-Race Waiting Time (seconds)
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -93,9 +92,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.sessionOverTimeSeconds}</label>
|
||||||
Session Over Time (seconds)
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -107,9 +104,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.postQualySeconds}</label>
|
||||||
Post Qualify Seconds
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -121,9 +116,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.postRaceSeconds}</label>
|
||||||
Post Race Seconds
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -142,9 +135,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.ambientTemp}</label>
|
||||||
Ambient Temperature (°C)
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -157,9 +148,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.cloudLevel}</label>
|
||||||
Cloud Level (0.0-1.0)
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -173,7 +162,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Rain (0.0-1.0)</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.rain}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -187,9 +176,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.weatherRandomness}</label>
|
||||||
Weather Randomness
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -202,9 +189,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.simracerWeatherConditions}</label>
|
||||||
Simracer Weather Conditions
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData.simracerWeatherConditions}
|
value={formData.simracerWeatherConditions}
|
||||||
@@ -217,9 +202,7 @@ export function EventConfigEditor({ formData, disabled, onFormDataChange }: Even
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.isFixedConditionQualification}</label>
|
||||||
Fixed Condition Qualification
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData.isFixedConditionQualification}
|
value={formData.isFixedConditionQualification}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { EventRules } from '@/lib/schemas/config';
|
import type { EventRules } from '@/lib/schemas/config';
|
||||||
|
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||||
|
|
||||||
interface EventRulesEditorProps {
|
interface EventRulesEditorProps {
|
||||||
formData: EventRules;
|
formData: EventRules;
|
||||||
@@ -8,63 +9,21 @@ interface EventRulesEditorProps {
|
|||||||
onFormDataChange: (data: EventRules) => void;
|
onFormDataChange: (data: EventRules) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberFields = [
|
const numberFields: { key: keyof EventRules; min: number; max?: number }[] = [
|
||||||
{
|
{ key: 'pitWindowLengthSec', min: -1 },
|
||||||
key: 'pitWindowLengthSec' as keyof EventRules,
|
{ key: 'driverStintTimeSec', min: -1 },
|
||||||
label: 'Pit Window Length (seconds)',
|
{ key: 'mandatoryPitstopCount', min: 0, max: 5 },
|
||||||
min: -1
|
{ key: 'maxTotalDrivingTime', min: -1 },
|
||||||
},
|
{ key: 'maxDriversCount', min: 1, max: 10 },
|
||||||
{
|
{ key: 'tyreSetCount', min: 0, max: 50 }
|
||||||
key: 'driverStintTimeSec' as keyof EventRules,
|
|
||||||
label: 'Driver Stint Time (seconds)',
|
|
||||||
min: -1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'mandatoryPitstopCount' as keyof EventRules,
|
|
||||||
label: 'Mandatory Pitstop Count',
|
|
||||||
min: 0,
|
|
||||||
max: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'maxTotalDrivingTime' as keyof EventRules,
|
|
||||||
label: 'Max Total Driving Time (seconds)',
|
|
||||||
min: -1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'maxDriversCount' as keyof EventRules,
|
|
||||||
label: 'Max Drivers Count',
|
|
||||||
min: 1,
|
|
||||||
max: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'tyreSetCount' as keyof EventRules,
|
|
||||||
label: 'Tyre Set Count',
|
|
||||||
min: 0,
|
|
||||||
max: 50
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const booleanFields = [
|
const booleanFields: { key: keyof EventRules }[] = [
|
||||||
{
|
{ key: 'isRefuellingAllowedInRace' },
|
||||||
key: 'isRefuellingAllowedInRace' as keyof EventRules,
|
{ key: 'isRefuellingTimeFixed' },
|
||||||
label: 'Refuelling Allowed in Race'
|
{ key: 'isMandatoryPitstopRefuellingRequired' },
|
||||||
},
|
{ key: 'isMandatoryPitstopTyreChangeRequired' },
|
||||||
{
|
{ key: 'isMandatoryPitstopSwapDriverRequired' }
|
||||||
key: 'isRefuellingTimeFixed' as keyof EventRules,
|
|
||||||
label: 'Refuelling Time Fixed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'isMandatoryPitstopRefuellingRequired' as keyof EventRules,
|
|
||||||
label: 'Mandatory Pitstop Refuelling Required'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'isMandatoryPitstopTyreChangeRequired' as keyof EventRules,
|
|
||||||
label: 'Mandatory Pitstop Tyre Change Required'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'isMandatoryPitstopSwapDriverRequired' as keyof EventRules,
|
|
||||||
label: 'Mandatory Pitstop Swap Driver Required'
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function EventRulesEditor({ formData, disabled, onFormDataChange }: EventRulesEditorProps) {
|
export function EventRulesEditor({ formData, disabled, onFormDataChange }: EventRulesEditorProps) {
|
||||||
@@ -78,7 +37,7 @@ export function EventRulesEditor({ formData, disabled, onFormDataChange }: Event
|
|||||||
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">Race Rules</h3>
|
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">Race Rules</h3>
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Qualify Standing Type</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.qualifyStandingType}</label>
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData.qualifyStandingType}
|
value={formData.qualifyStandingType}
|
||||||
@@ -89,9 +48,9 @@ export function EventRulesEditor({ formData, disabled, onFormDataChange }: Event
|
|||||||
<option value={2}>Average Lap</option>
|
<option value={2}>Average Lap</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{numberFields.map(({ key, label, min, max }) => (
|
{numberFields.map(({ key, min, max }) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -111,9 +70,9 @@ export function EventRulesEditor({ formData, disabled, onFormDataChange }: Event
|
|||||||
Pitstop & Refuelling Rules
|
Pitstop & Refuelling Rules
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
{booleanFields.map(({ key, label }) => (
|
{booleanFields.map(({ key }) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData[key] ? 'true' : 'false'}
|
value={formData[key] ? 'true' : 'false'}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { ServerSettings } from '@/lib/schemas/config';
|
import type { ServerSettings } from '@/lib/schemas/config';
|
||||||
|
import { FIELD_LABELS } from '@/lib/schemas/config';
|
||||||
|
|
||||||
interface ServerSettingsEditorProps {
|
interface ServerSettingsEditorProps {
|
||||||
formData: ServerSettings;
|
formData: ServerSettings;
|
||||||
@@ -8,86 +9,31 @@ interface ServerSettingsEditorProps {
|
|||||||
onFormDataChange: (data: ServerSettings) => void;
|
onFormDataChange: (data: ServerSettings) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textFields = [
|
const textFields: { key: keyof ServerSettings; type: 'text' | 'password' }[] = [
|
||||||
{
|
{ key: 'serverName', type: 'text' },
|
||||||
key: 'serverName' as keyof ServerSettings,
|
{ key: 'adminPassword', type: 'password' },
|
||||||
label: 'Server Name',
|
{ key: 'password', type: 'password' },
|
||||||
type: 'text'
|
{ key: 'spectatorPassword', type: 'password' },
|
||||||
},
|
{ key: 'centralEntryListPath', type: 'text' }
|
||||||
{
|
|
||||||
key: 'adminPassword' as keyof ServerSettings,
|
|
||||||
label: 'Admin Password',
|
|
||||||
type: 'password'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'password' as keyof ServerSettings,
|
|
||||||
label: 'Password',
|
|
||||||
type: 'password'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'spectatorPassword' as keyof ServerSettings,
|
|
||||||
label: 'Spectator Password',
|
|
||||||
type: 'password'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'centralEntryListPath' as keyof ServerSettings,
|
|
||||||
label: 'Central Entry List Path',
|
|
||||||
type: 'text'
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const carGroups = ['FreeForAll', 'GT3', 'GT4', 'GT2', 'GTC', 'TCX'];
|
const carGroups = ['FreeForAll', 'GT3', 'GT4', 'GT2', 'GTC', 'TCX'];
|
||||||
|
|
||||||
const numberFields = [
|
const numberFields: { key: keyof ServerSettings; min: number; max: number }[] = [
|
||||||
{
|
{ key: 'trackMedalsRequirement', min: 0, max: 3 },
|
||||||
key: 'trackMedalsRequirement' as keyof ServerSettings,
|
{ key: 'safetyRatingRequirement', min: -1, max: 99 },
|
||||||
label: 'Track Medals Requirement',
|
{ key: 'racecraftRatingRequirement', min: -1, max: 99 },
|
||||||
min: 0,
|
{ key: 'maxCarSlots', min: 1, max: 30 }
|
||||||
max: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'safetyRatingRequirement' as keyof ServerSettings,
|
|
||||||
label: 'Safety Rating Requirement',
|
|
||||||
min: -1,
|
|
||||||
max: 99
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'racecraftRatingRequirement' as keyof ServerSettings,
|
|
||||||
label: 'Racecraft Rating Requirement',
|
|
||||||
min: -1,
|
|
||||||
max: 99
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'maxCarSlots' as keyof ServerSettings,
|
|
||||||
label: 'Max Car Slots',
|
|
||||||
min: 1,
|
|
||||||
max: 30
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const selectFields = [
|
const selectFields: { key: keyof ServerSettings }[] = [
|
||||||
{
|
{ key: 'dumpLeaderboards' },
|
||||||
key: 'dumpLeaderboards' as keyof ServerSettings,
|
{ key: 'isRaceLocked' },
|
||||||
label: 'Dump Leaderboards'
|
{ key: 'randomizeTrackWhenEmpty' },
|
||||||
},
|
{ key: 'allowAutoDQ' },
|
||||||
{ key: 'isRaceLocked' as keyof ServerSettings, label: 'Race Locked' },
|
{ key: 'shortFormationLap' },
|
||||||
{
|
{ key: 'dumpEntryList' },
|
||||||
key: 'randomizeTrackWhenEmpty' as keyof ServerSettings,
|
{ key: 'ignorePrematureDisconnects' }
|
||||||
label: 'Randomize Track When Empty'
|
|
||||||
},
|
|
||||||
{ key: 'allowAutoDQ' as keyof ServerSettings, label: 'Allow Auto DQ' },
|
|
||||||
{
|
|
||||||
key: 'shortFormationLap' as keyof ServerSettings,
|
|
||||||
label: 'Short Formation Lap'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'dumpEntryList' as keyof ServerSettings,
|
|
||||||
label: 'Dump Entry List'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'ignorePrematureDisconnects' as keyof ServerSettings,
|
|
||||||
label: 'Ignore Premature Disconnects'
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: ServerSettingsEditorProps) {
|
export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: ServerSettingsEditorProps) {
|
||||||
@@ -102,9 +48,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
|||||||
Basic Settings
|
Basic Settings
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
{textFields.map(({ key, label, type }) => (
|
{textFields.map(({ key, type }) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -116,7 +62,7 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">Car Group</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.carGroup}</label>
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData.carGroup}
|
value={formData.carGroup}
|
||||||
@@ -138,9 +84,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
|||||||
Requirements & Limits
|
Requirements & Limits
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{numberFields.map(({ key, label, min, max }) => (
|
{numberFields.map(({ key, min, max }) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -160,9 +106,9 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
|||||||
Race Options
|
Race Options
|
||||||
</h3>
|
</h3>
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{selectFields.map(({ key, label }) => (
|
{selectFields.map(({ key }) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS[key]}</label>
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData[key] as number}
|
value={formData[key] as number}
|
||||||
@@ -176,9 +122,7 @@ export function ServerSettingsEditor({ formData, disabled, onFormDataChange }: S
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-sm font-medium text-secondary">
|
<label className="mb-1.5 block text-sm font-medium text-secondary">{FIELD_LABELS.formationLapType}</label>
|
||||||
Formation Lap Type
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={formData.formationLapType}
|
value={formData.formationLapType}
|
||||||
|
|||||||
@@ -224,7 +224,13 @@ export function ServerConfigurationTabs({
|
|||||||
{isConfigTab && (
|
{isConfigTab && (
|
||||||
<div className="border-border-muted bg-overlay flex items-center justify-between border-b px-5 py-3">
|
<div className="border-border-muted bg-overlay flex items-center justify-between border-b px-5 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{saveError && <p className="text-red text-sm">{saveError}</p>}
|
{saveError && (
|
||||||
|
<ul className="flex flex-col gap-0.5">
|
||||||
|
{saveError.split('\n').map((line, i) => (
|
||||||
|
<li key={i} className="text-red text-sm">• {line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
{saveSuccess && <p className="text-green text-sm">All configurations saved.</p>}
|
{saveSuccess && <p className="text-green text-sm">All configurations saved.</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { loginUser, getOpenToken } from '@/lib/api/server/auth';
|
import { loginUser, getOpenToken } from '@/lib/api/server/auth';
|
||||||
import { login, logout } from '@/lib/auth/server';
|
import { login, logout } from '@/lib/auth/server';
|
||||||
|
import { formatZodIssues } from '@/lib/utils';
|
||||||
import { loginSchema, loginResponseSchema } from '../schemas';
|
import { loginSchema, loginResponseSchema } from '../schemas';
|
||||||
|
|
||||||
export type LoginResult = {
|
export type LoginResult = {
|
||||||
@@ -19,7 +20,7 @@ export async function loginAction(prevState: LoginResult, formData: FormData) {
|
|||||||
if (!loginData.success) {
|
if (!loginData.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: loginData.error.issues[0]?.message ?? 'Validation failed'
|
message: formatZodIssues(loginData.error.issues)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { requireAuth } from '@/lib/auth/server';
|
import { requireAuth } from '@/lib/auth/server';
|
||||||
import { getServerConfigurations, updateServerConfiguration, bulkUpdateServerConfigurations } from '@/lib/api/server/configuration';
|
import { getServerConfigurations, updateServerConfiguration, bulkUpdateServerConfigurations } from '@/lib/api/server/configuration';
|
||||||
|
import { boolToInt, formatZodIssues } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
assistRulesSchema,
|
assistRulesSchema,
|
||||||
ConfigFile,
|
ConfigFile,
|
||||||
@@ -34,7 +35,6 @@ export async function getConfigurationsAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
import { boolToInt } from '@/lib/utils';
|
|
||||||
|
|
||||||
export async function updateConfigurationAction(serverId: string, formData: FormData) {
|
export async function updateConfigurationAction(serverId: string, formData: FormData) {
|
||||||
try {
|
try {
|
||||||
@@ -87,7 +87,7 @@ export async function updateAssistRulesAction(serverId: string, formData: FormDa
|
|||||||
|
|
||||||
const config = assistRulesSchema.safeParse(rawConfig);
|
const config = assistRulesSchema.safeParse(rawConfig);
|
||||||
if (!config.success) {
|
if (!config.success) {
|
||||||
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' };
|
return { success: false, message: formatZodIssues(config.error.issues) };
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateServerConfiguration(
|
await updateServerConfiguration(
|
||||||
@@ -135,7 +135,7 @@ export async function updateServerSettingsAction(serverId: string, formData: For
|
|||||||
};
|
};
|
||||||
const config = serverSettingsSchema.safeParse(rawConfig);
|
const config = serverSettingsSchema.safeParse(rawConfig);
|
||||||
if (!config.success) {
|
if (!config.success) {
|
||||||
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' };
|
return { success: false, message: formatZodIssues(config.error.issues) };
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateServerConfiguration(
|
await updateServerConfiguration(
|
||||||
@@ -182,7 +182,7 @@ export async function updateEventConfigAction(serverId: string, formData: FormDa
|
|||||||
};
|
};
|
||||||
const config = eventConfigSchema.safeParse(rawConfig);
|
const config = eventConfigSchema.safeParse(rawConfig);
|
||||||
if (!config.success) {
|
if (!config.success) {
|
||||||
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' };
|
return { success: false, message: formatZodIssues(config.error.issues) };
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateServerConfiguration(
|
await updateServerConfiguration(
|
||||||
@@ -231,7 +231,7 @@ export async function updateEventRulesAction(serverId: string, formData: FormDat
|
|||||||
|
|
||||||
const config = eventRulesSchema.safeParse(rawConfig);
|
const config = eventRulesSchema.safeParse(rawConfig);
|
||||||
if (!config.success) {
|
if (!config.success) {
|
||||||
return { success: false, message: config.error.issues[0]?.message ?? 'Validation failed' };
|
return { success: false, message: formatZodIssues(config.error.issues) };
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateServerConfiguration(
|
await updateServerConfiguration(
|
||||||
@@ -262,7 +262,7 @@ export async function bulkUpdateConfigurationsAction(
|
|||||||
|
|
||||||
const validated = configurationsSchema.partial().safeParse(configs);
|
const validated = configurationsSchema.partial().safeParse(configs);
|
||||||
if (!validated.success) {
|
if (!validated.success) {
|
||||||
return { success: false, message: validated.error.issues[0]?.message ?? 'Validation failed' };
|
return { success: false, message: formatZodIssues(validated.error.issues) };
|
||||||
}
|
}
|
||||||
|
|
||||||
await bulkUpdateServerConfigurations(session.token!, serverId, validated.data, restart);
|
await bulkUpdateServerConfigurations(session.token!, serverId, validated.data, restart);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { requireAuth } from '@/lib/auth/server';
|
import { requireAuth } from '@/lib/auth/server';
|
||||||
import { fetchServerAPI } from '@/lib/api/server/base';
|
import { fetchServerAPI } from '@/lib/api/server/base';
|
||||||
|
import { formatZodIssues } from '@/lib/utils';
|
||||||
import { leaderboardSchema, type Leaderboard } from '@/lib/schemas/leaderboard';
|
import { leaderboardSchema, type Leaderboard } from '@/lib/schemas/leaderboard';
|
||||||
|
|
||||||
export async function saveLeaderboardAction(serverId: string, data: Leaderboard) {
|
export async function saveLeaderboardAction(serverId: string, data: Leaderboard) {
|
||||||
@@ -10,7 +11,7 @@ export async function saveLeaderboardAction(serverId: string, data: Leaderboard)
|
|||||||
const session = await requireAuth();
|
const session = await requireAuth();
|
||||||
const validated = leaderboardSchema.safeParse(data);
|
const validated = leaderboardSchema.safeParse(data);
|
||||||
if (!validated.success) {
|
if (!validated.success) {
|
||||||
return { success: false, message: validated.error.issues[0]?.message ?? 'Validation failed' };
|
return { success: false, message: formatZodIssues(validated.error.issues) };
|
||||||
}
|
}
|
||||||
await fetchServerAPI(`/server/${serverId}/leaderboard`, session.token!, 'PUT', validated.data);
|
await fetchServerAPI(`/server/${serverId}/leaderboard`, session.token!, 'PUT', validated.data);
|
||||||
revalidatePath(`/dashboard/server/${serverId}`);
|
revalidatePath(`/dashboard/server/${serverId}`);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { requireAuth } from '@/lib/auth/server';
|
import { requireAuth } from '@/lib/auth/server';
|
||||||
import { createUser, deleteUser } from '@/lib/api/server/membership';
|
import { createUser, deleteUser } from '@/lib/api/server/membership';
|
||||||
|
import { formatZodIssues } from '@/lib/utils';
|
||||||
import { userCreateSchema } from '../schemas';
|
import { userCreateSchema } from '../schemas';
|
||||||
|
|
||||||
export async function createUserAction(formData: FormData) {
|
export async function createUserAction(formData: FormData) {
|
||||||
@@ -15,7 +16,7 @@ export async function createUserAction(formData: FormData) {
|
|||||||
const rawData = { username, password, role };
|
const rawData = { username, password, role };
|
||||||
const data = userCreateSchema.safeParse(rawData);
|
const data = userCreateSchema.safeParse(rawData);
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
return { success: false, message: data.error.issues[0]?.message ?? 'Validation failed' };
|
return { success: false, message: formatZodIssues(data.error.issues) };
|
||||||
}
|
}
|
||||||
|
|
||||||
await createUser(session.token!, data.data);
|
await createUser(session.token!, data.data);
|
||||||
|
|||||||
@@ -5,16 +5,20 @@ export const actionTypeSchema = z.enum([
|
|||||||
'server_start',
|
'server_start',
|
||||||
'server_stop',
|
'server_stop',
|
||||||
'server_restart',
|
'server_restart',
|
||||||
'leaderboard_update'
|
'leaderboard_update',
|
||||||
|
'preset_create',
|
||||||
|
'preset_apply'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type ActionType = z.infer<typeof actionTypeSchema>;
|
export type ActionType = z.infer<typeof actionTypeSchema>;
|
||||||
|
|
||||||
// Minimal server shape embedded in activity log responses (avoids circular import with server.ts)
|
// Minimal server shape embedded in activity log responses (avoids circular import with server.ts)
|
||||||
const embeddedServerSchema = z.object({
|
const embeddedServerSchema = z
|
||||||
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string()
|
name: z.string()
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
export type EmbeddedServer = z.infer<typeof embeddedServerSchema>;
|
export type EmbeddedServer = z.infer<typeof embeddedServerSchema>;
|
||||||
|
|
||||||
@@ -58,5 +62,7 @@ export const ACTION_TYPE_LABELS: Record<ActionType, string> = {
|
|||||||
server_start: 'Server Start',
|
server_start: 'Server Start',
|
||||||
server_stop: 'Server Stop',
|
server_stop: 'Server Stop',
|
||||||
server_restart: 'Server Restart',
|
server_restart: 'Server Restart',
|
||||||
leaderboard_update: 'Leaderboard Update'
|
leaderboard_update: 'Leaderboard Update',
|
||||||
|
preset_create: 'Preset Create',
|
||||||
|
preset_apply: 'Preset Apply'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -138,3 +138,76 @@ export const configSchemaMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Config = Configuration | AssistRules | EventConfig | EventRules | ServerSettings;
|
export type Config = Configuration | AssistRules | EventConfig | EventRules | ServerSettings;
|
||||||
|
|
||||||
|
export const SECTION_LABELS: Record<string, string> = {
|
||||||
|
configuration: 'Configuration',
|
||||||
|
assistRules: 'Assist Rules',
|
||||||
|
event: 'Event Config',
|
||||||
|
eventRules: 'Event Rules',
|
||||||
|
settings: 'Server Settings'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FIELD_LABELS: Record<string, string> = {
|
||||||
|
// configuration.json
|
||||||
|
udpPort: 'UDP Port',
|
||||||
|
tcpPort: 'TCP Port',
|
||||||
|
maxConnections: 'Max Connections',
|
||||||
|
lanDiscovery: 'LAN Discovery',
|
||||||
|
registerToLobby: 'Register To Lobby',
|
||||||
|
configVersion: 'Config Version',
|
||||||
|
// assistRules.json
|
||||||
|
stabilityControlLevelMax: 'Stability Control Level Max',
|
||||||
|
disableAutosteer: 'Disable Autosteer',
|
||||||
|
disableAutoLights: 'Disable Auto Lights',
|
||||||
|
disableAutoWiper: 'Disable Auto Wiper',
|
||||||
|
disableAutoEngineStart: 'Disable Auto Engine Start',
|
||||||
|
disableAutoPitLimiter: 'Disable Auto Pit Limiter',
|
||||||
|
disableAutoGear: 'Disable Auto Gear',
|
||||||
|
disableAutoClutch: 'Disable Auto Clutch',
|
||||||
|
disableIdealLine: 'Disable Ideal Line',
|
||||||
|
// event.json
|
||||||
|
track: 'Track',
|
||||||
|
preRaceWaitingTimeSeconds: 'Pre-Race Waiting Time (seconds)',
|
||||||
|
sessionOverTimeSeconds: 'Session Over Time (seconds)',
|
||||||
|
ambientTemp: 'Ambient Temperature (°C)',
|
||||||
|
cloudLevel: 'Cloud Level (0.0-1.0)',
|
||||||
|
rain: 'Rain (0.0-1.0)',
|
||||||
|
weatherRandomness: 'Weather Randomness',
|
||||||
|
postQualySeconds: 'Post Qualify Seconds',
|
||||||
|
postRaceSeconds: 'Post Race Seconds',
|
||||||
|
simracerWeatherConditions: 'Simracer Weather Conditions',
|
||||||
|
isFixedConditionQualification: 'Fixed Condition Qualification',
|
||||||
|
sessions: 'Sessions',
|
||||||
|
// eventRules.json
|
||||||
|
qualifyStandingType: 'Qualify Standing Type',
|
||||||
|
pitWindowLengthSec: 'Pit Window Length (seconds)',
|
||||||
|
driverStintTimeSec: 'Driver Stint Time (seconds)',
|
||||||
|
mandatoryPitstopCount: 'Mandatory Pitstop Count',
|
||||||
|
maxTotalDrivingTime: 'Max Total Driving Time (seconds)',
|
||||||
|
maxDriversCount: 'Max Drivers Count',
|
||||||
|
isRefuellingAllowedInRace: 'Refuelling Allowed in Race',
|
||||||
|
isRefuellingTimeFixed: 'Refuelling Time Fixed',
|
||||||
|
isMandatoryPitstopRefuellingRequired: 'Mandatory Pitstop Refuelling Required',
|
||||||
|
isMandatoryPitstopTyreChangeRequired: 'Mandatory Pitstop Tyre Change Required',
|
||||||
|
isMandatoryPitstopSwapDriverRequired: 'Mandatory Pitstop Swap Driver Required',
|
||||||
|
tyreSetCount: 'Tyre Set Count',
|
||||||
|
// settings.json
|
||||||
|
serverName: 'Server Name',
|
||||||
|
adminPassword: 'Admin Password',
|
||||||
|
carGroup: 'Car Group',
|
||||||
|
trackMedalsRequirement: 'Track Medals Requirement',
|
||||||
|
safetyRatingRequirement: 'Safety Rating Requirement',
|
||||||
|
racecraftRatingRequirement: 'Racecraft Rating Requirement',
|
||||||
|
password: 'Password',
|
||||||
|
spectatorPassword: 'Spectator Password',
|
||||||
|
maxCarSlots: 'Max Car Slots',
|
||||||
|
dumpLeaderboards: 'Dump Leaderboards',
|
||||||
|
isRaceLocked: 'Race Locked',
|
||||||
|
randomizeTrackWhenEmpty: 'Randomize Track When Empty',
|
||||||
|
centralEntryListPath: 'Central Entry List Path',
|
||||||
|
allowAutoDQ: 'Allow Auto DQ',
|
||||||
|
shortFormationLap: 'Short Formation Lap',
|
||||||
|
dumpEntryList: 'Dump Entry List',
|
||||||
|
formationLapType: 'Formation Lap Type',
|
||||||
|
ignorePrematureDisconnects: 'Ignore Premature Disconnects'
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx';
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import { FIELD_LABELS, SECTION_LABELS } from '@/lib/schemas/config';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -10,5 +11,18 @@ export function boolToInt(val: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function intToBool(val: number) {
|
export function intToBool(val: number) {
|
||||||
return !!val
|
return !!val;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatZodIssues(issues: Array<{ path: PropertyKey[]; message: string }>): string {
|
||||||
|
return issues
|
||||||
|
.map((issue) => {
|
||||||
|
const path = issue.path.map(String);
|
||||||
|
const sectionKey = path.length > 1 ? path[0] : null;
|
||||||
|
const fieldKey = path.at(-1) ?? '';
|
||||||
|
const section = sectionKey && SECTION_LABELS[sectionKey] ? `[${SECTION_LABELS[sectionKey]}] ` : '';
|
||||||
|
const label = FIELD_LABELS[fieldKey] ?? fieldKey;
|
||||||
|
return `${section}${label}: ${issue.message}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user