start on custom server

This commit is contained in:
MapleLeaf
2022-01-02 23:36:50 -06:00
committed by Darius
parent 14450cc743
commit 41a22175e3
30 changed files with 2257 additions and 33 deletions

View File

@@ -0,0 +1,63 @@
{
"name": "reacord-docs-new",
"type": "module",
"private": true,
"scripts": {
"dev": "npm-run-all --parallel --print-label --race dev-*",
"dev-nodemon": "nodemon --exec esmo --ext js,ts,tsx --experimental-import-meta-resolve --no-warnings src/main.tsx",
"dev-bsync": "wait-on tcp:localhost:3000 && browser-sync start --proxy http://localhost:3000 --watch src",
"build": "vite build && vite build --ssr",
"start": "NODE_ENV=production esmo server.ts",
"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",
"express": "^4.17.2",
"gray-matter": "^4.0.3",
"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",
"reload": "^3.2.0",
"shrink-ray-current": "^4.1.3",
"sirv": "^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",
"@vitejs/plugin-react": "^1.1.3",
"autoprefixer": "^10.4.1",
"browser-sync": "^2.27.7",
"compression": "^1.7.4",
"esno": "^0.13.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",
"tailwindcss": "^3.0.8",
"type-fest": "^2.8.0",
"typescript": "^4.5.4",
"vite": "^2.7.10",
"vite-plugin-markdown": "^2.0.2",
"wait-on": "^6.0.0"
}
}

View File

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

View File

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

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

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

@@ -0,0 +1,40 @@
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">
<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

@@ -0,0 +1,67 @@
import { MenuAlt4Icon } from "@heroicons/react/outline"
import { useRect } from "@reach/rect"
import clsx from "clsx"
import React, { useRef, useState } from "react"
import { FocusOn } from "react-focus-on"
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}
>
<MenuAlt4Icon className="w-6" />
</button>
<FocusOn
enabled={visible}
onClickOutside={() => setVisible(false)}
onEscapeKey={() => setVisible(false)}
>
<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>
</>
)
}
PopoverMenu.itemClass = clsx`
px-3 py-2 transition text-left font-medium block
opacity-50 hover:opacity-100 hover:bg-black/30
`

View File

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

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

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

View File

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

View File

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

@@ -0,0 +1,19 @@
<!-- 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

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

View File

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

@@ -0,0 +1,22 @@
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 tailwindTemplate = await readFile(
fileURLToPath(await import.meta.resolve!("tailwindcss/tailwind.css")),
"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)
}
res.set("Content-Type", "text/css").send(result.css)
}
}

View File

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

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

@@ -0,0 +1,38 @@
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" />
<title>{title}</title>
</head>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,32 @@
import compression from "compression"
import type { Request, Response } from "express"
import express from "express"
import * as React from "react"
import { renderToStaticMarkup } from "react-dom/server.js"
import { serveTailwindCss } from "./helpers/tailwind"
import { Landing } from "./pages/landing"
const projectRoot = new URL("..", import.meta.url).pathname
const port = process.env.PORT || 3000
function sendJsx(res: Response, jsx: React.ReactElement) {
res.set("Content-Type", "text/html")
res.send(`<!DOCTYPE html>\n${renderToStaticMarkup(jsx)}`)
}
express()
.use(compression())
.get("/tailwind.css", serveTailwindCss())
.get("/", (req, res) => {
sendJsx(res, <Landing />)
})
.get("/docs/:docPath+", (req: Request<{ docPath: string }>, res) => {
res.send("doc: " + req.params.docPath)
})
.listen(port, () => {
console.info(`Server is running on https://localhost:${port}`)
})

View File

@@ -0,0 +1,59 @@
import clsx from "clsx"
import React from "react"
import { AppLink } from "../components/app-link"
import { MainNavigation } from "../components/main-navigation"
import { guideLinks } from "../data/guide-links"
import { useScrolled } from "../hooks/dom/use-scrolled"
import { Html } from "../html"
import {
docsProseClass,
linkClass,
maxWidthContainer,
} from "../styles/components"
export default function DocsPage({
title,
description,
html,
}: {
title: string
description: string
html: string
}) {
return (
<Html title={`${title} | Reacord`} description={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: 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

@@ -0,0 +1,34 @@
import packageJson from "reacord/package.json"
import React from "react"
import { MainNavigation } from "../components/main-navigation"
import { Html } from "../html"
import { maxWidthContainer } from "../styles/components"
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"
// todo
// 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>
</Html>
)
}

5
packages/docs-new/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

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

@@ -0,0 +1,133 @@
/**
* 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

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

View File

@@ -0,0 +1,15 @@
// @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

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

View File

@@ -1,5 +1,5 @@
{
"name": "reacord-docs-new",
"name": "reacord-docs",
"type": "module",
"private": true,
"scripts": {

1399
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff