Skip to content

Commit 4da775d

Browse files
authored
relative: keep player in the centre to avoid layout shift (#33)
1 parent 8cacbbd commit 4da775d

File tree

8 files changed

+133
-40
lines changed

8 files changed

+133
-40
lines changed

src/app/storage/defaultDashboard.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export const defaultDashboard: DashboardLayout = {
5555
width: 400,
5656
height: 296,
5757
},
58+
config: {
59+
buffer: 3,
60+
},
5861
},
5962
{
6063
id: 'map',
Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { useState } from 'react';
22
import { BaseSettingsSection } from '../components/BaseSettingsSection';
3-
import { RelativeWidgetSettings } from '../types';
43
import { useDashboard } from '@irdashies/context';
4+
import { RelativeWidgetSettings } from '../types';
5+
6+
const SETTING_ID = 'relative';
7+
8+
const defaultConfig: RelativeWidgetSettings['config'] = {
9+
buffer: 3,
10+
};
511

612
export const RelativeSettings = () => {
713
const { currentDashboard } = useDashboard();
14+
const savedSettings = currentDashboard?.widgets.find(w => w.id === SETTING_ID) as RelativeWidgetSettings | undefined;
815
const [settings, setSettings] = useState<RelativeWidgetSettings>({
9-
enabled: currentDashboard?.widgets.find(w => w.id === 'relative')?.enabled ?? false,
10-
config: currentDashboard?.widgets.find(w => w.id === 'relative')?.config ?? {},
16+
enabled: savedSettings?.enabled ?? true,
17+
config: savedSettings?.config ?? defaultConfig,
1118
});
1219

1320
if (!currentDashboard) {
@@ -22,10 +29,29 @@ export const RelativeSettings = () => {
2229
onSettingsChange={setSettings}
2330
widgetId="relative"
2431
>
25-
{/* Add specific settings controls here */}
26-
<div className="text-slate-300">
27-
Additional settings will appear here
28-
</div>
32+
{(handleConfigChange) => (
33+
<div className="space-y-4">
34+
<div className="flex items-center justify-between">
35+
<div>
36+
<span className="text-sm text-slate-300">Buffer Size</span>
37+
<p className="text-xs text-slate-400">
38+
Number of drivers to show above and below the player
39+
</p>
40+
</div>
41+
<select
42+
value={settings.config.buffer}
43+
onChange={(e) => handleConfigChange({ buffer: parseInt(e.target.value) })}
44+
className="bg-slate-700 text-slate-200 px-3 py-1 rounded border border-slate-600 focus:border-blue-500 focus:outline-none"
45+
>
46+
{Array.from({ length: 10 }, (_, i) => (
47+
<option key={i} value={i + 1}>
48+
{i + 1}
49+
</option>
50+
))}
51+
</select>
52+
</div>
53+
</div>
54+
)}
2955
</BaseSettingsSection>
3056
);
3157
};

src/frontend/components/Settings/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export interface StandingsWidgetSettings extends BaseWidgetSettings {
1717
}
1818

1919
export interface RelativeWidgetSettings extends BaseWidgetSettings {
20-
// Add specific relative settings here
20+
config: {
21+
buffer: number;
22+
};
2123
}
2224

2325
export interface WeatherWidgetSettings extends BaseWidgetSettings {
Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,96 @@
11
import { useAutoAnimate } from '@formkit/auto-animate/react';
22
import { DriverInfoRow } from './components/DriverInfoRow/DriverInfoRow';
3-
import { useDriverRelatives } from './hooks/useDriverRelatives';
3+
import { useRelativeSettings, useDriverRelatives } from './hooks';
44
import { DriverRatingBadge } from './components/DriverRatingBadge/DriverRatingBadge';
55
import { SessionBar } from './components/SessionBar/SessionBar';
66
import { SessionFooter } from './components/SessionFooter/SessionFooter';
77

88
export const Relative = () => {
9-
const standings = useDriverRelatives({ buffer: 3 });
9+
const config = useRelativeSettings();
10+
const buffer = config?.buffer ?? 3;
11+
const standings = useDriverRelatives({ buffer });
1012
const [parent] = useAutoAnimate();
1113

14+
// Always render 2 * buffer + 1 rows (buffer above + player + buffer below)
15+
const totalRows = 2 * buffer + 1;
16+
const playerIndex = standings.findIndex((result) => result.isPlayer);
17+
18+
// If no player found, render empty table with consistent height
19+
if (playerIndex === -1) {
20+
const emptyRows = Array.from({ length: totalRows }, (_, index) => (
21+
<DummyDriverRow key={`empty-${index}`} />
22+
));
23+
24+
return (
25+
<div className="w-full h-full">
26+
<SessionBar />
27+
<table className="w-full table-auto text-sm border-separate border-spacing-y-0.5 mb-3 mt-3">
28+
<tbody ref={parent}>{emptyRows}</tbody>
29+
</table>
30+
<SessionFooter />
31+
</div>
32+
);
33+
}
34+
35+
// Create an array of fixed size with placeholder rows
36+
const rows = Array.from({ length: totalRows }, (_, index) => {
37+
// Calculate the actual index in the standings array
38+
// Center the player in the middle of the display
39+
const centerIndex = Math.floor(totalRows / 2); // buffer
40+
const actualIndex = index - centerIndex + playerIndex;
41+
const result = standings[actualIndex];
42+
43+
if (!result) {
44+
// If no result, render a dummy row with visibility hidden
45+
return <DummyDriverRow key={`placeholder-${index}`} />;
46+
}
47+
48+
return (
49+
<DriverInfoRow
50+
key={result.carIdx}
51+
carIdx={result.carIdx}
52+
classColor={result.carClass.color}
53+
carNumber={result.driver?.carNum || ''}
54+
name={result.driver?.name || ''}
55+
isPlayer={result.isPlayer}
56+
hasFastestTime={result.hasFastestTime}
57+
delta={result.delta}
58+
position={result.classPosition}
59+
onPitRoad={result.onPitRoad}
60+
onTrack={result.onTrack}
61+
radioActive={result.radioActive}
62+
isLapped={result.lappedState === 'behind'}
63+
isLappingAhead={result.lappedState === 'ahead'}
64+
badge={
65+
<DriverRatingBadge
66+
license={result.driver?.license}
67+
rating={result.driver?.rating}
68+
/>
69+
}
70+
/>
71+
);
72+
});
73+
1274
return (
1375
<div className="w-full h-full">
1476
<SessionBar />
1577
<table className="w-full table-auto text-sm border-separate border-spacing-y-0.5 mb-3 mt-3">
16-
<tbody ref={parent}>
17-
{standings.map((result) => (
18-
<DriverInfoRow
19-
key={result.carIdx}
20-
carIdx={result.carIdx}
21-
classColor={result.carClass.color}
22-
carNumber={result.driver?.carNum || ''}
23-
name={result.driver?.name || ''}
24-
isPlayer={result.isPlayer}
25-
hasFastestTime={result.hasFastestTime}
26-
delta={result.delta}
27-
position={result.classPosition}
28-
onPitRoad={result.onPitRoad}
29-
onTrack={result.onTrack}
30-
radioActive={result.radioActive}
31-
isLapped={result.lappedState === 'behind'}
32-
isLappingAhead={result.lappedState === 'ahead'}
33-
badge={
34-
<DriverRatingBadge
35-
license={result.driver?.license}
36-
rating={result.driver?.rating}
37-
/>
38-
}
39-
/>
40-
))}
41-
</tbody>
78+
<tbody ref={parent}>{rows}</tbody>
4279
</table>
43-
4480
<SessionFooter />
4581
</div>
4682
);
4783
};
84+
85+
// Dummy driver row component with visibility hidden to maintain consistent height
86+
const DummyDriverRow = () => (
87+
<DriverInfoRow
88+
carIdx={0}
89+
classColor={0}
90+
carNumber="33"
91+
name="Franz Hermann"
92+
isPlayer={false}
93+
hasFastestTime={false}
94+
hidden={true}
95+
/>
96+
);

src/frontend/components/Standings/components/DriverInfoRow/DriverInfoRow.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface DriverRowInfoProps {
2222
radioActive?: boolean;
2323
isLapped?: boolean;
2424
isLappingAhead?: boolean;
25+
hidden?: boolean;
2526
}
2627

2728
export const DriverInfoRow = ({
@@ -42,6 +43,7 @@ export const DriverInfoRow = ({
4243
isLapped,
4344
isLappingAhead,
4445
iratingChange,
46+
hidden,
4547
}: DriverRowInfoProps) => {
4648
// convert seconds to mm:ss:ms
4749
const lastTimeString = formatTime(lastTime);
@@ -56,6 +58,7 @@ export const DriverInfoRow = ({
5658
isPlayer ? 'text-amber-300' : '',
5759
!isPlayer && isLapped ? 'text-blue-400' : '',
5860
!isPlayer && isLappingAhead ? 'text-red-400' : '',
61+
hidden ? 'invisible' : '',
5962
].join(' ')}
6063
>
6164
<td
@@ -69,7 +72,7 @@ export const DriverInfoRow = ({
6972
#{carNumber}
7073
</td>
7174
<td className={`px-2 py-0.5 w-full`}>
72-
<div className="flex justify-between align-center">
75+
<div className="flex justify-between align-center items-center">
7376
<div className="flex">
7477
<span
7578
className={`animate-pulse transition-[width] duration-300 ${radioActive ? 'w-4 mr-1' : 'w-0 overflow-hidden'}`}
@@ -79,7 +82,7 @@ export const DriverInfoRow = ({
7982
<span className="truncate">{name}</span>
8083
</div>
8184
{onPitRoad && (
82-
<span className="text-white animate-pulse text-xs border-yellow-500 border-2 rounded-md px-2">
85+
<span className="text-white animate-pulse text-xs border-yellow-500 border-2 rounded-md text-center text-nowrap px-2 m-0 leading-tight">
8386
PIT
8487
</span>
8588
)}

src/frontend/components/Standings/components/DriverRatingBadge/DriverRatingBadge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const DriverRatingBadge = ({
2525

2626
return (
2727
<div
28-
className={`text-center text-white text-nowrap border-solid rounded-md text-xs m-0 px-1 border-2 ${color}`}
28+
className={`text-center text-white text-nowrap border-solid rounded-md text-xs m-0 px-1 border-2 leading-tight ${color}`}
2929
>
3030
{formattedLicense} {simplifiedRating}k
3131
</div>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export * from './useCarClassStats';
22
export * from './useDriverIncidents';
33
export * from './useDriverStandings';
4+
export * from './useDriverRelatives';
45
export * from './useSessionLapCount';
56
export * from './useStandingsSettings';
7+
export * from './useRelativeSettings';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { useDashboard } from '@irdashies/context';
2+
import { RelativeWidgetSettings } from '../../Settings/types';
3+
4+
export const useRelativeSettings = (): RelativeWidgetSettings['config'] => {
5+
const { currentDashboard } = useDashboard();
6+
const widget = currentDashboard?.widgets.find(w => w.id === 'relative')?.config;
7+
return widget as RelativeWidgetSettings['config'];
8+
};

0 commit comments

Comments
 (0)