diff --git a/components/CellData.tsx b/components/CellData.tsx index bee6c56..a6ea24a 100644 --- a/components/CellData.tsx +++ b/components/CellData.tsx @@ -48,7 +48,7 @@ const jsonFetcher = async (url: string): Promise => { type Props = { selectedCell: { x: number; y: number }; - counts: [number, number, number, number][]; + counts: Record | null; }; const CellData: React.FC = ({ selectedCell, counts }) => { diff --git a/components/CellModList.tsx b/components/CellModList.tsx index 2c6c8ef..9b8ea0c 100644 --- a/components/CellModList.tsx +++ b/components/CellModList.tsx @@ -8,7 +8,7 @@ const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; type Props = { mods: Mod[]; - counts: [number, number, number, number][]; + counts: Record | null; }; type ModWithCounts = Mod & { @@ -19,12 +19,12 @@ type ModWithCounts = Mod & { const CellModList: React.FC = ({ mods, counts }) => { const modsWithCounts: ModWithCounts[] = mods.map((mod) => { - const modCounts = counts.find((count) => count[0] === mod.nexus_mod_id); + const modCounts = counts && counts[mod.nexus_mod_id]; return { ...mod, - total_downloads: modCounts ? modCounts[1] : 0, - unique_downloads: modCounts ? modCounts[2] : 0, - views: modCounts ? modCounts[3] : 0, + total_downloads: modCounts ? modCounts[0] : 0, + unique_downloads: modCounts ? modCounts[1] : 0, + views: modCounts ? modCounts[2] : 0, }; }); diff --git a/components/Map.tsx b/components/Map.tsx index bdbe53c..5365d33 100644 --- a/components/Map.tsx +++ b/components/Map.tsx @@ -21,7 +21,11 @@ colorGradient.setGradient( ); 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((res) => res.json()); +const csvFetcher = (url: string) => fetch(url).then((res) => res.text()); const Map: React.FC = () => { const router = useRouter(); @@ -43,10 +47,19 @@ const Map: React.FC = () => { } | null>(null); const sidebarOpen = selectedCell !== null || router.query.mod !== undefined; - const { data, error } = useSWRImmutable( + const { data: cellsData, error: cellsError } = useSWRImmutable( "https://cells.modmapper.com/edits.json", jsonFetcher ); + // The live download counts are not really immutable, but I'd still rather load them once per session + const [counts, setCounts] = useState | null>(null); + const { data: countsData, error: countsError } = useSWRImmutable( + LIVE_DOWNLOAD_COUNTS_URL, + csvFetcher + ); const selectMapCell = useCallback( (cell: { x: number; y: number }) => { @@ -240,7 +253,7 @@ const Map: React.FC = () => { }, [setMapLoaded]); useEffect(() => { - if (!data || !router.isReady || !mapLoaded) return; // wait for map to initialize and data to load + if (!cellsData || !router.isReady || !mapLoaded) return; // wait for map to initialize and data to load if (map.current.getSource("graticule")) return; // don't initialize twice const zoom = map.current.getZoom(); @@ -368,7 +381,7 @@ const Map: React.FC = () => { x * cellSize + viewportNW.x, y * cellSize + viewportNW.y + cellSize, ]); - const editCount = (data as Record)[ + const editCount = (cellsData as Record)[ `${x - 57},${50 - y}` ]; grid.features.push({ @@ -462,7 +475,18 @@ const Map: React.FC = () => { }); setHeatmapLoaded(true); - }, [data, mapLoaded, router, setHeatmapLoaded]); + }, [cellsData, mapLoaded, router, setHeatmapLoaded]); + + useEffect(() => { + if (countsData) { + const newCounts: Record = {}; + for (const line of countsData.split("\n")) { + const nums = line.split(",").map((count) => parseInt(count, 10)); + newCounts[nums[0]] = [nums[1], nums[2], nums[3]]; + } + setCounts(newCounts); + } + }, [setCounts, countsData]); return ( <> @@ -477,11 +501,14 @@ const Map: React.FC = () => { selectedCell={selectedCell} clearSelectedCell={() => router.push({ query: {} })} map={map} + counts={counts} + countsError={countsError} /> router.push({ query: {} })} + counts={counts} /> diff --git a/components/ModData.tsx b/components/ModData.tsx index dcd8174..a393624 100644 --- a/components/ModData.tsx +++ b/components/ModData.tsx @@ -45,7 +45,7 @@ const jsonFetcher = async (url: string): Promise => { type Props = { selectedMod: number; - counts: [number, number, number, number][]; + counts: Record | null; }; const ModData: React.FC = ({ selectedMod, counts }) => { @@ -65,10 +65,10 @@ const ModData: React.FC = ({ selectedMod, counts }) => { return
Mod could not be found.
; let numberFmt = new Intl.NumberFormat("en-US"); - const modCounts = counts.find((count) => count[0] === data.nexus_mod_id); - const total_downloads = modCounts ? modCounts[1] : 0; - const unique_downloads = modCounts ? modCounts[2] : 0; - const views = modCounts ? modCounts[3] : 0; + const modCounts = counts && counts[data.nexus_mod_id]; + const total_downloads = modCounts ? modCounts[0] : 0; + const unique_downloads = modCounts ? modCounts[1] : 0; + const views = modCounts ? modCounts[2] : 0; if (selectedMod && data) { return ( diff --git a/components/SearchBar.tsx b/components/SearchBar.tsx index c699185..f396e71 100644 --- a/components/SearchBar.tsx +++ b/components/SearchBar.tsx @@ -6,10 +6,12 @@ import useSWRImmutable from "swr/immutable"; import styles from "../styles/SearchBar.module.css"; import { join } from "path/posix"; +import { countReset } from "console"; type Props = { clearSelectedCell: () => void; map: React.MutableRefObject; + counts: Record | null; }; interface Mod { @@ -30,7 +32,7 @@ const jsonFetcher = async (url: string): Promise => { return res.json(); }; -const SearchBar: React.FC = ({ clearSelectedCell, map }) => { +const SearchBar: React.FC = ({ clearSelectedCell, counts, map }) => { const router = useRouter(); const searchEngine = useRef | null>( @@ -122,16 +124,25 @@ const SearchBar: React.FC = ({ clearSelectedCell, map }) => { /> {results.length > 0 && (
    - {results.map((result) => ( -
  • setClickingResult(true)} - onMouseDown={() => setClickingResult(true)} - > - {result.name} -
  • - ))} + {results + .sort((resultA, resultB) => { + if (counts) { + const countA = counts[resultA.id]; + const countB = counts[resultB.id]; + if (countA && countB) return countB[2] - countA[2]; + } + return 0; + }) + .map((result) => ( +
  • setClickingResult(true)} + onMouseDown={() => setClickingResult(true)} + > + {result.name} +
  • + ))}
)} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 0555754..8aa5ee2 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -7,10 +7,6 @@ import ModData from "./ModData"; import styles from "../styles/Sidebar.module.css"; import { render } from "react-dom"; -const LIVE_DOWNLOAD_COUNTS_URL = - "https://staticstats.nexusmods.com/live_download_counts/mods/1704.csv"; - -const csvFetcher = (url: string) => fetch(url).then((res) => res.text()); interface Cell { x: number; y: number; @@ -21,27 +17,18 @@ type Props = { selectedCell: { x: number; y: number } | null; clearSelectedCell: () => void; map: React.MutableRefObject; + counts: Record | null; + countsError: Error | null; }; -const Sidebar: React.FC = ({ selectedCell, clearSelectedCell, map }) => { +const Sidebar: React.FC = ({ + selectedCell, + clearSelectedCell, + counts, + countsError, + map, +}) => { const router = useRouter(); - // The live download counts are not really immutable, but I'd still rather load them once per session - const { data, error } = useSWRImmutable(LIVE_DOWNLOAD_COUNTS_URL, csvFetcher); - const [counts, setCounts] = useState< - [number, number, number, number][] | null - >(null); - - useEffect(() => { - if (data) { - setCounts( - data - .split("\n") - .map((line) => - line.split(",").map((count) => parseInt(count, 10)) - ) as [number, number, number, number][] - ); - } - }, [setCounts, data]); const renderLoadError = (error: Error) => (
{`Error loading live download counts: ${error.message}`}
@@ -50,14 +37,14 @@ const Sidebar: React.FC = ({ selectedCell, clearSelectedCell, map }) => { const renderLoading = () =>
Loading...
; const renderCellData = (selectedCell: { x: number; y: number }) => { - if (error) return renderLoadError(error); + if (countsError) return renderLoadError(countsError); if (!counts) return renderLoading(); return ; }; const renderModData = (selectedMod: number) => { - if (error) return renderLoadError(error); + if (countsError) return renderLoadError(countsError); if (!counts) return renderLoading(); return ;