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 = {
|
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} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -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
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 { 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) {
|
||||||
|
const results: { item: Mod; refIndex: number }[] = fuse.current.search(
|
||||||
e.target.value
|
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>
|
||||||
|
@ -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 {
|
||||||
|
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