From 6bf5561d1cba3d3e69686f431654bbba31d6dd84 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:37:48 +0100 Subject: [PATCH 1/3] Added Next plugin viewer --- app/plugins/page.tsx | 160 +++++++++++++++++ next.config.mjs | 7 - next.config.ts | 19 ++ package.json | 1 + pnpm-lock.yaml | 261 ++++++++++++++++++++++++++++ src/components/Plugin.tsx | 34 ++-- src/pagesToDisplay/MainHeadline.tsx | 5 +- src/store/Plugin.ts | 59 ++++++- src/store/store.ts | 8 +- 9 files changed, 517 insertions(+), 37 deletions(-) create mode 100644 app/plugins/page.tsx delete mode 100644 next.config.mjs create mode 100644 next.config.ts diff --git a/app/plugins/page.tsx b/app/plugins/page.tsx new file mode 100644 index 0000000..a533af1 --- /dev/null +++ b/app/plugins/page.tsx @@ -0,0 +1,160 @@ +'use client' + +import * as Checkbox from '@radix-ui/react-checkbox'; +import {useUIStore} from "../../src/store/store"; +import {useEffect, useMemo, useState} from "react"; +import {CheckIcon} from '../../src/components/CheckIcon'; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faSearch} from "@fortawesome/free-solid-svg-icons"; +import {PluginCom} from "../../src/components/Plugin"; +import axios, {AxiosResponse} from "axios"; +import {PluginMappedResponseVal, PluginResponse, ServerStats} from "../../src/store/Plugin"; +import Link from "next/link"; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +const IMAGE_REGEX = /\b(https?:\/\/[\S]+?(?:png|jpe?g|gif))\b/; + + +export default function PluginViewer() { + const setPlugin = useUIStore(state => state.setPlugins) + const plugins = useUIStore(state => state.plugins) + const pluginSearchTerm = useUIStore(state => state.pluginSearchTerm) + const setPluginSearchTerm = useUIStore(state => state.setPluginSearchTerm) + const [officalOnly, setOfficalOnly] = useState(false) + const serverStats = useUIStore(state => state.serverStats) + const totalDownloads = useMemo(()=>{ + if (!plugins) return 0 + return Object.values(plugins).reduce((acc, val) => acc + val.downloads, 0) + },[plugins]) + const [sortKey, setSortKey] = useState('newest') + const [downloadPercentage, setDownloadAveragePercentage] = useState(0) + const totalCount = useMemo(()=>{ + if (!plugins) return 0 + return Object.keys(plugins).length + }, [plugins]) + const filteredPlugins = useMemo(()=>{ + if (!plugins) return plugins + let average = 0 + + const entry: PluginMappedResponseVal[] = Object.entries(plugins).filter((plugin) => { + if (officalOnly && plugin[1].official == false) { + return false + } + if (plugin[1].keywords && pluginSearchTerm) { + for (let i = 0; i < plugin[1].keywords.length; i++) { + let keyword = plugin[1].keywords[i]; + if (keyword.toUpperCase().indexOf(pluginSearchTerm) > -1) { + return true; + } + } + } + + average += plugin[1].downloads + + return true + }).map(plugin=> { + return { + ...plugin[1], + name: plugin[0], + image: plugin[1].readme.match(IMAGE_REGEX)?.[0] + } satisfies PluginMappedResponseVal + }) + + average = average / entry.length + setDownloadAveragePercentage(average) + entry.sort(function (a, b) { + if (sortKey === 'newest') { + if (a.time === undefined) { + return 1; + } else if (b.time === undefined) { + return -1; + } + return a.time < b.time ? 1 : -1; + } else if (sortKey === 'updated') { + if (a.modified === undefined) { + return 1; + } else if (b.modified === undefined) { + return -1; + } + return a.modified < b.modified ? 1 : -1; + } else { + return a.downloads < b.downloads ? 1 : -1; + }}) + + + return entry + }, [plugins, officalOnly, pluginSearchTerm]) + + + function performSearch() { + axios.get('/plugins.viewer.json') + .then((data: AxiosResponse) => { + setPlugin(data.data) + }) + } + + function performStatSearch() { + axios.get('/server-stats.json') + .then((data: AxiosResponse)=>{ + useUIStore.setState({serverStats:data.data}) + }) + } + + useEffect(() => { + performSearch(); + performStatSearch(); + }, []); + + return ( +
+
+

PluginViewer

+
+ This page lists all available plugins for etherpad hosted on npm.
{totalDownloads} downloads of {totalCount} plugins in the last month.
+ For more information about Etherpad visit https://etherpad.org +
+
+

Plugins ({totalCount})

+ setOfficalOnly(!officalOnly)} id="c1"> + + + + + + Sort by: + + {sortKey} + + { + setSortKey('created') + }}>Created + { + setSortKey('updated') + }}>Updated + + +
+
+ setPluginSearchTerm(v.target.value)}/> + +
+
+ { + filteredPlugins && filteredPlugins?.map((plugin, i) => { + return + }) + } +
+
+
+ ) +} diff --git a/next.config.mjs b/next.config.mjs deleted file mode 100644 index 57fc45c..0000000 --- a/next.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - output: 'export', // Outputs a Single-Page Application (SPA). - distDir: './dist', // Changes the build output directory to `./dist/`. -} - -export default nextConfig diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..85d369d --- /dev/null +++ b/next.config.ts @@ -0,0 +1,19 @@ +/** @type {import('next').NextConfig} */ +const nextConfig: import('next').NextConfig = { + output: 'export', // Outputs a Single-Page Application (SPA). + distDir: './dist', // Changes the build output directory to `./dist/`. + rewrites: async () => { + return [ + { + source: '/plugins.viewer.json', + destination: 'https://static.etherpad.org/plugins.viewer.json', + }, + { + source: '/server-stats.json', + destination: 'https://static.etherpad.org/server-stats.json', + } + ] + } +} + +export default nextConfig diff --git a/package.json b/package.json index 06d5280..5b5b55d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", "axios": "^1.7.7", "javascript-time-ago": "^2.5.11", "next": "^15.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47dd37e..431a2a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@radix-ui/react-dialog': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) axios: specifier: ^1.7.7 version: 1.7.7 @@ -303,6 +306,21 @@ packages: resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.6.8': + resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + + '@floating-ui/dom@1.6.12': + resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + '@fortawesome/fontawesome-common-types@6.6.0': resolution: {integrity: sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==} engines: {node: '>=6'} @@ -542,6 +560,19 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/react-arrow@1.1.0': + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-checkbox@1.1.2': resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==} peerDependencies: @@ -555,6 +586,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.0': + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.0': resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} peerDependencies: @@ -564,6 +608,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.0': + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.1': resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} peerDependencies: @@ -586,6 +639,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.1': resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} peerDependencies: @@ -599,6 +661,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.2': + resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: @@ -630,6 +705,32 @@ packages: '@types/react': optional: true + '@radix-ui/react-menu@2.1.2': + resolution: {integrity: sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.0': + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.2': resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} peerDependencies: @@ -669,6 +770,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.0': + resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.1.0': resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: @@ -723,6 +837,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-size@1.1.0': resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} peerDependencies: @@ -732,6 +855,9 @@ packages: '@types/react': optional: true + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@rollup/rollup-android-arm-eabi@4.25.0': resolution: {integrity: sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==} cpu: [arm] @@ -2127,6 +2253,23 @@ snapshots: dependencies: levn: 0.4.1 + '@floating-ui/core@1.6.8': + dependencies: + '@floating-ui/utils': 0.2.8 + + '@floating-ui/dom@1.6.12': + dependencies: + '@floating-ui/core': 1.6.8 + '@floating-ui/utils': 0.2.8 + + '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.12 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.8': {} + '@fortawesome/fontawesome-common-types@6.6.0': {} '@fortawesome/fontawesome-svg-core@6.6.0': @@ -2304,6 +2447,15 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-checkbox@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -2320,12 +2472,30 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-context@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + '@radix-ui/react-context@1.1.1(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 @@ -2354,6 +2524,12 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-direction@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -2367,6 +2543,21 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.12)(react@18.3.1)': dependencies: react: 18.3.1 @@ -2391,6 +2582,50 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2420,6 +2655,23 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -2459,6 +2711,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -2466,6 +2725,8 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/rect@1.1.0': {} + '@rollup/rollup-android-arm-eabi@4.25.0': optional: true diff --git a/src/components/Plugin.tsx b/src/components/Plugin.tsx index f5b5771..2718dda 100644 --- a/src/components/Plugin.tsx +++ b/src/components/Plugin.tsx @@ -1,17 +1,15 @@ -import {PluginMetaData, Plugin} from "../store/Plugin.ts"; -import {FC} from "react"; +import {PluginResponseVal} from "../store/Plugin.ts"; +import {FC, useMemo} from "react"; import TimeAgo from 'javascript-time-ago' import en from 'javascript-time-ago/locale/en' import sanitizeHtml from 'sanitize-html' import * as marked from 'marked' import {PluginAuthorComp} from "./PluginAuthorComp.tsx"; import {Chip} from "./Chip.tsx"; -import {Waypoint} from "react-waypoint"; -import {useUIStore} from "../store/store.ts"; type PluginProps = { - metadata: PluginMetaData, - plugins: Plugin, - index: number + plugins: PluginResponseVal, + index: number, + averageDownload: number } TimeAgo.addDefaultLocale(en) const timeago = new TimeAgo('en-US') @@ -21,27 +19,17 @@ const formatTime = (isoDate: string) => { return timeago.format(new Date(isoDate)) } - - -export const PluginCom: FC = ({plugins, index}) => { - const pluginResp = useUIStore(state => state.plugins) - //const setPlugins = useUIStore(state=>state.setPlugins) +export const PluginCom: FC = ({plugins, averageDownload}) => { const renderMarkdown = (text: string) => { const unsafeHtml = marked.parse(text) as string const sanitizedHtml = sanitizeHtml(unsafeHtml) return {__html: sanitizedHtml} } - - const loadNextPage = () => { - /* fetch('/api/plugins') - .then(response => response.json()) - .then((data: PluginResponse) => setPlugins(data))*/ - } + const popularityScore = useMemo(() => (plugins.downloads / averageDownload), [plugins.downloads, averageDownload]) return
- {pluginResp && pluginResp?.plugins.length - 2 === index && loadNextPage()}/>}
{plugins.name} {plugins.version} @@ -50,8 +38,8 @@ export const PluginCom: FC = ({plugins, index}) => { {plugins.time &&
{formatTime(plugins.time)}
}
-
+
@@ -64,12 +52,12 @@ export const PluginCom: FC = ({plugins, index}) => { dangerouslySetInnerHTML={renderMarkdown(plugins.readme)}>
}
- + License: {plugins.license ? plugins.license : '--'} { - plugins.keywords.trim().length > 0 && plugins.keywords.split(",").map((k, i) => {k}) } diff --git a/src/pagesToDisplay/MainHeadline.tsx b/src/pagesToDisplay/MainHeadline.tsx index ce51827..6f41c2d 100644 --- a/src/pagesToDisplay/MainHeadline.tsx +++ b/src/pagesToDisplay/MainHeadline.tsx @@ -2,6 +2,7 @@ import gif from '../assets/img/etherpad_demo.gif' import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faCogs, faLanguage, faServer, faUsers} from "@fortawesome/free-solid-svg-icons"; import {Suspense} from "react"; +import Link from "next/link"; export const MainHeadline = () => { return
@@ -19,8 +20,8 @@ export const MainHeadline = () => {
+ 290 + Plugins
105 Languages
diff --git a/src/store/Plugin.ts b/src/store/Plugin.ts index 16110e2..dd6e3bf 100644 --- a/src/store/Plugin.ts +++ b/src/store/Plugin.ts @@ -21,6 +21,61 @@ export type PluginMetaData = { export type PluginResponse = { - metadata: PluginMetaData, - plugins: Plugin[] + [key: string]: PluginResponseVal +} + + +export type PluginResponseVal = { + name: string, + description: string, + time: string, + created: string, + modified: string, + version: string, + license: string, + official: boolean, + downloads: number, + keywords: string[], + readme: string, + author: { + name: string, + email: string + } +} + + +export type PluginMappedResponseVal = PluginResponseVal & { + name: string, + image?: string +} + + +export type ServerStats = { + clients: { + count: number + }, + org: [ + { + name: string, + count: number + } + ], + isp: [ + { + name: string, + count: number + } + ], + country: [ + { + name: string, + count: number + } + ], + user_agent:[ + { + name: string, + count: number + } + ] } diff --git a/src/store/store.ts b/src/store/store.ts index 2232ba7..9d93a8b 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,5 +1,5 @@ import {create} from "zustand"; -import {PluginResponse} from "./Plugin.ts"; +import {PluginResponse, ServerStats} from "./Plugin.ts"; export type FileNotPresentMetaData = { @@ -100,7 +100,8 @@ type StoreType = { plugins: PluginResponse | undefined, setPlugins: (plugins: PluginResponse) => void, pluginSearchTerm: string, - setPluginSearchTerm: (pluginSearchTerm: string) => void + setPluginSearchTerm: (pluginSearchTerm: string) => void, + serverStats: ServerStats | undefined } @@ -154,5 +155,6 @@ export const useUIStore = create((set) => ({ pluginSearchTerm: "", setPluginSearchTerm: (pluginSearchTerm: string) => set({ pluginSearchTerm - }) + }), + serverStats: undefined, })) From 3d3f8998ef24cec58448145ccdf754467cdaf14e Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Mon, 11 Nov 2024 19:42:56 +0100 Subject: [PATCH 2/3] Added total number popularity score. --- app/plugins/page.tsx | 11 +++++++---- src/components/Plugin.tsx | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/plugins/page.tsx b/app/plugins/page.tsx index a533af1..4746cd5 100644 --- a/app/plugins/page.tsx +++ b/app/plugins/page.tsx @@ -34,7 +34,8 @@ export default function PluginViewer() { }, [plugins]) const filteredPlugins = useMemo(()=>{ if (!plugins) return plugins - let average = 0 + let totalNum = 0 + let highestDownload = 0 const entry: PluginMappedResponseVal[] = Object.entries(plugins).filter((plugin) => { if (officalOnly && plugin[1].official == false) { @@ -49,7 +50,10 @@ export default function PluginViewer() { } } - average += plugin[1].downloads + totalNum += plugin[1].downloads + if (plugin[1].downloads > highestDownload) { + highestDownload = plugin[1].downloads + } return true }).map(plugin=> { @@ -60,8 +64,7 @@ export default function PluginViewer() { } satisfies PluginMappedResponseVal }) - average = average / entry.length - setDownloadAveragePercentage(average) + setDownloadAveragePercentage(highestDownload / totalNum) entry.sort(function (a, b) { if (sortKey === 'newest') { if (a.time === undefined) { diff --git a/src/components/Plugin.tsx b/src/components/Plugin.tsx index 2718dda..e52ada2 100644 --- a/src/components/Plugin.tsx +++ b/src/components/Plugin.tsx @@ -26,7 +26,7 @@ export const PluginCom: FC = ({plugins, averageDownload}) => { const sanitizedHtml = sanitizeHtml(unsafeHtml) return {__html: sanitizedHtml} } - const popularityScore = useMemo(() => (plugins.downloads / averageDownload), [plugins.downloads, averageDownload]) + const popularityScore = useMemo(() => (plugins.downloads / (averageDownload*100*100)), [plugins.downloads, averageDownload]) return
From 3e45be47dad85c93267036181fabd1c840edbf8a Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:33:45 +0100 Subject: [PATCH 3/3] Added plugin viewer. --- .env | 1 + .env.local | 1 + app/index.css | 12 +- app/plugins/page.tsx | 134 ++++++++++++------- components.json | 21 +++ package.json | 7 +- pnpm-lock.yaml | 115 ++++++++++++++++ src/components/Plugin.tsx | 11 +- src/components/ui/button.tsx | 57 ++++++++ src/components/ui/checkbox.tsx | 28 ++++ src/components/ui/dropdown-menu.tsx | 199 ++++++++++++++++++++++++++++ src/components/ui/pagination.tsx | 117 ++++++++++++++++ src/components/ui/select.tsx | 157 ++++++++++++++++++++++ src/lib/utils.ts | 6 + src/pagesToDisplay/PluginViewer.tsx | 77 ----------- src/store/Plugin.ts | 3 +- tailwind.config.ts | 23 ++-- tsconfig.json | 7 +- 18 files changed, 837 insertions(+), 139 deletions(-) create mode 100644 .env create mode 100644 .env.local create mode 100644 components.json create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/pagination.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/lib/utils.ts delete mode 100644 src/pagesToDisplay/PluginViewer.tsx diff --git a/.env b/.env new file mode 100644 index 0000000..02eaebe --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=https://static.etherpad.org diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..afb3e74 --- /dev/null +++ b/.env.local @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL= diff --git a/app/index.css b/app/index.css index a033c8b..bfa65af 100644 --- a/app/index.css +++ b/app/index.css @@ -316,7 +316,7 @@ div#footer img.logo { } .readme-of-plugin code, .readme-of-plugin pre { - @apply text-sm p-1 rounded-sm dark:bg-gray-700 bg-gray-200 overflow-hidden; + @apply text-sm p-1 rounded-sm bg-gray-700 overflow-hidden; } .readme-of-plugin { @@ -330,3 +330,13 @@ div#footer img.logo { #banner { @apply sticky bottom-0 } + +@layer base { + :root { + --radius: 0.5rem; + } +} + +[aria-current="page"] { + @apply bg-primary bg-none; +} diff --git a/app/plugins/page.tsx b/app/plugins/page.tsx index 4746cd5..564a621 100644 --- a/app/plugins/page.tsx +++ b/app/plugins/page.tsx @@ -1,16 +1,28 @@ 'use client' -import * as Checkbox from '@radix-ui/react-checkbox'; import {useUIStore} from "../../src/store/store"; import {useEffect, useMemo, useState} from "react"; -import {CheckIcon} from '../../src/components/CheckIcon'; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faSearch} from "@fortawesome/free-solid-svg-icons"; import {PluginCom} from "../../src/components/Plugin"; import axios, {AxiosResponse} from "axios"; import {PluginMappedResponseVal, PluginResponse, ServerStats} from "../../src/store/Plugin"; import Link from "next/link"; -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, +} from "@/components/ui/select" +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination" +import { Checkbox } from "@/components/ui/checkbox" const IMAGE_REGEX = /\b(https?:\/\/[\S]+?(?:png|jpe?g|gif))\b/; @@ -21,11 +33,11 @@ export default function PluginViewer() { const pluginSearchTerm = useUIStore(state => state.pluginSearchTerm) const setPluginSearchTerm = useUIStore(state => state.setPluginSearchTerm) const [officalOnly, setOfficalOnly] = useState(false) - const serverStats = useUIStore(state => state.serverStats) const totalDownloads = useMemo(()=>{ if (!plugins) return 0 return Object.values(plugins).reduce((acc, val) => acc + val.downloads, 0) },[plugins]) + const [sortKey, setSortKey] = useState('newest') const [downloadPercentage, setDownloadAveragePercentage] = useState(0) const totalCount = useMemo(()=>{ @@ -34,7 +46,6 @@ export default function PluginViewer() { }, [plugins]) const filteredPlugins = useMemo(()=>{ if (!plugins) return plugins - let totalNum = 0 let highestDownload = 0 const entry: PluginMappedResponseVal[] = Object.entries(plugins).filter((plugin) => { @@ -50,7 +61,6 @@ export default function PluginViewer() { } } - totalNum += plugin[1].downloads if (plugin[1].downloads > highestDownload) { highestDownload = plugin[1].downloads } @@ -64,7 +74,7 @@ export default function PluginViewer() { } satisfies PluginMappedResponseVal }) - setDownloadAveragePercentage(highestDownload / totalNum) + setDownloadAveragePercentage(highestDownload) entry.sort(function (a, b) { if (sortKey === 'newest') { if (a.time === undefined) { @@ -84,20 +94,32 @@ export default function PluginViewer() { return a.downloads < b.downloads ? 1 : -1; }}) + const chunkSize = 30; + const chunkedArray = [] + for (let i = 0; i < entry.length; i += chunkSize) { + const chunk = entry.slice(i, i + chunkSize); + chunkedArray.push(chunk) + } + - return entry + return chunkedArray }, [plugins, officalOnly, pluginSearchTerm]) + const [currentPage, setCurrentPage] = useState(0) + const pluginsToDisplay = useMemo(()=>{ + if (!filteredPlugins) return [] + return filteredPlugins[currentPage] + }, [currentPage, filteredPlugins]) function performSearch() { - axios.get('/plugins.viewer.json') + axios.get(process.env!.NEXT_PUBLIC_API_URL!+'/plugins.viewer.json') .then((data: AxiosResponse) => { setPlugin(data.data) }) } function performStatSearch() { - axios.get('/server-stats.json') + axios.get(process.env!.NEXT_PUBLIC_API_URL!+'/server-stats.json') .then((data: AxiosResponse)=>{ useUIStore.setState({serverStats:data.data}) }) @@ -109,53 +131,75 @@ export default function PluginViewer() { }, []); return ( -
+
-

PluginViewer

-
- This page lists all available plugins for etherpad hosted on npm.
{totalDownloads} downloads of {totalCount} plugins in the last month.
- For more information about Etherpad visit https://etherpad.org -
-
-

Plugins ({totalCount})

- setOfficalOnly(!officalOnly)} id="c1"> - - - - -
diff --git a/components.json b/components.json new file mode 100644 index 0000000..ea67b18 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/index.css", + "baseColor": "neutral", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/package.json b/package.json index 5b5b55d..8eb1ba2 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,20 @@ "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slot": "^1.1.0", "axios": "^1.7.7", + "class-variance-authority": "^0.7.0", "javascript-time-ago": "^2.5.11", - "next": "^15.0.3", + "lucide-react": "^0.456.0", "marked": "^15.0.0", + "next": "^15.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-ga": "^3.3.1", "react-waypoint": "^10.3.0", "sanitize-html": "^2.13.1", + "tailwindcss-animate": "^1.0.7", "timeago": "^1.6.7" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 431a2a7..315d72b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,12 +29,24 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.1.0 + version: 1.1.0(@types/react@18.3.12)(react@18.3.1) axios: specifier: ^1.7.7 version: 1.7.7 + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 javascript-time-ago: specifier: ^2.5.11 version: 2.5.11 + lucide-react: + specifier: ^0.456.0 + version: 0.456.0(react@18.3.1) marked: specifier: ^15.0.0 version: 15.0.0 @@ -56,6 +68,9 @@ importers: sanitize-html: specifier: ^2.13.1 version: 2.13.1 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.14) timeago: specifier: ^1.6.7 version: 1.6.7 @@ -557,6 +572,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} @@ -783,6 +801,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.1.2': + resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.1.0': resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: @@ -855,6 +886,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-visually-hidden@1.1.0': + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} @@ -1219,9 +1263,16 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + class-variance-authority@0.7.0: + resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1601,6 +1652,11 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lucide-react@0.456.0: + resolution: {integrity: sha512-DIIGJqTT5X05sbAsQ+OhA8OtJYyD4NsEMCA/HQW/Y6ToPQ7gwbtujIoeAaup4HpHzV35SQOarKAWH8LYglB6eA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + marked@15.0.0: resolution: {integrity: sha512-0mouKmBROJv/WSHJBPZZyYofUgawMChnD5je/g+aOBXsHDjb/IsnTQj7mnhQZu+qPJmRQ0ecX3mLGEUm3BgwYA==} engines: {node: '>= 18'} @@ -1977,6 +2033,11 @@ packages: tailwind-merge@2.5.4: resolution: {integrity: sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==} + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + tailwindcss@3.4.14: resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} engines: {node: '>=14.0.0'} @@ -2445,6 +2506,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.1.0': {} '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -2672,6 +2735,35 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-select@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -2725,6 +2817,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/rect@1.1.0': {} '@rollup/rollup-android-arm-eabi@4.25.0': @@ -3065,8 +3166,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + class-variance-authority@0.7.0: + dependencies: + clsx: 2.0.0 + client-only@0.0.1: {} + clsx@2.0.0: {} + clsx@2.1.1: {} color-convert@2.0.1: @@ -3446,6 +3553,10 @@ snapshots: lru-cache@10.4.3: {} + lucide-react@0.456.0(react@18.3.1): + dependencies: + react: 18.3.1 + marked@15.0.0: {} merge2@1.4.1: {} @@ -3828,6 +3939,10 @@ snapshots: tailwind-merge@2.5.4: {} + tailwindcss-animate@1.0.7(tailwindcss@3.4.14): + dependencies: + tailwindcss: 3.4.14 + tailwindcss@3.4.14: dependencies: '@alloc/quick-lru': 5.2.0 diff --git a/src/components/Plugin.tsx b/src/components/Plugin.tsx index e52ada2..8d6be77 100644 --- a/src/components/Plugin.tsx +++ b/src/components/Plugin.tsx @@ -26,9 +26,9 @@ export const PluginCom: FC = ({plugins, averageDownload}) => { const sanitizedHtml = sanitizeHtml(unsafeHtml) return {__html: sanitizedHtml} } - const popularityScore = useMemo(() => (plugins.downloads / (averageDownload*100*100)), [plugins.downloads, averageDownload]) + const popularityScore = useMemo(() => (plugins.downloads / (averageDownload)), [plugins.downloads, averageDownload]) - return
+ return
{plugins.name} @@ -55,12 +55,15 @@ export const PluginCom: FC = ({plugins, averageDownload}) => { License: {plugins.license ? plugins.license : '--'} - +
+
+
+
{ plugins.keywords && plugins.keywords.map((k, i) => {k}) } - +
} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..be0dce8 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-neutral-300", + { + variants: { + variant: { + default: + "bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90", + destructive: + "bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90", + outline: + "border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", + secondary: + "bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80", + ghost: "hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", + link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..0c5facc --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..4ed1dcb --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,199 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0 dark:focus:bg-neutral-800 dark:focus:text-neutral-50", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx new file mode 100644 index 0000000..d331105 --- /dev/null +++ b/src/components/ui/pagination.tsx @@ -0,0 +1,117 @@ +import * as React from "react" +import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" +import { ButtonProps, buttonVariants } from "@/components/ui/button" + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +