modmapper-web/components/ModData.tsx

394 lines
12 KiB
TypeScript
Raw Normal View History

import { format } from "date-fns";
import Head from "next/head";
import React, { useCallback, useContext, useEffect, useState } from "react";
import useSWRImmutable from "swr/immutable";
2022-08-20 04:12:10 +00:00
import { useAppDispatch, useAppSelector } from "../lib/hooks";
import CellList from "./CellList";
import styles from "../styles/ModData.module.css";
import { jsonFetcher } from "../lib/api";
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";
import { DownloadCountsContext } from "./DownloadCountsProvider";
import { GamesContext } from "./GamesProvider";
export interface CellCoord {
x: number;
y: number;
}
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;
}
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;
is_translation: boolean;
updated_at: string;
created_at: string;
last_update_at: string;
first_upload_at: string;
last_updated_files_at: string;
cells: CellCoord[];
files: ModFile[];
}
export const NEXUS_MODS_URL = "https://www.nexusmods.com";
type Props = {
game: string;
selectedMod: number;
2022-08-20 03:50:16 +00:00
selectedFile: number;
selectedPlugin: string;
setSelectedCells: (cells: { x: number; y: number }[] | null) => void;
2022-08-20 03:50:16 +00:00
onSelectFile: (fileId: number) => void;
onSelectPlugin: (hash: string) => void;
};
const ModData: React.FC<Props> = ({
game,
selectedMod,
2022-08-20 03:50:16 +00:00
selectedFile,
selectedPlugin,
setSelectedCells,
2022-08-20 03:50:16 +00:00
onSelectFile,
onSelectPlugin,
}) => {
const {
games,
getGameNameById,
error: gamesError,
} = useContext(GamesContext);
const counts = useContext(DownloadCountsContext);
const [showAddRemovePluginNotification, setShowAddRemovePluginNotification] =
useState<boolean>(false);
2022-08-20 03:50:16 +00:00
const { data: modData, error: modError } = useSWRImmutable(
`https://mods.modmapper.com/${game}/${selectedMod}.json`,
(_) => jsonFetcher<Mod>(_)
);
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
useEffect(() => {
if (pluginData) setSelectedCells(pluginData.cells);
}, [pluginData, setSelectedCells]);
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) {
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-08-20 03:50:16 +00:00
if (modData === undefined)
return <div className={styles.status}>Loading...</div>;
2022-08-20 03:50:16 +00:00
if (modData === null)
return <div className={styles.status}>Mod could not be found.</div>;
let numberFmt = new Intl.NumberFormat("en-US");
const gameName = getGameNameById(modData.game_id);
const gameDownloadCounts = gameName && counts[gameName].counts;
const modCounts =
gameDownloadCounts && gameDownloadCounts[modData.nexus_mod_id];
const total_downloads = modCounts ? modCounts[0] : 0;
const unique_downloads = modCounts ? modCounts[1] : 0;
const views = modCounts ? modCounts[2] : 0;
2022-08-20 03:50:16 +00:00
if (selectedMod && modData) {
return (
<>
<Head>
2022-08-20 03:50:16 +00:00
<title key="title">{`Modmapper - ${modData.name}`}</title>
<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}`}
/>
<meta
key="og:title"
property="og:title"
2022-08-20 03:50:16 +00:00
content={`Modmapper - ${modData.name}`}
/>
<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}`}
/>
<meta
key="twitter:title"
name="twitter:title"
2022-08-20 03:50:16 +00:00
content={`Modmapper - ${modData.name}`}
/>
<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}`}
/>
<meta
key="og:url"
property="og:url"
content={`https://modmapper.com/?game=${getGameNameById(
modData.game_id
)}&mod=${modData.nexus_mod_id}`}
/>
</Head>
<h1>
<a
href={`${NEXUS_MODS_URL}/${getGameNameById(modData.game_id)}/mods/${
modData.nexus_mod_id
}`}
target="_blank"
rel="noreferrer noopener"
2022-02-27 06:17:52 +00:00
className={styles.name}
>
2022-08-20 03:50:16 +00:00
{modData.name}
</a>
</h1>
<div>
<strong>Edition:&nbsp;</strong>
{
editionNames[
getGameNameById(modData.game_id) ?? "skyrimspecialedition"
]
}
</div>
<div>
<strong>Category:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/${getGameNameById(
modData.game_id
)}/mods/categories/${modData.category_id}`}
target="_blank"
rel="noreferrer noopener"
>
2022-08-20 03:50:16 +00:00
{modData.category_name}
</a>
2022-08-20 03:50:16 +00:00
{modData.is_translation && <strong>&nbsp;(translation)</strong>}
</div>
<div>
<strong>Author:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/${getGameNameById(
modData.game_id
)}/users/${modData.author_id}`}
target="_blank"
rel="noreferrer noopener"
>
2022-08-20 03:50:16 +00:00
{modData.author_name}
</a>
</div>
<div>
<strong>Uploaded:</strong>{" "}
2022-08-20 03:50:16 +00:00
{format(new Date(modData.first_upload_at), "d MMM y")}
</div>
<div>
<strong>Last Update:</strong>{" "}
2022-08-20 03:50:16 +00:00
{format(new Date(modData.last_update_at), "d MMM y")}
</div>
{(!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)}
<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 ? (
<>
<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 })
);
}
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}
/>
</>
);
}
return null;
};
export default ModData;