@opexa/portal-components 0.0.35 → 0.0.36

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.
@@ -6,6 +6,9 @@ type KnownVariables = {
6
6
  [x: string]: unknown;
7
7
  };
8
8
  interface UseInfiniteQueryHelperReturn<T extends PaginatedQueryResult<unknown>> {
9
+ /**
10
+ * @description The rows for the current page.
11
+ */
9
12
  rows: Array<T['edges'][0]['node']>;
10
13
  page: number;
11
14
  setPage: (page: number) => void;
@@ -19,6 +22,11 @@ interface UseInfiniteQueryHelperReturn<T extends PaginatedQueryResult<unknown>>
19
22
  hasPrevPage: boolean;
20
23
  loading: boolean;
21
24
  firstPage: () => void;
25
+ /**
26
+ * @description All rows loaded so far, regardless of pagination.
27
+ */
28
+ availableRows: Array<T['edges'][0]['node']>;
29
+ totalAvailableRows: number;
22
30
  }
23
31
  export declare function useInfiniteQueryHelper<QueryData extends PaginatedQueryResult<unknown>, QueryVariables extends KnownVariables>(useQuery: InfiniteQuery<QueryData, QueryVariables>, variables: QueryVariables): UseInfiniteQueryHelperReturn<QueryData>;
24
32
  export declare namespace useInfiniteQueryHelper {
@@ -2,32 +2,32 @@ import { chunk } from 'lodash-es';
2
2
  import { useState } from 'react';
3
3
  export function useInfiniteQueryHelper(useQuery, variables) {
4
4
  const [page, setPage] = useState(1);
5
- const [pageSize, setPageSize_] = useState(10);
5
+ const [pageSize, setPageSize_] = useState(variables.first ?? 10);
6
6
  const setPageSize = (size) => {
7
7
  setPage(1);
8
8
  setPageSize_(size);
9
9
  };
10
10
  const query = useQuery({
11
- first: pageSize,
12
- after: null,
13
11
  ...variables,
12
+ first: pageSize,
14
13
  });
15
- const rowsLoaded = query.data?.pages
14
+ const availableRows = query.data?.pages
16
15
  .map((page) => page.edges.map((edge) => edge.node))
17
16
  .flat() ?? [];
18
- const pagesLoaded = chunk(rowsLoaded, pageSize);
19
- const totalPagesLoaded = Math.ceil(rowsLoaded.length / pageSize);
20
- const rows = pagesLoaded.at(page - 1) ?? [];
17
+ const availablePages = chunk(availableRows, pageSize);
18
+ const totalAvailableRows = availableRows.length;
19
+ const totalAAvailablePages = Math.ceil(availableRows.length / pageSize);
20
+ const rows = availablePages.at(page - 1) ?? [];
21
21
  const totalRows = query.data?.pages.at(-1)?.totalCount ?? 0;
22
22
  const totalPages = Math.ceil(totalRows / pageSize);
23
- const hasNextPage = page < totalPages;
23
+ const hasNextPage = query.data?.pages.at(-1)?.pageInfo.hasNextPage ?? false;
24
24
  const hasPrevPage = page > 1;
25
25
  const next = async () => {
26
26
  if (!hasNextPage) {
27
27
  return;
28
28
  }
29
29
  const nextPage = page + 1;
30
- if (totalPagesLoaded < nextPage) {
30
+ if (totalAAvailablePages < nextPage) {
31
31
  await query.fetchNextPage();
32
32
  }
33
33
  setPage((prev) => prev + 1);
@@ -56,6 +56,8 @@ export function useInfiniteQueryHelper(useQuery, variables) {
56
56
  hasPrevPage,
57
57
  loading,
58
58
  firstPage,
59
+ availableRows,
60
+ totalAvailableRows,
59
61
  };
60
62
  }
61
63
  useInfiniteQueryHelper.config = {
@@ -0,0 +1 @@
1
+ export declare function Game(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Image from 'next/image';
3
+ import { twMerge } from 'tailwind-merge';
4
+ import { useFavoriteGamesQuery, useMarkGameAsFavoriteMutation, useSessionQuery, useUnmarkGameAsFavoriteMutation, } from '../../client/hooks/index.js';
5
+ import { Star01Icon } from '../../icons/index.js';
6
+ import { GameLaunchTrigger } from '../GameLaunch/index.js';
7
+ import { useGameContext } from './GameContext.js';
8
+ export function Game() {
9
+ const game = useGameContext();
10
+ const sessionQuery = useSessionQuery();
11
+ const session = sessionQuery.data;
12
+ const favoriteGamesQuery = useFavoriteGamesQuery();
13
+ const favoriteGames = favoriteGamesQuery.data ?? [];
14
+ const markGameAsFavoriteMutation = useMarkGameAsFavoriteMutation();
15
+ const unmarkGameAsFavoriteMutation = useUnmarkGameAsFavoriteMutation();
16
+ const markedAsFavorite = favoriteGames.some((favoriteGame) => favoriteGame.id === game.id);
17
+ return (_jsxs(GameLaunchTrigger, { game: game, className: "relative block w-full shadow-sm", children: [session?.status === 'authenticated' && (_jsx("div", { role: "button", onClick: (e) => {
18
+ e.preventDefault();
19
+ e.stopPropagation();
20
+ if (markGameAsFavoriteMutation.isPending)
21
+ return;
22
+ if (unmarkGameAsFavoriteMutation.isPending)
23
+ return;
24
+ if (markedAsFavorite) {
25
+ unmarkGameAsFavoriteMutation.mutate(game.reference);
26
+ }
27
+ else {
28
+ markGameAsFavoriteMutation.mutate(game.reference);
29
+ }
30
+ }, "aria-disabled": markGameAsFavoriteMutation.isPending ||
31
+ unmarkGameAsFavoriteMutation.isPending, className: "rounded-xs absolute right-2 top-2 flex size-9 items-center justify-center bg-black/65 blur-[0.5px]", children: _jsx(Star01Icon, { className: twMerge('size-5', markedAsFavorite
32
+ ? 'fill-yellow-400 text-yellow-400'
33
+ : 'text-white') }) })), _jsx(Image, { src: game.image, alt: "", width: 200, height: 200, priority: true, className: "aspect-square w-full rounded-t-md object-cover" }), _jsx("span", { className: "bg-bg-tertiary text-text-primary-brand block w-full truncate rounded-b-md px-2 py-2.5 text-center text-xs font-semibold", children: game.name })] }));
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { Game } from '@opexa/portal-sdk-core';
2
+ export declare const GameContext: import("react").Context<Game | null>;
3
+ export declare const useGameContext: () => Game;
@@ -0,0 +1,8 @@
1
+ import { createContext, useContext } from 'react';
2
+ import invariant from 'tiny-invariant';
3
+ export const GameContext = createContext(null);
4
+ export const useGameContext = () => {
5
+ const context = useContext(GameContext);
6
+ invariant(context);
7
+ return context;
8
+ };
@@ -1,15 +1,8 @@
1
- import type { GamesInput } from '@opexa/portal-sdk-core';
2
- export interface GamesProps {
3
- /** @default "Games" */
4
- heading?: string;
5
- /**
6
- * @description URL for viewing all games
7
- * @default "/games"
8
- */
9
- viewAllUrl?: string;
10
- /** @default 1 */
11
- numOfRows?: 1 | 2 | 3;
12
- className?: string;
13
- filter?: GamesInput['filter'];
14
- }
1
+ import { type GamesCarouselProps } from './GamesCarousel';
2
+ import { type GamesListProps } from './GamesList';
3
+ export type GamesProps = ({
4
+ layout: 'list';
5
+ } & GamesListProps) | ({
6
+ layout?: 'carousel';
7
+ } & GamesCarouselProps);
15
8
  export declare function Games(props: GamesProps): Promise<import("react/jsx-runtime").JSX.Element>;
@@ -1,11 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { prefetchGamesQuery } from '../../server/utils/index.js';
3
- import { Games__client } from './Games.client.js';
2
+ import { GamesCarousel } from './GamesCarousel.js';
3
+ import { GamesList } from './GamesList.js';
4
4
  export async function Games(props) {
5
- const numOfRows = props.numOfRows ?? 1;
6
- await prefetchGamesQuery({
7
- first: (numOfRows + 1) * 6,
8
- filter: props.filter,
9
- });
10
- return _jsx(Games__client, { ...props });
5
+ return props.layout === 'list' ? (_jsx(GamesList, { ...props })) : (_jsx(GamesCarousel, { ...props }));
11
6
  }
@@ -0,0 +1,2 @@
1
+ import type { GamesCarouselProps } from './GamesCarousel';
2
+ export declare function GamesCarousel__client(props: GamesCarouselProps): import("react/jsx-runtime").JSX.Element;
@@ -1,15 +1,15 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import useEmblaCarousel, {} from 'embla-carousel-react';
4
- import Image from 'next/image';
5
4
  import Link from 'next/link';
6
5
  import { useCallback, useEffect, useState } from 'react';
7
6
  import { twMerge } from 'tailwind-merge';
8
- import { useFavoriteGamesQuery, useGamesQuery, useMarkGameAsFavoriteMutation, useSessionQuery, useUnmarkGameAsFavoriteMutation, } from '../../client/hooks/index.js';
9
- import { ArrowLeftIcon, ArrowRightIcon, ChevronRightIcon, SpinnerIcon, Star01Icon, } from '../../icons/index.js';
7
+ import { useGamesQuery } from '../../client/hooks/index.js';
8
+ import { ArrowLeftIcon, ArrowRightIcon, ChevronRightIcon, SpinnerIcon, } from '../../icons/index.js';
10
9
  import { Button } from '../../ui/index.js';
11
- import { GameLaunchTrigger } from '../GameLaunch/index.js';
12
- export function Games__client(props) {
10
+ import { Game } from './Game.js';
11
+ import { GameContext } from './GameContext.js';
12
+ export function GamesCarousel__client(props) {
13
13
  const numOfRows = props.numOfRows ?? 1;
14
14
  const gamesQuery = useGamesQuery({
15
15
  first: (numOfRows + 1) * 6,
@@ -54,31 +54,5 @@ export function Games__client(props) {
54
54
  ? 'grid-rows-3'
55
55
  : numOfRows === 2
56
56
  ? 'grid-rows-2'
57
- : 'grid-rows-1'), children: games.map((game) => (_jsx(Item, { game: game }, game.id))) }) })] }));
58
- }
59
- function Item({ game }) {
60
- const sessionQuery = useSessionQuery();
61
- const session = sessionQuery.data;
62
- const favoriteGamesQuery = useFavoriteGamesQuery();
63
- const favoriteGames = favoriteGamesQuery.data ?? [];
64
- const markGameAsFavoriteMutation = useMarkGameAsFavoriteMutation();
65
- const unmarkGameAsFavoriteMutation = useUnmarkGameAsFavoriteMutation();
66
- const markedAsFavorite = favoriteGames.some((favoriteGame) => favoriteGame.id === game.id);
67
- return (_jsxs(GameLaunchTrigger, { game: game, className: "relative block w-full shadow-sm", children: [session?.status === 'authenticated' && (_jsx("div", { role: "button", onClick: (e) => {
68
- e.preventDefault();
69
- e.stopPropagation();
70
- if (markGameAsFavoriteMutation.isPending)
71
- return;
72
- if (unmarkGameAsFavoriteMutation.isPending)
73
- return;
74
- if (markedAsFavorite) {
75
- unmarkGameAsFavoriteMutation.mutate(game.reference);
76
- }
77
- else {
78
- markGameAsFavoriteMutation.mutate(game.reference);
79
- }
80
- }, "aria-disabled": markGameAsFavoriteMutation.isPending ||
81
- unmarkGameAsFavoriteMutation.isPending, className: "rounded-xs absolute right-2 top-2 flex size-9 items-center justify-center bg-black/65 blur-[0.5px]", children: _jsx(Star01Icon, { className: twMerge('size-5', markedAsFavorite
82
- ? 'fill-yellow-400 text-yellow-400'
83
- : 'text-white') }) })), _jsx(Image, { src: game.image, alt: "", width: 200, height: 200, priority: true, className: "aspect-square w-full rounded-t-md object-cover" }), _jsx("span", { className: "bg-bg-tertiary text-text-primary-brand block w-full truncate rounded-b-md px-2 py-2.5 text-center text-xs font-semibold", children: game.name })] }));
57
+ : 'grid-rows-1'), children: games.map((game) => (_jsx(GameContext, { value: game, children: _jsx(Game, {}) }, game.id))) }) })] }));
84
58
  }
@@ -0,0 +1,15 @@
1
+ import type { GamesInput } from '@opexa/portal-sdk-core';
2
+ export interface GamesCarouselProps {
3
+ /** @default "Games" */
4
+ heading?: string;
5
+ /**
6
+ * @description URL for viewing all games
7
+ * @default "/games"
8
+ */
9
+ viewAllUrl?: string;
10
+ /** @default 1 */
11
+ numOfRows?: 1 | 2 | 3;
12
+ filter?: GamesInput['filter'];
13
+ className?: string;
14
+ }
15
+ export declare function GamesCarousel(props: GamesCarouselProps): Promise<import("react/jsx-runtime").JSX.Element>;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { prefetchGamesQuery } from '../../server/utils/index.js';
3
+ import { GamesCarousel__client } from './GamesCarousel.client.js';
4
+ export async function GamesCarousel(props) {
5
+ const numOfRows = props.numOfRows ?? 1;
6
+ await prefetchGamesQuery({
7
+ first: (numOfRows + 1) * 6,
8
+ filter: props.filter,
9
+ });
10
+ return _jsx(GamesCarousel__client, { ...props });
11
+ }
@@ -0,0 +1,2 @@
1
+ import type { GamesListProps } from './GamesList';
2
+ export declare function GamesList__client(props: GamesListProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { noop } from 'lodash-es';
4
+ import { useGamesQuery, useInfiniteQueryHelper } from '../../client/hooks/index.js';
5
+ import { Button, Progress } from '../../ui/index.js';
6
+ import { Game } from './Game.js';
7
+ import { GameContext } from './GameContext.js';
8
+ export function GamesList__client(props) {
9
+ const { availableRows, totalRows, totalAvailableRows, hasNextPage, next } = useInfiniteQueryHelper(useGamesQuery, {
10
+ first: props.first ?? 24,
11
+ filter: props.filter,
12
+ });
13
+ return (_jsxs("div", { className: props.className, children: [_jsx("h2", { className: "text-lg font-semibold", children: props.heading ?? 'Games' }), _jsx("div", { className: "mt-lg grid grid-cols-3 gap-1.5 lg:grid-cols-6 lg:gap-2.5", children: availableRows.map((game) => (_jsx(GameContext, { value: game, children: _jsx(Game, {}) }, game.id))) }), _jsxs("div", { className: "mt-3xl flex flex-col items-center", children: [_jsx(Progress.Root, { min: 0, max: totalRows, value: totalAvailableRows, onValueChange: noop, className: "w-[12.5rem]", children: _jsx(Progress.Track, { children: _jsx(Progress.Range, {}) }) }), _jsx("p", { className: "text-button-tertiary-fg mt-md text-sm", children: `Displaying ${totalAvailableRows} of ${totalRows}` }), hasNextPage && (_jsx(Button, { size: "sm", variant: "outline", fullWidth: false, onClick: next, className: "mt-lg", children: "Load More" }))] })] }));
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { GamesInput } from '@opexa/portal-sdk-core';
2
+ export interface GamesListProps {
3
+ /** @default 24 */
4
+ first?: number;
5
+ filter?: GamesInput['filter'];
6
+ /** @default "Games"*/
7
+ heading?: string;
8
+ className?: string;
9
+ }
10
+ export declare function GamesList(props: GamesListProps): Promise<import("react/jsx-runtime").JSX.Element>;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { prefetchGamesQuery } from '../../server/utils/index.js';
3
+ import { GamesList__client } from './GamesList.client.js';
4
+ export async function GamesList(props) {
5
+ await prefetchGamesQuery({
6
+ first: props.first ?? 24,
7
+ filter: props.filter,
8
+ });
9
+ return _jsx(GamesList__client, { ...props });
10
+ }
@@ -10,17 +10,29 @@ import { FacebookPixel, GoogleRecaptcha } from '../../third-parties/index.js';
10
10
  import { getQueryClient } from '../../utils/index.js';
11
11
  export function PortalProvider__client({ googleAnalyticsId, googleTagManagerId, googleRecaptchaSiteKey, facebookPixelId, children, }) {
12
12
  const queryClient = getQueryClient();
13
- return (_jsxs(QueryClientProvider, { client: queryClient, children: [children, _jsx(Toaster, { toaster: toaster, children: (toast) => (_jsxs(Toast.Root, { className: "shadow-xs border-gray-300 group z-[var(--z-index)] flex h-[var(--height)] w-[860px] max-w-full scale-[var(--scale)] items-center gap-lg rounded-xl border bg-white p-xl opacity-[var(--opacity)] transition-all duration-300 [translate:var(--x)_var(--y)_0]", children: [_jsx("div", { className: twMerge('flex size-9.5 items-center justify-center rounded-full border-2', toast.type === 'error'
14
- ? 'border-red-500/15'
15
- : 'border-green-500/15'), children: _jsx("div", { className: twMerge('flex size-7 items-center justify-center rounded-full border-2', toast.type === 'error'
16
- ? 'border-red-500/35'
17
- : 'border-green-500/35'), children: _jsx(AlertCircleIcon, { className: twMerge('size-5', toast.type === 'error' ? 'text-red-500' : 'text-green-500') }) }) }), _jsxs("div", { className: "grow text-sm leading-none", children: [_jsx(Toast.Title, { className: "font-semibold", children: toast.title
18
- ? toast.title
19
- : toast.type === 'error'
20
- ? 'Error'
21
- : 'Success' }), _jsx(Toast.Description, { className: "text-gray-700 mt-1", children: toast.description
22
- ? toast.description
23
- : toast.type === 'error'
24
- ? 'Something went wrong.'
25
- : 'Operation completed successfully.' })] }), _jsx(Toast.CloseTrigger, { className: "text-gray-400 hover:text-gray-500 self-start", children: _jsx(XIcon, { className: "size-5" }) })] }, toast.id)) }), facebookPixelId && _jsx(FacebookPixel, { fbId: facebookPixelId }), googleAnalyticsId && _jsx(GoogleAnalytics, { gaId: googleAnalyticsId }), googleTagManagerId && _jsx(GoogleTagManager, { gtmId: googleTagManagerId }), googleRecaptchaSiteKey && (_jsx(GoogleRecaptcha, { siteKey: googleRecaptchaSiteKey }))] }));
13
+ return (_jsxs(QueryClientProvider, { client: queryClient, children: [children, _jsx(Toaster, { toaster: toaster, children: (toast) => (_jsxs(Toast.Root, { className: twMerge('group', 'p-xl', 'border', 'border-border-primary', 'bg-bg-primary-alt', 'flex', 'items-start', 'gap-lg', 'w-[21.438rem]', 'lg:w-[1200px]', 'max-w-full', 'rounded-xl', 'shadow-xs', 'h-[var(--height)]', 'z-[var(--z-index)]', 'scale-[var(--scale)]', 'opacity-[var(--opacity)]', '[translate:var(--x)_var(--y)_0]', 'transition-all', 'duration-300'), children: [_jsxs("div", { className: "flex grow flex-col gap-xl lg:flex-row lg:gap-lg", children: [_jsx("div", { className: twMerge('flex size-9.5 items-center justify-center rounded-full border-2', toast.type === 'error'
14
+ ? 'border-text-error-primary/15'
15
+ : toast.type === 'warning'
16
+ ? 'border-text-warning-primary/15'
17
+ : 'border-text-success-primary/15'), children: _jsx("div", { className: twMerge('flex size-7 items-center justify-center rounded-full border-2', toast.type === 'error'
18
+ ? 'border-text-error-primary/35'
19
+ : toast.type === 'warning'
20
+ ? 'border-text-warning-primary/35'
21
+ : 'border-text-success-primary/35'), children: _jsx(AlertCircleIcon, { className: twMerge('size-5', toast.type === 'error'
22
+ ? 'text-text-error-primary'
23
+ : toast.type === 'warning'
24
+ ? 'text-text-warning-primary'
25
+ : 'text-text-success-primary') }) }) }), _jsxs("div", { className: "grow text-sm leading-none", children: [_jsx(Toast.Title, { className: "text-text-secondary-700 font-semibold", children: toast.title
26
+ ? toast.title
27
+ : toast.type === 'error'
28
+ ? 'Error'
29
+ : toast.type === 'warning'
30
+ ? 'Warning'
31
+ : 'Success' }), _jsx(Toast.Description, { className: "text-text-tertiary-600 mt-xs", children: toast.description
32
+ ? toast.description
33
+ : toast.type === 'error'
34
+ ? 'Something went wrong.'
35
+ : toast.type === 'warning'
36
+ ? 'Please be cautious.'
37
+ : 'Operation completed successfully.' })] })] }), _jsx(Toast.CloseTrigger, { className: "text-text-secondary-700", children: _jsx(XIcon, { className: "size-5" }) })] }, toast.id)) }), facebookPixelId && _jsx(FacebookPixel, { fbId: facebookPixelId }), googleAnalyticsId && _jsx(GoogleAnalytics, { gaId: googleAnalyticsId }), googleTagManagerId && _jsx(GoogleTagManager, { gtmId: googleTagManagerId }), googleRecaptchaSiteKey && (_jsx(GoogleRecaptcha, { siteKey: googleRecaptchaSiteKey }))] }));
26
38
  }