From 0dfa448df84f2f33e9b9caffdc029d152554f49c Mon Sep 17 00:00:00 2001 From: Federico <38290480+FedeIlLeone@users.noreply.github.com> Date: Thu, 19 Oct 2023 21:54:43 +0200 Subject: [PATCH 1/6] feat(store): add sorting by downloads --- api/src/api/store/items.ts | 47 ++++++++++++++++++++++- web/src/components/store/Store.tsx | 20 +++++++++- web/src/components/store/store.module.css | 29 ++++++++++++-- web/src/components/util/Form.tsx | 11 ++++-- 4 files changed, 96 insertions(+), 11 deletions(-) diff --git a/api/src/api/store/items.ts b/api/src/api/store/items.ts index 78205de..df82dc6 100644 --- a/api/src/api/store/items.ts +++ b/api/src/api/store/items.ts @@ -159,8 +159,9 @@ export default function (fastify: FastifyInstance, _: unknown, done: () => void) page?: string; items?: string; query?: string; + sort?: "downloads" | "name"; }; - }>("/list/:type", (request, reply) => { + }>("/list/:type", async (request, reply) => { // @ts-expect-error includes bs if (!ADDON_TYPES.includes(request.params.type.replace(/s$/, ""))) { reply.code(400).send({ @@ -191,7 +192,7 @@ export default function (fastify: FastifyInstance, _: unknown, done: () => void) return; } - const manifests = listAddons(type, request.query.query); + let manifests = listAddons(type, request.query.query); const numPages = Math.ceil(manifests.length / perPage); if (page > numPages) { reply.code(404).send({ @@ -203,6 +204,48 @@ export default function (fastify: FastifyInstance, _: unknown, done: () => void) const start = (page - 1) * perPage; const end = start + perPage; + const sort = request.query.sort ?? "downloads"; + if (sort === "downloads") { + const collection = fastify.mongo.db!.collection("storeStats"); + const aggregation = collection.aggregate([ + { + $match: { + type: "install", + }, + }, + { + $group: { + _id: "$id", + ips: { + $addToSet: "$ipHash", + }, + }, + }, + { + $project: { + count: { + $size: "$ips", + }, + }, + }, + { + $sort: { + count: -1, + }, + }, + ]); + const stats = await aggregation.toArray(); + + manifests = manifests.sort((a, b) => { + const aStat = stats.find((x) => x._id === a.id); + const bStat = stats.find((x) => x._id === b.id); + if (!aStat && !bStat) return 0; + if (!aStat) return 1; + if (!bStat) return -1; + return bStat.count - aStat.count; + }); + } + return { page, numPages, diff --git a/web/src/components/store/Store.tsx b/web/src/components/store/Store.tsx index 9165836..7e5b341 100644 --- a/web/src/components/store/Store.tsx +++ b/web/src/components/store/Store.tsx @@ -14,6 +14,7 @@ import { toArray } from "../util/misc"; import { getError, installAddon, useInstalledAddons } from "./utils"; import { Routes } from "../../constants"; import { RouteError } from "../../types"; +import { SelectField } from "../util/Form"; type StoreKind = "plugin" | "theme"; @@ -238,6 +239,8 @@ export function StandaloneStoreItem({ export default function Store({ kind, installedAddons, updateAddonList }: StoreProps): VNode { useTitle(`Replugged ${LABELS[kind]}`); + const [sort, setSort] = useState("downloads"); + const [query, setQuery] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(query); @@ -252,12 +255,13 @@ export default function Store({ kind, installedAddons, updateAddonList }: StoreP ); const itemsQuery = useInfiniteQuery({ - queryKey: ["store", kind, debouncedQuery], + queryKey: ["store", kind, debouncedQuery, sort], queryFn: async ({ pageParam: page }) => { const queryString = new URLSearchParams({ page: page?.toString() ?? pageQuery ?? "1", items: (12).toString(), query: debouncedQuery, + sort, }); const res = await fetch(`/api/store/list/${kind}?${queryString}`); @@ -290,7 +294,17 @@ export default function Store({ kind, installedAddons, updateAddonList }: StoreP return (

Replugged {LABELS[kind]}

-
+
+ setSort(e.currentTarget.value)} + /> {items.length > 0 || debouncedQuery ? ( setQuery(e.currentTarget.value)} /> ) : null} +
+
; value?: string; options: Array<{ id: string; name: string }>; }; @@ -81,7 +85,7 @@ function useField(note?: ComponentChild, error?: ComponentChild, rk?: number): F function BaseField(props: BaseProps): VNode { return ( -
+