Show exterior cells edited in ModList

Also abstracts jsonFetcher and csvFetcher into common lib functions.
This commit is contained in:
Tyler Hallada 2022-03-18 01:06:20 -04:00
parent f120090039
commit 1a39f7c5e4
8 changed files with 70 additions and 76 deletions

View File

@ -5,6 +5,7 @@ import useSWRImmutable from "swr/immutable";
import styles from "../styles/CellData.module.css"; import styles from "../styles/CellData.module.css";
import ModList from "./ModList"; import ModList from "./ModList";
import PluginList from "./PluginsList"; import PluginList from "./PluginsList";
import { jsonFetcher } from "../lib/api";
export interface Mod { export interface Mod {
id: number; id: number;
@ -36,19 +37,6 @@ export interface Cell {
mods: Mod[]; mods: Mod[];
} }
const jsonFetcher = async (url: string): Promise<Cell | 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 = { type Props = {
selectedCell: { x: number; y: number }; selectedCell: { x: number; y: number };
counts: Record<number, [number, number, number]> | null; counts: Record<number, [number, number, number]> | null;
@ -57,7 +45,7 @@ type Props = {
const CellData: React.FC<Props> = ({ selectedCell, counts }) => { 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<Cell>(_)
); );
if (error && error.status === 404) { if (error && error.status === 404) {

View File

@ -5,8 +5,6 @@ import MiniSearch from "minisearch";
import styles from "../styles/CellList.module.css"; import styles from "../styles/CellList.module.css";
import type { CellCoord } from "./ModData"; import type { CellCoord } from "./ModData";
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
type Props = { type Props = {
cells: CellCoord[]; cells: CellCoord[];
}; };

View File

@ -10,6 +10,7 @@ import styles from "../styles/Map.module.css";
import Sidebar from "./Sidebar"; import Sidebar from "./Sidebar";
import ToggleLayersControl from "./ToggleLayersControl"; import ToggleLayersControl from "./ToggleLayersControl";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import { csvFetcher, jsonFetcherWithLastModified } from "../lib/api";
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? ""; mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? "";
@ -26,13 +27,6 @@ colorGradient.setMidpoint(360);
const LIVE_DOWNLOAD_COUNTS_URL = const LIVE_DOWNLOAD_COUNTS_URL =
"https://staticstats.nexusmods.com/live_download_counts/mods/1704.csv"; "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 Map: React.FC = () => {
const router = useRouter(); const router = useRouter();
const mapContainer = useRef<HTMLDivElement | null>( const mapContainer = useRef<HTMLDivElement | null>(
@ -67,7 +61,7 @@ const Map: React.FC = () => {
const { data: cellsData, error: cellsError } = useSWRImmutable( const { data: cellsData, error: cellsError } = useSWRImmutable(
"https://cells.modmapper.com/edits.json", "https://cells.modmapper.com/edits.json",
jsonFetcher (_) => jsonFetcherWithLastModified<Record<string, number>>(_)
); );
// The live download counts are not really immutable, but I'd still rather load them once per session // The live download counts are not really immutable, but I'd still rather load them once per session
const [counts, setCounts] = useState<Record< const [counts, setCounts] = useState<Record<
@ -560,9 +554,7 @@ const Map: React.FC = () => {
x * cellSize + viewportNW.x, x * cellSize + viewportNW.x,
y * cellSize + viewportNW.y + cellSize, y * cellSize + viewportNW.y + cellSize,
]); ]);
const editCount = (cellsData.data as Record<string, number>)[ const editCount = cellsData.data[`${x - 57},${50 - y}`];
`${x - 57},${50 - y}`
];
grid.features.push({ grid.features.push({
type: "Feature", type: "Feature",
id: x * 1000 + y, id: x * 1000 + y,

View File

@ -5,6 +5,7 @@ import useSWRImmutable from "swr/immutable";
import CellList from "./CellList"; import CellList from "./CellList";
import styles from "../styles/ModData.module.css"; import styles from "../styles/ModData.module.css";
import { jsonFetcher } from "../lib/api";
export interface CellCoord { export interface CellCoord {
x: number; x: number;
@ -33,19 +34,6 @@ export interface Mod {
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; 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 = { type Props = {
selectedMod: number; selectedMod: number;
counts: Record<number, [number, number, number]> | null; counts: Record<number, [number, number, number]> | null;
@ -59,7 +47,7 @@ const ModData: React.FC<Props> = ({
}) => { }) => {
const { data, error } = useSWRImmutable( const { data, error } = useSWRImmutable(
`https://mods.modmapper.com/${selectedMod}.json`, `https://mods.modmapper.com/${selectedMod}.json`,
jsonFetcher (_) => jsonFetcher<Mod>(_)
); );
if (error && error.status === 404) { if (error && error.status === 404) {

View File

@ -2,11 +2,13 @@ import { format } from "date-fns";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import MiniSearch from "minisearch"; import MiniSearch from "minisearch";
import Link from "next/link"; import Link from "next/link";
import useSWRImmutable from "swr/immutable";
import styles from "../styles/ModList.module.css"; import styles from "../styles/ModList.module.css";
import type { Mod } from "./CellData"; import type { Mod } from "./CellData";
import type { File } from "../slices/plugins"; import type { File } from "../slices/plugins";
import { formatBytes } from "../lib/plugins"; import { formatBytes } from "../lib/plugins";
import { jsonFetcher } from "../lib/api";
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
@ -20,6 +22,7 @@ type ModWithCounts = Mod & {
total_downloads: number; total_downloads: number;
unique_downloads: number; unique_downloads: number;
views: number; views: number;
exterior_cells_edited: number;
}; };
const ModList: React.FC<Props> = ({ mods, files, counts }) => { const ModList: React.FC<Props> = ({ mods, files, counts }) => {
@ -29,6 +32,11 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
const [category, setCategory] = useState<string>("All"); const [category, setCategory] = useState<string>("All");
const [filterResults, setFilterResults] = useState<Set<number>>(new Set()); const [filterResults, setFilterResults] = useState<Set<number>>(new Set());
const { data: cellCounts, error: cellCountsError } = useSWRImmutable(
`https://mods.modmapper.com/mod_cell_counts.json`,
(_) => jsonFetcher<Record<string, number>>(_)
);
const modsWithCounts: ModWithCounts[] = mods const modsWithCounts: ModWithCounts[] = mods
.map((mod) => { .map((mod) => {
const modCounts = counts && counts[mod.nexus_mod_id]; const modCounts = counts && counts[mod.nexus_mod_id];
@ -37,6 +45,9 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
total_downloads: modCounts ? modCounts[0] : 0, total_downloads: modCounts ? modCounts[0] : 0,
unique_downloads: modCounts ? modCounts[1] : 0, unique_downloads: modCounts ? modCounts[1] : 0,
views: modCounts ? modCounts[2] : 0, views: modCounts ? modCounts[2] : 0,
exterior_cells_edited: cellCounts
? cellCounts[mod.nexus_mod_id] ?? 0
: 0,
}; };
}) })
.filter( .filter(
@ -106,14 +117,17 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
setSortBy(event.target.value as keyof ModWithCounts) setSortBy(event.target.value as keyof ModWithCounts)
} }
> >
<option value="unique_downloads">Unique Downloads</option>
<option value="total_downloads">Total Downloads</option>
<option value="views">Views</option>
<option value="name">Name</option> <option value="name">Name</option>
<option value="nexus_mod_id">ID</option>
<option value="author_name">Author</option> <option value="author_name">Author</option>
<option value="first_upload_at">Upload Date</option> <option value="first_upload_at">Upload Date</option>
<option value="last_update_at">Last Update</option> <option value="last_update_at">Last Update</option>
<option value="total_downloads">Total Downloads</option>
<option value="unique_downloads">Unique Downloads</option>
<option value="views">Views</option>
<option value="exterior_cells_edited">
Exterior Cells Edited
</option>
<option value="nexus_mod_id">ID</option>
</select> </select>
</div> </div>
<div className={styles["filter-row"]}> <div className={styles["filter-row"]}>
@ -221,6 +235,12 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
<strong>Unique Downloads:</strong>{" "} <strong>Unique Downloads:</strong>{" "}
{numberFmt.format(mod.unique_downloads)} {numberFmt.format(mod.unique_downloads)}
</div> </div>
{cellCounts && (
<div>
<strong>Exterior Cells Edited:</strong>{" "}
{numberFmt.format(mod.exterior_cells_edited)}
</div>
)}
<ul className={styles["file-list"]}> <ul className={styles["file-list"]}>
{files && {files &&
files files

View File

@ -12,21 +12,7 @@ import CellList from "./CellList";
import type { CellCoord } from "./ModData"; import type { CellCoord } from "./ModData";
import PluginData, { Plugin as PluginProps } from "./PluginData"; import PluginData, { Plugin as PluginProps } from "./PluginData";
import styles from "../styles/PluginData.module.css"; import styles from "../styles/PluginData.module.css";
import { jsonFetcher } from "../lib/api";
const jsonFetcher = async (
url: string
): Promise<PluginsByHashWithMods | 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 buildPluginProps = ( const buildPluginProps = (
data?: PluginsByHashWithMods | null, data?: PluginsByHashWithMods | null,
@ -65,7 +51,7 @@ type Props = {
const PluginDetail: React.FC<Props> = ({ hash, counts }) => { const PluginDetail: React.FC<Props> = ({ hash, counts }) => {
const { data, error } = useSWRImmutable( const { data, error } = useSWRImmutable(
`https://plugins.modmapper.com/${hash}.json`, `https://plugins.modmapper.com/${hash}.json`,
jsonFetcher (_) => jsonFetcher<PluginsByHashWithMods>(_)
); );
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();

View File

@ -5,6 +5,7 @@ import MiniSearch, { SearchResult } from "minisearch";
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable";
import styles from "../styles/SearchBar.module.css"; import styles from "../styles/SearchBar.module.css";
import { jsonFetcher } from "../lib/api";
type Props = { type Props = {
counts: Record<number, [number, number, number]> | null; counts: Record<number, [number, number, number]> | null;
@ -16,19 +17,6 @@ interface Mod {
id: number; id: number;
} }
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();
};
let cells = []; let cells = [];
for (let x = -77; x < 76; x++) { for (let x = -77; x < 76; x++) {
@ -58,7 +46,7 @@ const SearchBar: React.FC<Props> = ({ counts, sidebarOpen }) => {
const { data, error } = useSWRImmutable( const { data, error } = useSWRImmutable(
`https://mods.modmapper.com/mod_search_index.json`, `https://mods.modmapper.com/mod_search_index.json`,
jsonFetcher (_) => jsonFetcher<Mod[]>(_)
); );
useEffect(() => { useEffect(() => {
@ -72,7 +60,7 @@ const SearchBar: React.FC<Props> = ({ counts, sidebarOpen }) => {
prefix: true, prefix: true,
}, },
}); });
modSearch.current.addAll(data as unknown as Mod[]); modSearch.current.addAll(data);
} }
}, [data]); }, [data]);

34
lib/api.ts Normal file
View File

@ -0,0 +1,34 @@
export async function jsonFetcher<T>(url: string): Promise<T | 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();
};
export async function jsonFetcherWithLastModified<T>(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<string> {
return (await fetch(url)).text();
}