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,83 +0,0 @@
{
"name": "reacord-docs-new",
"type": "module",
"private": true,
"main": "./src/main.tsx",
"scripts": {
"dev": "esmo --no-warnings scripts/dev.ts | pino-colada",
"serve": "esmo --experimental-import-meta-resolve --no-warnings --enable-source-maps src/main.tsx",
"start": "NODE_ENV=production pnpm serve",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@heroicons/react": "^1.0.5",
"@reach/rect": "^0.16.0",
"@tinyhttp/app": "^2.0.15",
"@tinyhttp/logger": "^1.3.0",
"clsx": "^1.1.1",
"esbuild": "^0.14.10",
"express": "^4.17.2",
"gray-matter": "^4.0.3",
"http-terminator": "^3.0.4",
"pino": "^7.6.2",
"pino-colada": "^2.2.2",
"pino-http": "^6.5.0",
"pino-pretty": "^7.3.0",
"reacord": "workspace:*",
"react": "^18.0.0-rc.0",
"react-dom": "^18.0.0-rc.0",
"react-focus-on": "^3.5.4",
"react-head": "^3.4.0",
"react-router": "^6.2.1",
"react-router-dom": "^6.2.1",
"rehype-stringify": "^9.0.2",
"reload": "^3.2.0",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"shrink-ray-current": "^4.1.3",
"sirv": "^2.0.0",
"unified": "^10.1.1",
"unified-stream": "^2.0.0",
"vite-plugin-ssr": "^0.3.42"
},
"devDependencies": {
"@mapbox/rehype-prism": "^0.8.0",
"@tailwindcss/typography": "^0.5.0",
"@types/browser-sync": "^2.26.3",
"@types/compression": "^1.7.2",
"@types/express": "^4.17.13",
"@types/markdown-it": "^12.2.3",
"@types/node": "*",
"@types/nodemon": "^1.19.1",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.9",
"@types/update-notifier": "^5.1.0",
"@types/wait-on": "^5.3.1",
"@vitejs/plugin-react": "^1.1.3",
"autoprefixer": "^10.4.1",
"browser-sync": "^2.27.7",
"chokidar": "^3.5.2",
"compression": "^1.7.4",
"debounce-fn": "^5.1.0",
"esno": "^0.13.0",
"execa": "^6.0.0",
"fast-glob": "^3.2.7",
"markdown-it": "^12.3.0",
"markdown-it-prism": "^2.2.1",
"nodemon": "^2.0.15",
"npm-run-all": "^4.1.5",
"polka": "^0.5.2",
"postcss": "^8.4.5",
"rehype-highlight": "^5.0.2",
"rehype-prism-plus": "^1.1.3",
"remark-frontmatter": "^4.0.1",
"rxjs": "^7.5.1",
"tailwindcss": "^3.0.8",
"type-fest": "^2.8.0",
"typescript": "^4.5.4",
"update-notifier": "^5.1.0",
"vite": "^2.7.10",
"vite-plugin-markdown": "^2.0.2",
"wait-on": "^6.0.0"
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,27 +0,0 @@
import React from "react"
import { ExternalLink } from "./external-link"
export type AppLinkProps = {
type: "internal" | "external"
label: React.ReactNode
to: string
className?: string
}
export function AppLink(props: AppLinkProps) {
switch (props.type) {
case "internal":
return (
<a className={props.className} href={props.to}>
{props.label}
</a>
)
case "external":
return (
<ExternalLink className={props.className} href={props.to}>
{props.label}
</ExternalLink>
)
}
}

View File

@@ -1,10 +0,0 @@
import type { ComponentPropsWithoutRef } from "react"
import React from "react"
export function ExternalLink(props: ComponentPropsWithoutRef<"a">) {
return (
<a target="_blank" rel="noopener noreferrer" {...props}>
{props.children}
</a>
)
}

View File

@@ -1,49 +0,0 @@
import clsx from "clsx"
import React from "react"
import { guideLinks } from "../data/guide-links"
import { useScrolled } from "../hooks/dom/use-scrolled"
import {
docsProseClass,
linkClass,
maxWidthContainer,
} from "../styles/components"
import { AppLink } from "./app-link"
import { MainNavigation } from "./main-navigation"
export function GuidePageLayout() {
return (
<>
<HeaderPanel>
<div className={maxWidthContainer}>
<MainNavigation />
</div>
</HeaderPanel>
<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>
<ul className="mt-3 flex flex-col gap-2 items-start">
{guideLinks.map((link) => (
<li key={link.to}>
<AppLink {...link} className={linkClass} />
</li>
))}
</ul>
</nav>
<section className={clsx(docsProseClass, "pb-8 flex-1 min-w-0")}>
{/* todo */}
</section>
</main>
</>
)
}
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,19 +0,0 @@
<!-- prettier-ignore -->
```tsx
import * as React from "react"
import { Embed, Button } from "reacord"
function Counter() {
const [count, setCount] = React.useState(0)
return (
<>
<Embed title="Counter">
This button has been clicked {count} times.
</Embed>
<Button onClick={() => setCount(count + 1)}>
+1
</Button>
</>
)
}
```

View File

@@ -1,40 +0,0 @@
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"
import { PopoverMenu } from "./popover-menu"
export function MainNavigation() {
return (
<nav className="flex justify-between items-center h-16">
<a href="/">
<h1 className="text-3xl font-light">reacord</h1>
</a>
<div className="hidden md:flex gap-4">
{mainLinks.map((link) => (
<AppLink {...link} key={link.to} className={linkClass} />
))}
</div>
<div className="md:hidden" id="main-navigation-popover">
<PopoverMenu>
{mainLinks.map((link) => (
<AppLink
{...link}
key={link.to}
className={PopoverMenu.itemClass}
/>
))}
<hr className="border-0 h-[2px] bg-black/50" />
{guideLinks.map((link) => (
<AppLink
{...link}
key={link.to}
className={PopoverMenu.itemClass}
/>
))}
</PopoverMenu>
</div>
</nav>
)
}

View File

@@ -1,26 +0,0 @@
import { MenuAlt4Icon } from "@heroicons/react/outline"
import clsx from "clsx"
import React from "react"
import { linkClass } from "../styles/components"
export function PopoverMenu({ children }: { children: React.ReactNode }) {
return (
<div data-popover className="relative">
<button data-popover-button title="Menu" className={linkClass}>
<MenuAlt4Icon className="w-6" />
</button>
<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)]"
>
{children}
</div>
</div>
)
}
PopoverMenu.itemClass = clsx`
px-3 py-2 transition text-left font-medium block
opacity-50 hover:opacity-100 hover:bg-black/30
`

View File

@@ -1,38 +0,0 @@
import {
CodeIcon,
DocumentTextIcon,
ExternalLinkIcon,
} from "@heroicons/react/solid"
import React from "react"
import type { AppLinkProps } from "../components/app-link"
import { inlineIconClass } from "../styles/components"
export const mainLinks: AppLinkProps[] = [
{
type: "internal",
to: "/docs/getting-started",
label: (
<>
<DocumentTextIcon className={inlineIconClass} /> Guides
</>
),
},
{
type: "internal",
to: "/docs/api",
label: (
<>
<CodeIcon className={inlineIconClass} /> API Reference
</>
),
},
{
type: "external",
to: "https://github.com/itsMapleLeaf/reacord",
label: (
<>
<ExternalLinkIcon className={inlineIconClass} /> GitHub
</>
),
},
]

View File

@@ -1,9 +0,0 @@
---
order: 3
title: Buttons
description: Using button components
---
# Buttons
todo

View File

@@ -1,9 +0,0 @@
---
order: 2
title: Embeds
description: Using embed components
---
# Embeds
todo

View File

@@ -1,43 +0,0 @@
---
order: 0
title: Getting Started
description: Learn how to get started with Reacord.
---
# Getting Started
This guide assumes some familiarity with JavaScript, [React](https://reactjs.org), [Discord.js](https://discord.js.org) and the [Discord API](https://discord.dev). Keep these pages as reference if you need it.
**Note:** Ensure your project has support for running code with JSX. I recommend using [esno](https://npm.im/esno).
## Install
```bash
# npm
npm install reacord discord.js
# yarn
yarn add reacord discord.js
# pnpm
pnpm add reacord discord.js
```
## Setup
Create a Discord.js client and a Reacord instance:
```js
// main.js
import { Client } from "discord.js"
import { ReacordDiscordJs } from "reacord"
const client = new Client()
const reacord = new ReacordDiscordJs(client)
client.on("ready", () => {
console.log("Ready!")
})
await client.login(process.env.BOT_TOKEN)
```

View File

@@ -1,8 +0,0 @@
---
title: Select Menus
description: Using select menu components
---
# Select Menus
todo

View File

@@ -1,78 +0,0 @@
---
order: 1
title: Sending Messages
description: Sending messages by creating Reacord instances
---
# Sending Messages with Instances
You can send messages via Reacord to a channel like so.
```jsx
const channelId = "abc123deadbeef"
client.on("ready", () => {
reacord.send(channelId, "Hello, world!")
})
```
The `.send()` function creates a **Reacord instance**. You can pass strings, numbers, or anything that can be rendered by React, such as JSX!
Components rendered through this instance can include state and effects, and the message on Discord will update automatically.
```jsx
function Uptime() {
const [startTime] = useState(Date.now())
const [currentTime, setCurrentTime] = useState(Date.now())
useEffect(() => {
const interval = setInterval(() => {
currentTime(Date.now())
}, 3000)
return () => clearInterval(interval)
}, [])
return <>this message has been shown for {currentTime - startTime}ms</>
}
client.on("ready", () => {
reacord.send(channelId, <Uptime />)
})
```
The instance can be rendered to multiple times, which will update the message each time.
```jsx
const Hello = ({ subject }) => <>Hello, {subject}!</>
client.on("ready", () => {
const instance = reacord.send(channel)
instance.render(<Hello subject="World" />)
instance.render(<Hello subject="Moon" />)
})
```
## Cleaning Up Instances
If you no longer want to use the instance, you can clean it up in a few ways:
- `instance.destroy()` - This will remove the message.
- `instance.deactivate()` - This will keep the message, but it will disable the components on the message, and no longer listen to user interactions.
By default, Reacord has a max limit on the number of active instances, and deactivates older instances to conserve memory. This can be configured through the Reacord options:
```js
const reacord = new ReacordDiscordJs(client, {
// after sending four messages,
// the first one will be deactivated
maxInstances: 3,
})
```
## Discord Slash Commands
<aside className="opacity-75 italic">
This section also applies to other kinds of application commands, such as context menu commands.
</aside>
todo

View File

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

View File

@@ -1,11 +0,0 @@
import { useEffect } from "react"
export function useWindowEvent<EventType extends keyof WindowEventMap>(
type: EventType,
handler: (event: WindowEventMap[EventType]) => void,
) {
useEffect(() => {
window.addEventListener(type, handler)
return () => window.removeEventListener(type, handler)
})
}

View File

@@ -1,10 +0,0 @@
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
}
}

View File

@@ -1,24 +0,0 @@
import clsx from "clsx"
export const maxWidthContainer = clsx`mx-auto w-full max-w-screen-lg px-4`
export const inlineIconClass = clsx`inline w-5 align-sub`
export const linkClass = clsx`
font-medium inline-block relative
opacity-60 hover:opacity-100 transition-opacity
after:absolute after:block after:w-full after:h-px after:bg-white/50 after:translate-y-[3px] after:opacity-0 after:transition
hover:after:translate-y-[-1px] hover:after:opacity-100
`
export const docsProseClass = clsx`
prose prose-invert
prose-h1:font-light prose-h1:mb-4 prose-h1:text-3xl lg:prose-h1:text-4xl
prose-h2:font-light
prose-h3:font-light
prose-p:my-4
prose-a:font-medium prose-a:text-emerald-400 hover:prose-a:no-underline
prose-strong:font-medium prose-strong:text-emerald-400
prose-pre:font-monospace prose-pre:overflow-x-auto
max-w-none
`

View File

@@ -1,133 +0,0 @@
/**
* Nord Theme Originally by Arctic Ice Studio
* https://nordtheme.com
*
* Ported for PrismJS by Zane Hitchcoxc (@zwhitchcox) and Gabriel Ramos (@gabrieluizramos)
*/
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
/* font-family: "Fira Code", Consolas, Monaco, "Andale Mono", "Ubuntu Mono",
monospace; */
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.7;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
/* background: #2e3440; */
background: rgba(0, 0, 0, 0.3);
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #636f88;
}
.token.punctuation {
color: #81a1c1;
}
.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #81a1c1;
}
.token.number {
color: #b48ead;
}
.token.boolean {
color: #81a1c1;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #a3be8c;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #81a1c1;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #88c0d0;
}
.token.keyword {
color: #81a1c1;
}
.token.regex,
.token.important {
color: #ebcb8b;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.code-line.highlight-line {
background-color: rgba(255, 255, 255, 0.08);
padding: 0 1rem;
margin: 0 -1rem;
display: block;
}

View File

@@ -1,15 +0,0 @@
// @ts-nocheck
module.exports = {
content: ["./src/**/*.{ts,tsx,md}"],
theme: {
fontFamily: {
sans: ["Rubik", "sans-serif"],
monospace: ["'JetBrains Mono'", "monospace"],
},
boxShadow: {
DEFAULT: "0 2px 9px 0 rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3)",
},
extend: {},
},
plugins: [require("@tailwindcss/typography")],
}

View File

@@ -1,3 +0,0 @@
{
"extends": "../../tsconfig.base.json"
}

View File

@@ -1,19 +1,28 @@
{ {
"name": "reacord-docs", "name": "reacord-docs-new",
"type": "module", "type": "module",
"private": true, "private": true,
"main": "./src/main.tsx",
"scripts": { "scripts": {
"dev": "esmo server.ts", "dev": "esmo --no-warnings scripts/dev.ts | pino-colada",
"build": "vite build && vite build --ssr", "serve": "esmo --experimental-import-meta-resolve --no-warnings --enable-source-maps src/main.tsx",
"start": "NODE_ENV=production esmo server.ts", "start": "NODE_ENV=production pnpm serve",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^1.0.5", "@heroicons/react": "^1.0.5",
"@reach/rect": "^0.16.0", "@reach/rect": "^0.16.0",
"@tinyhttp/app": "^2.0.15",
"@tinyhttp/logger": "^1.3.0",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"esbuild": "^0.14.10",
"express": "^4.17.2", "express": "^4.17.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"http-terminator": "^3.0.4",
"pino": "^7.6.2",
"pino-colada": "^2.2.2",
"pino-http": "^6.5.0",
"pino-pretty": "^7.3.0",
"reacord": "workspace:*", "reacord": "workspace:*",
"react": "^18.0.0-rc.0", "react": "^18.0.0-rc.0",
"react-dom": "^18.0.0-rc.0", "react-dom": "^18.0.0-rc.0",
@@ -21,29 +30,54 @@
"react-head": "^3.4.0", "react-head": "^3.4.0",
"react-router": "^6.2.1", "react-router": "^6.2.1",
"react-router-dom": "^6.2.1", "react-router-dom": "^6.2.1",
"rehype-stringify": "^9.0.2",
"reload": "^3.2.0",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"shrink-ray-current": "^4.1.3",
"sirv": "^2.0.0",
"unified": "^10.1.1",
"unified-stream": "^2.0.0",
"vite-plugin-ssr": "^0.3.42" "vite-plugin-ssr": "^0.3.42"
}, },
"devDependencies": { "devDependencies": {
"@mapbox/rehype-prism": "^0.8.0", "@mapbox/rehype-prism": "^0.8.0",
"@tailwindcss/typography": "^0.5.0", "@tailwindcss/typography": "^0.5.0",
"@types/browser-sync": "^2.26.3",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/markdown-it": "^12.2.3", "@types/markdown-it": "^12.2.3",
"@types/node": "*", "@types/node": "*",
"@types/nodemon": "^1.19.1",
"@types/react": "^17.0.38", "@types/react": "^17.0.38",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"@types/update-notifier": "^5.1.0",
"@types/wait-on": "^5.3.1",
"@vitejs/plugin-react": "^1.1.3", "@vitejs/plugin-react": "^1.1.3",
"autoprefixer": "^10.4.1", "autoprefixer": "^10.4.1",
"browser-sync": "^2.27.7",
"chokidar": "^3.5.2",
"compression": "^1.7.4", "compression": "^1.7.4",
"debounce-fn": "^5.1.0",
"esno": "^0.13.0", "esno": "^0.13.0",
"execa": "^6.0.0",
"fast-glob": "^3.2.7", "fast-glob": "^3.2.7",
"markdown-it": "^12.3.0", "markdown-it": "^12.3.0",
"markdown-it-prism": "^2.2.1", "markdown-it-prism": "^2.2.1",
"nodemon": "^2.0.15",
"npm-run-all": "^4.1.5",
"polka": "^0.5.2",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"rehype-highlight": "^5.0.2",
"rehype-prism-plus": "^1.1.3",
"remark-frontmatter": "^4.0.1",
"rxjs": "^7.5.1",
"tailwindcss": "^3.0.8", "tailwindcss": "^3.0.8",
"type-fest": "^2.8.0", "type-fest": "^2.8.0",
"typescript": "^4.5.4", "typescript": "^4.5.4",
"update-notifier": "^5.1.0",
"vite": "^2.7.10", "vite": "^2.7.10",
"vite-plugin-markdown": "^2.0.2" "vite-plugin-markdown": "^2.0.2",
"wait-on": "^6.0.0"
} }
} }

View File

@@ -1,40 +0,0 @@
import { unlink, writeFile } from "node:fs/promises"
import type { Plugin } from "vite"
import { transformWithEsbuild } from "vite"
const prevalPattern = /\.preval\.(js|ts|jsx|tsx|mts|cts)$/
export function preval(): Plugin {
return {
name: "preval",
enforce: "pre",
async transform(code, filePath) {
if (!prevalPattern.test(filePath)) return
const tempFilePath = `${filePath}.preval.mjs`
try {
const transformResult = await transformWithEsbuild(code, filePath, {
target: "node16",
format: "esm",
})
await writeFile(tempFilePath, transformResult.code)
const runResult = await import(tempFilePath)
const serialized = Object.entries(runResult)
.map(([key, value]) => {
return key === "default"
? `export default ${JSON.stringify(value)}`
: `export const ${key} = ${JSON.stringify(value)}`
})
.join("\n")
return serialized + "\n"
} finally {
await unlink(tempFilePath).catch(console.warn)
}
},
}
}

View File

@@ -1,42 +0,0 @@
import compression from "compression"
import express from "express"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { createPageRenderer } from "vite-plugin-ssr"
const isProduction = process.env.NODE_ENV === "production"
const root = dirname(fileURLToPath(import.meta.url))
const app = express()
app.use(compression())
let viteDevServer
if (isProduction) {
app.use(express.static(join(root, "dist/client")))
} else {
const vite = await import("vite")
viteDevServer = await vite.createServer({
root,
server: { middlewareMode: "ssr" },
})
app.use(viteDevServer.middlewares)
}
const renderPage = createPageRenderer({ viteDevServer, isProduction, root })
app.get("*", async (req, res, next) => {
const url = req.originalUrl
const pageContextInit = {
url,
}
const pageContext = await renderPage(pageContextInit)
const { httpResponse } = pageContext
if (!httpResponse) return next()
const { body, statusCode, contentType } = httpResponse
res.status(statusCode).type(contentType).send(body)
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.info(`Server running at http://localhost:${port}`)
})

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" import { ExternalLink } from "./external-link"
export type AppLinkProps = { export type AppLinkProps = {

View File

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

View File

@@ -1,5 +1,6 @@
import clsx from "clsx" 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 { useScrolled } from "../hooks/dom/use-scrolled"
import { import {
docsProseClass, docsProseClass,

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 { mainLinks } from "../data/main-links"
import { linkClass } from "../styles/components" import { linkClass } from "../styles/components"
import { AppLink } from "./app-link" import { AppLink } from "./app-link"
@@ -15,7 +16,7 @@ export function MainNavigation() {
<AppLink {...link} key={link.to} className={linkClass} /> <AppLink {...link} key={link.to} className={linkClass} />
))} ))}
</div> </div>
<div className="md:hidden"> <div className="md:hidden" id="main-navigation-popover">
<PopoverMenu> <PopoverMenu>
{mainLinks.map((link) => ( {mainLinks.map((link) => (
<AppLink <AppLink

View File

@@ -1,63 +1,22 @@
import { MenuAlt4Icon } from "@heroicons/react/outline" import { MenuAlt4Icon } from "@heroicons/react/outline"
import { useRect } from "@reach/rect"
import clsx from "clsx" import clsx from "clsx"
import { useRef, useState } from "react" import React from "react"
import { FocusOn } from "react-focus-on"
import { linkClass } from "../styles/components" import { linkClass } from "../styles/components"
// todo: remove useRect usage and rely on css absolute positioning instead
export function PopoverMenu({ children }: { children: React.ReactNode }) { 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 ( return (
<> <div data-popover className="relative">
<button <button data-popover-button title="Menu" className={linkClass}>
title="Menu"
className={linkClass}
onClick={() => setVisible(!visible)}
ref={buttonRef}
>
<MenuAlt4Icon className="w-6" /> <MenuAlt4Icon className="w-6" />
</button> </button>
<FocusOn
enabled={visible}
onClickOutside={() => setVisible(false)}
onEscapeKey={() => setVisible(false)}
>
<div <div
className="fixed" data-popover-panel
style={{ hidden
left: (buttonRect?.right ?? 0) - (panelRect?.width ?? 0), 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)]"
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} {children}
</div> </div>
</div> </div>
</div>
</div>
</FocusOn>
</>
) )
} }

View File

@@ -1,34 +0,0 @@
import glob from "fast-glob"
import grayMatter from "gray-matter"
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import type { AppLinkProps } from "../components/app-link"
const docsFolderPath = new URL("../docs", import.meta.url).pathname
const guideFiles = await glob("**/*.md", { cwd: docsFolderPath })
const entries = await Promise.all(
guideFiles.map(async (file) => {
const content = await readFile(join(docsFolderPath, file), "utf-8")
const { data } = grayMatter(content)
let order = Number(data.order)
if (!Number.isFinite(order)) {
order = Number.POSITIVE_INFINITY
}
return {
route: `/docs/${file.replace(/\.mdx?$/, "")}`,
title: String(data.title || ""),
order,
}
}),
)
export const guideLinks: AppLinkProps[] = entries
.sort((a, b) => a.order - b.order)
.map((item) => ({
type: "internal",
label: item.title,
to: item.route,
}))

View File

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

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

@@ -2,7 +2,9 @@ import { useState } from "react"
import { useWindowEvent } from "./use-window-event" import { useWindowEvent } from "./use-window-event"
export function useScrolled() { 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)) useWindowEvent("scroll", () => setScrolled(window.scrollY > 0))
return scrolled return scrolled
} }

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,55 +0,0 @@
import clsx from "clsx"
import { Meta, Title } from "react-head"
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 {
docsProseClass,
linkClass,
maxWidthContainer,
} from "../styles/components"
import type { DocsPageProps } from "./docs.page.server"
export default function DocsPage() {
const data = usePageData<DocsPageProps>()
return (
<>
<Title>{data.title} | Reacord</Title>
<Meta name="description" content={data.description} />
<HeaderPanel>
<div className={maxWidthContainer}>
<MainNavigation />
</div>
</HeaderPanel>
<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>
<ul className="mt-3 flex flex-col gap-2 items-start">
{guideLinks.map((link) => (
<li key={link.to}>
<AppLink {...link} className={linkClass} />
</li>
))}
</ul>
</nav>
<section
className={clsx(docsProseClass, "pb-8 flex-1 min-w-0")}
dangerouslySetInnerHTML={{ __html: data.content }}
/>
</main>
</>
)
}
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

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

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
}

View File

@@ -1,6 +1,3 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json"
"compilerOptions": {
"jsx": "react-jsx"
}
} }

View File

@@ -1,35 +0,0 @@
import react from "@vitejs/plugin-react"
import MarkdownIt from "markdown-it"
import prism from "markdown-it-prism"
import { createRequire } from "node:module"
import { defineConfig } from "vite"
import type * as markdownType from "vite-plugin-markdown"
import ssr from "vite-plugin-ssr/plugin"
import { preval } from "./plugins/preval"
const require = createRequire(import.meta.url)
const markdown: typeof markdownType = require("vite-plugin-markdown")
export default defineConfig({
build: {
target: ["node16", "chrome89", "firefox89"],
},
plugins: [
ssr(),
react(),
markdown.default({
mode: [markdown.Mode.HTML],
markdownIt: new MarkdownIt({
html: true,
linkify: true,
}).use(prism),
}),
preval(),
],
resolve: {
alias: {
// https://github.com/brillout/vite-plugin-mdx/issues/44#issuecomment-974540152
"react/jsx-runtime": "react/jsx-runtime.js",
},
},
})