Very slow mod search, and mod data component
This commit is contained in:
parent
b263c6b0cb
commit
8d3b801aab
@ -48,9 +48,10 @@ const jsonFetcher = async (url: string): Promise<Cell | null> => {
|
||||
|
||||
type Props = {
|
||||
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(
|
||||
`https://cells.modmapper.com/${selectedCell.x}/${selectedCell.y}.json`,
|
||||
jsonFetcher
|
||||
@ -82,7 +83,7 @@ const CellData: React.FC<Props> = ({ selectedCell }) => {
|
||||
<span>{data.plugins_count}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<CellModList mods={data.mods} />
|
||||
<CellModList mods={data.mods} counts={counts} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
@ -1,18 +1,14 @@
|
||||
import { format } from "date-fns";
|
||||
import React from "react";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
import styles from "../styles/CellModList.module.css";
|
||||
import type { Mod } from "./CellData";
|
||||
|
||||
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 = {
|
||||
mods: Mod[];
|
||||
counts: [number, number, number, number][];
|
||||
};
|
||||
|
||||
type ModWithCounts = Mod & {
|
||||
@ -21,18 +17,7 @@ type ModWithCounts = Mod & {
|
||||
views: number;
|
||||
};
|
||||
|
||||
const CellModList: React.FC<Props> = ({ mods }) => {
|
||||
// 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 CellModList: React.FC<Props> = ({ mods, counts }) => {
|
||||
const modsWithCounts: ModWithCounts[] = mods.map((mod) => {
|
||||
const modCounts = counts.find((count) => count[0] === mod.nexus_mod_id);
|
||||
return {
|
||||
|
123
components/ModData.tsx
Normal file
123
components/ModData.tsx
Normal 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: </strong>
|
||||
<a
|
||||
href={`${NEXUS_MODS_URL}/mods/categories/${data.category_id}`}
|
||||
className={styles.link}
|
||||
>
|
||||
{data.category_name}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Author: </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;
|
@ -2,8 +2,10 @@ import React, { useEffect, useState, useRef } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import type mapboxgl from "mapbox-gl";
|
||||
import Fuse from "fuse.js";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
import styles from "../styles/SearchBar.module.css";
|
||||
import { join } from "path/posix";
|
||||
|
||||
type Props = {
|
||||
clearSelectedCell: () => void;
|
||||
@ -11,8 +13,8 @@ type Props = {
|
||||
};
|
||||
|
||||
interface Mod {
|
||||
title: string;
|
||||
nexus_mod_id: number;
|
||||
name: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
@ -20,15 +22,36 @@ interface SearchResult {
|
||||
refIndex: number;
|
||||
}
|
||||
|
||||
const list: Mod[] = [
|
||||
{ title: "Unofficial Skyrim Special Edition Patch", nexus_mod_id: 1 },
|
||||
{ title: "Enhanced Lights and FX", nexus_mod_id: 2 },
|
||||
{ title: "Majestic Mountains", nexus_mod_id: 3 },
|
||||
];
|
||||
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();
|
||||
};
|
||||
|
||||
const SearchBar: React.FC<Props> = ({ clearSelectedCell, map }) => {
|
||||
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 [search, setSearch] = useState<string>("");
|
||||
@ -54,17 +77,19 @@ const SearchBar: React.FC<Props> = ({ clearSelectedCell, map }) => {
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.target.value);
|
||||
const results: { item: Mod; refIndex: number }[] = fuse.search(
|
||||
if (fuse.current) {
|
||||
const results: { item: Mod; refIndex: number }[] = fuse.current.search(
|
||||
e.target.value
|
||||
);
|
||||
setResults(results);
|
||||
}
|
||||
};
|
||||
|
||||
const onChooseResult =
|
||||
(item: Mod) =>
|
||||
(e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>) => {
|
||||
clearSelectedCell();
|
||||
router.push({ query: { mod: item.nexus_mod_id } });
|
||||
router.push({ query: { mod: item.id } });
|
||||
setSearch("");
|
||||
setResults([]);
|
||||
setClickingResult(false);
|
||||
@ -91,17 +116,18 @@ const SearchBar: React.FC<Props> = ({ clearSelectedCell, map }) => {
|
||||
}}
|
||||
value={search}
|
||||
ref={searchInput}
|
||||
disabled={!data}
|
||||
/>
|
||||
{results.length > 0 && (
|
||||
<ul className={styles["search-results"]}>
|
||||
{results.map((result) => (
|
||||
<li
|
||||
key={result.item.nexus_mod_id}
|
||||
key={result.item.id}
|
||||
onClick={onChooseResult(result.item)}
|
||||
onTouchStart={() => setClickingResult(true)}
|
||||
onMouseDown={() => setClickingResult(true)}
|
||||
>
|
||||
{result.item.title}
|
||||
{result.item.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
@ -1,9 +1,16 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
import CellData from "./CellData";
|
||||
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;
|
||||
@ -18,6 +25,43 @@ type Props = {
|
||||
|
||||
const Sidebar: React.FC<Props> = ({ selectedCell, clearSelectedCell, 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) => (
|
||||
<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 = () => {
|
||||
clearSelectedCell();
|
||||
@ -32,16 +76,17 @@ const Sidebar: React.FC<Props> = ({ selectedCell, clearSelectedCell, map }) => {
|
||||
<h1>
|
||||
Cell {selectedCell.x}, {selectedCell.y}
|
||||
</h1>
|
||||
{selectedCell && <CellData selectedCell={selectedCell} />}
|
||||
{renderCellData(selectedCell)}
|
||||
</div>
|
||||
);
|
||||
} else if (router.query.mod) {
|
||||
const modId = parseInt(router.query.mod as string, 10);
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<button className={styles.close} onClick={onClose}>
|
||||
✖
|
||||
</button>
|
||||
<h1>Mod {router.query.mod}</h1>
|
||||
{!Number.isNaN(modId) && renderModData(modId)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
3
styles/ModData.module.css
Normal file
3
styles/ModData.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.status {
|
||||
margin-top: 24px;
|
||||
}
|
Loading…
Reference in New Issue
Block a user