@sybilion/uilib 1.2.23 → 1.2.26
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.
- package/dist/esm/components/ui/AppHeader/AppHeader.styl.js +1 -1
- package/dist/esm/components/ui/Card/Card.styl.js +1 -1
- package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.js +1 -1
- package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.styl.js +1 -1
- package/dist/esm/components/ui/DropdownMenu/DropdownMenu.js +4 -4
- package/dist/esm/components/ui/NavUserHeader/NavUserHeader.js +5 -2
- package/dist/esm/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.js +1 -1
- package/dist/esm/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.js +2 -2
- package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.constants.json.js +6 -0
- package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.js +21 -0
- package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.styl.js +7 -0
- package/dist/esm/components/widgets/DriverMap/DriverMap.helpers.js +107 -0
- package/dist/esm/components/widgets/DriverMap/DriverMap.js +129 -0
- package/dist/esm/components/widgets/DriverMap/DriverMap.styl.js +7 -0
- package/dist/esm/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.js +8 -0
- package/dist/esm/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl.js +7 -0
- package/dist/esm/components/widgets/DriverMap/MapBackground/MapBackground.js +10 -0
- package/dist/esm/components/widgets/DriverMap/MapBackground/MapBackground.styl.js +7 -0
- package/dist/esm/components/widgets/DriverMap/MapBackground/map.svg.js +3 -0
- package/dist/esm/components/widgets/DriverMap/driverCategoryIcon.js +194 -0
- package/dist/esm/components/widgets/DriverMap/driverMapGeography.js +345 -0
- package/dist/esm/components/widgets/DriverMap/driverMapSelection.js +17 -0
- package/dist/esm/hooks/index.js +1 -0
- package/dist/esm/hooks/useEvent.js +0 -2
- package/dist/esm/index.js +6 -0
- package/dist/esm/types/src/components/ui/DropdownMenu/DropdownMenu.d.ts +2 -2
- package/dist/esm/types/src/components/widgets/DriverMap/DriverIcon/DriverIcon.d.ts +17 -0
- package/dist/esm/types/src/components/widgets/DriverMap/DriverMap.d.ts +8 -0
- package/dist/esm/types/src/components/widgets/DriverMap/DriverMap.helpers.d.ts +21 -0
- package/dist/esm/types/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriverMap/MapBackground/MapBackground.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriverMap/driverCategoryIcon.d.ts +1 -0
- package/dist/esm/types/src/components/widgets/DriverMap/driverMapGeography.d.ts +80 -0
- package/dist/esm/types/src/components/widgets/DriverMap/driverMapSelection.d.ts +3 -0
- package/dist/esm/types/src/components/widgets/DriverMap/index.d.ts +8 -0
- package/dist/esm/types/src/docs/pages/DriverMapPage.d.ts +1 -0
- package/dist/esm/types/src/docs/registry.d.ts +1 -1
- package/dist/esm/types/src/hooks/index.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +1 -0
- package/dist/esm/types/src/sybilion-auth/SybilionAuthProvider.d.ts +1 -1
- package/package.json +4 -4
- package/src/components/ui/AppHeader/AppHeader.styl +4 -0
- package/src/components/ui/Card/Card.styl +1 -0
- package/src/components/ui/Chat/ChatSheet/ChatSelector.styl +3 -1
- package/src/components/ui/Chat/ChatSheet/ChatSelector.tsx +1 -1
- package/src/components/ui/DropdownMenu/DropdownMenu.tsx +4 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.tsx +9 -3
- package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl +12 -0
- package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.styl.d.ts +1 -0
- package/src/components/ui/WorkspaceAppSwitcher/WorkspaceAppSwitcher.tsx +1 -1
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.constants.json +3 -0
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.styl +125 -0
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.styl.d.ts +22 -0
- package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.tsx +81 -0
- package/src/components/widgets/DriverMap/DriverMap.helpers.ts +164 -0
- package/src/components/widgets/DriverMap/DriverMap.styl +45 -0
- package/src/components/widgets/DriverMap/DriverMap.styl.d.ts +11 -0
- package/src/components/widgets/DriverMap/DriverMap.tsx +214 -0
- package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl +24 -0
- package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.styl.d.ts +9 -0
- package/src/components/widgets/DriverMap/LoadingSpinner/LoadingSpinner.tsx +11 -0
- package/src/components/widgets/DriverMap/MapBackground/MapBackground.styl +13 -0
- package/src/components/widgets/DriverMap/MapBackground/MapBackground.styl.d.ts +7 -0
- package/src/components/widgets/DriverMap/MapBackground/MapBackground.tsx +18 -0
- package/src/components/widgets/DriverMap/MapBackground/map.svg +4337 -0
- package/src/components/widgets/DriverMap/MapBackground/mapAspect.mixin.styl +3 -0
- package/src/components/widgets/DriverMap/MapBackground/mapAspect.mixin.styl.d.ts +2 -0
- package/src/components/widgets/DriverMap/driverCategoryIcon.tsx +279 -0
- package/src/components/widgets/DriverMap/driverMapGeography.ts +478 -0
- package/src/components/widgets/DriverMap/driverMapSelection.ts +23 -0
- package/src/components/widgets/DriverMap/index.ts +18 -0
- package/src/docs/config/webpack.config.js +25 -9
- package/src/docs/pages/DriverMapPage.tsx +114 -0
- package/src/docs/pages/StandaloneAppLayoutPage/StandaloneAppLayoutPage.tsx +5 -7
- package/src/docs/pages/TooltipPage.tsx +14 -10
- package/src/docs/registry.ts +6 -5
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useEvent.ts +0 -2
- package/src/index.ts +1 -0
- package/src/sybilion-auth/SybilionAuthProvider.tsx +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { BADGE_SIZES, type BadgeSize } from './DriverIcon/DriverIcon';
|
|
2
|
+
import {
|
|
3
|
+
type DriverData,
|
|
4
|
+
geographicToSVG,
|
|
5
|
+
getResponsiveCoordinates,
|
|
6
|
+
svgToPercentage,
|
|
7
|
+
} from './driverMapGeography';
|
|
8
|
+
import { getDriverImportance } from './driverMapSelection';
|
|
9
|
+
|
|
10
|
+
// Calculate badge sizes for all drivers based on their importance
|
|
11
|
+
export const calculateBadgeSizes = (
|
|
12
|
+
drivers: DriverData[],
|
|
13
|
+
): Record<string, BadgeSize> => {
|
|
14
|
+
if (drivers.length === 0) return {};
|
|
15
|
+
|
|
16
|
+
const importanceValues = drivers
|
|
17
|
+
.map(getDriverImportance)
|
|
18
|
+
.filter(
|
|
19
|
+
(val): val is number =>
|
|
20
|
+
typeof val === 'number' && !isNaN(val) && val >= 0,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (importanceValues.length === 0) {
|
|
24
|
+
return drivers.reduce(
|
|
25
|
+
(acc, driver) => {
|
|
26
|
+
acc[driver.id] = BADGE_SIZES.s;
|
|
27
|
+
return acc;
|
|
28
|
+
},
|
|
29
|
+
{} as Record<string, BadgeSize>,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const min = Math.min(...importanceValues);
|
|
34
|
+
const max = Math.max(...importanceValues);
|
|
35
|
+
const range = max - min;
|
|
36
|
+
|
|
37
|
+
if (range === 0) {
|
|
38
|
+
return drivers.reduce(
|
|
39
|
+
(acc, driver) => {
|
|
40
|
+
acc[driver.id] = BADGE_SIZES.s;
|
|
41
|
+
return acc;
|
|
42
|
+
},
|
|
43
|
+
{} as Record<string, BadgeSize>,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return drivers.reduce(
|
|
48
|
+
(acc, driver) => {
|
|
49
|
+
const importance = getDriverImportance(driver);
|
|
50
|
+
|
|
51
|
+
if (
|
|
52
|
+
typeof importance !== 'number' ||
|
|
53
|
+
isNaN(importance) ||
|
|
54
|
+
importance < 0
|
|
55
|
+
) {
|
|
56
|
+
acc[driver.id] = BADGE_SIZES.s;
|
|
57
|
+
return acc;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const normalized = (importance - min) / range;
|
|
61
|
+
acc[driver.id] =
|
|
62
|
+
normalized >= 0.66
|
|
63
|
+
? BADGE_SIZES.l
|
|
64
|
+
: normalized >= 0.33
|
|
65
|
+
? BADGE_SIZES.m
|
|
66
|
+
: BADGE_SIZES.s;
|
|
67
|
+
return acc;
|
|
68
|
+
},
|
|
69
|
+
{} as Record<string, BadgeSize>,
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const hasValidCoords = (
|
|
74
|
+
coords: { x: number; y: number } | null | undefined,
|
|
75
|
+
): boolean => {
|
|
76
|
+
return (
|
|
77
|
+
coords?.x != null &&
|
|
78
|
+
coords?.y != null &&
|
|
79
|
+
typeof coords.x === 'number' &&
|
|
80
|
+
typeof coords.y === 'number' &&
|
|
81
|
+
!isNaN(coords.x) &&
|
|
82
|
+
!isNaN(coords.y) &&
|
|
83
|
+
Math.abs(coords.x) > 0 &&
|
|
84
|
+
Math.abs(coords.y) > 0
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const findMostSpecificRegion = (
|
|
89
|
+
driver: DriverData,
|
|
90
|
+
): {
|
|
91
|
+
name: string | null;
|
|
92
|
+
coordinates: { x: number; y: number } | null;
|
|
93
|
+
} => {
|
|
94
|
+
if (driver.region?.length > 0) {
|
|
95
|
+
return {
|
|
96
|
+
name: driver.region[driver.region.length - 1],
|
|
97
|
+
coordinates: null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const srcName = driver.src_region?.name;
|
|
102
|
+
const tgtName = driver.tgt_region?.name;
|
|
103
|
+
const srcCoords = driver.src_region?.coordinates;
|
|
104
|
+
const tgtCoords = driver.tgt_region?.coordinates;
|
|
105
|
+
const srcIsNonWorld =
|
|
106
|
+
srcName && srcName !== 'World' && hasValidCoords(srcCoords);
|
|
107
|
+
const tgtIsNonWorld =
|
|
108
|
+
tgtName && tgtName !== 'World' && hasValidCoords(tgtCoords);
|
|
109
|
+
|
|
110
|
+
if (tgtIsNonWorld) {
|
|
111
|
+
return {
|
|
112
|
+
name: tgtName,
|
|
113
|
+
coordinates: tgtCoords!,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (srcIsNonWorld) {
|
|
117
|
+
return {
|
|
118
|
+
name: srcName,
|
|
119
|
+
coordinates: srcCoords!,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { name: null, coordinates: null };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const getDriverCoordinates = (
|
|
127
|
+
driver: DriverData,
|
|
128
|
+
specificRegion: {
|
|
129
|
+
name: string | null;
|
|
130
|
+
coordinates: { x: number; y: number } | null;
|
|
131
|
+
},
|
|
132
|
+
existingDrivers: DriverData[] = [],
|
|
133
|
+
): DriverData['coordinates'] => {
|
|
134
|
+
if (!specificRegion.name || specificRegion.name === 'World') {
|
|
135
|
+
return driver.coordinates;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (specificRegion.coordinates) {
|
|
139
|
+
const isLatLng =
|
|
140
|
+
Math.abs(specificRegion.coordinates.x) <= 180 &&
|
|
141
|
+
Math.abs(specificRegion.coordinates.y) <= 90;
|
|
142
|
+
|
|
143
|
+
if (isLatLng) {
|
|
144
|
+
const svgCoords = geographicToSVG(
|
|
145
|
+
specificRegion.coordinates.y,
|
|
146
|
+
specificRegion.coordinates.x,
|
|
147
|
+
);
|
|
148
|
+
const percentageCoords = svgToPercentage(svgCoords.x, svgCoords.y);
|
|
149
|
+
return {
|
|
150
|
+
x: Math.min(95, Math.max(5, percentageCoords.x)),
|
|
151
|
+
y: Math.min(95, Math.max(5, percentageCoords.y)),
|
|
152
|
+
continent: driver.coordinates?.continent || 'Global',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
x: specificRegion.coordinates.x,
|
|
158
|
+
y: specificRegion.coordinates.y,
|
|
159
|
+
continent: driver.coordinates?.continent || 'Global',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return getResponsiveCoordinates(specificRegion.name, existingDrivers);
|
|
164
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
@import '../../../lib/theme.styl'
|
|
2
|
+
@import './MapBackground/mapAspect.mixin.styl'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
.root
|
|
6
|
+
position relative
|
|
7
|
+
width 100%
|
|
8
|
+
mapAspect()
|
|
9
|
+
padding 0
|
|
10
|
+
overflow hidden
|
|
11
|
+
|
|
12
|
+
@media (min-width: 768px)
|
|
13
|
+
height 35rem
|
|
14
|
+
width fit-content
|
|
15
|
+
flex 1
|
|
16
|
+
|
|
17
|
+
.mapInner
|
|
18
|
+
position relative
|
|
19
|
+
max-height 100%
|
|
20
|
+
mapAspect()
|
|
21
|
+
margin 0 auto
|
|
22
|
+
border-radius var(--p-4)
|
|
23
|
+
overflow hidden
|
|
24
|
+
|
|
25
|
+
.shimmerOverlay
|
|
26
|
+
z-index 20
|
|
27
|
+
|
|
28
|
+
.inTransition
|
|
29
|
+
pointer-events none
|
|
30
|
+
|
|
31
|
+
.worldDrivers
|
|
32
|
+
position absolute
|
|
33
|
+
bottom 0
|
|
34
|
+
display flex
|
|
35
|
+
align-items center
|
|
36
|
+
gap 0.5rem
|
|
37
|
+
padding 0.5rem
|
|
38
|
+
|
|
39
|
+
button
|
|
40
|
+
position static !important
|
|
41
|
+
transform none !important
|
|
42
|
+
|
|
43
|
+
& > span
|
|
44
|
+
transform none !important
|
|
45
|
+
opacity 1 !important
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is automatically generated.
|
|
2
|
+
// Please do not change this file!
|
|
3
|
+
interface CssExports {
|
|
4
|
+
'inTransition': string;
|
|
5
|
+
'mapInner': string;
|
|
6
|
+
'root': string;
|
|
7
|
+
'shimmerOverlay': string;
|
|
8
|
+
'worldDrivers': string;
|
|
9
|
+
}
|
|
10
|
+
export const cssExports: CssExports;
|
|
11
|
+
export default cssExports;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import cn from 'classnames';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import useEvent from '#uilib/hooks/useEvent';
|
|
7
|
+
import { Shimmer } from '@homecode/ui';
|
|
8
|
+
|
|
9
|
+
import { BadgeSize, DriverIcon } from './DriverIcon/DriverIcon';
|
|
10
|
+
import constants from './DriverIcon/DriverIcon.constants.json';
|
|
11
|
+
import {
|
|
12
|
+
calculateBadgeSizes,
|
|
13
|
+
findMostSpecificRegion,
|
|
14
|
+
getDriverCoordinates,
|
|
15
|
+
hasValidCoords,
|
|
16
|
+
} from './DriverMap.helpers';
|
|
17
|
+
import S from './DriverMap.styl';
|
|
18
|
+
import { MapBackground } from './MapBackground/MapBackground';
|
|
19
|
+
import type { DriverData } from './driverMapGeography';
|
|
20
|
+
import { getHighestImportanceDriver } from './driverMapSelection';
|
|
21
|
+
|
|
22
|
+
export interface DriverMapProps {
|
|
23
|
+
drivers: DriverData[];
|
|
24
|
+
isLoading: boolean;
|
|
25
|
+
setSelectedDriver: (driver: DriverData) => void;
|
|
26
|
+
selectedDriver: DriverData | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const delay = (ms: number) => new Promise<void>(r => setTimeout(r, ms));
|
|
30
|
+
|
|
31
|
+
const transitionDelay = () => delay(constants.FADE_DURATION);
|
|
32
|
+
|
|
33
|
+
export function DriverMap({
|
|
34
|
+
drivers,
|
|
35
|
+
isLoading,
|
|
36
|
+
setSelectedDriver,
|
|
37
|
+
selectedDriver,
|
|
38
|
+
}: DriverMapProps) {
|
|
39
|
+
const visibleDriversRef = useRef<DriverData[]>([]);
|
|
40
|
+
|
|
41
|
+
const [showingDrivers, setShowingDrivers] = useState<DriverData[]>(drivers);
|
|
42
|
+
const [badgeSizes, setBadgeSizes] = useState<Record<string, BadgeSize>>({});
|
|
43
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
44
|
+
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
45
|
+
|
|
46
|
+
const makeTransition = async (nextDrivers: DriverData[]) => {
|
|
47
|
+
const hadDrivers = showingDrivers.length > 0;
|
|
48
|
+
|
|
49
|
+
setIsTransitioning(true);
|
|
50
|
+
|
|
51
|
+
if (hadDrivers) {
|
|
52
|
+
setIsVisible(false);
|
|
53
|
+
await transitionDelay();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (nextDrivers.length > 0) {
|
|
57
|
+
setShowingDrivers(nextDrivers);
|
|
58
|
+
setIsVisible(false);
|
|
59
|
+
|
|
60
|
+
const sizes = calculateBadgeSizes(nextDrivers);
|
|
61
|
+
setBadgeSizes(sizes);
|
|
62
|
+
|
|
63
|
+
await delay(30);
|
|
64
|
+
setIsVisible(true);
|
|
65
|
+
await transitionDelay();
|
|
66
|
+
} else {
|
|
67
|
+
setShowingDrivers([]);
|
|
68
|
+
setBadgeSizes({});
|
|
69
|
+
setIsVisible(false);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setIsTransitioning(false);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const [worldDrivers, otherDrivers] = useMemo(() => {
|
|
76
|
+
const visibleDrivers: DriverData[] = [];
|
|
77
|
+
const worldDriversList: DriverData[] = [];
|
|
78
|
+
|
|
79
|
+
drivers.forEach(driver => {
|
|
80
|
+
let importance = driver.importance || 0;
|
|
81
|
+
if (importance === 0) {
|
|
82
|
+
const mean = driver?.rawImportance?.overall?.mean;
|
|
83
|
+
if (typeof mean === 'number') {
|
|
84
|
+
importance = Math.abs(mean) * 100;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const specificRegion = findMostSpecificRegion(driver);
|
|
89
|
+
const isEmptyRegion = driver.region?.length === 0;
|
|
90
|
+
const isWorldRegion =
|
|
91
|
+
specificRegion.name === 'World' ||
|
|
92
|
+
driver.region?.slice(-1)[0] === 'World';
|
|
93
|
+
|
|
94
|
+
const coordinates = getDriverCoordinates(
|
|
95
|
+
driver,
|
|
96
|
+
specificRegion,
|
|
97
|
+
visibleDrivers,
|
|
98
|
+
);
|
|
99
|
+
const driverToAdd = {
|
|
100
|
+
...driver,
|
|
101
|
+
coordinates,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const shouldGoToWorldDrivers =
|
|
105
|
+
isWorldRegion ||
|
|
106
|
+
!hasValidCoords(driverToAdd.coordinates) ||
|
|
107
|
+
(isEmptyRegion && !specificRegion.name);
|
|
108
|
+
|
|
109
|
+
if (shouldGoToWorldDrivers && importance >= 0) {
|
|
110
|
+
worldDriversList.push(driver);
|
|
111
|
+
} else if (hasValidCoords(driverToAdd.coordinates) && importance >= 0) {
|
|
112
|
+
visibleDrivers.push(driverToAdd);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return [
|
|
117
|
+
worldDriversList.sort((a, b) => b.importance - a.importance),
|
|
118
|
+
visibleDrivers,
|
|
119
|
+
];
|
|
120
|
+
}, [drivers]);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
void makeTransition(drivers);
|
|
124
|
+
|
|
125
|
+
if (drivers.length > 0) {
|
|
126
|
+
const idx = getHighestImportanceDriver(drivers);
|
|
127
|
+
if (idx !== null && idx >= 0) {
|
|
128
|
+
setSelectedDriver(drivers[idx]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, [drivers]);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
visibleDriversRef.current = otherDrivers;
|
|
135
|
+
}, [otherDrivers]);
|
|
136
|
+
|
|
137
|
+
const handleKeyDown = useCallback(
|
|
138
|
+
(e: KeyboardEvent) => {
|
|
139
|
+
const items = visibleDriversRef.current;
|
|
140
|
+
|
|
141
|
+
if (!selectedDriver || items.length === 0 || e.metaKey || e.ctrlKey)
|
|
142
|
+
return;
|
|
143
|
+
|
|
144
|
+
const stop = () => {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const currIndex = items.findIndex(d => d.id === selectedDriver.id);
|
|
150
|
+
|
|
151
|
+
if (e.key === 'ArrowLeft') {
|
|
152
|
+
const prevIndex = currIndex > 0 ? currIndex - 1 : items.length - 1;
|
|
153
|
+
|
|
154
|
+
setSelectedDriver(items[prevIndex]);
|
|
155
|
+
stop();
|
|
156
|
+
} else if (e.key === 'ArrowRight') {
|
|
157
|
+
const nextIndex = currIndex < items.length - 1 ? currIndex + 1 : 0;
|
|
158
|
+
|
|
159
|
+
setSelectedDriver(items[nextIndex]);
|
|
160
|
+
stop();
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[selectedDriver, setSelectedDriver],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
useEvent({
|
|
167
|
+
event: 'keydown',
|
|
168
|
+
callback: handleKeyDown,
|
|
169
|
+
isCapture: true,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className={cn(S.root, isTransitioning && S.inTransition)}>
|
|
174
|
+
<div className={S.mapInner}>
|
|
175
|
+
<MapBackground />
|
|
176
|
+
{isLoading && <Shimmer className={S.shimmerOverlay} size="l" />}
|
|
177
|
+
{otherDrivers.map(driver => (
|
|
178
|
+
<DriverIcon
|
|
179
|
+
key={driver.id}
|
|
180
|
+
isLoading={isLoading}
|
|
181
|
+
isVisible={isVisible}
|
|
182
|
+
driver={driver}
|
|
183
|
+
size={badgeSizes[driver.id] || 's'}
|
|
184
|
+
isSelected={selectedDriver?.id === driver.id}
|
|
185
|
+
onClick={() => setSelectedDriver(driver)}
|
|
186
|
+
/>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
<div className={S.worldDrivers}>
|
|
190
|
+
{worldDrivers.map(driver => {
|
|
191
|
+
const driverWithCoords = {
|
|
192
|
+
...driver,
|
|
193
|
+
coordinates: driver.coordinates || {
|
|
194
|
+
x: 0,
|
|
195
|
+
y: 0,
|
|
196
|
+
continent: 'Global',
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
return (
|
|
200
|
+
<DriverIcon
|
|
201
|
+
key={driver.id}
|
|
202
|
+
isLoading={isLoading}
|
|
203
|
+
isVisible={isVisible}
|
|
204
|
+
driver={driverWithCoords}
|
|
205
|
+
size={badgeSizes[driver.id] || 's'}
|
|
206
|
+
isSelected={selectedDriver?.id === driver.id}
|
|
207
|
+
onClick={() => setSelectedDriver(driver)}
|
|
208
|
+
/>
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Loading spinner component styles
|
|
2
|
+
.loadingSpinnerContainer
|
|
3
|
+
position absolute
|
|
4
|
+
inset 0
|
|
5
|
+
display flex
|
|
6
|
+
align-items center
|
|
7
|
+
justify-content center
|
|
8
|
+
z-index 20
|
|
9
|
+
|
|
10
|
+
.loadingSpinner
|
|
11
|
+
width 1.5rem
|
|
12
|
+
height 1.5rem
|
|
13
|
+
border 2px solid var(--muted-foreground)
|
|
14
|
+
border-opacity 0.2
|
|
15
|
+
border-top-color var(--muted-foreground)
|
|
16
|
+
border-top-opacity 0.6
|
|
17
|
+
border-radius 50%
|
|
18
|
+
animation spin 1s linear infinite
|
|
19
|
+
|
|
20
|
+
@keyframes spin
|
|
21
|
+
from
|
|
22
|
+
transform rotate(0deg)
|
|
23
|
+
to
|
|
24
|
+
transform rotate(360deg)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@import './mapAspect.mixin.styl'
|
|
2
|
+
|
|
3
|
+
// Map is <img src> from bundled URL (data URL or file). Avoids CSS url() resolving to broken relative paths in consumers.
|
|
4
|
+
.mapBackground
|
|
5
|
+
position absolute
|
|
6
|
+
display block
|
|
7
|
+
width 100%
|
|
8
|
+
height 100%
|
|
9
|
+
mapAspect()
|
|
10
|
+
object-fit contain
|
|
11
|
+
object-position center
|
|
12
|
+
pointer-events none
|
|
13
|
+
user-select none
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import S from './MapBackground.styl';
|
|
4
|
+
import mapBgUrl from './map.svg';
|
|
5
|
+
|
|
6
|
+
export function MapBackground() {
|
|
7
|
+
const src = mapBgUrl as unknown as string;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<img
|
|
11
|
+
alt=""
|
|
12
|
+
className={S.mapBackground}
|
|
13
|
+
decoding="async"
|
|
14
|
+
draggable={false}
|
|
15
|
+
src={src}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|