diff --git a/src/components/linkButton.tsx b/src/components/linkButton.tsx
new file mode 100644
index 00000000..5fefd798
--- /dev/null
+++ b/src/components/linkButton.tsx
@@ -0,0 +1,98 @@
+import { useContext, forwardRef } from "react";
+import Link from "next/link";
+import { createStyles } from "@mantine/core";
+import type { DifficultyColor } from "~/styles/difficultyColors";
+import { colorsForDifficultyIndex } from "~/styles/modsColors";
+import { currentDifficultyTabIndexContext } from "./mods/modsTable";
+
+
+
+
+const useStyles = createStyles(
+ (
+ _theme,
+ {
+ colors,
+ }: {
+ colors: DifficultyColor;
+ }
+ ) => {
+ return ({
+ button: {
+ backgroundColor: colors.primary.backgroundColor,
+ color: colors.primary.textColor,
+ /* left/right top/bottom */
+ padding: "2px 10px",
+ border: "none",
+ borderRadius: "8px",
+ "&:hover": {
+ backgroundColor: colors.primaryHover.backgroundColor,
+ color: colors.primaryHover.textColor,
+ }
+ },
+ });
+ }
+);
+
+
+
+
+type LinkButtonProps = {
+ children: React.ReactNode;
+ href: string;
+ onMouseEnter?: () => void;
+ onMouseLeave?: () => void;
+ linkWrapper?: boolean;
+};
+
+
+
+
+export const LinkButton = forwardRef<
+ HTMLAnchorElement,
+ LinkButtonProps
+>(
+ (
+ {
+ children,
+ href,
+ onMouseEnter,
+ onMouseLeave,
+ linkWrapper = true,
+ },
+ ref,
+ ) => {
+ const currentTabIndex = useContext(currentDifficultyTabIndexContext);
+
+ const colors = colorsForDifficultyIndex(currentTabIndex ?? 0); // default to beginner colors if no context is provided. null would give the highest valid difficulty's color.
+
+ const { classes } = useStyles({ colors });
+
+
+ return (
+ linkWrapper ? (
+
+ {children}
+
+ ) : (
+
+ {children}
+
+ )
+ );
+ }
+);
\ No newline at end of file
diff --git a/src/components/mods/expandedMod.tsx b/src/components/mods/expandedMod.tsx
index f514f847..9e34d4c4 100644
--- a/src/components/mods/expandedMod.tsx
+++ b/src/components/mods/expandedMod.tsx
@@ -1,11 +1,9 @@
-import { Flex, Group, Loader, Stack, createStyles } from "@mantine/core";
-import { Mod } from "~/components/mods/types";
-import Maps from "./maps/maps";
-import PublisherName from "./publisherName";
-import PublicationDate from "./publicationDate";
+import { Flex, Loader, Stack, Text, createStyles } from "@mantine/core";
+import type { ModWithInfo } from "~/components/mods/types";
+import { Maps } from "./maps/maps";
import { ModDownloadButton } from "./modDownloadButton/modDownloadButton";
-import Link from "next/link";
-import ModCarousel from "./modCarousel";
+import { ModCarousel } from "./modCarousel";
+import { LinkButton } from "~/components/linkButton";
import { COMING_SOON_PATHNAME } from "~/consts/pathnames";
import { expandedModColors } from "~/styles/expandedModColors";
import type { DifficultyColor } from "~/styles/difficultyColors";
@@ -29,12 +27,12 @@ const useStyles = createStyles(
// We move the expanded mod up to make
// the mod row and expanded mod look like a single row.
transform: "translate(0, -45px)",
- },
- moreInfo: {
- fontSize: "1rem",
+ paddingTop: "10px",
},
modDetails: {
- padding: "10px 25px",
+ width: "100%",
+ /** top and bottom | left and right */
+ padding: "0 20px",
}
}),
);
@@ -43,15 +41,15 @@ const useStyles = createStyles(
type ExpandedModProps = {
- isLoading: boolean,
- mod: Mod,
- colors: DifficultyColor,
+ isLoading: boolean;
+ mod: ModWithInfo;
+ colors: DifficultyColor;
};
-const ExpandedMod = ({
+export const ExpandedMod = ({
isLoading,
mod,
colors,
@@ -59,41 +57,51 @@ const ExpandedMod = ({
const isMapperNameVisiblePermitted = false;
- const publicationDateInSeconds = mod.timeCreatedGamebanana;
- const publicationDate = publicationDateInSeconds > 0 ? new Date(publicationDateInSeconds * 1000) : undefined;
-
-
const { classes } = useStyles();
if (isLoading) return ;
return (
-
-
-
-
-
-
+
+
+
- More Info
-
-
-
- id)}
+
+
+
+ More Info
+
+
+
+
-
-
-
+
+
);
-};
-
-export default ExpandedMod;
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/src/components/mods/maps/maps.tsx b/src/components/mods/maps/maps.tsx
index 58c064e4..add22402 100644
--- a/src/components/mods/maps/maps.tsx
+++ b/src/components/mods/maps/maps.tsx
@@ -1,133 +1,132 @@
import { Stack, Title } from "@mantine/core";
-import type { Difficulty, Length, Map, MapRatingData, MapYesRatingData, Quality } from "~/components/mods/types";
+import type { Difficulty, Length, MapRatingData, MapYesRatingData, MapWithTechInfo, MapWithTechAndRatingInfo, Quality, Mod } from "~/components/mods/types";
import { api } from "~/utils/api";
import { useMemo } from "react";
import { noRatingsFoundMessage } from "~/consts/noRatingsFoundMessage";
-import MapsTable from "./mapsTable";
+import { MapsTable } from "./mapsTable";
import type { DifficultyColor } from "~/styles/difficultyColors";
-type MapWithInfo = {
- lengthName: string,
- overallCount: number,
- qualityName: string,
- qualityCount: number,
- difficultyName: string,
- difficultyCount: number,
- chapterSide?: string;
-} & Map;
-
-
export type MapsProps = {
isLoadingMod: boolean;
- isNormalMod: boolean;
+ modType: Mod["type"];
isMapperNameVisiblePermitted: boolean;
- mapIds: number[];
+ mapsWithTechInfo: MapWithTechInfo[];
colors: DifficultyColor;
};
-const getMapsWithInfo = (isLoading: boolean, maps: Map[], ratingsFromMapIds: MapRatingData[], lengths: Length[], qualities: Quality[], difficulties: Difficulty[]): MapWithInfo[] => {
+const getMapsWithTechAndRatingInfo = (
+ isLoading: boolean,
+ mapsWithTechInfo: MapWithTechInfo[],
+ ratingsFromMapIds: MapRatingData[],
+ lengths: Length[],
+ qualities: Quality[],
+ difficulties: Difficulty[],
+): MapWithTechAndRatingInfo[] => {
if (isLoading) return [];
- const mapsWithInfo: MapWithInfo[] = maps.map((map) => {
- const rating = ratingsFromMapIds.find((rating) => rating.mapId === map.id);
+ const mapsWithTechAndRatingInfo: MapWithTechAndRatingInfo[] = mapsWithTechInfo.map(
+ (mapWithTechInfo) => {
+ const rating = ratingsFromMapIds.find((rating) => rating.mapId === mapWithTechInfo.id);
- if (rating === undefined) throw `Map ${map.id} has an undefined rating - this should not happen.`;
+ if (rating === undefined) throw `Map ${mapWithTechInfo.id} has an undefined rating - this should not happen.`;
- //get length info from map
- const length = lengths.find((length) => length.id === map.lengthId);
+ //get length info from map
+ const length = lengths.find((length) => length.id === mapWithTechInfo.lengthId);
- if (!length) throw `Length ${map.lengthId}, linked to by map ${map.id}, not found. This should not happen.`;
+ if (!length) throw `Length ${mapWithTechInfo.lengthId}, linked to map ${mapWithTechInfo.id}, not found. This should not happen.`;
- //get quality and difficulty info from rating. this is more complicated than lengths because ratings are optional.
- let overallCount = 0;
- let qualityId = -1;
- let qualityName: string;
- let qualityCount = 0;
- let difficultyId = -1;
- let difficultyName: string;
- let difficultyCount = 0;
+ //get quality and difficulty info from rating. this is more complicated than lengths because ratings are optional.
+ let overallCount = 0;
+ let qualityId = -1;
+ let qualityName: string;
+ let qualityCount = 0;
+ let difficultyId = -1;
+ let difficultyName: string;
+ let difficultyCount = 0;
- if ("overallCount" in rating === false) { //no ratings exist for this map
- qualityName = noRatingsFoundMessage;
- difficultyName = noRatingsFoundMessage;
- } else { //ratings exist for this map
- const narrowedRating = rating as MapYesRatingData;
+ if ("overallCount" in rating === false) { //no ratings exist for this map
+ qualityName = noRatingsFoundMessage;
+ difficultyName = noRatingsFoundMessage;
+ } else { //ratings exist for this map
+ const narrowedRating = rating as MapYesRatingData;
- overallCount = narrowedRating.overallCount;
- qualityCount = narrowedRating.qualityCount;
- difficultyCount = narrowedRating.difficultyCount;
+ overallCount = narrowedRating.overallCount;
+ qualityCount = narrowedRating.qualityCount;
+ difficultyCount = narrowedRating.difficultyCount;
- if (narrowedRating.averageQualityId) qualityId = narrowedRating.averageQualityId;
+ if (narrowedRating.averageQualityId) qualityId = narrowedRating.averageQualityId;
- if (narrowedRating.averageDifficultyId) difficultyId = narrowedRating.averageDifficultyId;
- }
+ if (narrowedRating.averageDifficultyId) difficultyId = narrowedRating.averageDifficultyId;
+ }
- if (qualityId === -1) qualityName = noRatingsFoundMessage;
- else {
- if (qualityCount === 0) throw `Quality count is 0 for map ${map.id} but qualityId is ${qualityId} (and not -1) - this should not happen.`;
+ if (qualityId === -1) qualityName = noRatingsFoundMessage;
+ else {
+ if (qualityCount === 0) throw `Quality count is 0 for map ${mapWithTechInfo.id} but qualityId is ${qualityId} (and not -1) - this should not happen.`;
- const quality = qualities.find((quality) => quality.id === qualityId);
+ const quality = qualities.find((quality) => quality.id === qualityId);
- if (!quality) throw `Quality ${qualityId} not found. This should not happen.`;
+ if (!quality) throw `Quality ${qualityId} not found. This should not happen.`;
- qualityName = quality.name;
- }
+ qualityName = quality.name;
+ }
- if (difficultyId === -1) difficultyName = noRatingsFoundMessage;
- else {
- if (difficultyCount === 0) throw `Difficulty count is 0 for map ${map.id} but difficultyId is ${difficultyId} (and not -1) - this should not happen.`;
+ if (difficultyId === -1) difficultyName = noRatingsFoundMessage;
+ else {
+ if (difficultyCount === 0) throw `Difficulty count is 0 for map ${mapWithTechInfo.id} but difficultyId is ${difficultyId} (and not -1) - this should not happen.`;
- const difficulty = difficulties.find((difficulty) => difficulty.id === difficultyId);
+ const difficulty = difficulties.find((difficulty) => difficulty.id === difficultyId);
- if (!difficulty) throw `Difficulty ${difficultyId} not found. This should not happen.`;
+ if (!difficulty) throw `Difficulty ${difficultyId} not found. This should not happen.`;
- difficultyName = difficulty.name;
- }
+ difficultyName = difficulty.name;
+ }
- const chapterSide = `${map.chapter ?? ""}${map.side ?? ""}`;
+ const chapterSide = `${mapWithTechInfo.chapter ?? ""}${mapWithTechInfo.side ?? ""}`;
- return {
- ...map,
- lengthName: length.name,
- overallCount,
- qualityName,
- qualityCount,
- difficultyName,
- difficultyCount,
- chapterSide,
- };
- });
+ return {
+ ...mapWithTechInfo,
+ lengthName: length.name,
+ lengthDescription: length.description,
+ overallCount,
+ qualityName,
+ qualityCount,
+ difficultyName,
+ difficultyCount,
+ chapterSide,
+ };
+ }
+ );
- return mapsWithInfo;
+ return mapsWithTechAndRatingInfo;
};
-const Maps = ({
+export const Maps = ({
isLoadingMod,
- isNormalMod,
+ modType,
isMapperNameVisiblePermitted,
- mapIds,
+ mapsWithTechInfo,
colors,
}: MapsProps) => {
//get common data
@@ -141,50 +140,21 @@ const Maps = ({
const lengths = lengthQuery.data ?? [];
- //get maps data
- const mapsQueries = api.useQueries(
- (useQueriesApi) => mapIds.map(
- (id) => useQueriesApi.map.getById(
- { id },
- { queryKey: ["map.getById", { id, tableName: "Map" }] },
- ),
- ),
- );
-
- const isLoadingMaps = isLoadingMod || mapsQueries.some((query) => query.isLoading);
-
- const maps = useMemo(() => {
- if (isLoadingMaps) return [];
-
-
- const maps_maybeEmpty: Map[] = [];
-
- mapsQueries.forEach((mapQuery) => {
- const map = mapQuery.data;
-
- if (map) maps_maybeEmpty.push(map);
- });
-
- if (!maps_maybeEmpty.length) console.log(`maps_maybeEmpty is empty. mapIds = "${mapIds}"`);
-
-
- if (isNormalMod) return maps_maybeEmpty.sort((a, b) => a.name.localeCompare(b.name));
-
- return maps_maybeEmpty;
- }, [isLoadingMaps, mapsQueries, mapIds, isNormalMod]);
-
-
//get ratings data
const ratingQueries = api.useQueries(
- (useQueriesApi) => mapIds.map(
- (id) => useQueriesApi.rating.getMapRatingData(
- { mapId: id },
- { queryKey: ["rating.getMapRatingData", { mapId: id }] },
- ),
- ),
+ (useQueriesApi) => {
+ if (isLoadingMod) return [];
+
+ return mapsWithTechInfo.map(
+ (mapWithTechInfo) => useQueriesApi.rating.getMapRatingData(
+ { mapId: mapWithTechInfo.id },
+ { queryKey: ["rating.getMapRatingData", { mapId: mapWithTechInfo.id }] },
+ ),
+ );
+ },
);
- const isLoadingRatings = isLoadingMaps || ratingQueries.some((query) => query.isLoading);
+ const isLoadingRatings = isLoadingMod || ratingQueries.some((query) => query.isLoading);
const ratingsFromMapIds = useMemo(() => {
if (isLoadingRatings) return [];
@@ -197,28 +167,26 @@ const Maps = ({
if (rating !== undefined) ratings_maybeEmpty.push(rating);
});
- if (!ratings_maybeEmpty.length) console.log(`ratings_maybeEmpty is empty. mapIds = "${mapIds}"`);
+ if (!ratings_maybeEmpty.length) console.log(`ratings_maybeEmpty is empty. mapsWithTechInfo = "${mapsWithTechInfo}"`);
return ratings_maybeEmpty;
- }, [isLoadingRatings, ratingQueries, mapIds]); //TODO: figure out if mapIds can be removed from this dependency array
+ }, [isLoadingRatings, ratingQueries, mapsWithTechInfo]); //TODO: figure out if mapsWithTechInfo can be removed from this dependency array
//check that all data is loaded
- const isLoading = isLoadingMaps || isLoadingRatings || qualityQuery.isLoading || difficultyQuery.isLoading || lengthQuery.isLoading;
+ const isLoading = isLoadingRatings || qualityQuery.isLoading || difficultyQuery.isLoading || lengthQuery.isLoading;
//get maps with quality, difficulty, and length names
- const mapsWithInfo = useMemo(
- () => getMapsWithInfo(isLoading, maps, ratingsFromMapIds, lengths, qualities, difficulties),
- [isLoading, maps, ratingsFromMapIds, qualities, difficulties, lengths],
+ const mapsWithTechAndRatingInfo = useMemo(
+ () => getMapsWithTechAndRatingInfo(isLoading, mapsWithTechInfo, ratingsFromMapIds, lengths, qualities, difficulties),
+ [isLoading, mapsWithTechInfo, ratingsFromMapIds, qualities, difficulties, lengths],
);
return (
Maps
-
+
);
-};
-
-export default Maps;
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/src/components/mods/maps/mapsTable.tsx b/src/components/mods/maps/mapsTable.tsx
index 5d30c2e8..7665069e 100644
--- a/src/components/mods/maps/mapsTable.tsx
+++ b/src/components/mods/maps/mapsTable.tsx
@@ -1,11 +1,15 @@
-import { ActionIcon, createStyles } from "@mantine/core";
-import { DataTable, type DataTableSortStatus } from "mantine-datatable";
-import type { Map } from "~/components/mods/types";
import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react";
+import Link from "next/link";
+import { ActionIcon, Text, createStyles } from "@mantine/core";
+import { DataTable, type DataTableSortStatus } from "mantine-datatable";
+import type { MapWithTechAndRatingInfo, Mod } from "~/components/mods/types";
import { CirclePlus } from "tabler-icons-react";
+import { ModsTableTooltip } from "../modsTableTooltip";
import { expandedModColors } from "~/styles/expandedModColors";
import { TABLE_HEADER_ARROW_ZOOM } from "~/consts/tableHeaderArrowZoom";
import type { DifficultyColor } from "~/styles/difficultyColors";
+import { getOrdinal } from "~/utils/getOrdinal";
+import { COMING_SOON_PATHNAME } from "~/consts/pathnames";
@@ -22,11 +26,14 @@ const useStyles = createStyles(
margin: `0 ${theme.spacing.sm} ${theme.spacing.xl}`,
backgroundColor: expandedModColors.default.backgroundColor,
},
- "&&&& table": {
+ "&&&&&& table": {
borderSpacing: "0 20px",
// Border spacing adds space before the header, so we move the table up
transform: 'translate(0, -20px)',
},
+ "&&&&&& thead": {
+ top: "0",
+ },
"&&&& th": {
fontWeight: "bold",
border: "none",
@@ -99,26 +106,15 @@ const useStyles = createStyles(
-type MapWithInfo = {
- lengthName: string,
- overallCount: number,
- qualityName: string,
- qualityCount: number,
- difficultyName: string,
- difficultyCount: number,
- chapterSide?: string;
-} & Map;
-
-
type MapsTableSortStatus = {
- columnAccessor: keyof MapWithInfo; //narrow from "typeof string"
+ columnAccessor: keyof MapWithTechAndRatingInfo; //narrow from "typeof string"
} & DataTableSortStatus;
export type MapsTableProps = {
- isNormalMod: boolean;
+ modType: Mod["type"];
isMapperNameVisiblePermitted: boolean;
- mapsWithInfo: MapWithInfo[];
+ mapsWithTechAndRatingInfo: MapWithTechAndRatingInfo[];
isLoading: boolean;
colors: DifficultyColor;
};
@@ -141,15 +137,18 @@ const getSortStatusFromIsNormalMod = (isNormalMod: boolean): MapsTableSortStatus
-const MapsTable = (
+export const MapsTable = (
{
- isNormalMod,
+ modType,
isMapperNameVisiblePermitted,
- mapsWithInfo,
+ mapsWithTechAndRatingInfo,
isLoading,
colors,
}: MapsTableProps
) => {
+ const isNormalMod = modType === "Normal";
+
+
//handle sorting
const [sortStatus, setSortStatus] = useState(getSortStatusFromIsNormalMod(isNormalMod));
@@ -159,7 +158,7 @@ const MapsTable = (
);
const sortedMapsWithInfo = useMemo(() => {
- const sortedMaps = [...mapsWithInfo].sort(
+ const sortedMaps = [...mapsWithTechAndRatingInfo].sort(
(a, b) => {
const columnAccessor = sortStatus.columnAccessor;
@@ -178,14 +177,14 @@ const MapsTable = (
return sortedMaps;
- }, [mapsWithInfo, sortStatus]);
+ }, [mapsWithTechAndRatingInfo, sortStatus]);
//handle mapper name visibility
const isMapperNameVisible = !isNormalMod && isMapperNameVisiblePermitted;
- const { cx, classes } = useStyles({ colors });
+ const { classes } = useStyles({ colors });
return (
{
+ const { name, chapterSide, overallRank } = mapWithTechAndRatingInfo;
+
+
+ let dropdownBaseString: string | undefined = undefined;
+
+ if (modType === "Normal") {
+ if (chapterSide === undefined) throw `chapterSide is undefined for map ${mapWithTechAndRatingInfo.id} in a Normal mod.`;
+
+ dropdownBaseString = `Level: ${chapterSide}.`;
+ } else if (modType === "Contest") {
+ if (overallRank === null) dropdownBaseString = "";
+ else dropdownBaseString = `Place: ${getOrdinal(overallRank, false)}.`;
+ }
+
+
+ const mapNameStringForTooltip = `Map: ${name}.`;
+
+ return (
+ dropdownBaseString === undefined ? (
+
+ {name}
+
+ ) : (
+
+ )
+ );
+ },
titleClassName: classes.leftColumnTitle,
cellsClassName: classes.leftColumnCells,
},
{
accessor: "qualityName",
title: "Quality",
+ ellipsis: true,
+ render: (mapWithTechAndRatingInfo) => {
+ if (mapWithTechAndRatingInfo.qualityCount === 0) return (
+
+ {mapWithTechAndRatingInfo.qualityName}
+
+ );
+
+ return (
+
+ );
+ },
cellsClassName: classes.columnCells,
},
{
accessor: "difficultyName",
title: "Difficulty",
+ ellipsis: true,
+ render: (mapWithTechAndRatingInfo) => {
+ const difficultyNameFromMap = mapWithTechAndRatingInfo.difficultyName;
+
+ let difficultyStringForDisplay: string;
+ if (mapWithTechAndRatingInfo.difficultyCount === 0) {
+ difficultyStringForDisplay = mapWithTechAndRatingInfo.difficultyName;
+ } else {
+ const [parentDifficulty, childDifficulty] = difficultyNameFromMap.split(": ");
+
+ if (parentDifficulty === undefined || childDifficulty === undefined) return "";
+
+ difficultyStringForDisplay = `${childDifficulty} ${parentDifficulty}`;
+ }
+
+
+ if (mapWithTechAndRatingInfo.difficultyCount === 0) return (
+
+ {mapWithTechAndRatingInfo.difficultyName}
+
+ );
+
+ return (
+
+ );
+ },
cellsClassName: classes.columnCells,
},
{
accessor: "lengthName",
title: "Length",
+ ellipsis: true,
+ render: (mapWithTechAndRatingInfo) => (
+
+ ),
cellsClassName: classes.columnCells,
},
{
accessor: "mapperNameString",
title: "Mapper Name",
+ ellipsis: true,
+ render: (mapWithTechAndRatingInfo) => (
+
+ ),
hidden: !isMapperNameVisible,
cellsClassName: classes.columnCells,
},
{
accessor: "rate",
title: "Rate",
- render: (_) => (
-
-
-
+ ellipsis: true,
+ render: (_mapWithTechAndRatingInfo) => (
+
+
+
+
+
+ )}
+ />
+
),
titleClassName: classes.rightColumnTitle,
cellsClassName: classes.rightColumnCells,
@@ -242,6 +356,4 @@ const MapsTable = (
onSortStatusChange={setSortStatus as Dispatch>} //un-narrow type to match types in DataTable
/>
);
-};
-
-export default MapsTable;
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/src/components/mods/modCarousel.tsx b/src/components/mods/modCarousel.tsx
index 369b1c20..99419770 100644
--- a/src/components/mods/modCarousel.tsx
+++ b/src/components/mods/modCarousel.tsx
@@ -10,7 +10,7 @@ import type { DifficultyColor } from "~/styles/difficultyColors";
const useStyles = createStyles(
(
- theme,
+ _theme,
{ colors }: { colors: DifficultyColor; },
) => ({
carousel: {
@@ -21,7 +21,8 @@ const useStyles = createStyles(
flexDirection: "column",
alignItems: "stretch",
gap: "10px",
- padding: "20px",
+ /** top | left and right | bottom */
+ padding: "21px 20px 20px",
},
},
viewport: {
@@ -66,7 +67,7 @@ type modCarouselProps = {
-const ModCarousel = ({ gamebananaModId, numberOfMaps, colors }: modCarouselProps) => {
+export const ModCarousel = ({ gamebananaModId, numberOfMaps, colors }: modCarouselProps) => {
const { imageUrls } = useGamebananaModImageUrls({ gamebananaModId });
@@ -99,7 +100,4 @@ const ModCarousel = ({ gamebananaModId, numberOfMaps, colors }: modCarouselProps
)
);
-};
-
-
-export default ModCarousel;
+};
\ No newline at end of file
diff --git a/src/components/mods/modDownloadButton/modDownloadButton.tsx b/src/components/mods/modDownloadButton/modDownloadButton.tsx
index c3cdfd91..e8ef47b2 100644
--- a/src/components/mods/modDownloadButton/modDownloadButton.tsx
+++ b/src/components/mods/modDownloadButton/modDownloadButton.tsx
@@ -1,15 +1,11 @@
-
-import { useContext } from "react";
import Link from "next/link";
import Image from "next/image";
import { Group, Popover, Text, createStyles } from "@mantine/core";
import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
+import { LinkButton } from "~/components/linkButton";
import { useGamebananaModDownloadUrl } from "~/hooks/gamebananaApi";
import { FAQ_PAGE_PATHNAME } from "~/consts/pathnames";
import { OLYMPUS_INSTALLATION_URL } from "~/consts/olympusInstallationUrl";
-import { type DifficultyColor } from "~/styles/difficultyColors";
-import { colorsForDifficultyIndex } from "~/styles/modsColors";
-import { currentDifficultyTabIndexContext } from "../modsTable";
import everestLogo from "../../../../public/images/everest-logo/everest-logo.png";
@@ -23,24 +19,8 @@ type ModDownloadButtonProps = {
const useStyles = createStyles(
- (
- theme,
- {
- colors,
- isOpened,
- }: {
- colors: DifficultyColor;
- isOpened: boolean;
- }
- ) => {
+ (theme) => {
return ({
- downloadButton: {
- backgroundColor: isOpened ? colors.primaryHover.backgroundColor : colors.primary.backgroundColor,
- color: isOpened ? colors.primaryHover.textColor : colors.primary.textColor,
- /* left/right top/bottom */
- padding: "2px 10px",
- borderRadius: "8px",
- },
dropdown: {
'&&': {
backgroundColor: theme.white,
@@ -75,26 +55,23 @@ export const ModDownloadButton = ({ gamebananaModId }: ModDownloadButtonProps) =
const [debouncedIsOpened] = useDebouncedValue(isOpened, 110);
- const currentTabIndex = useContext(currentDifficultyTabIndexContext);
-
-
- const colors = colorsForDifficultyIndex(currentTabIndex);
-
- const { classes } = useStyles({ colors, isOpened });
+ const { classes } = useStyles();
return (
-
+ classNames={{ dropdown: classes.dropdown, arrow: classes.arrow }}
+ >
-
-
+
1-Click Install
-
+
-
+ onMouseLeave={close}
+ >
+
Install the mod directly using Olympus, a mod manager for Celeste.
-
+
You can also use one of the other methods.
diff --git a/src/components/mods/modsTable.tsx b/src/components/mods/modsTable.tsx
index 597eabdf..1ee15bef 100644
--- a/src/components/mods/modsTable.tsx
+++ b/src/components/mods/modsTable.tsx
@@ -1,23 +1,25 @@
import { DataTable, DataTableSortStatus } from "mantine-datatable";
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState, createContext } from "react";
import { createPortal } from "react-dom";
-import ExpandedMod from "~/components/mods/expandedMod";
-import { createStyles } from "@mantine/core";
+import { ExpandedMod } from "~/components/mods/expandedMod";
+import { createStyles, Text } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { Difficulty, Quality } from "~/components/mods/types";
-import { type ModType, ModType as modTypes } from "@prisma/client";
+import type { ModType, Publisher as PrismaPublisher, Mod } from "@prisma/client";
import { StringSearch } from "~/components/filterPopovers/stringSearch";
import { NumberSearch } from "~/components/filterPopovers/numberSearch";
import { ListSelect } from "~/components/filterPopovers/listSelect";
-import { getNonEmptyArray } from "~/utils/getNonEmptyArray";
-import type { ModWithInfo } from "~/components/mods/types";
+import { ModsTableTooltip } from "./modsTableTooltip";
+import { truncateString } from "~/utils/truncateString";
+import type { ModWithInfo, Tech } from "~/components/mods/types";
import { noRatingsFoundMessage } from "~/consts/noRatingsFoundMessage";
+import { defaultToLocaleDateStringOptions } from "~/consts/defaultToLocaleDateStringOptions";
import { colorsForDifficultyIndex, greatestValidDifficultyIndex } from "~/styles/modsColors";
import { canonicalDifficultyNames, difficultyColors, type DifficultyColor } from "~/styles/difficultyColors";
import { expandedModColors } from "~/styles/expandedModColors";
import { TABLE_HEADER_ARROW_ZOOM } from "~/consts/tableHeaderArrowZoom";
-import { blackBackgroundColor } from "~/styles/layoutColors";
import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels";
+import { blackBackgroundColor } from "~/styles/layoutColors";
@@ -26,8 +28,18 @@ export const currentDifficultyTabIndexContext = createContext(nul
const PAGE_SIZES = [5, 10, 15, 20, 25, 50, 100, 250, 500, 1000];
const DEFAULT_PAGE_SIZE_INDEX = 1;
+const QUERY_DEBOUNCE_TIME_MILLISECONDS = 200;
const ACTIVE_DIFFICULTY_TAB_BORDER_HEIGHT = "2px";
+// TODO: remove these parameters, limit the width of the columns in the table in some way, and let the datatable columns' `ellipsis` property handle the overflow.
+const NAME_COLUMN_MAX_LETTERS = 35;
+const PUBLISHER_COLUMN_MAX_LETTERS = 15;
+const TECHS_COLUMN_MAX_LETTERS = 20;
+
+
+/** Easiest difficulty first */
+const defaultDifficultyNamesForFallback = canonicalDifficultyNames.map(lowercaseDifficultyName => lowercaseDifficultyName.charAt(0).toUpperCase() + lowercaseDifficultyName.slice(1));
+
const useStyles = createStyles(
(
@@ -136,8 +148,8 @@ const useStyles = createStyles(
modCell: {
// 4 ampersands to increase selectivity of class to ensure it overrides any other css
"&&&&": {
- /* top | left and right | bottom */
- padding: `${theme.spacing.sm} ${theme.spacing.xl} ${theme.spacing.sm}`,
+ /* top and bottom | left and right */
+ padding: `${theme.spacing.sm} ${theme.spacing.xl}`,
backgroundColor: expandedModColors.default.backgroundColor,
color: expandedModColors.default.textColor,
borderWidth: 0,
@@ -247,14 +259,23 @@ const useStyles = createStyles(
+type AdditionalColumnAccessor = "qualityCount" | "difficultyCount";
+
+type ColumnAccessor = keyof ModWithInfo | AdditionalColumnAccessor;
+
+type ExtendedModWithInfo = {
+ [key in AdditionalColumnAccessor]: number;
+} & ModWithInfo;
+
type ModsTableSortStatus = {
- columnAccessor: keyof ModWithInfo; //narrow from "typeof string"
+ columnAccessor: ColumnAccessor; // narrow from "typeof string"
} & DataTableSortStatus;
type ModsTableProps = {
qualities: Quality[];
difficulties: Difficulty[];
+ techs: Tech[];
modsWithInfo: ModWithInfo[];
isLoading: boolean;
};
@@ -264,10 +285,32 @@ type ModsTableProps = {
// We create a seperate ModsTable component to prevent the Mods queries
// running again when the ModsTable state changes.
-export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }: ModsTableProps) => {
+export const ModsTable = ({ qualities, difficulties, techs, modsWithInfo, isLoading }: ModsTableProps) => {
const [currentTabIndex, setCurrentTabIndex] = useState(null); //track the currently selected parent difficulty
+ const techNames = useMemo( // get tech names for filter component
+ () => [...techs]
+ .sort( // sort first by difficulty order, then by name
+ (a, b) => {
+ const aDifficulty = difficulties.find(difficulty => difficulty.id === a.difficultyId);
+ if (!aDifficulty) throw `Difficulty ${a.difficultyId} doesn't exist. Tech ${a.id} is invalid.`;
+
+ const bDifficulty = difficulties.find(difficulty => difficulty.id === b.difficultyId);
+ if (!bDifficulty) throw `Difficulty ${b.difficultyId} doesn't exist. Tech ${b.id} is invalid.`;
+
+ if (aDifficulty.order !== bDifficulty.order) { // sort by difficulty order
+ return bDifficulty.order - aDifficulty.order; // harder difficulties have higher orders, and we want them to sort first
+ }
+
+ return a.name.localeCompare(b.name); // sort by name
+ }
+ )
+ .map((tech) => tech.name),
+ [qualities],
+ );
+
+
const qualityNames = useMemo( //get quality names for filter component
() => [...qualities]
.sort((a, b) => b.order - a.order) //better qualities have higher orders, so we want them to sort first
@@ -278,10 +321,16 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
const parentDifficultyNames = useMemo( //get parent difficulty names for filter component
- () => difficulties
- .filter((difficulty) => difficulty.parentDifficultyId === 0) //parent difficulties all have the nullParent difficulty, with id = 0, as their parent
- .sort((a, b) => a.order - b.order) //easier difficulties have lower orders, and we want them to sort first
- .map((difficulty) => difficulty.name),
+ () => {
+ if (difficulties.length === 0) {
+ return defaultDifficultyNamesForFallback;
+ }
+
+ return difficulties
+ .filter((difficulty) => difficulty.parentDifficultyId === 0) //parent difficulties all have the nullParent difficulty, with id = 0, as their parent
+ .sort((a, b) => a.order - b.order) //easier difficulties have lower orders, and we want them to sort first
+ .map((difficulty) => difficulty.name);
+ },
[difficulties],
);
@@ -328,28 +377,67 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
[difficulties, modsWithInfo, parentDifficultyNames, currentTabIndex],
);
+
+
+
//handle filtering
const [nameQuery, setNameQuery] = useState("");
- const [debouncedNameQuery, _cancelDebouncedNameQueryChange] = useDebouncedValue(nameQuery, 200);
+ const [debouncedNameQuery, _cancelDebouncedNameQueryChange] = useDebouncedValue(nameQuery, QUERY_DEBOUNCE_TIME_MILLISECONDS);
const isNameFiltered = nameQuery !== "";
- const [mapCountRange, setMapCountRange] = useState<[number | undefined, number | undefined]>([undefined, undefined]); //[min, max]
- const isMapCountFiltered = mapCountRange[0] !== undefined || mapCountRange[1] !== undefined;
const [selectedModTypes, setSelectedModTypes] = useState([]);
const isModTypeFiltered = selectedModTypes.length > 0;
+
+ const [publisherQuery, setPublisherQuery] = useState("");
+ const [debouncedPublisherQuery, _cancelDebouncedPublisherQueryChange] = useDebouncedValue(publisherQuery, QUERY_DEBOUNCE_TIME_MILLISECONDS);
+ const isPublishersFiltered = publisherQuery !== "";
+
+
+ type PublicationDate = Mod["timeCreatedGamebanana"];
+ /** [min, max] */
+ type PublicationDateRange = [PublicationDate | undefined, PublicationDate | undefined];
+
+ const [publicationDateRange, setPublicationDateRange] = useState([undefined, undefined]); // [min, max]
+ const isPublicationDateFiltered = publicationDateRange[0] !== undefined || publicationDateRange[1] !== undefined;
+
+
+ const [selectedTechsAny, setSelectedTechsAny] = useState([]);
+ const isTechsAnyFiltered = selectedTechsAny.length > 0;
+
+ const [selectedTechsFC, setSelectedTechsFC] = useState([]);
+ const isTechsFCFiltered = selectedTechsFC.length > 0;
+
+ const [doesTechsFCFilterIncludeTechsAny, setDoesTechsFCFilterIncludeTechsAny] = useState(false);
+
+
const [selectedQualities, setSelectedQualities] = useState([]);
const isQualityFiltered = selectedQualities.length > 0;
+
+ const [qualityRatingsCountRange, setQualityRatingsCountRange] = useState<[number | undefined, number | undefined]>([undefined, undefined]); // [min, max]
+ const isQualityRatingsCountFiltered = qualityRatingsCountRange[0] !== undefined || qualityRatingsCountRange[1] !== undefined;
+
+
const [selectedChildDifficulties, setSelectedChildDifficulties] = useState([]);
- const isChildDifficultyFiltered = selectedChildDifficulties.length > 0;
+ const isChildDifficultiesFiltered = selectedChildDifficulties.length > 0;
+
+
+ const [difficultyRatingsCountRange, setDifficultyRatingsCountRange] = useState<[number | undefined, number | undefined]>([undefined, undefined]); // [min, max]
+ const isDifficultyRatingsCountFiltered = difficultyRatingsCountRange[0] !== undefined || difficultyRatingsCountRange[1] !== undefined;
+
+
+ const [mapCountRange, setMapCountRange] = useState<[number | undefined, number | undefined]>([undefined, undefined]); // [min, max]
+ const isMapCountFiltered = mapCountRange[0] !== undefined || mapCountRange[1] !== undefined;
+
// Reset tab index if the difficulties change.
useEffect(() => {
setCurrentTabIndex(parentDifficultyNames.length > 0 ? 0 : null);
}, [difficulties, parentDifficultyNames]);
+
// Check selected child difficulties when childDifficultyNames changes.
useEffect(() => {
const newSelectedChildDifficulties = selectedChildDifficulties.filter(childDifficulty => childDifficultyNames.includes(childDifficulty));
@@ -359,6 +447,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
}
}, [selectedChildDifficulties, childDifficultyNames]);
+
const filteredModsWithInfo = useMemo(() => {
return modsWithInfo.filter((modWithInfo) => {
if (
@@ -370,28 +459,52 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
if (
- mapCountRange[0] !== undefined ||
- mapCountRange[1] !== undefined
+ selectedModTypes.length &&
+ !selectedModTypes.includes(modWithInfo.type)
) {
- if (
- mapCountRange[0] !== undefined &&
- modWithInfo.Map.length < mapCountRange[0]
- ) {
- return false;
- }
+ return false;
+ }
- if (
- mapCountRange[1] !== undefined &&
- modWithInfo.Map.length > mapCountRange[1]
- ) {
- return false;
- }
+
+ if (
+ debouncedPublisherQuery &&
+ !modWithInfo.publisherName.toLowerCase().includes(debouncedPublisherQuery.trim().toLowerCase())
+ ) {
+ return false;
}
if (
- selectedModTypes.length &&
- !selectedModTypes.includes(modWithInfo.type)
+ publicationDateRange[0] !== undefined &&
+ modWithInfo.timeCreatedGamebanana < publicationDateRange[0]
+ ) {
+ return false;
+ }
+
+ if (
+ publicationDateRange[1] !== undefined &&
+ modWithInfo.timeCreatedGamebanana > publicationDateRange[1]
+ ) {
+ return false;
+ }
+
+
+ if (
+ selectedTechsAny.length &&
+ !selectedTechsAny.some(techId => modWithInfo.TechsAny.includes(techId))
+ ) {
+ return false;
+ }
+
+
+ if (
+ selectedTechsFC.length && (
+ (
+ doesTechsFCFilterIncludeTechsAny && !selectedTechsFC.some(techId => modWithInfo.TechsAny.includes(techId) || modWithInfo.TechsFC.includes(techId))
+ ) || (
+ !doesTechsFCFilterIncludeTechsAny && !selectedTechsFC.some(techId => modWithInfo.TechsFC.includes(techId))
+ )
+ )
) {
return false;
}
@@ -405,6 +518,21 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
}
+ if (
+ qualityRatingsCountRange[0] !== undefined &&
+ modWithInfo.Quality.count < qualityRatingsCountRange[0]
+ ) {
+ return false;
+ }
+
+ if (
+ qualityRatingsCountRange[1] !== undefined &&
+ modWithInfo.Quality.count > qualityRatingsCountRange[1]
+ ) {
+ return false;
+ }
+
+
if (currentTabIndex !== null) {
// Check parent difficulty
const parentDifficultyName = parentDifficultyNames[currentTabIndex];
@@ -422,12 +550,13 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
return false;
}
}
- // Mod has a difficulty rating, so we check if it's difficulty is a child of parentDifficulty.
+ // Mod has a difficulty rating, so we check if its difficulty is a child of parentDifficulty.
else if (!modWithInfo.Difficulty.name.startsWith(parentDifficultyName)) {
return false;
}
}
+
if (
selectedChildDifficulties.length &&
!selectedChildDifficulties.some(childDifficulty => modWithInfo.Difficulty.name.endsWith(childDifficulty))
@@ -436,9 +565,39 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
}
+ if (
+ difficultyRatingsCountRange[0] !== undefined &&
+ modWithInfo.Difficulty.count < difficultyRatingsCountRange[0]
+ ) {
+ return false;
+ }
+
+ if (
+ difficultyRatingsCountRange[1] !== undefined &&
+ modWithInfo.Difficulty.count > difficultyRatingsCountRange[1]
+ ) {
+ return false;
+ }
+
+
+ if (
+ mapCountRange[0] !== undefined &&
+ modWithInfo.MapsWithTechInfo.length < mapCountRange[0]
+ ) {
+ return false;
+ }
+
+ if (
+ mapCountRange[1] !== undefined &&
+ modWithInfo.MapsWithTechInfo.length > mapCountRange[1]
+ ) {
+ return false;
+ }
+
+
return true;
});
- }, [modsWithInfo, difficulties, debouncedNameQuery, mapCountRange, selectedModTypes, selectedQualities, selectedChildDifficulties, currentTabIndex, parentDifficultyNames]);
+ }, [debouncedNameQuery, selectedModTypes, debouncedPublisherQuery, publicationDateRange, selectedTechsAny, selectedTechsFC, selectedQualities, qualityRatingsCountRange, selectedChildDifficulties, difficultyRatingsCountRange, mapCountRange, currentTabIndex, parentDifficultyNames, difficulties, modsWithInfo]);
@@ -454,27 +613,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
const sortedModsWithInfo = [...filteredModsWithInfo];
- if (columnAccessor === "Map") {
- sortedModsWithInfo.sort(
- (a, b) => {
- const propertyANum = Number(a.Map.length);
- const propertyBNum = Number(b.Map.length);
-
- const aIsNan = isNaN(propertyANum);
- const bIsNan = isNaN(propertyBNum);
-
- if (aIsNan && bIsNan) return 0;
- if (aIsNan) return -1;
- if (bIsNan) return 1;
-
- return (
- sortStatus.direction === "asc" ?
- propertyANum - propertyBNum :
- propertyBNum - propertyANum
- );
- },
- );
- } else if (columnAccessor === "Quality") {
+ if (columnAccessor === "Quality") {
sortedModsWithInfo.sort(
(a, b) => {
if (a === b) return 0;
@@ -488,7 +627,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
return (
sortStatus.direction === "asc" ?
- bQuality.order - aQuality.order : //b-a because better qualities have higher orders, but we want them to sort first when ascending
+ bQuality.order - aQuality.order : // b-a because better qualities have higher orders, but we want them to sort first when ascending
aQuality.order - bQuality.order
);
},
@@ -507,12 +646,35 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
return (
sortStatus.direction === "asc" ?
- aDifficulty.order - bDifficulty.order :
+ aDifficulty.order - bDifficulty.order : // a-b because easier difficulties have lower orders, but we want them to sort first when ascending
bDifficulty.order - aDifficulty.order
);
},
);
- } else {
+ } else if (columnAccessor === "mapCount") { // map count
+ sortedModsWithInfo.sort(
+ (a, b) => {
+ const propertyAString = String((a as ExtendedModWithInfo)[columnAccessor]);
+ const propertyBString = String((b as ExtendedModWithInfo)[columnAccessor]);
+
+ const propertyANum = Number(propertyAString);
+ const propertyBNum = Number(propertyBString);
+
+ const aIsNan = isNaN(propertyANum);
+ const bIsNan = isNaN(propertyBNum);
+
+ if (aIsNan && bIsNan) return 0;
+ if (aIsNan) return -1;
+ if (bIsNan) return 1;
+
+ return (
+ sortStatus.direction === "asc" ?
+ propertyANum - propertyBNum :
+ propertyBNum - propertyANum
+ );
+ },
+ );
+ } else if (columnAccessor === "name" || columnAccessor === "publisherName") { // handles name, publisherName
sortedModsWithInfo.sort(
(a, b) => {
const propertyAString = String(a[columnAccessor]);
@@ -525,6 +687,8 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
);
},
);
+ } else {
+ throw `Invalid sorting column accessor: "${columnAccessor}"`;
}
return sortedModsWithInfo;
@@ -560,7 +724,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
//reset page when required
useEffect(() => {
setPage(1);
- }, [sortStatus, pageSize, debouncedNameQuery, mapCountRange, selectedModTypes, selectedQualities, selectedChildDifficulties, currentTabIndex]);
+ }, [sortStatus, pageSize, debouncedNameQuery, selectedModTypes, debouncedPublisherQuery, publicationDateRange, selectedTechsAny, selectedTechsFC, selectedQualities, qualityRatingsCountRange, selectedChildDifficulties, difficultyRatingsCountRange, mapCountRange, currentTabIndex]);
//handle providing datatable with correct subset of data
// const [records, setRecords] = useState(sortedModsWithIsExpanded.slice(0, pageSize));
@@ -586,7 +750,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
- // apply the correct class to the body element to change the background color of the pagination dropdown
+ // apply the correct class (defined in ~/styles/globals.css) to the body element to change the background color of the pagination dropdown
useEffect(() => {
const menuClassNames = canonicalDifficultyNames.map((difficultyName) => `${difficultyName.toLowerCase()}-menu`);
@@ -621,6 +785,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
const { cx, classes } = useStyles({ colors });
+
const tabColors: string[] = Array(canonicalDifficultyNames.length);
Object.entries(classes).forEach(
@@ -658,7 +823,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
return () => {
tabsParent.removeChild(tabContainer);
-
+
setTabContainer(null);
};
}, [classes.tabContainer]);
@@ -694,7 +859,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
{
+ const modTypeString = (modWithInfo.type !== "Normal" && modWithInfo.type !== "LobbyOther") ?
+ modWithInfo.type : (
+ modWithInfo.type === "Normal" ? "Campaign" : "Other Lobby"
+ );
+
+ return (
+
+ );
+ },
filter: (
modWithInfo.Map.length,
- filter: (
-
- ),
- filtering: isMapCountFiltered,
- titleClassName: isMapCountFiltered ? classes.filteredColumnTitle : classes.unfilteredColumnTitle,
- },
- {
- accessor: "type",
- title: "Type",
+ accessor: "publisherName",
+ title: "Publisher",
sortable: true,
+ ellipsis: true,
+ render: (modWithInfo) => {
+ const publicationDate = new Date(modWithInfo.timeCreatedGamebanana * 1000); // convert from seconds to milliseconds
+ return (
+
+ );
+ },
filter: (
-
),
- filtering: isModTypeFiltered,
- titleClassName: isModTypeFiltered ? classes.filteredColumnTitle : classes.unfilteredColumnTitle
+ filtering: isPublishersFiltered,
+ titleClassName: isPublishersFiltered ? classes.filteredColumnTitle : classes.unfilteredColumnTitle,
},
{
accessor: "Quality",
title: "Quality",
sortable: true,
- render: (modWithInfo) => modWithInfo.Quality.name,
+ ellipsis: true,
+ render: (modWithInfo) => {
+ if (modWithInfo.Quality.count === 0) return (
+
+ {modWithInfo.Quality.name}
+
+ );
+
+ return (
+
+ );
+ },
filter: (
modWithInfo.Difficulty.name,
+ ellipsis: true,
+ render: (modWithInfo) => {
+ const difficultyNameFromMod = modWithInfo.Difficulty.name;
+
+ if (modWithInfo.Difficulty.count === 0) {
+ return (
+
+ {difficultyNameFromMod}
+
+ );
+ }
+
+
+ const [parentDifficultyName, childDifficultyName] = difficultyNameFromMod.split(": ");
+
+ if (parentDifficultyName === undefined || childDifficultyName === undefined) return "";
+
+
+ return (
+
+ );
+ },
filter: (
),
- filtering: isChildDifficultyFiltered,
- titleClassName: isChildDifficultyFiltered ? classes.filteredColumnTitle : classes.unfilteredColumnTitle,
+ filtering: isChildDifficultiesFiltered,
+ titleClassName: isChildDifficultiesFiltered ? classes.filteredColumnTitle : classes.unfilteredColumnTitle,
+ },
+ {
+ accessor: "mapCount",
+ title: "# Maps",
+ sortable: true,
+ ellipsis: true,
+ filter: (
+
+ ),
+ filtering: isMapCountFiltered,
+ titleClassName: isMapCountFiltered ? classes.filteredColumnTitle : classes.unfilteredColumnTitle,
+ },
+ {
+ accessor: "techsAny",
+ title: "Techs",
+ sortable: false,
+ ellipsis: true,
+ render: (modWithInfo) => {
+ const techsAnyString = modWithInfo.TechsAny.join(", ");
+ const techsFCString = modWithInfo.TechsFC.join(", ");
+
+ return (
+
+ );
+ },
+ filter: (
+
+ ),
+ filtering: isTechsAnyFiltered,
+ titleClassName: isTechsAnyFiltered ? classes.filteredColumnTitle : classes.unfilteredColumnTitle,
cellsClassName: (record) => {
return cx(
classes.modCell,
@@ -821,7 +1100,7 @@ export const ModsTable = ({ qualities, difficulties, modsWithInfo, isLoading }:
},
]}
sortStatus={sortStatus}
- onSortStatusChange={setSortStatus as Dispatch>} //un-narrow type to match types in DataTable
+ onSortStatusChange={setSortStatus as Dispatch>} // un-narrow type to match types in DataTable
rowExpansion={{
trigger: "click",
allowMultiple: false,
diff --git a/src/components/mods/modsTableTooltip.tsx b/src/components/mods/modsTableTooltip.tsx
new file mode 100644
index 00000000..fe64f76f
--- /dev/null
+++ b/src/components/mods/modsTableTooltip.tsx
@@ -0,0 +1,62 @@
+import { Tooltip, Text } from "@mantine/core";
+
+
+// styles are defined in ~/styles/globals.css
+
+
+type ModsTableTooltipProps_Base = {
+ dropdownString: string;
+ multiline?: boolean;
+ maxWidth?: number;
+};
+
+type ModsTableTooltipProps = (
+ {
+ targetString: string;
+ childComponent?: never;
+ } | {
+ targetString?: never;
+ childComponent: JSX.Element;
+
+ }
+) & ModsTableTooltipProps_Base;
+
+
+
+
+/** `target` must be able to accept a `ref`. */
+export const ModsTableTooltip = ({
+ targetString,
+ childComponent,
+ dropdownString,
+ multiline = false, // I couldn't make multiline work with static tooltips so we are using floating for now. we could also use a popover styled like a tooltip.
+ maxWidth,
+}: ModsTableTooltipProps) => {
+
+ return (
+
+ {dropdownString}
+
+ }
+ >
+ {
+ childComponent ?
+ childComponent : (
+
+ {targetString}
+
+ )
+ }
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/mods/publicationDate.tsx b/src/components/mods/publicationDate.tsx
index 235e1ce5..234ff93b 100644
--- a/src/components/mods/publicationDate.tsx
+++ b/src/components/mods/publicationDate.tsx
@@ -15,13 +15,10 @@ const PUBLICATION_DATE_LABEL = "Published: ";
-const PublicationDate = ({ publicationDate }: PublicationDateProps) => {
+export const PublicationDate = ({ publicationDate }: PublicationDateProps) => {
return (
{`${PUBLICATION_DATE_LABEL}${publicationDate === undefined ? "Undefined" : publicationDate?.toLocaleDateString()}`}
);
-};
-
-
-export default PublicationDate;
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/src/components/mods/publisherName.tsx b/src/components/mods/publisherName.tsx
index abf0f419..238aefc0 100644
--- a/src/components/mods/publisherName.tsx
+++ b/src/components/mods/publisherName.tsx
@@ -16,7 +16,7 @@ const PUBLISHER_NAME_LABEL = "Publisher: ";
-const PublisherName = ({ publisherId }: PublisherNameProps) => {
+export const PublisherName = ({ publisherId }: PublisherNameProps) => {
const publisherQuery = api.publisher.getById.useQuery({ id: publisherId }, { queryKey: ["publisher.getById", { id: publisherId }] });
const publisher = publisherQuery.data;
@@ -46,7 +46,4 @@ const PublisherName = ({ publisherId }: PublisherNameProps) => {
{PUBLISHER_NAME_LABEL + publisherName}
);
-};
-
-
-export default PublisherName;
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/src/components/mods/types.ts b/src/components/mods/types.ts
index 3afc1f9d..10fbaa5f 100644
--- a/src/components/mods/types.ts
+++ b/src/components/mods/types.ts
@@ -15,7 +15,7 @@ export type ModRatingData = RouterOutputs["rating"]["getModRatingData"];
export type ModNoRatingData = Pick;
export type ModYesRatingData = { //TODO: figure out how to do this through narrowing instead of directly referencing the type
modId: ModRatingData["modId"];
-} & RatingsInfo
+} & RatingsInfo;
export type Map = RouterOutputs["map"]["getById"];
@@ -24,11 +24,13 @@ export type MapRatingData = RouterOutputs["rating"]["getMapRatingData"];
export type MapNoRatingData = Pick;
export type MapYesRatingData = { //TODO: figure out how to do this through narrowing instead of directly referencing the type
mapId: MapRatingData["mapId"];
-} & RatingsInfo
+} & RatingsInfo;
export type Quality = RouterOutputs["quality"]["getAll"][number];
export type Difficulty = RouterOutputs["difficulty"]["getAll"][number];
+export type Publisher = RouterOutputs["publisher"]["getAll"][number];
+export type Tech = RouterOutputs["tech"]["getAll"][number];
export type Length = RouterOutputs["length"]["getAll"][number];
@@ -40,9 +42,32 @@ type RatingInfo = {
count: number;
};
+export type MapWithTechInfo = {
+ TechsAny: Tech[];
+ TechsFC: Tech[];
+} & Omit