website refactor
This commit is contained in:
91
apps/website/app/teams/TeamsPageClient.tsx
Normal file
91
apps/website/app/teams/TeamsPageClient.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { TeamsPageDto } from '@/lib/page-queries/page-queries/TeamsPageQuery';
|
||||
import { TeamsPresenter } from '@/lib/presenters/TeamsPresenter';
|
||||
import { TeamsTemplate } from '@/templates/TeamsTemplate';
|
||||
import type { TeamSummaryData } from '@/templates/view-data/TeamsViewData';
|
||||
|
||||
interface TeamsPageClientProps {
|
||||
pageDto: TeamsPageDto;
|
||||
}
|
||||
|
||||
export function TeamsPageClient({ pageDto }: TeamsPageClientProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// Use presenter to create ViewData
|
||||
const viewData = TeamsPresenter.createViewData(pageDto);
|
||||
|
||||
// UI state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
|
||||
// Filter teams based on search query
|
||||
const filteredTeams = useMemo(() => {
|
||||
if (!searchQuery) return viewData.teams;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return viewData.teams.filter((team: TeamSummaryData) =>
|
||||
team.teamName.toLowerCase().includes(query) ||
|
||||
team.leagueName.toLowerCase().includes(query)
|
||||
);
|
||||
}, [viewData.teams, searchQuery]);
|
||||
|
||||
// Update viewData with filtered teams
|
||||
const templateViewData = {
|
||||
...viewData,
|
||||
teams: filteredTeams,
|
||||
};
|
||||
|
||||
// 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 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' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TeamsTemplate
|
||||
teams={templateViewData.teams}
|
||||
searchQuery={searchQuery}
|
||||
showCreateForm={showCreateForm}
|
||||
onSearchChange={handleSearchChange}
|
||||
onShowCreateForm={handleShowCreateForm}
|
||||
onHideCreateForm={handleHideCreateForm}
|
||||
onTeamClick={handleTeamClick}
|
||||
onCreateSuccess={handleCreateSuccess}
|
||||
onBrowseTeams={handleBrowseTeams}
|
||||
onSkillLevelClick={handleSkillLevelClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
68
apps/website/app/teams/[id]/TeamDetailPageClient.tsx
Normal file
68
apps/website/app/teams/[id]/TeamDetailPageClient.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { TeamDetailPageDto } from '@/lib/page-queries/page-queries/TeamDetailPageQuery';
|
||||
import { TeamDetailPresenter } from '@/lib/presenters/TeamDetailPresenter';
|
||||
import TeamDetailTemplate from '@/templates/TeamDetailTemplate';
|
||||
|
||||
type Tab = 'overview' | 'roster' | 'standings' | 'admin';
|
||||
|
||||
interface TeamDetailPageClientProps {
|
||||
pageDto: TeamDetailPageDto;
|
||||
}
|
||||
|
||||
export function TeamDetailPageClient({ pageDto }: TeamDetailPageClientProps) {
|
||||
const router = useRouter();
|
||||
|
||||
// Use presenter to create ViewData
|
||||
const viewData = TeamDetailPresenter.createViewData(pageDto);
|
||||
|
||||
// UI state
|
||||
const [activeTab, setActiveTab] = useState<Tab>('overview');
|
||||
const [loading] = useState(false);
|
||||
|
||||
// Event handlers
|
||||
const handleTabChange = (tab: Tab) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
// Trigger a refresh by reloading the page
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const handleRemoveMember = (driverId: string) => {
|
||||
// This would call an API to remove the member
|
||||
// For now, just log
|
||||
console.log('Remove member:', driverId);
|
||||
// In a real implementation, you'd have a mutation hook here
|
||||
alert('Remove member functionality would be implemented here');
|
||||
};
|
||||
|
||||
const handleChangeRole = (driverId: string, newRole: 'owner' | 'admin' | 'member') => {
|
||||
// This would call an API to change the role
|
||||
console.log('Change role:', driverId, newRole);
|
||||
// In a real implementation, you'd have a mutation hook here
|
||||
alert('Change role functionality would be implemented here');
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<TeamDetailTemplate
|
||||
team={viewData.team}
|
||||
memberships={viewData.memberships}
|
||||
activeTab={activeTab}
|
||||
loading={loading}
|
||||
isAdmin={viewData.isAdmin}
|
||||
onTabChange={handleTabChange}
|
||||
onUpdate={handleUpdate}
|
||||
onRemoveMember={handleRemoveMember}
|
||||
onChangeRole={handleChangeRole}
|
||||
onGoBack={handleGoBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +1,22 @@
|
||||
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';
|
||||
|
||||
// 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={() => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
import { TeamDetailPageQuery } from '@/lib/page-queries/TeamDetailPageQuery';
|
||||
import TeamDetailPageClient from './TeamDetailPageClient';
|
||||
|
||||
export default async function Page({ params }: { params: { id: string } }) {
|
||||
// Validate params
|
||||
if (!params.id) {
|
||||
notFound();
|
||||
}
|
||||
const result = await TeamDetailPageQuery.execute(params.id);
|
||||
|
||||
// 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) {
|
||||
switch (result.status) {
|
||||
case 'ok':
|
||||
return <TeamDetailPageClient pageDto={result.dto} />;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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();
|
||||
case 'error':
|
||||
// For now, treat errors as not found
|
||||
// In production, you might want a proper error page
|
||||
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.',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -19,9 +19,9 @@ export default async function TeamLeaderboardPage() {
|
||||
);
|
||||
|
||||
// Prepare data for template
|
||||
const data: TeamSummaryViewModel[] | null = teamsData as TeamSummaryViewModel[] | null;
|
||||
const data: TeamSummaryViewModel[] | null = teamsData;
|
||||
|
||||
const hasData = (teamsData as any)?.length > 0;
|
||||
const hasData = (teamsData?.length ?? 0) > 0;
|
||||
|
||||
// Handle loading state (should be fast since we're using async/await)
|
||||
const isLoading = false;
|
||||
|
||||
@@ -1,97 +1,22 @@
|
||||
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';
|
||||
|
||||
// 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={() => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
import { TeamsPageQuery } from '@/lib/page-queries/TeamsPageQuery';
|
||||
import TeamsPageClient from './TeamsPageClient';
|
||||
|
||||
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',
|
||||
});
|
||||
const result = await TeamsPageQuery.execute();
|
||||
|
||||
// Create API client
|
||||
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
// Create service
|
||||
const service = new TeamService(teamsApiClient);
|
||||
|
||||
return await service.getAllTeams();
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
notFound();
|
||||
switch (result.status) {
|
||||
case 'ok':
|
||||
return <TeamsPageClient pageDto={result.dto} />;
|
||||
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();
|
||||
}
|
||||
|
||||
return <PageWrapper data={data} Template={TeamsTemplateWrapper} />;
|
||||
}
|
||||
Reference in New Issue
Block a user