diff --git a/components/CellData.tsx b/components/CellData.tsx index a75551b..cfb33b6 100644 --- a/components/CellData.tsx +++ b/components/CellData.tsx @@ -5,6 +5,7 @@ import useSWRImmutable from "swr/immutable"; import styles from "../styles/CellData.module.css"; import ModList from "./ModList"; import PluginList from "./PluginsList"; +import { jsonFetcher } from "../lib/api"; export interface Mod { id: number; @@ -36,19 +37,6 @@ export interface Cell { mods: Mod[]; } -const jsonFetcher = async (url: string): Promise => { - const res = await fetch(url); - - if (!res.ok) { - if (res.status === 404) { - return null; - } - const error = new Error("An error occurred while fetching the data."); - throw error; - } - return res.json(); -}; - type Props = { selectedCell: { x: number; y: number }; counts: Record | null; @@ -57,7 +45,7 @@ type Props = { const CellData: React.FC = ({ selectedCell, counts }) => { const { data, error } = useSWRImmutable( `https://cells.modmapper.com/${selectedCell.x}/${selectedCell.y}.json`, - jsonFetcher + (_) => jsonFetcher(_) ); if (error && error.status === 404) { diff --git a/components/CellList.tsx b/components/CellList.tsx index 091866d..9a81e3b 100644 --- a/components/CellList.tsx +++ b/components/CellList.tsx @@ -5,8 +5,6 @@ import MiniSearch from "minisearch"; import styles from "../styles/CellList.module.css"; import type { CellCoord } from "./ModData"; -const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; - type Props = { cells: CellCoord[]; }; diff --git a/components/Map.tsx b/components/Map.tsx index 891485b..f093487 100644 --- a/components/Map.tsx +++ b/components/Map.tsx @@ -10,6 +10,7 @@ import styles from "../styles/Map.module.css"; import Sidebar from "./Sidebar"; import ToggleLayersControl from "./ToggleLayersControl"; import SearchBar from "./SearchBar"; +import { csvFetcher, jsonFetcherWithLastModified } from "../lib/api"; mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? ""; @@ -26,13 +27,6 @@ colorGradient.setMidpoint(360); const LIVE_DOWNLOAD_COUNTS_URL = "https://staticstats.nexusmods.com/live_download_counts/mods/1704.csv"; -const jsonFetcher = (url: string) => - fetch(url).then(async (res) => ({ - lastModified: res.headers.get("Last-Modified"), - data: await res.json(), - })); -const csvFetcher = (url: string) => fetch(url).then((res) => res.text()); - const Map: React.FC = () => { const router = useRouter(); const mapContainer = useRef( @@ -67,7 +61,7 @@ const Map: React.FC = () => { const { data: cellsData, error: cellsError } = useSWRImmutable( "https://cells.modmapper.com/edits.json", - jsonFetcher + (_) => jsonFetcherWithLastModified>(_) ); // The live download counts are not really immutable, but I'd still rather load them once per session const [counts, setCounts] = useState { x * cellSize + viewportNW.x, y * cellSize + viewportNW.y + cellSize, ]); - const editCount = (cellsData.data as Record)[ - `${x - 57},${50 - y}` - ]; + const editCount = cellsData.data[`${x - 57},${50 - y}`]; grid.features.push({ type: "Feature", id: x * 1000 + y, diff --git a/components/ModData.tsx b/components/ModData.tsx index 35670b5..32326a2 100644 --- a/components/ModData.tsx +++ b/components/ModData.tsx @@ -5,6 +5,7 @@ import useSWRImmutable from "swr/immutable"; import CellList from "./CellList"; import styles from "../styles/ModData.module.css"; +import { jsonFetcher } from "../lib/api"; export interface CellCoord { x: number; @@ -33,19 +34,6 @@ export interface Mod { const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; -const jsonFetcher = async (url: string): Promise => { - const res = await fetch(url); - - if (!res.ok) { - if (res.status === 404) { - return null; - } - const error = new Error("An error occurred while fetching the data."); - throw error; - } - return res.json(); -}; - type Props = { selectedMod: number; counts: Record | null; @@ -59,7 +47,7 @@ const ModData: React.FC = ({ }) => { const { data, error } = useSWRImmutable( `https://mods.modmapper.com/${selectedMod}.json`, - jsonFetcher + (_) => jsonFetcher(_) ); if (error && error.status === 404) { diff --git a/components/ModList.tsx b/components/ModList.tsx index a1c1415..9e886de 100644 --- a/components/ModList.tsx +++ b/components/ModList.tsx @@ -2,11 +2,13 @@ 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"; @@ -20,6 +22,7 @@ type ModWithCounts = Mod & { total_downloads: number; unique_downloads: number; views: number; + exterior_cells_edited: number; }; const ModList: React.FC = ({ mods, files, counts }) => { @@ -29,6 +32,11 @@ const ModList: React.FC = ({ mods, files, counts }) => { 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]; @@ -37,6 +45,9 @@ const ModList: React.FC = ({ mods, files, counts }) => { 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( @@ -106,14 +117,17 @@ const ModList: React.FC = ({ mods, files, counts }) => { setSortBy(event.target.value as keyof ModWithCounts) } > - - - - + + + + +
@@ -221,6 +235,12 @@ const ModList: React.FC = ({ mods, files, counts }) => { Unique Downloads:{" "} {numberFmt.format(mod.unique_downloads)}
+ {cellCounts && ( +
+ Exterior Cells Edited:{" "} + {numberFmt.format(mod.exterior_cells_edited)} +
+ )}
    {files && files diff --git a/components/PluginDetail.tsx b/components/PluginDetail.tsx index 0e5b682..e773584 100644 --- a/components/PluginDetail.tsx +++ b/components/PluginDetail.tsx @@ -12,21 +12,7 @@ import CellList from "./CellList"; import type { CellCoord } from "./ModData"; import PluginData, { Plugin as PluginProps } from "./PluginData"; import styles from "../styles/PluginData.module.css"; - -const jsonFetcher = async ( - url: string -): Promise => { - const res = await fetch(url); - - if (!res.ok) { - if (res.status === 404) { - return null; - } - const error = new Error("An error occurred while fetching the data."); - throw error; - } - return res.json(); -}; +import { jsonFetcher } from "../lib/api"; const buildPluginProps = ( data?: PluginsByHashWithMods | null, @@ -65,7 +51,7 @@ type Props = { const PluginDetail: React.FC = ({ hash, counts }) => { const { data, error } = useSWRImmutable( `https://plugins.modmapper.com/${hash}.json`, - jsonFetcher + (_) => jsonFetcher(_) ); const dispatch = useAppDispatch(); diff --git a/components/SearchBar.tsx b/components/SearchBar.tsx index f257be2..c18d220 100644 --- a/components/SearchBar.tsx +++ b/components/SearchBar.tsx @@ -5,6 +5,7 @@ import MiniSearch, { SearchResult } from "minisearch"; import useSWRImmutable from "swr/immutable"; import styles from "../styles/SearchBar.module.css"; +import { jsonFetcher } from "../lib/api"; type Props = { counts: Record | null; @@ -16,19 +17,6 @@ interface Mod { id: number; } -const jsonFetcher = async (url: string): Promise => { - const res = await fetch(url); - - if (!res.ok) { - if (res.status === 404) { - return null; - } - const error = new Error("An error occurred while fetching the data."); - throw error; - } - return res.json(); -}; - let cells = []; for (let x = -77; x < 76; x++) { @@ -58,7 +46,7 @@ const SearchBar: React.FC = ({ counts, sidebarOpen }) => { const { data, error } = useSWRImmutable( `https://mods.modmapper.com/mod_search_index.json`, - jsonFetcher + (_) => jsonFetcher(_) ); useEffect(() => { @@ -72,7 +60,7 @@ const SearchBar: React.FC = ({ counts, sidebarOpen }) => { prefix: true, }, }); - modSearch.current.addAll(data as unknown as Mod[]); + modSearch.current.addAll(data); } }, [data]); diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..eaa1b5d --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,34 @@ +export async function jsonFetcher(url: string): Promise { + const res = await fetch(url); + + if (!res.ok) { + if (res.status === 404) { + return null; + } + const error = new Error("An error occurred while fetching the data."); + throw error; + } + + return res.json(); +}; + +export async function jsonFetcherWithLastModified(url: string): Promise<{ data: T, lastModified: string | null } | null> { + const res = await fetch(url); + + if (!res.ok) { + if (res.status === 404) { + return null; + } + const error = new Error("An error occurred while fetching the data."); + throw error; + } + + return { + lastModified: res.headers.get("Last-Modified"), + data: await res.json(), + }; +} + +export async function csvFetcher(url: string): Promise { + return (await fetch(url)).text(); +} \ No newline at end of file