@opexa/portal-components 0.1.33 → 0.1.34
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.
|
@@ -5,6 +5,7 @@ import { isString } from 'lodash-es';
|
|
|
5
5
|
import Image from 'next/image';
|
|
6
6
|
import { useState } from 'react';
|
|
7
7
|
import { twMerge } from 'tailwind-merge';
|
|
8
|
+
import { useDebounceValue } from 'usehooks-ts';
|
|
8
9
|
import { useBypassKycChecker, } from '../../client/hooks/useBypassKycChecker.js';
|
|
9
10
|
import { useGamesQuery } from '../../client/hooks/useGamesQuery.js';
|
|
10
11
|
import { SearchLgIcon } from '../../icons/SearchLgIcon.js';
|
|
@@ -14,16 +15,19 @@ import { Combobox } from '../../ui/Combobox/index.js';
|
|
|
14
15
|
import { Portal } from '../../ui/Portal/index.js';
|
|
15
16
|
import { Presence } from '../../ui/Presence/index.js';
|
|
16
17
|
import { getGameImageUrl } from '../../utils/getGameImageUrl.js';
|
|
17
|
-
import { sanitizeGamesSearch } from '../../utils/sanitizeGamesSearch.js';
|
|
18
|
+
import { collapseGamesSearch, mergeUniqueById, normalizeGameName, sanitizeGamesSearch, } from '../../utils/sanitizeGamesSearch.js';
|
|
18
19
|
import { GameLaunchTrigger } from '../GameLaunch/index.js';
|
|
19
20
|
export function GamesSearch(props) {
|
|
20
21
|
const isBypass = useBypassKycChecker(props.bypassDomains);
|
|
21
22
|
const [searchInput, setSearchInput] = useState('');
|
|
22
|
-
const
|
|
23
|
+
const [debouncedSearch] = useDebounceValue(searchInput, 300);
|
|
24
|
+
const sanitizedSearch = sanitizeGamesSearch(debouncedSearch);
|
|
25
|
+
const collapsedSearch = collapseGamesSearch(debouncedSearch);
|
|
26
|
+
const hasSpaces = collapsedSearch !== sanitizedSearch;
|
|
23
27
|
const trimmedInputLength = searchInput.trim().length;
|
|
24
28
|
const showMinLengthWarning = trimmedInputLength > 0 &&
|
|
25
29
|
trimmedInputLength < 3 &&
|
|
26
|
-
|
|
30
|
+
sanitizeGamesSearch(searchInput).length < 3;
|
|
27
31
|
const gamesQuery = useGamesQuery({
|
|
28
32
|
first: props.first ?? 18,
|
|
29
33
|
filter: props.filter,
|
|
@@ -32,9 +36,27 @@ export function GamesSearch(props) {
|
|
|
32
36
|
}, {
|
|
33
37
|
enabled: sanitizedSearch.length >= 3,
|
|
34
38
|
});
|
|
35
|
-
|
|
39
|
+
const collapsedGamesQuery = useGamesQuery({
|
|
40
|
+
first: props.first ?? 18,
|
|
41
|
+
filter: props.filter,
|
|
42
|
+
search: collapsedSearch,
|
|
43
|
+
sort: props.sort,
|
|
44
|
+
}, {
|
|
45
|
+
enabled: hasSpaces && collapsedSearch.length >= 3,
|
|
46
|
+
});
|
|
47
|
+
let games = mergeUniqueById(gamesQuery.data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)) ?? [], collapsedGamesQuery.data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)) ?? []);
|
|
36
48
|
if (props.variant === '88play') {
|
|
37
|
-
|
|
49
|
+
const normalizedSearch = normalizeGameName(sanitizedSearch);
|
|
50
|
+
games = games.filter((game) => normalizeGameName(game.name).includes(normalizedSearch));
|
|
51
|
+
}
|
|
52
|
+
const hasNextPage = gamesQuery.hasNextPage || collapsedGamesQuery.hasNextPage;
|
|
53
|
+
function fetchNextPage() {
|
|
54
|
+
if (gamesQuery.hasNextPage) {
|
|
55
|
+
void gamesQuery.fetchNextPage();
|
|
56
|
+
}
|
|
57
|
+
else if (collapsedGamesQuery.hasNextPage) {
|
|
58
|
+
void collapsedGamesQuery.fetchNextPage();
|
|
59
|
+
}
|
|
38
60
|
}
|
|
39
61
|
const collection = createListCollection({
|
|
40
62
|
items: games,
|
|
@@ -52,14 +74,14 @@ export function GamesSearch(props) {
|
|
|
52
74
|
placement: 'bottom',
|
|
53
75
|
}, inputValue: searchInput, onInputValueChange: (details) => {
|
|
54
76
|
setSearchInput(details.inputValue);
|
|
55
|
-
}, selectionBehavior: "preserve", allowCustomValue: true, children: [_jsxs(Combobox.Control, { className: "relative z-
|
|
77
|
+
}, selectionBehavior: "preserve", allowCustomValue: true, children: [_jsxs(Combobox.Control, { className: "relative ui-open:z-popover", children: [_jsx(Combobox.Input, { placeholder: props.placeholder ?? 'Search' }), _jsx(Combobox.Context, { children: (api) => {
|
|
56
78
|
if (api.inputValue.length <= 0)
|
|
57
79
|
return null;
|
|
58
80
|
return (_jsx("span", { className: "cursor-pointer px-3.5 font-semibold text-text-secondary-700", onClick: () => {
|
|
59
81
|
api.setInputValue('');
|
|
60
82
|
api.focus();
|
|
61
83
|
}, children: "Clear" }));
|
|
62
|
-
} })] }), _jsx(Portal, { children: _jsx(Combobox.Context, { children: (api) => (_jsxs(_Fragment, { children: [_jsx(Presence, { present: api.open, children: _jsx("div", { className: "fixed inset-0 z-
|
|
84
|
+
} })] }), _jsx(Portal, { children: _jsx(Combobox.Context, { children: (api) => (_jsxs(_Fragment, { children: [_jsx(Presence, { present: api.open, children: _jsx("div", { className: "fixed inset-0 z-backdrop bg-black/50 backdrop-blur-sm", onClick: () => api.setOpen(false) }) }), _jsx(Combobox.Positioner, { className: "!z-popover", children: searchInput.trim().length > 0 && (_jsx(Combobox.Content, { className: "max-h-[33.25rem] overflow-y-auto p-0", children: showMinLengthWarning ? (_jsx(Alert, { message: "Search requires at least 3 characters." })) : (_jsxs(_Fragment, { children: [games.length <= 0 && (_jsx(Alert, { message: "No results found" })), games.length > 0 && (_jsxs("div", { className: "p-xl", children: [_jsx(Combobox.Context, { children: (api) => (_jsx("div", { className: twMerge('grid grid-cols-3 gap-1.5 lg:grid-cols-9 lg:gap-3.5', classNames.gameSearchResult), children: games.map((game) => (_jsxs(GameLaunchTrigger, { bypassKycCheck: isBypass, game: game, onClick: () => {
|
|
63
85
|
api.setOpen(false);
|
|
64
86
|
}, className: twMerge('md:hover:-translate-y-1 relative flex h-full w-full flex-col shadow-sm transition-transform duration-200', classNames.thumbnailRoot), children: [_jsx(Image, { src: game.name === 'Rainbow Ball'
|
|
65
87
|
? RainbowballImg
|
|
@@ -67,7 +89,7 @@ export function GamesSearch(props) {
|
|
|
67
89
|
reference: game.reference,
|
|
68
90
|
provider: game.provider,
|
|
69
91
|
image: game.image,
|
|
70
|
-
}), alt: "", width: 200, height: 200, loading: "lazy", unoptimized: true, className: "aspect-square w-full rounded-t-md object-cover" }), _jsx("span", { className: twMerge('flex w-full flex-1 items-center justify-center break-words rounded-b-md bg-bg-tertiary px-2 py-2.5 text-center font-semibold text-text-primary-brand text-xs', classNames.thumbnailTitle), children: fixMojibake(game.name) })] }, game.id))) })) }), _jsx(Presence, { present:
|
|
92
|
+
}), alt: "", width: 200, height: 200, loading: "lazy", unoptimized: true, className: "aspect-square w-full rounded-t-md object-cover" }), _jsx("span", { className: twMerge('flex w-full flex-1 items-center justify-center break-words rounded-b-md bg-bg-tertiary px-2 py-2.5 text-center font-semibold text-text-primary-brand text-xs', classNames.thumbnailTitle), children: fixMojibake(game.name) })] }, game.id))) })) }), _jsx(Presence, { present: hasNextPage, children: _jsx(Button, { variant: "outline", className: twMerge('mx-auto mt-4xl w-fit', classNames.loadMoreButton), onClick: fetchNextPage, children: "Load More" }) })] }))] })) })) })] })) }) })] }) }));
|
|
71
93
|
}
|
|
72
94
|
function Alert({ message }) {
|
|
73
95
|
return (_jsxs("div", { className: "py-lg", role: "alert", "aria-live": "polite", children: [_jsx("div", { className: "mx-auto flex size-12 items-center justify-center rounded-lg border border-border-primary bg-bg-secondary shadow-xs", children: _jsx(SearchLgIcon, { className: "size-6 text-text-secondary-700" }) }), _jsx("p", { className: "mt-4 text-center text-text-secondary-700", children: message })] }));
|
|
@@ -23,12 +23,11 @@ import { Portal } from '../../ui/Portal/index.js';
|
|
|
23
23
|
import { Presence } from '../../ui/Presence/index.js';
|
|
24
24
|
import { callIfFn } from '../../utils/callIfFn.js';
|
|
25
25
|
import { getGameImageUrl } from '../../utils/getGameImageUrl.js';
|
|
26
|
+
import { collapseGamesSearch, mergeUniqueById, normalizeGameName, sanitizeGamesSearch, } from '../../utils/sanitizeGamesSearch.js';
|
|
26
27
|
import { GameLaunchTrigger } from '../GameLaunch/index.js';
|
|
27
28
|
function lookup(value, compare) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.replace(/\s+/g, '')
|
|
31
|
-
.includes(compare.toLowerCase().replace(/\s+/g, ''));
|
|
29
|
+
const normalizedCompare = normalizeGameName(compare);
|
|
30
|
+
return normalizeGameName(value).includes(normalizedCompare);
|
|
32
31
|
}
|
|
33
32
|
export function Search(props) {
|
|
34
33
|
const isBypass = useBypassKycChecker(props.bypassDomains);
|
|
@@ -37,6 +36,9 @@ export function Search(props) {
|
|
|
37
36
|
})));
|
|
38
37
|
const inputRef = useRef(null);
|
|
39
38
|
const [search, setSearch] = useState('');
|
|
39
|
+
const sanitizedSearch = sanitizeGamesSearch(search);
|
|
40
|
+
const collapsedSearch = collapseGamesSearch(search);
|
|
41
|
+
const hasSpaces = collapsedSearch !== sanitizedSearch;
|
|
40
42
|
const gameProviders = props.gameProviders
|
|
41
43
|
.map((provider) => GAME_PROVIDER_DATA[provider])
|
|
42
44
|
.filter((provider) => {
|
|
@@ -58,9 +60,36 @@ export function Search(props) {
|
|
|
58
60
|
}, {
|
|
59
61
|
enabled: search.length >= 2,
|
|
60
62
|
});
|
|
61
|
-
|
|
63
|
+
const collapsedGamesQuery = useGamesQuery({
|
|
64
|
+
first: 18,
|
|
65
|
+
search: collapsedSearch,
|
|
66
|
+
filter: {
|
|
67
|
+
type: {
|
|
68
|
+
in: props.gameTypes,
|
|
69
|
+
},
|
|
70
|
+
provider: {
|
|
71
|
+
in: props.gameProviders,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
}, {
|
|
75
|
+
enabled: search.length >= 2 &&
|
|
76
|
+
hasSpaces &&
|
|
77
|
+
gameProviders.length === 0 &&
|
|
78
|
+
collapsedSearch.length >= 2,
|
|
79
|
+
});
|
|
80
|
+
let games = mergeUniqueById(gamesQuery.data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)) ?? [], collapsedGamesQuery.data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)) ?? []);
|
|
62
81
|
if (props.variant === '88play') {
|
|
63
|
-
|
|
82
|
+
const normalizedSearch = normalizeGameName(search);
|
|
83
|
+
games = games.filter((game) => normalizeGameName(game.name).includes(normalizedSearch));
|
|
84
|
+
}
|
|
85
|
+
const hasNextPage = gamesQuery.hasNextPage || collapsedGamesQuery.hasNextPage;
|
|
86
|
+
function fetchNextPage() {
|
|
87
|
+
if (gamesQuery.hasNextPage) {
|
|
88
|
+
void gamesQuery.fetchNextPage();
|
|
89
|
+
}
|
|
90
|
+
else if (collapsedGamesQuery.hasNextPage) {
|
|
91
|
+
void collapsedGamesQuery.fetchNextPage();
|
|
92
|
+
}
|
|
64
93
|
}
|
|
65
94
|
const empty = games.length <= 0 && gameProviders.length <= 0;
|
|
66
95
|
const viewGamesUrl = (data) => {
|
|
@@ -93,7 +122,7 @@ export function Search(props) {
|
|
|
93
122
|
reference: game.reference,
|
|
94
123
|
provider: game.provider,
|
|
95
124
|
image: game.image,
|
|
96
|
-
}), alt: "", width: 200, height: 200, loading: "lazy", unoptimized: true, className: "aspect-square w-full rounded-t-md object-cover" }), _jsx("span", { className: twMerge('block w-full rounded-b-md bg-bg-tertiary px-2 py-2.5 text-center font-semibold text-text-primary-brand text-xs', props.variant !== '88play' && 'truncate', classNames.gameThumbnailTitle), children: fixMojibake(game.name) })] }, game.id))) })] })), _jsx(Presence, { present:
|
|
125
|
+
}), alt: "", width: 200, height: 200, loading: "lazy", unoptimized: true, className: "aspect-square w-full rounded-t-md object-cover" }), _jsx("span", { className: twMerge('block w-full rounded-b-md bg-bg-tertiary px-2 py-2.5 text-center font-semibold text-text-primary-brand text-xs', props.variant !== '88play' && 'truncate', classNames.gameThumbnailTitle), children: fixMojibake(game.name) })] }, game.id))) })] })), _jsx(Presence, { present: hasNextPage, children: _jsx(Button, { variant: "outline", className: twMerge('mx-auto mt-12 w-fit', classNames.loadMoreButton), onClick: fetchNextPage, children: "Load More" }) })] }))] })) }) })] }) })] }) }));
|
|
97
126
|
}
|
|
98
127
|
function DebouncedInput(props) {
|
|
99
128
|
const [value, setValue] = useControllableState({
|
|
@@ -1 +1,6 @@
|
|
|
1
1
|
export declare function sanitizeGamesSearch(search?: string): string;
|
|
2
|
+
export declare function collapseGamesSearch(search?: string): string;
|
|
3
|
+
export declare function normalizeGameName(value: string): string;
|
|
4
|
+
export declare function mergeUniqueById<T extends {
|
|
5
|
+
id: string;
|
|
6
|
+
}>(...lists: T[][]): T[];
|
|
@@ -7,3 +7,23 @@ export function sanitizeGamesSearch(search) {
|
|
|
7
7
|
const normalized = cleaned.trim().replace(/\s+/g, ' ');
|
|
8
8
|
return normalized.length > 0 ? normalized : '';
|
|
9
9
|
}
|
|
10
|
+
export function collapseGamesSearch(search) {
|
|
11
|
+
return sanitizeGamesSearch(search).replace(/\s+/g, '');
|
|
12
|
+
}
|
|
13
|
+
export function normalizeGameName(value) {
|
|
14
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
15
|
+
}
|
|
16
|
+
export function mergeUniqueById(...lists) {
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const result = [];
|
|
19
|
+
for (const list of lists) {
|
|
20
|
+
for (const item of list) {
|
|
21
|
+
if (seen.has(item.id)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
seen.add(item.id);
|
|
25
|
+
result.push(item);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|