diff --git a/components/CellData.tsx b/components/CellData.tsx index 120ca16..a75551b 100644 --- a/components/CellData.tsx +++ b/components/CellData.tsx @@ -3,7 +3,7 @@ import React from "react"; import useSWRImmutable from "swr/immutable"; import styles from "../styles/CellData.module.css"; -import CellModList from "./CellModList"; +import ModList from "./ModList"; import PluginList from "./PluginsList"; export interface Mod { @@ -123,7 +123,7 @@ const CellData: React.FC = ({ selectedCell, counts }) => { - + ) ); diff --git a/components/CellModList.tsx b/components/CellModList.tsx deleted file mode 100644 index 8da85f9..0000000 --- a/components/CellModList.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { format } from "date-fns"; -import React, { useState } from "react"; -import Link from "next/link"; - -import styles from "../styles/CellModList.module.css"; -import type { Mod } from "./CellData"; - -const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; - -type Props = { - mods: Mod[]; - counts: Record | null; -}; - -type ModWithCounts = Mod & { - total_downloads: number; - unique_downloads: number; - views: number; -}; - -const CellModList: React.FC = ({ mods, counts }) => { - const [includeTranslations, setIncludeTranslations] = useState(true); - - const modsWithCounts: ModWithCounts[] = mods - .map((mod) => { - const modCounts = counts && counts[mod.nexus_mod_id]; - return { - ...mod, - total_downloads: modCounts ? modCounts[0] : 0, - unique_downloads: modCounts ? modCounts[1] : 0, - views: modCounts ? modCounts[2] : 0, - }; - }) - .filter((mod) => includeTranslations || !mod.is_translation); - - let numberFmt = new Intl.NumberFormat("en-US"); - - return ( - mods && ( - <> -

Nexus Mods ({modsWithCounts.length})

- -
    - {modsWithCounts - .sort((a, b) => b.unique_downloads - a.unique_downloads) - .map((mod) => ( -
  • -
    - - - {mod.name} - - -
    - -
    - Category:  - - {mod.category_name} - -
    -
    - Author:  - - {mod.author_name} - -
    -
    - Uploaded:{" "} - {format(new Date(mod.first_upload_at), "d MMM y")} -
    -
    - Last Update:{" "} - {format(new Date(mod.last_update_at), "d MMM y")} -
    -
    - Total Downloads:{" "} - {numberFmt.format(mod.total_downloads)} -
    -
    - Unique Downloads:{" "} - {numberFmt.format(mod.unique_downloads)} -
    -
  • - ))} -
- - ) - ); -}; - -export default CellModList; diff --git a/components/ModList.tsx b/components/ModList.tsx new file mode 100644 index 0000000..a1c1415 --- /dev/null +++ b/components/ModList.tsx @@ -0,0 +1,269 @@ +import { format } from "date-fns"; +import React, { useEffect, useRef, useState } from "react"; +import MiniSearch from "minisearch"; +import Link from "next/link"; + +import styles from "../styles/ModList.module.css"; +import type { Mod } from "./CellData"; +import type { File } from "../slices/plugins"; +import { formatBytes } from "../lib/plugins"; + +const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; + +type Props = { + mods: Mod[]; + files?: File[]; + counts: Record | null; +}; + +type ModWithCounts = Mod & { + total_downloads: number; + unique_downloads: number; + views: number; +}; + +const ModList: React.FC = ({ mods, files, counts }) => { + const [includeTranslations, setIncludeTranslations] = useState(true); + const [sortBy, setSortBy] = useState("unique_downloads"); + const [filter, setFilter] = useState(""); + const [category, setCategory] = useState("All"); + const [filterResults, setFilterResults] = useState>(new Set()); + + const modsWithCounts: ModWithCounts[] = mods + .map((mod) => { + const modCounts = counts && counts[mod.nexus_mod_id]; + return { + ...mod, + total_downloads: modCounts ? modCounts[0] : 0, + unique_downloads: modCounts ? modCounts[1] : 0, + views: modCounts ? modCounts[2] : 0, + }; + }) + .filter( + (mod) => + (includeTranslations || !mod.is_translation) && + (!filter || filterResults.has(mod.id)) && + (category === "All" || mod.category_name === category) + ) + .sort((a, b) => { + const aVal = a[sortBy]; + const bVal = b[sortBy]; + if (typeof aVal === "number" && typeof bVal === "number") { + return bVal - aVal; + } else if ( + typeof aVal === "string" && + typeof bVal === "string" && + ["first_upload_at", "last_update_at"].includes(sortBy) + ) { + return new Date(bVal).getTime() - new Date(aVal).getTime(); + } else if (typeof aVal === "string" && typeof bVal === "string") { + return aVal.localeCompare(bVal); + } + return 0; + }); + + let numberFmt = new Intl.NumberFormat("en-US"); + + const modSearch = useRef | null>( + null + ) as React.MutableRefObject>; + + useEffect(() => { + modSearch.current = new MiniSearch({ + fields: ["name"], + storeFields: ["name", "id"], + searchOptions: { + fields: ["name"], + fuzzy: 0.2, + prefix: true, + }, + }); + modSearch.current.addAll(mods); + }, [mods]); + + useEffect(() => { + if (modSearch.current) { + setFilterResults( + new Set(modSearch.current.search(filter).map((result) => result.id)) + ); + } + }, [filter]); + + return ( + mods && ( + <> +

Nexus Mods ({modsWithCounts.length})

+
+
+
+ + +
+
+ + setFilter(event.target.value)} + /> +
+
+ + +
+
+ + setIncludeTranslations(!includeTranslations)} + /> +
+
+
+
    + {modsWithCounts.map((mod) => ( +
  • +
    + + + {mod.name} + + +
    + +
    + Category:  + + {mod.category_name} + +
    +
    + Author:  + + {mod.author_name} + +
    +
    + Uploaded:{" "} + {format(new Date(mod.first_upload_at), "d MMM y")} +
    +
    + Last Update:{" "} + {format(new Date(mod.last_update_at), "d MMM y")} +
    +
    + Total Downloads:{" "} + {numberFmt.format(mod.total_downloads)} +
    +
    + Unique Downloads:{" "} + {numberFmt.format(mod.unique_downloads)} +
    +
      + {files && + files + .filter((file) => file.mod_id === mod.id) + .sort((a, b) => b.nexus_file_id - a.nexus_file_id) + .map((file) => ( +
    • +
      + File: {file.name} +
      + {file.mod_version && ( +
      + Version: {file.mod_version} +
      + )} + {file.version && file.mod_version !== file.version && ( +
      + File Version: {file.version} +
      + )} + {file.category && ( +
      + Category: {file.category} +
      + )} +
      + Size: {formatBytes(file.size)} +
      + {file.uploaded_at && ( +
      + Uploaded:{" "} + {format(new Date(file.uploaded_at), "d MMM y")} +
      + )} +
    • + ))} +
    +
  • + ))} +
+ + ) + ); +}; + +export default ModList; diff --git a/components/PluginDetail.tsx b/components/PluginDetail.tsx index ed4e095..0e5b682 100644 --- a/components/PluginDetail.tsx +++ b/components/PluginDetail.tsx @@ -7,7 +7,7 @@ import { PluginFile, PluginsByHashWithMods, } from "../slices/plugins"; -import PluginModList from "./PluginModList"; +import ModList from "./ModList"; import CellList from "./CellList"; import type { CellCoord } from "./ModData"; import PluginData, { Plugin as PluginProps } from "./PluginData"; @@ -92,9 +92,7 @@ const PluginDetail: React.FC = ({ hash, counts }) => { return ( <> - {data && ( - - )} + {data && } | null; -}; - -type ModWithCounts = Mod & { - total_downloads: number; - unique_downloads: number; - views: number; -}; - -const PluginModList: React.FC = ({ mods, files, counts }) => { - const [includeTranslations, setIncludeTranslations] = useState(true); - - const modsWithCounts: ModWithCounts[] = mods - .map((mod) => { - const modCounts = counts && counts[mod.nexus_mod_id]; - return { - ...mod, - total_downloads: modCounts ? modCounts[0] : 0, - unique_downloads: modCounts ? modCounts[1] : 0, - views: modCounts ? modCounts[2] : 0, - }; - }) - .filter((mod) => includeTranslations || !mod.is_translation); - - let numberFmt = new Intl.NumberFormat("en-US"); - - return ( - mods && ( - <> -

Nexus Mods ({modsWithCounts.length})

- -
    - {modsWithCounts - .sort((a, b) => b.unique_downloads - a.unique_downloads) - .map((mod) => ( -
  • -
    - - - {mod.name} - - -
    - -
    - Category:  - - {mod.category_name} - -
    -
    - Author:  - - {mod.author_name} - -
    -
    - Uploaded:{" "} - {format(new Date(mod.first_upload_at), "d MMM y")} -
    -
    - Last Update:{" "} - {format(new Date(mod.last_update_at), "d MMM y")} -
    -
    - Total Downloads:{" "} - {numberFmt.format(mod.total_downloads)} -
    -
    - Unique Downloads:{" "} - {numberFmt.format(mod.unique_downloads)} -
    -
      - {files - .filter((file) => file.mod_id === mod.id) - .sort((a, b) => b.nexus_file_id - a.nexus_file_id) - .map((file) => ( -
    • -
      - File: {file.name} -
      - {file.mod_version && ( -
      - Version: {file.mod_version} -
      - )} - {file.version && file.mod_version !== file.version && ( -
      - File Version: {file.version} -
      - )} - {file.category && ( -
      - Category: {file.category} -
      - )} -
      - Size: {formatBytes(file.size)} -
      - {file.uploaded_at && ( -
      - Uploaded:{" "} - {format(new Date(file.uploaded_at), "d MMM y")} -
      - )} -
    • - ))} -
    -
  • - ))} -
- - ) - ); -}; - -export default PluginModList; diff --git a/components/PluginsList.tsx b/components/PluginsList.tsx index f7271e1..7081119 100644 --- a/components/PluginsList.tsx +++ b/components/PluginsList.tsx @@ -44,7 +44,11 @@ const PluginsList: React.FC = ({ selectedCell }) => { )} -
    +
      0 ? styles["bottom-spacing"] : "" + }`} + > {plugins.map((plugin) => (