@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 sanitizedSearch = sanitizeGamesSearch(searchInput);
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
- sanitizedSearch.length < 3;
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
- let games = gamesQuery.data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)) ?? [];
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
- games = games.filter((game) => game.name.toLowerCase().includes(sanitizedSearch.toLowerCase()));
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-50", children: [_jsx(Combobox.Input, { placeholder: props.placeholder ?? 'Search' }), _jsx(Combobox.Context, { children: (api) => {
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-40 bg-black/50 backdrop-blur-sm", onClick: () => api.setOpen(false) }) }), _jsx(Combobox.Positioner, { children: searchInput.trim().length > 0 && (_jsx(Combobox.Content, { className: "z-50 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: () => {
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: gamesQuery.hasNextPage, children: _jsx(Button, { variant: "outline", className: twMerge('mx-auto mt-4xl w-fit', classNames.loadMoreButton), onClick: () => gamesQuery.fetchNextPage(), children: "Load More" }) })] }))] })) })) })] })) }) })] }) }));
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
- return value
29
- .toLowerCase()
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
- let games = gamesQuery.data?.pages.flatMap((page) => page.edges.map((edge) => edge.node)) ?? [];
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
- games = games.filter((game) => game.name.toLowerCase().includes(search.toLowerCase()));
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: gamesQuery.hasNextPage, children: _jsx(Button, { variant: "outline", className: twMerge('mx-auto mt-12 w-fit', classNames.loadMoreButton), onClick: () => gamesQuery.fetchNextPage(), children: "Load More" }) })] }))] })) }) })] }) })] }) }));
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opexa/portal-components",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "exports": {
5
5
  "./ui/*": {
6
6
  "types": "./dist/ui/*/index.d.ts",