introduce zod schemas

This commit is contained in:
Fran Jurmanović
2025-09-24 20:07:57 +02:00
parent 0cbc6935db
commit 996f1a1385
43 changed files with 798 additions and 559 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import type { AssistRules } from '@/lib/types/config';
import type { AssistRules } from '@/lib/schemas/config';
import { updateAssistRulesAction } from '@/lib/actions/configuration';
interface AssistRulesEditorProps {
@@ -9,6 +9,54 @@ interface AssistRulesEditorProps {
config: AssistRules;
}
const assistFields = [
{
key: 'stabilityControlLevelMax' as keyof AssistRules,
label: 'Stability Control Level Max',
type: 'number'
},
{
key: 'disableAutosteer' as keyof AssistRules,
label: 'Disable Autosteer',
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({ serverId, config }: AssistRulesEditorProps) {
const [formData, setFormData] = useState<AssistRules>(config);
const [restart, setRestart] = useState(true);
@@ -43,54 +91,6 @@ export function AssistRulesEditor({ serverId, config }: AssistRulesEditorProps)
}));
};
const assistFields = [
{
key: 'stabilityControlLevelMax' as keyof AssistRules,
label: 'Stability Control Level Max',
type: 'number'
},
{
key: 'disableAutosteer' as keyof AssistRules,
label: 'Disable Autosteer',
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'
}
];
return (
<form onSubmit={handleSubmit} className="max-w-3xl space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import type { Configuration } from '@/lib/types/config';
import type { Configuration } from '@/lib/schemas/config';
import { updateConfigurationAction } from '@/lib/actions/configuration';
interface ConfigurationEditorProps {

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import type { EventConfig, Session } from '@/lib/types/config';
import type { EventConfig, Session } from '@/lib/schemas/config';
import { updateEventConfigAction } from '@/lib/actions/configuration';
interface EventConfigEditorProps {
@@ -9,6 +9,12 @@ interface EventConfigEditorProps {
config: EventConfig;
}
const sessionTypes = [
{ value: 'P', label: 'Practice' },
{ value: 'Q', label: 'Qualifying' },
{ value: 'R', label: 'Race' }
];
export function EventConfigEditor({ serverId, config }: EventConfigEditorProps) {
const [formData, setFormData] = useState<EventConfig>(config);
const [restart, setRestart] = useState(true);
@@ -90,12 +96,6 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
}));
};
const sessionTypes = [
{ value: 'P', label: 'Practice' },
{ value: 'Q', label: 'Qualifying' },
{ value: 'R', label: 'Race' }
];
return (
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
<div className="space-y-6">

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import type { EventRules } from '@/lib/types/config';
import type { EventRules } from '@/lib/schemas/config';
import { updateEventRulesAction } from '@/lib/actions/configuration';
interface EventRulesEditorProps {
@@ -9,6 +9,65 @@ interface EventRulesEditorProps {
config: EventRules;
}
const numberFields = [
{
key: 'qualifyStandingType' as keyof EventRules,
label: 'Qualify Standing Type',
min: -1,
max: 1
},
{
key: 'pitWindowLengthSec' as keyof EventRules,
label: 'Pit Window Length (seconds)',
min: -1
},
{
key: 'driverStintTimeSec' as keyof EventRules,
label: 'Driver Stint Time (seconds)',
min: -1
},
{
key: 'mandatoryPitstopCount' as keyof EventRules,
label: 'Mandatory Pitstop Count',
min: -1,
max: 5
},
{
key: 'maxTotalDrivingTime' as keyof EventRules,
label: 'Max Total Driving Time (seconds)',
min: -1
},
{
key: 'tyreSetCount' as keyof EventRules,
label: 'Tyre Set Count',
min: 0,
max: 50
}
];
const booleanFields = [
{
key: 'isRefuellingAllowedInRace' as keyof EventRules,
label: 'Refuelling Allowed in Race'
},
{
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({ serverId, config }: EventRulesEditorProps) {
const [formData, setFormData] = useState<EventRules>(config);
const [restart, setRestart] = useState(true);
@@ -43,65 +102,6 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
}));
};
const numberFields = [
{
key: 'qualifyStandingType' as keyof EventRules,
label: 'Qualify Standing Type',
min: -1,
max: 1
},
{
key: 'pitWindowLengthSec' as keyof EventRules,
label: 'Pit Window Length (seconds)',
min: -1
},
{
key: 'driverStintTimeSec' as keyof EventRules,
label: 'Driver Stint Time (seconds)',
min: -1
},
{
key: 'mandatoryPitstopCount' as keyof EventRules,
label: 'Mandatory Pitstop Count',
min: -1,
max: 5
},
{
key: 'maxTotalDrivingTime' as keyof EventRules,
label: 'Max Total Driving Time (seconds)',
min: -1
},
{
key: 'tyreSetCount' as keyof EventRules,
label: 'Tyre Set Count',
min: 0,
max: 50
}
];
const booleanFields = [
{
key: 'isRefuellingAllowedInRace' as keyof EventRules,
label: 'Refuelling Allowed in Race'
},
{
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'
}
];
return (
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
<div className="space-y-6">

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import type { ServerSettings } from '@/lib/types/config';
import type { ServerSettings } from '@/lib/schemas/config';
import { updateServerSettingsAction } from '@/lib/actions/configuration';
interface ServerSettingsEditorProps {
@@ -9,6 +9,84 @@ interface ServerSettingsEditorProps {
config: ServerSettings;
}
const textFields = [
{
key: 'serverName' as keyof ServerSettings,
label: 'Server Name',
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 numberFields = [
{
key: 'trackMedalsRequirement' as keyof ServerSettings,
label: 'Track Medals Requirement',
min: -1,
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 = [
{
key: 'dumpLeaderboards' as keyof ServerSettings,
label: 'Dump Leaderboards'
},
{ key: 'isRaceLocked' as keyof ServerSettings, label: 'Race Locked' },
{
key: 'randomizeTrackWhenEmpty' as keyof ServerSettings,
label: 'Randomize Track When Empty'
},
{ key: 'allowAutoDQ' as keyof ServerSettings, label: 'Allow Auto DQ' },
{
key: 'shortFormationLap' as keyof ServerSettings,
label: 'Short Formation Lap'
},
{
key: 'ignorePrematureDisconnects' as keyof ServerSettings,
label: 'Ignore Premature Disconnects'
}
];
export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorProps) {
const [formData, setFormData] = useState<ServerSettings>(config);
const [restart, setRestart] = useState(true);
@@ -43,84 +121,6 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
}));
};
const textFields = [
{
key: 'serverName' as keyof ServerSettings,
label: 'Server Name',
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 numberFields = [
{
key: 'trackMedalsRequirement' as keyof ServerSettings,
label: 'Track Medals Requirement',
min: -1,
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 = [
{
key: 'dumpLeaderboards' as keyof ServerSettings,
label: 'Dump Leaderboards'
},
{ key: 'isRaceLocked' as keyof ServerSettings, label: 'Race Locked' },
{
key: 'randomizeTrackWhenEmpty' as keyof ServerSettings,
label: 'Randomize Track When Empty'
},
{ key: 'allowAutoDQ' as keyof ServerSettings, label: 'Allow Auto DQ' },
{
key: 'shortFormationLap' as keyof ServerSettings,
label: 'Short Formation Lap'
},
{
key: 'ignorePrematureDisconnects' as keyof ServerSettings,
label: 'Ignore Premature Disconnects'
}
];
return (
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
<div className="space-y-6">

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import type { Role } from '@/lib/types';
import type { Role } from '@/lib/schemas';
import { createUserAction } from '@/lib/actions/membership';
interface CreateUserModalProps {

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import type { User } from '@/lib/types';
import type { User } from '@/lib/schemas';
import { deleteUserAction } from '@/lib/actions/membership';
interface DeleteUserModalProps {

View File

@@ -3,8 +3,8 @@
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import type { Role, User } from '@/lib/types';
import { hasPermission } from '@/lib/types/user';
import type { Role, User } from '@/lib/schemas';
import { hasPermission } from '@/lib/schemas/user';
import { CreateUserModal } from './CreateUserModal';
import { DeleteUserModal } from './DeleteUserModal';

View File

@@ -4,7 +4,7 @@ import { useState, useTransition } from 'react';
import { Modal } from '@/components/ui/Modal';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { deleteServerAction } from '@/lib/actions/server-management';
import { Server } from '@/lib/types/server';
import { Server } from '@/lib/schemas/server';
interface DeleteServerModalProps {
isOpen: boolean;

View File

@@ -1,6 +1,6 @@
import Link from 'next/link';
import { useTransition } from 'react';
import { Server, ServiceStatus, getStatusColor, serviceStatusToString } from '@/lib/types';
import { Server, ServiceStatus, getStatusColor, serviceStatusToString } from '@/lib/schemas';
import {
startServerEventAction,
restartServerEventAction,

View File

@@ -1,6 +1,6 @@
'use client';
import { Configurations, ServerTab } from '@/lib/types/config';
import { Configurations, ServerTab } from '@/lib/schemas/config';
import { ConfigurationEditor } from '@/components/configuration/ConfigurationEditor';
import { AssistRulesEditor } from '@/components/configuration/AssistRulesEditor';
import { EventConfigEditor } from '@/components/configuration/EventConfigEditor';
@@ -8,13 +8,21 @@ import { EventRulesEditor } from '@/components/configuration/EventRulesEditor';
import { ServerSettingsEditor } from '@/components/configuration/ServerSettingsEditor';
import { StatisticsDashboard } from '@/components/statistics/StatisticsDashboard';
import { useState } from 'react';
import { StateHistoryStats } from '@/lib/types';
import { StateHistoryStats } from '@/lib/schemas';
interface ServerConfigurationTabsProps {
serverId: string;
configurations: Configurations;
statistics: StateHistoryStats;
}
const tabs = [
{ id: ServerTab.statistics, name: 'Statistics', icon: '📊' },
{ id: ServerTab.configuration, name: 'Configuration', icon: '⚙️' },
{ id: ServerTab.assistRules, name: 'Assist Rules', icon: '🚗' },
{ id: ServerTab.event, name: 'Event Config', icon: '🏁' },
{ id: ServerTab.eventRules, name: 'Event Rules', icon: '📋' },
{ id: ServerTab.settings, name: 'Server Settings', icon: '🔧' }
];
export function ServerConfigurationTabs({
serverId,
@@ -22,14 +30,6 @@ export function ServerConfigurationTabs({
statistics
}: ServerConfigurationTabsProps) {
const [currentTab, setCurrentTab] = useState(ServerTab.statistics);
const tabs = [
{ id: ServerTab.statistics, name: 'Statistics', icon: '📊' },
{ id: ServerTab.configuration, name: 'Configuration', icon: '⚙️' },
{ id: ServerTab.assistRules, name: 'Assist Rules', icon: '🚗' },
{ id: ServerTab.event, name: 'Event Config', icon: '🏁' },
{ id: ServerTab.eventRules, name: 'Event Rules', icon: '📋' },
{ id: ServerTab.settings, name: 'Server Settings', icon: '🔧' }
];
const renderTabContent = () => {
switch (currentTab) {

View File

@@ -2,13 +2,13 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useWebSocket } from '@/lib/websocket/context';
import {
import type {
WebSocketMessage,
StepData,
SteamOutputData,
ErrorData,
CompleteData
} from '@/lib/websocket/client';
} from '@/lib/schemas/websocket';
interface ServerCreationPopupProps {
serverId: string;

View File

@@ -1,12 +1,12 @@
'use client';
import Link from 'next/link';
import { Server, getStatusColor, serviceStatusToString, ServiceStatus } from '@/lib/types/server';
import { Server, getStatusColor, serviceStatusToString, ServiceStatus } from '@/lib/schemas/server';
import {
startServerEventAction,
restartServerEventAction,
stopServerEventAction
} from '@/lib/actions/servers';
import { hasPermission, User } from '@/lib/types';
import { hasPermission, User } from '@/lib/schemas';
import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { DeleteServerModal } from './DeleteServerModal';

View File

@@ -1,8 +1,8 @@
'use client';
import { useCallback, useState } from 'react';
import { Server } from '@/lib/types/server';
import { User, hasPermission } from '@/lib/types/user';
import { Server } from '@/lib/schemas/server';
import { User, hasPermission } from '@/lib/schemas/user';
import { ServerCard } from './ServerCard';
import { CreateServerModal } from './CreateServerModal';
import RefreshButton from '@/components/ui/RefreshButton';

View File

@@ -14,16 +14,16 @@ interface SessionTypesChartProps {
data: SessionCount[];
}
export function SessionTypesChart({ data }: SessionTypesChartProps) {
const colors = [
'#3b82f6', // blue
'#10b981', // emerald
'#f59e0b', // amber
'#ef4444', // red
'#8b5cf6', // violet
'#06b6d4' // cyan
];
const colors = [
'#3b82f6', // blue
'#10b981', // emerald
'#f59e0b', // amber
'#ef4444', // red
'#8b5cf6', // violet
'#06b6d4' // cyan
];
export function SessionTypesChart({ data }: SessionTypesChartProps) {
const chartData = {
labels: data.map((item) => item.name),
datasets: [

View File

@@ -1,6 +1,6 @@
'use client';
import type { StateHistoryStats } from '@/lib/types/statistics';
import type { StateHistoryStats } from '@/lib/schemas/statistics';
import { PlayerCountChart } from './PlayerCountChart';
import { SessionTypesChart } from './SessionTypesChart';
import { DailyActivityChart } from './DailyActivityChart';

View File

@@ -8,27 +8,27 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
}
const baseClasses =
'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-gray-300 hover:text-white hover:bg-gray-700 focus:ring-gray-500'
};
const sizes = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props },
ref
) => {
const baseClasses =
'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-gray-300 hover:text-white hover:bg-gray-700 focus:ring-gray-500'
};
const sizes = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
};
return (
<button
className={cn(baseClasses, variants[variant], sizes[size], className)}