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;
@@ -0,0 +1,4 @@
1
+ import HRaceLive from "./HRaceLive";
2
+
3
+ export { HRaceLive };
4
+ 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
- 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, ErrorBoundary, ErrorScreen, ErrorTester };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heroes-of-chess-components",
3
- "version": "0.6.55",
3
+ "version": "0.6.57",
4
4
  "description": "Reusable React Components for Heroes of Chess Apps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",