From 4f5b2bae03f3b836a60c8324d1c994128633bc8f Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Sun, 31 Mar 2024 10:07:27 +0900 Subject: [PATCH 01/15] move tsconfig --- package.json | 1 + tsconfig.json | 35 ----------------------------------- web/tsconfig.json | 15 ++++++++++++++- 3 files changed, 15 insertions(+), 36 deletions(-) delete mode 100644 tsconfig.json diff --git a/package.json b/package.json index 742924e..23620a1 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "description": "https://slides.ihate.work", "scripts": { "wrangler": "wrangler", + "dev": "npm run --workspace=web dev", "test": "echo \"Error: no test specified\" && exit 1" }, "engines": { diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index a18214b..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "isolatedModules": true, - "jsx": "preserve", - "module": "esnext", - "moduleResolution": "node", - "noEmit": true, - "target": "esnext", - "skipLibCheck": true, - "typeRoots": [ - "typings", - "node_modules/@types" - ], - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "incremental": true - }, - "exclude": [ - "node_modules", - "scripts", - "coverage", - "build", - "__test__", - "bin", - "jest.config.js" - ], - "include": [ - "pages", - "src" - ] -} diff --git a/web/tsconfig.json b/web/tsconfig.json index 3c43903..f8b10d2 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,3 +1,16 @@ { - "extends": "../tsconfig.json" + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "esnext", + "jsx": "preserve", + "skipLibCheck": true, + "target": "esnext", + "isolatedModules": true, + "moduleResolution": "node", + "noEmit": true, + "lib": ["dom", "dom.iterable", "esnext"], + "incremental": true + }, + "exclude": ["node_modules", "scripts", "coverage", "build", "__test__", "bin", "jest.config.js"], + "include": ["pages", "src"] } From b43437865f4803ba4ccad7ca0a6264bdfd4f1da4 Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Sun, 31 Mar 2024 15:58:59 +0900 Subject: [PATCH 02/15] wip --- core/package.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 core/package.json diff --git a/core/package.json b/core/package.json new file mode 100644 index 0000000..b551e82 --- /dev/null +++ b/core/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ihate-work/slides-core", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} From dae0a914dc5bf19f70cb381bd699ab5b18803491 Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Sun, 31 Mar 2024 17:33:47 +0900 Subject: [PATCH 03/15] wip --- package-lock.json | 9 +++-- package.json | 3 ++ web/jest.config.js | 21 ++++++++--- web/package.json | 6 +-- web/src/core/GistSource.spec.ts | 12 ++++++ web/src/core/GistSource.ts | 53 ++++++++++++++------------- web/src/core/internal-url-provider.ts | 4 ++ web/src/routes/url-rewrite.ts | 31 +++++----------- 8 files changed, 80 insertions(+), 59 deletions(-) create mode 100644 web/src/core/GistSource.spec.ts create mode 100644 web/src/core/internal-url-provider.ts diff --git a/package-lock.json b/package-lock.json index 266f9ec..b66dc7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,14 @@ "./web" ], "devDependencies": { + "@types/jest": "^29", "@types/node": "^20", "eslint-plugin-import": "^2.29.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "gts": "^5.2.0", + "jest": "^29", + "ts-jest": "^29", "typescript": "^5.4", "wrangler": "^3.36.0" }, @@ -14957,7 +14960,7 @@ "devDependencies": { "@next/bundle-analyzer": "^14", "@types/debug": "^4.1.12", - "@types/jest": "^29", + "@types/jest": "*", "@types/lodash-es": "^4.17.12", "@types/node": "*", "@types/react": "^18.2.45", @@ -14965,11 +14968,11 @@ "@types/reveal.js": "^5.0.1", "autoprefixer": "^10", "gts": "*", - "jest": "^29", + "jest": "*", "postcss": "^8.4.32", "sass": "^1.69.5", "tailwindcss": "^3.3.6", - "ts-jest": "^29", + "ts-jest": "*", "typescript": "*", "vercel": "^33.6", "wrangler": "*" diff --git a/package.json b/package.json index 23620a1..bac7a76 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,14 @@ }, "dependencies": {}, "devDependencies": { + "@types/jest": "^29", "@types/node": "^20", "eslint-plugin-import": "^2.29.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "gts": "^5.2.0", + "jest": "^29", + "ts-jest": "^29", "typescript": "^5.4", "wrangler": "^3.36.0" }, diff --git a/web/jest.config.js b/web/jest.config.js index a868f92..a6d10a7 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -1,7 +1,7 @@ module.exports = { preset: 'ts-jest/presets/js-with-ts', roots: ['src', 'test'], - transformIgnorePatterns: ['/node_modules/.*\\.js', '/build/.*\\.js'], + transformIgnorePatterns: ['node_modules/.*\\.js', '/build/.*\\.js'], testMatch: ['**/*\\.(spec|test)\\.(ts|js|tsx|jsx)'], collectCoverageFrom: ['src/**/*.(ts|tsx)', '!out/', '!build/', '!**/node_modules', '!/coverage'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], @@ -11,11 +11,20 @@ module.exports = { '/test/mocks/resolves-to-path.json', '\\.(css|less|scss|sass)$': '/test/mocks/resolves-to-path.json', }, - globals: { - 'ts-jest': { - tsconfig: { - jsx: 'react', + transform: { + '^.+\\.(ts|tsx)$': [ + 'ts-jest', + { + 'ts-jest': { + tsconfig: { + isolatedModules: true, + jsx: 'react', + diagnostics: { + exclude: ['**'], + }, + }, + }, }, - }, + ], }, }; diff --git a/web/package.json b/web/package.json index 98ff2e4..2a9483e 100644 --- a/web/package.json +++ b/web/package.json @@ -50,7 +50,7 @@ "devDependencies": { "@next/bundle-analyzer": "^14", "@types/debug": "^4.1.12", - "@types/jest": "^29", + "@types/jest": "*", "@types/lodash-es": "^4.17.12", "@types/node": "*", "@types/react": "^18.2.45", @@ -58,11 +58,11 @@ "@types/reveal.js": "^5.0.1", "autoprefixer": "^10", "gts": "*", - "jest": "^29", + "jest": "*", "postcss": "^8.4.32", "sass": "^1.69.5", "tailwindcss": "^3.3.6", - "ts-jest": "^29", + "ts-jest": "*", "typescript": "*", "vercel": "^33.6", "wrangler": "*" diff --git a/web/src/core/GistSource.spec.ts b/web/src/core/GistSource.spec.ts new file mode 100644 index 0000000..e05d3cf --- /dev/null +++ b/web/src/core/GistSource.spec.ts @@ -0,0 +1,12 @@ +import { parseGistUrl } from './GistSource'; + +describe('GistSource', () => { + describe('parseGistUrl', () => { + it('should parse gist url', () => { + for (const url of []) { + const parsed = parseGistUrl(new URL(url)); + expect(parsed).toBeTruthy(); + } + }); + }); +}); diff --git a/web/src/core/GistSource.ts b/web/src/core/GistSource.ts index 033b9f8..1f80846 100644 --- a/web/src/core/GistSource.ts +++ b/web/src/core/GistSource.ts @@ -2,18 +2,34 @@ import { Octokit } from '@octokit/rest'; import { SlideBundle } from './SlideBundle'; import { SourceError } from './errors'; import { isUrl } from './url-loader'; +import { InternalUrlProvider } from './internal-url-provider'; interface GistSourceLocator { + rawUrl: string; ownerId: string; gistId: string; revisionId?: string; } -function parseUrl(u: URL): GistSourceLocator | null { +export function buildGistSource(url: URL): GistSource | null { + const l = parseGistUrl(url); + return l && new GistSource(l); +} + +/** + * - https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8 + * => page: gist + * - https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/4676f49e32f95fd76549ce4f7330f0f7aa4662b3 + * => page: gist revision + * - https://gist.githubusercontent.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/raw/4676f49e32f95fd76549ce4f7330f0f7aa4662b3/0-rancher-zerotier-k3s-deployment.md + * => an raw file + */ +export function parseGistUrl(u: URL): GistSourceLocator | null { const parts = u.pathname.split('/'); const [_empty, ownerId, gistId, revisionId, ...rest] = parts; if (gistId?.length === 32 && !revisionId) { return { + rawUrl: u.toString(), ownerId, gistId, }; @@ -22,42 +38,27 @@ function parseUrl(u: URL): GistSourceLocator | null { return null; } -export class GistSource { - static isGistUrl(url: string): boolean { - return isUrl(url) && !!parseUrl(new URL(url)); - } - - readonly locator: Readonly; - - /** - * @param gistUrl like: - * - https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8 - * => page: gist - * - https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/4676f49e32f95fd76549ce4f7330f0f7aa4662b3 - * => page: gist revision - * - https://gist.githubusercontent.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/raw/4676f49e32f95fd76549ce4f7330f0f7aa4662b3/0-rancher-zerotier-k3s-deployment.md - * => an raw file - * @param client - */ +class GistSource implements InternalUrlProvider { constructor( - readonly gistUrl: string, + readonly locator: Readonly, private readonly client = new Octokit(), - ) { - this.locator = parseUrl(new URL(gistUrl))!; - } + ) {} get fetchKey(): string { - return `gistId=${this.gistUrl}`; + return `gistUrl=${this.locator.rawUrl}`; } - get pageUrl(): string { + asInternalPageUrl(): string { const parsed = this.locator; + return `/gist/${parsed.ownerId}/${parsed.gistId}`; + } - return `https://gist.github.com/${parsed.ownerId}/${parsed.gistId}`; + asUpstreamUrl(): string { + return this.locator.rawUrl; } /** - * @return a asset bundle fetched from github + * @return a asset bundle fetched from GitHub */ async fetchSource(): Promise { const res = await this.client.rest.gists.get({ diff --git a/web/src/core/internal-url-provider.ts b/web/src/core/internal-url-provider.ts new file mode 100644 index 0000000..ddc3ed1 --- /dev/null +++ b/web/src/core/internal-url-provider.ts @@ -0,0 +1,4 @@ +export interface InternalUrlProvider { + asInternalPageUrl(): string; + asUpstreamUrl(): string; +} diff --git a/web/src/routes/url-rewrite.ts b/web/src/routes/url-rewrite.ts index b3d3f6d..e41fa8a 100644 --- a/web/src/routes/url-rewrite.ts +++ b/web/src/routes/url-rewrite.ts @@ -1,31 +1,20 @@ import { Route } from 'next'; -import { GistSource } from '../core/GistSource'; -import { SlideBundle } from '../core/SlideBundle'; -import { FetchTextSource } from '../core/FetchTextSource'; -import { isUrl } from '../core/url-loader'; +import { parseGistUrl, buildGistSource } from '../core/GistSource'; /** * For source URLs we have dedicated support, redirect to appropriate page route */ export function rewriteUrlToRoute(url: string): null | Route | Error { - if (!isUrl(url)) { - return new Error('Invalid URL'); - } - if (GistSource.isGistUrl(url)) { - const parsed = new GistSource(url).locator; - if (parsed?.gistId && !parsed?.revisionId) { - return `/gist/${parsed.ownerId}/${parsed.gistId}`; - } - } - return null; -} + let u, t; -export function rewriteSrcToRoute(s: SlideBundle): null | Route | Error { - if (s.gistSource instanceof GistSource) { - return `/gist/${s.gistSource.locator.ownerId}/${s.gistSource.locator.gistId}`; + try { + u = new URL(url); + } catch (e) { + return e as Error; } - if (s.fetchTextSource instanceof FetchTextSource) { - return `/markdown?markdownUrl=${encodeURIComponent(s.fetchTextSource.url)}`; + + if ((t = parseGistUrl(u))) { + return buildGistSource(u)!.asInternalPageUrl(); } - return new Error(`unknown source type: ${JSON.stringify(s)}`); + return null; } From fd9b448d401ff30ddf00f38eaa480da4d70a3da6 Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Sun, 31 Mar 2024 17:49:35 +0900 Subject: [PATCH 04/15] fix test --- web/src/core/GistSource.spec.ts | 7 +++- web/src/core/GistSource.ts | 41 ++++++++++++++----- .../__snapshots__/GistSource.spec.ts.snap | 28 +++++++++++++ 3 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 web/src/core/__snapshots__/GistSource.spec.ts.snap diff --git a/web/src/core/GistSource.spec.ts b/web/src/core/GistSource.spec.ts index e05d3cf..eb21b8d 100644 --- a/web/src/core/GistSource.spec.ts +++ b/web/src/core/GistSource.spec.ts @@ -3,9 +3,14 @@ import { parseGistUrl } from './GistSource'; describe('GistSource', () => { describe('parseGistUrl', () => { it('should parse gist url', () => { - for (const url of []) { + for (const url of [ + 'https://gist.githubusercontent.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/raw/4676f49e32f95fd76549ce4f7330f0f7aa4662b3/0-rancher-zerotier-k3s-deployment.md', + 'https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8', + 'https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/4676f49e32f95fd76549ce4f7330f0f7aa4662b3', + ]) { const parsed = parseGistUrl(new URL(url)); expect(parsed).toBeTruthy(); + expect(parsed).toMatchSnapshot(`parseGistUrl(${url})`); } }); }); diff --git a/web/src/core/GistSource.ts b/web/src/core/GistSource.ts index 1f80846..b15b3d5 100644 --- a/web/src/core/GistSource.ts +++ b/web/src/core/GistSource.ts @@ -1,7 +1,6 @@ import { Octokit } from '@octokit/rest'; import { SlideBundle } from './SlideBundle'; import { SourceError } from './errors'; -import { isUrl } from './url-loader'; import { InternalUrlProvider } from './internal-url-provider'; interface GistSourceLocator { @@ -9,6 +8,7 @@ interface GistSourceLocator { ownerId: string; gistId: string; revisionId?: string; + filename?: string; } export function buildGistSource(url: URL): GistSource | null { @@ -17,24 +17,45 @@ export function buildGistSource(url: URL): GistSource | null { } /** - * - https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8 - * => page: gist - * - https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/4676f49e32f95fd76549ce4f7330f0f7aa4662b3 - * => page: gist revision - * - https://gist.githubusercontent.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/raw/4676f49e32f95fd76549ce4f7330f0f7aa4662b3/0-rancher-zerotier-k3s-deployment.md - * => an raw file */ export function parseGistUrl(u: URL): GistSourceLocator | null { const parts = u.pathname.split('/'); - const [_empty, ownerId, gistId, revisionId, ...rest] = parts; - if (gistId?.length === 32 && !revisionId) { + const [_empty, ownerId, gistId, ...rest] = parts; + if (!(ownerId && gistId?.length === 32)) { + return null; + } + if (rest.length === 3 && rest[0] === 'raw') { + // a raw file like + // https://gist.githubusercontent.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/raw/4676f49e32f95fd76549ce4f7330f0f7aa4662b3/0-rancher-zerotier-k3s-deployment.md + return { + rawUrl: u.toString(), + ownerId, + gistId, + revisionId: rest[1], + filename: rest[2], + }; + } + + if (rest.length === 1) { + // a gist like + // https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8 + return { + rawUrl: u.toString(), + ownerId, + gistId, + revisionId: rest[0], + }; + } + + if (!rest.length) { + // a gist revision like: + // https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/4676f49e32f95fd76549ce4f7330f0f7aa4662b3 return { rawUrl: u.toString(), ownerId, gistId, }; } - // TODO: support more patterns return null; } diff --git a/web/src/core/__snapshots__/GistSource.spec.ts.snap b/web/src/core/__snapshots__/GistSource.spec.ts.snap new file mode 100644 index 0000000..3d6e53a --- /dev/null +++ b/web/src/core/__snapshots__/GistSource.spec.ts.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GistSource parseGistUrl should parse gist url: parseGistUrl(https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8) 1`] = ` +{ + "gistId": "2ae304016d8c25b09a68a9221f6c07c8", + "ownerId": "jokester", + "rawUrl": "https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8", +} +`; + +exports[`GistSource parseGistUrl should parse gist url: parseGistUrl(https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/4676f49e32f95fd76549ce4f7330f0f7aa4662b3) 1`] = ` +{ + "gistId": "2ae304016d8c25b09a68a9221f6c07c8", + "ownerId": "jokester", + "rawUrl": "https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/4676f49e32f95fd76549ce4f7330f0f7aa4662b3", + "revisionId": "4676f49e32f95fd76549ce4f7330f0f7aa4662b3", +} +`; + +exports[`GistSource parseGistUrl should parse gist url: parseGistUrl(https://gist.githubusercontent.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/raw/4676f49e32f95fd76549ce4f7330f0f7aa4662b3/0-rancher-zerotier-k3s-deployment.md) 1`] = ` +{ + "filename": "0-rancher-zerotier-k3s-deployment.md", + "gistId": "2ae304016d8c25b09a68a9221f6c07c8", + "ownerId": "jokester", + "rawUrl": "https://gist.githubusercontent.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/raw/4676f49e32f95fd76549ce4f7330f0f7aa4662b3/0-rancher-zerotier-k3s-deployment.md", + "revisionId": "4676f49e32f95fd76549ce4f7330f0f7aa4662b3", +} +`; From 33570f5ed24052267ee82c719aa4a99e0cc7037e Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Sun, 31 Mar 2024 18:41:44 +0900 Subject: [PATCH 05/15] wip --- web/pages/gist/[...pathSegments].tsx | 4 +-- web/src/components/external-src-input.tsx | 35 +++++++++++----------- web/src/components/useRenderSwr.tsx | 7 ++++- web/src/core/GistSource.ts | 36 +++++++++++++++-------- web/src/core/url-loader.ts | 17 ++++------- 5 files changed, 55 insertions(+), 44 deletions(-) diff --git a/web/pages/gist/[...pathSegments].tsx b/web/pages/gist/[...pathSegments].tsx index 655887b..c8fb1b8 100644 --- a/web/pages/gist/[...pathSegments].tsx +++ b/web/pages/gist/[...pathSegments].tsx @@ -1,7 +1,7 @@ import debug from 'debug'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; -import { GistSource } from '../../src/core/GistSource'; +import { GistSource, GistSourceLocator } from '../../src/core/GistSource'; import useSWR from 'swr'; import { GistTextarea } from '../../src/gist/gist-textarea'; import { PageContainer, PageHeader } from '../../src/layouts'; @@ -54,7 +54,7 @@ export default function GistPresentPage() { try { const url = new URL(location.href); url.pathname = url.pathname.slice('/gist'.length); - const src = new GistSource(url.toString()); + const src = new GistSource(url); setSrc(src); } catch (e: any) { const err = new Error(e?.message || 'error', { diff --git a/web/src/components/external-src-input.tsx b/web/src/components/external-src-input.tsx index 6805ef6..c0feb13 100644 --- a/web/src/components/external-src-input.tsx +++ b/web/src/components/external-src-input.tsx @@ -1,17 +1,15 @@ -import { Button, Input, TextField } from '@mui/material'; +import { Button, IconButton, Input, TextField } from '@mui/material'; import clsx from 'clsx'; import { useState } from 'react'; -import { useToggle } from 'react-use'; -import { detectSourceUrlType, isUrl, loadExternalSourceUrl } from '../core/url-loader'; -import { GitHub } from '@mui/icons-material'; +import { detectSourceUrlType, isUrl } from '../core/url-loader'; +import { GitHub, Clear, Http } from '@mui/icons-material'; export function ExternalSourceInput(props: { onSubmit?(url: string): void }) { const [urlValue, setUrlValue] = useState(''); - const [loading, setLoading] = useToggle(false); - const detectedType = detectSourceUrlType(urlValue); + const urlType = detectSourceUrlType(urlValue); const onSubmit = () => { - if (!isUrl(urlValue)) { + if (urlType === 'unknown') { alert('Invalid URL'); return; } @@ -23,7 +21,7 @@ export function ExternalSourceInput(props: { onSubmit?(url: string): void }) {
setUrlValue(ev.target.value)} @@ -32,6 +30,13 @@ export function ExternalSourceInput(props: { onSubmit?(url: string): void }) { onSubmit(); } }} + InputProps={{ + endAdornment: urlValue && ( + setUrlValue('')}> + + + ), + }} />
@@ -41,19 +46,15 @@ export function ExternalSourceInput(props: { onSubmit?(url: string): void }) { variant="outlined" color="primary" onClick={onSubmit} - aria-busy={loading} - disabled={loading || !urlValue} + disabled={!urlValue} > { - loading ? ( - 'Loading...' - ) : !detectedType ? ( - 'Load' // but disabled - ) : detectedType === 'gist' ? ( + urlType === 'gist' ? ( <> - Load from Github Gist + Load from Gist   + - ) : detectedType === 'unknown' ? ( + ) : urlType === 'unknown' ? ( 'Load Raw URL' ) : ( 'Load' diff --git a/web/src/components/useRenderSwr.tsx b/web/src/components/useRenderSwr.tsx index 645b4b3..54b2d1c 100644 --- a/web/src/components/useRenderSwr.tsx +++ b/web/src/components/useRenderSwr.tsx @@ -2,15 +2,20 @@ import { SWRResponse } from 'swr'; import { ReactElement } from 'react'; import { extractErrorMessage } from '../utils'; +function _renderError(swrError: SWRResponse['error']): string | ReactElement { + return extractErrorMessage(swrError); +} + export function useRenderSwr( res: SWRResponse, onValue: (d: Data) => ReactElement, + renderError: typeof _renderError = _renderError, ): string | ReactElement { if (res.isLoading) { return 'Loading...'; } if (res.error) { - return extractErrorMessage(res.error); + return renderError(res.error); } return onValue(res.data!); } diff --git a/web/src/core/GistSource.ts b/web/src/core/GistSource.ts index b15b3d5..de5f54d 100644 --- a/web/src/core/GistSource.ts +++ b/web/src/core/GistSource.ts @@ -3,8 +3,8 @@ import { SlideBundle } from './SlideBundle'; import { SourceError } from './errors'; import { InternalUrlProvider } from './internal-url-provider'; -interface GistSourceLocator { - rawUrl: string; +export interface GistSourceLocator { + rawUrl: URL; ownerId: string; gistId: string; revisionId?: string; @@ -16,8 +16,6 @@ export function buildGistSource(url: URL): GistSource | null { return l && new GistSource(l); } -/** - */ export function parseGistUrl(u: URL): GistSourceLocator | null { const parts = u.pathname.split('/'); const [_empty, ownerId, gistId, ...rest] = parts; @@ -28,7 +26,7 @@ export function parseGistUrl(u: URL): GistSourceLocator | null { // a raw file like // https://gist.githubusercontent.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/raw/4676f49e32f95fd76549ce4f7330f0f7aa4662b3/0-rancher-zerotier-k3s-deployment.md return { - rawUrl: u.toString(), + rawUrl: u, ownerId, gistId, revisionId: rest[1], @@ -40,7 +38,7 @@ export function parseGistUrl(u: URL): GistSourceLocator | null { // a gist like // https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8 return { - rawUrl: u.toString(), + rawUrl: u, ownerId, gistId, revisionId: rest[0], @@ -51,7 +49,7 @@ export function parseGistUrl(u: URL): GistSourceLocator | null { // a gist revision like: // https://gist.github.com/jokester/2ae304016d8c25b09a68a9221f6c07c8/4676f49e32f95fd76549ce4f7330f0f7aa4662b3 return { - rawUrl: u.toString(), + rawUrl: u, ownerId, gistId, }; @@ -59,23 +57,35 @@ export function parseGistUrl(u: URL): GistSourceLocator | null { return null; } -class GistSource implements InternalUrlProvider { +export class GistSource implements InternalUrlProvider { + readonly locator: Readonly; constructor( - readonly locator: Readonly, + locator: string | URL | GistSourceLocator, private readonly client = new Octokit(), - ) {} + ) { + if (typeof locator === 'string') { + this.locator = parseGistUrl(new URL(locator))!; + } else if (locator instanceof URL) { + this.locator = parseGistUrl(locator)!; + } else { + this.locator = locator; + } + if (!this.locator) { + throw new Error(`invalid github URL`); + } + } get fetchKey(): string { return `gistUrl=${this.locator.rawUrl}`; } asInternalPageUrl(): string { - const parsed = this.locator; - return `/gist/${parsed.ownerId}/${parsed.gistId}`; + const { rawUrl } = this.locator; + return `/gist${rawUrl.pathname}`; } asUpstreamUrl(): string { - return this.locator.rawUrl; + return this.locator.rawUrl.toString(); } /** diff --git a/web/src/core/url-loader.ts b/web/src/core/url-loader.ts index 82674e0..7a25ae3 100644 --- a/web/src/core/url-loader.ts +++ b/web/src/core/url-loader.ts @@ -1,23 +1,18 @@ import { SlideBundle } from './SlideBundle'; -import { GistSource } from './GistSource'; +import { GistSource, parseGistUrl } from './GistSource'; import { FetchTextSource } from './FetchTextSource'; export function isUrl(u: unknown): u is string { - try { - new URL(u as string); - return true; - } catch { - return false; - } + return typeof u === 'string' && URL.canParse(u); } -type SourceType = 'gist' | 'unknown' | null; +type SourceType = 'gist' | 'unknown' | 'invalid'; export function detectSourceUrlType(url: unknown): SourceType { if (!isUrl(url)) { - return null; + return 'invalid'; } - if (GistSource.isGistUrl(url)) { + if (parseGistUrl(new URL(url))) { return 'gist'; } return 'unknown'; @@ -27,7 +22,7 @@ export async function loadExternalSourceUrl(url: string): Promise { if (!isUrl(url)) { throw new Error('Invalid URL'); } - if (GistSource.isGistUrl(url)) { + if (parseGistUrl(new URL(url))) { const src = new GistSource(url); return await src.fetchSource(); } From da1e5c7c4db59f996aacb0e8fa5e45a2653e5bcd Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Sun, 31 Mar 2024 19:41:36 +0900 Subject: [PATCH 06/15] wip --- package-lock.json | 41 +++++++++++++++++++ web/package.json | 1 + web/pages/_app.tsx | 10 +++-- web/pages/_document.tsx | 8 ++-- web/pages/index.tsx | 48 ++++++++++++++++++----- web/src/components/external-src-input.tsx | 26 ++++++++++-- web/src/layouts/index.tsx | 4 +- web/src/layouts/theme.ts | 9 +++++ 8 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 web/src/layouts/theme.ts diff --git a/package-lock.json b/package-lock.json index b66dc7c..90ebdec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2238,6 +2238,46 @@ } } }, + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.169", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.169.tgz", + "integrity": "sha512-h6xe1K6ISKUbyxTDgdvql4qoDP6+q8ad5fg9nXQxGLUrIeT2jVrBuT/jRECSTufbnhzP+V5kulvYxaMfM8rEdA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": ">=5.15.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.15.14", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz", @@ -14934,6 +14974,7 @@ "@emotion/styled": "^11.11.0", "@jokester/ts-commonutil": "^0.5.0", "@mui/icons-material": "^5.15.0", + "@mui/lab": "^5.0.0-alpha.169", "@mui/material": "^5.15.0", "@next/eslint-plugin-next": "^14.0.4", "@octokit/core": "^5.1.0", diff --git a/web/package.json b/web/package.json index 2a9483e..75d7eb1 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "@emotion/styled": "^11.11.0", "@jokester/ts-commonutil": "^0.5.0", "@mui/icons-material": "^5.15.0", + "@mui/lab": "^5.0.0-alpha.169", "@mui/material": "^5.15.0", "@next/eslint-plugin-next": "^14.0.4", "@octokit/core": "^5.1.0", diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index f3f7831..0dc4000 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -5,6 +5,8 @@ import { DefaultMeta } from '../src/components/meta/default-meta'; import { SnackbarProvider } from 'notistack'; import Head from 'next/head'; import { useRevealPreload } from '../src/player/use-reveal-preload'; +import { ThemeProvider } from '@mui/material'; +import { globalTheme } from '../src/layouts/theme'; const CustomApp: React.FC & Partial> = (props) => { useRevealPreload(); @@ -15,12 +17,14 @@ const CustomApp: React.FC & Partial - - + + + + ); }; diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx index d902c51..2cf2db5 100644 --- a/web/pages/_document.tsx +++ b/web/pages/_document.tsx @@ -16,18 +16,16 @@ const stylesheetPreloads = [ ), ]; -const defaultStyleSheets = [ - // , -] as const; - export default function CustomDocument(): React.ReactElement { return ( + + {stylesheetPreloads} diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 25cbca9..6979be5 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { NextPage } from 'next'; import { useRouter } from 'next/router'; import { MarkdownHelp, PageContainer, PageFooter, PageHeader } from '../src/layouts'; @@ -6,12 +6,14 @@ import { ExternalSourceInput } from '../src/components/external-src-input'; import debug from 'debug'; import { rewriteUrlToRoute } from '../src/routes/url-rewrite'; import Link from 'next/link'; -import { Button } from '@mui/material'; +import { Box, Button, Divider, Tab, Tabs } from '@mui/material'; +import { TabContext, TabList, TabPanel } from '@mui/lab'; const logger = debug('pages:index'); function IndexPageContent() { const router = useRouter(); + const [activeTab, setActiveTab] = useState('0'); const onSourceUrlSubmit = (url: string) => { const nextPath = rewriteUrlToRoute(url); logger('onSourceUrlSubmit', url, nextPath); @@ -23,20 +25,46 @@ function IndexPageContent() { router.push(`/markdown?markdownUrl=${encodeURIComponent(url)}`); } }; + + const openTabContent = ( + <> +
+

Load from URL:

+ +
+ + ); + return ( -
- +
+ + + setActiveTab(newValue)}> + + + + + + {openTabContent} + + + + + + + + + + +
-

or

-
- - - + +
+

Input markdown text or file:  

-
diff --git a/web/src/components/external-src-input.tsx b/web/src/components/external-src-input.tsx index c0feb13..707960a 100644 --- a/web/src/components/external-src-input.tsx +++ b/web/src/components/external-src-input.tsx @@ -1,15 +1,20 @@ -import { Button, IconButton, Input, TextField } from '@mui/material'; +import { Button, ButtonGroup, IconButton, Input, TextField } from '@mui/material'; import clsx from 'clsx'; import { useState } from 'react'; import { detectSourceUrlType, isUrl } from '../core/url-loader'; import { GitHub, Clear, Http } from '@mui/icons-material'; +const demoUrls = { + gist1: 'https://gist.github.com/jokester/983bfc399f4d6e5d677774c054250a94', + githubRaw1: 'https://raw.githubusercontent.com/jokester/slides.ihate.work/main/README.md', +} as const; + export function ExternalSourceInput(props: { onSubmit?(url: string): void }) { const [urlValue, setUrlValue] = useState(''); const urlType = detectSourceUrlType(urlValue); const onSubmit = () => { - if (urlType === 'unknown') { + if (urlType === 'invalid') { alert('Invalid URL'); return; } @@ -55,13 +60,28 @@ export function ExternalSourceInput(props: { onSubmit?(url: string): void }) { ) : urlType === 'unknown' ? ( - 'Load Raw URL' + <> + Load from HTTP URL   + + ) : ( 'Load' ) // fallback }
+
+ Examples: +
+ + / + +
+
); } diff --git a/web/src/layouts/index.tsx b/web/src/layouts/index.tsx index bc22a10..43fdd6e 100644 --- a/web/src/layouts/index.tsx +++ b/web/src/layouts/index.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { Fragment, PropsWithChildren } from 'react'; export function PageContainer(props: PropsWithChildren) { - return
{props.children}
; + return
{props.children}
; } export function PageHeader() { @@ -13,7 +13,7 @@ export function PageHeader() { slides.ihate.work -

A site to present Markdown slides

+

Present Markdown slides


); diff --git a/web/src/layouts/theme.ts b/web/src/layouts/theme.ts new file mode 100644 index 0000000..7aca839 --- /dev/null +++ b/web/src/layouts/theme.ts @@ -0,0 +1,9 @@ +import { createTheme } from '@mui/material/styles'; + +export const globalTheme = createTheme({ + typography: { + button: { + textTransform: 'none', + }, + }, +}); From 1468659180ddca656b2cea4cdc2c4ce5666f321f Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Sun, 31 Mar 2024 22:27:16 +0900 Subject: [PATCH 07/15] update --- web/pages/404.tsx | 8 +- web/pages/about.tsx | 43 ++++------ web/pages/index.tsx | 10 +-- web/pages/local.tsx | 49 +++++++++++ web/pages/markdown.tsx | 39 ++++++--- web/src/core/SlideBundle.ts | 1 + web/src/gist/gist-textarea.tsx | 9 +- web/src/layouts/index.tsx | 36 ++++++-- web/src/markdown/markdown-form.tsx | 17 ---- web/src/markdown/markdown-textarea.tsx | 114 +++++++++++++++++-------- web/src/routes/bundle-storage.ts | 17 ++++ web/src/routes/url-rewrite.ts | 10 +-- 12 files changed, 228 insertions(+), 125 deletions(-) create mode 100644 web/pages/local.tsx create mode 100644 web/src/routes/bundle-storage.ts diff --git a/web/pages/404.tsx b/web/pages/404.tsx index 4e7f7f5..6f08999 100644 --- a/web/pages/404.tsx +++ b/web/pages/404.tsx @@ -1,5 +1,6 @@ import { useRouter } from 'next/router'; import { useEffect } from 'react'; +import { PageContainer, PageHeader } from '../src/layouts'; export default function NotFoundPage() { const router = useRouter(); @@ -9,5 +10,10 @@ export default function NotFoundPage() { }, 5e3); return () => clearTimeout(timer); }, [router]); - return
Page not found. You will be redirected to site root shortly.
; + return ( + + +
Page not found. You will be redirected to site root shortly.
+
+ ); } diff --git a/web/pages/about.tsx b/web/pages/about.tsx index c701f4c..51d0166 100644 --- a/web/pages/about.tsx +++ b/web/pages/about.tsx @@ -1,36 +1,27 @@ -import { ExampleLinks } from '../src/dummy/example-links'; import * as React from 'react'; import { NextPage } from 'next'; -import { useRouter } from 'next/router'; -import { OnlyInBrowser } from '../src/components/OnlyInBrowser'; -/** - * - */ -interface PageProps { - renderedAt: number; - renderedBy: string; +import { MarkdownHelp, PageContainer, CreditsFooter, PageHeader } from '../src/layouts'; + +function RevealjsHelp() { + return ( +
+ +
+ ); } -const AboutPage: NextPage = (props) => { - const query = useRouter().query; +const AboutPage: NextPage = (props) => { return ( - <> -

AboutPage in {__filename}

- {/**/} - {/**/} - - - - + + + + +
+ +
); }; -export const runtime = 'experimental-edge'; - -// @ts-ignore -AboutPage.DISABLED_getInitialProps = async (ctx) => ({ - renderedAt: 0 && Date.now(), - renderedBy: ctx.req ? 'server' : 'browser', -}); +// export const runtime = 'experimental-edge'; export default AboutPage; diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 6979be5..83d289d 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { NextPage } from 'next'; import { useRouter } from 'next/router'; -import { MarkdownHelp, PageContainer, PageFooter, PageHeader } from '../src/layouts'; +import { MarkdownHelp, PageContainer, CreditsFooter, PageHeader } from '../src/layouts'; import { ExternalSourceInput } from '../src/components/external-src-input'; import debug from 'debug'; import { rewriteUrlToRoute } from '../src/routes/url-rewrite'; @@ -29,7 +29,6 @@ function IndexPageContent() { const openTabContent = ( <>
-

Load from URL:

@@ -50,7 +49,7 @@ function IndexPageContent() { {openTabContent} - + @@ -62,11 +61,8 @@ function IndexPageContent() {
-
-

Input markdown text or file:  

-
- +
); } diff --git a/web/pages/local.tsx b/web/pages/local.tsx new file mode 100644 index 0000000..acc245f --- /dev/null +++ b/web/pages/local.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { SlideBundle } from '../src/core/SlideBundle'; +import { defaultSlideText } from '../src/markdown/markdown-form'; +import { DefaultMeta } from '../src/components/meta/default-meta'; +import { PageContainer, PageHeader } from '../src/layouts'; +import { ClearButton, MarkdownTextarea, OpenFileButton, StartPlaybackButton } from '../src/markdown/markdown-textarea'; +import { RevealSlidePlayer } from '../src/player/reveal-slide-player'; +import clsx from 'clsx'; + +export default function LocalMarkdownPage() { + const [text, setText] = useState(''); + const [playback, setPlayback] = useState(null); + const onTextChange = (newText: string, isManualEdit: boolean) => { + if (isManualEdit || !text || newText === text || text === defaultSlideText) { + setText(newText); + } else if (confirm('Overwrite current input?')) { + setText(newText); + } + }; + const onStartPlayback = () => { + setPlayback({ + slideText: text, + }); + }; + + if (!playback) { + return ( + + + +
+ {text ? ( + <> + + setText('')} /> + + ) : ( + <> + setText(v.slideText)} /> +   or type slide text + + )} +
+ +
+ ); + } + return setPlayback(null)} bundle={playback} />; +} diff --git a/web/pages/markdown.tsx b/web/pages/markdown.tsx index b145a4a..71a6481 100644 --- a/web/pages/markdown.tsx +++ b/web/pages/markdown.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import { DefaultMeta } from '../src/components/meta/default-meta'; -import { MarkdownForm, defaultSlideText } from '../src/markdown/markdown-form'; -import { MarkdownHelp, PageContainer, PageFooter, PageHeader } from '../src/layouts'; +import { defaultSlideText } from '../src/markdown/markdown-form'; +import { PageContainer, PageHeader } from '../src/layouts'; import { useAsyncEffect } from '@jokester/ts-commonutil/lib/react/hook/use-async-effect'; import { isUrl } from '../src/core/url-loader'; import { rewriteUrlToRoute } from '../src/routes/url-rewrite'; @@ -10,18 +10,22 @@ import debug from 'debug'; import { useRouter } from 'next/router'; import { SlideBundle } from '../src/core/SlideBundle'; import { RevealSlidePlayer } from '../src/player/reveal-slide-player'; +import { Button } from '@mui/material'; +import { MarkdownTextarea, StartPlaybackButton } from '../src/markdown/markdown-textarea'; const logger = debug('pages:markdown'); /** - * /markdown page - * for markdown input from textarea / file / general external URL + * @page /markdown + * 1. try to load Markdown text from external url + * 2. init MD form with loaded text + * 3. if no external URL specified, have user try random text or example slides */ -export default function MarkdownPage() { +export default function RemoteMarkdownPage() { const [text, setText] = useState(''); const [playback, setPlayback] = useState(null); const onTextChange = (newText: string, isManualEdit: boolean) => { - if (isManualEdit || !text || text === defaultSlideText) { + if (isManualEdit || !text || newText === text || text === defaultSlideText) { setText(newText); } else if (confirm('Overwrite current input?')) { setText(newText); @@ -41,12 +45,22 @@ export default function MarkdownPage() { if (!playback) { return ( <> - + - - -
+
+ {text ? ( + + ) : ( + <> + Type some markdown text to start , or   + + + )} +
+ ); @@ -54,7 +68,6 @@ export default function MarkdownPage() { return ( <> - setPlayback(null)} bundle={playback} /> ); @@ -73,12 +86,10 @@ export function useTextInitialize(onRawFetched: (x: string) => void) { } const markdownUrl = router.query.markdownUrl as string | undefined; if (!markdownUrl) { - onRawFetched(defaultSlideText); return; } if (!isUrl(markdownUrl)) { alert('Invalid URL'); - onRawFetched(defaultSlideText); return; } const redirect = rewriteUrlToRoute(markdownUrl); diff --git a/web/src/core/SlideBundle.ts b/web/src/core/SlideBundle.ts index dc5cacd..a0c0694 100644 --- a/web/src/core/SlideBundle.ts +++ b/web/src/core/SlideBundle.ts @@ -5,5 +5,6 @@ export interface SlideBundle { // markdown text slideText: string; fetchTextSource?: FetchTextSource; + localTextSource?: Blob; gistSource?: GistSource; } diff --git a/web/src/gist/gist-textarea.tsx b/web/src/gist/gist-textarea.tsx index af5e9bf..425eb64 100644 --- a/web/src/gist/gist-textarea.tsx +++ b/web/src/gist/gist-textarea.tsx @@ -11,12 +11,5 @@ export function GistTextarea(props: { onStart?(markdownText: string): void; }): ReactElement { const [text, setText] = useState(props.initialValue || props.bundle.slideText); - return ( - props.onStart?.(text)} - className={props.className} - /> - ); + return ; } diff --git a/web/src/layouts/index.tsx b/web/src/layouts/index.tsx index 43fdd6e..c45abbd 100644 --- a/web/src/layouts/index.tsx +++ b/web/src/layouts/index.tsx @@ -1,5 +1,7 @@ import Link from 'next/link'; import { Fragment, PropsWithChildren } from 'react'; +import { Help, Info, PresentToAll } from '@mui/icons-material'; +import { Button } from '@mui/material'; export function PageContainer(props: PropsWithChildren) { return
{props.children}
; @@ -8,18 +10,36 @@ export function PageContainer(props: PropsWithChildren) { export function PageHeader() { return ( <> -

- - slides.ihate.work +
+

+ +   slides.ihate.work + +

+    +

Present Markdown slides

+
+
+ + + -

-

Present Markdown slides

-
+ + + +
+
); } -export function PageFooter() { +export function CreditsFooter() { return (