From d1a4a54d2a49703c2569f6f5daa17a6b717cd4fb Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Mon, 15 Jul 2024 13:26:57 -0400 Subject: [PATCH] docs: Implement self-hosted docs * Add npm scripts for installing/building docusaurus from main project * Set base url based on ENV * Build docsite in docker image * Add link to doc in dashboard and link to github * Update nodejs install instructions to include doc build command * Update GH pages workflow to use correct base url * Add hint page to main app when docs are not built --- .github/workflows/docsDeploy.yml | 1 + Dockerfile | 3 +- codeshift/transform.ts | 217 ---------------------- docsite/docs/installation/installation.md | 1 + docsite/docusaurus.config.ts | 41 ++-- package-lock.json | 102 +--------- package.json | 10 +- src/backend/server/index.ts | 4 + src/client/App.tsx | 71 +++++-- src/client/Version.tsx | 4 +- src/client/components/CopyToClipboard.tsx | 30 +++ src/client/components/ExternalLink.tsx | 24 +++ 12 files changed, 147 insertions(+), 361 deletions(-) delete mode 100644 codeshift/transform.ts create mode 100644 src/client/components/CopyToClipboard.tsx create mode 100644 src/client/components/ExternalLink.tsx diff --git a/.github/workflows/docsDeploy.yml b/.github/workflows/docsDeploy.yml index f7b60f81..a782c597 100644 --- a/.github/workflows/docsDeploy.yml +++ b/.github/workflows/docsDeploy.yml @@ -27,6 +27,7 @@ jobs: env: ANALYTICS: ${{ vars.ANALYTICS }} ANALYTICS_DOMAIN: ${{ vars.ANALYTICS_DOMAIN }} + DOCS_BASE: '/multi-scrobbler' run: npm run build working-directory: ./docsite diff --git a/Dockerfile b/Dockerfile index 09294b53..97a3d478 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,7 +84,7 @@ COPY --chown=abc:abc . /app # need to set before build so server/client build is optimized and has constants (if needed) ENV NODE_ENV=production -RUN npm run build && rm -rf node_modules +RUN npm run docs:install && npm run build && rm -rf node_modules && rm -rf docsite/node_modules FROM base as app @@ -92,6 +92,7 @@ COPY --chown=abc:abc package*.json ./ COPY --chown=abc:abc patches ./patches COPY --from=build --chown=abc:abc /app/dist /app/dist COPY --from=build --chown=abc:abc /app/src /app/src +COPY --from=build --chown=abc:abc /app/docsite /app/docsite COPY --from=base /usr/bin /usr/bin COPY --from=base /usr/lib /usr/lib diff --git a/codeshift/transform.ts b/codeshift/transform.ts deleted file mode 100644 index 3862b143..00000000 --- a/codeshift/transform.ts +++ /dev/null @@ -1,217 +0,0 @@ -import {API, Collection, FileInfo, Options} from 'jscodeshift'; -import path from 'path'; -import fs from 'fs'; - -export type TransformFrom = 'none' | 'any' | string; -export type TransformTo = 'none' | 'any' | string; -export type ImportTypes = 'any' | 'relative' | 'alias'; - -const base = process.cwd(); -export default function transformer( - file: FileInfo, - { jscodeshift: j }: API, - options: Options, -) { - - const tf = options.transformFrom as string | undefined; - if(tf === undefined) { - throw new Error(`arg '--transformFrom' must be defined.`); - } - const transformFrom = tf.split(',').map(x => x.toLocaleLowerCase()) as TransformFrom[]; - const tfIsAny = transformFrom.includes('any'); - - const tt = options.transformTo as string | undefined; - if(tt === undefined) { - throw new Error(`arg '--transformTo' must be defined.`); - } - const transformTo = tt.split(',').map(x => x.toLocaleLowerCase()) as TransformTo[]; - - if(transformTo.length > 1 && transformFrom.length !== transformTo.length) { - throw new Error('When more than one Transform To is specified then number of Transform From arguments must match'); - } - - const itypes = options.importTypes as string | undefined; - if(itypes === undefined) { - throw new Error(`arg '--importTypes' must be defined.`); - } - const importTypes = itypes.split(',').map(x => x.toLocaleLowerCase()) as ImportTypes[]; - const importsIsAny = importTypes.includes('any'); - - const fileDir = path.dirname(path.join(base, file.path)); - const processingFilePath = file.path; - console.log(`Processing => ${processingFilePath}`); - let source: Collection; - try { - source = j(file.source); - } catch (e) { - console.error(`Failed to parse ${processingFilePath}, will skip`); - console.error(e); - } - - const imports = source.find(j.ImportDeclaration) - imports.forEach((x) => { - const importPath = x.value.source.value as string; - const importPathPrefix = `${importPath.padEnd(60, ' ')} =>`; - - const pathInfo = path.parse(importPath); - const hasNoExt = pathInfo.ext === ''; - let filenameFromDir: string; - if(tfIsAny || (hasNoExt && transformFrom.includes('none')) || (transformFrom.includes(pathInfo.ext.replace('.', '').toLocaleLowerCase()))) { - const pathFull = path.join(fileDir, importPath); - const dir = path.dirname(pathFull); - const normalExt = pathInfo.ext.replace('.', ''); - let isRelative: boolean | undefined; - let dirExists: boolean | undefined; - let fileExists: boolean | undefined; - try { - // is dir path real? - fs.realpathSync(dir); - dirExists = true; - } catch (e) { - isRelative = false; - dirExists = false; - } - - if(isRelative === undefined) { - // if extension is none we need to check for any file in dir with this name - if(hasNoExt) { - const dirFiles = fs.readdirSync(dir) - filenameFromDir = dirFiles.find(x => path.parse(x).name === pathInfo.name); - if(filenameFromDir === undefined) { - fileExists = false; - isRelative = false; - } else { - fileExists = true; - isRelative = true; - } - } else { - try { - // does a file exist? - fs.realpathSync(pathFull); - isRelative = true; - } catch (e) { - isRelative = false; - fileExists = false; - } - } - } - - if(!importsIsAny) { - if(!isRelative && !importTypes.includes('alias')) { - console.log(`${importPathPrefix} Import looks like an alias ${dirExists ? '(dir exists, file does not)' : '(dir does not exist)'} but import types does specify alias`); - return; - } else if(isRelative && !importTypes.includes('relative')) { - console.log(`${pathFull} => Import is relative but import types does not specify relative`); - return; - } - } - - // determine transformTo - - // if there is only one TO then use it - let derivedTT: undefined | string = transformTo.length === 1 ? transformTo[0] : undefined; - if(derivedTT === undefined) { - // otherwise we find the TO by using the same index as the matching FROM extension type - const tfIndex = transformFrom.findIndex((x) => { - if(x === 'any') { - return true; - } - if(x === 'none' && hasNoExt) { - return true; - } - if(x === normalExt) { - return true; - } - }); - if(tfIndex === -1) { - console.warn(`${importPathPrefix} did not match a Transform From type. Will not transform`); - return; - } - derivedTT = transformTo[tfIndex]; - } - - let transformPrefix = ` --> ${hasNoExt ? '(None)' : normalExt} TO ${derivedTT} <--`; - - let transformedImport: string; - - switch(derivedTT) { - case 'none': - if(hasNoExt) { - console.log(`${importPathPrefix} ${transformPrefix} => Import already has no extension, nothing to do`); - return; - } - transformedImport = combineDirFile(pathInfo.dir, pathInfo.name); // path.join(pathInfo.root, pathInfo.dir, pathInfo.name); - break; - case 'any': - if(hasNoExt && filenameFromDir) { - transformedImport = filenameFromDir; - } else { - const otherFilename = fs.readdirSync(dir).find(x => { - const pinfo = path.parse(x); - return pinfo.name === pathInfo.name && pinfo.ext !== pathInfo.ext; - }); - if(otherFilename === undefined) { - console.warn(`${importPathPrefix} ${transformPrefix} => Could not find another file in directory that had same name but different extension. Will not transform.`); - return; - } - transformedImport = combineDirFile(pathInfo.dir, otherFilename); // path.join(pathInfo.root, dir, otherFilename); - transformPrefix = `${transformPrefix} (${path.parse(otherFilename).ext})` - } - break; - default: - transformedImport = combineDirFile(pathInfo.dir, `${path.parse(filenameFromDir).name}.${derivedTT}`); // path.join(pathInfo.root, dir, pathInfo.name, `.${derivedTT}`); - break; - } - - console.log(`${importPathPrefix} ${transformPrefix} => Replacing with ${transformedImport}`); - j(x).replaceWith( - j.importDeclaration( - x.node.specifiers, - j.stringLiteral(transformedImport) - ) - ); - - } else { - console.log(`${importPathPrefix} Import did not match transformFrom ('${tf}')`); - return; - } - }); - - /** - * Early exit condition - * ----- - * It is often good practice to exit early and return the original source file - * if it does not contain code relevant to the codemod. - * See this page for more information: - * https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/your-first-codemod#output - */ - // if (/* Some condition here */ true) { - // return file.source; - // } - - /** - * Codemod logic goes here 👇 - * ----- - * This is where the core logic for your codemod will go, - * consider grouping specific actions into 'motions' and running them in sequence - * - * See this page for more information: - * https://codeshiftcommunity.github.io/CodeshiftCommunity/docs/authoring#motions - */ - //source.findVariableDeclarators('foo').renameTo('bar'); - - /** - * Return your modified AST here 👇 - * ----- - * This is where your modified AST will be transformed back into a string - * and written back to the file. - */ - return source.toSource(options.printOptions); -} - -const combineDirFile = (dir: string, file: string) => { - if(dir === '') { - return file; - } - return `${dir}${path.sep}${file}`; -} diff --git a/docsite/docs/installation/installation.md b/docsite/docs/installation/installation.md index 41e6ba59..46c031cd 100644 --- a/docsite/docs/installation/installation.md +++ b/docsite/docs/installation/installation.md @@ -16,6 +16,7 @@ git clone https://github.com/FoxxMD/multi-scrobbler.git . cd multi-scrobbler nvm use # optional, to set correct Node version npm install +npm run docs:install && npm run build npm run start ``` diff --git a/docsite/docusaurus.config.ts b/docsite/docusaurus.config.ts index 09329498..42bd1fd9 100644 --- a/docsite/docusaurus.config.ts +++ b/docsite/docusaurus.config.ts @@ -12,7 +12,7 @@ const config: Config = { url: 'https://foxxmd.github.io', // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' - baseUrl: '/multi-scrobbler', + baseUrl: process.env.DOCS_BASE !== undefined && process.env.DOCS_BASE !== '' ? process.env.DOCS_BASE : '/docs', // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. @@ -21,7 +21,7 @@ const config: Config = { trailingSlash: false, - onBrokenLinks: 'throw', + onBrokenLinks: 'warn', onBrokenMarkdownLinks: 'warn', // Even if you don't use internalization, you can use this field to set useful @@ -105,6 +105,11 @@ const config: Config = { label: 'GitHub', position: 'right', }, + { + href: 'https://foxxmd.github.io/multi-scrobbler/', + label: 'Website', + position: 'right', + }, ], }, footer: { @@ -119,31 +124,18 @@ const config: Config = { }, { label: 'Installation', - to: '/docs/installation', + to: 'docs/installation', }, { label: 'Configuration', - to: '/docs/configuration', - }, - ], - }, -/* { - title: 'Community', - items: [ - { - label: 'Stack Overflow', - href: 'https://stackoverflow.com/questions/tagged/docusaurus', - }, - { - label: 'Discord', - href: 'https://discordapp.com/invite/docusaurus', + to: 'docs/configuration', }, { - label: 'Twitter', - href: 'https://twitter.com/docusaurus', + label: 'Development', + to: 'docs/development/dev-common', }, ], - },*/ + }, { title: 'More', items: [ @@ -151,6 +143,10 @@ const config: Config = { label: 'GitHub', href: 'https://github.com/foxxmd/multi-scrobbler', }, + { + label: 'Website', + href: 'https://foxxmd.github.io/multi-scrobbler/', + }, ], }, ], @@ -160,6 +156,11 @@ const config: Config = { theme: themes.themes.github, darkTheme: themes.themes.dracula, }, + colorMode: { + defaultMode: 'dark', + disableSwitch: false, + respectPrefersColorScheme: false, + }, } satisfies Preset.ThemeConfig, }; diff --git a/package-lock.json b/package-lock.json index 7b72e889..fead4ad6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,14 +114,13 @@ "msw": "^2.1.2", "nodemon": "^3.0.3", "ts-essentials": "^9.1.2", - "typedoc": "^0.25", "typescript": "^5.3.3", "typescript-eslint": "^7.0.1", "typescript-json-schema": "~0.55", "vite": "^5.2.12" }, "engines": { - "node": ">=18.0.0", + "node": ">=18.19.1", "npm": ">=9.1.0" } }, @@ -3012,12 +3011,6 @@ "node": ">=8" } }, - "node_modules/ansi-sequence-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", - "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", - "dev": true - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6445,12 +6438,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -6661,12 +6648,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "dev": true - }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -6698,18 +6679,6 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "devOptional": true }, - "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "dev": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -9202,18 +9171,6 @@ "node": ">=8" } }, - "node_modules/shiki": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", - "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", - "dev": true, - "dependencies": { - "ansi-sequence-parser": "^1.1.0", - "jsonc-parser": "^3.2.0", - "vscode-oniguruma": "^1.7.0", - "vscode-textmate": "^8.0.0" - } - }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -9968,51 +9925,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typedoc": { - "version": "0.25.13", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz", - "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==", - "dev": true, - "dependencies": { - "lunr": "^2.3.9", - "marked": "^4.3.0", - "minimatch": "^9.0.3", - "shiki": "^0.14.7" - }, - "bin": { - "typedoc": "bin/typedoc" - }, - "engines": { - "node": ">= 16" - }, - "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x" - } - }, - "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/typedoc/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -10379,18 +10291,6 @@ "picocolors": "^1.0.0" } }, - "node_modules/vscode-oniguruma": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", - "dev": true - }, - "node_modules/vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", - "dev": true - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b2a16b30..e19c3cff 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,17 @@ "schema-aio": "typescript-json-schema src/backend/tsconfig.json AIOConfig --out src/backend/common/schema/aio.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage", "schema-aiosource": "typescript-json-schema src/backend/tsconfig.json AIOSourceConfig --out src/backend/common/schema/aio-source.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage", "schema-aioclient": "typescript-json-schema src/backend/tsconfig.json AIOClientConfig --out src/backend/common/schema/aio-client.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage", - "typedoc": "typedoc", "circular": "madge --circular --extensions ts src/index.ts", "test": "npm run -s test:backend", "test:backend": "mocha --reporter spec --recursive src/backend/tests/**/*.test.ts", - "fileEndings": "jscodeshift --transformFrom js --transformTo none --importTypes relative --extensions=ts --parser tsx --transform codeshift/transform.ts src/backend", "dev": "nodemon -w src/backend -x tsx src/backend/index.ts", "start": "NODE_ENV=production tsx src/backend/index.ts", - "build:frontend": "APP_VERSION=$npm_package_version vite build", + "build:frontend": "vite build", "build:backend": "tsc -p src/backend", - "build": "npm run -s build:backend && npm run -s build:frontend", + "build": "npm run -s build:backend && npm run -s build:frontend && npm run -s docs:build", + "docs:install": "cd docsite && npm ci", + "docs:start": "cd docsite && npm start", + "docs:build": "cd docsite && npm run build", "postinstall": "patch-package" }, "exports": { @@ -148,7 +149,6 @@ "msw": "^2.1.2", "nodemon": "^3.0.3", "ts-essentials": "^9.1.2", - "typedoc": "^0.25", "typescript": "^5.3.3", "typescript-eslint": "^7.0.1", "typescript-json-schema": "~0.55", diff --git a/src/backend/server/index.ts b/src/backend/server/index.ts index 822f53aa..90baa9db 100644 --- a/src/backend/server/index.ts +++ b/src/backend/server/index.ts @@ -6,7 +6,9 @@ import express from 'express'; import session from 'express-session'; import { PassThrough } from "node:stream"; import passport from 'passport'; +import path from "path"; import ViteExpress from "vite-express"; +import { projectDir } from "../common/index.js"; import { getRoot } from "../ioc.js"; import { getAddress, parseBool } from "../utils.js"; import { setupApi } from "./api.js"; @@ -72,6 +74,8 @@ export const initServer = async (parentLogger: Logger, appLoggerStream: PassThro } } + app.use('/docs', express.static(path.resolve(projectDir, `./docsite/build`))); + if(process.env.USE_HASH_ROUTER === undefined) { process.env.USE_HASH_ROUTER = root.get('isSubPath'); } diff --git a/src/client/App.tsx b/src/client/App.tsx index b1c449e2..9ef8da7d 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -6,6 +6,8 @@ import { } from "react-router-dom"; import {connect, ConnectedProps, Provider} from 'react-redux' import './App.css'; +import CopyToClipboard from "./components/CopyToClipboard"; +import ExternalLink from "./components/ExternalLink"; import {store} from './store'; import Dashboard from "./dashboard/dashboard"; import RecentPage from "./recent/RecentPage"; @@ -16,7 +18,7 @@ import {useEventSource, useEventSourceListener} from "@react-nano/use-event-sour import Version from "./Version"; function NoMatch() { - let location = useLocation(); + const location = useLocation(); return (
@@ -25,22 +27,52 @@ function NoMatch() { ); } +// https://tailwindflex.com/@sienna/copy-code-block +function MissingDocs() { + return ( +
+
Oops! You need to build docs first. Run the following commands to build:
+ + + + $ + + + + + npm run docs:install && npm run docs:build + + + + + + +
+ ); +} + const routes: RouteObject[] = [ { path: "/", - element: , + element: , }, { path: "/recent", - element: , + element: , }, { path: "/scrobbled", - element: , + element: , }, { path: "/dead", - element: , + element: , + }, + { + path: "/docs", + element: }, { path: "*", @@ -93,18 +125,27 @@ function App() {
-
- - -
+
+ + +
); diff --git a/src/client/Version.tsx b/src/client/Version.tsx index 0d3bd125..bc91880d 100644 --- a/src/client/Version.tsx +++ b/src/client/Version.tsx @@ -18,8 +18,8 @@ const Version = () => { data = undefined, } = useGetVersionQuery(undefined); - return - Multi Scrobbler {data === undefined ? null : `${data.version}`} + return + {data === undefined ? null : `${data.version}`} ; } diff --git a/src/client/components/CopyToClipboard.tsx b/src/client/components/CopyToClipboard.tsx new file mode 100644 index 00000000..55519702 --- /dev/null +++ b/src/client/components/CopyToClipboard.tsx @@ -0,0 +1,30 @@ +import clsx from "clsx"; + +export interface Props { + classNames?: string[] + style?: object + text?: string +} + +const defaultStyle = {} +const defaultClassNames: string[] = ['shrink-0', 'h-5', 'w-5', 'transition', 'text-gray-500', 'hover:text-white', 'cursor-pointer']; + +const CopyToClipboard = (props: Props) => { + const {classNames = [], style = defaultStyle, text } = props; + const classes = defaultClassNames; + clsx(classes.concat(classNames)) + return ( + {navigator.clipboard.writeText(text)}} + className={clsx(classes.concat(classNames))} style={style} + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + + + + +) +} + +export default CopyToClipboard; diff --git a/src/client/components/ExternalLink.tsx b/src/client/components/ExternalLink.tsx new file mode 100644 index 00000000..a05e3aee --- /dev/null +++ b/src/client/components/ExternalLink.tsx @@ -0,0 +1,24 @@ +import {PropsWithChildren} from "react"; +import clsx from "clsx"; + +export interface TooltipProps { + classNames?: string[] + style?: object +} + +const defaultStyle = {marginLeft: '2px', display: 'inline-block'}; + +const ExternalLink = (props: PropsWithChildren) => { + const {classNames = [], style = defaultStyle } = props; + const classes = []; + clsx(classes.concat(classNames)) + return ( + + ) +} + +export default ExternalLink; -- 2.51.2