diff --git a/package.json b/package.json index d153de7..f03463b 100644 --- a/package.json +++ b/package.json @@ -15,18 +15,23 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@radix-ui/react-dialog": "^1.0.4", + "javascript-time-ago": "^2.5.9", + "marked": "^9.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-ga": "^3.3.1", "react-router": "^6.14.0", "react-router-dom": "^6.14.0", - "react-router-hash-link": "^2.4.3" + "react-router-hash-link": "^2.4.3", + "sanitize-html": "^2.11.0", + "timeago": "^1.6.7" }, "devDependencies": { "@saber2pr/types-github-api": "^0.0.9", "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", "@types/react-lazy-load-image-component": "^1.5.3", + "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", "@vitejs/plugin-react-swc": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70cb680..04161ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ dependencies: '@radix-ui/react-dialog': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.0.11)(@types/react@18.0.37)(react-dom@18.2.0)(react@18.2.0) + javascript-time-ago: + specifier: ^2.5.9 + version: 2.5.9 + marked: + specifier: ^9.0.3 + version: 9.0.3 react: specifier: ^18.2.0 version: 18.2.0 @@ -38,6 +44,12 @@ dependencies: react-router-hash-link: specifier: ^2.4.3 version: 2.4.3(react-router-dom@6.14.0)(react@18.2.0) + sanitize-html: + specifier: ^2.11.0 + version: 2.11.0 + timeago: + specifier: ^1.6.7 + version: 1.6.7 devDependencies: '@saber2pr/types-github-api': @@ -52,6 +64,9 @@ devDependencies: '@types/react-lazy-load-image-component': specifier: ^1.5.3 version: 1.5.3 + '@types/sanitize-html': + specifier: ^2.9.0 + version: 2.9.0 '@typescript-eslint/eslint-plugin': specifier: ^5.59.0 version: 5.59.0(@typescript-eslint/parser@5.59.0)(eslint@8.38.0)(typescript@5.0.2) @@ -900,6 +915,12 @@ packages: '@types/scheduler': 0.16.3 csstype: 3.1.2 + /@types/sanitize-html@2.9.0: + resolution: {integrity: sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==} + dependencies: + htmlparser2: 8.0.2 + dev: true + /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} @@ -1261,6 +1282,11 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: false + /detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false @@ -1287,10 +1313,37 @@ packages: esutils: 2.0.3 dev: true + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + /electron-to-chromium@1.4.508: resolution: {integrity: sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==} dev: true + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + /esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -1329,7 +1382,6 @@ packages: /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - dev: true /eslint-plugin-react-hooks@4.6.0(eslint@8.38.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} @@ -1616,6 +1668,14 @@ packages: function-bind: 1.1.1 dev: true + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -1686,15 +1746,30 @@ packages: engines: {node: '>=8'} dev: true + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /javascript-time-ago@2.5.9: + resolution: {integrity: sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==} + dependencies: + relative-time-format: 1.1.6 + dev: false + /jiti@1.19.3: resolution: {integrity: sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w==} hasBin: true dev: true + /jquery@3.7.1: + resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + dev: false + /js-sdsl@4.4.2: resolution: {integrity: sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==} dev: true @@ -1768,6 +1843,12 @@ packages: yallist: 4.0.0 dev: true + /marked@9.0.3: + resolution: {integrity: sha512-pI/k4nzBG1PEq1J3XFEHxVvjicfjl8rgaMaqclouGSMPhk7Q3Ejb2ZRxx/ZQOcQ1909HzVoWCFYq6oLgtL4BpQ==} + engines: {node: '>= 16'} + hasBin: true + dev: false + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1803,7 +1884,6 @@ packages: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -1875,6 +1955,10 @@ packages: callsites: 3.1.0 dev: true + /parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1901,7 +1985,6 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -1986,7 +2069,6 @@ packages: nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: true /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -2143,6 +2225,10 @@ packages: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} dev: false + /relative-time-format@1.1.6: + resolution: {integrity: sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==} + dev: false + /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2183,6 +2269,17 @@ packages: queue-microtask: 1.2.3 dev: true + /sanitize-html@2.11.0: + resolution: {integrity: sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==} + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.4.29 + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -2217,7 +2314,6 @@ packages: /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dev: true /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -2309,6 +2405,12 @@ packages: any-promise: 1.3.0 dev: true + /timeago@1.6.7: + resolution: {integrity: sha512-FikcjN98+ij0siKH4VO4dZ358PR3oDDq4Vdl1+sN9gWz1/+JXGr3uZbUShYH/hL7bMhcTpPbplJU5Tej4b4jbQ==} + dependencies: + jquery: 3.7.1 + dev: false + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} diff --git a/src/components/Plugin.tsx b/src/components/Plugin.tsx new file mode 100644 index 0000000..fcae5fe --- /dev/null +++ b/src/components/Plugin.tsx @@ -0,0 +1,49 @@ +import {PluginMetaData,Plugin} from "../store/Plugin.ts"; +import {FC} 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' +type PluginProps = { + metadata: PluginMetaData, + plugins: Plugin +} +TimeAgo.addDefaultLocale(en) +const timeago = new TimeAgo('en-US') + + +const formatTime = (isoDate: string) => { + return timeago.format(new Date(isoDate)) +} + +const renderMarkdown = (text: string) => { + const unsafeHtml = marked.parse(text) + const sanitizedHtml = sanitizeHtml(unsafeHtml) + return {__html: sanitizedHtml} +} + +export const PluginCom: FC = ({plugins})=>{ + return
+
+
+ {plugins.name} + {plugins.version} +
+
+ {plugins.time&&
{formatTime(plugins.time)}
} +
+
+
+
+
+
+
{plugins.description}
+
+ {plugins.image&&{"Image} + {plugins.readme&&
} +
+
+ +
+
+} diff --git a/src/pages/PluginViewer.tsx b/src/pages/PluginViewer.tsx index 15eb871..9eaeaa0 100644 --- a/src/pages/PluginViewer.tsx +++ b/src/pages/PluginViewer.tsx @@ -1,33 +1,40 @@ -import {Plugins} from "../store/Plugin.ts"; +import {PluginResponse} from "../store/Plugin.ts"; import {useUIStore} from "../store/store.ts"; -import {useEffect, useMemo} from "react"; - +import {useEffect} from "react"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faSearch} from "@fortawesome/free-solid-svg-icons"; +import {PluginCom} from "../components/Plugin.tsx"; export const PluginViewer = ()=>{ const setPlugin = useUIStore(state => state.setPlugins) const plugins = useUIStore(state => state.plugins) - const numberOfTotalDownloads = useMemo(()=>{ - return Object.keys(plugins).reduce((acc, key)=>{ - return acc + plugins[key].data.downloads - },0) - }, [plugins] - ) - const retrievePlugins = async ()=>{ - fetch('https://static.etherpad.org/plugins.full.json') - .then(response => response.json()) - .then((data:Plugins) =>setPlugin(data)) - } + const pluginSearchTerm = useUIStore(state => state.pluginSearchTerm) + const setPluginSearchTerm = useUIStore(state => state.setPluginSearchTerm) + useEffect(() => { - if (Object.keys(plugins).length > 0) return - retrievePlugins() + fetch('/api/plugins') + .then(response => response.json()) + .then((data:PluginResponse) =>setPlugin(data)) }, []); - return
-

PluginViewer

+ return
+
+

PluginViewer

- This page lists all available plugins for etherpad hosted on npm. {numberOfTotalDownloads} downloads of 231 plugins in the last month. + This page lists all available plugins for etherpad hosted on npm. {plugins?.metadata.total_downloads} downloads of {plugins?.metadata.total_count} plugins in the last month. For more information about Etherpad visit https://etherpad.org. - +
+ setPluginSearchTerm(v.target.value)}/> + +
+
+ { + plugins?.plugins.map((plugin)=> { + return + }) + } +
+
} diff --git a/src/store/Plugin.ts b/src/store/Plugin.ts index c43ee48..7ed4239 100644 --- a/src/store/Plugin.ts +++ b/src/store/Plugin.ts @@ -1,38 +1,25 @@ -export type Plugins = { - [key:string]: PluginData -} - -export type PluginData = { +export type Plugin = { name: string, description: string, - time: string, version: string, + time: string, + author: string, + author_email: string, official: boolean, - data: { - _id: string, - _rev: string, - name: string, - dist_tags: { - latest: string - }, - versions: { - [key: string]: { - name: string, - version: string, - description: string, - author: { - name: string, - email: string, - }, - license: string, - keywords: string[], - repository: { - type: string, - url: string, - } - } - }, - license: string, - downloads: number - } + popularity_score: number, + keywords: string[], + image: string, + readme: string, +} + +export type PluginMetaData = { + total_count: number, + total_downloads: number, + page_size: number, +} + + +export type PluginResponse = { + metadata: PluginMetaData, + plugins: Plugin[] } diff --git a/src/store/store.ts b/src/store/store.ts index 6f551fb..8b7f1fc 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,5 +1,5 @@ import {create} from "zustand"; -import {Plugins} from "./Plugin.ts"; +import {PluginResponse} from "./Plugin.ts"; export type FileNotPresentMetaData = { @@ -97,8 +97,10 @@ type StoreType = { openFileNotPresentDialog: (fileNotPresentDialog: boolean) => void, fileNotPresentMetaData: FileNotPresentMetaData | undefined, setFileNotPresentMetaData: (fileNotPresentMetaData: FileNotPresentMetaData) => void, - plugins: Plugins, - setPlugins: (plugins: Plugins) => void, + plugins: PluginResponse | undefined, + setPlugins: (plugins: PluginResponse) => void, + pluginSearchTerm: string, + setPluginSearchTerm: (pluginSearchTerm: string) => void } const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches @@ -146,6 +148,12 @@ export const useUIStore = create((set) => ({ fileNotPresentMetaData } }), - plugins: {}, - setPlugins: (plugins: Plugins) => set(plugins) + plugins: undefined, + setPlugins: (plugins1: PluginResponse) => set({ + plugins: plugins1 + }), + pluginSearchTerm: "", + setPluginSearchTerm: (pluginSearchTerm: string) => set({ + pluginSearchTerm + }) })) diff --git a/vite.config.ts b/vite.config.ts index 861b04b..7b7e2cf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + server:{ + proxy:{ + "/api": { + target: "http://localhost:9000", + changeOrigin: true, + secure: false, + } + } + } })