modmapper-web/components/ModData.tsx

347 lines
10 KiB
TypeScript
Raw Normal View History

import { format } from "date-fns";
import Head from "next/head";
2022-08-20 03:50:16 +00:00
import React, { useCallback, 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";
2022-08-20 04:12:10 +00:00
import {
PluginsByHashWithMods,
removeFetchedPlugin,
updateFetchedPlugin,
} from "../slices/plugins";
import Link from "next/link";
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/skyrimspecialedition";
type Props = {
selectedMod: number;
2022-08-20 03:50:16 +00:00
selectedFile: number;
selectedPlugin: string;
counts: Record<number, [number, number, number]> | null;
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> = ({
selectedMod,
2022-08-20 03:50:16 +00:00
selectedFile,
selectedPlugin,
counts,
setSelectedCells,
2022-08-20 03:50:16 +00:00
onSelectFile,
onSelectPlugin,
}) => {
const [showAddRemovePluginNotification, setShowAddRemovePluginNotification] =
useState<boolean>(false);
2022-08-20 03:50:16 +00:00
const { data: modData, error: modError } = useSWRImmutable(
`https://mods.modmapper.com/${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]);
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");
2022-08-20 03:50:16 +00:00
const modCounts = counts && counts[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"
2022-08-20 03:50:16 +00:00
content={`https://modmapper.com/?mod=${modData.nexus_mod_id}`}
/>
</Head>
<h1>
<a
2022-08-20 03:50:16 +00:00
href={`${NEXUS_MODS_URL}/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>Category:&nbsp;</strong>
<a
2022-08-20 03:50:16 +00:00
href={`${NEXUS_MODS_URL}/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
2022-08-20 03:50:16 +00:00
href={`${NEXUS_MODS_URL}/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>
<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;