replace old docs

This commit is contained in:
MapleLeaf
2022-01-03 04:20:29 -06:00
committed by Darius
parent 557fb4f8dc
commit e46b62f888
63 changed files with 67 additions and 1113 deletions

View File

@@ -1,25 +0,0 @@
import React from "react"
import { createRoot } from "react-dom"
import { HeadProvider } from "react-head"
import type { PageContextBuiltInClient } from "vite-plugin-ssr/client"
import { getPage } from "vite-plugin-ssr/client"
import { App } from "./app"
import { RouteContextProvider } from "./route-context"
const context = await getPage<PageContextBuiltInClient>()
createRoot(document.querySelector("#app")!).render(
<HeadProvider>
<RouteContextProvider value={{ routeParams: {}, ...(context as any) }}>
<App>
<context.Page />
</App>
</RouteContextProvider>
</HeadProvider>,
)
declare module "react-dom" {
export function createRoot(element: Element): {
render(element: React.ReactNode): void
}
}

View File

@@ -1,49 +0,0 @@
import React from "react"
import { renderToStaticMarkup, renderToString } from "react-dom/server.js"
import { HeadProvider } from "react-head"
import type { PageContextBuiltIn } from "vite-plugin-ssr"
import { dangerouslySkipEscape, escapeInject } from "vite-plugin-ssr"
import { App } from "./app"
import { RouteContextProvider } from "./route-context"
export const passToClient = ["routeParams", "pageData"]
export function render(context: PageContextBuiltIn) {
const headTags: React.ReactElement[] = []
const pageHtml = renderToString(
<HeadProvider headTags={headTags}>
<RouteContextProvider value={context}>
<App>
<context.Page />
</App>
</RouteContextProvider>
</HeadProvider>,
)
const documentHtml = escapeInject/* HTML */ `
<!DOCTYPE html>
<html lang="en" class="bg-slate-900 text-slate-100">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin=""
/>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&family=Rubik:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
rel="stylesheet"
/>
${dangerouslySkipEscape(renderToStaticMarkup(<>{headTags}</>))}
</head>
<body>
<div id="app" class="contents">${dangerouslySkipEscape(pageHtml)}</div>
</body>
</html>
`
return { documentHtml }
}

View File

@@ -1,14 +0,0 @@
import { description } from "reacord/package.json"
import { Meta, Title } from "react-head"
import "tailwindcss/tailwind.css"
import "./styles/prism-theme.css"
export function App({ children }: { children: React.ReactNode }) {
return (
<>
<Title>Reacord</Title>
<Meta name="description" content={description} />
{children}
</>
)
}

View File

@@ -1,3 +1,4 @@
import React from "react"
import { ExternalLink } from "./external-link"
export type AppLinkProps = {

View File

@@ -1,4 +1,5 @@
import type { ComponentPropsWithoutRef } from "react"
import React from "react"
export function ExternalLink(props: ComponentPropsWithoutRef<"a">) {
return (

View File

@@ -1,5 +1,6 @@
import clsx from "clsx"
import { guideLinks } from "../data/guide-links.preval"
import React from "react"
import { guideLinks } from "../data/guide-links"
import { useScrolled } from "../hooks/dom/use-scrolled"
import {
docsProseClass,

View File

@@ -0,0 +1,23 @@
import * as React from "react"
import { mainLinks } from "../data/main-links"
import type { AppLinkProps } from "./app-link"
import { AppLink } from "./app-link"
import { PopoverMenu } from "./popover-menu"
export type MainNavigationMobileMenuData = {
guideLinks: AppLinkProps[]
}
export function render(data: MainNavigationMobileMenuData) {
return (
<PopoverMenu>
{mainLinks.map((link) => (
<AppLink {...link} key={link.to} className={PopoverMenu.itemClass} />
))}
<hr className="border-0 h-[2px] bg-black/50" />
{data.guideLinks.map((link) => (
<AppLink {...link} key={link.to} className={PopoverMenu.itemClass} />
))}
</PopoverMenu>
)
}

View File

@@ -1,4 +1,5 @@
import { guideLinks } from "../data/guide-links.preval"
import React from "react"
import { guideLinks } from "../data/guide-links"
import { mainLinks } from "../data/main-links"
import { linkClass } from "../styles/components"
import { AppLink } from "./app-link"
@@ -15,7 +16,7 @@ export function MainNavigation() {
<AppLink {...link} key={link.to} className={linkClass} />
))}
</div>
<div className="md:hidden">
<div className="md:hidden" id="main-navigation-popover">
<PopoverMenu>
{mainLinks.map((link) => (
<AppLink

View File

@@ -0,0 +1,38 @@
import clsx from "clsx"
const menus = document.querySelectorAll("[data-popover]")
for (const menu of menus) {
const button = menu.querySelector<HTMLButtonElement>("[data-popover-button]")!
const panel = menu.querySelector<HTMLDivElement>("[data-popover-panel]")!
const panelClasses = clsx`${panel.className} transition-all`
const visibleClass = clsx`${panelClasses} visible opacity-100 translate-y-0`
const hiddenClass = clsx`${panelClasses} invisible opacity-0 translate-y-2`
let visible = false
const setVisible = (newVisible: boolean) => {
visible = newVisible
panel.className = visible ? visibleClass : hiddenClass
if (!visible) return
requestAnimationFrame(() => {
const handleClose = (event: MouseEvent) => {
if (panel.contains(event.target as Node)) return
setVisible(false)
window.removeEventListener("click", handleClose)
}
window.addEventListener("click", handleClose)
})
}
const toggleVisible = () => setVisible(!visible)
button.addEventListener("click", toggleVisible)
setVisible(false)
panel.hidden = false
}

View File

@@ -1,63 +1,22 @@
import { MenuAlt4Icon } from "@heroicons/react/outline"
import { useRect } from "@reach/rect"
import clsx from "clsx"
import { useRef, useState } from "react"
import { FocusOn } from "react-focus-on"
import React from "react"
import { linkClass } from "../styles/components"
// todo: remove useRect usage and rely on css absolute positioning instead
export function PopoverMenu({ children }: { children: React.ReactNode }) {
const [visible, setVisible] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const buttonRect = useRect(buttonRef)
const panelRef = useRef<HTMLDivElement>(null)
const panelRect = useRect(panelRef)
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
return (
<>
<button
title="Menu"
className={linkClass}
onClick={() => setVisible(!visible)}
ref={buttonRef}
>
<div data-popover className="relative">
<button data-popover-button title="Menu" className={linkClass}>
<MenuAlt4Icon className="w-6" />
</button>
<FocusOn
enabled={visible}
onClickOutside={() => setVisible(false)}
onEscapeKey={() => setVisible(false)}
<div
data-popover-panel
hidden
className="absolute w-48 bg-slate-800 rounded-lg shadow overflow-hidden max-h-[calc(100vh-4rem)] overflow-y-auto right-0 top-[calc(100%+8px)]"
>
<div
className="fixed"
style={{
left: (buttonRect?.right ?? 0) - (panelRect?.width ?? 0),
top: (buttonRect?.bottom ?? 0) + 8,
}}
onClick={() => setVisible(false)}
>
<div
className={clsx(
"transition-all",
visible
? "opacity-100 visible"
: "translate-y-2 opacity-0 invisible",
)}
>
<div ref={panelRef}>
<div className="w-48 bg-slate-800 rounded-lg shadow overflow-hidden max-h-[calc(100vh-4rem)] overflow-y-auto">
{children}
</div>
</div>
</div>
</div>
</FocusOn>
</>
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,16 @@
import type { ComponentPropsWithoutRef } from "react"
import React from "react"
import type { Merge } from "type-fest"
export function Script({
children,
...props
}: Merge<ComponentPropsWithoutRef<"script">, { children: string }>) {
return (
<script
type="module"
dangerouslySetInnerHTML={{ __html: children }}
{...props}
/>
)
}

View File

@@ -3,6 +3,7 @@ import {
DocumentTextIcon,
ExternalLinkIcon,
} from "@heroicons/react/solid"
import React from "react"
import type { AppLinkProps } from "../components/app-link"
import { inlineIconClass } from "../styles/components"

View File

@@ -0,0 +1,51 @@
import { build } from "esbuild"
import { readFile } from "node:fs/promises"
import { dirname } from "node:path"
import React from "react"
import { Script } from "../components/script"
let nextId = 0
export async function createHydrater<Data>(scriptFilePath: string) {
const id = `hydrate-root-${nextId}`
nextId += 1
const scriptSource = await readFile(scriptFilePath, "utf-8")
const scriptBuild = await build({
bundle: true,
stdin: {
contents: [scriptSource, clientBootstrap(id)].join(";\n"),
sourcefile: scriptFilePath,
loader: "tsx",
resolveDir: dirname(scriptFilePath),
},
target: ["chrome89", "firefox89"],
format: "esm",
write: false,
})
const serverModule = await import(scriptFilePath)
return function Hydrater({ data }: { data: Data }) {
return (
<>
<div id={id} data-server-data={JSON.stringify(data)}>
{serverModule.render(data)}
</div>
<Script>{scriptBuild.outputFiles[0]?.text!}</Script>
</>
)
}
}
function clientBootstrap(id: string) {
return /* ts */ `
import { createRoot } from "react-dom"
const rootElement = document.querySelector("#${id}")
const data = JSON.parse(rootElement.dataset.serverData)
createRoot(rootElement).render(render(data))
`
}

View File

@@ -1,11 +0,0 @@
import { lazy } from "react"
export function lazyNamed<
Key extends string,
Component extends React.ComponentType,
>(key: Key, loadModule: () => Promise<Record<Key, Component>>) {
return lazy<Component>(async () => {
const mod = await loadModule()
return { default: mod[key] }
})
}

View File

@@ -0,0 +1,15 @@
import grayMatter from "gray-matter"
import MarkdownIt from "markdown-it"
import prism from "markdown-it-prism"
import { readFile } from "node:fs/promises"
const renderer = new MarkdownIt({
html: true,
linkify: true,
}).use(prism)
export async function renderMarkdownFile(filePath: string) {
const { data, content } = grayMatter(await readFile(filePath, "utf8"))
const html = renderer.render(content)
return { html, data }
}

View File

@@ -0,0 +1,7 @@
import type { Response } from "express"
import { renderToStaticMarkup } from "react-dom/server.js"
export function sendJsx(res: Response, jsx: React.ReactElement) {
res.set("Content-Type", "text/html")
res.send(`<!DOCTYPE html>\n${renderToStaticMarkup(jsx)}`)
}

View File

@@ -0,0 +1,26 @@
import { build } from "esbuild"
import type { RequestHandler } from "express"
import { readFile } from "node:fs/promises"
import { dirname } from "node:path"
export async function serveCompiledScript(
scriptFilePath: string,
): Promise<RequestHandler> {
const scriptBuild = await build({
bundle: true,
stdin: {
contents: await readFile(scriptFilePath, "utf-8"),
sourcefile: scriptFilePath,
loader: "tsx",
resolveDir: dirname(scriptFilePath),
},
target: ["chrome89", "firefox89"],
format: "esm",
write: false,
})
return (req, res) => {
res.setHeader("Content-Type", "application/javascript")
res.end(scriptBuild.outputFiles[0]!.contents)
}
}

View File

@@ -0,0 +1,5 @@
import type { RequestHandler } from "express"
export function serveFile(path: string): RequestHandler {
return (req, res) => res.sendFile(path)
}

View File

@@ -0,0 +1,25 @@
import type { RequestHandler } from "express"
import { readFile } from "node:fs/promises"
import { fileURLToPath } from "node:url"
import type { Result } from "postcss"
import postcss from "postcss"
import tailwindcss from "tailwindcss"
const tailwindTemplatePath = fileURLToPath(
await import.meta.resolve!("tailwindcss/tailwind.css"),
)
const tailwindTemplate = await readFile(tailwindTemplatePath, "utf-8")
let result: Result | undefined
export function serveTailwindCss(): RequestHandler {
return async (req, res) => {
if (!result || process.env.NODE_ENV !== "production") {
result = await postcss(tailwindcss).process(tailwindTemplate, {
from: tailwindTemplatePath,
})
}
res.set("Content-Type", "text/css").send(result.css)
}
}

View File

@@ -2,7 +2,9 @@ import { useState } from "react"
import { useWindowEvent } from "./use-window-event"
export function useScrolled() {
const [scrolled, setScrolled] = useState(false)
const [scrolled, setScrolled] = useState(
typeof window !== "undefined" ? window.scrollY > 0 : false,
)
useWindowEvent("scroll", () => setScrolled(window.scrollY > 0))
return scrolled
}

View File

@@ -0,0 +1,41 @@
import packageJson from "reacord/package.json"
import type { ReactNode } from "react"
import React from "react"
export function Html({
title = "Reacord",
description = packageJson.description,
children,
}: {
title?: string
description?: string
children: ReactNode
}) {
return (
<html lang="en" className="bg-slate-900 text-slate-100">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="description" content={description} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&family=Rubik:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
rel="stylesheet"
/>
<link href="/tailwind.css" rel="stylesheet" />
<link href="/prism-theme.css" rel="stylesheet" />
<script type="module" src="/popover-menu.client.js" />
<title>{title}</title>
</head>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,72 @@
import compression from "compression"
import type { Request } from "express"
import express from "express"
import httpTerminator from "http-terminator"
import pino from "pino"
import pinoHttp from "pino-http"
import * as React from "react"
import { renderMarkdownFile } from "./helpers/markdown"
import { sendJsx } from "./helpers/send-jsx"
import { serveCompiledScript } from "./helpers/serve-compiled-script"
import { serveFile } from "./helpers/serve-file"
import { serveTailwindCss } from "./helpers/tailwind"
import DocsPage from "./pages/docs"
import { Landing } from "./pages/landing"
const logger = pino()
const port = process.env.PORT || 3000
const app = express()
.use(pinoHttp({ logger }))
.use(compression())
.get("/tailwind.css", serveTailwindCss())
.get(
"/prism-theme.css",
serveFile(new URL("./styles/prism-theme.css", import.meta.url).pathname),
)
.get(
"/popover-menu.client.js",
await serveCompiledScript(
new URL("./components/popover-menu.client.tsx", import.meta.url).pathname,
),
)
.get("/docs/*", async (req: Request<{ 0: string }>, res) => {
const { html, data } = await renderMarkdownFile(
`src/docs/${req.params[0]}.md`,
)
sendJsx(
res,
<DocsPage
title={data.title}
description={data.description}
html={html}
/>,
)
})
.get("/", (req, res) => {
sendJsx(res, <Landing />)
})
const server = app.listen(port, () => {
logger.info(`Server is running on https://localhost:${port}`)
})
const terminator = httpTerminator.createHttpTerminator({ server })
process.on("SIGINT", () => {
terminator
.terminate()
.then(() => {
logger.info("Server terminated")
})
.catch((error) => {
logger.error(error)
})
.finally(() => {
process.exit()
})
})

View File

@@ -1 +0,0 @@
export default "/docs/*"

View File

@@ -1,23 +0,0 @@
import type { OnBeforeRenderFn } from "../router-types"
export type DocsPageProps = {
title?: string
description?: string
content: string
}
export const onBeforeRender: OnBeforeRenderFn<DocsPageProps> = async (
context,
) => {
const documentPath = context.routeParams["*"]
const document = await import(`../docs/${documentPath}.md`)
return {
pageContext: {
pageData: {
title: document.attributes.title,
description: document.attributes.description,
content: document.html,
},
},
}
}

View File

@@ -1,28 +1,31 @@
import clsx from "clsx"
import { Meta, Title } from "react-head"
import React from "react"
import { AppLink } from "../components/app-link"
import { MainNavigation } from "../components/main-navigation"
import { guideLinks } from "../data/guide-links.preval"
import { useScrolled } from "../hooks/dom/use-scrolled"
import { usePageData } from "../route-context"
import { guideLinks } from "../data/guide-links"
import { Html } from "../html"
import {
docsProseClass,
linkClass,
maxWidthContainer,
} from "../styles/components"
import type { DocsPageProps } from "./docs.page.server"
export default function DocsPage() {
const data = usePageData<DocsPageProps>()
export default function DocsPage({
title,
description,
html,
}: {
title: string
description: string
html: string
}) {
return (
<>
<Title>{data.title} | Reacord</Title>
<Meta name="description" content={data.description} />
<HeaderPanel>
<Html title={`${title} | Reacord`} description={description}>
<header className="bg-slate-700/30 shadow sticky top-0 backdrop-blur-sm transition z-10 flex">
<div className={maxWidthContainer}>
<MainNavigation />
</div>
</HeaderPanel>
</header>
<main className={clsx(maxWidthContainer, "mt-8 flex items-start gap-4")}>
<nav className="w-48 sticky top-24 hidden md:block">
<h2 className="text-2xl">Guides</h2>
@@ -36,20 +39,9 @@ export default function DocsPage() {
</nav>
<section
className={clsx(docsProseClass, "pb-8 flex-1 min-w-0")}
dangerouslySetInnerHTML={{ __html: data.content }}
dangerouslySetInnerHTML={{ __html: html }}
/>
</main>
</>
</Html>
)
}
function HeaderPanel({ children }: { children: React.ReactNode }) {
const isScrolled = useScrolled()
const className = clsx(
isScrolled ? "bg-slate-700/30" : "bg-slate-800",
"shadow sticky top-0 backdrop-blur-sm transition z-10 flex",
)
return <header className={className}>{children}</header>
}

View File

@@ -1,30 +0,0 @@
import packageJson from "reacord/package.json"
import { html as landingExampleHtml } from "../components/landing-example.md"
import { MainNavigation } from "../components/main-navigation"
import { maxWidthContainer } from "../styles/components"
export default function LandingPage() {
return (
<div className="flex flex-col min-w-0 min-h-screen text-center">
<header className={maxWidthContainer}>
<MainNavigation />
</header>
<div className="px-4 pb-8 flex flex-1">
<main className="px-4 py-6 rounded-lg shadow bg-slate-800 space-y-5 m-auto w-full max-w-xl">
<h1 className="text-6xl font-light">reacord</h1>
<section
className="mx-auto text-sm sm:text-base"
dangerouslySetInnerHTML={{ __html: landingExampleHtml }}
/>
<p className="text-2xl font-light">{packageJson.description}</p>
<a
href="/docs/getting-started"
className="inline-block px-4 py-3 text-xl transition rounded-lg bg-emerald-700 hover:translate-y-[-2px] hover:bg-emerald-800 hover:shadow"
>
Get Started
</a>
</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
import packageJson from "reacord/package.json"
import React from "react"
import { MainNavigation } from "../components/main-navigation"
import { renderMarkdownFile } from "../helpers/markdown"
import { Html } from "../html"
import { maxWidthContainer } from "../styles/components"
const landingExample = await renderMarkdownFile(
new URL("../components/landing-example.md", import.meta.url).pathname,
)
export function Landing() {
return (
<Html>
<div className="flex flex-col min-w-0 min-h-screen text-center">
<header className={maxWidthContainer}>
<MainNavigation />
</header>
<div className="px-4 pb-8 flex flex-1">
<main className="px-4 py-6 rounded-lg shadow bg-slate-800 space-y-5 m-auto w-full max-w-xl">
<h1 className="text-6xl font-light">reacord</h1>
<section
className="mx-auto text-sm sm:text-base"
dangerouslySetInnerHTML={{ __html: landingExample.html }}
/>
<p className="text-2xl font-light">{packageJson.description}</p>
<a
href="/docs/getting-started"
className="inline-block px-4 py-3 text-xl transition rounded-lg bg-emerald-700 hover:translate-y-[-2px] hover:bg-emerald-800 hover:shadow"
>
Get Started
</a>
</main>
</div>
</div>
</Html>
)
}

View File

@@ -2,3 +2,9 @@ import "react"
declare module "react" {
export function createContext<Value>(): Context<Value | undefined>
}
declare module "react-dom" {
export function createRoot(element: Element): {
render(element: React.ReactNode): void
}
}

5
packages/docs/src/reload.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module "reload" {
import type { Express } from "express"
function reload(server: Express): Promise<void>
export = reload
}

View File

@@ -1,18 +0,0 @@
import { createContext, useContext } from "react"
export type RouteContextValue = {
routeParams: Record<string, string>
pageData?: Record<string, unknown>
}
const Context = createContext<RouteContextValue>()
export const RouteContextProvider = Context.Provider
export function useRouteParams() {
return useContext(Context)?.routeParams ?? {}
}
export function usePageData<T>() {
return useContext(Context)?.pageData as T
}

View File

@@ -1,10 +0,0 @@
import type { Promisable } from "type-fest"
import type { PageContextBuiltIn } from "vite-plugin-ssr"
export type OnBeforeRenderFn<PageProps> = (
context: PageContextBuiltIn,
) => Promisable<{
pageContext: {
pageData: PageProps
}
}>

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,8 +0,0 @@
/// <reference types="vite/client" />
declare module "*.md" {
import type { ComponentType } from "react"
export const attributes: Record<string, any>
export const html: string
export const ReactComponent: ComponentType
}