Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7f88982

Browse files
committedFeb 13, 2025·
Improved ELO graph
1 parent ea6a59d commit 7f88982

6 files changed

+355
-121
lines changed
 

‎app/components/elo-history-charts.tsx

+176-27
Original file line numberDiff line numberDiff line change
@@ -14,34 +14,140 @@ import {
1414
type TeamPlayerELOLog,
1515
} from '@prisma/client';
1616
import { BASE_ELO } from '~/utils/constants';
17+
import { useWindowSize } from '~/utils/hooks/use-window-size';
18+
import { useState } from 'react';
19+
import { ToggleSwitch } from '~/components/toggle-switch';
20+
21+
const formatSimpleDate = (date: string) => {
22+
return new Date(date).toLocaleString('no-NO', {
23+
month: '2-digit',
24+
day: '2-digit',
25+
});
26+
};
27+
28+
const formatDateWithTime = (date: string) => {
29+
return new Date(date).toLocaleString('no-NO', {
30+
year: 'numeric',
31+
month: '2-digit',
32+
day: '2-digit',
33+
hour: '2-digit',
34+
minute: '2-digit',
35+
});
36+
};
37+
38+
const formatDateOnly = (date: string) => {
39+
return new Date(date).toLocaleString('no-NO', {
40+
year: 'numeric',
41+
month: '2-digit',
42+
day: '2-digit',
43+
});
44+
};
45+
46+
const aggregateByDay = (data: any[]) => {
47+
if (data.length === 0) return [];
48+
49+
// First, group by day as before
50+
const groupedByDay = data.reduce((acc, item) => {
51+
const date = new Date(item.date);
52+
const dayKey = new Date(date.getFullYear(), date.getMonth(), date.getDate())
53+
.toISOString()
54+
.split('T')[0];
55+
56+
if (!acc[dayKey]) {
57+
acc[dayKey] = { date: dayKey, elos: [] };
58+
}
59+
acc[dayKey].elos.push(item.elo);
60+
return acc;
61+
}, {});
62+
63+
// Find start date and use today as end date
64+
const dates = Object.keys(groupedByDay).sort();
65+
const startDate = new Date(dates[0]);
66+
const endDate = new Date(); // Use today's date
67+
endDate.setHours(23, 59, 59, 999); // Set to end of today
68+
69+
// Fill in all days from start until today
70+
const filledData = [];
71+
let currentDate = new Date(startDate);
72+
let lastKnownElo = BASE_ELO;
73+
74+
while (currentDate <= endDate) {
75+
const dateKey = currentDate.toISOString().split('T')[0];
76+
const dayData = groupedByDay[dateKey];
77+
78+
if (dayData) {
79+
// Calculate average ELO for days with matches
80+
const avgElo = Math.round(
81+
dayData.elos.reduce((sum: number, elo: number) => sum + elo, 0) /
82+
dayData.elos.length
83+
);
84+
lastKnownElo = avgElo;
85+
filledData.push({
86+
date: dateKey,
87+
elo: avgElo,
88+
hasMatches: true,
89+
});
90+
} else {
91+
// Use last known ELO for days without matches
92+
filledData.push({
93+
date: dateKey,
94+
elo: lastKnownElo,
95+
hasMatches: false,
96+
});
97+
}
98+
99+
// Move to next day
100+
currentDate.setDate(currentDate.getDate() + 1);
101+
}
102+
103+
return filledData;
104+
};
17105

18106
interface Props {
19107
data: ELOLog[] | TeamELOLog[] | TeamPlayerELOLog[];
20108
}
21109

110+
const CustomTooltip = ({ active, payload, showDailyView }: any) => {
111+
if (active && payload && payload.length) {
112+
const date = payload[0].payload.date;
113+
const formattedDate = showDailyView
114+
? formatDateOnly(date)
115+
: formatDateWithTime(date);
116+
117+
return (
118+
<div className="rounded-lg border border-gray-700 bg-gray-800/95 p-2 text-sm shadow-lg sm:p-3 sm:text-base">
119+
<p className="text-xs text-gray-200 sm:text-sm">{formattedDate}</p>
120+
<p className="text-base font-semibold text-white sm:text-lg">
121+
ELO: {payload[0].value}
122+
</p>
123+
</div>
124+
);
125+
}
126+
return null;
127+
};
128+
22129
export const EloHistoryChart = ({ data }: Props) => {
130+
const [showDailyView, setShowDailyView] = useState(false);
131+
const { width } = useWindowSize();
132+
const isMobile = width ? width < 640 : false;
133+
134+
const processedData = showDailyView ? aggregateByDay(data) : data;
135+
23136
const massagedData =
24-
data.length > 0
137+
processedData.length > 0
25138
? [
26139
{
27140
date: new Date(
28-
new Date(data[0].date).getTime() - 5 * 60000
141+
new Date(processedData[0].date).getTime() - 5 * 60000
29142
).toISOString(),
30143
elo: BASE_ELO,
31144
},
32-
...data.map((dataPoint) => ({
33-
date: dataPoint.date,
34-
elo: dataPoint.elo,
35-
})),
145+
...processedData,
36146
].map((item) => ({
37-
date: new Date(item.date).toLocaleString('no-NO', {
38-
year: 'numeric',
39-
month: '2-digit',
40-
day: '2-digit',
41-
hour: '2-digit',
42-
minute: '2-digit',
43-
}),
147+
date: item.date,
148+
displayDate: formatSimpleDate(item.date.toString()),
44149
elo: item.elo,
150+
hasMatches: 'hasMatches' in item ? item.hasMatches : false,
45151
}))
46152
: [];
47153

@@ -50,21 +156,64 @@ export const EloHistoryChart = ({ data }: Props) => {
50156

51157
const yAxisDomain = [minElo - 50, maxElo + 50];
52158

159+
const axisColor = '#94a3b8';
160+
53161
return (
54-
<ResponsiveContainer width="100%" height={300}>
55-
<LineChart data={massagedData}>
56-
<CartesianGrid strokeDasharray="3 3" />
57-
<XAxis dataKey="date" />
58-
<YAxis domain={yAxisDomain} />
59-
<Tooltip />
60-
<Legend />
61-
<Line
62-
type="monotone"
63-
dataKey="elo"
64-
stroke="#8884d8"
65-
activeDot={{ r: 8 }}
162+
<div className="space-y-4">
163+
<div className="flex justify-end">
164+
<ToggleSwitch
165+
checked={showDailyView}
166+
onChange={setShowDailyView}
167+
label="Vis ELO-endring per dag"
66168
/>
67-
</LineChart>
68-
</ResponsiveContainer>
169+
</div>
170+
<ResponsiveContainer width="100%" height={isMobile ? 200 : 300}>
171+
<LineChart data={massagedData}>
172+
<defs>
173+
<linearGradient id="eloColor" x1="0" y1="0" x2="0" y2="1">
174+
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8} />
175+
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
176+
</linearGradient>
177+
</defs>
178+
<CartesianGrid
179+
strokeDasharray="3 3"
180+
stroke="rgba(255, 255, 255, 0.1)"
181+
vertical={false}
182+
/>
183+
<XAxis
184+
dataKey="displayDate"
185+
tick={{ fill: axisColor, fontSize: isMobile ? 10 : 12 }}
186+
tickLine={{ stroke: axisColor }}
187+
stroke={axisColor}
188+
angle={isMobile ? -45 : 0}
189+
textAnchor={isMobile ? 'end' : 'middle'}
190+
height={isMobile ? 60 : 30}
191+
/>
192+
<YAxis
193+
domain={yAxisDomain}
194+
tick={{ fill: axisColor, fontSize: isMobile ? 10 : 12 }}
195+
tickLine={{ stroke: axisColor }}
196+
stroke={axisColor}
197+
width={isMobile ? 30 : 40}
198+
/>
199+
<Tooltip
200+
content={(props) => (
201+
<CustomTooltip {...props} showDailyView={showDailyView} />
202+
)}
203+
wrapperStyle={{ zIndex: 1000 }}
204+
/>
205+
<Legend wrapperStyle={{ color: axisColor }} />
206+
<Line
207+
type="monotone"
208+
dataKey="elo"
209+
stroke="#3b82f6"
210+
strokeWidth={2}
211+
dot={false}
212+
activeDot={{ r: 6, fill: '#3b82f6', stroke: '#fff' }}
213+
fill="url(#eloColor)"
214+
/>
215+
</LineChart>
216+
</ResponsiveContainer>
217+
</div>
69218
);
70219
};

‎app/components/toggle-switch.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
interface ToggleSwitchProps {
2+
checked: boolean;
3+
onChange: (checked: boolean) => void;
4+
label: string;
5+
}
6+
7+
export function ToggleSwitch({ checked, onChange, label }: ToggleSwitchProps) {
8+
return (
9+
<label className="flex cursor-pointer items-center gap-2">
10+
<div className="relative">
11+
<input
12+
type="checkbox"
13+
className="sr-only"
14+
checked={checked}
15+
onChange={(e) => onChange(e.target.checked)}
16+
/>
17+
<div
18+
className={`h-6 w-10 rounded-full shadow-inner ${
19+
checked ? 'bg-blue-500' : 'bg-gray-400'
20+
}`}
21+
/>
22+
<div
23+
className={`absolute left-1 top-1 h-4 w-4 transform rounded-full bg-white transition-transform ${
24+
checked ? 'translate-x-4' : 'translate-x-0'
25+
}`}
26+
/>
27+
</div>
28+
<span className="text-sm text-gray-700 dark:text-gray-300">{label}</span>
29+
</label>
30+
);
31+
}

‎app/routes/compare-players.$player1Id.$player2Id.tsx

+41-38
Original file line numberDiff line numberDiff line change
@@ -112,45 +112,48 @@ export default function Index() {
112112
</div>
113113

114114
{player1 && player2 && (
115-
<>
116-
<div className="container flex flex-col justify-center">
117-
<h2 className="mb-2 text-xl font-bold dark:text-green-200">
118-
Sammenligning {player1.name} vs {player2.name}
119-
</h2>
120-
<table
121-
className="mb-2 table-auto rounded-lg bg-blue-100
122-
p-4 text-lg text-black shadow-lg dark:bg-gray-700 dark:text-white"
123-
>
124-
<thead>
125-
<tr className="text-md">
126-
<th className="w-1/5 py-2"># kamper</th>
127-
<th className="w-1/5 py-2"># seiere</th>
128-
<th className="w-1/5 py-2"># tap</th>
129-
<th className="w-2/5 py-2">% overlegenhet</th>
130-
</tr>
131-
</thead>
132-
<tbody>
133-
<tr className="text-md">
134-
<td className="border py-2">
135-
{player1WinStats?.numberOfMatches}
136-
</td>
137-
<td className="border py-2">
138-
{player1WinStats?.numberOfMatchesWonByPlayer}
139-
</td>
140-
<td className="border py-2">
141-
{player1WinStats?.numberOfMatchesLostByPlayer}
142-
</td>
143-
<td className="border py-2">
144-
{player1WinStats?.winPercentage
145-
? player1WinStats.winPercentage.toFixed(2)
146-
: 0}
147-
%
148-
</td>
149-
</tr>
150-
</tbody>
151-
</table>
115+
<div className="container flex flex-col justify-center">
116+
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white">
117+
Sammenligning {player1.name} vs {player2.name}
118+
</h2>
119+
<div className="grid grid-cols-4 gap-4 rounded-lg bg-white p-6 pr-8 shadow-lg dark:bg-gray-800">
120+
<div className="text-center">
121+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
122+
{player1WinStats?.numberOfMatches}
123+
</div>
124+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
125+
Kamper
126+
</div>
127+
</div>
128+
<div className="text-center">
129+
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
130+
{player1WinStats?.numberOfMatchesWonByPlayer}
131+
</div>
132+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
133+
Seiere
134+
</div>
135+
</div>
136+
<div className="text-center">
137+
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
138+
{player1WinStats?.numberOfMatchesLostByPlayer}
139+
</div>
140+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
141+
Tap
142+
</div>
143+
</div>
144+
<div className="text-center">
145+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
146+
{player1WinStats?.winPercentage
147+
? player1WinStats.winPercentage.toFixed(2)
148+
: 0}
149+
%
150+
</div>
151+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
152+
Overlegenhet
153+
</div>
154+
</div>
152155
</div>
153-
</>
156+
</div>
154157
)}
155158
</div>
156159
);

‎app/routes/profile.$profileId.tsx

+42-31
Original file line numberDiff line numberDiff line change
@@ -232,47 +232,58 @@ export default function Index() {
232232
</div>
233233
</ul>
234234
<div className="flex flex-col justify-center">
235-
<h2 className="mb-2 text-xl font-bold dark:text-green-200">
236-
Duellspill
235+
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white">
236+
Duellspill Statistikk
237237
</h2>
238-
<table
239-
className="mb-2 table-auto rounded-lg bg-blue-100
240-
p-4 text-lg text-black shadow-lg dark:bg-gray-700 dark:text-white"
241-
>
242-
<thead>
243-
<tr className="text-md">
244-
<th className="w-1/5 py-2"># kamper</th>
245-
<th className="w-1/5 py-2"># seiere</th>
246-
<th className="w-1/5 py-2"># tap</th>
247-
<th className="w-2/5 py-2">% overlegenhet</th>
248-
</tr>
249-
</thead>
250-
<tbody>
251-
<tr className="text-md">
252-
<td className="border py-2">{numberOfMatches}</td>
253-
<td className="border py-2">{numberOfWins}</td>
254-
<td className="border py-2">{numberOfLosses}</td>
255-
<td className="border py-2">
256-
{`${winPercentage ? winPercentage.toFixed(2) : 0} %`}
257-
</td>
258-
</tr>
259-
</tbody>
260-
</table>
238+
<div className="grid grid-cols-4 gap-4 rounded-lg bg-white p-6 shadow-lg dark:bg-gray-800">
239+
<div className="text-center">
240+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
241+
{numberOfMatches}
242+
</div>
243+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
244+
Kamper
245+
</div>
246+
</div>
247+
<div className="text-center">
248+
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
249+
{numberOfWins}
250+
</div>
251+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
252+
Seiere
253+
</div>
254+
</div>
255+
<div className="text-center">
256+
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
257+
{numberOfLosses}
258+
</div>
259+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
260+
Tap
261+
</div>
262+
</div>
263+
<div className="text-center">
264+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
265+
{winPercentage ? winPercentage.toFixed(1) : 0}%
266+
</div>
267+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
268+
Win Rate
269+
</div>
270+
</div>
271+
</div>
261272
</div>
262-
{player.teamPlayerELOLog.length > 0 && (
273+
{player.eloLogs.length > 0 && (
263274
<>
264275
<h1 className="my-4 text-xl font-bold">
265-
{player.name} sin ELO-historikk i lagspill
276+
{player.name} sin ELO-historikk i duellspill
266277
</h1>
267-
<EloHistoryChart data={[...player.teamPlayerELOLog].reverse()} />
278+
<EloHistoryChart data={[...player.eloLogs].reverse()} />
268279
</>
269280
)}
270-
{player.eloLogs.length > 0 && (
281+
{player.teamPlayerELOLog.length > 0 && (
271282
<>
272283
<h1 className="my-4 text-xl font-bold">
273-
{player.name} sin ELO-historikk i duellspill
284+
{player.name} sin ELO-historikk i lagspill
274285
</h1>
275-
<EloHistoryChart data={[...player.eloLogs].reverse()} />
286+
<EloHistoryChart data={[...player.teamPlayerELOLog].reverse()} />
276287
</>
277288
)}
278289
</div>

‎app/routes/team-profile.$teamId.tsx

+36-25
Original file line numberDiff line numberDiff line change
@@ -89,32 +89,43 @@ export default function Index() {
8989
</li>
9090
</ul>
9191
<div className="flex flex-col justify-center">
92-
<h2 className="mb-2 text-xl font-bold dark:text-green-200">
93-
Lagspill
92+
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white">
93+
Lagspill Statistikk
9494
</h2>
95-
<table
96-
className="mb-2 table-auto rounded-lg bg-blue-100
97-
p-4 text-lg text-black shadow-lg dark:bg-gray-700 dark:text-white"
98-
>
99-
<thead>
100-
<tr className="text-md">
101-
<th className="w-1/5 py-2"># kamper</th>
102-
<th className="w-1/5 py-2"># seiere</th>
103-
<th className="w-1/5 py-2"># tap</th>
104-
<th className="w-2/5 py-2">% overlegenhet</th>
105-
</tr>
106-
</thead>
107-
<tbody>
108-
<tr className="text-md">
109-
<td className="border py-2">{numberOfMatches}</td>
110-
<td className="border py-2">{numberOfWins}</td>
111-
<td className="border py-2">{numberOfLosses}</td>
112-
<td className="border py-2">
113-
{`${winPercentage ? winPercentage.toFixed(2) : 0} %`}
114-
</td>
115-
</tr>
116-
</tbody>
117-
</table>
95+
<div className="grid grid-cols-4 gap-4 rounded-lg bg-white p-6 pr-8 shadow-lg dark:bg-gray-800">
96+
<div className="text-center">
97+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
98+
{numberOfMatches}
99+
</div>
100+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
101+
Kamper
102+
</div>
103+
</div>
104+
<div className="text-center">
105+
<div className="text-3xl font-bold text-green-600 dark:text-green-400">
106+
{numberOfWins}
107+
</div>
108+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
109+
Seiere
110+
</div>
111+
</div>
112+
<div className="text-center">
113+
<div className="text-3xl font-bold text-red-600 dark:text-red-400">
114+
{numberOfLosses}
115+
</div>
116+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
117+
Tap
118+
</div>
119+
</div>
120+
<div className="text-center">
121+
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
122+
{winPercentage ? winPercentage.toFixed(1) : 0}%
123+
</div>
124+
<div className="mt-1 text-sm text-gray-600 dark:text-gray-400">
125+
Win Rate
126+
</div>
127+
</div>
128+
</div>
118129
</div>
119130
{team.TeamELOLog.length > 0 && (
120131
<>

‎app/utils/hooks/use-window-size.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useState, useEffect } from 'react';
2+
3+
interface WindowSize {
4+
width: number | undefined;
5+
height: number | undefined;
6+
}
7+
8+
export function useWindowSize() {
9+
const [windowSize, setWindowSize] = useState<WindowSize>({
10+
width: undefined,
11+
height: undefined,
12+
});
13+
14+
useEffect(() => {
15+
function handleResize() {
16+
setWindowSize({
17+
width: window.innerWidth,
18+
height: window.innerHeight,
19+
});
20+
}
21+
22+
window.addEventListener('resize', handleResize);
23+
handleResize();
24+
25+
return () => window.removeEventListener('resize', handleResize);
26+
}, []);
27+
28+
return windowSize;
29+
}

0 commit comments

Comments
 (0)
Please sign in to comment.