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"; 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; exterior_cells_edited: 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 { 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 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) => (
  • 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;