Add pagination to cell and mod lists

Speeds up showing and hiding of the sidebar.
This commit is contained in:
Tyler Hallada 2022-08-28 02:27:53 -04:00
parent 47e95f4d59
commit 5491894e00
7 changed files with 242 additions and 112 deletions

View File

@ -1,10 +1,13 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import MiniSearch from "minisearch"; import MiniSearch from "minisearch";
import ReactPaginate from "react-paginate";
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 PAGE_SIZE = 100;
type Props = { type Props = {
cells: CellCoord[]; cells: CellCoord[];
}; };
@ -32,6 +35,7 @@ const CellList: React.FC<Props> = ({ cells }) => {
const [filter, setFilter] = useState<string>(""); const [filter, setFilter] = useState<string>("");
const [filterResults, setFilterResults] = useState<Set<string>>(new Set()); const [filterResults, setFilterResults] = useState<Set<string>>(new Set());
const [page, setPage] = useState<number>(0);
const filteredCells = cells const filteredCells = cells
.filter((cell) => !filter || filterResults.has(`${cell.x},${cell.y}`)) .filter((cell) => !filter || filterResults.has(`${cell.x},${cell.y}`))
@ -45,6 +49,11 @@ const CellList: React.FC<Props> = ({ cells }) => {
} }
}, [filter]); }, [filter]);
useEffect(() => {
setPage(0);
document.getElementById("sidebar")?.scrollTo(0, 0);
}, [filterResults]);
return ( return (
filteredCells && ( filteredCells && (
<> <>
@ -64,25 +73,46 @@ const CellList: React.FC<Props> = ({ cells }) => {
<hr /> <hr />
</div> </div>
<ul className={styles["cell-list"]}> <ul className={styles["cell-list"]}>
{filteredCells.map((cell) => ( {filteredCells
<li .slice(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE)
key={`cell-${cell.x},${cell.y}`} .map((cell) => (
className={styles["cell-list-item"]} <li
> key={`cell-${cell.x},${cell.y}`}
<div className={styles["cell-title"]}> className={styles["cell-list-item"]}
<strong> >
<Link <div className={styles["cell-title"]}>
href={`/?cell=${encodeURIComponent(`${cell.x},${cell.y}`)}`} <strong>
> <Link
<a> href={`/?cell=${encodeURIComponent(
{cell.x}, {cell.y} `${cell.x},${cell.y}`
</a> )}`}
</Link> >
</strong> <a>
</div> {cell.x}, {cell.y}
</li> </a>
))} </Link>
</strong>
</div>
</li>
))}
</ul> </ul>
<ReactPaginate
breakLabel="..."
nextLabel=">"
forcePage={page}
onPageChange={(event) => {
setPage(event.selected);
document.getElementById("sidebar")?.scrollTo(0, 0);
}}
pageRangeDisplayed={3}
marginPagesDisplayed={2}
pageCount={Math.ceil(filteredCells.length / PAGE_SIZE)}
previousLabel="<"
renderOnZeroPageCount={() => null}
className={styles.pagination}
activeClassName={styles["active-page"]}
hrefBuilder={() => "#"}
/>
</> </>
) )
); );

View File

@ -4,6 +4,7 @@ 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 useSWRImmutable from "swr/immutable";
import ReactPaginate from "react-paginate";
import styles from "../styles/ModList.module.css"; import styles from "../styles/ModList.module.css";
import type { Mod } from "./CellData"; import type { Mod } from "./CellData";
@ -21,6 +22,7 @@ import {
import { useAppDispatch, useAppSelector } from "../lib/hooks"; import { useAppDispatch, useAppSelector } from "../lib/hooks";
const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition"; const NEXUS_MODS_URL = "https://www.nexusmods.com/skyrimspecialedition";
const PAGE_SIZE = 50;
type Props = { type Props = {
mods: Mod[]; mods: Mod[];
@ -33,6 +35,7 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
const { sortBy, sortAsc, filter, category, includeTranslations } = const { sortBy, sortAsc, filter, category, includeTranslations } =
useAppSelector((state) => state.modListFilters); useAppSelector((state) => state.modListFilters);
const [filterResults, setFilterResults] = useState<Set<number>>(new Set()); const [filterResults, setFilterResults] = useState<Set<number>>(new Set());
const [page, setPage] = useState<number>(0);
const { data: cellCounts, error: cellCountsError } = useSWRImmutable( const { data: cellCounts, error: cellCountsError } = useSWRImmutable(
`https://mods.modmapper.com/mod_cell_counts.json`, `https://mods.modmapper.com/mod_cell_counts.json`,
@ -106,6 +109,11 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
} }
}, [filter]); }, [filter]);
useEffect(() => {
setPage(0);
document.getElementById("sidebar")?.scrollTo(0, 0);
}, [filterResults, category, includeTranslations, sortBy, sortAsc]);
return ( return (
mods && ( mods && (
<> <>
@ -219,106 +227,125 @@ const ModList: React.FC<Props> = ({ mods, files, counts }) => {
<hr /> <hr />
</div> </div>
<ul className={styles["mod-list"]}> <ul className={styles["mod-list"]}>
{modsWithCounts.map((mod) => ( {modsWithCounts
<li key={mod.id} className={styles["mod-list-item"]}> .slice(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE)
<div className={styles["mod-title"]}> .map((mod) => (
<strong> <li key={mod.id} className={styles["mod-list-item"]}>
<Link href={`/?mod=${mod.nexus_mod_id}`}> <div className={styles["mod-title"]}>
<a>{mod.name}</a> <strong>
</Link> <Link href={`/?mod=${mod.nexus_mod_id}`}>
</strong> <a>{mod.name}</a>
</div> </Link>
<div> </strong>
<a
href={`${NEXUS_MODS_URL}/mods/${mod.nexus_mod_id}`}
target="_blank"
rel="noreferrer noopener"
>
View on Nexus Mods
</a>
</div>
<div>
<strong>Category:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/mods/categories/${mod.category_id}`}
target="_blank"
rel="noreferrer noopener"
>
{mod.category_name}
</a>
</div>
<div>
<strong>Author:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/users/${mod.author_id}`}
target="_blank"
rel="noreferrer noopener"
>
{mod.author_name}
</a>
</div>
<div>
<strong>Uploaded:</strong>{" "}
{format(new Date(mod.first_upload_at), "d MMM y")}
</div>
<div>
<strong>Last Update:</strong>{" "}
{format(new Date(mod.last_update_at), "d MMM y")}
</div>
<div>
<strong>Total Downloads:</strong>{" "}
{numberFmt.format(mod.total_downloads)}
</div>
<div>
<strong>Unique Downloads:</strong>{" "}
{numberFmt.format(mod.unique_downloads)}
</div>
{cellCounts && (
<div>
<strong>Exterior Cells Edited:</strong>{" "}
{numberFmt.format(mod.exterior_cells_edited)}
</div> </div>
)} <div>
<ul className={styles["file-list"]}> <a
{files && href={`${NEXUS_MODS_URL}/mods/${mod.nexus_mod_id}`}
files target="_blank"
.filter((file) => file.mod_id === mod.id) rel="noreferrer noopener"
.sort((a, b) => b.nexus_file_id - a.nexus_file_id) >
.map((file) => ( View on Nexus Mods
<li key={file.id}> </a>
<div> </div>
<strong>File:</strong> {file.name} <div>
</div> <strong>Category:&nbsp;</strong>
{file.mod_version && ( <a
href={`${NEXUS_MODS_URL}/mods/categories/${mod.category_id}`}
target="_blank"
rel="noreferrer noopener"
>
{mod.category_name}
</a>
</div>
<div>
<strong>Author:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/users/${mod.author_id}`}
target="_blank"
rel="noreferrer noopener"
>
{mod.author_name}
</a>
</div>
<div>
<strong>Uploaded:</strong>{" "}
{format(new Date(mod.first_upload_at), "d MMM y")}
</div>
<div>
<strong>Last Update:</strong>{" "}
{format(new Date(mod.last_update_at), "d MMM y")}
</div>
<div>
<strong>Total Downloads:</strong>{" "}
{numberFmt.format(mod.total_downloads)}
</div>
<div>
<strong>Unique Downloads:</strong>{" "}
{numberFmt.format(mod.unique_downloads)}
</div>
{cellCounts && (
<div>
<strong>Exterior Cells Edited:</strong>{" "}
{numberFmt.format(mod.exterior_cells_edited)}
</div>
)}
<ul className={styles["file-list"]}>
{files &&
files
.filter((file) => file.mod_id === mod.id)
.sort((a, b) => b.nexus_file_id - a.nexus_file_id)
.map((file) => (
<li key={file.id}>
<div> <div>
<strong>Version:</strong> {file.mod_version} <strong>File:</strong> {file.name}
</div> </div>
)} {file.mod_version && (
{file.version && file.mod_version !== file.version && ( <div>
<strong>Version:</strong> {file.mod_version}
</div>
)}
{file.version && file.mod_version !== file.version && (
<div>
<strong>File Version:</strong> {file.version}
</div>
)}
{file.category && (
<div>
<strong>Category:</strong> {file.category}
</div>
)}
<div> <div>
<strong>File Version:</strong> {file.version} <strong>Size:</strong> {formatBytes(file.size)}
</div> </div>
)} {file.uploaded_at && (
{file.category && ( <div>
<div> <strong>Uploaded:</strong>{" "}
<strong>Category:</strong> {file.category} {format(new Date(file.uploaded_at), "d MMM y")}
</div> </div>
)} )}
<div> </li>
<strong>Size:</strong> {formatBytes(file.size)} ))}
</div> </ul>
{file.uploaded_at && ( </li>
<div> ))}
<strong>Uploaded:</strong>{" "}
{format(new Date(file.uploaded_at), "d MMM y")}
</div>
)}
</li>
))}
</ul>
</li>
))}
</ul> </ul>
<ReactPaginate
breakLabel="..."
nextLabel=">"
forcePage={page}
onPageChange={(event) => {
setPage(event.selected);
document.getElementById("sidebar")?.scrollTo(0, 0);
}}
pageRangeDisplayed={3}
marginPagesDisplayed={2}
pageCount={Math.ceil(modsWithCounts.length / PAGE_SIZE)}
previousLabel="<"
renderOnZeroPageCount={() => null}
className={styles.pagination}
activeClassName={styles["active-page"]}
hrefBuilder={() => "#"}
/>
</> </>
) )
); );

View File

@ -100,6 +100,7 @@ const Sidebar: React.FC<Props> = ({
<div <div
className={styles.sidebar} className={styles.sidebar}
style={!open ? { display: "none" } : {}} style={!open ? { display: "none" } : {}}
id="sidebar"
> >
<div className={styles["sidebar-content"]}> <div className={styles["sidebar-content"]}>
<div className={styles["sidebar-header"]}> <div className={styles["sidebar-header"]}>
@ -123,6 +124,7 @@ const Sidebar: React.FC<Props> = ({
<div <div
className={styles.sidebar} className={styles.sidebar}
style={!open ? { display: "none" } : {}} style={!open ? { display: "none" } : {}}
id="sidebar"
> >
<div className={styles["sidebar-content"]}> <div className={styles["sidebar-content"]}>
<div className={styles["sidebar-header"]}> <div className={styles["sidebar-header"]}>
@ -140,6 +142,7 @@ const Sidebar: React.FC<Props> = ({
<div <div
className={styles.sidebar} className={styles.sidebar}
style={!open ? { display: "none" } : {}} style={!open ? { display: "none" } : {}}
id="sidebar"
> >
<div className={styles["sidebar-content"]}> <div className={styles["sidebar-content"]}>
<div className={styles["sidebar-header"]}> <div className={styles["sidebar-header"]}>
@ -161,6 +164,7 @@ const Sidebar: React.FC<Props> = ({
<div <div
className={styles.sidebar} className={styles.sidebar}
style={!open ? { display: "none" } : {}} style={!open ? { display: "none" } : {}}
id="sidebar"
> >
<div className={styles["sidebar-content"]}> <div className={styles["sidebar-content"]}>
<h1 className={styles.title}>Modmapper</h1> <h1 className={styles.title}>Modmapper</h1>

20
package-lock.json generated
View File

@ -18,6 +18,7 @@
"next": "12.1.1-canary.15", "next": "12.1.1-canary.15",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-paginate": "^8.1.3",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"skyrim-cell-dump-wasm": "0.1.0", "skyrim-cell-dump-wasm": "0.1.0",
"swr": "^1.1.2" "swr": "^1.1.2"
@ -2949,6 +2950,17 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}, },
"node_modules/react-paginate": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-8.1.3.tgz",
"integrity": "sha512-zBp80DBRcaeBnAeHUfbGKD0XHfbGNUolQ+S60Ymfs8o7rusYaJYZMAt1j93ADDNLlzRmJ0tMF/NeTlcdKf7dlQ==",
"dependencies": {
"prop-types": "^15.6.1"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18"
}
},
"node_modules/react-redux": { "node_modules/react-redux": {
"version": "7.2.6", "version": "7.2.6",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
@ -5619,6 +5631,14 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}, },
"react-paginate": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-8.1.3.tgz",
"integrity": "sha512-zBp80DBRcaeBnAeHUfbGKD0XHfbGNUolQ+S60Ymfs8o7rusYaJYZMAt1j93ADDNLlzRmJ0tMF/NeTlcdKf7dlQ==",
"requires": {
"prop-types": "^15.6.1"
}
},
"react-redux": { "react-redux": {
"version": "7.2.6", "version": "7.2.6",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",

View File

@ -21,6 +21,7 @@
"next": "12.1.1-canary.15", "next": "12.1.1-canary.15",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-paginate": "^8.1.3",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"skyrim-cell-dump-wasm": "0.1.0", "skyrim-cell-dump-wasm": "0.1.0",
"swr": "^1.1.2" "swr": "^1.1.2"

View File

@ -48,3 +48,27 @@
.filter { .filter {
width: 175px; width: 175px;
} }
.pagination {
display: flex;
flex-direction: row;
list-style-type: none;
padding: 0;
margin-top: 0;
width: 100%;
justify-content: center;
}
.pagination li {
padding: 4px;
}
.pagination li a {
cursor: pointer;
user-select: none;
}
.active-page a {
color: black;
text-decoration: none;
}

View File

@ -107,3 +107,27 @@
.desc { .desc {
transform: rotate(90deg); transform: rotate(90deg);
} }
.pagination {
display: flex;
flex-direction: row;
list-style-type: none;
padding: 0;
margin-top: 0;
width: 100%;
justify-content: center;
}
.pagination li {
padding: 4px;
}
.pagination li a {
cursor: pointer;
user-select: none;
}
.active-page a {
color: black;
text-decoration: none;
}