website refactor
This commit is contained in:
@@ -1,95 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { DashboardViewData } from './DashboardViewData';
|
||||
import type { DashboardOverviewViewModelData } from '@/lib/view-models/DashboardOverviewViewModelData';
|
||||
import { DashboardOverviewViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
|
||||
import React from 'react';
|
||||
import type { DashboardViewData } from '@/templates/view-data/DashboardViewData';
|
||||
import type { DashboardPageDto } from '@/lib/page-queries/page-dtos/DashboardPageDto';
|
||||
import { DashboardPresenter } from '@/lib/presenters/DashboardPresenter';
|
||||
import { DashboardTemplate } from '@/templates/DashboardTemplate';
|
||||
|
||||
interface DashboardPageClientProps {
|
||||
initialViewData: DashboardViewData;
|
||||
dto: DashboardOverviewViewModelData;
|
||||
pageDto: DashboardPageDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard Page Client Component
|
||||
*
|
||||
* Two-phase render:
|
||||
* 1. Initial SSR: Uses ViewData built directly from DTO (no ViewModel)
|
||||
* 2. Post-hydration: Instantiates ViewModel and re-renders with enhanced data
|
||||
*
|
||||
* Uses Presenter to transform Page DTO into ViewData
|
||||
* Presenter is deterministic and side-effect free
|
||||
*/
|
||||
export function DashboardPageClient({ initialViewData, dto }: DashboardPageClientProps) {
|
||||
const [viewData, setViewData] = useState<DashboardViewData>(initialViewData);
|
||||
|
||||
useEffect(() => {
|
||||
// Phase 2: After hydration, instantiate ViewModel and enhance data
|
||||
const viewModel = new DashboardOverviewViewModel(dto);
|
||||
|
||||
const enhancedViewData: DashboardViewData = {
|
||||
currentDriver: {
|
||||
name: viewModel.currentDriverName,
|
||||
avatarUrl: viewModel.currentDriverAvatarUrl,
|
||||
country: viewModel.currentDriverCountry,
|
||||
rating: viewModel.currentDriverRating,
|
||||
rank: viewModel.currentDriverRank,
|
||||
totalRaces: viewModel.currentDriverTotalRaces,
|
||||
wins: viewModel.currentDriverWins,
|
||||
podiums: viewModel.currentDriverPodiums,
|
||||
consistency: viewModel.currentDriverConsistency,
|
||||
},
|
||||
nextRace: viewModel.nextRace ? {
|
||||
id: viewModel.nextRace.id,
|
||||
track: viewModel.nextRace.track,
|
||||
car: viewModel.nextRace.car,
|
||||
scheduledAt: viewModel.nextRace.scheduledAt,
|
||||
formattedDate: viewModel.nextRace.formattedDate,
|
||||
formattedTime: viewModel.nextRace.formattedTime,
|
||||
timeUntil: viewModel.nextRace.timeUntil,
|
||||
isMyLeague: viewModel.nextRace.isMyLeague,
|
||||
} : null,
|
||||
upcomingRaces: viewModel.upcomingRaces.map((race) => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
formattedDate: race.formattedDate,
|
||||
formattedTime: race.formattedTime,
|
||||
timeUntil: race.timeUntil,
|
||||
isMyLeague: race.isMyLeague,
|
||||
})),
|
||||
leagueStandings: viewModel.leagueStandings.map((standing) => ({
|
||||
leagueId: standing.leagueId,
|
||||
leagueName: standing.leagueName,
|
||||
position: standing.position,
|
||||
points: standing.points,
|
||||
totalDrivers: standing.totalDrivers,
|
||||
})),
|
||||
feedItems: viewModel.feedItems.map((item) => ({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
headline: item.headline,
|
||||
body: item.body,
|
||||
timestamp: item.timestamp,
|
||||
formattedTime: item.formattedTime,
|
||||
ctaHref: item.ctaHref,
|
||||
ctaLabel: item.ctaLabel,
|
||||
})),
|
||||
friends: viewModel.friends.map((friend) => ({
|
||||
id: friend.id,
|
||||
name: friend.name,
|
||||
avatarUrl: friend.avatarUrl,
|
||||
country: friend.country,
|
||||
})),
|
||||
activeLeaguesCount: viewModel.activeLeaguesCount,
|
||||
friendCount: viewModel.friendCount,
|
||||
hasUpcomingRaces: viewModel.hasUpcomingRaces,
|
||||
hasLeagueStandings: viewModel.hasLeagueStandings,
|
||||
hasFeedItems: viewModel.hasFeedItems,
|
||||
hasFriends: viewModel.hasFriends,
|
||||
};
|
||||
|
||||
setViewData(enhancedViewData);
|
||||
}, [dto]);
|
||||
export function DashboardPageClient({ pageDto }: DashboardPageClientProps) {
|
||||
const viewData: DashboardViewData = DashboardPresenter.createViewData(pageDto);
|
||||
|
||||
return <DashboardTemplate data={viewData} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Dashboard ViewData
|
||||
*
|
||||
* SSR-safe data structure that can be built directly from DTO
|
||||
* without ViewModel instantiation. Contains formatted values
|
||||
* for display and ISO string timestamps for JSON serialization.
|
||||
*/
|
||||
|
||||
export interface DashboardViewData {
|
||||
currentDriver: {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
country: string;
|
||||
rating: string;
|
||||
rank: string;
|
||||
totalRaces: string;
|
||||
wins: string;
|
||||
podiums: string;
|
||||
consistency: string;
|
||||
};
|
||||
nextRace: {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO string
|
||||
formattedDate: string;
|
||||
formattedTime: string;
|
||||
timeUntil: string;
|
||||
isMyLeague: boolean;
|
||||
} | null;
|
||||
upcomingRaces: Array<{
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO string
|
||||
formattedDate: string;
|
||||
formattedTime: string;
|
||||
timeUntil: string;
|
||||
isMyLeague: boolean;
|
||||
}>;
|
||||
leagueStandings: Array<{
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
position: string;
|
||||
points: string;
|
||||
totalDrivers: string;
|
||||
}>;
|
||||
feedItems: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
headline: string;
|
||||
body?: string;
|
||||
timestamp: string; // ISO string
|
||||
formattedTime: string;
|
||||
ctaHref?: string;
|
||||
ctaLabel?: string;
|
||||
}>;
|
||||
friends: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
country: string;
|
||||
}>;
|
||||
activeLeaguesCount: string;
|
||||
friendCount: string;
|
||||
hasUpcomingRaces: boolean;
|
||||
hasLeagueStandings: boolean;
|
||||
hasFeedItems: boolean;
|
||||
hasFriends: boolean;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { DashboardOverviewViewModelData } from '@/lib/view-models/DashboardOverviewViewModelData';
|
||||
import type { DashboardViewData } from './DashboardViewData';
|
||||
import {
|
||||
formatDashboardDate,
|
||||
formatRating,
|
||||
formatRank,
|
||||
formatConsistency,
|
||||
formatRaceCount,
|
||||
formatFriendCount,
|
||||
formatLeaguePosition,
|
||||
formatPoints,
|
||||
formatTotalDrivers,
|
||||
} from '@/lib/display-objects/DashboardDisplay';
|
||||
|
||||
/**
|
||||
* Build DashboardViewData directly from ViewModelData
|
||||
* Used for SSR phase - no ViewModel instantiation
|
||||
*/
|
||||
export function buildDashboardViewData(viewModelData: DashboardOverviewViewModelData): DashboardViewData {
|
||||
return {
|
||||
currentDriver: {
|
||||
name: viewModelData.currentDriver?.name || '',
|
||||
avatarUrl: viewModelData.currentDriver?.avatarUrl || '',
|
||||
country: viewModelData.currentDriver?.country || '',
|
||||
rating: viewModelData.currentDriver ? formatRating(viewModelData.currentDriver.rating) : '0.0',
|
||||
rank: viewModelData.currentDriver ? formatRank(viewModelData.currentDriver.globalRank) : '0',
|
||||
totalRaces: viewModelData.currentDriver ? formatRaceCount(viewModelData.currentDriver.totalRaces) : '0',
|
||||
wins: viewModelData.currentDriver ? formatRaceCount(viewModelData.currentDriver.wins) : '0',
|
||||
podiums: viewModelData.currentDriver ? formatRaceCount(viewModelData.currentDriver.podiums) : '0',
|
||||
consistency: viewModelData.currentDriver ? formatConsistency(viewModelData.currentDriver.consistency) : '0%',
|
||||
},
|
||||
nextRace: viewModelData.nextRace ? (() => {
|
||||
const dateInfo = formatDashboardDate(new Date(viewModelData.nextRace.scheduledAt));
|
||||
return {
|
||||
id: viewModelData.nextRace.id,
|
||||
track: viewModelData.nextRace.track,
|
||||
car: viewModelData.nextRace.car,
|
||||
scheduledAt: viewModelData.nextRace.scheduledAt,
|
||||
formattedDate: dateInfo.date,
|
||||
formattedTime: dateInfo.time,
|
||||
timeUntil: dateInfo.relative,
|
||||
isMyLeague: viewModelData.nextRace.isMyLeague,
|
||||
};
|
||||
})() : null,
|
||||
upcomingRaces: viewModelData.upcomingRaces.map((race) => {
|
||||
const dateInfo = formatDashboardDate(new Date(race.scheduledAt));
|
||||
return {
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
scheduledAt: race.scheduledAt,
|
||||
formattedDate: dateInfo.date,
|
||||
formattedTime: dateInfo.time,
|
||||
timeUntil: dateInfo.relative,
|
||||
isMyLeague: race.isMyLeague,
|
||||
};
|
||||
}),
|
||||
leagueStandings: viewModelData.leagueStandingsSummaries.map((standing) => ({
|
||||
leagueId: standing.leagueId,
|
||||
leagueName: standing.leagueName,
|
||||
position: formatLeaguePosition(standing.position),
|
||||
points: formatPoints(standing.points),
|
||||
totalDrivers: formatTotalDrivers(standing.totalDrivers),
|
||||
})),
|
||||
feedItems: viewModelData.feedSummary.items.map((item) => ({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
headline: item.headline,
|
||||
body: item.body,
|
||||
timestamp: item.timestamp,
|
||||
formattedTime: formatDashboardDate(new Date(item.timestamp)).relative,
|
||||
ctaHref: item.ctaHref,
|
||||
ctaLabel: item.ctaLabel,
|
||||
})),
|
||||
friends: viewModelData.friends.map((friend) => ({
|
||||
id: friend.id,
|
||||
name: friend.name,
|
||||
avatarUrl: friend.avatarUrl,
|
||||
country: friend.country,
|
||||
})),
|
||||
activeLeaguesCount: formatRaceCount(viewModelData.activeLeaguesCount),
|
||||
friendCount: formatFriendCount(viewModelData.friends.length),
|
||||
hasUpcomingRaces: viewModelData.upcomingRaces.length > 0,
|
||||
hasLeagueStandings: viewModelData.leagueStandingsSummaries.length > 0,
|
||||
hasFeedItems: viewModelData.feedSummary.items.length > 0,
|
||||
hasFriends: viewModelData.friends.length > 0,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { DashboardPageQuery } from '@/lib/page-queries/DashboardPageQuery';
|
||||
import { DashboardPageQuery } from '@/lib/page-queries/page-queries/DashboardPageQuery';
|
||||
import { DashboardPageClient } from './DashboardPageClient';
|
||||
import { buildDashboardViewData } from './DashboardViewDataBuilder';
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const result = await DashboardPageQuery.execute();
|
||||
@@ -9,23 +8,18 @@ export default async function DashboardPage() {
|
||||
// Handle result based on status
|
||||
switch (result.status) {
|
||||
case 'ok':
|
||||
const viewModelData = result.data;
|
||||
|
||||
// Build SSR ViewData directly from ViewModelData
|
||||
const ssrViewData = buildDashboardViewData(viewModelData);
|
||||
|
||||
// Pass both ViewData (for SSR) and ViewModelData (for client enhancement)
|
||||
return <DashboardPageClient initialViewData={ssrViewData} dto={viewModelData} />;
|
||||
// Pass Page DTO to client component
|
||||
return <DashboardPageClient pageDto={result.dto} />;
|
||||
|
||||
case 'notFound':
|
||||
notFound();
|
||||
|
||||
case 'redirect':
|
||||
redirect(result.destination);
|
||||
redirect(result.to);
|
||||
|
||||
case 'error':
|
||||
// For now, treat as notFound. Could also show error page
|
||||
console.error('Dashboard error:', result.error);
|
||||
console.error('Dashboard error:', result.errorId);
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user