Skip to content

Commit

Permalink
Merge pull request #1 from game-node-app/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Lamarcke authored Nov 6, 2024
2 parents f414006 + 2e7823f commit c160846
Show file tree
Hide file tree
Showing 29 changed files with 1,505 additions and 395 deletions.
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,20 @@
"@hookform/resolvers": "3.3.4",
"@lamarcke/openapi-typescript-codegen": "^0.29.1",
"@mantine/charts": "^7.13.4",
"@mantine/core": "^7.11.1",
"@mantine/dates": "^7.11.1",
"@mantine/core": "^7.13.4",
"@mantine/dates": "^7.13.4",
"@mantine/dropzone": "^7.11.1",
"@mantine/hooks": "^7.11.1",
"@mantine/hooks": "^7.13.4",
"@mantine/modals": "^7.11.1",
"@mantine/notifications": "^7.11.1",
"@mantine/nprogress": "^7.11.1",
"@tabler/icons-react": "^2.47.0",
"@tabler/icons-react": "^3.21.0",
"@tanstack/react-query": "5.18.1",
"@tanstack/react-table": "^8.19.2",
"dayjs": "^1.11.11",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"mantine-react-table": "^2.0.0-beta.7",
"next": "14.2.15",
"next": "^14.2.15",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.50.0",
Expand Down
23 changes: 23 additions & 0 deletions src/app/(dashboard)/dashboard/game/filter/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import { PageContainer } from "@/components/PageContainer/PageContainer";
import ExcludedGamesTable from "@/components/game/filter/ExcludedGamesTable";
import { Box, Text } from "@mantine/core";

const Page = () => {
return (
<PageContainer title={"Manage game exclusions"}>
<Box className={"w-10/12"}>
<Text className={"text-sm text-dimmed"}>
Excluded games won't show up in front-facing content, like
the home page, explore screen, or the activities page. They
can still be searched for, visited and be visible in user's
collections.
</Text>
</Box>

<ExcludedGamesTable />
</PageContainer>
);
};

export default Page;
11 changes: 11 additions & 0 deletions src/components/Navbar/Navbar.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
IconCheckbox,
IconComponents,
IconDashboard,
IconDeviceGamepad,
IconLock,
IconMoodSmile,
IconReport,
Expand All @@ -28,4 +29,14 @@ export const navLinks: NavItem[] = [
},
],
},
{
label: "Game",
icon: IconDeviceGamepad,
links: [
{
label: "Manage exclusions",
link: "/dashboard/game/filter",
},
],
},
];
65 changes: 65 additions & 0 deletions src/components/game/figure/GameFigureImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, {
ComponentProps,
PropsWithChildren,
useEffect,
useState,
} from "react";
import { AspectRatio, Image, ImageProps } from "@mantine/core";
import Link from "next/link";
import {
getSizedImageUrl,
ImageSize,
} from "@/components/game/util/getSizedImageUrl";
import { TGameOrSearchGame } from "@/components/game/util/types";
import { getCoverUrl } from "@/components/game/util/getCoverUrl";
import MainAppLink from "@/components/general/MainAppLink";

export interface IGameFigureProps
extends PropsWithChildren<Omit<ComponentProps<typeof Link>, "href">> {
game: TGameOrSearchGame | undefined;
onClick?: (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
imageProps?: ImageProps;
href?: string;
}

/**
* This component is the base building block for anything related to showing a game's metadata.
* It only handles logic related to image loading (skeletons, etc.).
*
* @param metadata
* @param href
* @constructor
*/
const GameFigureImage = ({
game,
imageProps,
href,
onClick,
children,
...others
}: IGameFigureProps) => {
const coverUrl = getCoverUrl(game);
const sizedCoverUrl = getSizedImageUrl(coverUrl, ImageSize.COVER_BIG);
const defaultHref = `/game/${game?.id}`;
return (
<MainAppLink
href={href ?? defaultHref}
className="w-full h-auto"
onClick={onClick}
{...others}
>
<AspectRatio ratio={264 / 354} pos="relative" h={"100%"} w={"auto"}>
<Image
radius={"sm"}
src={sizedCoverUrl ?? "/img/game_placeholder.jpeg"}
alt={"Game cover"}
className="w-full h-auto max-h-full"
{...imageProps}
/>
{children}
</AspectRatio>
</MainAppLink>
);
};

export default GameFigureImage;
249 changes: 249 additions & 0 deletions src/components/game/filter/ExcludedGamesTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
"use client";

import React, { useMemo, useState } from "react";
import { useExcludedGames } from "@/components/game/filter/hooks/useExcludedGames";
import {
MantineReactTable,
MRT_ColumnDef,
MRT_PaginationState,
} from "mantine-react-table";
import { useCustomTable } from "@/components/table/hooks/use-custom-table";
import {
ChangeExclusionStatusDto,
Game,
GameExclusion,
GameFilterService,
} from "@/wrapper/server";
import {
ActionIcon,
Badge,
Box,
Button,
Group,
MantineColor,
Menu,
Modal,
Paper,
Tooltip,
Text,
} from "@mantine/core";
import { UserAvatarGroup } from "@/components/general/avatar/UserAvatarGroup";
import { useGames } from "@/components/game/hooks/useGames";
import GameFigureImage from "@/components/game/figure/GameFigureImage";
import {
IconAdjustmentsPlus,
IconCirclePlus,
IconPlus,
IconSquarePlus,
} from "@tabler/icons-react";
import { useDisclosure } from "@mantine/hooks";
import AddGameExclusionForm from "@/components/game/filter/form/AddGameExclusionForm";
import { useMutation } from "@tanstack/react-query";
import { notifications } from "@mantine/notifications";
import { getErrorMessage } from "@/util/getErrorMessage";
import { modals } from "@mantine/modals";

// necessary because useMutation only allows for one parameter in mutationFn
interface ChangeExclusionMutationRequest extends ChangeExclusionStatusDto {
gameId: number;
}

interface GameExclusionWithGameInfo extends GameExclusion {
game: Game;
}

const columns: MRT_ColumnDef<GameExclusionWithGameInfo>[] = [
{
header: "Image",
id: "figure",
maxSize: 100,
Cell: ({ row }) => {
return (
<Box className={"max-w-20"}>
<GameFigureImage game={row.original.game} />
</Box>
);
},
},
{
header: "Game Id",
accessorKey: "targetGameId",
enableSorting: false,
},
{
header: "Name",
accessorKey: "game.name",
},
{
accessorFn: (row) => {
return row.isActive ? "Active" : "Inactive";
},
header: "Status",
filterVariant: "select",
mantineFilterSelectProps: {
data: [
{ label: "Active", value: "Active" },
{ label: "Inactive", value: "Inactive" },
],
},
Cell: ({ row, renderedCellValue }) => {
const item = row.original;
const color: MantineColor = item.isActive ? "green" : "red";
return <Badge color={color}>{renderedCellValue}</Badge>;
},
},
{
header: "Issued by",
accessorKey: "issuerUserId",
Cell: ({ row }) => {
return <UserAvatarGroup userId={row.original.issuerUserId} />;
},
},
{
header: "Created At",
accessorFn: (row) => new Date(row.createdAt).toLocaleString("en-US"),
sortingFn: (rowA, rowB, columnId) => {
const createDateA = new Date(rowA.original.createdAt);
const createDateB = new Date(rowB.original.createdAt);

return createDateA.getTime() - createDateB.getTime();
},
id: "createdAt",
},
];

const ExcludedGamesTable = () => {
const [pagination, setPagination] = useState<MRT_PaginationState>({
pageIndex: 0,
pageSize: 20,
});

const [addExclusionModalOpened, addExclusionModalUtils] = useDisclosure();

const offset = pagination.pageIndex * pagination.pageSize;
const limit = pagination.pageSize;

const { data, isLoading, isError, isFetching, invalidate } =
useExcludedGames(offset, limit);

const gameIds = useMemo(() => {
return data?.data.map((exclusion) => exclusion.targetGameId);
}, [data]);

const gamesQuery = useGames({
gameIds,
relations: {
cover: true,
},
});

const items = useMemo(() => {
if (data && gamesQuery.data) {
return data.data.map((exclusion): GameExclusionWithGameInfo => {
const relatedGame = gamesQuery.data.find(
(game) => game.id === exclusion.targetGameId,
)!;

return {
...exclusion,
game: relatedGame,
};
});
}
}, [data, gamesQuery.data]);

const changeStatusMutation = useMutation({
mutationFn: async (dto: ChangeExclusionMutationRequest) => {
await GameFilterService.gameFilterControllerChangeStatus(
dto.gameId,
{
isActive: dto.isActive,
},
);

return dto.isActive;
},
onSuccess: (isActive) => {
notifications.show({
color: "green",
message: `Sucessfully ${isActive ? "activated" : "deactivated"} filter.`,
});
},
onError: (err) => {
const msg = getErrorMessage(err);

notifications.show({
color: "red",
message: msg,
});
},
onSettled: () => {
invalidate();
},
});

const table = useCustomTable<GameExclusionWithGameInfo>({
columns: columns,
data: items ?? [],
state: {
isLoading: isLoading,
showAlertBanner: isError,
showProgressBars: isFetching,
pagination,
},
manualPagination: true,
onPaginationChange: setPagination,
rowCount: data?.pagination.totalItems ?? 0,
renderTopToolbarCustomActions: (table) => {
return (
<Group className={"w-full h-full items-center justify-end"}>
<Tooltip label={"Add exclusion"}>
<ActionIcon
variant={"subtle"}
color={"gray"}
size={"lg"}
onClick={addExclusionModalUtils.open}
>
<IconSquarePlus />
</ActionIcon>
</Tooltip>
</Group>
);
},
enableRowActions: true,
renderRowActionMenuItems: (tableItem) => {
const item = tableItem.row.original;
return (
<>
<Menu.Item
onClick={() => {
const dto: ChangeExclusionMutationRequest = {
gameId: item.targetGameId,
isActive: !item.isActive,
};
changeStatusMutation.mutate(dto);
}}
>
{item.isActive ? "Deactivate" : "Activate"}
</Menu.Item>
</>
);
},
});

return (
<Paper withBorder radius="md" p="md" mt="lg">
<Modal
opened={addExclusionModalOpened}
onClose={addExclusionModalUtils.close}
title={"Add game exclusion"}
size={"lg"}
>
<AddGameExclusionForm onClose={addExclusionModalUtils.close} />
</Modal>
<MantineReactTable table={table} />
</Paper>
);
};

export default ExcludedGamesTable;
Loading

0 comments on commit c160846

Please sign in to comment.