Merge pull request #43 from domin-mnd/main

This commit is contained in:
Darius
2023-11-01 15:51:44 -05:00
committed by GitHub
9 changed files with 152 additions and 73 deletions

View File

@@ -24,19 +24,21 @@ pnpm add reacord react discord.js
<!-- prettier-ignore --> <!-- prettier-ignore -->
```tsx ```tsx
import * as React from "react" import { useState } from "react"
import { Embed, Button } from "reacord" import { Embed, Button } from "reacord"
function Counter() { function Counter() {
const [count, setCount] = React.useState(0) const [count, setCount] = useState(0)
return ( return (
<> <>
<Embed title="Counter"> <Embed title="Counter">
This button has been clicked {count} times. This button has been clicked {count} times.
</Embed> </Embed>
<Button onClick={() => setCount(count + 1)}> <Button
+1 label="+1"
</Button> onClick={() => setCount(count + 1)}
/>
</> </>
) )
} }

View File

@@ -1,6 +1,8 @@
import { raise } from "@reacord/helpers/raise.js" import { raise } from "@reacord/helpers/raise.js"
import { import {
Button, Button,
Embed,
EmbedField,
Link, Link,
Option, Option,
ReacordDiscordJs, ReacordDiscordJs,
@@ -11,7 +13,6 @@ import type { TextChannel } from "discord.js"
import { ChannelType, Client, IntentsBitField } from "discord.js" import { ChannelType, Client, IntentsBitField } from "discord.js"
import "dotenv/config" import "dotenv/config"
import { kebabCase } from "lodash-es" import { kebabCase } from "lodash-es"
import * as React from "react"
import { useState } from "react" import { useState } from "react"
const client = new Client({ intents: IntentsBitField.Flags.Guilds }) const client = new Client({ intents: IntentsBitField.Flags.Guilds })
@@ -53,9 +54,57 @@ await createTest("basic", (channel) => {
reacord.createChannelMessage(channel).render("Hello, world!") reacord.createChannelMessage(channel).render("Hello, world!")
}) })
await createTest("readme counter", (channel) => {
interface EmbedCounterProps {
count: number
visible: boolean
}
function EmbedCounter({ count, visible }: EmbedCounterProps) {
if (!visible) return <></>
return (
<Embed title="the counter">
<EmbedField name="is it even?">{count % 2 ? "no" : "yes"}</EmbedField>
</Embed>
)
}
function Counter() {
const [showEmbed, setShowEmbed] = useState(false)
const [count, setCount] = useState(0)
const instance = useInstance()
return (
<>
this button was clicked {count} times
<EmbedCounter count={count} visible={showEmbed} />
<Button
style="primary"
label="clicc"
onClick={() => setCount(count + 1)}
/>
<Button
style="secondary"
label={showEmbed ? "hide embed" : "show embed"}
onClick={() => setShowEmbed(!showEmbed)}
/>
<Button
style="danger"
label="deactivate"
onClick={() => instance.destroy()}
/>
</>
)
}
reacord.createChannelMessage(channel).render(<Counter />)
})
await createTest("counter", (channel) => { await createTest("counter", (channel) => {
const Counter = () => { function Counter() {
const [count, setCount] = React.useState(0) const [count, setCount] = useState(0)
return ( return (
<> <>
count: {count} count: {count}

View File

@@ -6,7 +6,7 @@ slug: getting-started
# Getting Started # Getting Started
These guides assume some familiarity with [JavaScript](https://developer.mozilla.org/en-US/docs/Web/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. These guides assume some familiarity with [JavaScript](https://developer.mozilla.org/en-US/docs/Web/javascript), [TypeScript](https://www.typescriptlang.org/), [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 ## Setup from template
@@ -29,31 +29,16 @@ pnpm add reacord react discord.js
Create a Discord.js client and a Reacord instance: Create a Discord.js client and a Reacord instance:
```js ```ts
// main.jsx import { Client, Events } from "discord.js"
import { Client } from "discord.js"
import { ReacordDiscordJs } from "reacord" import { ReacordDiscordJs } from "reacord"
const client = new Client() const client = new Client()
const reacord = new ReacordDiscordJs(client) const reacord = new ReacordDiscordJs(client)
client.on("ready", () => { client.once(Events.ClientReady, () => {
console.log("Ready!") console.log("Ready!")
}) })
await client.login(process.env.BOT_TOKEN) await client.login(process.env.BOT_TOKEN)
``` ```
To use JSX in your code, run it with [tsx](https://npm.im/tsx):
```bash
npm install -D tsx
npx tsx main.tsx
```
For production, I recommend compiling it with [tsup](https://npm.im/tsup):
```bash
npm install -D tsup
npx tsup src/main.tsx --target node20
```

View File

@@ -8,8 +8,8 @@ slug: sending-messages
You can send messages via Reacord to a channel like so. You can send messages via Reacord to a channel like so.
```jsx ```tsx
client.on("ready", () => { client.once(Events.ClientReady, () => {
const channel = await client.channels.fetch("abc123deadbeef") const channel = await client.channels.fetch("abc123deadbeef")
reacord.createChannelMessage(channel).render("Hello, world!") reacord.createChannelMessage(channel).render("Hello, world!")
}) })
@@ -19,7 +19,9 @@ The `.createChannelMessage()` function creates a **Reacord instance**. You can p
Components rendered through this instance can include state and effects, and the message on Discord will update automatically. Components rendered through this instance can include state and effects, and the message on Discord will update automatically.
```jsx ```tsx
import { useEffect, useState } from "react"
function Uptime() { function Uptime() {
const [startTime] = useState(Date.now()) const [startTime] = useState(Date.now())
const [currentTime, setCurrentTime] = useState(Date.now()) const [currentTime, setCurrentTime] = useState(Date.now())
@@ -34,7 +36,7 @@ function Uptime() {
return <>this message has been shown for {currentTime - startTime}ms</> return <>this message has been shown for {currentTime - startTime}ms</>
} }
client.on("ready", () => { client.once(Events.ClientReady, () => {
const instance = reacord.createChannelMessage(channel) const instance = reacord.createChannelMessage(channel)
instance.render(<Uptime />) instance.render(<Uptime />)
}) })
@@ -42,10 +44,14 @@ client.on("ready", () => {
The instance can be rendered to multiple times, which will update the message each time. The instance can be rendered to multiple times, which will update the message each time.
```jsx ```tsx
const Hello = ({ subject }) => <>Hello, {subject}!</> interface HelloProps {
subject: string
}
client.on("ready", () => { const Hello = ({ subject }: HelloProps) => <>Hello, {subject}!</>
client.once(Events.ClientReady, () => {
const instance = reacord.createChannelMessage(channel) const instance = reacord.createChannelMessage(channel)
instance.render(<Hello subject="World" />) instance.render(<Hello subject="World" />)
instance.render(<Hello subject="Moon" />) instance.render(<Hello subject="Moon" />)
@@ -54,7 +60,7 @@ client.on("ready", () => {
You can specify various options for the message: You can specify various options for the message:
```jsx ```tsx
const instance = reacord.createChannelMessage(channel, { const instance = reacord.createChannelMessage(channel, {
tts: true, tts: true,
reply: { reply: {
@@ -75,7 +81,7 @@ If you no longer want to use the instance, you can clean it up in a few ways:
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: 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 ```ts
const reacord = new ReacordDiscordJs(client, { const reacord = new ReacordDiscordJs(client, {
// after sending four messages, // after sending four messages,
// the first one will be deactivated // the first one will be deactivated
@@ -91,29 +97,29 @@ This section also applies to other kinds of application commands, such as contex
To reply to a command interaction, use the `.createInteractionReply()` function. This function returns an instance that works the same way as the one from `.createChannelMessage()`. Here's an example: To reply to a command interaction, use the `.createInteractionReply()` function. This function returns an instance that works the same way as the one from `.createChannelMessage()`. Here's an example:
```jsx ```tsx
import { Client } from "discord.js" import { Client, Events } from "discord.js"
import { Button, ReacordDiscordJs } from "reacord" import { Button, ReacordDiscordJs } from "reacord"
import * as React from "react" import * as React from "react"
const client = new Client({ intents: [] }) const client = new Client({ intents: [] })
const reacord = new ReacordDiscordJs(client) const reacord = new ReacordDiscordJs(client)
client.on("ready", () => { client.once(Events.ClientReady, () => {
client.application?.commands.create({ client.application?.commands.create({
name: "ping", name: "ping",
description: "pong!", description: "pong!",
}) })
}) })
client.on("interactionCreate", (interaction) => { client.on(Events.InteractionCreate, (interaction) => {
if (interaction.isCommand() && interaction.commandName === "ping") { if (interaction.isCommand() && interaction.commandName === "ping") {
// Use the createInteractionReply() function instead of createChannelMessage // Use the createInteractionReply() function instead of createChannelMessage
reacord.createInteractionReply(interaction).render(<>pong!</>) reacord.createInteractionReply(interaction).render(<>pong!</>)
} }
}) })
client.login(process.env.DISCORD_TOKEN) await client.login(process.env.DISCORD_TOKEN)
``` ```
<aside> <aside>
@@ -122,15 +128,26 @@ This example uses <a href="https://discord.com/developers/docs/interactions/appl
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: 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 ```tsx
function handleCommands(client, commands) { import type { Client, CommandInteraction } from "discord.js"
client.on("ready", () => {
interface Command {
// Command name
name: string
// A mandatory description for the command
description: string
// Specific handler for the command
run: (interaction: CommandInteraction) => Promise<void> | void
}
function handleCommands(client: Client, commands: Command[]) {
client.once(Events.ClientReady, () => {
for (const { name, description } of commands) { for (const { name, description } of commands) {
client.application?.commands.create({ name, description }) client.application?.commands.create({ name, description })
} }
}) })
client.on("interactionCreate", (interaction) => { client.on(Events.InteractionCreate, (interaction) => {
if (interaction.isCommand()) { if (interaction.isCommand()) {
for (const command of commands) { for (const command of commands) {
if (interaction.commandName === command.name) { if (interaction.commandName === command.name) {
@@ -142,7 +159,7 @@ function handleCommands(client, commands) {
} }
``` ```
```jsx ```tsx
handleCommands(client, [ handleCommands(client, [
{ {
name: "ping", name: "ping",
@@ -165,7 +182,7 @@ handleCommands(client, [
Ephemeral replies are replies that only appear for one user. To create them, use the `.createInteractionReply()` function and provide `ephemeral` option. Ephemeral replies are replies that only appear for one user. To create them, use the `.createInteractionReply()` function and provide `ephemeral` option.
```jsx ```tsx
handleCommands(client, [ handleCommands(client, [
{ {
name: "pong", name: "pong",
@@ -183,7 +200,7 @@ handleCommands(client, [
Additionally interaction replies may have `tts` option to turn on text-to-speech ability for the reply. To create such reply, use `.createInteractionReply()` function and provide `tts` option. Additionally interaction replies may have `tts` option to turn on text-to-speech ability for the reply. To create such reply, use `.createInteractionReply()` function and provide `tts` option.
```jsx ```tsx
handleCommands(client, [ handleCommands(client, [
{ {
name: "pong", name: "pong",

View File

@@ -8,10 +8,15 @@ slug: embeds
Reacord comes with an `<Embed />` component for sending rich embeds. Reacord comes with an `<Embed />` component for sending rich embeds.
```jsx ```tsx
import { Embed } from "reacord" import { Embed } from "reacord"
function FancyMessage({ title, description }) { interface FancyMessageProps {
title: string
description: string
}
function FancyMessage({ title, description }: FancyMessageProps) {
return ( return (
<Embed <Embed
title={title} title={title}
@@ -23,7 +28,7 @@ function FancyMessage({ title, description }) {
} }
``` ```
```jsx ```tsx
reacord reacord
.createChannelMessage(channel) .createChannelMessage(channel)
.render(<FancyMessage title="Hello" description="World" />) .render(<FancyMessage title="Hello" description="World" />)
@@ -31,10 +36,15 @@ reacord
Reacord also comes with multiple embed components, for defining embeds on a piece-by-piece basis. This enables composition: Reacord also comes with multiple embed components, for defining embeds on a piece-by-piece basis. This enables composition:
```jsx ```tsx
import { Embed, EmbedTitle } from "reacord" import { Embed, EmbedTitle } from "reacord"
function FancyDetails({ title, description }) { interface FancyDetailsProps {
title: string
description: string
}
function FancyDetails({ title, description }: FancyDetailsProps) {
return ( return (
<> <>
<EmbedTitle>{title}</EmbedTitle> <EmbedTitle>{title}</EmbedTitle>
@@ -44,7 +54,11 @@ function FancyDetails({ title, description }) {
) )
} }
function FancyMessage({ children }) { interface FancyMessageProps {
children: React.ReactNode
}
function FancyMessage({ children }: FancyMessageProps) {
return ( return (
<Embed color={0x00ff00} timestamp={Date.now()}> <Embed color={0x00ff00} timestamp={Date.now()}>
{children} {children}
@@ -53,7 +67,7 @@ function FancyMessage({ children }) {
} }
``` ```
```jsx ```tsx
reacord.createChannelMessage(channel).render( reacord.createChannelMessage(channel).render(
<FancyMessage> <FancyMessage>
<FancyDetails title="Hello" description="World" /> <FancyDetails title="Hello" description="World" />

View File

@@ -8,10 +8,11 @@ slug: buttons
Use the `<Button />` component to create a message with a button, and use the `onClick` callback to respond to button clicks. Use the `<Button />` component to create a message with a button, and use the `onClick` callback to respond to button clicks.
```jsx ```tsx
import { Button } from "reacord" import { Button } from "reacord"
import { useState } from "react"
function Counter() { export function Counter() {
const [count, setCount] = useState(0) const [count, setCount] = useState(0)
return ( return (
@@ -25,12 +26,12 @@ function Counter() {
The `onClick` callback receives an `event` object. It includes some information, such as the user who clicked the button, and functions for creating new replies in response. These functions return message instances. The `onClick` callback receives an `event` object. It includes some information, such as the user who clicked the button, and functions for creating new replies in response. These functions return message instances.
```jsx ```tsx
import { Button } from "reacord" import { Button, type ComponentEvent } from "reacord"
function TheButton() { export function SuspiciousButton() {
function handleClick(event) { function handleClick(event: ComponentEvent) {
const name = event.guild.member.displayName || event.user.username const name = event.guild.member.displayName ?? event.user.username
const publicReply = event.reply(`${name} clicked the button. wow`) const publicReply = event.reply(`${name} clicked the button. wow`)
setTimeout(() => publicReply.destroy(), 3000) setTimeout(() => publicReply.destroy(), 3000)

View File

@@ -8,10 +8,10 @@ slug: links
In Discord, links are a type of button, and they work similarly. Clicking on it leads you to the given URL. They only have one style, and can't be listened to for clicks. In Discord, links are a type of button, and they work similarly. Clicking on it leads you to the given URL. They only have one style, and can't be listened to for clicks.
```jsx ```tsx
import { Link } from "reacord" import { Link } from "reacord"
function AwesomeLinks() { export function AwesomeLinks() {
return ( return (
<> <>
<Link label="look at this" url="https://google.com" /> <Link label="look at this" url="https://google.com" />

View File

@@ -8,9 +8,16 @@ slug: select-menus
To create a select menu, use the `Select` component, and pass a list of `Option` components as children. Use the `value` prop to set a currently selected value. You can respond to changes in the value via `onChangeValue`. To create a select menu, use the `Select` component, and pass a list of `Option` components as children. Use the `value` prop to set a currently selected value. You can respond to changes in the value via `onChangeValue`.
```jsx ```tsx
export function FruitSelect({ onConfirm }) { import { Button, Option, Select } from "reacord"
const [value, setValue] = useState() import { useState } from "react"
interface FruitSelectProps {
onConfirm: (choice: string) => void
}
export function FruitSelect({ onConfirm }: FruitSelectProps) {
const [value, setValue] = useState<string>()
return ( return (
<> <>
@@ -35,7 +42,7 @@ export function FruitSelect({ onConfirm }) {
} }
``` ```
```jsx ```tsx
const instance = reacord.createChannelMessage(channel).render( const instance = reacord.createChannelMessage(channel).render(
<FruitSelect <FruitSelect
onConfirm={(value) => { onConfirm={(value) => {
@@ -48,9 +55,13 @@ const instance = reacord.createChannelMessage(channel).render(
For a multi-select, use the `multiple` prop, then you can use `values` and `onChangeMultiple` to handle multiple values. For a multi-select, use the `multiple` prop, then you can use `values` and `onChangeMultiple` to handle multiple values.
```jsx ```tsx
export function FruitSelect({ onConfirm }) { interface FruitSelectProps {
const [values, setValues] = useState([]) onConfirm: (choices: string[]) => void
}
export function FruitSelect({ onConfirm }: FruitSelectProps) {
const [values, setValues] = useState<string[]>([])
return ( return (
<Select <Select

View File

@@ -8,10 +8,10 @@ slug: use-instance
You can use `useInstance` to get the current [instance](/guides/sending-messages) within a component. This can be used to let a component destroy or deactivate itself. You can use `useInstance` to get the current [instance](/guides/sending-messages) within a component. This can be used to let a component destroy or deactivate itself.
```jsx ```tsx
import { Button, useInstance } from "reacord" import { Button, useInstance } from "reacord"
function SelfDestruct() { export function SelfDestruct() {
const instance = useInstance() const instance = useInstance()
return ( return (
<Button <Button