website refactor
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
36
apps/website/app/drivers/DriversPageClient.tsx
Normal file
36
apps/website/app/drivers/DriversPageClient.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
41
apps/website/app/leaderboards/LeaderboardsPageClient.tsx
Normal file
41
apps/website/app/leaderboards/LeaderboardsPageClient.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
65
apps/website/app/races/RacesPageClient.tsx
Normal file
65
apps/website/app/races/RacesPageClient.tsx
Normal 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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
85
apps/website/app/races/[id]/RaceDetailPageClient.tsx
Normal file
85
apps/website/app/races/[id]/RaceDetailPageClient.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user