diff --git a/packages/website/.gitignore b/packages/website/.gitignore index 66a647d..cded633 100644 --- a/packages/website/.gitignore +++ b/packages/website/.gitignore @@ -1,5 +1,4 @@ node_modules - /.cache /build /public/build @@ -9,3 +8,4 @@ cypress/videos cypress/screenshots *.out.css /api +.astro diff --git a/packages/website/astro.config.mjs b/packages/website/astro.config.mjs index 8882402..7d0aa3e 100644 --- a/packages/website/astro.config.mjs +++ b/packages/website/astro.config.mjs @@ -1,7 +1,17 @@ +import prefetch from "@astrojs/prefetch" import react from "@astrojs/react" import tailwind from "@astrojs/tailwind" import { defineConfig } from "astro/config" +// https://astro.build/config export default defineConfig({ - integrations: [tailwind({ config: { applyBaseStyles: false } }), react()], + integrations: [ + tailwind({ + config: { + applyBaseStyles: false, + }, + }), + react(), + prefetch(), + ], }) diff --git a/packages/website/package.json b/packages/website/package.json index 5221a56..92bbb9d 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -14,6 +14,7 @@ "typecheck": "tsc --noEmit && tsc --project cypress/tsconfig.json --noEmit" }, "dependencies": { + "@astrojs/prefetch": "^0.2.0", "@astrojs/react": "^2.1.0", "@fontsource/jetbrains-mono": "^4.5.12", "@fontsource/rubik": "^4.5.14", diff --git a/packages/website/src/components/external-link.astro b/packages/website/src/components/external-link.astro index ddc1f2c..1150dc2 100644 --- a/packages/website/src/components/external-link.astro +++ b/packages/website/src/components/external-link.astro @@ -1,5 +1,5 @@ --- -export type Props = astroHTML.JSX.AnchorHTMLProps +export type Props = astroHTML.JSX.AnchorHTMLAttributes --- diff --git a/packages/website/src/components/guide-layout.astro b/packages/website/src/components/guide-layout.astro new file mode 100644 index 0000000..e678f38 --- /dev/null +++ b/packages/website/src/components/guide-layout.astro @@ -0,0 +1,38 @@ +--- +import { getCollection } from "astro:content" +import Layout from "./layout.astro" +import MainNavigation from "./main-navigation.astro" + +const guides = await getCollection("guides") +--- + + +
+
+
+ +
+
+
+ +
+ +
+
+
+ diff --git a/packages/website/src/layout.astro b/packages/website/src/components/layout.astro similarity index 100% rename from packages/website/src/layout.astro rename to packages/website/src/components/layout.astro diff --git a/packages/website/src/components/main-navigation.astro b/packages/website/src/components/main-navigation.astro index 30ad013..65b4a8a 100644 --- a/packages/website/src/components/main-navigation.astro +++ b/packages/website/src/components/main-navigation.astro @@ -5,6 +5,7 @@ import { DocumentTextIcon, } from "@heroicons/react/20/solid" import { Bars3Icon } from "@heroicons/react/24/outline" +import { getCollection } from "astro:content" import AppLogo from "./app-logo.astro" import ExternalLink from "./external-link.astro" import MenuItem from "./menu-item.astro" @@ -16,6 +17,7 @@ const links = [ label: "Guides", icon: DocumentTextIcon, component: "a", + prefetch: true, }, { href: "/api/", @@ -30,6 +32,8 @@ const links = [ component: ExternalLink, }, ] + +const guides = await getCollection("guides") --- diff --git a/packages/website/src/components/menu-item.astro b/packages/website/src/components/menu-item.astro index 40ed329..845b401 100644 --- a/packages/website/src/components/menu-item.astro +++ b/packages/website/src/components/menu-item.astro @@ -9,5 +9,5 @@ export type Props = { class="px-3 py-2 transition text-left font-medium block w-full opacity-50 inline-flex gap-1 items-center" > - {Astro.props.label} + {Astro.props.label} diff --git a/packages/website/src/content/config.ts b/packages/website/src/content/config.ts new file mode 100644 index 0000000..3c7323f --- /dev/null +++ b/packages/website/src/content/config.ts @@ -0,0 +1,10 @@ +import { defineCollection, z } from "astro:content" + +export const collections = { + guides: defineCollection({ + schema: z.object({ + title: z.string(), + description: z.string(), + }), + }), +} diff --git a/packages/website/src/content/guides/0-getting-started.md b/packages/website/src/content/guides/0-getting-started.md new file mode 100644 index 0000000..17d3616 --- /dev/null +++ b/packages/website/src/content/guides/0-getting-started.md @@ -0,0 +1,52 @@ +--- +title: Getting Started +description: Learn how to get started with Reacord. +slug: getting-started +--- + +# Getting Started + +These guides assume 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. + +## Setup from template + +[Use this starter template](https://github.com/itsMapleLeaf/reacord-starter) to get off the ground quickly. + +## Adding to an existing project + +Install Reacord and dependencies: + +```bash +# npm +npm install reacord react discord.js + +# yarn +yarn add reacord react discord.js + +# pnpm +pnpm add reacord react discord.js +``` + +Create a Discord.js client and a Reacord instance: + +```js +// main.jsx +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) +``` + +To use JSX in your code, run it with [tsx](https://npm.im/tsx): + +```bash +npm install tsx +tsx main.tsx +``` diff --git a/packages/website/src/content/guides/1-sending-messages.md b/packages/website/src/content/guides/1-sending-messages.md new file mode 100644 index 0000000..71b94b7 --- /dev/null +++ b/packages/website/src/content/guides/1-sending-messages.md @@ -0,0 +1,166 @@ +--- +title: Sending Messages +description: Sending messages by creating Reacord instances +slug: sending-messages +--- + +# 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(() => { + setCurrentTime(Date.now()) + }, 3000) + return () => clearInterval(interval) + }, []) + + return <>this message has been shown for {currentTime - startTime}ms +} + +client.on("ready", () => { + reacord.send(channelId, ) +}) +``` + +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() + instance.render() +}) +``` + +## 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 + + + +To reply to a command interaction, use the `.reply()` function. This function returns an instance that works the same way as the one from `.send()`. Here's an example: + +```jsx +import { Client } from "discord.js" +import * as React from "react" +import { Button, ReacordDiscordJs } from "reacord" + +const client = new Client({ intents: [] }) +const reacord = new ReacordDiscordJs(client) + +client.on("ready", () => { + client.application?.commands.create({ + name: "ping", + description: "pong!", + }) +}) + +client.on("interactionCreate", (interaction) => { + if (interaction.isCommand() && interaction.commandName === "ping") { + // Use the reply() function instead of send + reacord.reply(interaction, <>pong!) + } +}) + +client.login(process.env.DISCORD_TOKEN) +``` + + + +However, the process of creating commands can get really repetitive and error-prone. A command framework could help with this, or you could make a small helper: + +```jsx +function handleCommands(client, commands) { + client.on("ready", () => { + for (const { name, description } of commands) { + client.application?.commands.create({ name, description }) + } + }) + + client.on("interactionCreate", (interaction) => { + if (interaction.isCommand()) { + for (const command of commands) { + if (interaction.commandName === command.name) { + command.run(interaction) + } + } + } + }) +} +``` + +```jsx +handleCommands(client, [ + { + name: "ping", + description: "pong!", + run: (interaction) => { + reacord.reply(interaction, <>pong!) + }, + }, + { + name: "hi", + description: "say hi", + run: (interaction) => { + reacord.reply(interaction, <>hi) + }, + }, +]) +``` + +## Ephemeral Command Replies + +Ephemeral replies are replies that only appear for one user. To create them, use the `.ephemeralReply()` function. + +```tsx +handleCommands(client, [ + { + name: "pong", + description: "pong, but in secret", + run: (interaction) => { + reacord.ephemeralReply(interaction, <>(pong)) + }, + }, +]) +``` + +The `ephemeralReply` function also returns an instance, but ephemeral replies cannot be updated via `instance.render()`. You can `.deactivate()` them, but `.destroy()` will not delete the message; only the user can hide it from view. diff --git a/packages/website/src/content/guides/2-embeds.md b/packages/website/src/content/guides/2-embeds.md new file mode 100644 index 0000000..7712d93 --- /dev/null +++ b/packages/website/src/content/guides/2-embeds.md @@ -0,0 +1,63 @@ +--- +title: Embeds +description: Using embed components +slug: embeds +--- + +# Embeds + +Reacord comes with an `` component for sending rich embeds. + +```jsx +import { Embed } from "reacord" + +function FancyMessage({ title, description }) { + return ( + + ) +} +``` + +```jsx +reacord.send(channelId, ) +``` + +Reacord also comes with multiple embed components, for defining embeds on a piece-by-piece basis. This enables composition: + +```jsx +import { Embed, EmbedTitle } from "reacord" + +function FancyDetails({ title, description }) { + return ( + <> + {title} + {/* embed descriptions are just text */} + {description} + + ) +} + +function FancyMessage({ children }) { + return ( + + {children} + + ) +} +``` + +```jsx +reacord.send( + channelId, + + + , +) +``` + +See the [API Reference](/api/index.html#EmbedAuthorProps) for the full list of embed components. diff --git a/packages/website/src/content/guides/3-buttons.md b/packages/website/src/content/guides/3-buttons.md new file mode 100644 index 0000000..d8f8d81 --- /dev/null +++ b/packages/website/src/content/guides/3-buttons.md @@ -0,0 +1,46 @@ +--- +title: Buttons +description: Using button components +slug: buttons +--- + +# Buttons + +Use the `