Very slow mod search, and mod data component

This commit is contained in:
Tyler Hallada 2022-01-25 00:31:25 -05:00
parent b263c6b0cb
commit 8d3b801aab
6 changed files with 220 additions and 37 deletions

View File

@ -48,9 +48,10 @@ const jsonFetcher = async (url: string): Promise<Cell | null> => {
type Props = { type Props = {
selectedCell: { x: number; y: number }; selectedCell: { x: number; y: number };
counts: [number, number, number, number][];
}; };
const CellData: React.FC<Props> = ({ selectedCell }) => { const CellData: React.FC<Props> = ({ selectedCell, counts }) => {
const { data, error } = useSWRImmutable( const { data, error } = useSWRImmutable(
`https://cells.modmapper.com/${selectedCell.x}/${selectedCell.y}.json`, `https://cells.modmapper.com/${selectedCell.x}/${selectedCell.y}.json`,
jsonFetcher jsonFetcher
@ -82,7 +83,7 @@ const CellData: React.FC<Props> = ({ selectedCell }) => {
<span>{data.plugins_count}</span> <span>{data.plugins_count}</span>
</li> </li>
</ul> </ul>
<CellModList mods={data.mods} /> <CellModList mods={data.mods} counts={counts} />
</> </>
) )
); );

View File

@ -1,18 +1,14 @@
import { format } from "date-fns"; import { format } from "date-fns";
import React from "react"; import React from "react";
import useSWRImmutable from "swr/immutable";
import styles from "../styles/CellModList.module.css"; import styles from "../styles/CellModList.module.css";
import type { Mod } from "./CellData"; import type { Mod } from "./CellData";
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
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());
type Props = { type Props = {
mods: Mod[]; mods: Mod[];
counts: [number, number, number, number][];
}; };
type ModWithCounts = Mod & { type ModWithCounts = Mod & {
@ -21,18 +17,7 @@ type ModWithCounts = Mod & {
views: number; views: number;
}; };
const CellModList: React.FC<Props> = ({ mods }) => { const CellModList: React.FC<Props> = ({ mods, counts }) => {
// 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);
if (error)
return <div>{`Error loading live download counts: ${error.message}`}</div>;
if (!data) return <div>Loading...</div>;
const counts = data
.split("\n")
.map((line) => line.split(",").map((count) => parseInt(count, 10)));
const modsWithCounts: ModWithCounts[] = mods.map((mod) => { const modsWithCounts: ModWithCounts[] = mods.map((mod) => {
const modCounts = counts.find((count) => count[0] === mod.nexus_mod_id); const modCounts = counts.find((count) => count[0] === mod.nexus_mod_id);
return { return {

123
components/ModData.tsx Normal file
View File

@ -0,0 +1,123 @@
import { format } from "date-fns";
import React from "react";
import useSWRImmutable from "swr/immutable";
import styles from "../styles/ModData.module.css";
export interface CellCoord {
x: number;
y: number;
}
export interface Mod {
id: number;
name: string;
nexus_mod_id: number;
author_name: string;
author_id: number;
category_name: string;
category_id: number;
description: string;
thumbnail_link: string;
game_id: number;
updated_at: string;
created_at: string;
last_update_at: string;
first_upload_at: string;
last_updated_files_at: string;
cells: CellCoord[];
}
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
const jsonFetcher = async (url: string): Promise<Mod | 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 res.json();
};
type Props = {
selectedMod: number;
counts: [number, number, number, number][];
};
const ModData: React.FC<Props> = ({ selectedMod, counts }) => {
const { data, error } = useSWRImmutable(
`https://mods.modmapper.com/${selectedMod}.json`,
jsonFetcher
);
if (error && error.status === 404) {
return <div>Mod could not be found.</div>;
} else if (error) {
return <div>{`Error loading mod data: ${error.message}`}</div>;
}
if (data === undefined)
return <div className={styles.status}>Loading...</div>;
if (data === null)
return <div className={styles.status}>Mod could not be found.</div>;
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;
if (selectedMod && data) {
return (
<>
<h1>
<a
href={`${NEXUS_MODS_URL}/mods/${data.nexus_mod_id}`}
className={styles.link}
>
{data.name}
</a>
</h1>
<div>
<strong>Category:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/mods/categories/${data.category_id}`}
className={styles.link}
>
{data.category_name}
</a>
</div>
<div>
<strong>Author:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/users/${data.author_id}`}
className={styles.link}
>
{data.author_name}
</a>
</div>
<div>
<strong>Uploaded:</strong>{" "}
{format(new Date(data.first_upload_at), "d MMM y")}
</div>
<div>
<strong>Last Update:</strong>{" "}
{format(new Date(data.last_update_at), "d MMM y")}
</div>
<div>
<strong>Total Downloads:</strong> {numberFmt.format(total_downloads)}
</div>
<div>
<strong>Unique Downloads:</strong>{" "}
{numberFmt.format(unique_downloads)}
</div>
</>
);
}
return null;
};
export default ModData;

View File

@ -2,8 +2,10 @@ import React, { useEffect, useState, useRef } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import type mapboxgl from "mapbox-gl"; import type mapboxgl from "mapbox-gl";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import useSWRImmutable from "swr/immutable";
import styles from "../styles/SearchBar.module.css"; import styles from "../styles/SearchBar.module.css";
import { join } from "path/posix";
type Props = { type Props = {
clearSelectedCell: () => void; clearSelectedCell: () => void;
@ -11,8 +13,8 @@ type Props = {
}; };
interface Mod { interface Mod {
title: string; name: string;
nexus_mod_id: number; id: number;
} }
interface SearchResult { interface SearchResult {
@ -20,15 +22,36 @@ interface SearchResult {
refIndex: number; refIndex: number;
} }
const list: Mod[] = [ const jsonFetcher = async (url: string): Promise<Mod | null> => {
{ title: "Unofficial Skyrim Special Edition Patch", nexus_mod_id: 1 }, const res = await fetch(url);
{ title: "Enhanced Lights and FX", nexus_mod_id: 2 },
{ title: "Majestic Mountains", nexus_mod_id: 3 }, 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();
};
const SearchBar: React.FC<Props> = ({ clearSelectedCell, map }) => { const SearchBar: React.FC<Props> = ({ clearSelectedCell, map }) => {
const router = useRouter(); const router = useRouter();
const fuse = new Fuse(list, { keys: ["title"] });
const fuse = useRef<Fuse<Mod> | null>(null) as React.MutableRefObject<
Fuse<Mod>
>;
const { data, error } = useSWRImmutable(
`https://mods.modmapper.com/mod_search_index.json`,
jsonFetcher
);
useEffect(() => {
if (data && !fuse.current) {
fuse.current = new Fuse(data as unknown as Mod[], { keys: ["name"] });
}
}, [data]);
const searchInput = useRef<HTMLInputElement | null>(null); const searchInput = useRef<HTMLInputElement | null>(null);
const [search, setSearch] = useState<string>(""); const [search, setSearch] = useState<string>("");
@ -54,17 +77,19 @@ const SearchBar: React.FC<Props> = ({ clearSelectedCell, map }) => {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); setSearch(e.target.value);
const results: { item: Mod; refIndex: number }[] = fuse.search( if (fuse.current) {
e.target.value const results: { item: Mod; refIndex: number }[] = fuse.current.search(
); e.target.value
setResults(results); );
setResults(results);
}
}; };
const onChooseResult = const onChooseResult =
(item: Mod) => (item: Mod) =>
(e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>) => { (e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>) => {
clearSelectedCell(); clearSelectedCell();
router.push({ query: { mod: item.nexus_mod_id } }); router.push({ query: { mod: item.id } });
setSearch(""); setSearch("");
setResults([]); setResults([]);
setClickingResult(false); setClickingResult(false);
@ -91,17 +116,18 @@ const SearchBar: React.FC<Props> = ({ clearSelectedCell, map }) => {
}} }}
value={search} value={search}
ref={searchInput} ref={searchInput}
disabled={!data}
/> />
{results.length > 0 && ( {results.length > 0 && (
<ul className={styles["search-results"]}> <ul className={styles["search-results"]}>
{results.map((result) => ( {results.map((result) => (
<li <li
key={result.item.nexus_mod_id} key={result.item.id}
onClick={onChooseResult(result.item)} onClick={onChooseResult(result.item)}
onTouchStart={() => setClickingResult(true)} onTouchStart={() => setClickingResult(true)}
onMouseDown={() => setClickingResult(true)} onMouseDown={() => setClickingResult(true)}
> >
{result.item.title} {result.item.name}
</li> </li>
))} ))}
</ul> </ul>

View File

@ -1,9 +1,16 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWRImmutable from "swr/immutable";
import CellData from "./CellData"; import CellData from "./CellData";
import ModData from "./ModData";
import styles from "../styles/Sidebar.module.css"; 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 { interface Cell {
x: number; x: number;
y: number; y: number;
@ -18,6 +25,43 @@ type Props = {
const Sidebar: React.FC<Props> = ({ selectedCell, clearSelectedCell, map }) => { const Sidebar: React.FC<Props> = ({ selectedCell, clearSelectedCell, map }) => {
const router = useRouter(); 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) => (
<div>{`Error loading live download counts: ${error.message}`}</div>
);
const renderLoading = () => <div>Loading...</div>;
const renderCellData = (selectedCell: { x: number; y: number }) => {
if (error) return renderLoadError(error);
if (!counts) return renderLoading();
return <CellData selectedCell={selectedCell} counts={counts} />;
};
const renderModData = (selectedMod: number) => {
if (error) return renderLoadError(error);
if (!counts) return renderLoading();
return <ModData selectedMod={selectedMod} counts={counts} />;
};
const onClose = () => { const onClose = () => {
clearSelectedCell(); clearSelectedCell();
@ -32,16 +76,17 @@ const Sidebar: React.FC<Props> = ({ selectedCell, clearSelectedCell, map }) => {
<h1> <h1>
Cell {selectedCell.x}, {selectedCell.y} Cell {selectedCell.x}, {selectedCell.y}
</h1> </h1>
{selectedCell && <CellData selectedCell={selectedCell} />} {renderCellData(selectedCell)}
</div> </div>
); );
} else if (router.query.mod) { } else if (router.query.mod) {
const modId = parseInt(router.query.mod as string, 10);
return ( return (
<div className={styles.sidebar}> <div className={styles.sidebar}>
<button className={styles.close} onClick={onClose}> <button className={styles.close} onClick={onClose}>
</button> </button>
<h1>Mod {router.query.mod}</h1> {!Number.isNaN(modId) && renderModData(modId)}
</div> </div>
); );
} else { } else {

View File

@ -0,0 +1,3 @@
.status {
margin-top: 24px;
}