/* eslint-disable @next/next/no-img-element */ import { format } from "date-fns"; import React, { useContext, useEffect, useRef, useState } from "react"; import MiniSearch from "minisearch"; import Link from "next/link"; import useSWRImmutable from "swr/immutable"; import ReactPaginate from "react-paginate"; import styles from "../styles/ModList.module.css"; import type { Mod } from "./CellData"; import type { File } from "../slices/plugins"; import { formatBytes } from "../lib/plugins"; import { jsonFetcher } from "../lib/api"; import { ModWithCounts, setSortBy, setSortAsc, setFilter, setGame, setCategory, setIncludeTranslations, } from "../slices/modListFilters"; import { editionNames } from "../lib/games"; import { useAppDispatch, useAppSelector } from "../lib/hooks"; import { DownloadCountsContext } from "./DownloadCountsProvider"; import { GamesContext } from "./GamesProvider"; const NEXUS_MODS_URL = "https://www.nexusmods.com"; const PAGE_SIZE = 50; type Props = { mods: Mod[]; files?: File[]; }; const ModList: React.FC = ({ mods, files }) => { const { games, getGameNameById, error: gamesError, } = useContext(GamesContext); const counts = useContext(DownloadCountsContext); const dispatch = useAppDispatch(); const { sortBy, sortAsc, filter, category, game, includeTranslations } = useAppSelector((state) => state.modListFilters); const [filterResults, setFilterResults] = useState>(new Set()); const [page, setPage] = useState(0); const { data: cellCounts, error: cellCountsError } = useSWRImmutable( `https://mods.modmapper.com/mod_cell_counts.json`, (_) => jsonFetcher>(_) ); const modsWithCounts: ModWithCounts[] = mods .map((mod) => { const gameName = getGameNameById(mod.game_id); const gameDownloadCounts = gameName && counts[gameName].counts; const modCounts = gameDownloadCounts && gameDownloadCounts[mod.nexus_mod_id]; return { ...mod, total_downloads: modCounts ? modCounts[0] : 0, unique_downloads: modCounts ? modCounts[1] : 0, views: modCounts ? modCounts[2] : 0, exterior_cells_edited: cellCounts ? cellCounts[mod.nexus_mod_id] ?? 0 : 0, }; }) .filter( (mod) => (includeTranslations || !mod.is_translation) && (!filter || filterResults.has(mod.id)) && (game === "All" || getGameNameById(mod.game_id) === game) && (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 sortAsc ? aVal - bVal : bVal - aVal; } else if ( typeof aVal === "string" && typeof bVal === "string" && ["first_upload_at", "last_update_at"].includes(sortBy) ) { const aTime = new Date(aVal).getTime(); const bTime = new Date(bVal).getTime(); return sortAsc ? aTime - bTime : bTime - aTime; } else if (typeof aVal === "string" && typeof bVal === "string") { return sortAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); } return 0; }); let numberFmt = new Intl.NumberFormat("en-US"); const renderDownloadCountsLoading = () => (
Loading live download counts...
); const renderDownloadCountsError = (error: Error) => (
{`Error loading live download counts: ${error.message}`}
); const renderGamesError = (error?: Error) => error ? (
{`Error loading games: ${error.message}`}
) : (
Error loading games
); 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]); useEffect(() => { setPage(0); }, [filterResults, category, includeTranslations, sortBy, sortAsc]); const renderPagination = () => ( { setPage(event.selected); document.getElementById("nexus-mods")?.scrollIntoView(); }} pageRangeDisplayed={3} marginPagesDisplayed={2} pageCount={Math.ceil(modsWithCounts.length / PAGE_SIZE)} previousLabel="<" renderOnZeroPageCount={() => null} className={styles.pagination} activeClassName={styles["active-page"]} hrefBuilder={() => "#"} /> ); return ( mods && ( <>

Nexus Mods ({modsWithCounts.length})


dispatch(setFilter(event.target.value))} />
dispatch(setIncludeTranslations(!includeTranslations)) } />

{renderPagination()}
    {(!counts.skyrim.counts || !counts.skyrimspecialedition.counts) && renderDownloadCountsLoading()} {(!games || gamesError) && renderGamesError(gamesError)} {counts.skyrim.error && renderDownloadCountsError(counts.skyrim.error)} {counts.skyrimspecialedition.error && renderDownloadCountsError(counts.skyrimspecialedition.error)} {modsWithCounts .slice(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE) .map((mod) => (
  • Edition:  { editionNames[ getGameNameById(mod.game_id) ?? "skyrimspecialedition" ] }
    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)}
    {cellCounts && (
    Exterior Cells Edited:{" "} {numberFmt.format(mod.exterior_cells_edited)}
    )}
      {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")}
      )}
    • ))}
  • ))}
{renderPagination()} ) ); }; export default ModList;