move static data
This commit is contained in:
@@ -22,9 +22,10 @@ import {
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FormEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel';
|
||||
|
||||
import { useCreateLeagueWizard } from '@/hooks/useLeagueWizardService';
|
||||
import { useLeagueScoringPresets } from '@/hooks/useLeagueScoringPresets';
|
||||
import { LeagueBasicsSection } from './LeagueBasicsSection';
|
||||
@@ -494,78 +495,29 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for scoring preset selection - delegates to application-level config helper
|
||||
// Handler for scoring preset selection (timings default from API)
|
||||
const handleScoringPresetChange = (patternId: string) => {
|
||||
setForm((prev) => {
|
||||
// Convert to LeagueWizardFormData for the command model
|
||||
const formData: any = {
|
||||
leagueId: prev.leagueId || '',
|
||||
basics: {
|
||||
name: prev.basics?.name || '',
|
||||
description: prev.basics?.description || '',
|
||||
visibility: (prev.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public',
|
||||
gameId: prev.basics?.gameId || 'iracing',
|
||||
},
|
||||
structure: {
|
||||
mode: (prev.structure?.mode as 'solo' | 'fixedTeams') || 'solo',
|
||||
maxDrivers: prev.structure?.maxDrivers || 24,
|
||||
maxTeams: prev.structure?.maxTeams || 0,
|
||||
driversPerTeam: prev.structure?.driversPerTeam || 0,
|
||||
},
|
||||
championships: {
|
||||
enableDriverChampionship: prev.championships?.enableDriverChampionship ?? true,
|
||||
enableTeamChampionship: prev.championships?.enableTeamChampionship ?? false,
|
||||
enableNationsChampionship: prev.championships?.enableNationsChampionship ?? false,
|
||||
enableTrophyChampionship: prev.championships?.enableTrophyChampionship ?? false,
|
||||
},
|
||||
scoring: {
|
||||
patternId: prev.scoring?.patternId,
|
||||
customScoringEnabled: prev.scoring?.customScoringEnabled ?? false,
|
||||
},
|
||||
dropPolicy: {
|
||||
strategy: (prev.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults',
|
||||
n: prev.dropPolicy?.n || 6,
|
||||
},
|
||||
timings: {
|
||||
practiceMinutes: prev.timings?.practiceMinutes || 0,
|
||||
qualifyingMinutes: prev.timings?.qualifyingMinutes || 0,
|
||||
sprintRaceMinutes: prev.timings?.sprintRaceMinutes || 0,
|
||||
mainRaceMinutes: prev.timings?.mainRaceMinutes || 0,
|
||||
sessionCount: prev.timings?.sessionCount || 0,
|
||||
roundsPlanned: prev.timings?.roundsPlanned || 0,
|
||||
raceDayOfWeek: prev.timings?.raceDayOfWeek || 0,
|
||||
raceTimeUtc: prev.timings?.raceTimeUtc || '',
|
||||
weekdays: (prev.timings?.weekdays as Weekday[]) || [],
|
||||
recurrenceStrategy: prev.timings?.recurrenceStrategy || '',
|
||||
timezoneId: prev.timings?.timezoneId || '',
|
||||
seasonStartDate: prev.timings?.seasonStartDate || '',
|
||||
},
|
||||
stewarding: {
|
||||
decisionMode: (prev.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only',
|
||||
requiredVotes: prev.stewarding?.requiredVotes || 2,
|
||||
requireDefense: prev.stewarding?.requireDefense ?? false,
|
||||
defenseTimeLimit: prev.stewarding?.defenseTimeLimit || 48,
|
||||
voteTimeLimit: prev.stewarding?.voteTimeLimit || 72,
|
||||
protestDeadlineHours: prev.stewarding?.protestDeadlineHours || 48,
|
||||
stewardingClosesHours: prev.stewarding?.stewardingClosesHours || 168,
|
||||
notifyAccusedOnProtest: prev.stewarding?.notifyAccusedOnProtest ?? true,
|
||||
notifyOnVoteRequired: prev.stewarding?.notifyOnVoteRequired ?? true,
|
||||
},
|
||||
};
|
||||
const selectedPreset = presets.find((p) => p.id === patternId);
|
||||
|
||||
const updated = LeagueWizardCommandModel.applyScoringPresetToConfig(formData, patternId);
|
||||
|
||||
// Convert back to LeagueWizardFormModel
|
||||
return {
|
||||
basics: updated.basics,
|
||||
structure: updated.structure,
|
||||
championships: updated.championships,
|
||||
scoring: updated.scoring,
|
||||
dropPolicy: updated.dropPolicy,
|
||||
timings: updated.timings,
|
||||
stewarding: updated.stewarding,
|
||||
seasonName: prev.seasonName,
|
||||
} as LeagueWizardFormModel;
|
||||
...prev,
|
||||
scoring: {
|
||||
...prev.scoring,
|
||||
patternId,
|
||||
customScoringEnabled: false,
|
||||
},
|
||||
timings: selectedPreset
|
||||
? {
|
||||
...prev.timings,
|
||||
practiceMinutes: prev.timings?.practiceMinutes ?? selectedPreset.defaultTimings.practiceMinutes,
|
||||
qualifyingMinutes: prev.timings?.qualifyingMinutes ?? selectedPreset.defaultTimings.qualifyingMinutes,
|
||||
sprintRaceMinutes: prev.timings?.sprintRaceMinutes ?? selectedPreset.defaultTimings.sprintRaceMinutes,
|
||||
mainRaceMinutes: prev.timings?.mainRaceMinutes ?? selectedPreset.defaultTimings.mainRaceMinutes,
|
||||
sessionCount: selectedPreset.defaultTimings.sessionCount,
|
||||
}
|
||||
: prev.timings,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ import Card from "../ui/Card";
|
||||
import Button from "../ui/Button";
|
||||
import { Clock, Grid3x3, TrendingDown, AlertCircle, Filter, Flag } from "lucide-react";
|
||||
|
||||
type PenaltyType = "time_penalty" | "grid_penalty" | "points_deduction" | "disqualification" | "warning" | "license_points";
|
||||
|
||||
interface PenaltyHistoryListProps {
|
||||
protests: ProtestViewModel[];
|
||||
races: Record<string, RaceViewModel>;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { usePenaltyTypesReference } from "@/hooks/usePenaltyTypesReference";
|
||||
import type { PenaltyValueKindDTO } from "@/lib/types/PenaltyTypesReferenceDTO";
|
||||
import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
|
||||
import Modal from "../ui/Modal";
|
||||
import Button from "../ui/Button";
|
||||
@@ -21,7 +23,7 @@ import {
|
||||
FileWarning,
|
||||
} from "lucide-react";
|
||||
|
||||
type PenaltyType = "time_penalty" | "grid_penalty" | "points_deduction" | "disqualification" | "warning" | "license_points" | "probation" | "fine" | "race_ban";
|
||||
type PenaltyType = string;
|
||||
|
||||
interface ReviewProtestModalProps {
|
||||
protest: ProtestViewModel | null;
|
||||
@@ -94,25 +96,63 @@ export function ReviewProtestModal({
|
||||
}
|
||||
};
|
||||
|
||||
const getPenaltyLabel = (type: PenaltyType) => {
|
||||
const getPenaltyName = (type: PenaltyType) => {
|
||||
switch (type) {
|
||||
case "time_penalty":
|
||||
return "seconds";
|
||||
return "Time Penalty";
|
||||
case "grid_penalty":
|
||||
return "grid positions";
|
||||
return "Grid Penalty";
|
||||
case "points_deduction":
|
||||
return "points";
|
||||
return "Points Deduction";
|
||||
case "disqualification":
|
||||
return "Disqualification";
|
||||
case "warning":
|
||||
return "Warning";
|
||||
case "license_points":
|
||||
return "points";
|
||||
return "License Points";
|
||||
case "probation":
|
||||
return "Probation";
|
||||
case "fine":
|
||||
return "points";
|
||||
return "Fine";
|
||||
case "race_ban":
|
||||
return "races";
|
||||
return "Race Ban";
|
||||
default:
|
||||
return type.replaceAll("_", " ");
|
||||
}
|
||||
};
|
||||
|
||||
const getPenaltyValueLabel = (valueKind: PenaltyValueKindDTO): string => {
|
||||
switch (valueKind) {
|
||||
case "seconds":
|
||||
return "seconds";
|
||||
case "grid_positions":
|
||||
return "grid positions";
|
||||
case "points":
|
||||
return "points";
|
||||
case "races":
|
||||
return "races";
|
||||
case "none":
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getPenaltyDefaultValue = (type: PenaltyType, valueKind: PenaltyValueKindDTO): number => {
|
||||
if (type === "license_points") return 2;
|
||||
if (type === "race_ban") return 1;
|
||||
switch (valueKind) {
|
||||
case "seconds":
|
||||
return 5;
|
||||
case "grid_positions":
|
||||
return 3;
|
||||
case "points":
|
||||
return 5;
|
||||
case "races":
|
||||
return 1;
|
||||
case "none":
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const getPenaltyColor = (type: PenaltyType) => {
|
||||
switch (type) {
|
||||
case "time_penalty":
|
||||
@@ -138,6 +178,25 @@ export function ReviewProtestModal({
|
||||
}
|
||||
};
|
||||
|
||||
const { data: penaltyTypesReference, isLoading: penaltyTypesLoading } = usePenaltyTypesReference();
|
||||
|
||||
const penaltyOptions = useMemo(() => {
|
||||
const refs = penaltyTypesReference?.penaltyTypes ?? [];
|
||||
return refs.map((ref) => ({
|
||||
type: ref.type as PenaltyType,
|
||||
name: getPenaltyName(ref.type),
|
||||
requiresValue: ref.requiresValue,
|
||||
valueLabel: getPenaltyValueLabel(ref.valueKind),
|
||||
defaultValue: getPenaltyDefaultValue(ref.type, ref.valueKind),
|
||||
Icon: getPenaltyIcon(ref.type),
|
||||
colorClass: getPenaltyColor(ref.type),
|
||||
}));
|
||||
}, [penaltyTypesReference]);
|
||||
|
||||
const selectedPenalty = useMemo(() => {
|
||||
return penaltyOptions.find((p) => p.type === penaltyType);
|
||||
}, [penaltyOptions, penaltyType]);
|
||||
|
||||
if (showConfirmation) {
|
||||
return (
|
||||
<Modal title="Confirm Decision" isOpen={true} onOpenChange={() => setShowConfirmation(false)}>
|
||||
@@ -160,7 +219,9 @@ export function ReviewProtestModal({
|
||||
<h3 className="text-xl font-bold text-white">Confirm Decision</h3>
|
||||
<p className="text-gray-400 mt-2">
|
||||
{decision === "accept"
|
||||
? `Issue ${penaltyValue} ${getPenaltyLabel(penaltyType)} penalty?`
|
||||
? (selectedPenalty?.requiresValue
|
||||
? `Issue ${penaltyValue} ${selectedPenalty.valueLabel} penalty?`
|
||||
: `Issue ${selectedPenalty?.name ?? penaltyType} penalty?`)
|
||||
: "Reject this protest?"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -300,43 +361,39 @@ export function ReviewProtestModal({
|
||||
<label className="text-sm font-medium text-gray-400 mb-2 block">
|
||||
Penalty Type
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ type: "time_penalty" as PenaltyType, label: "Time Penalty" },
|
||||
{ type: "grid_penalty" as PenaltyType, label: "Grid Penalty" },
|
||||
{ type: "points_deduction" as PenaltyType, label: "Points Deduction" },
|
||||
{ type: "disqualification" as PenaltyType, label: "Disqualification" },
|
||||
{ type: "warning" as PenaltyType, label: "Warning" },
|
||||
{ type: "license_points" as PenaltyType, label: "License Points" },
|
||||
{ type: "probation" as PenaltyType, label: "Probation" },
|
||||
{ type: "fine" as PenaltyType, label: "Fine" },
|
||||
{ type: "race_ban" as PenaltyType, label: "Race Ban" },
|
||||
].map(({ type, label }) => {
|
||||
const Icon = getPenaltyIcon(type);
|
||||
const colorClass = getPenaltyColor(type);
|
||||
const isSelected = penaltyType === type;
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setPenaltyType(type)}
|
||||
className={`p-3 rounded-lg border transition-all ${
|
||||
isSelected
|
||||
? `${colorClass} border-2`
|
||||
: "border-charcoal-outline hover:border-gray-600 bg-iron-gray/50"
|
||||
}`}
|
||||
>
|
||||
<Icon className={`h-5 w-5 mx-auto mb-1 ${isSelected ? '' : 'text-gray-400'}`} />
|
||||
<p className={`text-xs font-medium ${isSelected ? '' : 'text-gray-400'}`}>{label}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{penaltyTypesLoading ? (
|
||||
<div className="text-sm text-gray-500">Loading penalty types…</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{penaltyOptions.map(({ type, name, Icon, colorClass, defaultValue }) => {
|
||||
const isSelected = penaltyType === type;
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => {
|
||||
setPenaltyType(type);
|
||||
setPenaltyValue(defaultValue);
|
||||
}}
|
||||
className={`p-3 rounded-lg border transition-all ${
|
||||
isSelected
|
||||
? `${colorClass} border-2`
|
||||
: "border-charcoal-outline hover:border-gray-600 bg-iron-gray/50"
|
||||
}`}
|
||||
>
|
||||
<Icon className={`h-5 w-5 mx-auto mb-1 ${isSelected ? "" : "text-gray-400"}`} />
|
||||
<p className={`text-xs font-medium ${isSelected ? "" : "text-gray-400"}`}>{name}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{['time_penalty', 'grid_penalty', 'points_deduction', 'license_points', 'fine', 'race_ban'].includes(penaltyType) && (
|
||||
{selectedPenalty?.requiresValue && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-400 mb-2 block">
|
||||
Penalty Value ({getPenaltyLabel(penaltyType)})
|
||||
Penalty Value ({selectedPenalty.valueLabel})
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
|
||||
import { BarChart3, Building2, ChevronDown, CreditCard, Handshake, LogOut, Megaphone, Paintbrush, Settings, TrendingUp, Trophy } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import DriverSummaryPill from '@/components/profile/DriverSummaryPill';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`UserPill > renders auth links when there is no session 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<a
|
||||
class="inline-flex items-center gap-2 rounded-full bg-iron-gray border border-charcoal-outline px-4 py-1.5 text-xs font-medium text-gray-300 hover:text-white hover:border-gray-500 transition-all"
|
||||
href="/auth/login"
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
<a
|
||||
class="inline-flex items-center gap-2 rounded-full bg-primary-blue px-4 py-1.5 text-xs font-semibold text-white shadow-[0_0_12px_rgba(25,140,255,0.5)] hover:bg-primary-blue/90 hover:shadow-[0_0_18px_rgba(25,140,255,0.8)] transition-all"
|
||||
href="/auth/signup"
|
||||
>
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -172,22 +172,26 @@ export default function SponsorWorkflowMockup() {
|
||||
transition={{ duration: 0.3 }}
|
||||
className="mt-8 pt-6 border-t border-charcoal-outline"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg bg-iron-gray flex items-center justify-center`}>
|
||||
{(() => {
|
||||
const Icon = WORKFLOW_STEPS[activeStep].icon;
|
||||
return <Icon className={`w-4 h-4 ${WORKFLOW_STEPS[activeStep].color}`} />;
|
||||
})()}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-sm text-gray-400">
|
||||
Step {activeStep + 1} of {WORKFLOW_STEPS.length}
|
||||
</p>
|
||||
<p className="text-white font-medium">
|
||||
{WORKFLOW_STEPS[activeStep].title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
const currentStep = WORKFLOW_STEPS[activeStep] ?? WORKFLOW_STEPS[0];
|
||||
if (!currentStep) return null;
|
||||
|
||||
const Icon = currentStep.icon;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-iron-gray flex items-center justify-center">
|
||||
<Icon className={`w-4 h-4 ${currentStep.color}`} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-sm text-gray-400">
|
||||
Step {activeStep + 1} of {WORKFLOW_STEPS.length}
|
||||
</p>
|
||||
<p className="text-white font-medium">{currentStep.title}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user