website refactor
This commit is contained in:
93
apps/website/components/auth/AuthContext.tsx
Normal file
93
apps/website/components/auth/AuthContext.tsx
Normal 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;
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -24,7 +24,6 @@ interface LeagueMemberRowProps {
|
||||
|
||||
export function LeagueMemberRow({
|
||||
driver,
|
||||
driverId,
|
||||
isCurrentUser,
|
||||
isTopPerformer,
|
||||
role,
|
||||
|
||||
@@ -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({});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user