Show exterior cells edited in ModList
Also abstracts jsonFetcher and csvFetcher into common lib functions.
This commit is contained in:
parent
f120090039
commit
1a39f7c5e4
@ -5,6 +5,7 @@ import useSWRImmutable from "swr/immutable";
|
||||
import styles from "../styles/CellData.module.css";
|
||||
import ModList from "./ModList";
|
||||
import PluginList from "./PluginsList";
|
||||
import { jsonFetcher } from "../lib/api";
|
||||
|
||||
export interface Mod {
|
||||
id: number;
|
||||
@ -36,19 +37,6 @@ export interface Cell {
|
||||
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 = {
|
||||
selectedCell: { x: number; y: number };
|
||||
counts: Record<number, [number, number, number]> | null;
|
||||
@ -57,7 +45,7 @@ type Props = {
|
||||
const CellData: React.FC<Props> = ({ selectedCell, counts }) => {
|
||||
const { data, error } = useSWRImmutable(
|
||||
`https://cells.modmapper.com/${selectedCell.x}/${selectedCell.y}.json`,
|
||||
jsonFetcher
|
||||
(_) => jsonFetcher<Cell>(_)
|
||||
);
|
||||
|
||||
if (error && error.status === 404) {
|
||||
|
@ -5,8 +5,6 @@ import MiniSearch from "minisearch";
|
||||
import styles from "../styles/CellList.module.css";
|
||||
import type { CellCoord } from "./ModData";
|
||||
|
||||
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
|
||||
|
||||
type Props = {
|
||||
cells: CellCoord[];
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import styles from "../styles/Map.module.css";
|
||||
import Sidebar from "./Sidebar";
|
||||
import ToggleLayersControl from "./ToggleLayersControl";
|
||||
import SearchBar from "./SearchBar";
|
||||
import { csvFetcher, jsonFetcherWithLastModified } from "../lib/api";
|
||||
|
||||
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? "";
|
||||
|
||||
@ -26,13 +27,6 @@ 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(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 router = useRouter();
|
||||
const mapContainer = useRef<HTMLDivElement | null>(
|
||||
@ -67,7 +61,7 @@ const Map: React.FC = () => {
|
||||
|
||||
const { data: cellsData, error: cellsError } = useSWRImmutable(
|
||||
"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
|
||||
const [counts, setCounts] = useState<Record<
|
||||
@ -560,9 +554,7 @@ const Map: React.FC = () => {
|
||||
x * cellSize + viewportNW.x,
|
||||
y * cellSize + viewportNW.y + cellSize,
|
||||
]);
|
||||
const editCount = (cellsData.data as Record<string, number>)[
|
||||
`${x - 57},${50 - y}`
|
||||
];
|
||||
const editCount = cellsData.data[`${x - 57},${50 - y}`];
|
||||
grid.features.push({
|
||||
type: "Feature",
|
||||
id: x * 1000 + y,
|
||||
|
@ -5,6 +5,7 @@ import useSWRImmutable from "swr/immutable";
|
||||
|
||||
import CellList from "./CellList";
|
||||
import styles from "../styles/ModData.module.css";
|
||||
import { jsonFetcher } from "../lib/api";
|
||||
|
||||
export interface CellCoord {
|
||||
x: number;
|
||||
@ -33,19 +34,6 @@ export interface Mod {
|
||||
|
||||
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: Record<number, [number, number, number]> | null;
|
||||
@ -59,7 +47,7 @@ const ModData: React.FC<Props> = ({
|
||||
}) => {
|
||||
const { data, error } = useSWRImmutable(
|
||||
`https://mods.modmapper.com/${selectedMod}.json`,
|
||||
jsonFetcher
|
||||
(_) => jsonFetcher<Mod>(_)
|
||||
);
|
||||
|
||||
if (error && error.status === 404) {
|
||||
|
@ -2,11 +2,13 @@ import { format } from "date-fns";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import MiniSearch from "minisearch";
|
||||
import Link from "next/link";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
import styles from "../styles/ModList.module.css";
|
||||
import type { Mod } from "./CellData";
|
||||
import type { File } from "../slices/plugins";
|
||||
import { formatBytes } from "../lib/plugins";
|
||||
import { jsonFetcher } from "../lib/api";
|
||||
|
||||
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
|
||||
|
||||
@ -20,6 +22,7 @@ type ModWithCounts = Mod & {
|
||||
total_downloads: number;
|
||||
unique_downloads: number;
|
||||
views: number;
|
||||
exterior_cells_edited: number;
|
||||
};
|
||||
|
||||
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 [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
|
||||
.map((mod) => {
|
||||
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,
|
||||
unique_downloads: modCounts ? modCounts[1] : 0,
|
||||
views: modCounts ? modCounts[2] : 0,
|
||||
exterior_cells_edited: cellCounts
|
||||
? cellCounts[mod.nexus_mod_id] ?? 0
|
||||
: 0,
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
@ -106,14 +117,17 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
|
||||
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="nexus_mod_id">ID</option>
|
||||
<option value="author_name">Author</option>
|
||||
<option value="first_upload_at">Upload Date</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>
|
||||
</div>
|
||||
<div className={styles["filter-row"]}>
|
||||
@ -221,6 +235,12 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
|
||||
<strong>Unique Downloads:</strong>{" "}
|
||||
{numberFmt.format(mod.unique_downloads)}
|
||||
</div>
|
||||
{cellCounts && (
|
||||
<div>
|
||||
<strong>Exterior Cells Edited:</strong>{" "}
|
||||
{numberFmt.format(mod.exterior_cells_edited)}
|
||||
</div>
|
||||
)}
|
||||
<ul className={styles["file-list"]}>
|
||||
{files &&
|
||||
files
|
||||
|
@ -12,21 +12,7 @@ import CellList from "./CellList";
|
||||
import type { CellCoord } from "./ModData";
|
||||
import PluginData, { Plugin as PluginProps } from "./PluginData";
|
||||
import styles from "../styles/PluginData.module.css";
|
||||
|
||||
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();
|
||||
};
|
||||
import { jsonFetcher } from "../lib/api";
|
||||
|
||||
const buildPluginProps = (
|
||||
data?: PluginsByHashWithMods | null,
|
||||
@ -65,7 +51,7 @@ type Props = {
|
||||
const PluginDetail: React.FC<Props> = ({ hash, counts }) => {
|
||||
const { data, error } = useSWRImmutable(
|
||||
`https://plugins.modmapper.com/${hash}.json`,
|
||||
jsonFetcher
|
||||
(_) => jsonFetcher<PluginsByHashWithMods>(_)
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -5,6 +5,7 @@ import MiniSearch, { SearchResult } from "minisearch";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
import styles from "../styles/SearchBar.module.css";
|
||||
import { jsonFetcher } from "../lib/api";
|
||||
|
||||
type Props = {
|
||||
counts: Record<number, [number, number, number]> | null;
|
||||
@ -16,19 +17,6 @@ interface Mod {
|
||||
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 = [];
|
||||
|
||||
for (let x = -77; x < 76; x++) {
|
||||
@ -58,7 +46,7 @@ const SearchBar: React.FC<Props> = ({ counts, sidebarOpen }) => {
|
||||
|
||||
const { data, error } = useSWRImmutable(
|
||||
`https://mods.modmapper.com/mod_search_index.json`,
|
||||
jsonFetcher
|
||||
(_) => jsonFetcher<Mod[]>(_)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -72,7 +60,7 @@ const SearchBar: React.FC<Props> = ({ counts, sidebarOpen }) => {
|
||||
prefix: true,
|
||||
},
|
||||
});
|
||||
modSearch.current.addAll(data as unknown as Mod[]);
|
||||
modSearch.current.addAll(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
34
lib/api.ts
Normal file
34
lib/api.ts
Normal 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();
|
||||
}
|
Loading…
Reference in New Issue
Block a user