diff --git a/api/src/controllers/torrent.js b/api/src/controllers/torrent.js index b5e0518..f3026c2 100644 --- a/api/src/controllers/torrent.js +++ b/api/src/controllers/torrent.js @@ -452,10 +452,19 @@ export const getTorrentsPage = async ({ tag, uploadedBy, userId, + sort, tracker, }) => { const queryNGrams = nGrams(query, false, 2, false).join(" "); + const [sortField, sortDirString] = sort?.split(":") ?? []; + const sortDir = sortDirString === "asc" ? 1 : -1; + + const combinedSort = {}; + if (sortField) combinedSort[sortField] = sortDir; + if (query) combinedSort.confidenceScore = { $meta: "textScore" }; + combinedSort.created = -1; + const torrents = await Torrent.aggregate([ ...(query ? [ @@ -529,17 +538,6 @@ export const getTorrentsPage = async ({ }, ] : []), - { - $sort: query - ? { confidenceScore: { $meta: "textScore" } } - : { created: -1 }, - }, - { - $skip: skip, - }, - { - $limit: limit, - }, { $lookup: { from: "comments", @@ -588,6 +586,15 @@ export const getTorrentsPage = async ({ }, }, { $unwind: { path: "$fetchedBy", preserveNullAndEmptyArrays: true } }, + { + $sort: combinedSort, + }, + { + $skip: skip, + }, + { + $limit: limit, + }, ]); const [count] = await Torrent.aggregate([ @@ -682,7 +689,7 @@ export const listAll = async (req, res, next) => { }; export const searchTorrents = (tracker) => async (req, res, next) => { - const { query, category, source, tag, page } = req.query; + const { query, category, source, tag, page, sort } = req.query; try { const torrents = await getTorrentsPage({ skip: page ? parseInt(page) : 0, @@ -691,6 +698,7 @@ export const searchTorrents = (tracker) => async (req, res, next) => { source, tag: tag ? decodeURIComponent(tag) : undefined, userId: req.userId, + sort: sort ? decodeURIComponent(sort) : undefined, tracker, }); res.json(torrents); diff --git a/client/components/List.js b/client/components/List.js index 9d9620f..ec9c607 100644 --- a/client/components/List.js +++ b/client/components/List.js @@ -1,6 +1,10 @@ -import React from "react"; +import React, { useState } from "react"; import Link from "next/link"; +import { useRouter } from "next/router"; import { toPath } from "lodash"; +import qs from "qs"; +import { CaretUp } from "@styled-icons/boxicons-regular/CaretUp"; +import { CaretDown } from "@styled-icons/boxicons-regular/CaretDown"; import Box from "../components/Box"; import Text from "../components/Text"; @@ -47,7 +51,17 @@ const ListItem = ({ children }) => { ); }; +const getSortIcon = (accessor, sort = "") => { + const [sortAccessor, sortDirection] = sort.split(":"); + if (accessor !== sortAccessor) return null; + if (sortDirection === "asc") return CaretUp; + if (sortDirection === "desc") return CaretDown; + return null; +}; + const List = ({ data = [], columns = [], ...rest }) => { + const router = useRouter(); + const { sort } = router.query; return ( @@ -65,7 +79,42 @@ const List = ({ data = [], columns = [], ...rest }) => { fontWeight={600} fontSize={1} textAlign={col.rightAlign ? "right" : "left"} - _css={{ textTransform: "uppercase" }} + _css={{ + textTransform: "uppercase", + cursor: col.sortable ? "pointer" : "text", + userSelect: col.sortable ? "none" : "auto", + }} + onClick={ + col.sortable + ? () => { + const query = window.location.search; + const parsed = qs.parse(query.replace("?", "")); + if (parsed.sort) { + const [accessor, direction] = parsed.sort.split(":"); + if (accessor === col.accessor) { + if (direction === "asc") + parsed.sort = `${col.accessor}:desc`; + else if (direction === "desc") delete parsed.sort; + } else { + parsed.sort = `${col.accessor}:asc`; + } + } else { + parsed.sort = `${col.accessor}:asc`; + } + router.replace( + Object.keys(parsed).length + ? `${window.location.pathname}?${qs.stringify( + parsed + )}` + : window.location.pathname + ); + } + : undefined + } + icon={getSortIcon(col.accessor, sort)} + iconTextWrapperProps={{ + justifyContent: col.rightAlign ? "flex-end" : "flex-start", + }} > {col.header} diff --git a/client/components/TorrentList.js b/client/components/TorrentList.js index dd649df..053c183 100644 --- a/client/components/TorrentList.js +++ b/client/components/TorrentList.js @@ -1,8 +1,9 @@ -import React from "react"; +import React, { useEffect } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import moment from "moment"; import slugify from "slugify"; +import qs from "qs"; import { ListUl } from "@styled-icons/boxicons-regular/ListUl"; import { Upload } from "@styled-icons/boxicons-regular/Upload"; import { Download } from "@styled-icons/boxicons-regular/Download"; @@ -18,28 +19,62 @@ import Text from "./Text"; import Box from "./Box"; import Button from "./Button"; -const TorrentList = ({ torrents = [], categories, total }) => { +const pageSize = 25; + +const TorrentList = ({ + torrents = [], + setTorrents, + categories, + total, + fetchPath, + token, +}) => { const { publicRuntimeConfig: { SQ_SITE_WIDE_FREELEECH }, } = getConfig(); const router = useRouter(); const { - asPath, - query: { page: pageParam }, + query: { page: pageParam, sort }, } = router; const page = pageParam ? parseInt(pageParam) - 1 : 0; - const maxPage = Math.floor(total / 25); + const maxPage = total > pageSize ? Math.floor(total / pageSize) : 0; const canPrevPage = page > 0; const canNextPage = page < maxPage; const setPage = (number) => { - if (number === 0) router.push(asPath.split("?")[0]); - else router.push(`${asPath.split("?")[0]}?page=${number + 1}`); + const query = qs.parse(window.location.search.replace("?", "")); + if (number === 0) delete query.page; + else query.page = number + 1; + router.push( + Object.keys(query).length + ? `${window.location.pathname}?${qs.stringify(query)}` + : window.location.pathname + ); }; + useEffect(() => { + const fetchTorrents = async () => { + try { + const searchRes = await fetch( + `${fetchPath}?${qs.stringify(router.query)}`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + const results = await searchRes.json(); + setTorrents(results.torrents); + } catch (e) {} + }; + if (fetchPath && token) fetchTorrents(); + }, [sort, page]); + return ( <> { ), gridWidth: "100px", rightAlign: true, + sortable: !!token, }, { header: "Leechers", @@ -109,6 +145,7 @@ const TorrentList = ({ torrents = [], categories, total }) => { ), gridWidth: "100px", rightAlign: true, + sortable: !!token, }, { header: "Downloads", @@ -121,8 +158,9 @@ const TorrentList = ({ torrents = [], categories, total }) => { {value || 0} ), - gridWidth: "100px", + gridWidth: "115px", rightAlign: true, + sortable: !!token, }, { header: "Comments", @@ -135,8 +173,9 @@ const TorrentList = ({ torrents = [], categories, total }) => { {value || 0} ), - gridWidth: "100px", + gridWidth: "110px", rightAlign: true, + sortable: !!token, }, { header: "Uploaded", @@ -146,6 +185,7 @@ const TorrentList = ({ torrents = [], categories, total }) => { ), gridWidth: "140px", rightAlign: true, + sortable: !!token, }, ]} /> diff --git a/client/pages/categories/[category].js b/client/pages/categories/[category].js index 139972c..6bfad32 100644 --- a/client/pages/categories/[category].js +++ b/client/pages/categories/[category].js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; import getConfig from "next/config"; import qs from "qs"; @@ -8,14 +8,16 @@ import SEO from "../../components/SEO"; import Text from "../../components/Text"; import TorrentList from "../../components/TorrentList"; -const Category = ({ results }) => { +const Category = ({ results, token }) => { + const [torrents, setTorrents] = useState(results?.torrents ?? []); + const router = useRouter(); const { query: { category: categorySlug }, } = router; const { - publicRuntimeConfig: { SQ_TORRENT_CATEGORIES }, + publicRuntimeConfig: { SQ_TORRENT_CATEGORIES, SQ_API_URL }, } = getConfig(); const category = Object.keys(SQ_TORRENT_CATEGORIES).find( @@ -28,11 +30,14 @@ const Category = ({ results }) => { Browse {category} - {results?.torrents.length ? ( + {torrents.length ? ( ) : ( No results. @@ -74,7 +79,7 @@ export const getServerSideProps = withAuthServerSideProps( throw "banned"; } const results = await searchRes.json(); - return { props: { results } }; + return { props: { results, token } }; } catch (e) { if (e === "banned") throw "banned"; return { props: {} }; diff --git a/client/pages/search/[[...query]].js b/client/pages/search/[[...query]].js index 3f5f089..271ad51 100644 --- a/client/pages/search/[[...query]].js +++ b/client/pages/search/[[...query]].js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import getConfig from "next/config"; import { useRouter } from "next/router"; import qs from "qs"; @@ -10,7 +10,9 @@ import Button from "../../components/Button"; import Box from "../../components/Box"; import TorrentList from "../../components/TorrentList"; -const Search = ({ results, error }) => { +const Search = ({ results, error, token }) => { + const [torrents, setTorrents] = useState(results?.torrents ?? []); + const router = useRouter(); let { query: { query }, @@ -18,7 +20,7 @@ const Search = ({ results, error }) => { query = query ? decodeURIComponent(query) : ""; const { - publicRuntimeConfig: { SQ_TORRENT_CATEGORIES }, + publicRuntimeConfig: { SQ_TORRENT_CATEGORIES, SQ_API_URL }, } = getConfig(); const handleSearch = (e) => { @@ -44,11 +46,14 @@ const Search = ({ results, error }) => { <> {query && ( <> - {results.torrents.length ? ( + {torrents.length ? ( ) : ( No results. @@ -92,7 +97,7 @@ export const getServerSideProps = withAuthServerSideProps( return { props: { error: message } }; } else { const results = await searchRes.json(); - return { props: { results } }; + return { props: { results, token } }; } } catch (e) { if (e === "banned") throw "banned"; diff --git a/client/pages/tags/[tag].js b/client/pages/tags/[tag].js index 7154d5c..15554b8 100644 --- a/client/pages/tags/[tag].js +++ b/client/pages/tags/[tag].js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; import getConfig from "next/config"; import qs from "qs"; @@ -7,14 +7,16 @@ import SEO from "../../components/SEO"; import Text from "../../components/Text"; import TorrentList from "../../components/TorrentList"; -const Tag = ({ results }) => { +const Tag = ({ results, token }) => { + const [torrents, setTorrents] = useState(results?.torrents ?? []); + const router = useRouter(); const { query: { tag }, } = router; const { - publicRuntimeConfig: { SQ_TORRENT_CATEGORIES }, + publicRuntimeConfig: { SQ_TORRENT_CATEGORIES, SQ_API_URL }, } = getConfig(); return ( @@ -23,11 +25,14 @@ const Tag = ({ results }) => { Tagged with “{tag}” - {results?.torrents.length ? ( + {torrents.length ? ( ) : ( No results. @@ -64,7 +69,7 @@ export const getServerSideProps = withAuthServerSideProps( throw "banned"; } const results = await searchRes.json(); - return { props: { results } }; + return { props: { results, token } }; } catch (e) { if (e === "banned") throw "banned"; return { props: {} };