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 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) {
|
||||||
|
@ -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[];
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
@ -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
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