/* eslint-disable @next/next/no-img-element */ import { format } from "date-fns"; import React, { useEffect, useRef, useState } from "react"; import MiniSearch from "minisearch"; import Link from "next/link"; import useSWRImmutable from "swr/immutable"; 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, setCategory, setIncludeTranslations, } from "../slices/modListFilters"; import { useAppDispatch, useAppSelector } from "../lib/hooks"; const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; type Props = { mods: Mod[]; files?: File[]; counts: Record | null; }; const ModList: React.FC = ({ mods, files, counts }) => { const dispatch = useAppDispatch(); const { sortBy, sortAsc, filter, category, includeTranslations } = useAppSelector((state) => state.modListFilters); const [filterResults, setFilterResults] = useState>(new Set()); const { data: cellCounts, error: cellCountsError } = useSWRImmutable( `https://mods.modmapper.com/mod_cell_counts.json`, (_) => jsonFetcher>(_) ); 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, exterior_cells_edited: cellCounts ? cellCounts[mod.nexus_mod_id] ?? 0 : 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 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 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})


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

    {modsWithCounts.map((mod) => (
  • 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")}
      )}
    • ))}
  • ))}
) ); }; export default ModList;