From 3a948015b08e485964cffd30f3f24c110e66f4e2 Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Sun, 9 Aug 2026 08:50:11 +0200 Subject: [PATCH] feat(frontend): serve the documentation from the frontend --- README.md | 78 ++---- ...rends-auth.spec.ts => wizard-auth.spec.ts} | 6 +- apps/frontend/package.json | 6 + apps/frontend/src/Root.tsx | 34 +++ apps/frontend/src/axios-override.ts | 22 +- apps/frontend/src/docs/DocsApp.tsx | 45 ++++ apps/frontend/src/docs/DocsContent.tsx | 60 +++++ apps/frontend/src/docs/DocsSidebar.tsx | 47 ++++ apps/frontend/src/docs/docs.css | 196 +++++++++++++++ apps/frontend/src/docs/isPlainClick.ts | 9 + apps/frontend/src/docs/pages.ts | 79 ++++++ apps/frontend/src/docs/resolveDocUrl.test.ts | 95 ++++++++ apps/frontend/src/docs/resolveDocUrl.ts | 96 ++++++++ apps/frontend/src/docs/slugify.test.ts | 13 + apps/frontend/src/docs/slugify.ts | 14 ++ .../src/docs/transformDocHtml.test.ts | 49 ++++ apps/frontend/src/docs/transformDocHtml.ts | 53 ++++ apps/frontend/src/docs/useDocRoute.ts | 75 ++++++ apps/frontend/src/globals.d.ts | 8 + apps/frontend/src/index.tsx | 4 +- apps/frontend/src/modules.d.ts | 7 + apps/frontend/src/pages/App/Header.tsx | 63 ----- apps/frontend/src/route.ts | 28 +++ apps/frontend/src/shared/AppBar.tsx | 59 +++++ apps/frontend/src/shared/LinkExternal.tsx | 37 +++ .../Generic => shared}/ThemePicker.tsx | 4 +- apps/frontend/src/wizard/Header.tsx | 33 +++ .../src/{pages => wizard}/Home/Home.tsx | 18 +- .../Home/buildCardUrl.test.ts | 8 +- .../{pages => wizard}/Home/buildCardUrl.ts | 12 +- .../src/{pages => wizard}/Home/cardOptions.ts | 8 +- .../Home/stages/Customize.tsx | 34 +-- .../{pages => wizard}/Home/stages/Display.tsx | 8 +- .../Home/stages/Login/Login.tsx | 0 .../stages/Login/LoginAccountDeleteModal.tsx | 2 +- .../stages/Login/LoginAccountManagement.tsx | 4 +- .../Home/stages/Login/LoginBox.tsx | 0 .../Home/stages/Login/LoginBoxDemoCards.tsx | 6 +- .../Home/stages/Login/LoginOptions.tsx | 2 +- .../Home/stages/SelectCard.tsx | 8 +- .../{pages => wizard}/Home/stages/Theme.tsx | 17 +- .../Home/useCardDescriptor.ts | 4 +- .../App/AppTrends.tsx => wizard/Wizard.tsx} | 16 +- apps/frontend/src/{ => wizard}/api/user.ts | 2 +- .../src/{ => wizard}/components/Card/Card.tsx | 0 .../components/Card/CardImage.tsx | 2 +- .../components/Card/SvgInline.tsx | 6 +- .../components/Card/themeBackdrop.ts | 0 .../components/Generic/Button.tsx | 0 .../components/Generic/Checkbox.tsx | 0 .../components/Generic/Select.tsx | 0 .../components/Home/CheckboxSection.tsx | 0 .../Home/LanguagesLayoutSection.tsx | 0 .../components/Home/NumericSection.tsx | 0 .../{ => wizard}/components/Home/Progress.tsx | 0 .../{ => wizard}/components/Home/Section.tsx | 0 .../components/Home/StatsRankSection.tsx | 0 .../components/Home/TextSection.tsx | 0 .../components/Home/WakatimeLayoutSection.tsx | 0 .../{ => wizard}/hooks/useDebouncedField.ts | 0 apps/frontend/src/{ => wizard}/mock-http.ts | 0 .../mockData/additional_user_stars.json | 0 .../mockData/commented_issues.json | 0 .../{ => wizard}/mockData/commented_prs.json | 0 .../src/{ => wizard}/mockData/commits.json | 0 .../{ => wizard}/mockData/gist-graphql.json | 0 .../src/{ => wizard}/mockData/gist-rest.json | 0 .../src/{ => wizard}/mockData/repository.json | 0 .../{ => wizard}/mockData/reviewed_prs.json | 0 .../{ => wizard}/mockData/top_languages.json | 0 .../src/{ => wizard}/mockData/user_stats.json | 0 .../{ => wizard}/mockData/wakatime_proxy.json | 0 .../src/{ => wizard}/models/CardType.ts | 0 .../src/{ => wizard}/models/CardUrl.test.ts | 0 .../src/{ => wizard}/models/CardUrl.ts | 0 .../frontend/src/{ => wizard}/models/Stage.ts | 0 apps/frontend/vite.config.ts | 64 ++++- docs/advanced_documentation.md | 12 +- docs/deploy.md | 2 +- docs/index.md | 97 ++++++++ pnpm-lock.yaml | 226 ++++++++++++++++++ vercel-preparation.sh | 4 + 82 files changed, 1552 insertions(+), 230 deletions(-) rename apps/frontend/e2e/{app-trends-auth.spec.ts => wizard-auth.spec.ts} (90%) create mode 100644 apps/frontend/src/Root.tsx create mode 100644 apps/frontend/src/docs/DocsApp.tsx create mode 100644 apps/frontend/src/docs/DocsContent.tsx create mode 100644 apps/frontend/src/docs/DocsSidebar.tsx create mode 100644 apps/frontend/src/docs/docs.css create mode 100644 apps/frontend/src/docs/isPlainClick.ts create mode 100644 apps/frontend/src/docs/pages.ts create mode 100644 apps/frontend/src/docs/resolveDocUrl.test.ts create mode 100644 apps/frontend/src/docs/resolveDocUrl.ts create mode 100644 apps/frontend/src/docs/slugify.test.ts create mode 100644 apps/frontend/src/docs/slugify.ts create mode 100644 apps/frontend/src/docs/transformDocHtml.test.ts create mode 100644 apps/frontend/src/docs/transformDocHtml.ts create mode 100644 apps/frontend/src/docs/useDocRoute.ts delete mode 100644 apps/frontend/src/pages/App/Header.tsx create mode 100644 apps/frontend/src/route.ts create mode 100644 apps/frontend/src/shared/AppBar.tsx create mode 100644 apps/frontend/src/shared/LinkExternal.tsx rename apps/frontend/src/{components/Generic => shared}/ThemePicker.tsx (97%) create mode 100644 apps/frontend/src/wizard/Header.tsx rename apps/frontend/src/{pages => wizard}/Home/Home.tsx (94%) rename apps/frontend/src/{pages => wizard}/Home/buildCardUrl.test.ts (90%) rename apps/frontend/src/{pages => wizard}/Home/buildCardUrl.ts (90%) rename apps/frontend/src/{pages => wizard}/Home/cardOptions.ts (78%) rename apps/frontend/src/{pages => wizard}/Home/stages/Customize.tsx (92%) rename apps/frontend/src/{pages => wizard}/Home/stages/Display.tsx (91%) rename apps/frontend/src/{pages => wizard}/Home/stages/Login/Login.tsx (100%) rename apps/frontend/src/{pages => wizard}/Home/stages/Login/LoginAccountDeleteModal.tsx (96%) rename apps/frontend/src/{pages => wizard}/Home/stages/Login/LoginAccountManagement.tsx (97%) rename apps/frontend/src/{pages => wizard}/Home/stages/Login/LoginBox.tsx (100%) rename apps/frontend/src/{pages => wizard}/Home/stages/Login/LoginBoxDemoCards.tsx (91%) rename apps/frontend/src/{pages => wizard}/Home/stages/Login/LoginOptions.tsx (96%) rename apps/frontend/src/{pages => wizard}/Home/stages/SelectCard.tsx (92%) rename apps/frontend/src/{pages => wizard}/Home/stages/Theme.tsx (85%) rename apps/frontend/src/{pages => wizard}/Home/useCardDescriptor.ts (95%) rename apps/frontend/src/{pages/App/AppTrends.tsx => wizard/Wizard.tsx} (89%) rename apps/frontend/src/{ => wizard}/api/user.ts (97%) rename apps/frontend/src/{ => wizard}/components/Card/Card.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Card/CardImage.tsx (94%) rename apps/frontend/src/{ => wizard}/components/Card/SvgInline.tsx (97%) rename apps/frontend/src/{ => wizard}/components/Card/themeBackdrop.ts (100%) rename apps/frontend/src/{ => wizard}/components/Generic/Button.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Generic/Checkbox.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Generic/Select.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Home/CheckboxSection.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Home/LanguagesLayoutSection.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Home/NumericSection.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Home/Progress.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Home/Section.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Home/StatsRankSection.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Home/TextSection.tsx (100%) rename apps/frontend/src/{ => wizard}/components/Home/WakatimeLayoutSection.tsx (100%) rename apps/frontend/src/{ => wizard}/hooks/useDebouncedField.ts (100%) rename apps/frontend/src/{ => wizard}/mock-http.ts (100%) rename apps/frontend/src/{ => wizard}/mockData/additional_user_stars.json (100%) rename apps/frontend/src/{ => wizard}/mockData/commented_issues.json (100%) rename apps/frontend/src/{ => wizard}/mockData/commented_prs.json (100%) rename apps/frontend/src/{ => wizard}/mockData/commits.json (100%) rename apps/frontend/src/{ => wizard}/mockData/gist-graphql.json (100%) rename apps/frontend/src/{ => wizard}/mockData/gist-rest.json (100%) rename apps/frontend/src/{ => wizard}/mockData/repository.json (100%) rename apps/frontend/src/{ => wizard}/mockData/reviewed_prs.json (100%) rename apps/frontend/src/{ => wizard}/mockData/top_languages.json (100%) rename apps/frontend/src/{ => wizard}/mockData/user_stats.json (100%) rename apps/frontend/src/{ => wizard}/mockData/wakatime_proxy.json (100%) rename apps/frontend/src/{ => wizard}/models/CardType.ts (100%) rename apps/frontend/src/{ => wizard}/models/CardUrl.test.ts (100%) rename apps/frontend/src/{ => wizard}/models/CardUrl.ts (100%) rename apps/frontend/src/{ => wizard}/models/Stage.ts (100%) create mode 100644 docs/index.md diff --git a/README.md b/README.md index 46f255235ced4..23a738519be38 100644 --- a/README.md +++ b/README.md @@ -5,82 +5,34 @@ -This project is the [extended, actively maintained successor](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters. - -## Table of Contents - -- [Quick Start](#quick-start) -- [Migration from github-readme-stats](#migration-from-github-readme-stats) -- [Card Types](#card-types) -- [Advanced Customization](#advanced-customization) -- [Run It Yourself](#run-it-yourself) -- [Acknowledgements](#acknowledgements) -- [Contributing](#contributing) +This project is the [extended, actively maintained successor](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates stats cards about your GitHub contributions, your top languages and more, all customizable through a large set of parameters. ## Quick Start -- Copy and paste this into your markdown: - ```markdown - [![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended) - ``` -- Change the `?username=` value to your GitHub username. -- Done! - ---- - -As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) to create your custom stats card. Copy the generated markdown code and paste it into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme). Done! - -## Migration from github-readme-stats +Copy and paste this into your markdown, then change the `?username=` value to your GitHub username: -To migrate from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) you only need to change the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`: - -```diff -- https://github-readme-stats.vercel.app/api?username=octocat&theme=radical -+ https://github-stats-extended.vercel.app/api?username=octocat&theme=radical +```markdown +[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended) ``` -GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For more details see [Compatibility Notes](docs/fork.md#compatibility-notes). - -## Card Types - -- Show your GitHub statistics: - - ![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra) - -- ...your top languages...: - - ![Top Langs](https://github-stats-extended.vercel.app/api/top-langs/?username=anuraghazra&langs_count=4) - -- ...and development time: - - [![Alan's WakaTime stats](https://github-stats-extended.vercel.app/api/wakatime?username=alan&langs_count=6)](https://wakatime.com/@alan) - -- Pin more than 6 repos in your GitHub profile: - - [![Readme Card](https://github-stats-extended.vercel.app/api/pin/?username=anuraghazra&repo=github-readme-stats)](https://github.com/anuraghazra/github-readme-stats) - -- Pin Gists in your GitHub profile: - - [![Gist Card](https://github-stats-extended.vercel.app/api/gist?id=bbfce31e0217a3689c8d961a356cb10d)](https://gist.github.com/Yizack/bbfce31e0217a3689c8d961a356cb10d) - -- Customize all the cards: - - [![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api/?username=anuraghazra&show_icons=true&theme=calm&rank_icon=github&include_all_commits=true&custom_title=Anurag's+Stats&disable_animations=true&number_format=long&show=prs_merged_percentage,prs_reviewed)](https://github-stats-extended.vercel.app/api/?username=anuraghazra&show_icons=true&theme=calm&rank_icon=github&include_all_commits=true&custom_title=Anurag's+Stats&disable_animations=true&number_format=long&show=prs_merged_percentage,prs_reviewed) - -## Advanced Customization +As a more comfortable alternative, use the [card wizard](https://github-stats-extended.vercel.app/frontend) to configure your card visually and copy the generated markdown. -The [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization check out the [advanced documentation](docs/advanced_documentation.md). +## Documentation -## Run It Yourself +The full documentation is served by the deployment it describes, so every sample it shows is rendered by the version that is actually running: -If you want to run GitHub-Stats-Extended on your own, there are two main deployment options: you can use [github-readme-stats-action](https://github.com/stats-organization/github-readme-stats-action) to generate cards in your own GitHub Actions workflow. Or you can self-host GitHub-Stats-Extended on Vercel. +**[github-stats-extended.vercel.app/frontend/docs](https://github-stats-extended.vercel.app/frontend/docs)** -See [Run It Yourself](docs/deploy.md) for detailed instructions. +- [Overview](docs/index.md) — card types, quick start and migration from github-readme-stats. +- [Advanced Customization](docs/advanced_documentation.md) — every parameter the cards accept. +- [Available Themes](packages/core/src/themes/README.md) — the built-in themes. +- [Run It Yourself](docs/deploy.md) — GitHub Actions or a self-hosted Vercel deployment. +- [Fork Information](docs/fork.md) — what this project adds on top of github-readme-stats. ## Acknowledgements -This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of that project's functionality GitHub-Stats-Extended adds several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend added to GitHub-Stats-Extended is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️ +This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats), and the card wizard on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️ ## Contributing -Contributions are welcome! +Contributions are welcome! See [CONTRIBUTING.md](.github/CONTRIBUTING.md) to get started. diff --git a/apps/frontend/e2e/app-trends-auth.spec.ts b/apps/frontend/e2e/wizard-auth.spec.ts similarity index 90% rename from apps/frontend/e2e/app-trends-auth.spec.ts rename to apps/frontend/e2e/wizard-auth.spec.ts index 6976e1eb0fc33..a06ed35af02af 100644 --- a/apps/frontend/e2e/app-trends-auth.spec.ts +++ b/apps/frontend/e2e/wizard-auth.spec.ts @@ -5,7 +5,7 @@ import type { Page } from "@playwright/test"; * Puts the SPA into an authenticated state without contacting GitHub by * stubbing the OAuth code exchange and the follow-up user-access lookup: * `authenticate` returns a userId (which flips `isAuthenticated` to true), and - * `user-access` returns metadata so AppTrends does not immediately log back out. + * `user-access` returns metadata so the wizard does not immediately log back out. * @param page - The Playwright page to install the route handlers on. */ async function mockAuthEndpoints(page: Page): Promise { @@ -25,7 +25,7 @@ async function mockAuthEndpoints(page: Page): Promise { ); } -test.describe("AppTrends auth-driven stage transition", () => { +test.describe("the wizard auth-driven stage transition", () => { test("auto-advances from Login to Select a Card once authenticated", async ({ page, }) => { @@ -34,7 +34,7 @@ test.describe("AppTrends auth-driven stage transition", () => { // Land on the app as GitHub does after the OAuth redirect (URL carries `code`). await page.goto("?code=test-oauth-code"); - // AppTrends starts unauthenticated on stage 0 ("Login"). When the code + // the wizard starts unauthenticated on stage 0 ("Login"). When the code // exchange flips `isAuthenticated` false -> true, the render-phase // transition must advance the stepper to stage 1 ("Select a Card"). await expect(page.getByRole("heading", { level: 1 })).toContainText( diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 13bce35494c22..c28c320b00ace 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -32,12 +32,18 @@ "uuid": "^14.0.1" }, "devDependencies": { + "@types/markdown-it": "14.1.2", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "6.0.3", "clsx": "2.1.1", + "jsdom": "catalog:default", + "markdown-it": "14.3.0", + "markdown-it-anchor": "9.2.1", + "markdown-it-github-alerts": "1.0.1", "tailwindcss": "4.3.3", "vite": "catalog:default", + "vite-plugin-markdown": "2.2.0", "vitest": "catalog:default" }, "browserslist": { diff --git a/apps/frontend/src/Root.tsx b/apps/frontend/src/Root.tsx new file mode 100644 index 0000000000000..15f157c949878 --- /dev/null +++ b/apps/frontend/src/Root.tsx @@ -0,0 +1,34 @@ +import { Suspense, lazy, useEffect, useState } from "react"; +import type { JSX } from "react"; + +import { isDocsPath } from "./route"; + +/* + * Both views are loaded on demand so neither pays for the other: + * - the wizard does not ship the markdown renderer + * - the documentation does not ship the card builder + */ +const DocsApp = lazy(async () => ({ + default: (await import("./docs/DocsApp")).DocsApp, +})); + +const Wizard = lazy(async () => ({ + default: (await import("./wizard/Wizard")).Wizard, +})); + +export function Root(): JSX.Element { + const [docs, setDocs] = useState(() => isDocsPath(window.location.pathname)); + + useEffect(() => { + const handlePopState = () => { + setDocs(isDocsPath(window.location.pathname)); + }; + + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + + return {docs ? : }; +} diff --git a/apps/frontend/src/axios-override.ts b/apps/frontend/src/axios-override.ts index c518926f3fb79..a007ed1a58a53 100644 --- a/apps/frontend/src/axios-override.ts +++ b/apps/frontend/src/axios-override.ts @@ -8,17 +8,17 @@ import { DEMO_WAKATIME_USER, HOST, } from "./constants"; -import additionalUserStars from "./mockData/additional_user_stars.json" with { type: "json" }; -import commentedIssues from "./mockData/commented_issues.json" with { type: "json" }; -import commentedPrs from "./mockData/commented_prs.json" with { type: "json" }; -import commits from "./mockData/commits.json" with { type: "json" }; -import gist_graphql from "./mockData/gist-graphql.json" with { type: "json" }; -import gist_rest from "./mockData/gist-rest.json" with { type: "json" }; -import repository from "./mockData/repository.json" with { type: "json" }; -import reviewedPrs from "./mockData/reviewed_prs.json" with { type: "json" }; -import topLanguages from "./mockData/top_languages.json" with { type: "json" }; -import userStats from "./mockData/user_stats.json" with { type: "json" }; -import wakatimeProxy from "./mockData/wakatime_proxy.json" with { type: "json" }; +import additionalUserStars from "./wizard/mockData/additional_user_stars.json" with { type: "json" }; +import commentedIssues from "./wizard/mockData/commented_issues.json" with { type: "json" }; +import commentedPrs from "./wizard/mockData/commented_prs.json" with { type: "json" }; +import commits from "./wizard/mockData/commits.json" with { type: "json" }; +import gist_graphql from "./wizard/mockData/gist-graphql.json" with { type: "json" }; +import gist_rest from "./wizard/mockData/gist-rest.json" with { type: "json" }; +import repository from "./wizard/mockData/repository.json" with { type: "json" }; +import reviewedPrs from "./wizard/mockData/reviewed_prs.json" with { type: "json" }; +import topLanguages from "./wizard/mockData/top_languages.json" with { type: "json" }; +import userStats from "./wizard/mockData/user_stats.json" with { type: "json" }; +import wakatimeProxy from "./wizard/mockData/wakatime_proxy.json" with { type: "json" }; const cachedAxios = setupCache(axios, { // Cache for 30 minutes diff --git a/apps/frontend/src/docs/DocsApp.tsx b/apps/frontend/src/docs/DocsApp.tsx new file mode 100644 index 0000000000000..4a91e942b1762 --- /dev/null +++ b/apps/frontend/src/docs/DocsApp.tsx @@ -0,0 +1,45 @@ +import { useEffect } from "react"; +import type { JSX } from "react"; + +import { WIZARD_PATH } from "../route"; +import { AppBar } from "../shared/AppBar"; +import { LinkExternal } from "../shared/LinkExternal"; + +import { DocsContent } from "./DocsContent"; +import { DocsSidebar } from "./DocsSidebar"; +import { repoBlobUrl } from "./resolveDocUrl"; +import { useDocRoute } from "./useDocRoute"; + +// Loaded with this view, so the wizard never downloads the markdown styles. +import "./docs.css"; + +export function DocsApp(): JSX.Element { + const { page, navigate } = useDocRoute(); + + useEffect(() => { + document.title = `${page.title} · GitHub Stats Extended`; + }, [page.title]); + + return ( + <> + + +
+ + +
+ + +
+ + Edit this page on GitHub + +
+
+
+ + ); +} diff --git a/apps/frontend/src/docs/DocsContent.tsx b/apps/frontend/src/docs/DocsContent.tsx new file mode 100644 index 0000000000000..af08f0687518c --- /dev/null +++ b/apps/frontend/src/docs/DocsContent.tsx @@ -0,0 +1,60 @@ +import { useMemo } from "react"; +import type { JSX, MouseEvent } from "react"; + +import { isPlainClick } from "./isPlainClick"; +import type { DocPage } from "./pages"; +import { transformDocHtml } from "./transformDocHtml"; + +interface DocsContentProps { + page: DocPage; + onNavigate: (slug: string, hash?: string) => void; +} + +/** + * Whether the document opens with its own title. + * The generated theme list starts at `

`, + * so the page supplies a heading for it rather than leaving the document without one. + */ +function hasTopLevelHeading(html: string): boolean { + return /]/i.test(html); +} + +export function DocsContent({ + page, + onNavigate, +}: DocsContentProps): JSX.Element { + const html = useMemo( + () => transformDocHtml(page.html, page.file), + [page.html, page.file], + ); + + /** + * Intercepts clicks on links this app can serve itself, so moving between + * documents does not reload the page. Everything else keeps its default + * behaviour. + */ + const handleClick = (event: MouseEvent) => { + const anchor = (event.target as HTMLElement).closest("a"); + const href = anchor?.getAttribute("href"); + if (!href?.startsWith("?page=") || !isPlainClick(event)) { + return; + } + + event.preventDefault(); + const [query = "", hash] = href.slice(1).split("#"); + const slug = new URLSearchParams(query).get("page"); + if (slug) { + onNavigate(slug, hash); + } + }; + + return ( + // Link handling is delegated from the container; the rendered anchors keep + // their href and focus behaviour, so keyboard users are unaffected. +
+ {!hasTopLevelHeading(page.html) &&

{page.title}

} + {/* eslint-disable-next-line @eslint-react/dom-no-dangerously-set-innerhtml -- the markup is the repository's own markdown, compiled at build time; nothing here comes from user input or the network */} +
+
+ ); +} diff --git a/apps/frontend/src/docs/DocsSidebar.tsx b/apps/frontend/src/docs/DocsSidebar.tsx new file mode 100644 index 0000000000000..445c41d314ade --- /dev/null +++ b/apps/frontend/src/docs/DocsSidebar.tsx @@ -0,0 +1,47 @@ +import { clsx } from "clsx"; +import type { JSX } from "react"; + +import { isPlainClick } from "./isPlainClick"; +import { DOC_PAGES } from "./pages"; + +interface DocsSidebarProps { + currentSlug: string; + onNavigate: (slug: string) => void; +} + +export function DocsSidebar({ + currentSlug, + onNavigate, +}: DocsSidebarProps): JSX.Element { + return ( + + ); +} diff --git a/apps/frontend/src/docs/docs.css b/apps/frontend/src/docs/docs.css new file mode 100644 index 0000000000000..0e86254b4b39f --- /dev/null +++ b/apps/frontend/src/docs/docs.css @@ -0,0 +1,196 @@ +/* + * Typography for the rendered markdown. Scoped to `.docs-content` because the + * markdown produces bare tags with no classes of their own, and written against + * the DaisyUI theme variables so it follows the theme picker like the rest of + * the app. + */ + +.docs-content { + line-height: 1.7; + overflow-wrap: break-word; +} + +.docs-content > :first-child { + margin-top: 0; +} + +.docs-content h1, +.docs-content h2, +.docs-content h3, +.docs-content h4 { + font-weight: 600; + line-height: 1.3; + margin: 2rem 0 0.75rem; + scroll-margin-top: 1rem; +} + +.docs-content h1 { + font-size: 2rem; +} +.docs-content h2 { + font-size: 1.5rem; + padding-bottom: 0.3rem; + border-bottom: 1px solid var(--color-base-300); +} +.docs-content h3 { + font-size: 1.2rem; +} + +.docs-content p, +.docs-content ul, +.docs-content ol, +.docs-content blockquote, +.docs-content pre, +.docs-content table { + margin: 0.9rem 0; +} + +.docs-content ul, +.docs-content ol { + padding-left: 1.5rem; +} +.docs-content ul { + list-style: disc; +} +.docs-content ol { + list-style: decimal; +} +.docs-content li + li { + margin-top: 0.35rem; +} + +.docs-content a { + color: var(--color-primary); + text-decoration: underline; + text-underline-offset: 2px; +} +.docs-content a:hover { + text-decoration-thickness: 2px; +} + +/* + * Per-heading link, appended by `transformDocHtml`. Hidden until the heading is + * hovered or the link itself is focused, so it stays available to the keyboard + * without cluttering the page. + */ +.docs-content .heading-anchor { + margin-left: 0.4rem; + font-weight: 400; + text-decoration: none; + opacity: 0; +} +.docs-content :is(h2, h3, h4):hover .heading-anchor, +.docs-content .heading-anchor:focus-visible { + opacity: 1; +} + +.docs-content code { + background-color: color-mix( + in oklab, + var(--color-base-content), + transparent 90% + ); + border-radius: 0.25rem; + padding: 0.15em 0.35em; + font-size: 0.9em; +} + +.docs-content pre { + background-color: var(--color-base-200); + border: 1px solid var(--color-base-300); + border-radius: 0.5rem; + padding: 0.9rem 1rem; + /* Long command lines and YAML samples scroll instead of widening the page. */ + overflow-x: auto; +} +.docs-content pre code { + background: none; + padding: 0; + font-size: 0.875rem; +} + +.docs-content blockquote { + border-left: 4px solid var(--color-primary); + padding: 0.1rem 0 0.1rem 1rem; + opacity: 0.9; +} + +/* + * GitHub alerts (`> [!NOTE]`, `> [!WARNING]`, …). Styled against the DaisyUI + * theme variables rather than the plugin's own stylesheet, which hard-codes + * GitHub's palette and would ignore the theme picker. `--alert-color` is set + * per severity below and drives the border, title and icon together. + */ +.docs-content .markdown-alert { + --alert-color: var(--color-info); + border-left: 4px solid var(--alert-color); + background-color: color-mix(in oklab, var(--alert-color), transparent 92%); + border-radius: 0 0.375rem 0.375rem 0; + padding: 0.75rem 1rem; + margin: 1.1rem 0; +} + +.docs-content .markdown-alert > :first-child { + margin-top: 0; +} +.docs-content .markdown-alert > :last-child { + margin-bottom: 0; +} + +.docs-content .markdown-alert-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + color: var(--alert-color); + margin-bottom: 0.35rem; +} + +/* The plugin inlines an octicon, which inherits the title colour. */ +.docs-content .markdown-alert-title svg { + fill: currentColor; + flex-shrink: 0; +} + +.docs-content .markdown-alert-note { + --alert-color: var(--color-info); +} +.docs-content .markdown-alert-tip { + --alert-color: var(--color-success); +} +.docs-content .markdown-alert-important { + --alert-color: var(--color-primary); +} +.docs-content .markdown-alert-warning { + --alert-color: var(--color-warning); +} +.docs-content .markdown-alert-caution { + --alert-color: var(--color-error); +} + +.docs-content table { + border-collapse: collapse; + display: block; + overflow-x: auto; + max-width: 100%; +} +.docs-content th, +.docs-content td { + border: 1px solid var(--color-base-300); + padding: 0.5rem 0.75rem; + text-align: left; +} +.docs-content th { + background-color: var(--color-base-200); +} + +.docs-content img { + max-width: 100%; + height: auto; +} + +.docs-content hr { + border: 0; + border-top: 1px solid var(--color-base-300); + margin: 2rem 0; +} diff --git a/apps/frontend/src/docs/isPlainClick.ts b/apps/frontend/src/docs/isPlainClick.ts new file mode 100644 index 0000000000000..94dd096dccf91 --- /dev/null +++ b/apps/frontend/src/docs/isPlainClick.ts @@ -0,0 +1,9 @@ +import type { MouseEvent } from "react"; + +/** + * Whether a click should be handled in-app. Modified clicks are left to the + * browser so "open in new tab" and friends keep working on documentation links. + */ +export function isPlainClick(event: MouseEvent): boolean { + return !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey; +} diff --git a/apps/frontend/src/docs/pages.ts b/apps/frontend/src/docs/pages.ts new file mode 100644 index 0000000000000..b30381e4fc44b --- /dev/null +++ b/apps/frontend/src/docs/pages.ts @@ -0,0 +1,79 @@ +/* + * The docs site renders the markdown under `docs/`, compiled to HTML at build + * time by `vite-plugin-markdown`. The sources stay where they are so they + * remain readable on GitHub and are the single source for both. + */ +import { html as advancedHtml } from "../../../../docs/advanced_documentation.md"; +import { html as deployHtml } from "../../../../docs/deploy.md"; +import { html as forkHtml } from "../../../../docs/fork.md"; +import { html as overviewHtml } from "../../../../docs/index.md"; +import { html as themesHtml } from "../../../../packages/core/src/themes/README.md"; + +export interface DocPage { + /** Value of the `?page=` query parameter. */ + slug: string; + /** Label shown in the navigation. */ + title: string; + /** + * Path relative to the repository root. Used to resolve the relative links + * inside the document and to link back to the source on GitHub. + */ + file: string; + /** Markup compiled from the markdown at build time. */ + html: string; +} + +/** Landing page, and the fallback for an unknown slug. */ +const OVERVIEW = { + slug: "overview", + title: "Overview", + file: "docs/index.md", + html: overviewHtml, +} as const satisfies DocPage; + +export const DOC_PAGES = [ + OVERVIEW, + { + slug: "customization", + title: "Advanced Customization", + file: "docs/advanced_documentation.md", + html: advancedHtml, + }, + { + slug: "themes", + title: "Available Themes", + file: "packages/core/src/themes/README.md", + html: themesHtml, + }, + { + slug: "deploy", + title: "Run It Yourself", + file: "docs/deploy.md", + html: deployHtml, + }, + { + slug: "fork", + title: "Fork Information", + file: "docs/fork.md", + html: forkHtml, + }, +] as const satisfies ReadonlyArray; + +/** Every slug the documentation actually serves. */ +export type DocSlug = (typeof DOC_PAGES)[number]["slug"]; + +/** + * Repository path to page slug, for resolving cross-document links. The key + * stays a plain `string` because lookups come from links in the markdown. + */ +export const SLUG_BY_FILE = new Map( + DOC_PAGES.map((page) => [page.file, page.slug]), +); + +/** + * An unknown slug (stale link, hand-edited URL) falls back to the overview + * rather than rendering an empty page. + */ +export function findPage(slug: string | null): DocPage { + return DOC_PAGES.find((page) => page.slug === slug) ?? OVERVIEW; +} diff --git a/apps/frontend/src/docs/resolveDocUrl.test.ts b/apps/frontend/src/docs/resolveDocUrl.test.ts new file mode 100644 index 0000000000000..f067058155e98 --- /dev/null +++ b/apps/frontend/src/docs/resolveDocUrl.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; + +import { resolveDocUrl, resolveRepoPath } from "./resolveDocUrl"; + +const HOSTED = "https://github-stats-extended.vercel.app"; + +describe("resolveRepoPath", () => { + it("resolves against the directory of the linking document", () => { + expect(resolveRepoPath("docs/fork.md", "advanced_documentation.md")).toBe( + "docs/advanced_documentation.md", + ); + }); + + it("walks up out of the document directory", () => { + expect( + resolveRepoPath("docs/advanced_documentation.md", "../packages/core.md"), + ).toBe("packages/core.md"); + }); + + it("ignores the current directory marker", () => { + expect(resolveRepoPath("docs/index.md", "./fork.md")).toBe("docs/fork.md"); + }); +}); + +describe("resolveDocUrl", () => { + it("makes hosted card samples relative so they hit the serving deployment", () => { + expect( + resolveDocUrl( + `${HOSTED}/api?username=anuraghazra&theme=dark`, + "docs/index.md", + ), + ).toBe("/api?username=anuraghazra&theme=dark"); + }); + + it("maps the hosted root to a root-relative URL", () => { + expect(resolveDocUrl(HOSTED, "docs/index.md")).toBe("/"); + }); + + it("turns a link to another documentation page into a page query", () => { + expect(resolveDocUrl("fork.md", "docs/index.md")).toBe("?page=fork"); + }); + + it("keeps the anchor when linking into another page", () => { + expect(resolveDocUrl("fork.md#compatibility-notes", "docs/index.md")).toBe( + "?page=fork#compatibility-notes", + ); + }); + + it("resolves a link relative to the document that contains it", () => { + expect( + resolveDocUrl("advanced_documentation.md#themes", "docs/fork.md"), + ).toBe("?page=customization#themes"); + }); + + it("resolves the generated theme list outside the docs folder", () => { + expect( + resolveDocUrl( + "../packages/core/src/themes/README.md", + "docs/advanced_documentation.md", + ), + ).toBe("?page=themes"); + }); + + it("sends markdown outside the docs site to the repository", () => { + expect( + resolveDocUrl( + "../.github/CONTRIBUTING.md#translations-contribution", + "docs/advanced_documentation.md", + ), + ).toBe( + "https://github.com/stats-organization/github-stats-extended/blob/master/.github/CONTRIBUTING.md", + ); + }); + + it("resolves a local image to its bundled asset", () => { + expect(resolveDocUrl("frontend-screenshot.png", "docs/fork.md")).toContain( + "frontend-screenshot", + ); + }); + + it("leaves bare anchors alone", () => { + expect(resolveDocUrl("#card-types", "docs/index.md")).toBe("#card-types"); + }); + + it("leaves links to other sites alone", () => { + const url = "https://github.com/anuraghazra/github-readme-stats"; + expect(resolveDocUrl(url, "docs/index.md")).toBe(url); + }); + + it("leaves root-relative URLs alone", () => { + expect(resolveDocUrl("/api?username=x", "docs/index.md")).toBe( + "/api?username=x", + ); + }); +}); diff --git a/apps/frontend/src/docs/resolveDocUrl.ts b/apps/frontend/src/docs/resolveDocUrl.ts new file mode 100644 index 0000000000000..b74764d1e269f --- /dev/null +++ b/apps/frontend/src/docs/resolveDocUrl.ts @@ -0,0 +1,96 @@ +import { SLUG_BY_FILE } from "./pages"; + +/** + * Absolute URL of the hosted deployment as written in the repository markdown. + * + * Occurrences are rewritten to site-relative URLs so a docs page always renders + * its card samples through the deployment that serves it, rather than pinning + * every sample to production. This is what keeps docs and API in sync: a page + * built from a commit can only show what that same commit's API supports. + */ +const HOSTED_ORIGIN = "https://github-stats-extended.vercel.app"; + +const REPO_BLOB_URL = + "https://github.com/stats-organization/github-stats-extended/blob/master"; + +/** Link to a repository file on GitHub, by its path from the repository root. */ +export function repoBlobUrl(repoPath: string): string { + return `${REPO_BLOB_URL}/${repoPath}`; +} + +/** + * Local images referenced from the markdown. Importing them here is what puts + * them through Vite's asset pipeline; the keys are repository-root paths, which + * is what `resolveRepoPath` produces. + */ +const ASSETS: Record = { + "docs/frontend-screenshot.png": new URL( + "../../../../docs/frontend-screenshot.png", + import.meta.url, + ).href, +}; + +/** + * Resolves `target` against the directory holding `fromFile`, both being + * repository-root paths. A browser-side equivalent of `path.resolve` limited to + * what the markdown sources use. + */ +export function resolveRepoPath(fromFile: string, target: string): string { + const segments = fromFile.split("/").slice(0, -1); + + for (const segment of target.split("/")) { + if (segment === "." || segment === "") { + continue; + } + if (segment === "..") { + segments.pop(); + continue; + } + segments.push(segment); + } + + return segments.join("/"); +} + +/** + * Rewrites one URL found in `fromFile` so it resolves inside the docs page. + * + * Passed to `react-markdown` as `urlTransform`, so it covers links and images + * alike, including the reference-style image definitions the generated theme + * list uses. + */ +export function resolveDocUrl(url: string, fromFile: string): string { + if (url.startsWith(HOSTED_ORIGIN)) { + return url.slice(HOSTED_ORIGIN.length) || "/"; + } + + // Bare anchors and URLs to other sites are already correct. + if (url.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(url)) { + return url; + } + + const [target = "", hash] = url.split("#"); + const suffix = hash ? `#${hash}` : ""; + + // A root-relative URL already points at this deployment. + if (target.startsWith("/")) { + return url; + } + + const repoPath = resolveRepoPath(fromFile, target); + + if (target.endsWith(".md")) { + const slug = SLUG_BY_FILE.get(repoPath); + // Markdown outside the docs site (CONTRIBUTING.md, …) keeps working by + // pointing at the repository. + return slug ? `?page=${slug}${suffix}` : repoBlobUrl(repoPath); + } + + const asset = ASSETS[repoPath]; + if (asset) { + return asset; + } + + // Anything else is a repository file with no in-app equivalent. + return repoBlobUrl(repoPath); +} diff --git a/apps/frontend/src/docs/slugify.test.ts b/apps/frontend/src/docs/slugify.test.ts new file mode 100644 index 0000000000000..d3c10069decd5 --- /dev/null +++ b/apps/frontend/src/docs/slugify.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; + +import { slugify } from "./slugify"; + +describe("slugify", () => { + it("matches the anchors GitHub generates", () => { + expect(slugify("Run It Yourself")).toBe("run-it-yourself"); + expect(slugify("Migration from github-readme-stats")).toBe( + "migration-from-github-readme-stats", + ); + expect(slugify("What's new?")).toBe("whats-new"); + }); +}); diff --git a/apps/frontend/src/docs/slugify.ts b/apps/frontend/src/docs/slugify.ts new file mode 100644 index 0000000000000..c64f93d4dee13 --- /dev/null +++ b/apps/frontend/src/docs/slugify.ts @@ -0,0 +1,14 @@ +/** + * GitHub-compatible heading slug: lowercase, punctuation dropped, spaces to + * dashes. Matching GitHub matters because the markdown sources already link + * between headings with anchors that work on github.com. + * + * Used by `markdown-it-anchor` at build time, see `vite.config.ts`. + */ +export function slugify(text: string): string { + return text + .trim() + .toLowerCase() + .replace(/[^\p{L}\p{N}\s-]/gu, "") + .replace(/\s+/g, "-"); +} diff --git a/apps/frontend/src/docs/transformDocHtml.test.ts b/apps/frontend/src/docs/transformDocHtml.test.ts new file mode 100644 index 0000000000000..35f8a5bdb7e64 --- /dev/null +++ b/apps/frontend/src/docs/transformDocHtml.test.ts @@ -0,0 +1,49 @@ +// @vitest-environment jsdom +import { describe, expect, it } from "vitest"; + +import { transformDocHtml } from "./transformDocHtml"; + +const OVERVIEW = "docs/index.md"; +const HOSTED = "https://github-stats-extended.vercel.app"; + +describe("transformDocHtml", () => { + it("rewrites hosted card samples to the serving deployment", () => { + const html = transformDocHtml( + ``, + OVERVIEW, + ); + expect(html).toContain('src="/api?username=anuraghazra&theme=dark"'); + }); + + it("rewrites the srcset of a dark-mode picture source", () => { + const html = transformDocHtml( + ``, + OVERVIEW, + ); + expect(html).toContain('srcset="/api?username=x"'); + }); + + it("turns a cross-document link into a page query", () => { + const html = transformDocHtml('Fork', OVERVIEW); + expect(html).toContain('href="?page=fork"'); + }); + + it("opens off-site links in a new tab", () => { + const html = transformDocHtml( + 'GitHub', + OVERVIEW, + ); + expect(html).toContain('target="_blank"'); + expect(html).toContain('rel="noopener noreferrer"'); + }); + + it("keeps in-app navigation in the same tab", () => { + const html = transformDocHtml('Fork', OVERVIEW); + expect(html).not.toContain("target="); + }); + + it("defers image loading", () => { + const html = transformDocHtml('', OVERVIEW); + expect(html).toContain('loading="lazy"'); + }); +}); diff --git a/apps/frontend/src/docs/transformDocHtml.ts b/apps/frontend/src/docs/transformDocHtml.ts new file mode 100644 index 0000000000000..c56905f4b3a9e --- /dev/null +++ b/apps/frontend/src/docs/transformDocHtml.ts @@ -0,0 +1,53 @@ +import { resolveDocUrl } from "./resolveDocUrl"; + +/** + * Prepares the markup compiled at build time for display. + * + * This module exists because the sources are read in two places. + * On github.com they have to work as they are, so they keep absolute card URLs and + * repository-relative `.md` links; here those same links have to point at the + * serving deployment and at `?page=` routes. + * Rewriting them on the way to the DOM keeps one source correct in both. + * + * Heading ids and their permalinks are added by `markdown-it-anchor` during the build. + * What is left for here is everything markdown-it cannot see: + * URLs inside raw HTML blocks, which it passes through untouched, and local images, + * whose hashed asset URL is only known to the bundler. + * + * If the site ever becomes the only place the docs are read, the raw HTML + * blocks could use site-relative URLs and the rest could move into markdown-it + * rules, at which point this module can go. + */ +export function transformDocHtml(html: string, file: string): string { + const doc = new DOMParser().parseFromString(html, "text/html"); + + for (const anchor of doc.querySelectorAll("a[href]")) { + const href = anchor.getAttribute("href"); + if (href) { + anchor.setAttribute("href", resolveDocUrl(href, file)); + } + // Send off-site links to a new tab, but keep in-page navigation inline. + if (/^https?:/i.test(anchor.getAttribute("href") ?? "")) { + anchor.setAttribute("target", "_blank"); + anchor.setAttribute("rel", "noopener noreferrer"); + } + } + + for (const image of doc.querySelectorAll("img[src]")) { + const src = image.getAttribute("src"); + if (src) { + image.setAttribute("src", resolveDocUrl(src, file)); + } + image.setAttribute("loading", "lazy"); + } + + // `` offers a dark-mode variant of the same card. + for (const source of doc.querySelectorAll("source[srcset]")) { + const srcset = source.getAttribute("srcset"); + if (srcset) { + source.setAttribute("srcset", resolveDocUrl(srcset, file)); + } + } + + return doc.body.innerHTML; +} diff --git a/apps/frontend/src/docs/useDocRoute.ts b/apps/frontend/src/docs/useDocRoute.ts new file mode 100644 index 0000000000000..4963b398ac507 --- /dev/null +++ b/apps/frontend/src/docs/useDocRoute.ts @@ -0,0 +1,75 @@ +import { useCallback, useEffect, useState } from "react"; + +import { findPage } from "./pages"; +import type { DocPage } from "./pages"; + +/** + * The page is selected with a query parameter rather than a path segment so the + * docs stay a single static file: `/docs/index.html` answers every page without + * the host needing rewrite rules. It also leaves the URL fragment free for the + * heading anchors the markdown already links to. + */ +const PAGE_PARAM = "page"; + +/** Null when the URL names no page, which `findPage` resolves to the overview. */ +function currentSlug(): string | null { + return new URLSearchParams(window.location.search).get(PAGE_PARAM); +} + +/** + * Scrolls to the heading a hash names. Deferred a frame because the target only + * exists once the page it belongs to has rendered, which is also true of a deep + * link arriving on load. + */ +function scrollToHash(hash: string): void { + const id = decodeURIComponent(hash.replace(/^#/, "")); + requestAnimationFrame(() => { + document.getElementById(id)?.scrollIntoView(); + }); +} + +interface DocRoute { + page: DocPage; + /** Navigates without a reload, keeping an entry in the browser history. */ + navigate: (slug: string, hash?: string) => void; +} + +export function useDocRoute(): DocRoute { + const [slug, setSlug] = useState(currentSlug); + + useEffect(() => { + const handlePopState = () => { + setSlug(currentSlug()); + }; + + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + + useEffect(() => { + // A deep link arrives before the page is on screen, so the browser has + // nothing to scroll to on load. + if (window.location.hash) { + scrollToHash(window.location.hash); + } + }, []); + + const navigate = useCallback((nextSlug: string, hash?: string) => { + const url = new URL(window.location.href); + url.searchParams.set(PAGE_PARAM, nextSlug); + url.hash = hash ?? ""; + + window.history.pushState(null, "", url); + setSlug(nextSlug); + + if (hash) { + scrollToHash(hash); + } else { + window.scrollTo({ top: 0 }); + } + }, []); + + return { page: findPage(slug), navigate }; +} diff --git a/apps/frontend/src/globals.d.ts b/apps/frontend/src/globals.d.ts index 29d02c17124b4..f0a6feff6d735 100644 --- a/apps/frontend/src/globals.d.ts +++ b/apps/frontend/src/globals.d.ts @@ -1,4 +1,12 @@ declare global { + /** The subset of Vite's build-time constants this app reads. */ + interface ImportMeta { + readonly env: { + /** Value of `base` in `vite.config.ts`, with a trailing slash. */ + readonly BASE_URL: string; + }; + } + interface CustomProcess { env: { FETCH_MULTI_PAGE_STARS: string | undefined; diff --git a/apps/frontend/src/index.tsx b/apps/frontend/src/index.tsx index f2a0f657e5cd4..bcc00935e4f7b 100644 --- a/apps/frontend/src/index.tsx +++ b/apps/frontend/src/index.tsx @@ -4,7 +4,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { Provider } from "react-redux"; -import { AppTrends } from "./pages/App/AppTrends"; +import { Root } from "./Root"; import { store } from "./redux/store"; import "./index.css"; @@ -14,7 +14,7 @@ const root = createRoot(document.getElementById("root") as HTMLElement); root.render( - + , ); diff --git a/apps/frontend/src/modules.d.ts b/apps/frontend/src/modules.d.ts index 8e5a1b4b5d2fe..30e020ea3991e 100644 --- a/apps/frontend/src/modules.d.ts +++ b/apps/frontend/src/modules.d.ts @@ -5,6 +5,13 @@ declare module "*.png" { export default img; } +// Documentation markdown, compiled to HTML at build time by `vite-plugin-markdown`. +// https://github.com/hmsk/vite-plugin-markdown#type-declarations +declare module "*.md" { + const html: string; + export { html }; +} + // This package doesn't have a @types counter part declare module "save-svg-as-png" { const saveSvgAsPng: ( diff --git a/apps/frontend/src/pages/App/Header.tsx b/apps/frontend/src/pages/App/Header.tsx deleted file mode 100644 index 7701ef1c1109d..0000000000000 --- a/apps/frontend/src/pages/App/Header.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { JSX } from "react"; -import { FaGithub as GithubIcon } from "react-icons/fa"; - -import appIcon from "../../assets/appLogo64.png"; -import { ThemePicker } from "../../components/Generic/ThemePicker"; -import { ProgressBar } from "../../components/Home/Progress"; -import { STAGE_LABELS } from "../../models/Stage"; -import type { StageIndex } from "../../models/Stage"; - -interface HeaderProps { - currStageIndex: StageIndex; - onStageIndexChange: (stageIndex: StageIndex) => void; -} - -const items = STAGE_LABELS.map((it) => it.shortTitle); - -export function Header({ - currStageIndex, - onStageIndexChange, -}: HeaderProps): JSX.Element { - return ( - <> -
-
- {/* Logo */} - - logo - GitHub Stats Extended - - {/* Star on GitHub + theme toggle */} - -
-
- { - onStageIndexChange(itemIndex as StageIndex); - }} - /> - - ); -} diff --git a/apps/frontend/src/route.ts b/apps/frontend/src/route.ts new file mode 100644 index 0000000000000..aaf403bbd3110 --- /dev/null +++ b/apps/frontend/src/route.ts @@ -0,0 +1,28 @@ +import type { DocSlug } from "./docs/pages"; + +/** + * The documentation is part of the frontend, not a separate app, so it lives + * under the same Vite `base` as the card wizard. That makes the path identical + * in the dev server and in the deployment. + */ +export const WIZARD_PATH = import.meta.env.BASE_URL; + +export const DOCS_PATH = `${WIZARD_PATH}docs`; + +/** Whether `pathname` addresses the documentation rather than the wizard. */ +export function isDocsPath(pathname: string): boolean { + return /\/docs\/?$/.test(pathname); +} + +/** + * Link to a documentation page, by the slug it is registered under in + * `docs/pages.ts`. Linking here rather than to the markdown on GitHub keeps the + * wizard pointing at the documentation for the version that is running. + * + * The slug is typed from the page list, so renaming or dropping a page turns + * every stale link into a compile error. The import is type-only, so the wizard + * does not pull the compiled documentation into its bundle. + */ +export function docsUrl(slug: DocSlug, hash?: string): string { + return `${DOCS_PATH}?page=${slug}${hash ? `#${hash}` : ""}`; +} diff --git a/apps/frontend/src/shared/AppBar.tsx b/apps/frontend/src/shared/AppBar.tsx new file mode 100644 index 0000000000000..b34597631863b --- /dev/null +++ b/apps/frontend/src/shared/AppBar.tsx @@ -0,0 +1,59 @@ +import type { JSX } from "react"; +import { FaGithub as GithubIcon } from "react-icons/fa"; + +import appIcon from "../assets/appLogo64.png"; + +import { LinkExternal } from "./LinkExternal"; +import { ThemePicker } from "./ThemePicker"; + +interface AppBarProps { + /** + * Link shown next to the GitHub button, used to move between the wizard and + * the documentation. + */ + crossLink: { href: string; label: string }; +} + +/** + * Top bar shared by the wizard and the documentation, so both pages keep the + * same branding, theme picker and navigation. + */ +export function AppBar({ crossLink }: AppBarProps): JSX.Element { + return ( +
+
+ {/* Logo */} + + logo + GitHub Stats Extended + + {/* Cross link + star on GitHub + theme toggle */} +
+ + {crossLink.label} + + + Star on + + + +
+
+
+ ); +} diff --git a/apps/frontend/src/shared/LinkExternal.tsx b/apps/frontend/src/shared/LinkExternal.tsx new file mode 100644 index 0000000000000..75d250091a41c --- /dev/null +++ b/apps/frontend/src/shared/LinkExternal.tsx @@ -0,0 +1,37 @@ +import { clsx } from "clsx"; +import type { JSX, ReactNode } from "react"; +import { FaExternalLinkAlt as ExternalIcon } from "react-icons/fa"; + +interface LinkExternalProps { + href: string; + children: ReactNode; + className?: string; + /** + * Set to `false` for links that already carry their own mark, such as the + * branded GitHub button, where a second icon would only add noise. + */ + showIcon?: boolean; +} + +/** + * A link that opens in a new tab, with the `rel` that `target="_blank"` + * requires, and a trailing icon so the new tab is announced before the click. + */ +export function LinkExternal({ + href, + children, + className, + showIcon = true, +}: LinkExternalProps): JSX.Element { + return ( + + {children} + {showIcon && } + + ); +} diff --git a/apps/frontend/src/components/Generic/ThemePicker.tsx b/apps/frontend/src/shared/ThemePicker.tsx similarity index 97% rename from apps/frontend/src/components/Generic/ThemePicker.tsx rename to apps/frontend/src/shared/ThemePicker.tsx index a4397497f62d0..b79794eee81e3 100644 --- a/apps/frontend/src/components/Generic/ThemePicker.tsx +++ b/apps/frontend/src/shared/ThemePicker.tsx @@ -7,8 +7,8 @@ import { FaSun as SunIcon, } from "react-icons/fa"; -import { useTheme } from "../../redux/selectors/themeSelectors"; -import { THEMES } from "../../redux/slices/theme"; +import { useTheme } from "../redux/selectors/themeSelectors"; +import { THEMES } from "../redux/slices/theme"; interface ThemeIconProps { isDark: boolean; diff --git a/apps/frontend/src/wizard/Header.tsx b/apps/frontend/src/wizard/Header.tsx new file mode 100644 index 0000000000000..985c221c5dbb5 --- /dev/null +++ b/apps/frontend/src/wizard/Header.tsx @@ -0,0 +1,33 @@ +import type { JSX } from "react"; + +import { DOCS_PATH } from "../route"; +import { AppBar } from "../shared/AppBar"; + +import { ProgressBar } from "./components/Home/Progress"; +import { STAGE_LABELS } from "./models/Stage"; +import type { StageIndex } from "./models/Stage"; + +interface HeaderProps { + currStageIndex: StageIndex; + onStageIndexChange: (stageIndex: StageIndex) => void; +} + +const items = STAGE_LABELS.map((it) => it.shortTitle); + +export function Header({ + currStageIndex, + onStageIndexChange, +}: HeaderProps): JSX.Element { + return ( + <> + + { + onStageIndexChange(itemIndex as StageIndex); + }} + /> + + ); +} diff --git a/apps/frontend/src/pages/Home/Home.tsx b/apps/frontend/src/wizard/Home/Home.tsx similarity index 94% rename from apps/frontend/src/pages/Home/Home.tsx rename to apps/frontend/src/wizard/Home/Home.tsx index 7c22adef91d09..11d860072f35c 100644 --- a/apps/frontend/src/pages/Home/Home.tsx +++ b/apps/frontend/src/wizard/Home/Home.tsx @@ -4,13 +4,7 @@ import { useDispatch } from "react-redux"; import { BounceLoader } from "react-spinners"; import { v4 as uuidv4 } from "uuid"; -import { authenticate } from "../../api/user"; -import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection"; -import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection"; import { DEMO_USER } from "../../constants"; -import { CardType } from "../../models/CardType"; -import { STAGE_LABELS } from "../../models/Stage"; -import type { StageIndex } from "../../models/Stage"; import { useTheme } from "../../redux/selectors/themeSelectors"; import { useIsAuthenticated, @@ -18,6 +12,13 @@ import { useUserId, } from "../../redux/selectors/userSelectors"; import { login } from "../../redux/slices/user"; +import { LinkExternal } from "../../shared/LinkExternal"; +import { authenticate } from "../api/user"; +import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../components/Home/LanguagesLayoutSection"; +import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../components/Home/WakatimeLayoutSection"; +import { CardType } from "../models/CardType"; +import { STAGE_LABELS } from "../models/Stage"; +import type { StageIndex } from "../models/Stage"; import { buildCardUrl } from "./buildCardUrl"; import { getDefaultCardOptions } from "./cardOptions"; @@ -198,13 +199,12 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {

You are logged in as{" "} - {userId} - + .

diff --git a/apps/frontend/src/pages/Home/buildCardUrl.test.ts b/apps/frontend/src/wizard/Home/buildCardUrl.test.ts similarity index 90% rename from apps/frontend/src/pages/Home/buildCardUrl.test.ts rename to apps/frontend/src/wizard/Home/buildCardUrl.test.ts index 350eecd12c848..ee92f3d470627 100644 --- a/apps/frontend/src/pages/Home/buildCardUrl.test.ts +++ b/apps/frontend/src/wizard/Home/buildCardUrl.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection"; -import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection"; -import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection"; -import { CardType } from "../../models/CardType"; +import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../components/Home/LanguagesLayoutSection"; +import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../components/Home/StatsRankSection"; +import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../components/Home/WakatimeLayoutSection"; +import { CardType } from "../models/CardType"; import { buildCardUrl } from "./buildCardUrl"; import type { CardOptions } from "./cardOptions"; diff --git a/apps/frontend/src/pages/Home/buildCardUrl.ts b/apps/frontend/src/wizard/Home/buildCardUrl.ts similarity index 90% rename from apps/frontend/src/pages/Home/buildCardUrl.ts rename to apps/frontend/src/wizard/Home/buildCardUrl.ts index 70743b823612c..54dda20802023 100644 --- a/apps/frontend/src/pages/Home/buildCardUrl.ts +++ b/apps/frontend/src/wizard/Home/buildCardUrl.ts @@ -1,9 +1,9 @@ -import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection"; -import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection"; -import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection"; -import { CardType } from "../../models/CardType"; -import { cardUrl } from "../../models/CardUrl"; -import type { CardUrlBuilder } from "../../models/CardUrl"; +import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../components/Home/LanguagesLayoutSection"; +import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../components/Home/StatsRankSection"; +import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../components/Home/WakatimeLayoutSection"; +import { CardType } from "../models/CardType"; +import { cardUrl } from "../models/CardUrl"; +import type { CardUrlBuilder } from "../models/CardUrl"; import type { CardOptions } from "./cardOptions"; diff --git a/apps/frontend/src/pages/Home/cardOptions.ts b/apps/frontend/src/wizard/Home/cardOptions.ts similarity index 78% rename from apps/frontend/src/pages/Home/cardOptions.ts rename to apps/frontend/src/wizard/Home/cardOptions.ts index 7e9a3650a60e1..461e997502169 100644 --- a/apps/frontend/src/pages/Home/cardOptions.ts +++ b/apps/frontend/src/wizard/Home/cardOptions.ts @@ -1,8 +1,8 @@ -import type { SelectOption } from "../../components/Generic/Select"; -import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection"; -import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection"; -import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection"; import { DEMO_GIST, DEMO_REPO, DEMO_WAKATIME_USER } from "../../constants"; +import type { SelectOption } from "../components/Generic/Select"; +import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../components/Home/LanguagesLayoutSection"; +import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../components/Home/StatsRankSection"; +import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../components/Home/WakatimeLayoutSection"; /** * All user-tunable card parameters collected during the customize stage. diff --git a/apps/frontend/src/pages/Home/stages/Customize.tsx b/apps/frontend/src/wizard/Home/stages/Customize.tsx similarity index 92% rename from apps/frontend/src/pages/Home/stages/Customize.tsx rename to apps/frontend/src/wizard/Home/stages/Customize.tsx index 50c828ec55d04..18c4c0af823d8 100644 --- a/apps/frontend/src/pages/Home/stages/Customize.tsx +++ b/apps/frontend/src/wizard/Home/stages/Customize.tsx @@ -1,22 +1,24 @@ import type { JSX } from "react"; -import { CardImage } from "../../../components/Card/CardImage"; -import { CheckboxSection } from "../../../components/Home/CheckboxSection"; -import { LanguagesLayoutSection } from "../../../components/Home/LanguagesLayoutSection"; -import { NumericSection } from "../../../components/Home/NumericSection"; -import { StatsRankSection } from "../../../components/Home/StatsRankSection"; -import { TextSection } from "../../../components/Home/TextSection"; -import { WakatimeLayoutSection } from "../../../components/Home/WakatimeLayoutSection"; import { DEMO_GIST, DEMO_REPO, DEMO_USER, DEMO_WAKATIME_USER, } from "../../../constants"; -import { CardType } from "../../../models/CardType"; -import type { CardUrlBuilder } from "../../../models/CardUrl"; -import type { StageIndex } from "../../../models/Stage"; import { useIsAuthenticated } from "../../../redux/selectors/userSelectors"; +import { docsUrl } from "../../../route"; +import { LinkExternal } from "../../../shared/LinkExternal"; +import { CardImage } from "../../components/Card/CardImage.js"; +import { CheckboxSection } from "../../components/Home/CheckboxSection.js"; +import { LanguagesLayoutSection } from "../../components/Home/LanguagesLayoutSection.js"; +import { NumericSection } from "../../components/Home/NumericSection.js"; +import { StatsRankSection } from "../../components/Home/StatsRankSection.js"; +import { TextSection } from "../../components/Home/TextSection.js"; +import { WakatimeLayoutSection } from "../../components/Home/WakatimeLayoutSection.js"; +import { CardType } from "../../models/CardType.js"; +import type { CardUrlBuilder } from "../../models/CardUrl.js"; +import type { StageIndex } from "../../models/Stage.js"; import type { CardOptions } from "../cardOptions"; /** @@ -209,13 +211,12 @@ export function CustomizeStage({ description={ <> Set your{" "} - WakaTime - {" "} + {" "} username to fetch your stats. } @@ -399,13 +400,12 @@ export function CustomizeStage({ )}

For more customization options check the{" "} - customization documentation - {" "} + {" "} after you copied your card URL in step 5.
diff --git a/apps/frontend/src/pages/Home/stages/Display.tsx b/apps/frontend/src/wizard/Home/stages/Display.tsx similarity index 91% rename from apps/frontend/src/pages/Home/stages/Display.tsx rename to apps/frontend/src/wizard/Home/stages/Display.tsx index a04e1f7ad8ad7..e366ca752c20f 100644 --- a/apps/frontend/src/pages/Home/stages/Display.tsx +++ b/apps/frontend/src/wizard/Home/stages/Display.tsx @@ -2,12 +2,12 @@ import type { JSX } from "react"; import { toast } from "react-toastify"; import { saveSvgAsPng } from "save-svg-as-png"; -import { CardImage } from "../../../components/Card/CardImage"; -import { getCardThemeBackdrop } from "../../../components/Card/themeBackdrop"; -import { Button } from "../../../components/Generic/Button"; import { HOST } from "../../../constants"; -import type { CardUrlBuilder } from "../../../models/CardUrl"; import { useTheme } from "../../../redux/selectors/themeSelectors"; +import { CardImage } from "../../components/Card/CardImage"; +import { getCardThemeBackdrop } from "../../components/Card/themeBackdrop"; +import { Button } from "../../components/Generic/Button"; +import type { CardUrlBuilder } from "../../models/CardUrl"; interface DisplayStageProps { filename: string; diff --git a/apps/frontend/src/pages/Home/stages/Login/Login.tsx b/apps/frontend/src/wizard/Home/stages/Login/Login.tsx similarity index 100% rename from apps/frontend/src/pages/Home/stages/Login/Login.tsx rename to apps/frontend/src/wizard/Home/stages/Login/Login.tsx diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx b/apps/frontend/src/wizard/Home/stages/Login/LoginAccountDeleteModal.tsx similarity index 96% rename from apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx rename to apps/frontend/src/wizard/Home/stages/Login/LoginAccountDeleteModal.tsx index 905205d210ca3..85d87ad472d9d 100644 --- a/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx +++ b/apps/frontend/src/wizard/Home/stages/Login/LoginAccountDeleteModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react"; import type { JSX, RefObject } from "react"; import { createPortal } from "react-dom"; -import { Button } from "../../../../components/Generic/Button"; +import { Button } from "../../../components/Generic/Button"; function useOutsideAlerter( ref: RefObject, diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginAccountManagement.tsx b/apps/frontend/src/wizard/Home/stages/Login/LoginAccountManagement.tsx similarity index 97% rename from apps/frontend/src/pages/Home/stages/Login/LoginAccountManagement.tsx rename to apps/frontend/src/wizard/Home/stages/Login/LoginAccountManagement.tsx index f3f6e7a900df6..d5792a6b578e8 100644 --- a/apps/frontend/src/pages/Home/stages/Login/LoginAccountManagement.tsx +++ b/apps/frontend/src/wizard/Home/stages/Login/LoginAccountManagement.tsx @@ -3,8 +3,6 @@ import type { JSX } from "react"; import { FaGithub as GithubIcon } from "react-icons/fa"; import { useDispatch } from "react-redux"; -import { deleteAccount } from "../../../../api/user"; -import { Button } from "../../../../components/Generic/Button"; import { CLIENT_ID, GITHUB_PRIVATE_AUTH_URL, @@ -16,6 +14,8 @@ import { useUserKey, } from "../../../../redux/selectors/userSelectors"; import { logout } from "../../../../redux/slices/user"; +import { deleteAccount } from "../../../api/user"; +import { Button } from "../../../components/Generic/Button"; import { LoginAccountDeleteModal } from "./LoginAccountDeleteModal"; import { LoginBox } from "./LoginBox"; diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginBox.tsx b/apps/frontend/src/wizard/Home/stages/Login/LoginBox.tsx similarity index 100% rename from apps/frontend/src/pages/Home/stages/Login/LoginBox.tsx rename to apps/frontend/src/wizard/Home/stages/Login/LoginBox.tsx diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginBoxDemoCards.tsx b/apps/frontend/src/wizard/Home/stages/Login/LoginBoxDemoCards.tsx similarity index 91% rename from apps/frontend/src/pages/Home/stages/Login/LoginBoxDemoCards.tsx rename to apps/frontend/src/wizard/Home/stages/Login/LoginBoxDemoCards.tsx index d8b92ea622a41..5256c9899a62e 100644 --- a/apps/frontend/src/pages/Home/stages/Login/LoginBoxDemoCards.tsx +++ b/apps/frontend/src/wizard/Home/stages/Login/LoginBoxDemoCards.tsx @@ -1,15 +1,15 @@ import type { JSX } from "react"; -import { CardImage } from "../../../../components/Card/CardImage"; import { DEMO_GIST, DEMO_REPO, DEMO_USER, DEMO_WAKATIME_USER, } from "../../../../constants"; -import { CardType } from "../../../../models/CardType"; -import { cardUrl } from "../../../../models/CardUrl"; import { useTheme } from "../../../../redux/selectors/themeSelectors"; +import { CardImage } from "../../../components/Card/CardImage"; +import { CardType } from "../../../models/CardType"; +import { cardUrl } from "../../../models/CardUrl"; const cards = [ cardUrl(CardType.PIN).repo(DEMO_REPO).disableAnimations(), diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginOptions.tsx b/apps/frontend/src/wizard/Home/stages/Login/LoginOptions.tsx similarity index 96% rename from apps/frontend/src/pages/Home/stages/Login/LoginOptions.tsx rename to apps/frontend/src/wizard/Home/stages/Login/LoginOptions.tsx index 6ed1a7a639492..813edc7fcabb7 100644 --- a/apps/frontend/src/pages/Home/stages/Login/LoginOptions.tsx +++ b/apps/frontend/src/wizard/Home/stages/Login/LoginOptions.tsx @@ -1,11 +1,11 @@ import type { JSX } from "react"; import { FaGithub as GithubIcon } from "react-icons/fa"; -import { Button } from "../../../../components/Generic/Button"; import { GITHUB_PRIVATE_AUTH_URL, GITHUB_PUBLIC_AUTH_URL, } from "../../../../constants"; +import { Button } from "../../../components/Generic/Button"; import { LoginBox } from "./LoginBox"; diff --git a/apps/frontend/src/pages/Home/stages/SelectCard.tsx b/apps/frontend/src/wizard/Home/stages/SelectCard.tsx similarity index 92% rename from apps/frontend/src/pages/Home/stages/SelectCard.tsx rename to apps/frontend/src/wizard/Home/stages/SelectCard.tsx index b961253916e05..208ca4efc2c91 100644 --- a/apps/frontend/src/pages/Home/stages/SelectCard.tsx +++ b/apps/frontend/src/wizard/Home/stages/SelectCard.tsx @@ -1,18 +1,18 @@ import { useMemo } from "react"; import type { JSX } from "react"; -import { Card } from "../../../components/Card/Card"; import { DEMO_GIST, DEMO_REPO, DEMO_USER, DEMO_WAKATIME_USER, } from "../../../constants"; -import { CardType } from "../../../models/CardType"; -import { cardUrl } from "../../../models/CardUrl"; -import type { CardUrlBuilder } from "../../../models/CardUrl"; import { useTheme } from "../../../redux/selectors/themeSelectors"; import { useUserId } from "../../../redux/selectors/userSelectors"; +import { Card } from "../../components/Card/Card"; +import { CardType } from "../../models/CardType"; +import { cardUrl } from "../../models/CardUrl"; +import type { CardUrlBuilder } from "../../models/CardUrl"; interface SelectCardStageProps { selectedCardType: CardType; diff --git a/apps/frontend/src/pages/Home/stages/Theme.tsx b/apps/frontend/src/wizard/Home/stages/Theme.tsx similarity index 85% rename from apps/frontend/src/pages/Home/stages/Theme.tsx rename to apps/frontend/src/wizard/Home/stages/Theme.tsx index 7d386b812cb91..6129e689d3121 100644 --- a/apps/frontend/src/pages/Home/stages/Theme.tsx +++ b/apps/frontend/src/wizard/Home/stages/Theme.tsx @@ -1,13 +1,15 @@ import { themes } from "@stats-organization/github-readme-stats-core"; import type { JSX } from "react"; -import { Card } from "../../../components/Card/Card"; +import { useTheme } from "../../../redux/selectors/themeSelectors"; +import { docsUrl } from "../../../route"; +import { LinkExternal } from "../../../shared/LinkExternal"; +import { Card } from "../../components/Card/Card"; import { getCardThemeBackdrop, getThemeSortRank, -} from "../../../components/Card/themeBackdrop"; -import type { CardUrlBuilder } from "../../../models/CardUrl"; -import { useTheme } from "../../../redux/selectors/themeSelectors"; +} from "../../components/Card/themeBackdrop"; +import type { CardUrlBuilder } from "../../models/CardUrl"; const excludedThemes = [ "merko", @@ -69,13 +71,12 @@ export function ThemeStage({
{"For more theme options check the "} - customization documentation - + {" after you copied your card URL in step 5."}
diff --git a/apps/frontend/src/pages/Home/useCardDescriptor.ts b/apps/frontend/src/wizard/Home/useCardDescriptor.ts similarity index 95% rename from apps/frontend/src/pages/Home/useCardDescriptor.ts rename to apps/frontend/src/wizard/Home/useCardDescriptor.ts index 56d5fb67763d5..1723ffba4986a 100644 --- a/apps/frontend/src/pages/Home/useCardDescriptor.ts +++ b/apps/frontend/src/wizard/Home/useCardDescriptor.ts @@ -8,8 +8,8 @@ import { DEMO_WAKATIME_USER, HOST, } from "../../constants"; -import { CardType } from "../../models/CardType"; -import type { CardUrlBuilder } from "../../models/CardUrl"; +import { CardType } from "../models/CardType"; +import type { CardUrlBuilder } from "../models/CardUrl"; /** Per-card metadata for the final display stage. */ export interface CardDescriptor { diff --git a/apps/frontend/src/pages/App/AppTrends.tsx b/apps/frontend/src/wizard/Wizard.tsx similarity index 89% rename from apps/frontend/src/pages/App/AppTrends.tsx rename to apps/frontend/src/wizard/Wizard.tsx index 6b557e2a48e8c..867916361550c 100644 --- a/apps/frontend/src/pages/App/AppTrends.tsx +++ b/apps/frontend/src/wizard/Wizard.tsx @@ -2,22 +2,22 @@ import { useEffect, useState } from "react"; import { useDispatch } from "react-redux"; import { ToastContainer, toast } from "react-toastify"; -import { getUserMetadata } from "../../api/user"; -import { clearAxiosCache } from "../../axios-override"; -import type { StageIndex } from "../../models/Stage"; -import { useTheme } from "../../redux/selectors/themeSelectors"; +import { clearAxiosCache } from "../axios-override"; +import { useTheme } from "../redux/selectors/themeSelectors"; import { useIsAuthenticated, useUserKey, useUserToken, -} from "../../redux/selectors/userSelectors"; +} from "../redux/selectors/userSelectors"; import { logout as _logout, setUserAccess as _setUserAccess, -} from "../../redux/slices/user"; -import { HomeScreen } from "../Home/Home"; +} from "../redux/slices/user"; import { Header } from "./Header"; +import { HomeScreen } from "./Home/Home"; +import { getUserMetadata } from "./api/user"; +import type { StageIndex } from "./models/Stage"; const toMessage = ( input: string | ErrorEvent | PromiseRejectionEvent, @@ -53,7 +53,7 @@ const showError = (event: ErrorEvent | PromiseRejectionEvent): void => { }); }; -export function AppTrends() { +export function Wizard() { const userKey = useUserKey(); const userToken = useUserToken(); const isAuthenticated = useIsAuthenticated(); diff --git a/apps/frontend/src/api/user.ts b/apps/frontend/src/wizard/api/user.ts similarity index 97% rename from apps/frontend/src/api/user.ts rename to apps/frontend/src/wizard/api/user.ts index 4fe73bb2f63fa..445f013660e0e 100644 --- a/apps/frontend/src/api/user.ts +++ b/apps/frontend/src/wizard/api/user.ts @@ -1,6 +1,6 @@ import axios from "axios"; -import { HOST } from "../constants"; +import { HOST } from "../../constants"; const authenticate = async ( code: string, diff --git a/apps/frontend/src/components/Card/Card.tsx b/apps/frontend/src/wizard/components/Card/Card.tsx similarity index 100% rename from apps/frontend/src/components/Card/Card.tsx rename to apps/frontend/src/wizard/components/Card/Card.tsx diff --git a/apps/frontend/src/components/Card/CardImage.tsx b/apps/frontend/src/wizard/components/Card/CardImage.tsx similarity index 94% rename from apps/frontend/src/components/Card/CardImage.tsx rename to apps/frontend/src/wizard/components/Card/CardImage.tsx index 9d0e73a620f49..09519100c6324 100644 --- a/apps/frontend/src/components/Card/CardImage.tsx +++ b/apps/frontend/src/wizard/components/Card/CardImage.tsx @@ -1,6 +1,6 @@ import { clsx } from "clsx"; -import { HOST } from "../../constants"; +import { HOST } from "../../../constants"; import type { CardUrlBuilder } from "../../models/CardUrl"; import { SvgInline } from "./SvgInline"; diff --git a/apps/frontend/src/components/Card/SvgInline.tsx b/apps/frontend/src/wizard/components/Card/SvgInline.tsx similarity index 97% rename from apps/frontend/src/components/Card/SvgInline.tsx rename to apps/frontend/src/wizard/components/Card/SvgInline.tsx index 68d0bd758347c..c5cd9b47307a0 100644 --- a/apps/frontend/src/components/Card/SvgInline.tsx +++ b/apps/frontend/src/wizard/components/Card/SvgInline.tsx @@ -7,12 +7,12 @@ import type { JSX } from "react"; import Skeleton from "react-loading-skeleton"; import "react-loading-skeleton/dist/skeleton.css"; -import { setShouldMock } from "../../axios-override.js"; -import { createMockRequest, createMockResponse } from "../../mock-http.js"; +import { setShouldMock } from "../../../axios-override.js"; import { useIsAuthenticated, useUserToken, -} from "../../redux/selectors/userSelectors.js"; +} from "../../../redux/selectors/userSelectors.js"; +import { createMockRequest, createMockResponse } from "../../mock-http.js"; interface SvgInlineProps { url: string; diff --git a/apps/frontend/src/components/Card/themeBackdrop.ts b/apps/frontend/src/wizard/components/Card/themeBackdrop.ts similarity index 100% rename from apps/frontend/src/components/Card/themeBackdrop.ts rename to apps/frontend/src/wizard/components/Card/themeBackdrop.ts diff --git a/apps/frontend/src/components/Generic/Button.tsx b/apps/frontend/src/wizard/components/Generic/Button.tsx similarity index 100% rename from apps/frontend/src/components/Generic/Button.tsx rename to apps/frontend/src/wizard/components/Generic/Button.tsx diff --git a/apps/frontend/src/components/Generic/Checkbox.tsx b/apps/frontend/src/wizard/components/Generic/Checkbox.tsx similarity index 100% rename from apps/frontend/src/components/Generic/Checkbox.tsx rename to apps/frontend/src/wizard/components/Generic/Checkbox.tsx diff --git a/apps/frontend/src/components/Generic/Select.tsx b/apps/frontend/src/wizard/components/Generic/Select.tsx similarity index 100% rename from apps/frontend/src/components/Generic/Select.tsx rename to apps/frontend/src/wizard/components/Generic/Select.tsx diff --git a/apps/frontend/src/components/Home/CheckboxSection.tsx b/apps/frontend/src/wizard/components/Home/CheckboxSection.tsx similarity index 100% rename from apps/frontend/src/components/Home/CheckboxSection.tsx rename to apps/frontend/src/wizard/components/Home/CheckboxSection.tsx diff --git a/apps/frontend/src/components/Home/LanguagesLayoutSection.tsx b/apps/frontend/src/wizard/components/Home/LanguagesLayoutSection.tsx similarity index 100% rename from apps/frontend/src/components/Home/LanguagesLayoutSection.tsx rename to apps/frontend/src/wizard/components/Home/LanguagesLayoutSection.tsx diff --git a/apps/frontend/src/components/Home/NumericSection.tsx b/apps/frontend/src/wizard/components/Home/NumericSection.tsx similarity index 100% rename from apps/frontend/src/components/Home/NumericSection.tsx rename to apps/frontend/src/wizard/components/Home/NumericSection.tsx diff --git a/apps/frontend/src/components/Home/Progress.tsx b/apps/frontend/src/wizard/components/Home/Progress.tsx similarity index 100% rename from apps/frontend/src/components/Home/Progress.tsx rename to apps/frontend/src/wizard/components/Home/Progress.tsx diff --git a/apps/frontend/src/components/Home/Section.tsx b/apps/frontend/src/wizard/components/Home/Section.tsx similarity index 100% rename from apps/frontend/src/components/Home/Section.tsx rename to apps/frontend/src/wizard/components/Home/Section.tsx diff --git a/apps/frontend/src/components/Home/StatsRankSection.tsx b/apps/frontend/src/wizard/components/Home/StatsRankSection.tsx similarity index 100% rename from apps/frontend/src/components/Home/StatsRankSection.tsx rename to apps/frontend/src/wizard/components/Home/StatsRankSection.tsx diff --git a/apps/frontend/src/components/Home/TextSection.tsx b/apps/frontend/src/wizard/components/Home/TextSection.tsx similarity index 100% rename from apps/frontend/src/components/Home/TextSection.tsx rename to apps/frontend/src/wizard/components/Home/TextSection.tsx diff --git a/apps/frontend/src/components/Home/WakatimeLayoutSection.tsx b/apps/frontend/src/wizard/components/Home/WakatimeLayoutSection.tsx similarity index 100% rename from apps/frontend/src/components/Home/WakatimeLayoutSection.tsx rename to apps/frontend/src/wizard/components/Home/WakatimeLayoutSection.tsx diff --git a/apps/frontend/src/hooks/useDebouncedField.ts b/apps/frontend/src/wizard/hooks/useDebouncedField.ts similarity index 100% rename from apps/frontend/src/hooks/useDebouncedField.ts rename to apps/frontend/src/wizard/hooks/useDebouncedField.ts diff --git a/apps/frontend/src/mock-http.ts b/apps/frontend/src/wizard/mock-http.ts similarity index 100% rename from apps/frontend/src/mock-http.ts rename to apps/frontend/src/wizard/mock-http.ts diff --git a/apps/frontend/src/mockData/additional_user_stars.json b/apps/frontend/src/wizard/mockData/additional_user_stars.json similarity index 100% rename from apps/frontend/src/mockData/additional_user_stars.json rename to apps/frontend/src/wizard/mockData/additional_user_stars.json diff --git a/apps/frontend/src/mockData/commented_issues.json b/apps/frontend/src/wizard/mockData/commented_issues.json similarity index 100% rename from apps/frontend/src/mockData/commented_issues.json rename to apps/frontend/src/wizard/mockData/commented_issues.json diff --git a/apps/frontend/src/mockData/commented_prs.json b/apps/frontend/src/wizard/mockData/commented_prs.json similarity index 100% rename from apps/frontend/src/mockData/commented_prs.json rename to apps/frontend/src/wizard/mockData/commented_prs.json diff --git a/apps/frontend/src/mockData/commits.json b/apps/frontend/src/wizard/mockData/commits.json similarity index 100% rename from apps/frontend/src/mockData/commits.json rename to apps/frontend/src/wizard/mockData/commits.json diff --git a/apps/frontend/src/mockData/gist-graphql.json b/apps/frontend/src/wizard/mockData/gist-graphql.json similarity index 100% rename from apps/frontend/src/mockData/gist-graphql.json rename to apps/frontend/src/wizard/mockData/gist-graphql.json diff --git a/apps/frontend/src/mockData/gist-rest.json b/apps/frontend/src/wizard/mockData/gist-rest.json similarity index 100% rename from apps/frontend/src/mockData/gist-rest.json rename to apps/frontend/src/wizard/mockData/gist-rest.json diff --git a/apps/frontend/src/mockData/repository.json b/apps/frontend/src/wizard/mockData/repository.json similarity index 100% rename from apps/frontend/src/mockData/repository.json rename to apps/frontend/src/wizard/mockData/repository.json diff --git a/apps/frontend/src/mockData/reviewed_prs.json b/apps/frontend/src/wizard/mockData/reviewed_prs.json similarity index 100% rename from apps/frontend/src/mockData/reviewed_prs.json rename to apps/frontend/src/wizard/mockData/reviewed_prs.json diff --git a/apps/frontend/src/mockData/top_languages.json b/apps/frontend/src/wizard/mockData/top_languages.json similarity index 100% rename from apps/frontend/src/mockData/top_languages.json rename to apps/frontend/src/wizard/mockData/top_languages.json diff --git a/apps/frontend/src/mockData/user_stats.json b/apps/frontend/src/wizard/mockData/user_stats.json similarity index 100% rename from apps/frontend/src/mockData/user_stats.json rename to apps/frontend/src/wizard/mockData/user_stats.json diff --git a/apps/frontend/src/mockData/wakatime_proxy.json b/apps/frontend/src/wizard/mockData/wakatime_proxy.json similarity index 100% rename from apps/frontend/src/mockData/wakatime_proxy.json rename to apps/frontend/src/wizard/mockData/wakatime_proxy.json diff --git a/apps/frontend/src/models/CardType.ts b/apps/frontend/src/wizard/models/CardType.ts similarity index 100% rename from apps/frontend/src/models/CardType.ts rename to apps/frontend/src/wizard/models/CardType.ts diff --git a/apps/frontend/src/models/CardUrl.test.ts b/apps/frontend/src/wizard/models/CardUrl.test.ts similarity index 100% rename from apps/frontend/src/models/CardUrl.test.ts rename to apps/frontend/src/wizard/models/CardUrl.test.ts diff --git a/apps/frontend/src/models/CardUrl.ts b/apps/frontend/src/wizard/models/CardUrl.ts similarity index 100% rename from apps/frontend/src/models/CardUrl.ts rename to apps/frontend/src/wizard/models/CardUrl.ts diff --git a/apps/frontend/src/models/Stage.ts b/apps/frontend/src/wizard/models/Stage.ts similarity index 100% rename from apps/frontend/src/models/Stage.ts rename to apps/frontend/src/wizard/models/Stage.ts diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index dc47bb39d7c7b..83f354755084a 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -2,12 +2,56 @@ import path from "node:path"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; +import { default as MarkdownIt } from "markdown-it"; +import { default as anchor } from "markdown-it-anchor"; +import { default as githubAlerts } from "markdown-it-github-alerts"; +import { Mode, plugin as markdown } from "vite-plugin-markdown"; import { defineProject } from "vitest/config"; +import { slugify } from "./src/docs/slugify"; + +/** + * `html: true` keeps + * - the raw `` elements the sources use for theme-aware card samples + * - the alerts plugin renders GitHub's + * `> [!NOTE]` blockquotes the same way github.com does. + * + * Heading ids and their permalinks are done here rather than in + * `transformDocHtml`, because headings are plain markdown and markdown-it + * already has them as tokens. The rest of the rewriting stays at runtime: it + * has to reach inside raw HTML blocks, which markdown-it passes through + * untouched, and it resolves local images to hashed Vite asset URLs, which are + * only known to the bundler. + */ +const markdownIt = MarkdownIt({ html: true }) + .use(githubAlerts) + .use(anchor, { + // The page title is the whole document, so it needs no link of its own. + level: [2, 3, 4], + slugify, + permalink: anchor.permalink.linkInsideHeader({ + class: "heading-anchor", + symbol: "#", + placement: "after", + // `docs.css` owns the gap. + space: false, + // The link is decorative next to its heading text, but it is still a + // real tab stop, so it gets a label rather than being hidden. + ariaHidden: false, + renderAttrs: (slug) => ({ "aria-label": `Link to ${slug}` }), + }), + }); + // https://vitejs.dev/config/ export default defineProject({ base: "/frontend/", - plugins: [react(), tailwindcss()], + plugins: [ + react(), + tailwindcss(), + // Compiles the documentation markdown to HTML at build time, so no markdown + // parser is shipped to the browser. + markdown({ mode: [Mode.HTML], markdownIt }), + ], build: { outDir: "build", sourcemap: true, @@ -17,6 +61,24 @@ export default defineProject({ rolldownOptions: { external: ["pg"], + + onwarn(warning, defaultHandler) { + /** + * `vite-plugin-markdown` transforms each documentation file without emitting a sourcemap, + * so every one of them raises `SOURCEMAP_BROKEN`. + * @see https://github.com/hmsk/vite-plugin-markdown/issues/474 + * Reported upstream, closed without a fix and not re-opened after a later report. + * The compiled output is markup rather than code, + * so there is nothing to map back to and the warning carries no information. + */ + if ( + warning.code === "SOURCEMAP_BROKEN" && + warning.plugin === "vite-plugin-markdown" + ) { + return; + } + defaultHandler(warning); + }, }, }, resolve: { diff --git a/docs/advanced_documentation.md b/docs/advanced_documentation.md index 01d2acb36f522..b44abed1075a0 100644 --- a/docs/advanced_documentation.md +++ b/docs/advanced_documentation.md @@ -104,7 +104,7 @@ You can use [GitHub's new media feature](https://github.blog/changelog/2022-05-1 ```
-:eyes: Show example +👀 Show example -:eyes: Show example +👀 Show example [![Anurag's GitHub stats-Dark](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=dark#gh-dark-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-dark-mode-only) [![Anurag's GitHub stats-Light](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=default#gh-light-mode-only)](https://github.com/stats-organization/github-stats-extended#gh-light-mode-only) @@ -143,7 +143,7 @@ We have included a `transparent` theme that has a transparent background. This t ```
-:eyes: Show example +👀 Show example ![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&theme=transparent) @@ -158,7 +158,7 @@ You can use the `bg_color` parameter to make any of [the available themes](../pa ```
-:eyes: Show example +👀 Show example ![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&bg_color=00000000) @@ -703,7 +703,7 @@ By default, GitHub does not lay out the cards side by side. To do that, you can ```
-:eyes: Show example +👀 Show example -:eyes: Show example +👀 Show example diff --git a/docs/deploy.md b/docs/deploy.md index b3de00cbfa62c..c4641664d9fbb 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -74,7 +74,7 @@ Selecting the right scopes for your token is important in case you want to displ #### Fine-grained token -> [!WARNING]\ +> [!WARNING] > This limits the scope of commits to public repositories only. - Go to [Account → Settings → Developer Settings → Personal access tokens → Fine-grained tokens](https://github.com/settings/personal-access-tokens). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000000..ecb0c95872e56 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,97 @@ +# GitHub Stats Extended + +Dynamically generate GitHub stats for your READMEs. + +GitHub-Stats-Extended is the [extended, actively maintained successor](fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates stats cards about your GitHub contributions, your top languages and more, which you can [customize](advanced_documentation.md) through a large set of parameters. + + + + Anurag's GitHub stats + + +## Quick Start + +Copy and paste this into your markdown, then change the `?username=` value to your GitHub username: + +```md +[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended) +``` + +As a more comfortable alternative, use the [card wizard](https://github-stats-extended.vercel.app/frontend) to configure your card visually, then copy the generated markdown into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme). + +## Card Types + +### Stats Card + + + + Anurag's GitHub stats + + +### Top Languages Card + + + + Top Langs + + +### WakaTime Card + + + + Alan's WakaTime stats + + +### Repo Card + + + + Readme Card + + +### Gist Card + + + + Gist Card + + +## Migration from github-readme-stats + +To migrate from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) you only need to change the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`: + +```diff +- https://github-readme-stats.vercel.app/api?username=octocat&theme=radical ++ https://github-stats-extended.vercel.app/api?username=octocat&theme=radical +``` + +GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For details see [Compatibility Notes](fork.md#compatibility-notes). + +## Where to next + +- [Advanced Customization](advanced_documentation.md) — every parameter the cards accept. +- [Available Themes](../packages/core/src/themes/README.md) — the built-in themes, rendered as live samples. +- [Run It Yourself](deploy.md) — GitHub Actions or a self-hosted Vercel deployment. +- [Fork Information](fork.md) — what this project adds on top of github-readme-stats. + +## Acknowledgements + +This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). The card wizard is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db4b3b676f80e..28037d7296be1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,9 @@ importers: specifier: ^14.0.1 version: 14.0.1 devDependencies: + '@types/markdown-it': + specifier: 14.1.2 + version: 14.1.2 '@types/react': specifier: 19.2.17 version: 19.2.17 @@ -178,12 +181,27 @@ importers: clsx: specifier: 2.1.1 version: 2.1.1 + jsdom: + specifier: catalog:default + version: 29.1.1 + markdown-it: + specifier: 14.3.0 + version: 14.3.0 + markdown-it-anchor: + specifier: 9.2.1 + version: 9.2.1(@types/markdown-it@14.1.2)(markdown-it@14.3.0) + markdown-it-github-alerts: + specifier: 1.0.1 + version: 1.0.1(markdown-it@14.3.0) tailwindcss: specifier: 4.3.3 version: 4.3.3 vite: specifier: catalog:default version: 8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0) + vite-plugin-markdown: + specifier: 2.2.0 + version: 2.2.0(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)) vitest: specifier: catalog:default version: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)) @@ -1106,6 +1124,15 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} @@ -1402,6 +1429,9 @@ packages: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1599,6 +1629,19 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1620,6 +1663,16 @@ packages: resolution: {integrity: sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==} engines: {node: '>=10.13.0'} + entities@2.1.0: + resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@8.0.0: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} @@ -1763,6 +1816,11 @@ packages: resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -1866,6 +1924,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + front-matter@4.0.2: + resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1948,6 +2009,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -2070,6 +2134,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.15.1: + resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} + hasBin: true + js-yaml@5.2.1: resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} hasBin: true @@ -2182,6 +2250,12 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + linkify-it@3.0.3: + resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + + linkify-it@5.0.2: + resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} + lint-staged@17.0.8: resolution: {integrity: sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==} engines: {node: '>=22.22.1'} @@ -2225,6 +2299,25 @@ packages: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} + markdown-it-anchor@9.2.1: + resolution: {integrity: sha512-p6APiLJDFAW2GEvaavDvhIBn7jrX2jLv77NkBGgNacFTurbORYc4pyYySg/mI6mpR6cHQuAtzKtmqgQr4K8dsQ==} + peerDependencies: + '@types/markdown-it': '*' + markdown-it: '*' + + markdown-it-github-alerts@1.0.1: + resolution: {integrity: sha512-NNATF4QdoGI07hyCitoB2YqJ1YcNVCKT89ut2VtfFY9rkeFCXe/V2lOonKQLpJiq5DjiZZepf97BJx5xOjFIAw==} + peerDependencies: + markdown-it: '>= 13.0.0' + + markdown-it@12.3.2: + resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} + hasBin: true + + markdown-it@14.3.0: + resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2232,6 +2325,12 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + mdurl@2.1.0: + resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} + media-typer@1.1.1: resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} @@ -2463,6 +2562,10 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2659,6 +2762,9 @@ packages: sponge-case@2.0.3: resolution: {integrity: sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -2797,6 +2903,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbash@4.0.4: resolution: {integrity: sha512-60m9IVGbavD6jholbxt0jVBXZkEB/HsMZq7Tyaghseve2/Sf0zQRAIfWsD34sde+DKP2tBxJS2wP88ZM0D1FhA==} engines: {node: '>=14'} @@ -2835,6 +2947,11 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-plugin-markdown@2.2.0: + resolution: {integrity: sha512-eH2tXMZcx3EHb5okd+/0VIyoR8Gp9pGe24UXitOOcGkzObbJ1vl48aGOAbakoT88FBdzC8MXNkMfBIB9VK0Ndg==} + peerDependencies: + vite: '>= 2.0.0' + vite@8.1.5: resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3735,6 +3852,15 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + '@types/node@24.13.3': dependencies: undici-types: 7.18.2 @@ -4014,6 +4140,10 @@ snapshots: are-docs-informative@0.0.2: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-query@5.3.0: @@ -4193,6 +4323,24 @@ snapshots: dom-accessibility-api@0.6.3: {} + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4212,6 +4360,12 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + entities@2.1.0: {} + + entities@2.2.0: {} + + entities@4.5.0: {} + entities@8.0.0: {} environment@1.1.0: {} @@ -4444,6 +4598,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -4560,6 +4716,10 @@ snapshots: fresh@2.0.0: {} + front-matter@4.0.2: + dependencies: + js-yaml: 3.15.1 + fsevents@2.3.2: optional: true @@ -4633,6 +4793,13 @@ snapshots: html-escaper@2.0.2: {} + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -4734,6 +4901,11 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@3.15.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@5.2.1: dependencies: argparse: 2.0.1 @@ -4846,6 +5018,14 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + linkify-it@3.0.3: + dependencies: + uc.micro: 1.0.6 + + linkify-it@5.0.2: + dependencies: + uc.micro: 2.1.0 + lint-staged@17.0.8: dependencies: listr2: 10.2.2 @@ -4899,10 +5079,40 @@ snapshots: map-cache@0.2.2: {} + markdown-it-anchor@9.2.1(@types/markdown-it@14.1.2)(markdown-it@14.3.0): + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.3.0 + + markdown-it-github-alerts@1.0.1(markdown-it@14.3.0): + dependencies: + markdown-it: 14.3.0 + + markdown-it@12.3.2: + dependencies: + argparse: 2.0.1 + entities: 2.1.0 + linkify-it: 3.0.3 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + markdown-it@14.3.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.2 + mdurl: 2.1.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + math-intrinsics@1.1.0: {} mdn-data@2.27.1: {} + mdurl@1.0.1: {} + + mdurl@2.1.0: {} + media-typer@1.1.1: {} merge-descriptors@2.0.0: {} @@ -5133,6 +5343,8 @@ snapshots: proxy-from-env@2.1.0: {} + punycode.js@2.3.1: {} + punycode@2.3.1: {} qs@6.15.3: @@ -5347,6 +5559,8 @@ snapshots: sponge-case@2.0.3: {} + sprintf-js@1.0.3: {} + stable-hash-x@0.2.0: {} stackback@0.0.2: {} @@ -5470,6 +5684,10 @@ snapshots: typescript@6.0.3: {} + uc.micro@1.0.6: {} + + uc.micro@2.1.0: {} + unbash@4.0.4: {} unc-path-regex@0.1.2: {} @@ -5519,6 +5737,14 @@ snapshots: vary@1.1.2: {} + vite-plugin-markdown@2.2.0(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + domhandler: 4.3.1 + front-matter: 4.0.2 + htmlparser2: 6.1.0 + markdown-it: 12.3.2 + vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0) + vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 diff --git a/vercel-preparation.sh b/vercel-preparation.sh index 410356e23bfac..6f74d5c12f956 100755 --- a/vercel-preparation.sh +++ b/vercel-preparation.sh @@ -15,5 +15,9 @@ cp -RP apps/backend/.vercel/output/functions/api.func/_dot_vercel_copy/output ap rm -rf apps/deployment pnpm install pnpm build:frontend +# serve the built app at `/frontend`, matching the Vite `base` its assets are requested under mkdir -p apps/backend/.vercel/output/static/frontend/ cp -RP apps/frontend/build/. apps/backend/.vercel/output/static/frontend/ +# serve the same app at `/frontend/docs`, where it renders the docs instead of the wizard +mkdir -p apps/backend/.vercel/output/static/frontend/docs/ +cp -P apps/frontend/build/index.html apps/backend/.vercel/output/static/frontend/docs/index.html