2022-01-25 05:31:25 +00:00
|
|
|
import { format } from "date-fns";
|
2022-02-14 05:38:21 +00:00
|
|
|
import Head from "next/head";
|
2022-09-03 06:51:57 +00:00
|
|
|
import React, { useCallback, useContext, useEffect, useState } from "react";
|
2022-01-25 05:31:25 +00:00
|
|
|
import useSWRImmutable from "swr/immutable";
|
|
|
|
|
2022-08-20 04:12:10 +00:00
|
|
|
import { useAppDispatch, useAppSelector } from "../lib/hooks";
|
2022-03-15 01:55:52 +00:00
|
|
|
import CellList from "./CellList";
|
2022-01-25 05:31:25 +00:00
|
|
|
import styles from "../styles/ModData.module.css";
|
2022-03-18 05:06:20 +00:00
|
|
|
import { jsonFetcher } from "../lib/api";
|
2022-09-06 19:15:27 +00:00
|
|
|
import { editionNames } from "../lib/games";
|
2022-08-20 04:12:10 +00:00
|
|
|
import {
|
|
|
|
PluginsByHashWithMods,
|
|
|
|
removeFetchedPlugin,
|
|
|
|
updateFetchedPlugin,
|
|
|
|
} from "../slices/plugins";
|
|
|
|
import Link from "next/link";
|
2022-09-03 06:51:57 +00:00
|
|
|
import { DownloadCountsContext } from "./DownloadCountsProvider";
|
|
|
|
import { GamesContext } from "./GamesProvider";
|
2022-01-25 05:31:25 +00:00
|
|
|
|
|
|
|
export interface CellCoord {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
}
|
|
|
|
|
2022-08-18 03:19:55 +00:00
|
|
|
export interface ModFile {
|
|
|
|
name: string;
|
|
|
|
version: string;
|
|
|
|
category: string;
|
|
|
|
nexus_file_id: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface FilePlugin {
|
|
|
|
hash: number;
|
|
|
|
file_path: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface FileCell {
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface File {
|
|
|
|
id: number;
|
|
|
|
name: string;
|
|
|
|
file_name: string;
|
|
|
|
nexus_file_id: number;
|
|
|
|
mod_id: number;
|
|
|
|
category: string;
|
|
|
|
version: string;
|
|
|
|
mod_version: string;
|
|
|
|
size: number;
|
|
|
|
uploaded_at: string;
|
|
|
|
created_at: string;
|
|
|
|
downloaded_at: string;
|
|
|
|
has_plugin: boolean;
|
|
|
|
unable_to_extract_plugins: boolean;
|
|
|
|
cells: FileCell[];
|
|
|
|
plugins: FilePlugin[];
|
|
|
|
plugin_count: number;
|
|
|
|
}
|
|
|
|
|
2022-01-25 05:31:25 +00:00
|
|
|
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;
|
2022-03-16 04:37:08 +00:00
|
|
|
is_translation: boolean;
|
2022-01-25 05:31:25 +00:00
|
|
|
updated_at: string;
|
|
|
|
created_at: string;
|
|
|
|
last_update_at: string;
|
|
|
|
first_upload_at: string;
|
|
|
|
last_updated_files_at: string;
|
|
|
|
cells: CellCoord[];
|
2022-08-18 03:19:55 +00:00
|
|
|
files: ModFile[];
|
2022-01-25 05:31:25 +00:00
|
|
|
}
|
|
|
|
|
2022-09-03 19:41:44 +00:00
|
|
|
export const NEXUS_MODS_URL = "https://www.nexusmods.com";
|
2022-01-25 05:31:25 +00:00
|
|
|
|
|
|
|
type Props = {
|
2022-09-03 19:41:44 +00:00
|
|
|
game: string;
|
2022-01-25 05:31:25 +00:00
|
|
|
selectedMod: number;
|
2022-08-20 03:50:16 +00:00
|
|
|
selectedFile: number;
|
|
|
|
selectedPlugin: string;
|
2022-02-07 03:00:14 +00:00
|
|
|
setSelectedCells: (cells: { x: number; y: number }[] | null) => void;
|
2022-08-20 03:50:16 +00:00
|
|
|
onSelectFile: (fileId: number) => void;
|
|
|
|
onSelectPlugin: (hash: string) => void;
|
2022-01-25 05:31:25 +00:00
|
|
|
};
|
|
|
|
|
2022-02-07 03:00:14 +00:00
|
|
|
const ModData: React.FC<Props> = ({
|
2022-09-03 19:41:44 +00:00
|
|
|
game,
|
2022-02-07 03:00:14 +00:00
|
|
|
selectedMod,
|
2022-08-20 03:50:16 +00:00
|
|
|
selectedFile,
|
|
|
|
selectedPlugin,
|
2022-02-07 03:00:14 +00:00
|
|
|
setSelectedCells,
|
2022-08-20 03:50:16 +00:00
|
|
|
onSelectFile,
|
|
|
|
onSelectPlugin,
|
2022-02-07 03:00:14 +00:00
|
|
|
}) => {
|
2022-09-03 06:51:57 +00:00
|
|
|
const {
|
|
|
|
games,
|
|
|
|
getGameNameById,
|
|
|
|
error: gamesError,
|
|
|
|
} = useContext(GamesContext);
|
|
|
|
const counts = useContext(DownloadCountsContext);
|
2022-08-29 03:43:44 +00:00
|
|
|
const [showAddRemovePluginNotification, setShowAddRemovePluginNotification] =
|
|
|
|
useState<boolean>(false);
|
2022-08-20 03:50:16 +00:00
|
|
|
const { data: modData, error: modError } = useSWRImmutable(
|
2022-09-03 19:41:44 +00:00
|
|
|
`https://mods.modmapper.com/${game}/${selectedMod}.json`,
|
2022-03-18 05:06:20 +00:00
|
|
|
(_) => jsonFetcher<Mod>(_)
|
2022-01-25 05:31:25 +00:00
|
|
|
);
|
|
|
|
|
2022-08-20 03:50:16 +00:00
|
|
|
const { data: fileData, error: fileError } = useSWRImmutable(
|
|
|
|
selectedFile ? `https://files.modmapper.com/${selectedFile}.json` : null,
|
|
|
|
(_) => jsonFetcher<File>(_)
|
|
|
|
);
|
|
|
|
|
|
|
|
const { data: pluginData, error: pluginError } = useSWRImmutable(
|
|
|
|
selectedPlugin
|
2022-08-20 04:12:10 +00:00
|
|
|
? `https://plugins.modmapper.com/${selectedPlugin}.json`
|
2022-08-20 03:50:16 +00:00
|
|
|
: null,
|
2022-08-20 04:12:10 +00:00
|
|
|
(_) => jsonFetcher<PluginsByHashWithMods>(_)
|
|
|
|
);
|
|
|
|
|
|
|
|
const dispatch = useAppDispatch();
|
|
|
|
const fetchedPlugin = useAppSelector((state) =>
|
|
|
|
state.plugins.fetchedPlugins.find(
|
|
|
|
(plugin) => plugin.hash === selectedPlugin
|
|
|
|
)
|
2022-08-20 03:50:16 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
const handleFileChange = useCallback(
|
|
|
|
(event) => {
|
|
|
|
onSelectFile(event.target.value);
|
|
|
|
},
|
|
|
|
[onSelectFile]
|
|
|
|
);
|
|
|
|
const handlePluginChange = useCallback(
|
|
|
|
(event) => {
|
|
|
|
onSelectPlugin(event.target.value);
|
|
|
|
},
|
|
|
|
[onSelectPlugin]
|
|
|
|
);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (modData && !selectedFile) setSelectedCells(modData.cells);
|
|
|
|
}, [modData, setSelectedCells, selectedFile]);
|
|
|
|
|
2022-03-19 19:38:33 +00:00
|
|
|
useEffect(() => {
|
2022-08-20 03:50:16 +00:00
|
|
|
if (fileData) setSelectedCells(fileData.cells);
|
|
|
|
}, [fileData, setSelectedCells]);
|
2022-03-19 19:38:33 +00:00
|
|
|
|
2022-08-20 04:52:40 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (pluginData) setSelectedCells(pluginData.cells);
|
|
|
|
}, [pluginData, setSelectedCells]);
|
|
|
|
|
2022-09-03 06:51:57 +00:00
|
|
|
const renderDownloadCountsLoading = () => (
|
|
|
|
<div>Loading live download counts...</div>
|
|
|
|
);
|
|
|
|
const renderDownloadCountsError = (error: Error) => (
|
|
|
|
<div>{`Error loading live download counts: ${error.message}`}</div>
|
|
|
|
);
|
|
|
|
const renderGamesError = (error?: Error) =>
|
|
|
|
error ? (
|
|
|
|
<div>{`Error loading games: ${error.message}`}</div>
|
|
|
|
) : (
|
|
|
|
<div>Error loading games</div>
|
|
|
|
);
|
|
|
|
|
2022-08-20 03:50:16 +00:00
|
|
|
if (modError && modError.status === 404) {
|
2022-01-25 05:31:25 +00:00
|
|
|
return <div>Mod could not be found.</div>;
|
2022-08-20 03:50:16 +00:00
|
|
|
} else if (modError) {
|
|
|
|
return <div>{`Error loading mod modData: ${modError.message}`}</div>;
|
2022-01-25 05:31:25 +00:00
|
|
|
}
|
2022-08-20 03:50:16 +00:00
|
|
|
if (modData === undefined)
|
2022-01-25 05:31:25 +00:00
|
|
|
return <div className={styles.status}>Loading...</div>;
|
2022-08-20 03:50:16 +00:00
|
|
|
if (modData === null)
|
2022-01-25 05:31:25 +00:00
|
|
|
return <div className={styles.status}>Mod could not be found.</div>;
|
|
|
|
|
|
|
|
let numberFmt = new Intl.NumberFormat("en-US");
|
2022-09-03 06:51:57 +00:00
|
|
|
const gameName = getGameNameById(modData.game_id);
|
|
|
|
const gameDownloadCounts = gameName && counts[gameName].counts;
|
|
|
|
const modCounts =
|
|
|
|
gameDownloadCounts && gameDownloadCounts[modData.nexus_mod_id];
|
2022-01-30 21:55:50 +00:00
|
|
|
const total_downloads = modCounts ? modCounts[0] : 0;
|
|
|
|
const unique_downloads = modCounts ? modCounts[1] : 0;
|
|
|
|
const views = modCounts ? modCounts[2] : 0;
|
2022-01-25 05:31:25 +00:00
|
|
|
|
2022-08-20 03:50:16 +00:00
|
|
|
if (selectedMod && modData) {
|
2022-01-25 05:31:25 +00:00
|
|
|
return (
|
|
|
|
<>
|
2022-02-14 05:38:21 +00:00
|
|
|
<Head>
|
2022-08-20 03:50:16 +00:00
|
|
|
<title key="title">{`Modmapper - ${modData.name}`}</title>
|
2022-02-14 05:38:21 +00:00
|
|
|
<meta
|
|
|
|
key="description"
|
|
|
|
name="description"
|
2022-08-20 03:50:16 +00:00
|
|
|
content={`Map of Skyrim showing ${modData.cells.length} cell edits from the mod: ${modData.name}`}
|
2022-02-14 05:38:21 +00:00
|
|
|
/>
|
|
|
|
<meta
|
|
|
|
key="og:title"
|
|
|
|
property="og:title"
|
2022-08-20 03:50:16 +00:00
|
|
|
content={`Modmapper - ${modData.name}`}
|
2022-02-14 05:38:21 +00:00
|
|
|
/>
|
|
|
|
<meta
|
|
|
|
key="og:description"
|
|
|
|
property="og:description"
|
2022-08-20 03:50:16 +00:00
|
|
|
content={`Map of Skyrim showing ${modData.cells.length} cell edits from the mod: ${modData.name}`}
|
2022-02-14 05:38:21 +00:00
|
|
|
/>
|
|
|
|
<meta
|
|
|
|
key="twitter:title"
|
|
|
|
name="twitter:title"
|
2022-08-20 03:50:16 +00:00
|
|
|
content={`Modmapper - ${modData.name}`}
|
2022-02-14 05:38:21 +00:00
|
|
|
/>
|
|
|
|
<meta
|
|
|
|
key="twitter:description"
|
|
|
|
name="twitter:description"
|
2022-08-20 03:50:16 +00:00
|
|
|
content={`Map of Skyrim showing ${modData.cells.length} cell edits from the mod: ${modData.name}`}
|
2022-02-14 05:38:21 +00:00
|
|
|
/>
|
|
|
|
<meta
|
|
|
|
key="og:url"
|
|
|
|
property="og:url"
|
2022-09-03 19:41:44 +00:00
|
|
|
content={`https://modmapper.com/?game=${getGameNameById(
|
|
|
|
modData.game_id
|
|
|
|
)}&mod=${modData.nexus_mod_id}`}
|
2022-02-14 05:38:21 +00:00
|
|
|
/>
|
|
|
|
</Head>
|
2022-01-25 05:31:25 +00:00
|
|
|
<h1>
|
|
|
|
<a
|
2022-09-03 19:41:44 +00:00
|
|
|
href={`${NEXUS_MODS_URL}/${getGameNameById(modData.game_id)}/mods/${
|
|
|
|
modData.nexus_mod_id
|
|
|
|
}`}
|
2022-02-07 03:00:14 +00:00
|
|
|
target="_blank"
|
|
|
|
rel="noreferrer noopener"
|
2022-02-27 06:17:52 +00:00
|
|
|
className={styles.name}
|
2022-01-25 05:31:25 +00:00
|
|
|
>
|
2022-08-20 03:50:16 +00:00
|
|
|
{modData.name}
|
2022-01-25 05:31:25 +00:00
|
|
|
</a>
|
|
|
|
</h1>
|
2022-09-06 19:15:27 +00:00
|
|
|
<div>
|
|
|
|
<strong>Edition: </strong>
|
|
|
|
{
|
|
|
|
editionNames[
|
|
|
|
getGameNameById(modData.game_id) ?? "skyrimspecialedition"
|
|
|
|
]
|
|
|
|
}
|
|
|
|
</div>
|
2022-01-25 05:31:25 +00:00
|
|
|
<div>
|
|
|
|
<strong>Category: </strong>
|
|
|
|
<a
|
2022-09-03 19:41:44 +00:00
|
|
|
href={`${NEXUS_MODS_URL}/${getGameNameById(
|
|
|
|
modData.game_id
|
|
|
|
)}/mods/categories/${modData.category_id}`}
|
2022-02-07 03:00:14 +00:00
|
|
|
target="_blank"
|
|
|
|
rel="noreferrer noopener"
|
2022-01-25 05:31:25 +00:00
|
|
|
>
|
2022-08-20 03:50:16 +00:00
|
|
|
{modData.category_name}
|
2022-01-25 05:31:25 +00:00
|
|
|
</a>
|
2022-08-20 03:50:16 +00:00
|
|
|
{modData.is_translation && <strong> (translation)</strong>}
|
2022-01-25 05:31:25 +00:00
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<strong>Author: </strong>
|
|
|
|
<a
|
2022-09-03 19:41:44 +00:00
|
|
|
href={`${NEXUS_MODS_URL}/${getGameNameById(
|
|
|
|
modData.game_id
|
|
|
|
)}/users/${modData.author_id}`}
|
2022-02-07 03:00:14 +00:00
|
|
|
target="_blank"
|
|
|
|
rel="noreferrer noopener"
|
2022-01-25 05:31:25 +00:00
|
|
|
>
|
2022-08-20 03:50:16 +00:00
|
|
|
{modData.author_name}
|
2022-01-25 05:31:25 +00:00
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<strong>Uploaded:</strong>{" "}
|
2022-08-20 03:50:16 +00:00
|
|
|
{format(new Date(modData.first_upload_at), "d MMM y")}
|
2022-01-25 05:31:25 +00:00
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<strong>Last Update:</strong>{" "}
|
2022-08-20 03:50:16 +00:00
|
|
|
{format(new Date(modData.last_update_at), "d MMM y")}
|
2022-01-25 05:31:25 +00:00
|
|
|
</div>
|
2022-09-03 06:51:57 +00:00
|
|
|
{(!counts.skyrim.counts || !counts.skyrimspecialedition.counts) &&
|
|
|
|
renderDownloadCountsLoading()}
|
|
|
|
{(!games || gamesError) && renderGamesError(gamesError)}
|
|
|
|
{counts.skyrim.error && renderDownloadCountsError(counts.skyrim.error)}
|
|
|
|
{counts.skyrimspecialedition.error &&
|
|
|
|
renderDownloadCountsError(counts.skyrimspecialedition.error)}
|
2022-01-25 05:31:25 +00:00
|
|
|
<div>
|
|
|
|
<strong>Total Downloads:</strong> {numberFmt.format(total_downloads)}
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<strong>Unique Downloads:</strong>{" "}
|
|
|
|
{numberFmt.format(unique_downloads)}
|
|
|
|
</div>
|
2022-08-20 03:50:16 +00:00
|
|
|
<div className={styles["select-container"]}>
|
|
|
|
<label htmlFor="mod-file-select" className={styles.label}>
|
|
|
|
Select file:
|
|
|
|
</label>
|
|
|
|
<select
|
|
|
|
name="file"
|
|
|
|
id="mod-file-select"
|
|
|
|
className={styles.select}
|
|
|
|
onChange={handleFileChange}
|
|
|
|
value={selectedFile ?? ""}
|
|
|
|
>
|
|
|
|
<option value="">--Select file--</option>
|
|
|
|
{[...modData.files].reverse().map((file) => (
|
|
|
|
<option key={file.nexus_file_id} value={file.nexus_file_id}>
|
|
|
|
{file.name} (v{file.version}) ({file.category})
|
|
|
|
</option>
|
|
|
|
))}
|
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
{fileData && (
|
|
|
|
<div className={styles["select-container"]}>
|
|
|
|
<label htmlFor="file-plugin-select" className={styles.label}>
|
|
|
|
Select plugin:
|
|
|
|
</label>
|
|
|
|
<select
|
|
|
|
name="plugin"
|
|
|
|
id="file-plugin-select"
|
|
|
|
className={styles.select}
|
|
|
|
onChange={handlePluginChange}
|
|
|
|
value={selectedPlugin ?? ""}
|
|
|
|
>
|
|
|
|
<option value="">--Select plugin--</option>
|
|
|
|
{fileData.plugins.map((plugin) => (
|
|
|
|
<option key={plugin.hash} value={plugin.hash}>
|
|
|
|
{plugin.file_path}
|
|
|
|
</option>
|
|
|
|
))}
|
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
)}
|
2022-08-20 04:12:10 +00:00
|
|
|
{pluginData ? (
|
2022-08-29 03:43:44 +00:00
|
|
|
<>
|
|
|
|
<div className={styles["plugin-actions"]}>
|
|
|
|
<Link href={`/?plugin=${pluginData.hash}`}>
|
|
|
|
<a className={styles["plugin-link"]}>View plugin</a>
|
|
|
|
</Link>
|
|
|
|
<button
|
|
|
|
className={styles.button}
|
|
|
|
onClick={() => {
|
|
|
|
if (fetchedPlugin) {
|
|
|
|
dispatch(removeFetchedPlugin(pluginData.hash));
|
|
|
|
} else {
|
|
|
|
dispatch(
|
2022-08-20 04:12:10 +00:00
|
|
|
updateFetchedPlugin({ ...pluginData, enabled: true })
|
2022-08-29 03:43:44 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
setShowAddRemovePluginNotification(true);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{Boolean(fetchedPlugin) ? "Remove plugin" : "Add plugin"}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
{showAddRemovePluginNotification && (
|
|
|
|
<span>
|
|
|
|
Plugin {Boolean(fetchedPlugin) ? "added" : "removed"}.{" "}
|
|
|
|
<Link href="/#added-plugins">
|
|
|
|
<a>View list</a>
|
|
|
|
</Link>
|
|
|
|
.
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</>
|
2022-08-20 04:12:10 +00:00
|
|
|
) : (
|
|
|
|
<div className={styles.spacer} />
|
|
|
|
)}
|
2022-08-20 03:50:16 +00:00
|
|
|
{fileError &&
|
|
|
|
(fileError.status === 404 ? (
|
|
|
|
<div>File cound not be found.</div>
|
|
|
|
) : (
|
|
|
|
<div>{`Error loading file data: ${fileError.message}`}</div>
|
|
|
|
))}
|
|
|
|
{pluginError &&
|
|
|
|
(pluginError.status === 404 ? (
|
|
|
|
<div>Plugin cound not be found.</div>
|
|
|
|
) : (
|
|
|
|
<div>{`Error loading plugin data: ${pluginError.message}`}</div>
|
|
|
|
))}
|
|
|
|
<CellList
|
|
|
|
cells={pluginData?.cells ?? fileData?.cells ?? modData.cells}
|
|
|
|
/>
|
2022-01-25 05:31:25 +00:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
export default ModData;
|