page wrapper

This commit is contained in:
2026-01-07 12:40:52 +01:00
parent e589c30bf8
commit 0db80fa98d
128 changed files with 7386 additions and 8096 deletions

View File

@@ -1,237 +0,0 @@
'use client';
import { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { Users, Search, Sparkles, Crown, Star, TrendingUp, Shield } from 'lucide-react';
import TeamsTemplate from '@/templates/TeamsTemplate';
import TeamHeroSection from '@/components/teams/TeamHeroSection';
import TeamSearchBar from '@/components/teams/TeamSearchBar';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
import CreateTeamForm from '@/components/teams/CreateTeamForm';
import WhyJoinTeamSection from '@/components/teams/WhyJoinTeamSection';
import SkillLevelSection from '@/components/teams/SkillLevelSection';
import FeaturedRecruiting from '@/components/teams/FeaturedRecruiting';
import TeamLeaderboardPreview from '@/components/teams/TeamLeaderboardPreview';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
// Shared state components
import { StateContainer } from '@/components/shared/state/StateContainer';
import { useAllTeams } from '@/hooks/team/useAllTeams';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
const SKILL_LEVELS: SkillLevel[] = ['pro', 'advanced', 'intermediate', 'beginner'];
export default function TeamsInteractive() {
const router = useRouter();
const { data: teams = [], isLoading: loading, error, retry } = useAllTeams();
const [searchQuery, setSearchQuery] = useState('');
const [showCreateForm, setShowCreateForm] = useState(false);
// Derive groups by skill level from the loaded teams
const groupsBySkillLevel = useMemo(() => {
const byLevel: Record<string, typeof teams> = {
beginner: [],
intermediate: [],
advanced: [],
pro: [],
};
if (teams) {
teams.forEach((team) => {
const level = team.performanceLevel || 'intermediate';
if (byLevel[level]) {
byLevel[level].push(team);
}
});
}
return byLevel;
}, [teams]);
// Select top teams by rating for the preview section
const topTeams = useMemo(() => {
if (!teams) return [];
const sortedByRating = [...teams].sort((a, b) => {
// Rating is not currently part of TeamSummaryViewModel in this build.
// Keep deterministic ordering by name until a rating field is exposed.
return a.name.localeCompare(b.name);
});
return sortedByRating.slice(0, 5);
}, [teams]);
const handleTeamClick = (teamId: string) => {
if (teamId.startsWith('demo-team-')) {
return;
}
router.push(`/teams/${teamId}`);
};
const handleCreateSuccess = (teamId: string) => {
setShowCreateForm(false);
router.push(`/teams/${teamId}`);
};
// Filter by search query
const filteredTeams = teams ? teams.filter((team) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
team.name.toLowerCase().includes(query) ||
(team.description ?? '').toLowerCase().includes(query) ||
(team.region ?? '').toLowerCase().includes(query) ||
(team.languages ?? []).some((lang) => lang.toLowerCase().includes(query))
);
}) : [];
// Group teams by skill level
const teamsByLevel = useMemo(() => {
return SKILL_LEVELS.reduce(
(acc, level) => {
const fromGroup = groupsBySkillLevel[level] ?? [];
acc[level] = filteredTeams.filter((team) =>
fromGroup.some((groupTeam) => groupTeam.id === team.id),
);
return acc;
},
{
beginner: [],
intermediate: [],
advanced: [],
pro: [],
} as Record<string, TeamSummaryViewModel[]>,
);
}, [groupsBySkillLevel, filteredTeams]);
const recruitingCount = teams ? teams.filter((t) => t.isRecruiting).length : 0;
const handleSkillLevelClick = (level: SkillLevel) => {
const element = document.getElementById(`level-${level}`);
element?.scrollIntoView({ behavior: 'smooth', block: 'start' });
};
const handleBrowseTeams = () => {
const element = document.getElementById('teams-list');
element?.scrollIntoView({ behavior: 'smooth' });
};
if (showCreateForm) {
return (
<div className="max-w-4xl mx-auto px-4">
<div className="mb-6">
<Button variant="secondary" onClick={() => setShowCreateForm(false)}>
Back to Teams
</Button>
</div>
<Card>
<h2 className="text-2xl font-bold text-white mb-6">Create New Team</h2>
<CreateTeamForm onCancel={() => setShowCreateForm(false)} onSuccess={handleCreateSuccess} />
</Card>
</div>
);
}
return (
<StateContainer
data={teams}
isLoading={loading}
error={error}
retry={retry}
config={{
loading: { variant: 'spinner', message: 'Loading teams...' },
error: { variant: 'full-screen' },
empty: {
icon: Users,
title: 'No teams yet',
description: 'Be the first to create a racing team. Gather drivers and compete together in endurance events.',
action: { label: 'Create Your First Team', onClick: () => setShowCreateForm(true) }
}
}}
>
{(teamsData) => (
<div className="max-w-7xl mx-auto px-4 pb-12">
{/* Hero Section */}
<TeamHeroSection
teams={teamsData}
teamsByLevel={teamsByLevel}
recruitingCount={recruitingCount}
onShowCreateForm={() => setShowCreateForm(true)}
onBrowseTeams={handleBrowseTeams}
onSkillLevelClick={handleSkillLevelClick}
/>
{/* Search Bar */}
<TeamSearchBar searchQuery={searchQuery} onSearchChange={setSearchQuery} />
{/* Why Join Section */}
{!searchQuery && <WhyJoinTeamSection />}
{/* Team Leaderboard Preview */}
{!searchQuery && <TeamLeaderboardPreview topTeams={topTeams} onTeamClick={handleTeamClick} />}
{/* Featured Recruiting */}
{!searchQuery && <FeaturedRecruiting teams={teamsData} onTeamClick={handleTeamClick} />}
{/* Teams by Skill Level */}
{teamsData.length === 0 ? (
<Card className="text-center py-16">
<div className="max-w-md mx-auto">
<div className="flex h-16 w-16 mx-auto items-center justify-center rounded-2xl bg-purple-500/10 border border-purple-500/20 mb-6">
<Users className="w-8 h-8 text-purple-400" />
</div>
<Heading level={2} className="text-2xl mb-3">
No teams yet
</Heading>
<p className="text-gray-400 mb-8">
Be the first to create a racing team. Gather drivers and compete together in endurance events.
</p>
<Button
variant="primary"
onClick={() => setShowCreateForm(true)}
className="flex items-center gap-2 mx-auto bg-purple-600 hover:bg-purple-500"
>
<Sparkles className="w-4 h-4" />
Create Your First Team
</Button>
</div>
</Card>
) : filteredTeams.length === 0 ? (
<Card className="text-center py-12">
<div className="flex flex-col items-center gap-4">
<Search className="w-10 h-10 text-gray-600" />
<p className="text-gray-400">No teams found matching "{searchQuery}"</p>
<Button variant="secondary" onClick={() => setSearchQuery('')}>
Clear search
</Button>
</div>
</Card>
) : (
<div>
{SKILL_LEVELS.map((level, index) => (
<div key={level} id={`level-${level}`} className="scroll-mt-8">
<SkillLevelSection
level={{
id: level,
label: level.charAt(0).toUpperCase() + level.slice(1),
icon: level === 'pro' ? Crown : level === 'advanced' ? Star : level === 'intermediate' ? TrendingUp : Shield,
color: level === 'pro' ? 'text-yellow-400' : level === 'advanced' ? 'text-purple-400' : level === 'intermediate' ? 'text-primary-blue' : 'text-green-400',
bgColor: level === 'pro' ? 'bg-yellow-400/10' : level === 'advanced' ? 'bg-purple-400/10' : level === 'intermediate' ? 'bg-primary-blue/10' : 'bg-green-400/10',
borderColor: level === 'pro' ? 'border-yellow-400/30' : level === 'advanced' ? 'border-purple-400/30' : level === 'intermediate' ? 'border-primary-blue/30' : 'border-green-400/30',
description: level === 'pro' ? 'Elite competition, sponsored teams' : level === 'advanced' ? 'Competitive racing, high consistency' : level === 'intermediate' ? 'Growing skills, regular practice' : 'Learning the basics, friendly environment',
}}
teams={teamsByLevel[level] ?? []}
onTeamClick={handleTeamClick}
defaultExpanded={index === 0}
/>
</div>
))}
</div>
)}
</div>
)}
</StateContainer>
);
}

View File

@@ -1,59 +0,0 @@
import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import TeamsTemplate from '@/templates/TeamsTemplate';
// This is a static component that receives data as props
// It can be used in server components or parent components that fetch data
// For client-side data fetching, use TeamsInteractive instead
interface TeamsStaticProps {
teams: TeamSummaryViewModel[];
isLoading?: boolean;
}
export default function TeamsStatic({ teams, isLoading = false }: TeamsStaticProps) {
// Calculate derived data that would normally be done in the template
const teamsBySkillLevel = teams.reduce(
(acc, team) => {
const level = team.performanceLevel || 'intermediate';
if (!acc[level]) {
acc[level] = [];
}
acc[level].push(team);
return acc;
},
{
beginner: [],
intermediate: [],
advanced: [],
pro: [],
} as Record<string, TeamSummaryViewModel[]>,
);
const topTeams = [...teams]
.sort((a, b) => a.name.localeCompare(b.name))
.slice(0, 5);
const recruitingCount = teams.filter((t) => t.isRecruiting).length;
// For static rendering, we don't have interactive state
// So we pass empty values and handlers that won't be used
return (
<TeamsTemplate
teams={teams}
isLoading={isLoading}
searchQuery=""
showCreateForm={false}
teamsByLevel={teamsBySkillLevel}
topTeams={topTeams}
recruitingCount={recruitingCount}
filteredTeams={teams}
onSearchChange={() => {}}
onShowCreateForm={() => {}}
onHideCreateForm={() => {}}
onTeamClick={() => {}}
onCreateSuccess={() => {}}
onBrowseTeams={() => {}}
onSkillLevelClick={() => {}}
/>
);
}

View File

@@ -1,139 +0,0 @@
'use client';
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import TeamDetailTemplate from '@/templates/TeamDetailTemplate';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
// Shared state components
import { StateContainer } from '@/components/shared/state/StateContainer';
import { useTeamDetails } from '@/hooks/team/useTeamDetails';
import { useTeamMembers } from '@/hooks/team/useTeamMembers';
import { useInject } from '@/lib/di/hooks/useInject';
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
import { Users } from 'lucide-react';
type Tab = 'overview' | 'roster' | 'standings' | 'admin';
export default function TeamDetailInteractive() {
const params = useParams();
const teamId = params.id as string;
const router = useRouter();
const currentDriverId = useEffectiveDriverId();
const teamService = useInject(TEAM_SERVICE_TOKEN);
const [activeTab, setActiveTab] = useState<Tab>('overview');
// Fetch team details using DI + React-Query
const { data: teamDetails, isLoading: teamLoading, error: teamError, retry: teamRetry } = useTeamDetails(teamId, currentDriverId);
// Fetch team members using DI + React-Query
const { data: memberships, isLoading: membersLoading, error: membersError, retry: membersRetry } = useTeamMembers(
teamId,
currentDriverId,
teamDetails?.ownerId || ''
);
const isLoading = teamLoading || membersLoading;
const error = teamError || membersError;
const retry = async () => {
await teamRetry();
await membersRetry();
};
// Determine admin status
const isAdmin = teamDetails?.isOwner ||
(memberships || []).some((m: any) => m.driverId === currentDriverId && (m.role === 'manager' || m.role === 'owner'));
const handleUpdate = () => {
retry();
};
const handleRemoveMember = async (driverId: string) => {
if (!confirm('Are you sure you want to remove this member?')) {
return;
}
try {
const performer = await teamService.getMembership(teamId, currentDriverId);
if (!performer || (performer.role !== 'owner' && performer.role !== 'manager')) {
throw new Error('Only owners or admins can remove members');
}
const membership = await teamService.getMembership(teamId, driverId);
if (!membership) {
throw new Error('Member not found');
}
if (membership.role === 'owner') {
throw new Error('Cannot remove the team owner');
}
await teamService.removeMembership(teamId, driverId);
handleUpdate();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to remove member');
}
};
const handleChangeRole = async (driverId: string, newRole: 'owner' | 'admin' | 'member') => {
try {
const performer = await teamService.getMembership(teamId, currentDriverId);
if (!performer || (performer.role !== 'owner' && performer.role !== 'manager')) {
throw new Error('Only owners or admins can update roles');
}
const membership = await teamService.getMembership(teamId, driverId);
if (!membership) {
throw new Error('Member not found');
}
if (membership.role === 'owner') {
throw new Error('Cannot change the owner role');
}
// Convert 'admin' to 'manager' for the service
const serviceRole = newRole === 'admin' ? 'manager' : newRole;
await teamService.updateMembership(teamId, driverId, serviceRole);
handleUpdate();
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to change role');
}
};
const handleGoBack = () => {
window.history.back();
};
return (
<StateContainer
data={teamDetails}
isLoading={isLoading}
error={error}
retry={retry}
config={{
loading: { variant: 'skeleton', message: 'Loading team details...' },
error: { variant: 'full-screen' },
empty: {
icon: Users,
title: 'Team not found',
description: 'The team may have been deleted or you may not have access',
action: { label: 'Back to Teams', onClick: () => router.push('/teams') }
}
}}
>
{(teamData) => (
<TeamDetailTemplate
team={teamData!}
memberships={memberships || []}
activeTab={activeTab}
loading={isLoading}
isAdmin={isAdmin}
onTabChange={setActiveTab}
onUpdate={handleUpdate}
onRemoveMember={handleRemoveMember}
onChangeRole={handleChangeRole}
onGoBack={handleGoBack}
/>
)}
</StateContainer>
);
}

View File

@@ -1,43 +0,0 @@
import TeamDetailTemplate from '@/templates/TeamDetailTemplate';
import type { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
import type { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
// This is a server component that can be used for static rendering
// It receives pre-fetched data and renders the template
interface TeamDetailStaticProps {
team: TeamDetailsViewModel | null;
memberships: TeamMemberViewModel[];
currentDriverId: string;
isLoading?: boolean;
}
export default function TeamDetailStatic({
team,
memberships,
currentDriverId,
isLoading = false
}: TeamDetailStaticProps) {
// Determine admin status
const isAdmin = team ? (
team.isOwner ||
memberships.some((m) => m.driverId === currentDriverId && (m.role === 'manager' || m.role === 'owner'))
) : false;
// For static rendering, we don't have interactive state
// So we pass empty values and handlers that won't be used
return (
<TeamDetailTemplate
team={team}
memberships={memberships}
activeTab="overview"
loading={isLoading}
isAdmin={isAdmin}
onTabChange={() => {}}
onUpdate={() => {}}
onRemoveMember={() => {}}
onChangeRole={() => {}}
onGoBack={() => {}}
/>
);
}

View File

@@ -1,3 +1,102 @@
import TeamDetailInteractive from './TeamDetailInteractive';
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { TeamService } from '@/lib/services/teams/TeamService';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { notFound } from 'next/navigation';
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
import TeamDetailTemplate from '@/templates/TeamDetailTemplate';
export default TeamDetailInteractive;
// Template wrapper to adapt TeamDetailTemplate for SSR
interface TeamDetailData {
team: TeamDetailsViewModel;
memberships: TeamMemberViewModel[];
isAdmin: boolean;
}
function TeamDetailTemplateWrapper({ data }: { data: TeamDetailData }) {
return (
<TeamDetailTemplate
team={data.team}
memberships={data.memberships}
activeTab="overview"
loading={false}
isAdmin={data.isAdmin}
// Event handlers are no-ops for SSR (client will handle real interactions)
onTabChange={() => {}}
onUpdate={() => {}}
onRemoveMember={() => {}}
onChangeRole={() => {}}
onGoBack={() => {}}
/>
);
}
export default async function Page({ params }: { params: { id: string } }) {
// Validate params
if (!params.id) {
notFound();
}
// Fetch data using PageDataFetcher.fetchManual
const data = await PageDataFetcher.fetchManual(async () => {
// Manual dependency creation
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
// Create API client
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
// Create service
const service = new TeamService(teamsApiClient);
// For server-side, we need a current driver ID
// This would typically come from session, but for server components we'll use a placeholder
const currentDriverId = ''; // Placeholder - would need session handling
// Fetch team details
const teamData = await service.getTeamDetails(params.id, currentDriverId);
if (!teamData) {
return null;
}
// Fetch team members
const membersData = await service.getTeamMembers(params.id, currentDriverId, teamData.ownerId || '');
// Determine admin status
const isAdmin = teamData.isOwner ||
(membersData || []).some((m: any) => m.driverId === currentDriverId && (m.role === 'manager' || m.role === 'owner'));
return {
team: teamData,
memberships: membersData || [],
isAdmin,
};
});
if (!data) {
notFound();
}
return (
<PageWrapper
data={data}
Template={TeamDetailTemplateWrapper}
loading={{ variant: 'skeleton', message: 'Loading team details...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
title: 'Team not found',
description: 'The team you are looking for does not exist or has been removed.',
}}
/>
);
}

View File

@@ -1,45 +0,0 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import TeamLeaderboardTemplate from '@/templates/TeamLeaderboardTemplate';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
interface TeamLeaderboardInteractiveProps {
teams: TeamSummaryViewModel[];
}
export default function TeamLeaderboardInteractive({ teams }: TeamLeaderboardInteractiveProps) {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState('');
const [filterLevel, setFilterLevel] = useState<SkillLevel | 'all'>('all');
const [sortBy, setSortBy] = useState<SortBy>('rating');
const handleTeamClick = (teamId: string) => {
if (teamId.startsWith('demo-team-')) {
return;
}
router.push(`/teams/${teamId}`);
};
const handleBackToTeams = () => {
router.push('/teams');
};
return (
<TeamLeaderboardTemplate
teams={teams}
searchQuery={searchQuery}
filterLevel={filterLevel}
sortBy={sortBy}
onSearchChange={setSearchQuery}
onFilterLevelChange={setFilterLevel}
onSortChange={setSortBy}
onTeamClick={handleTeamClick}
onBackToTeams={handleBackToTeams}
/>
);
}

View File

@@ -1,27 +0,0 @@
import { ServiceFactory } from '@/lib/services/ServiceFactory';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import TeamLeaderboardInteractive from './TeamLeaderboardInteractive';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
// ============================================================================
// SERVER COMPONENT - Fetches data and passes to Interactive wrapper
// ============================================================================
export default async function TeamLeaderboardStatic() {
// Create services for server-side data fetching
const serviceFactory = new ServiceFactory(getWebsiteApiBaseUrl());
const teamService = serviceFactory.createTeamService();
// Fetch data server-side
let teams: TeamSummaryViewModel[] = [];
try {
teams = await teamService.getAllTeams();
} catch (error) {
console.error('Failed to load team leaderboard:', error);
teams = [];
}
// Pass data to Interactive wrapper which handles client-side interactions
return <TeamLeaderboardInteractive teams={teams} />;
}

View File

@@ -1,9 +1,97 @@
import TeamLeaderboardStatic from './TeamLeaderboardStatic';
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import TeamLeaderboardTemplate from '@/templates/TeamLeaderboardTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { TEAM_SERVICE_TOKEN } from '@/lib/di/tokens';
import { TeamService } from '@/lib/services/teams/TeamService';
import { Trophy } from 'lucide-react';
import { redirect , useRouter } from 'next/navigation';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { useState } from 'react';
// ============================================================================
// TYPES
// ============================================================================
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
// ============================================================================
// WRAPPER COMPONENT (Client-side state management)
// ============================================================================
function TeamLeaderboardPageWrapper({ data }: { data: TeamSummaryViewModel[] | null }) {
const router = useRouter();
// Client-side state for filtering and sorting
const [searchQuery, setSearchQuery] = useState('');
const [filterLevel, setFilterLevel] = useState<SkillLevel | 'all'>('all');
const [sortBy, setSortBy] = useState<SortBy>('rating');
if (!data || data.length === 0) {
return null;
}
const handleTeamClick = (teamId: string) => {
router.push(`/teams/${teamId}`);
};
const handleBackToTeams = () => {
router.push('/teams');
};
return (
<TeamLeaderboardTemplate
teams={data}
searchQuery={searchQuery}
filterLevel={filterLevel}
sortBy={sortBy}
onSearchChange={setSearchQuery}
onFilterLevelChange={setFilterLevel}
onSortChange={setSortBy}
onTeamClick={handleTeamClick}
onBackToTeams={handleBackToTeams}
/>
);
}
// ============================================================================
// MAIN PAGE COMPONENT
// ============================================================================
export default function TeamLeaderboardPage() {
return <TeamLeaderboardStatic />;
export default async function TeamLeaderboardPage() {
// Fetch data using PageDataFetcher
const teamsData = await PageDataFetcher.fetch<TeamService, 'getAllTeams'>(
TEAM_SERVICE_TOKEN,
'getAllTeams'
);
// Prepare data for template
const data: TeamSummaryViewModel[] | null = teamsData as TeamSummaryViewModel[] | null;
const hasData = (teamsData as any)?.length > 0;
// Handle loading state (should be fast since we're using async/await)
const isLoading = false;
const error = null;
const retry = async () => {
// In server components, we can't retry without a reload
redirect('/teams/leaderboard');
};
return (
<PageWrapper
data={hasData ? data : null}
isLoading={isLoading}
error={error}
retry={retry}
Template={TeamLeaderboardPageWrapper}
loading={{ variant: 'full-screen', message: 'Loading team leaderboard...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
icon: Trophy,
title: 'No teams found',
description: 'There are no teams in the system yet.',
}}
/>
);
}

View File

@@ -1,3 +1,97 @@
import TeamsInteractive from './TeamsInteractive';
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import TeamsTemplate from '@/templates/TeamsTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { TeamService } from '@/lib/services/teams/TeamService';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { notFound } from 'next/navigation';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
export default TeamsInteractive;
// Helper to compute derived data for SSR
function computeDerivedData(teams: TeamSummaryViewModel[]) {
// Group teams by performance level (skill level)
const teamsByLevel = teams.reduce((acc, team) => {
const level = team.performanceLevel || 'intermediate';
if (!acc[level]) {
acc[level] = [];
}
acc[level].push(team);
return acc;
}, {} as Record<string, TeamSummaryViewModel[]>);
// Get top teams (by rating, descending)
const topTeams = [...teams]
.filter(t => t.rating !== undefined)
.sort((a, b) => (b.rating || 0) - (a.rating || 0))
.slice(0, 5);
// Count recruiting teams
const recruitingCount = teams.filter(t => t.isRecruiting).length;
// For SSR, filtered teams = all teams (no search filter applied server-side)
const filteredTeams = teams;
return {
teamsByLevel,
topTeams,
recruitingCount,
filteredTeams,
};
}
// Template wrapper for SSR
function TeamsTemplateWrapper({ data }: { data: TeamSummaryViewModel[] }) {
const derived = computeDerivedData(data);
// Provide default values for SSR
// The template will handle client-side state management
return (
<TeamsTemplate
teams={data}
isLoading={false}
searchQuery=""
showCreateForm={false}
teamsByLevel={derived.teamsByLevel}
topTeams={derived.topTeams}
recruitingCount={derived.recruitingCount}
filteredTeams={derived.filteredTeams}
// No-op handlers for SSR (client will override)
onSearchChange={() => {}}
onShowCreateForm={() => {}}
onHideCreateForm={() => {}}
onTeamClick={() => {}}
onCreateSuccess={() => {}}
onBrowseTeams={() => {}}
onSkillLevelClick={() => {}}
/>
);
}
export default async function Page() {
const data = await PageDataFetcher.fetchManual(async () => {
// Manual dependency creation
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
// Create API client
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
// Create service
const service = new TeamService(teamsApiClient);
return await service.getAllTeams();
});
if (!data) {
notFound();
}
return <PageWrapper data={data} Template={TeamsTemplateWrapper} />;
}