website refactor

This commit is contained in:
2026-01-14 23:46:04 +01:00
parent c1a86348d7
commit 4a2d7d15a5
294 changed files with 5637 additions and 3418 deletions

View File

@@ -1,7 +1,7 @@
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { createRouteGuard } from '@/lib/auth/createRouteGuard';
import Section from '@/components/ui/Section';
import Section from '@/ui/Section';
interface AdminLayoutProps {
children: React.ReactNode;

View File

@@ -1,6 +1,6 @@
import { AdminDashboardPageQuery } from '@/lib/page-queries/AdminDashboardPageQuery';
import { AdminDashboardWrapper } from '@/components/admin/AdminDashboardWrapper';
import { ErrorBanner } from '@/components/ui/ErrorBanner';
import { ErrorBanner } from '@/ui/ErrorBanner';
export default async function AdminPage() {
const result = await AdminDashboardPageQuery.execute();

View File

@@ -1,6 +1,6 @@
import { AdminUsersPageQuery } from '@/lib/page-queries/AdminUsersPageQuery';
import { AdminUsersWrapper } from '@/components/admin/AdminUsersWrapper';
import { ErrorBanner } from '@/components/ui/ErrorBanner';
import { ErrorBanner } from '@/ui/ErrorBanner';
export default async function AdminUsersPage() {
// Execute PageQuery using static method

View File

@@ -8,7 +8,7 @@
import { ForgotPasswordPageQuery } from '@/lib/page-queries/auth/ForgotPasswordPageQuery';
import { ForgotPasswordClient } from './ForgotPasswordClient';
import { AuthError } from '@/components/ui/AuthError';
import { AuthError } from '@/ui/AuthError';
export default async function ForgotPasswordPage({
searchParams,

View File

@@ -1,7 +1,7 @@
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { createRouteGuard } from '@/lib/auth/createRouteGuard';
import { AuthContainer } from '@/components/ui/AuthContainer';
import { AuthContainer } from '@/ui/AuthContainer';
interface AuthLayoutProps {
children: React.ReactNode;

View File

@@ -17,7 +17,7 @@ import { LoginMutation } from '@/lib/mutations/auth/LoginMutation';
import { validateLoginForm, type LoginFormValues } from '@/lib/utils/validation';
import { LoginViewModelBuilder } from '@/lib/builders/view-models/LoginViewModelBuilder';
import { LoginViewModel } from '@/lib/view-models/auth/LoginViewModel';
import { AuthLoading } from '@/components/ui/AuthLoading';
import { AuthLoading } from '@/ui/AuthLoading';
interface LoginClientProps {
viewData: LoginViewData;

View File

@@ -8,7 +8,7 @@
import { LoginPageQuery } from '@/lib/page-queries/auth/LoginPageQuery';
import { LoginClient } from './LoginClient';
import { AuthError } from '@/components/ui/AuthError';
import { AuthError } from '@/ui/AuthError';
export default async function LoginPage({
searchParams,

View File

@@ -8,7 +8,7 @@
import { ResetPasswordPageQuery } from '@/lib/page-queries/auth/ResetPasswordPageQuery';
import { ResetPasswordClient } from './ResetPasswordClient';
import { AuthError } from '@/components/ui/AuthError';
import { AuthError } from '@/ui/AuthError';
export default async function ResetPasswordPage({
searchParams,

View File

@@ -8,7 +8,7 @@
import { SignupPageQuery } from '@/lib/page-queries/auth/SignupPageQuery';
import { SignupClient } from './SignupClient';
import { AuthError } from '@/components/ui/AuthError';
import { AuthError } from '@/ui/AuthError';
export default async function SignupPage({
searchParams,

View File

@@ -0,0 +1,36 @@
'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import { DriversTemplate } from '@/templates/DriversTemplate';
import { useDriverSearch } from '@/lib/hooks/useDriverSearch';
import type { DriverLeaderboardViewModel } from '@/lib/view-data/DriverLeaderboardViewModel';
interface DriversPageClientProps {
data: DriverLeaderboardViewModel | null;
}
export function DriversPageClient({ data }: DriversPageClientProps) {
const router = useRouter();
const drivers = data?.drivers || [];
const { searchQuery, setSearchQuery, filteredDrivers } = useDriverSearch(drivers);
const handleDriverClick = (driverId: string) => {
router.push(`/drivers/${driverId}`);
};
const handleViewLeaderboard = () => {
router.push('/leaderboards/drivers');
};
return (
<DriversTemplate
data={data}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
filteredDrivers={filteredDrivers}
onDriverClick={handleDriverClick}
onViewLeaderboard={handleViewLeaderboard}
/>
);
}

View File

@@ -4,7 +4,7 @@ import { initializeApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
import { Metadata, Viewport } from 'next';
import React from 'react';
import './globals.css';
import { AppWrapper } from '@/ui/AppWrapper';
import { AppWrapper } from '@/components/AppWrapper';
import { Header } from '@/ui/Header';
import { HeaderContent } from '@/ui/HeaderContent';
import { MainContent } from '@/ui/MainContent';

View File

@@ -0,0 +1,41 @@
'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import { LeaderboardsTemplate } from '@/templates/LeaderboardsTemplate';
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
import { routes } from '@/lib/routing/RouteConfig';
interface LeaderboardsPageClientProps {
viewData: LeaderboardsViewData;
}
export function LeaderboardsPageClient({ viewData }: LeaderboardsPageClientProps) {
const router = useRouter();
const handleDriverClick = (driverId: string) => {
router.push(routes.driver.detail(driverId));
};
const handleTeamClick = (teamId: string) => {
router.push(routes.team.detail(teamId));
};
const handleNavigateToDrivers = () => {
router.push(routes.leaderboards.drivers);
};
const handleNavigateToTeams = () => {
router.push(routes.team.leaderboard);
};
return (
<LeaderboardsTemplate
viewData={viewData}
onDriverClick={handleDriverClick}
onTeamClick={handleTeamClick}
onNavigateToDrivers={handleNavigateToDrivers}
onNavigateToTeams={handleNavigateToTeams}
/>
);
}

View File

@@ -23,18 +23,26 @@ export default async function LeagueLayout({
// Return error state
return (
<LeagueDetailTemplate
leagueId={leagueId}
leagueName="Error"
leagueDescription="Failed to load league"
viewData={{
leagueId,
name: 'Error',
description: 'Failed to load league',
info: { name: 'Error', membersCount: 0, racesCount: 0, avgSOF: 0, structure: '', scoring: '', createdAt: '' },
runningRaces: [],
sponsors: [],
ownerSummary: null,
adminSummaries: [],
stewardSummaries: [],
sponsorInsights: null
}}
tabs={[]}
>
<Text align="center" className="text-gray-400">Failed to load league</Text>
<Text align="center">Failed to load league</Text>
</LeagueDetailTemplate>
);
}
const data = result.unwrap();
const league = data.league;
const viewData = result.unwrap();
// Define tab configuration
const baseTabs = [
@@ -58,9 +66,7 @@ export default async function LeagueLayout({
return (
<LeagueDetailTemplate
leagueId={leagueId}
leagueName={league.name}
leagueDescription={league.description}
viewData={viewData}
tabs={tabs}
>
{children}

View File

@@ -2,7 +2,7 @@ import { notFound } from 'next/navigation';
import { LeagueDetailTemplate } from '@/templates/LeagueDetailTemplate';
import { LeagueDetailPageQuery } from '@/lib/page-queries/page-queries/LeagueDetailPageQuery';
import { LeagueDetailViewDataBuilder } from '@/lib/builders/view-data/LeagueDetailViewDataBuilder';
import { ErrorBanner } from '@/components/ui/ErrorBanner';
import { ErrorBanner } from '@/ui/ErrorBanner';
interface Props {
params: { id: string };
@@ -49,5 +49,5 @@ export default async function Page({ params }: Props) {
sponsors: [],
});
return <LeagueDetailTemplate viewData={viewData} />;
return <LeagueDetailTemplate viewData={viewData} tabs={[]} children={null} />;
}

View File

@@ -0,0 +1,22 @@
'use client';
import React, { useState } from 'react';
import { LeagueRulebookTemplate } from '@/templates/LeagueRulebookTemplate';
import { type RulebookSection } from '@/components/leagues/RulebookTabs';
import type { LeagueRulebookViewData } from '@/lib/view-data/LeagueRulebookViewData';
interface LeagueRulebookPageClientProps {
viewData: LeagueRulebookViewData;
}
export function LeagueRulebookPageClient({ viewData }: LeagueRulebookPageClientProps) {
const [activeSection, setActiveSection] = useState<RulebookSection>('scoring');
return (
<LeagueRulebookTemplate
viewData={viewData}
activeSection={activeSection}
onSectionChange={setActiveSection}
/>
);
}

View File

@@ -4,8 +4,8 @@ import PenaltyFAB from '@/components/leagues/PenaltyFAB';
import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal';
import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
import StewardingStats from '@/components/leagues/StewardingStats';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Button from '@/ui/Button';
import Card from '@/ui/Card';
import { useLeagueStewardingMutations } from "@/lib/hooks/league/useLeagueStewardingMutations";
import {
AlertCircle,

View File

@@ -1,7 +1,7 @@
'use client';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Button from '@/ui/Button';
import Card from '@/ui/Card';
import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useInject } from '@/lib/di/hooks/useInject';

View File

@@ -1,8 +1,8 @@
'use client';
import React, { useState } from 'react';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
import TransactionRow from '@/components/leagues/TransactionRow';
import { LeagueWalletViewModel } from '@/lib/view-models/LeagueWalletViewModel';
import {

View File

@@ -3,8 +3,8 @@
import React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import CreateLeagueWizard from '@/components/leagues/CreateLeagueWizard';
import Section from '@/components/ui/Section';
import Container from '@/components/ui/Container';
import Section from '@/ui/Section';
import Container from '@/ui/Container';
type StepName = 'basics' | 'visibility' | 'structure' | 'schedule' | 'scoring' | 'stewarding' | 'review';

View File

@@ -1,20 +1,10 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Container from '@/components/ui/Container';
import Heading from '@/components/ui/Heading';
import Input from '@/components/ui/Input';
import TabNavigation from '@/components/ui/TabNavigation';
import { routes } from '@/lib/routing/RouteConfig';
import type { Result } from '@/lib/contracts/Result';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { ProfileTemplate, type ProfileTab } from '@/templates/ProfileTemplate';
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
type ProfileTab = 'overview' | 'history' | 'stats';
type SaveError = string | null;
import type { Result } from '@/lib/contracts/Result';
interface ProfilePageClientProps {
viewData: ProfileViewData;
@@ -23,112 +13,55 @@ interface ProfilePageClientProps {
}
export function ProfilePageClient({ viewData, mode, onSaveSettings }: ProfilePageClientProps) {
const [activeTab, setActiveTab] = useState<ProfileTab>('overview');
const router = useRouter();
const searchParams = useSearchParams();
const tabParam = searchParams.get('tab') as ProfileTab | null;
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
const [editMode, setEditMode] = useState(false);
const [bio, setBio] = useState(viewData.driver.bio ?? '');
const [countryCode, setCountryCode] = useState(viewData.driver.countryCode ?? '');
const [saveError, setSaveError] = useState<SaveError>(null);
const [friendRequestSent, setFriendRequestSent] = useState(false);
if (mode === 'needs-profile') {
return (
<Container size="md">
<Heading level={1}>Create your driver profile</Heading>
<Card>
<p>Driver profile not found for this account.</p>
<Link href={routes.protected.onboarding}>
<Button variant="primary">Start onboarding</Button>
</Link>
</Card>
</Container>
);
}
useEffect(() => {
const params = new URLSearchParams(searchParams.toString());
if (activeTab === 'overview') {
params.delete('tab');
} else {
params.set('tab', activeTab);
}
const query = params.toString();
const currentQuery = searchParams.toString();
if (query !== currentQuery) {
router.replace(`/profile${query ? `?${query}` : ''}`, { scroll: false });
}
}, [activeTab, searchParams, router]);
if (editMode) {
return (
<Container size="md">
<Heading level={1}>Edit profile</Heading>
<Card>
<Heading level={3}>Profile</Heading>
<Input
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder="Bio"
/>
<Input
value={countryCode}
onChange={(e) => setCountryCode(e.target.value)}
placeholder="Country code (e.g. DE)"
/>
{saveError ? <p>{saveError}</p> : null}
<Button
variant="primary"
onClick={async () => {
setSaveError(null);
const result = await onSaveSettings({ bio, country: countryCode });
if (result.isErr()) {
setSaveError(result.getError());
return;
}
setEditMode(false);
}}
>
Save
</Button>
<Button variant="secondary" onClick={() => setEditMode(false)}>
Cancel
</Button>
</Card>
</Container>
);
}
useEffect(() => {
const tab = searchParams.get('tab') as ProfileTab | null;
if (tab && tab !== activeTab) {
setActiveTab(tab);
}
}, [searchParams, activeTab]);
return (
<Container size="lg">
<Heading level={1}>{viewData.driver.name || 'Profile'}</Heading>
<Button variant="primary" onClick={() => setEditMode(true)}>
Edit profile
</Button>
<TabNavigation
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'history', label: 'Race History' },
{ id: 'stats', label: 'Detailed Stats' },
]}
activeTab={activeTab}
onTabChange={(tabId) => setActiveTab(tabId as ProfileTab)}
/>
{activeTab === 'overview' ? (
<Card>
<Heading level={3}>Driver</Heading>
<p>{viewData.driver.countryCode}</p>
<p>{viewData.driver.joinedAtLabel}</p>
<p>{viewData.driver.bio ?? ''}</p>
</Card>
) : null}
{activeTab === 'history' ? (
<Card>
<Heading level={3}>Race history</Heading>
<p>Race history is currently unavailable in this view.</p>
</Card>
) : null}
{activeTab === 'stats' ? (
<Card>
<Heading level={3}>Stats</Heading>
<p>{viewData.stats?.ratingLabel ?? ''}</p>
<p>{viewData.stats?.globalRankLabel ?? ''}</p>
</Card>
) : null}
</Container>
<ProfileTemplate
viewData={viewData}
mode={mode}
activeTab={activeTab}
onTabChange={setActiveTab}
editMode={editMode}
onEditModeChange={setEditMode}
friendRequestSent={friendRequestSent}
onFriendRequestSend={() => setFriendRequestSent(true)}
onSaveSettings={async (updates) => {
const result = await onSaveSettings(updates);
if (result.isErr()) {
// In a real app, we'd show a toast or error message.
// For now, we just throw to let the UI handle it if needed,
// or we could add an error state to this client component.
throw new Error(result.getError());
}
}}
/>
);
}

View File

@@ -1,8 +1,8 @@
import Link from 'next/link';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Container from '@/components/ui/Container';
import Heading from '@/components/ui/Heading';
import Button from '@/ui/Button';
import Card from '@/ui/Card';
import Container from '@/ui/Container';
import Heading from '@/ui/Heading';
import { routes } from '@/lib/routing/RouteConfig';
export default async function ProfileLiveriesPage() {

View File

@@ -1,8 +1,8 @@
import Link from 'next/link';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Container from '@/components/ui/Container';
import Heading from '@/components/ui/Heading';
import Button from '@/ui/Button';
import Card from '@/ui/Card';
import Container from '@/ui/Container';
import Heading from '@/ui/Heading';
import { routes } from '@/lib/routing/RouteConfig';
export default async function ProfileLiveryUploadPage() {

View File

@@ -4,7 +4,8 @@ import { updateProfileAction } from './actions';
import { ProfilePageClient } from './ProfilePageClient';
export default async function ProfilePage() {
const result = await ProfilePageQuery.execute();
const query = new ProfilePageQuery();
const result = await query.execute();
if (result.isErr()) {
notFound();
@@ -13,5 +14,11 @@ export default async function ProfilePage() {
const viewData = result.unwrap();
const mode = viewData.driver.id ? 'profile-exists' : 'needs-profile';
return <ProfilePageClient viewData={viewData} mode={mode} onSaveSettings={updateProfileAction} />;
return (
<ProfilePageClient
viewData={viewData}
mode={mode}
onSaveSettings={updateProfileAction}
/>
);
}

View File

@@ -1,8 +1,8 @@
import Link from 'next/link';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Container from '@/components/ui/Container';
import Heading from '@/components/ui/Heading';
import Button from '@/ui/Button';
import Card from '@/ui/Card';
import Container from '@/ui/Container';
import Heading from '@/ui/Heading';
import { routes } from '@/lib/routing/RouteConfig';
export default async function ProfileSettingsPage() {

View File

@@ -0,0 +1,65 @@
'use client';
import { useState, useMemo } from 'react';
import { RacesTemplate, type TimeFilter, type RaceStatusFilter } from '@/templates/RacesTemplate';
import type { RacesViewData } from '@/lib/view-data/RacesViewData';
interface RacesPageClientProps {
viewData: RacesViewData;
}
export function RacesPageClient({ viewData }: RacesPageClientProps) {
const [statusFilter, setStatusFilter] = useState<RaceStatusFilter>('all');
const [leagueFilter, setLeagueFilter] = useState<string>('all');
const [timeFilter, setTimeFilter] = useState<TimeFilter>('upcoming');
const [showFilterModal, setShowFilterModal] = useState(false);
const filteredRaces = useMemo(() => {
return viewData.races.filter((race) => {
if (statusFilter !== 'all' && race.status !== statusFilter) return false;
if (leagueFilter !== 'all' && race.leagueId !== leagueFilter) return false;
if (timeFilter === 'upcoming' && !race.isUpcoming) return false;
if (timeFilter === 'live' && !race.isLive) return false;
if (timeFilter === 'past' && !race.isPast) return false;
return true;
});
}, [viewData.races, statusFilter, leagueFilter, timeFilter]);
const racesByDate = useMemo(() => {
const grouped = new Map<string, typeof filteredRaces[0][]>();
filteredRaces.forEach((race) => {
const dateKey = race.scheduledAt.split('T')[0]!;
if (!grouped.has(dateKey)) {
grouped.set(dateKey, []);
}
grouped.get(dateKey)!.push(race);
});
return Array.from(grouped.entries()).map(([dateKey, dayRaces]) => ({
dateKey,
dateLabel: dayRaces[0]?.scheduledAtLabel || '',
races: dayRaces,
}));
}, [filteredRaces]);
return (
<RacesTemplate
viewData={{
...viewData,
races: filteredRaces,
racesByDate,
}}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
leagueFilter={leagueFilter}
setLeagueFilter={setLeagueFilter}
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
showFilterModal={showFilterModal}
setShowFilterModal={setShowFilterModal}
onRaceClick={(id) => console.log('Race click', id)}
onLeagueClick={(id) => console.log('League click', id)}
onWithdraw={(id) => console.log('Withdraw', id)}
onCancel={(id) => console.log('Cancel', id)}
/>
);
}

View File

@@ -0,0 +1,85 @@
'use client';
import React, { useState, useEffect } from 'react';
import { RaceDetailTemplate, type RaceDetailViewData } from '@/templates/RaceDetailTemplate';
interface RaceDetailPageClientProps {
viewData: RaceDetailViewData;
onBack: () => void;
onRegister: () => void;
onWithdraw: () => void;
onCancel: () => void;
onReopen: () => void;
onEndRace: () => void;
onFileProtest: () => void;
onResultsClick: () => void;
onStewardingClick: () => void;
onLeagueClick: (id: string) => void;
onDriverClick: (id: string) => void;
isOwnerOrAdmin: boolean;
}
export function RaceDetailPageClient({
viewData,
onBack,
onRegister,
onWithdraw,
onCancel,
onReopen,
onEndRace,
onFileProtest,
onResultsClick,
onStewardingClick,
onLeagueClick,
onDriverClick,
isOwnerOrAdmin
}: RaceDetailPageClientProps) {
const [showProtestModal, setShowProtestModal] = useState(false);
const [showEndRaceModal, setShowEndRaceModal] = useState(false);
const [animatedRatingChange, setAnimatedRatingChange] = useState(0);
const ratingChange = viewData.userResult?.ratingChange ?? null;
useEffect(() => {
if (ratingChange !== null) {
let start = 0;
const end = ratingChange;
const duration = 1000;
const startTime = performance.now();
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.round(start + (end - start) * eased);
setAnimatedRatingChange(current);
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
}, [ratingChange]);
return (
<RaceDetailTemplate
viewData={viewData}
isLoading={false}
onBack={onBack}
onRegister={onRegister}
onWithdraw={onWithdraw}
onCancel={onCancel}
onReopen={onReopen}
onEndRace={onEndRace}
onFileProtest={onFileProtest}
onResultsClick={onResultsClick}
onStewardingClick={onStewardingClick}
onLeagueClick={onLeagueClick}
onDriverClick={onDriverClick}
isOwnerOrAdmin={isOwnerOrAdmin}
animatedRatingChange={animatedRatingChange}
/>
);
}

View File

@@ -34,7 +34,7 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
data={null}
Template={({ data: _data }) => (
<RaceDetailTemplate
viewModel={undefined}
viewData={undefined}
isLoading={false}
error={new Error('Failed to load race details')}
onBack={() => {}}
@@ -48,12 +48,8 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
onStewardingClick={() => {}}
onLeagueClick={() => {}}
onDriverClick={() => {}}
currentDriverId={''}
isOwnerOrAdmin={false}
showProtestModal={false}
setShowProtestModal={() => {}}
showEndRaceModal={false}
setShowEndRaceModal={() => {}}
animatedRatingChange={0}
mutationLoading={{
register: false,
withdraw: false,
@@ -78,23 +74,12 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
const viewData = result.unwrap();
// Convert ViewData to ViewModel for the template
// The template expects a ViewModel, so we need to adapt
const viewModel = {
race: viewData.race,
league: viewData.league,
entryList: viewData.entryList,
registration: viewData.registration,
userResult: viewData.userResult,
canReopenRace: viewData.canReopenRace,
};
return (
<PageWrapper
data={viewData}
Template={({ data: _data }) => (
<RaceDetailTemplate
viewModel={viewModel}
viewData={viewData}
isLoading={false}
error={null}
onBack={() => {}}
@@ -108,12 +93,8 @@ export default async function RaceDetailPage({ params }: RaceDetailPageProps) {
onStewardingClick={() => {}}
onLeagueClick={() => {}}
onDriverClick={() => {}}
currentDriverId={''}
isOwnerOrAdmin={false}
showProtestModal={false}
setShowProtestModal={() => {}}
showEndRaceModal={false}
setShowEndRaceModal={() => {}}
animatedRatingChange={0}
mutationLoading={{
register: false,
withdraw: false,

View File

@@ -1,9 +1,10 @@
import { notFound } from 'next/navigation';
import { RacesTemplate } from '@/templates/RacesTemplate';
import { RacesPageQuery } from '@/lib/page-queries/races/RacesPageQuery';
import { RacesPageClient } from './RacesPageClient';
export default async function Page() {
const result = await RacesPageQuery.execute();
const query = new RacesPageQuery();
const result = await query.execute();
if (result.isErr()) {
const error = result.getError();
@@ -12,59 +13,13 @@ export default async function Page() {
case 'notFound':
notFound();
case 'redirect':
// Would redirect to login or other page
notFound();
default:
// For other errors, show error state in template
return <RacesTemplate
races={[]}
totalCount={0}
scheduledRaces={[]}
runningRaces={[]}
completedRaces={[]}
isLoading={false}
statusFilter="all"
setStatusFilter={() => {}}
leagueFilter="all"
setLeagueFilter={() => {}}
timeFilter="upcoming"
setTimeFilter={() => {}}
onRaceClick={() => {}}
onLeagueClick={() => {}}
onRegister={() => {}}
onWithdraw={() => {}}
onCancel={() => {}}
showFilterModal={false}
setShowFilterModal={() => {}}
currentDriverId={undefined}
userMemberships={[]}
/>;
notFound();
}
}
const viewData = result.unwrap();
return <RacesTemplate
races={viewData.races}
totalCount={viewData.totalCount}
scheduledRaces={viewData.scheduledRaces}
runningRaces={viewData.runningRaces}
completedRaces={viewData.completedRaces}
isLoading={false}
statusFilter="all"
setStatusFilter={() => {}}
leagueFilter="all"
setLeagueFilter={() => {}}
timeFilter="upcoming"
setTimeFilter={() => {}}
onRaceClick={() => {}}
onLeagueClick={() => {}}
onRegister={() => {}}
onWithdraw={() => {}}
onCancel={() => {}}
showFilterModal={false}
setShowFilterModal={() => {}}
currentDriverId={undefined}
userMemberships={[]}
/>;
}
return <RacesPageClient viewData={viewData} />;
}

View File

@@ -2,13 +2,13 @@
import { useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import StatCard from '@/components/ui/StatCard';
import SectionHeader from '@/components/ui/SectionHeader';
import StatusBadge from '@/components/ui/StatusBadge';
import InfoBanner from '@/components/ui/InfoBanner';
import PageHeader from '@/components/ui/PageHeader';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
import StatCard from '@/ui/StatCard';
import SectionHeader from '@/ui/SectionHeader';
import StatusBadge from '@/ui/StatusBadge';
import InfoBanner from '@/ui/InfoBanner';
import PageHeader from '@/ui/PageHeader';
import { siteConfig } from '@/lib/siteConfig';
import { useSponsorBilling } from "@/lib/hooks/sponsor/useSponsorBilling";
import {

View File

@@ -4,9 +4,9 @@ import { useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { motion, useReducedMotion, AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import InfoBanner from '@/components/ui/InfoBanner';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
import InfoBanner from '@/ui/InfoBanner';
import { useSponsorSponsorships } from "@/lib/hooks/sponsor/useSponsorSponsorships";
import {
Megaphone,

View File

@@ -2,13 +2,13 @@
import { useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Toggle from '@/components/ui/Toggle';
import SectionHeader from '@/components/ui/SectionHeader';
import FormField from '@/components/ui/FormField';
import PageHeader from '@/components/ui/PageHeader';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
import Input from '@/ui/Input';
import Toggle from '@/ui/Toggle';
import SectionHeader from '@/ui/SectionHeader';
import FormField from '@/ui/FormField';
import PageHeader from '@/ui/PageHeader';
import {
Settings,
Building2,

View File

@@ -2,9 +2,9 @@
import { useState } from 'react';
import { motion, useReducedMotion } from 'framer-motion';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import Card from '@/ui/Card';
import Button from '@/ui/Button';
import Input from '@/ui/Input';
import SponsorHero from '@/components/sponsors/SponsorHero';
import SponsorWorkflowMockup from '@/components/sponsors/SponsorWorkflowMockup';
import SponsorBenefitCard from '@/components/sponsors/SponsorBenefitCard';

View File

@@ -1,77 +1,36 @@
'use client';
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
import { TeamsTemplate } from '@/templates/TeamsTemplate';
import React from 'react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { TeamsTemplate } from '@/templates/TeamsTemplate';
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
import { routes } from '@/lib/routing/RouteConfig';
interface TeamsPageClientProps extends TeamsViewData {
searchQuery?: string;
showCreateForm?: boolean;
onSearchChange?: (query: string) => void;
onShowCreateForm?: () => void;
onHideCreateForm?: () => void;
onTeamClick?: (teamId: string) => void;
onCreateSuccess?: (teamId: string) => void;
onBrowseTeams?: () => void;
onSkillLevelClick?: (level: string) => void;
interface TeamsPageClientProps {
viewData: TeamsViewData;
}
export function TeamsPageClient({ teams }: TeamsPageClientProps) {
export function TeamsPageClient({ viewData }: TeamsPageClientProps) {
const router = useRouter();
// UI state only (no business logic)
const [searchQuery, setSearchQuery] = useState('');
const [showCreateForm, setShowCreateForm] = useState(false);
// Event handlers
const handleSearchChange = (query: string) => {
setSearchQuery(query);
};
const handleShowCreateForm = () => {
setShowCreateForm(true);
};
const handleHideCreateForm = () => {
setShowCreateForm(false);
};
const handleTeamClick = (teamId: string) => {
router.push(`/teams/${teamId}`);
};
const handleCreateSuccess = (teamId: string) => {
setShowCreateForm(false);
router.push(`/teams/${teamId}`);
const handleViewFullLeaderboard = () => {
router.push(routes.team.leaderboard);
};
const handleBrowseTeams = () => {
const element = document.getElementById('teams-list');
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
const handleSkillLevelClick = (level: string) => {
const element = document.getElementById(`level-${level}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
const handleCreateTeam = () => {
router.push(routes.team.detail('create'));
};
return (
<TeamsTemplate
teams={teams}
searchQuery={searchQuery}
showCreateForm={showCreateForm}
onSearchChange={handleSearchChange}
onShowCreateForm={handleShowCreateForm}
onHideCreateForm={handleHideCreateForm}
viewData={viewData}
onTeamClick={handleTeamClick}
onCreateSuccess={handleCreateSuccess}
onBrowseTeams={handleBrowseTeams}
onSkillLevelClick={handleSkillLevelClick}
onViewFullLeaderboard={handleViewFullLeaderboard}
onCreateTeam={handleCreateTeam}
/>
);
}

View File

@@ -1,24 +1,15 @@
import { notFound } from 'next/navigation';
import { TeamsPageQuery } from '@/lib/page-queries/page-queries/TeamsPageQuery';
import { TeamsViewDataBuilder } from '@/lib/builders/view-data/TeamsViewDataBuilder';
import { TeamsPageClient } from './TeamsPageClient';
export default async function Page() {
const result = await TeamsPageQuery.execute();
const query = new TeamsPageQuery();
const result = await query.execute();
switch (result.status) {
case 'ok':
const viewData = TeamsViewDataBuilder.build(result.dto);
return <TeamsPageClient teams={viewData.teams} />;
case 'notFound':
notFound();
case 'redirect':
// This would typically use redirect() from next/navigation
// but we need to handle it at the page level
return null;
case 'error':
// For now, treat errors as not found
// In production, you might want a proper error page
notFound();
if (result.isErr()) {
notFound();
}
}
const viewData = result.unwrap();
return <TeamsPageClient viewData={viewData} />;
}