Finish support for multiple games
* Adds game filter to ModList. * Prefixes search results with game * Adds `&game=X` to URL for mods * Loads json from new directories on static server
This commit is contained in:
parent
5ff11d568e
commit
8f254ef761
@ -7,9 +7,10 @@ import styles from "../styles/AddModData.module.css";
|
||||
import { jsonFetcher } from "../lib/api";
|
||||
import { DownloadCountsContext } from "./DownloadCountsProvider";
|
||||
import { GamesContext } from "./GamesProvider";
|
||||
import type { SelectedMod } from "./AddModDialog";
|
||||
|
||||
type Props = {
|
||||
selectedMod: number;
|
||||
selectedMod: SelectedMod;
|
||||
selectedPlugin: string | null;
|
||||
setSelectedPlugin: (plugin: string) => void;
|
||||
};
|
||||
@ -28,7 +29,9 @@ const AddModData: React.FC<Props> = ({
|
||||
const [selectedFile, setSelectedFile] = useState<number | null>(null);
|
||||
|
||||
const { data: modData, error: modError } = useSWRImmutable(
|
||||
selectedMod ? `https://mods.modmapper.com/${selectedMod}.json` : null,
|
||||
selectedMod
|
||||
? `https://mods.modmapper.com/${selectedMod.game}/${selectedMod.id}.json`
|
||||
: null,
|
||||
(_) => jsonFetcher<Mod>(_)
|
||||
);
|
||||
const { data: fileData, error: fileError } = useSWRImmutable(
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createPortal } from "react-dom";
|
||||
import React, { useCallback, useEffect, useState, useRef } from "react";
|
||||
import React, { useCallback, useState, useRef } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
|
||||
@ -10,8 +10,13 @@ import { updateFetchedPlugin, PluginsByHashWithMods } from "../slices/plugins";
|
||||
import styles from "../styles/AddModDialog.module.css";
|
||||
import EscapeListener from "./EscapeListener";
|
||||
|
||||
export interface SelectedMod {
|
||||
id: number;
|
||||
game: string;
|
||||
}
|
||||
|
||||
const AddModDialog: React.FC = () => {
|
||||
const [selectedMod, setSelectedMod] = useState<number | null>(null);
|
||||
const [selectedMod, setSelectedMod] = useState<SelectedMod | null>(null);
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<string | null>(null);
|
||||
const [dialogShown, setDialogShown] = useState(false);
|
||||
const searchInput = useRef<HTMLInputElement | null>(null);
|
||||
@ -45,7 +50,10 @@ const AddModDialog: React.FC = () => {
|
||||
placeholder="Search mods…"
|
||||
onSelectResult={(selectedItem) => {
|
||||
if (selectedItem) {
|
||||
setSelectedMod(selectedItem.id);
|
||||
setSelectedMod({
|
||||
id: selectedItem.id,
|
||||
game: selectedItem.game,
|
||||
});
|
||||
}
|
||||
}}
|
||||
inputRef={searchInput}
|
||||
|
@ -79,9 +79,10 @@ export interface Mod {
|
||||
files: ModFile[];
|
||||
}
|
||||
|
||||
export const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
|
||||
export const NEXUS_MODS_URL = "https://www.nexusmods.com";
|
||||
|
||||
type Props = {
|
||||
game: string;
|
||||
selectedMod: number;
|
||||
selectedFile: number;
|
||||
selectedPlugin: string;
|
||||
@ -91,6 +92,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const ModData: React.FC<Props> = ({
|
||||
game,
|
||||
selectedMod,
|
||||
selectedFile,
|
||||
selectedPlugin,
|
||||
@ -107,7 +109,7 @@ const ModData: React.FC<Props> = ({
|
||||
const [showAddRemovePluginNotification, setShowAddRemovePluginNotification] =
|
||||
useState<boolean>(false);
|
||||
const { data: modData, error: modError } = useSWRImmutable(
|
||||
`https://mods.modmapper.com/${selectedMod}.json`,
|
||||
`https://mods.modmapper.com/${game}/${selectedMod}.json`,
|
||||
(_) => jsonFetcher<Mod>(_)
|
||||
);
|
||||
|
||||
@ -220,12 +222,16 @@ const ModData: React.FC<Props> = ({
|
||||
<meta
|
||||
key="og:url"
|
||||
property="og:url"
|
||||
content={`https://modmapper.com/?mod=${modData.nexus_mod_id}`}
|
||||
content={`https://modmapper.com/?game=${getGameNameById(
|
||||
modData.game_id
|
||||
)}&mod=${modData.nexus_mod_id}`}
|
||||
/>
|
||||
</Head>
|
||||
<h1>
|
||||
<a
|
||||
href={`${NEXUS_MODS_URL}/mods/${modData.nexus_mod_id}`}
|
||||
href={`${NEXUS_MODS_URL}/${getGameNameById(modData.game_id)}/mods/${
|
||||
modData.nexus_mod_id
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className={styles.name}
|
||||
@ -236,7 +242,9 @@ const ModData: React.FC<Props> = ({
|
||||
<div>
|
||||
<strong>Category: </strong>
|
||||
<a
|
||||
href={`${NEXUS_MODS_URL}/mods/categories/${modData.category_id}`}
|
||||
href={`${NEXUS_MODS_URL}/${getGameNameById(
|
||||
modData.game_id
|
||||
)}/mods/categories/${modData.category_id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
@ -247,7 +255,9 @@ const ModData: React.FC<Props> = ({
|
||||
<div>
|
||||
<strong>Author: </strong>
|
||||
<a
|
||||
href={`${NEXUS_MODS_URL}/users/${modData.author_id}`}
|
||||
href={`${NEXUS_MODS_URL}/${getGameNameById(
|
||||
modData.game_id
|
||||
)}/users/${modData.author_id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
setSortBy,
|
||||
setSortAsc,
|
||||
setFilter,
|
||||
setGame,
|
||||
setCategory,
|
||||
setIncludeTranslations,
|
||||
} from "../slices/modListFilters";
|
||||
@ -23,7 +24,7 @@ import { useAppDispatch, useAppSelector } from "../lib/hooks";
|
||||
import { DownloadCountsContext } from "./DownloadCountsProvider";
|
||||
import { GamesContext } from "./GamesProvider";
|
||||
|
||||
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
|
||||
const NEXUS_MODS_URL = "https://www.nexusmods.com";
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
type Props = {
|
||||
@ -39,7 +40,7 @@ const ModList: React.FC<Props> = ({ mods, files }) => {
|
||||
} = useContext(GamesContext);
|
||||
const counts = useContext(DownloadCountsContext);
|
||||
const dispatch = useAppDispatch();
|
||||
const { sortBy, sortAsc, filter, category, includeTranslations } =
|
||||
const { sortBy, sortAsc, filter, category, game, includeTranslations } =
|
||||
useAppSelector((state) => state.modListFilters);
|
||||
const [filterResults, setFilterResults] = useState<Set<number>>(new Set());
|
||||
const [page, setPage] = useState<number>(0);
|
||||
@ -69,6 +70,7 @@ const ModList: React.FC<Props> = ({ mods, files }) => {
|
||||
(mod) =>
|
||||
(includeTranslations || !mod.is_translation) &&
|
||||
(!filter || filterResults.has(mod.id)) &&
|
||||
(game === "All" || getGameNameById(mod.game_id) === game) &&
|
||||
(category === "All" || mod.category_name === category)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
@ -226,6 +228,26 @@ const ModList: React.FC<Props> = ({ mods, files }) => {
|
||||
onChange={(event) => dispatch(setFilter(event.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["filter-row"]}>
|
||||
<label htmlFor="game">Game:</label>
|
||||
<select
|
||||
name="game"
|
||||
id="game"
|
||||
className={styles["game"]}
|
||||
value={game}
|
||||
onChange={(event) => dispatch(setGame(event.target.value))}
|
||||
>
|
||||
<option value="All">All</option>
|
||||
{games
|
||||
?.map((game) => game.name)
|
||||
.sort()
|
||||
.map((game) => (
|
||||
<option key={game} value={game}>
|
||||
{game}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className={styles["filter-row"]}>
|
||||
<label htmlFor="category">Category:</label>
|
||||
<select
|
||||
@ -283,14 +305,20 @@ const ModList: React.FC<Props> = ({ mods, files }) => {
|
||||
<li key={mod.id} className={styles["mod-list-item"]}>
|
||||
<div className={styles["mod-title"]}>
|
||||
<strong>
|
||||
<Link href={`/?mod=${mod.nexus_mod_id}`}>
|
||||
<Link
|
||||
href={`/?game=${getGameNameById(mod.game_id)}&mod=${
|
||||
mod.nexus_mod_id
|
||||
}`}
|
||||
>
|
||||
<a>{mod.name}</a>
|
||||
</Link>
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href={`${NEXUS_MODS_URL}/mods/${mod.nexus_mod_id}`}
|
||||
href={`${NEXUS_MODS_URL}/${getGameNameById(
|
||||
mod.game_id
|
||||
)}/mods/${mod.nexus_mod_id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
@ -300,7 +328,9 @@ const ModList: React.FC<Props> = ({ mods, files }) => {
|
||||
<div>
|
||||
<strong>Category: </strong>
|
||||
<a
|
||||
href={`${NEXUS_MODS_URL}/mods/categories/${mod.category_id}`}
|
||||
href={`${NEXUS_MODS_URL}/${getGameNameById(
|
||||
mod.game_id
|
||||
)}/mods/categories/${mod.category_id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
@ -310,7 +340,9 @@ const ModList: React.FC<Props> = ({ mods, files }) => {
|
||||
<div>
|
||||
<strong>Author: </strong>
|
||||
<a
|
||||
href={`${NEXUS_MODS_URL}/users/${mod.author_id}`}
|
||||
href={`${NEXUS_MODS_URL}/${getGameNameById(
|
||||
mod.game_id
|
||||
)}/users/${mod.author_id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
|
@ -16,9 +16,15 @@ type Props = {
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement | null>;
|
||||
};
|
||||
|
||||
interface Mod {
|
||||
name: string;
|
||||
id: number;
|
||||
function gamePrefex(game: GameName): string {
|
||||
switch (game) {
|
||||
case "skyrim":
|
||||
return "[LE]";
|
||||
case "skyrimspecialedition":
|
||||
return "[SSE]";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const SearchBar: React.FC<Props> = ({
|
||||
@ -36,11 +42,11 @@ const SearchBar: React.FC<Props> = ({
|
||||
const [searchFocused, setSearchFocused] = useState<boolean>(false);
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
|
||||
const renderSearchIndexError = (error: Error) => {
|
||||
const renderSearchIndexError = (error: Error) => (
|
||||
<div className={styles.error}>
|
||||
Error loading mod search index: {loadError}.
|
||||
</div>;
|
||||
};
|
||||
Error loading mod search index: {loadError.message}.
|
||||
</div>
|
||||
);
|
||||
const renderDownloadCountsLoading = () => (
|
||||
<div>Loading live download counts...</div>
|
||||
);
|
||||
@ -49,7 +55,6 @@ const SearchBar: React.FC<Props> = ({
|
||||
className={styles.error}
|
||||
>{`Error loading live download counts: ${error.message}`}</div>
|
||||
);
|
||||
console.log(loadError);
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@ -148,7 +153,7 @@ const SearchBar: React.FC<Props> = ({
|
||||
highlightedIndex === index ? styles["highlighted-result"] : ""
|
||||
}`}
|
||||
>
|
||||
{result.name}
|
||||
{gamePrefex(result.game)} {result.name}
|
||||
</li>
|
||||
))}
|
||||
{loadError && renderSearchIndexError(loadError)}
|
||||
|
@ -66,12 +66,12 @@ const SearchProvider: React.FC = ({ children }) => {
|
||||
useState(true);
|
||||
|
||||
const { data: skyrimData, error: skyrimError } = useSWRImmutable(
|
||||
`https://mods.modmapper.com/skyrim_mod_search_index.json`,
|
||||
`https://mods.modmapper.com/skyrim/mod_search_index.json`,
|
||||
(_) => jsonFetcher<Mod[]>(_, { notFoundOk: false })
|
||||
);
|
||||
const { data: skyrimspecialeditionData, error: skyrimspecialeditionError } =
|
||||
useSWRImmutable(
|
||||
`https://mods.modmapper.com/skyrimspecialedition_mod_search_index.json`,
|
||||
`https://mods.modmapper.com/skyrimspecialedition/mod_search_index.json`,
|
||||
(_) => jsonFetcher<Mod[]>(_, { notFoundOk: false })
|
||||
);
|
||||
|
||||
@ -101,10 +101,18 @@ const SearchProvider: React.FC = ({ children }) => {
|
||||
}, [skyrimspecialeditionData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!skyrimLoading && !skyrimspecialEditionLoading) {
|
||||
if (
|
||||
(!skyrimLoading || skyrimError) &&
|
||||
(!skyrimspecialEditionLoading || skyrimspecialeditionError)
|
||||
) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [skyrimLoading, skyrimspecialEditionLoading]);
|
||||
}, [
|
||||
skyrimLoading,
|
||||
skyrimError,
|
||||
skyrimspecialEditionLoading,
|
||||
skyrimspecialeditionError,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SearchContext.Provider
|
||||
|
@ -68,12 +68,20 @@ const Sidebar: React.FC<Props> = ({
|
||||
<h1 className={styles["cell-name-header"]}>
|
||||
Cell {selectedCell.x}, {selectedCell.y}
|
||||
</h1>
|
||||
<CellData selectedCell={selectedCell} />;
|
||||
<CellData selectedCell={selectedCell} />
|
||||
{renderLastModified(lastModified)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (router.query.mod) {
|
||||
if (!router.query.game) {
|
||||
router.replace(`/?game=skyrimspecialedition&mod=${router.query.mod}`);
|
||||
return null;
|
||||
}
|
||||
const game =
|
||||
typeof router.query.game === "string"
|
||||
? router.query.game
|
||||
: router.query.game[0];
|
||||
const modId = parseInt(router.query.mod as string, 10);
|
||||
const fileId = parseInt(router.query.file as string, 10);
|
||||
const pluginHash = router.query.plugin as string;
|
||||
@ -91,6 +99,7 @@ const Sidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
{!Number.isNaN(modId) && (
|
||||
<ModData
|
||||
game={game}
|
||||
selectedMod={modId}
|
||||
selectedFile={fileId}
|
||||
selectedPlugin={pluginHash}
|
||||
@ -123,7 +132,7 @@ const Sidebar: React.FC<Props> = ({
|
||||
: router.query.plugin[0]
|
||||
}
|
||||
/>
|
||||
;{renderLastModified(lastModified)}
|
||||
{renderLastModified(lastModified)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,8 @@
|
||||
/** @type {import('next-sitemap').IConfig} */
|
||||
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
|
||||
|
||||
const MOD_SEARCH_INDEX_URL = 'https://mods.modmapper.com/mod_search_index.json';
|
||||
const SSE_MOD_SEARCH_INDEX_URL = 'https://mods.modmapper.com/skyrimspecialedition/mod_search_index.json';
|
||||
const LE_MOD_SEARCH_INDEX_URL = 'https://mods.modmapper.com/skyrim/mod_search_index.json';
|
||||
|
||||
module.exports = {
|
||||
siteUrl: process.env.SITE_URL || 'https://modmapper.com',
|
||||
@ -9,12 +10,21 @@ module.exports = {
|
||||
additionalPaths: async (config) => {
|
||||
const result = []
|
||||
|
||||
const response = await fetch(MOD_SEARCH_INDEX_URL);
|
||||
const index = await response.json();
|
||||
const skyrimResponse = await fetch(LE_MOD_SEARCH_INDEX_URL);
|
||||
const skyrimIndex = await skyrimResponse.json();
|
||||
|
||||
for (const mod of index) {
|
||||
const skyrimspecialeditionResponse = await fetch(SSE_MOD_SEARCH_INDEX_URL);
|
||||
const skyrimspecialeditionIndex = await skyrimspecialeditionResponse.json();
|
||||
|
||||
for (const mod of skyrimIndex) {
|
||||
result.push({
|
||||
loc: '/?mod=' + mod.id,
|
||||
loc: `/?game=skyrim&mod=${mod.id}`,
|
||||
changefreq: 'daily',
|
||||
});
|
||||
}
|
||||
for (const mod of skyrimspecialeditionIndex) {
|
||||
result.push({
|
||||
loc: `/?game=skyrimspecialedition&mod=${mod.id}`,
|
||||
changefreq: 'daily',
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
|
||||
import type { AppState, AppThunk } from "../lib/store"
|
||||
import type { Mod } from '../components/CellData';
|
||||
|
||||
export type ModWithCounts = Mod & {
|
||||
@ -14,6 +13,7 @@ export type ModListFiltersState = {
|
||||
sortBy: keyof ModWithCounts,
|
||||
sortAsc: boolean,
|
||||
filter?: string,
|
||||
game: string,
|
||||
category: string,
|
||||
includeTranslations: boolean,
|
||||
}
|
||||
@ -22,6 +22,7 @@ const initialState: ModListFiltersState = {
|
||||
sortBy: "unique_downloads",
|
||||
sortAsc: false,
|
||||
filter: undefined,
|
||||
game: "All",
|
||||
category: "All",
|
||||
includeTranslations: true,
|
||||
};
|
||||
@ -42,6 +43,10 @@ export const modListFiltersSlice = createSlice({
|
||||
...state,
|
||||
filter: action.payload,
|
||||
}),
|
||||
setGame: (state, action: PayloadAction<string>) => ({
|
||||
...state,
|
||||
game: action.payload,
|
||||
}),
|
||||
setCategory: (state, action: PayloadAction<string>) => ({
|
||||
...state,
|
||||
category: action.payload,
|
||||
@ -54,6 +59,6 @@ export const modListFiltersSlice = createSlice({
|
||||
},
|
||||
})
|
||||
|
||||
export const { setSortBy, setSortAsc, setFilter, setCategory, setIncludeTranslations, clearFilters } = modListFiltersSlice.actions
|
||||
export const { setSortBy, setSortAsc, setFilter, setGame, setCategory, setIncludeTranslations, clearFilters } = modListFiltersSlice.actions
|
||||
|
||||
export default modListFiltersSlice.reducer
|
@ -70,6 +70,11 @@
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.game {
|
||||
min-width: 175px;
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.filter {
|
||||
width: 175px;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user