heroes-of-chess-components 0.6.55 → 0.6.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import styled, { keyframes } from "styled-components";
|
|
3
|
+
import { HocIconBlueCar, HocIconLightBlueCar, HocIconRedCar, HocIconYellowCar } from "../../assets/race";
|
|
4
|
+
import HBox from "../HBox/HBox";
|
|
5
|
+
import HTitle from "../HTitle/HTitle";
|
|
6
|
+
import HText from "../HText/HText";
|
|
7
|
+
|
|
8
|
+
// ========== TYPES ==========
|
|
9
|
+
|
|
10
|
+
interface RacePlayer {
|
|
11
|
+
idCustomer: {
|
|
12
|
+
_id?: string;
|
|
13
|
+
profileInfo?: {
|
|
14
|
+
name?: string;
|
|
15
|
+
lastName?: string;
|
|
16
|
+
};
|
|
17
|
+
} | string;
|
|
18
|
+
points: number;
|
|
19
|
+
username?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HRaceLiveProps {
|
|
23
|
+
/** Lista de jugadores/posiciones */
|
|
24
|
+
positions: RacePlayer[];
|
|
25
|
+
/** Tiempo restante en segundos o formato "MM:SS" */
|
|
26
|
+
timeRemaining?: number | string;
|
|
27
|
+
/** ID del customer actual (para resaltar en modo compact) */
|
|
28
|
+
currentCustomerId?: string;
|
|
29
|
+
/** Modo de visualización: 'full' muestra todos, 'compact' muestra top 3 + yo */
|
|
30
|
+
mode?: "full" | "compact";
|
|
31
|
+
/** Tamaño de los autos */
|
|
32
|
+
carSize?: number;
|
|
33
|
+
/** Tamaño del texto */
|
|
34
|
+
fontSize?: number;
|
|
35
|
+
/** Mostrar título "Classifica" */
|
|
36
|
+
showTitle?: boolean;
|
|
37
|
+
/** Altura de cada fila */
|
|
38
|
+
rowHeight?: number;
|
|
39
|
+
/** Mostrar mensaje cuando no hay participantes */
|
|
40
|
+
emptyMessage?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ========== ANIMATIONS ==========
|
|
44
|
+
|
|
45
|
+
const moveCar = keyframes`
|
|
46
|
+
0%, 100% { transform: translateY(0px); }
|
|
47
|
+
50% { transform: translateY(-2px); }
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
// ========== STYLED COMPONENTS ==========
|
|
51
|
+
|
|
52
|
+
const RaceTrackContainer = styled(HBox)<{ $isLast: boolean }>`
|
|
53
|
+
${({ $isLast }) => ($isLast ? "" : "border-bottom: 5px dashed #7025BE")};
|
|
54
|
+
position: relative;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const RaceFinish = styled.div`
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-template-columns: repeat(2, auto);
|
|
61
|
+
width: auto;
|
|
62
|
+
min-width: 40px;
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const FinishSquare = styled.div<{ $isBlack: boolean }>`
|
|
67
|
+
width: 20px;
|
|
68
|
+
height: 20px;
|
|
69
|
+
background-color: ${({ $isBlack }) => ($isBlack ? "black" : "white")};
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const CarWrapper = styled.div<{ $mode: "full" | "compact" }>`
|
|
73
|
+
position: absolute;
|
|
74
|
+
left: ${({ $mode }) => ($mode === "full" ? "0" : "10px")};
|
|
75
|
+
top: 50%;
|
|
76
|
+
transform: translateY(-50%);
|
|
77
|
+
width: ${({ $mode }) =>
|
|
78
|
+
$mode === "full" ? "calc(100% - 60px)" : "calc(100% - 80px)"};
|
|
79
|
+
height: 100%;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const CarImage = styled.div<{
|
|
84
|
+
$progress: number;
|
|
85
|
+
$zIndex: number;
|
|
86
|
+
$mode: "full" | "compact";
|
|
87
|
+
}>`
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: ${({ $mode }) => ($mode === "full" ? "8px" : "4px")};
|
|
91
|
+
animation: ${moveCar} 1s ease-in-out infinite;
|
|
92
|
+
position: absolute;
|
|
93
|
+
left: ${({ $progress, $mode }) =>
|
|
94
|
+
$mode === "full" ? `calc(${$progress}% - 20px)` : `${$progress}%`};
|
|
95
|
+
top: 50%;
|
|
96
|
+
transition: left 0.5s ease-out;
|
|
97
|
+
z-index: ${({ $zIndex }) => $zIndex};
|
|
98
|
+
flex-direction: row;
|
|
99
|
+
|
|
100
|
+
img {
|
|
101
|
+
width: ${({ $mode }) => ($mode === "full" ? "120px" : "60px")};
|
|
102
|
+
height: auto;
|
|
103
|
+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const UsernameLabel = styled.span<{ $fontSize: number }>`
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
font-size: ${({ $fontSize }) => $fontSize}px;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
color: #7025be;
|
|
112
|
+
max-width: ${({ $fontSize }) => $fontSize * 5}px;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
text-overflow: ellipsis;
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
// ========== COMPONENT ==========
|
|
118
|
+
|
|
119
|
+
export const HRaceLive: React.FC<HRaceLiveProps> = ({
|
|
120
|
+
positions,
|
|
121
|
+
timeRemaining,
|
|
122
|
+
currentCustomerId,
|
|
123
|
+
mode = "full",
|
|
124
|
+
carSize = 120,
|
|
125
|
+
fontSize = 18,
|
|
126
|
+
showTitle = false,
|
|
127
|
+
rowHeight = 70,
|
|
128
|
+
emptyMessage = "No hay participantes en la carrera",
|
|
129
|
+
}) => {
|
|
130
|
+
const [renderPositions, setRenderPositions] = useState<any[]>([]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!positions?.length) {
|
|
134
|
+
setRenderPositions([]);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Ordenar por puntos
|
|
139
|
+
const sorted = [...positions].sort((a, b) => b.points - a.points);
|
|
140
|
+
|
|
141
|
+
// Determinar qué jugadores mostrar según el modo
|
|
142
|
+
let playersToShow = [];
|
|
143
|
+
|
|
144
|
+
if (mode === "compact" && currentCustomerId) {
|
|
145
|
+
const myPosition = sorted.findIndex((p) => {
|
|
146
|
+
const playerId =
|
|
147
|
+
typeof p.idCustomer === "string"
|
|
148
|
+
? p.idCustomer
|
|
149
|
+
: p.idCustomer?._id;
|
|
150
|
+
return String(playerId) === String(currentCustomerId);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (myPosition < 3) {
|
|
154
|
+
playersToShow = sorted.slice(0, 3);
|
|
155
|
+
} else {
|
|
156
|
+
playersToShow = [...sorted.slice(0, 3), sorted[myPosition]];
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
playersToShow = sorted;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Asignar autos y normalizar datos
|
|
163
|
+
const mapped = playersToShow.map((point) => {
|
|
164
|
+
const realPosition = sorted.findIndex((p) => {
|
|
165
|
+
const pid1 =
|
|
166
|
+
typeof p.idCustomer === "string" ? p.idCustomer : p.idCustomer?._id;
|
|
167
|
+
const pid2 =
|
|
168
|
+
typeof point.idCustomer === "string"
|
|
169
|
+
? point.idCustomer
|
|
170
|
+
: point.idCustomer?._id;
|
|
171
|
+
return String(pid1) === String(pid2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
let carIcon;
|
|
175
|
+
if (realPosition === 0) {
|
|
176
|
+
carIcon = HocIconRedCar;
|
|
177
|
+
} else if (realPosition === 1) {
|
|
178
|
+
carIcon = HocIconLightBlueCar;
|
|
179
|
+
} else if (realPosition === 2) {
|
|
180
|
+
carIcon = HocIconBlueCar;
|
|
181
|
+
} else {
|
|
182
|
+
carIcon = HocIconYellowCar;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const playerId =
|
|
186
|
+
typeof point.idCustomer === "string"
|
|
187
|
+
? point.idCustomer
|
|
188
|
+
: point.idCustomer?._id;
|
|
189
|
+
const isMe =
|
|
190
|
+
currentCustomerId && String(playerId) === String(currentCustomerId);
|
|
191
|
+
|
|
192
|
+
// Obtener username
|
|
193
|
+
let username = "Usuario";
|
|
194
|
+
if (point.username) {
|
|
195
|
+
username = point.username;
|
|
196
|
+
} else if (
|
|
197
|
+
typeof point.idCustomer !== "string" &&
|
|
198
|
+
point.idCustomer?.profileInfo
|
|
199
|
+
) {
|
|
200
|
+
username = `${point.idCustomer.profileInfo.name || ""} ${
|
|
201
|
+
point.idCustomer.profileInfo.lastName || ""
|
|
202
|
+
}`.trim();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
...point,
|
|
207
|
+
car: carIcon,
|
|
208
|
+
realPosition: realPosition + 1,
|
|
209
|
+
points: Math.max(0, point.points || 0),
|
|
210
|
+
isMe,
|
|
211
|
+
username,
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
setRenderPositions(mapped);
|
|
216
|
+
}, [positions, currentCustomerId, mode]);
|
|
217
|
+
|
|
218
|
+
const maxPoints = useMemo(() => {
|
|
219
|
+
if (!renderPositions.length) return 0;
|
|
220
|
+
return Math.max(...renderPositions.map((p) => p.points || 0), 1);
|
|
221
|
+
}, [renderPositions]);
|
|
222
|
+
|
|
223
|
+
const calculateProgress = (
|
|
224
|
+
playerPoints: number,
|
|
225
|
+
position: number,
|
|
226
|
+
totalPlayers: number
|
|
227
|
+
) => {
|
|
228
|
+
const safePoints = Math.max(0, playerPoints || 0);
|
|
229
|
+
if (safePoints === 0 || maxPoints === 0) return 0;
|
|
230
|
+
|
|
231
|
+
const TOTAL_RACE_TIME = 120; // 2 minutos
|
|
232
|
+
let timeElapsed = 0;
|
|
233
|
+
|
|
234
|
+
// Convertir timeRemaining a segundos transcurridos
|
|
235
|
+
if (timeRemaining !== undefined && timeRemaining !== null) {
|
|
236
|
+
if (typeof timeRemaining === "string") {
|
|
237
|
+
// Formato "MM:SS"
|
|
238
|
+
const [minutes, seconds] = timeRemaining.split(":").map(Number);
|
|
239
|
+
const remaining = minutes * 60 + seconds;
|
|
240
|
+
timeElapsed = Math.max(0, TOTAL_RACE_TIME - remaining);
|
|
241
|
+
} else {
|
|
242
|
+
// Número de segundos
|
|
243
|
+
timeElapsed = Math.max(0, TOTAL_RACE_TIME - timeRemaining);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
timeElapsed = TOTAL_RACE_TIME / 2;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Calcular progreso máximo según tiempo transcurrido
|
|
250
|
+
const maxProgressByTime =
|
|
251
|
+
mode === "full"
|
|
252
|
+
? Math.min(100, 5 + (timeElapsed / TOTAL_RACE_TIME) * 95)
|
|
253
|
+
: Math.min(90, 5 + (timeElapsed / TOTAL_RACE_TIME) * 85);
|
|
254
|
+
|
|
255
|
+
// Calcular progreso relativo del jugador
|
|
256
|
+
const relativeProgress = (safePoints / maxPoints) * 100;
|
|
257
|
+
|
|
258
|
+
// Penalización por posición
|
|
259
|
+
const positionPenalty =
|
|
260
|
+
totalPlayers > 1 ? (position / totalPlayers) * (mode === "full" ? 5 : 3) : 0;
|
|
261
|
+
|
|
262
|
+
// Progreso final
|
|
263
|
+
const scaledProgress = (relativeProgress / 100) * maxProgressByTime;
|
|
264
|
+
const finalProgress = Math.max(0, scaledProgress - positionPenalty);
|
|
265
|
+
|
|
266
|
+
return finalProgress;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (!renderPositions?.length) {
|
|
270
|
+
return mode === "full" ? (
|
|
271
|
+
<HBox padding="20px" justify="center">
|
|
272
|
+
<HTitle text={emptyMessage} color="purpleMedium" as="h4" />
|
|
273
|
+
</HBox>
|
|
274
|
+
) : null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const showEllipsis = mode === "compact" && renderPositions.length === 4;
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<HBox
|
|
281
|
+
direction="column"
|
|
282
|
+
width="100%"
|
|
283
|
+
overflowX="hidden"
|
|
284
|
+
overflowY="auto"
|
|
285
|
+
background={"shadePurpleLight"}
|
|
286
|
+
justify="flex-start"
|
|
287
|
+
gap="0"
|
|
288
|
+
padding={mode === "full" ? "10px 0" : "10px"}
|
|
289
|
+
borderRadius={mode === "compact" ? "10px" : "0"}
|
|
290
|
+
margin={mode === "compact" ? "20px 0 0 0" : "0"}
|
|
291
|
+
>
|
|
292
|
+
{showTitle && (
|
|
293
|
+
<HTitle as="h3" margin="0 0 10px 0" padding="0 10px">
|
|
294
|
+
Classifica
|
|
295
|
+
</HTitle>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{renderPositions.map((point, index) => {
|
|
299
|
+
const progress = calculateProgress(
|
|
300
|
+
point.points,
|
|
301
|
+
index,
|
|
302
|
+
renderPositions.length
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<div
|
|
307
|
+
key={`race-${point.realPosition}-${index}`}
|
|
308
|
+
style={{ width: "100%" }}
|
|
309
|
+
>
|
|
310
|
+
{showEllipsis && index === 3 && (
|
|
311
|
+
<HText textAlign="center" margin="5px 0" fontSize="18px">
|
|
312
|
+
...
|
|
313
|
+
</HText>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
<HBox
|
|
317
|
+
align="center"
|
|
318
|
+
justify="space-between"
|
|
319
|
+
width="100%"
|
|
320
|
+
margin="0"
|
|
321
|
+
gap="8px"
|
|
322
|
+
padding="0 10px"
|
|
323
|
+
background={
|
|
324
|
+
point.isMe ? "rgba(255, 215, 0, 0.2)" : "transparent"
|
|
325
|
+
}
|
|
326
|
+
borderRadius={point.isMe ? "8px" : "0"}
|
|
327
|
+
>
|
|
328
|
+
{/* Número de posición */}
|
|
329
|
+
<div
|
|
330
|
+
style={{
|
|
331
|
+
minWidth: mode === "full" ? "50px" : "40px",
|
|
332
|
+
flexShrink: 0,
|
|
333
|
+
}}
|
|
334
|
+
>
|
|
335
|
+
<HTitle
|
|
336
|
+
text={point.realPosition}
|
|
337
|
+
background={
|
|
338
|
+
point.realPosition === 1 ? "shadeGold" : "purpleMedium"
|
|
339
|
+
}
|
|
340
|
+
color={
|
|
341
|
+
point.realPosition === 1 ? "purpleMedium" : "purpleLight"
|
|
342
|
+
}
|
|
343
|
+
textAlign="center"
|
|
344
|
+
borderRadius={"8px"}
|
|
345
|
+
as={mode === "full" ? "h1" : "h3"}
|
|
346
|
+
width={mode === "full" ? "40px" : "35px"}
|
|
347
|
+
padding={mode === "full" ? "5px" : "3px"}
|
|
348
|
+
/>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{/* Pista de carrera */}
|
|
352
|
+
<RaceTrackContainer
|
|
353
|
+
justify="flex-start"
|
|
354
|
+
align="center"
|
|
355
|
+
height={`${rowHeight}px`}
|
|
356
|
+
gap="0"
|
|
357
|
+
width="100%"
|
|
358
|
+
$isLast={index + 1 === renderPositions.length}
|
|
359
|
+
padding="0"
|
|
360
|
+
>
|
|
361
|
+
<CarWrapper $mode={mode}>
|
|
362
|
+
<CarImage
|
|
363
|
+
$progress={progress}
|
|
364
|
+
$zIndex={renderPositions.length - index}
|
|
365
|
+
$mode={mode}
|
|
366
|
+
>
|
|
367
|
+
<UsernameLabel $fontSize={fontSize}>
|
|
368
|
+
{point.username}
|
|
369
|
+
</UsernameLabel>
|
|
370
|
+
<img
|
|
371
|
+
src={point.car}
|
|
372
|
+
alt={`${point.username} car`}
|
|
373
|
+
draggable="false"
|
|
374
|
+
/>
|
|
375
|
+
</CarImage>
|
|
376
|
+
</CarWrapper>
|
|
377
|
+
</RaceTrackContainer>
|
|
378
|
+
|
|
379
|
+
{/* Meta con patrón de cuadros */}
|
|
380
|
+
<RaceFinish>
|
|
381
|
+
{[...Array(6)].map((_, i) => (
|
|
382
|
+
<FinishSquare
|
|
383
|
+
key={`finish-${i}`}
|
|
384
|
+
$isBlack={
|
|
385
|
+
index % 2 === 0
|
|
386
|
+
? i % 2 !== Math.floor(i / 2) % 2
|
|
387
|
+
: i % 2 === Math.floor(i / 2) % 2
|
|
388
|
+
}
|
|
389
|
+
/>
|
|
390
|
+
))}
|
|
391
|
+
</RaceFinish>
|
|
392
|
+
</HBox>
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
})}
|
|
396
|
+
</HBox>
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export default HRaceLive;
|
|
@@ -42,4 +42,5 @@ import { HNewsItem } from "./HNewsItem/HNewsItem";
|
|
|
42
42
|
import { ErrorBoundary, ErrorScreen, ErrorTester } from "./HErrorBoundary";
|
|
43
43
|
import HTrafficLight from "./HTrafficLight/HTrafficLight.tsx";
|
|
44
44
|
import HRaceTable from "./HRaceTable/HRaceTable.tsx";
|
|
45
|
-
|
|
45
|
+
import HRaceLive from "./HRaceLive/HRaceLive.tsx";
|
|
46
|
+
export { HBackground, HBlackBack, HBox, HButton, HCalendar, HCalendarStyled, HDropdown, HInput, HRadio, HSearchInput, HTitle, HTabs, HCoinLabel, HCardLayout, HModal, HModalSecurityCode, HPopUpContainer, HPopUp, HCircularButton, HCircularTextButton, HToggleButton, HLoaderSpinner, HLogin, HPagination, HText, HInputArea, HVideoPlayerReact, HAccordion, HSlider, ClientMyReservationsComponent, ClientCalendarComponent, HModalData, HScoreBar, HProgressBar, HLevelUpAnimation, HToggleButtonCustom, HInitBackgroundAnimation, HTable, HNews, HNewsItem, HTrafficLight, HRaceTable, HRaceLive, ErrorBoundary, ErrorScreen, ErrorTester };
|