website refactor

This commit is contained in:
2026-01-16 01:00:03 +01:00
parent ce7be39155
commit a98e3e3166
286 changed files with 5522 additions and 5261 deletions

View File

@@ -0,0 +1,93 @@
'use client';
import {
createContext,
useCallback,
useContext,
useMemo,
type ReactNode,
} from 'react';
import { useRouter } from 'next/navigation';
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
import { useCurrentSession } from "@/hooks/auth/useCurrentSession";
import { useLogout } from "@/hooks/auth/useLogout";
export type AuthContextValue = {
session: SessionViewModel | null;
loading: boolean;
login: (returnTo?: string) => void;
logout: () => Promise<void>;
refreshSession: () => Promise<void>;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
interface AuthProviderProps {
initialSession?: SessionViewModel | null;
children: ReactNode;
}
export function AuthProvider({ initialSession = null, children }: AuthProviderProps) {
const router = useRouter();
// Use React-Query hooks for session management
const { data: session, isLoading, refetch: refreshSession } = useCurrentSession({
initialData: initialSession,
});
// Use mutation hooks for logout
const logoutMutation = useLogout();
const login = useCallback(
(returnTo?: string) => {
const search = new URLSearchParams();
if (returnTo) {
search.set('returnTo', returnTo);
}
const target = search.toString()
? `/auth/login?${search.toString()}`
: '/auth/login';
router.push(target);
},
[router],
);
const logout = useCallback(async () => {
try {
await logoutMutation.mutateAsync();
router.push('/');
router.refresh();
} catch (error) {
console.error('Logout failed:', error);
router.push('/');
}
}, [logoutMutation, router]);
const handleRefreshSession = useCallback(async () => {
await refreshSession();
}, [refreshSession]);
const value = useMemo(
() => ({
session: session ?? null,
loading: isLoading,
login,
logout,
refreshSession: handleRefreshSession,
}),
[session, isLoading, login, logout, handleRefreshSession],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error('useAuth must be used within an AuthProvider');
}
return ctx;
}

View File

@@ -124,7 +124,7 @@ export function DebugModeToggle({ show }: DebugModeToggleProps) {
// Trigger a test API error
const testError = new Error('This is a test error for debugging');
(testError as any).type = 'TEST_ERROR';
(testError as Error & { type?: string }).type = 'TEST_ERROR';
const globalHandler = getGlobalErrorHandler();
globalHandler.report(testError, { test: true, timestamp: Date.now() });
@@ -138,7 +138,7 @@ export function DebugModeToggle({ show }: DebugModeToggleProps) {
try {
// This will fail and be logged
await fetch('https://httpstat.us/500');
} catch (error) {
} catch (_error) {
// Already logged by interceptor
console.log('%c[TEST] API call completed', 'color: #00aaff; font-weight: bold;');
}
@@ -173,7 +173,7 @@ export function DebugModeToggle({ show }: DebugModeToggleProps) {
},
errors: globalHandler.getStats(),
api: apiLogger.getStats(),
reactErrors: (window as any).__GRIDPILOT_REACT_ERRORS__ || [],
reactErrors: window.__GRIDPILOT_REACT_ERRORS__ || [],
};
try {
@@ -350,14 +350,14 @@ export function useDebugMode() {
globalHandler.initialize();
const apiLogger = getGlobalApiLogger();
if (!(window as any).__GRIDPILOT_FETCH_LOGGED__) {
if (!window.__GRIDPILOT_FETCH_LOGGED__) {
const loggedFetch = apiLogger.createLoggedFetch();
window.fetch = loggedFetch as any;
(window as any).__GRIDPILOT_FETCH_LOGGED__ = true;
window.fetch = loggedFetch as typeof fetch;
window.__GRIDPILOT_FETCH_LOGGED__ = true;
}
(window as any).__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
(window as any).__GRIDPILOT_API_LOGGER__ = apiLogger;
window.__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
window.__GRIDPILOT_API_LOGGER__ = apiLogger;
}, []);
const disable = useCallback(() => {

View File

@@ -1,4 +1,4 @@
import Link from 'next/link';
import { Link } from '@/ui/Link';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
@@ -35,7 +35,8 @@ export function DriverIdentity(props: DriverIdentityProps) {
alignItems="center"
justifyContent="center"
flexShrink={0}
style={{ width: avatarSize, height: avatarSize }}
w={`${avatarSize}px`}
h={`${avatarSize}px`}
>
{avatarUrl ? (
<Image
@@ -54,17 +55,17 @@ export function DriverIdentity(props: DriverIdentityProps) {
<Box flexGrow={1} minWidth="0">
<Box display="flex" alignItems="center" gap={2} minWidth="0">
<Text size={nameSize} weight="medium" color="text-white" className="truncate">
<Text size={nameSize} weight="medium" color="text-white" truncate>
{driver.name}
</Text>
{contextLabel && (
<Badge variant="default" className="bg-charcoal-outline/60 text-[10px] md:text-xs">
<Badge variant="default" bg="bg-charcoal-outline/60" size="xs">
{contextLabel}
</Badge>
)}
</Box>
{meta && (
<Text size="xs" color="text-gray-400" mt={0.5} className="truncate" block>
<Text size="xs" color="text-gray-400" mt={0.5} truncate block>
{meta}
</Text>
)}
@@ -74,7 +75,7 @@ export function DriverIdentity(props: DriverIdentityProps) {
if (href) {
return (
<Link href={href} className="flex items-center gap-3 md:gap-4 flex-1 min-w-0">
<Link href={href} block variant="ghost">
{content}
</Link>
);

View File

@@ -2,7 +2,6 @@
import React from 'react';
import { Check } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';

View File

@@ -5,7 +5,7 @@ import { ActivityFeedItem } from '@/components/feed/ActivityFeedItem';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { processLeagueActivities } from '@/lib/services/league/LeagueActivityService';
import { LeagueActivityService } from '@/lib/services/league/LeagueActivityService';
export type LeagueActivity =
| { type: 'race_completed'; raceId: string; raceName: string; timestamp: Date }
@@ -37,8 +37,10 @@ export function LeagueActivityFeed({ leagueId, limit = 10 }: LeagueActivityFeedP
const { data: raceList = [], isLoading } = useLeagueRaces(leagueId);
const activities = useMemo(() => {
if (isLoading || raceList.length === 0) return [];
return processLeagueActivities(raceList, limit);
if (isLoading || !Array.isArray(raceList) || raceList.length === 0) return [];
const service = new LeagueActivityService();
const result = service.processLeagueActivities(raceList, limit);
return result.isOk() ? result.unwrap() : [];
}, [raceList, isLoading, limit]);
if (isLoading) {

View File

@@ -24,7 +24,6 @@ interface LeagueMemberRowProps {
export function LeagueMemberRow({
driver,
driverId,
isCurrentUser,
isTopPerformer,
role,

View File

@@ -44,16 +44,17 @@ export function LeagueMembers({
const membershipData = leagueMembershipService.getLeagueMembers(leagueId);
setMembers(membershipData);
const uniqueDriverIds = Array.from(new Set(membershipData.map((m) => m.driverId)));
const uniqueDriverIds = Array.from(new Set(membershipData.map((m: LeagueMembership) => m.driverId)));
if (uniqueDriverIds.length > 0) {
const driverDtos = await driverService.findByIds(uniqueDriverIds);
const byId: Record<string, DriverViewModel> = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const dto of driverDtos as any[]) {
byId[dto.id] = new DriverViewModel({ ...dto, avatarUrl: dto.avatarUrl ?? null });
const result = await driverService.findByIds(uniqueDriverIds);
if (result.isOk()) {
const driverDtos = result.unwrap();
const byId: Record<string, DriverViewModel> = {};
for (const dto of driverDtos) {
byId[dto.id] = new DriverViewModel({ ...dto, avatarUrl: dto.avatarUrl ?? null });
}
setDriversById(byId);
}
setDriversById(byId);
} else {
setDriversById({});
}

View File

@@ -50,8 +50,9 @@ export function LeagueSponsorshipsSection({
const [tempPrice, setTempPrice] = useState<string>('');
// Load season ID if not provided
const { data: seasons = [] } = useLeagueSeasons(leagueId);
const activeSeason = seasons.find((s) => s.status === 'active') ?? seasons[0];
const { data: seasonsResult } = useLeagueSeasons(leagueId);
const seasons = seasonsResult?.isOk() ? seasonsResult.unwrap() : [];
const activeSeason = seasons.find((s: any) => s.status === 'active') ?? seasons[0];
const seasonId = propSeasonId || activeSeason?.seasonId;
// Load pending sponsorship requests

View File

@@ -2,7 +2,6 @@
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
import { Box } from "@/ui/Box";
import { Card } from "@/ui/Card";
import { ProtestListItem } from "./ProtestListItem";
@@ -12,17 +11,13 @@ import { Flag } from "lucide-react";
interface PendingProtestsListProps {
protests: ProtestViewModel[];
races: Record<string, RaceViewModel>;
drivers: Record<string, DriverViewModel>;
leagueId: string;
onReviewProtest: (protest: ProtestViewModel) => void;
onProtestReviewed: () => void;
}
export function PendingProtestsList({
protests,
drivers,
leagueId,
onReviewProtest,
}: PendingProtestsListProps) {

View File

@@ -57,10 +57,10 @@ export function ReviewProtestModal({
const [submitting, setSubmitting] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const { data: penaltyTypesReference, isLoading: penaltyTypesLoading } = usePenaltyTypesReference();
const { data: penaltyTypesReferenceResult, isLoading: penaltyTypesLoading } = usePenaltyTypesReference();
const penaltyOptions = useMemo(() => {
const refs = penaltyTypesReference?.penaltyTypes ?? [];
const refs = penaltyTypesReferenceResult?.isOk() ? penaltyTypesReferenceResult.unwrap().penaltyTypes : [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return refs.map((ref: any) => ({
type: ref.type as PenaltyType,
@@ -71,7 +71,7 @@ export function ReviewProtestModal({
Icon: getPenaltyIcon(ref.type),
colorClass: getPenaltyColor(ref.type),
}));
}, [penaltyTypesReference]);
}, [penaltyTypesReferenceResult]);
const selectedPenalty = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -99,7 +99,7 @@ export function ReviewProtestModal({
}
};
const getPenaltyIcon = (type: PenaltyType) => {
function getPenaltyIcon(type: PenaltyType) {
switch (type) {
case "time_penalty":
return Clock;
@@ -122,9 +122,9 @@ export function ReviewProtestModal({
default:
return AlertCircle;
}
};
}
const getPenaltyName = (type: PenaltyType) => {
function getPenaltyName(type: PenaltyType) {
switch (type) {
case "time_penalty":
return "Time Penalty";
@@ -147,9 +147,9 @@ export function ReviewProtestModal({
default:
return type.replaceAll("_", " ");
}
};
}
const getPenaltyValueLabel = (valueKind: PenaltyValueKindDTO): string => {
function getPenaltyValueLabel(valueKind: PenaltyValueKindDTO): string {
switch (valueKind) {
case "seconds":
return "seconds";
@@ -162,9 +162,9 @@ export function ReviewProtestModal({
case "none":
return "";
}
};
}
const getPenaltyDefaultValue = (type: PenaltyType, valueKind: PenaltyValueKindDTO): number => {
function getPenaltyDefaultValue(type: PenaltyType, valueKind: PenaltyValueKindDTO): number {
if (type === "license_points") return 2;
if (type === "race_ban") return 1;
switch (valueKind) {
@@ -179,9 +179,9 @@ export function ReviewProtestModal({
case "none":
return 0;
}
};
}
const getPenaltyColor = (type: PenaltyType) => {
function getPenaltyColor(type: PenaltyType) {
switch (type) {
case "time_penalty":
return "text-blue-400 bg-blue-500/10 border-blue-500/30";
@@ -204,7 +204,7 @@ export function ReviewProtestModal({
default:
return "text-warning-amber bg-warning-amber/10 border-warning-amber/30";
}
};
}
if (showConfirmation) {
return (
@@ -351,7 +351,6 @@ export function ReviewProtestModal({
</Box>
)}
</Stack>
<Box borderTop borderColor="border-gray-800" pt={6}>
<Stack gap={4}>
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">Stewarding Decision</Heading>
@@ -471,4 +470,4 @@ export function ReviewProtestModal({
</Stack>
</Modal>
);
}
}