Compare commits
7 Commits
reacord@0.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 061608323f | |||
| d1611d8f64 | |||
|
|
b641885112 | ||
|
|
2a8ee7885d | ||
|
|
6c71073d10 | ||
|
|
5674e3c1b6 | ||
|
|
a41c825cdd |
30
README.md
30
README.md
@@ -1,42 +1,30 @@
|
|||||||
<center>
|
|
||||||
<img src="packages/website/src/assets/banner.png" alt="Reacord: Create interactive Discord messages using React">
|
|
||||||
</center>
|
|
||||||
|
|
||||||
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
||||||
|
|
||||||
```console
|
```console
|
||||||
# npm
|
# bun
|
||||||
npm install reacord react discord.js
|
bun add reacord react discord.js
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn add reacord react discord.js
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm add reacord react discord.js
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Get Started
|
|
||||||
|
|
||||||
[Visit the docs to get started.](https://reacord.mapleleaf.dev/guides/getting-started)
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
<!-- 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)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -1,18 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "reacord-monorepo",
|
"name": "reacord-monorepo",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "run-s --continue-on-error lint:*",
|
"lint": "run-s --continue-on-error lint:*",
|
||||||
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
|
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
|
||||||
"lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different",
|
"lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different",
|
||||||
"lint:types": "tsc -b & pnpm -r --parallel run typecheck",
|
"lint:types": "bun run --cwd packages/helpers typecheck && bun run --cwd packages/reacord typecheck",
|
||||||
"astro-sync": "pnpm --filter website exec astro sync",
|
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "pnpm -r run build",
|
"build": "bun run --cwd packages/reacord build",
|
||||||
"build:website": "pnpm --filter website... run build",
|
"release": "bun run build && changeset publish"
|
||||||
"start": "pnpm -C packages/website run start",
|
|
||||||
"start:website": "pnpm -C packages/website run start",
|
|
||||||
"release": "pnpm -r run build && changeset publish"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.26.2",
|
"@changesets/cli": "^2.26.2",
|
||||||
@@ -21,7 +20,6 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tailwindcss": "^3.3.3",
|
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vitest": "^0.34.6"
|
"vitest": "^0.34.6"
|
||||||
},
|
},
|
||||||
@@ -33,8 +31,7 @@
|
|||||||
],
|
],
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"dist",
|
"dist"
|
||||||
"packages/website/public/api"
|
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ import type { ComponentEvent } from "../component-event"
|
|||||||
import { OptionNode } from "./option-node"
|
import { OptionNode } from "./option-node"
|
||||||
import { omit } from "@reacord/helpers/omit.js"
|
import { omit } from "@reacord/helpers/omit.js"
|
||||||
|
|
||||||
|
export type SelectMenuType =
|
||||||
|
| "string"
|
||||||
|
| "user"
|
||||||
|
| "role"
|
||||||
|
| "mentionable"
|
||||||
|
| "channel"
|
||||||
|
|
||||||
/** @category Select */
|
/** @category Select */
|
||||||
export interface SelectProps {
|
export interface SelectProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
@@ -25,6 +32,20 @@ export interface SelectProps {
|
|||||||
/** The text shown when no value is selected */
|
/** The text shown when no value is selected */
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The kind of select menu to render.
|
||||||
|
*
|
||||||
|
* Defaults to `string`.
|
||||||
|
*/
|
||||||
|
menuType?: SelectMenuType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit the channel types shown in a channel select menu.
|
||||||
|
*
|
||||||
|
* This is only used when `menuType` is `channel`.
|
||||||
|
*/
|
||||||
|
channelTypes?: number[]
|
||||||
|
|
||||||
/** Set to true to allow multiple selected values */
|
/** Set to true to allow multiple selected values */
|
||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { ReacordInstance } from "./instance.js"
|
import type { ReacordInstance } from "./instance.js"
|
||||||
import { raise } from "@reacord/helpers/raise.js"
|
import { raise } from "@reacord/helpers/raise.js"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import type { MessageStore } from "../internal/message-store.js"
|
||||||
|
|
||||||
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
||||||
|
const MessageContext = React.createContext<MessageStore | undefined>(undefined)
|
||||||
|
|
||||||
export const InstanceProvider = Context.Provider
|
export const InstanceProvider = Context.Provider
|
||||||
|
export const MessageProvider = MessageContext.Provider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the associated instance for the current component.
|
* Get the associated instance for the current component.
|
||||||
@@ -18,3 +21,27 @@ export function useInstance(): ReacordInstance {
|
|||||||
raise("Could not find instance, was this component rendered via Reacord?")
|
raise("Could not find instance, was this component rendered via Reacord?")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message that the current component is rendered into.
|
||||||
|
*
|
||||||
|
* @category Core
|
||||||
|
*/
|
||||||
|
export function useMessage() {
|
||||||
|
const store =
|
||||||
|
React.useContext(MessageContext) ??
|
||||||
|
raise("Could not find message store, was this component rendered via Reacord?")
|
||||||
|
|
||||||
|
const getSnapshot = React.useCallback(() => store.getSnapshot(), [store])
|
||||||
|
|
||||||
|
if (React.useSyncExternalStore) {
|
||||||
|
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState(getSnapshot)
|
||||||
|
React.useEffect(() => store.subscribe(() => setValue(getSnapshot())), [
|
||||||
|
store,
|
||||||
|
getSnapshot,
|
||||||
|
])
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
super(config)
|
super(config)
|
||||||
|
|
||||||
client.on("interactionCreate", (interaction) => {
|
client.on("interactionCreate", (interaction) => {
|
||||||
if (interaction.isButton() || interaction.isStringSelectMenu()) {
|
if (interaction.isButton() || interaction.isAnySelectMenu()) {
|
||||||
this.handleComponentInteraction(
|
this.handleComponentInteraction(
|
||||||
this.createReacordComponentInteraction(interaction),
|
this.createReacordComponentInteraction(interaction),
|
||||||
)
|
)
|
||||||
@@ -154,7 +154,9 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
raise(`Channel ${channel.id} must be a text channel`)
|
raise(`Channel ${channel.id} must be a text channel`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = await channel.send({
|
const textChannel = channel as Discord.TextBasedChannel &
|
||||||
|
Discord.PartialTextBasedChannelFields
|
||||||
|
const message = await textChannel.send({
|
||||||
...getDiscordMessageOptions(messageOptions),
|
...getDiscordMessageOptions(messageOptions),
|
||||||
...messageCreateOptions,
|
...messageCreateOptions,
|
||||||
})
|
})
|
||||||
@@ -212,41 +214,16 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
|
|
||||||
const message: ComponentEventMessage =
|
const message: ComponentEventMessage =
|
||||||
interaction.message instanceof Discord.Message
|
interaction.message instanceof Discord.Message
|
||||||
? {
|
? createComponentEventMessage(interaction.message)
|
||||||
...pick(interaction.message, [
|
|
||||||
"id",
|
|
||||||
"channelId",
|
|
||||||
"authorId",
|
|
||||||
"content",
|
|
||||||
"tts",
|
|
||||||
"mentionEveryone",
|
|
||||||
]),
|
|
||||||
timestamp: new Date(
|
|
||||||
interaction.message.createdTimestamp,
|
|
||||||
).toISOString(),
|
|
||||||
editedTimestamp: interaction.message.editedTimestamp
|
|
||||||
? new Date(interaction.message.editedTimestamp).toISOString()
|
|
||||||
: undefined,
|
|
||||||
mentions: interaction.message.mentions.users.map((u) => u.id),
|
|
||||||
authorId: interaction.message.author.id,
|
|
||||||
mentionEveryone: interaction.message.mentions.everyone,
|
|
||||||
}
|
|
||||||
: raise("Message not found")
|
: raise("Message not found")
|
||||||
|
|
||||||
const member: ComponentEventGuildMember | undefined =
|
const member: ComponentEventGuildMember | undefined =
|
||||||
interaction.member instanceof Discord.GuildMember
|
interaction.member instanceof Discord.GuildMember
|
||||||
? {
|
? {
|
||||||
...pruneNullishValues(
|
...pruneNullishValues(
|
||||||
pick(interaction.member, [
|
pick(interaction.member, ["nick", "avatarUrl", "pending"]),
|
||||||
"id",
|
|
||||||
"nick",
|
|
||||||
"displayName",
|
|
||||||
"avatarUrl",
|
|
||||||
"displayAvatarUrl",
|
|
||||||
"color",
|
|
||||||
"pending",
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
|
id: interaction.member.id,
|
||||||
displayName: interaction.member.displayName,
|
displayName: interaction.member.displayName,
|
||||||
roles: interaction.member.roles.cache.map((role) => role.id),
|
roles: interaction.member.roles.cache.map((role) => role.id),
|
||||||
joinedAt: interaction.member.joinedAt?.toISOString(),
|
joinedAt: interaction.member.joinedAt?.toISOString(),
|
||||||
@@ -260,15 +237,17 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
|
|
||||||
const guild: ComponentEventGuild | undefined = interaction.guild
|
const guild: ComponentEventGuild | undefined = interaction.guild
|
||||||
? {
|
? {
|
||||||
...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
|
id: interaction.guild.id,
|
||||||
|
name: interaction.guild.name,
|
||||||
member: member ?? raise("unexpected: member is undefined"),
|
member: member ?? raise("unexpected: member is undefined"),
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const user: ComponentEventUser = {
|
const user: ComponentEventUser = {
|
||||||
...pruneNullishValues(
|
id: interaction.user.id,
|
||||||
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
username: interaction.user.username,
|
||||||
),
|
discriminator: interaction.user.discriminator,
|
||||||
|
tag: interaction.user.tag,
|
||||||
avatarUrl: interaction.user.avatarURL(),
|
avatarUrl: interaction.user.avatarURL(),
|
||||||
accentColor: interaction.user.accentColor ?? undefined,
|
accentColor: interaction.user.accentColor ?? undefined,
|
||||||
}
|
}
|
||||||
@@ -331,7 +310,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.isStringSelectMenu()) {
|
if (interaction.isAnySelectMenu()) {
|
||||||
return {
|
return {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
type: "select",
|
type: "select",
|
||||||
@@ -348,6 +327,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
|
|
||||||
function createReacordMessage(message: Discord.Message): Message {
|
function createReacordMessage(message: Discord.Message): Message {
|
||||||
return {
|
return {
|
||||||
|
data: createComponentEventMessage(message),
|
||||||
edit: async (options) => {
|
edit: async (options) => {
|
||||||
await message.edit(getDiscordMessageOptions(options))
|
await message.edit(getDiscordMessageOptions(options))
|
||||||
},
|
},
|
||||||
@@ -357,6 +337,28 @@ function createReacordMessage(message: Discord.Message): Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createComponentEventMessage(
|
||||||
|
message: Discord.Message,
|
||||||
|
): ComponentEventMessage {
|
||||||
|
return {
|
||||||
|
...pick(message, [
|
||||||
|
"id",
|
||||||
|
"channelId",
|
||||||
|
"authorId",
|
||||||
|
"content",
|
||||||
|
"tts",
|
||||||
|
"mentionEveryone",
|
||||||
|
]),
|
||||||
|
timestamp: new Date(message.createdTimestamp).toISOString(),
|
||||||
|
editedTimestamp: message.editedTimestamp
|
||||||
|
? new Date(message.editedTimestamp).toISOString()
|
||||||
|
: undefined,
|
||||||
|
mentions: message.mentions.users.map((u) => u.id),
|
||||||
|
authorId: message.author.id,
|
||||||
|
mentionEveryone: message.mentions.everyone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
||||||
const styleMap = {
|
const styleMap = {
|
||||||
primary: Discord.ButtonStyle.Primary,
|
primary: Discord.ButtonStyle.Primary,
|
||||||
@@ -403,14 +405,58 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
|
|||||||
// future proofing
|
// future proofing
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (component.type === "select") {
|
if (component.type === "select") {
|
||||||
return {
|
const {
|
||||||
...component,
|
menuType,
|
||||||
type: Discord.ComponentType.SelectMenu,
|
values,
|
||||||
options: component.options.map((option) => ({
|
options: selectOptions,
|
||||||
...option,
|
channelTypes,
|
||||||
default: component.values?.includes(option.value),
|
multiple,
|
||||||
})),
|
...rest
|
||||||
|
} = component
|
||||||
|
|
||||||
|
if (menuType === "string" || menuType == undefined) {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.StringSelect,
|
||||||
|
options: selectOptions.map((option) => ({
|
||||||
|
...option,
|
||||||
|
default: values?.includes(option.value),
|
||||||
|
})),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (menuType === "user") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.UserSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuType === "role") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.RoleSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuType === "mentionable") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.MentionableSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuType === "channel") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.ChannelSelect,
|
||||||
|
channelTypes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raise(
|
||||||
|
`Unsupported select menu type: ${menuType ?? "string"}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
component satisfies never
|
component satisfies never
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
|
|||||||
import type { ComponentInteraction } from "../internal/interaction.js"
|
import type { ComponentInteraction } from "../internal/interaction.js"
|
||||||
import { reconciler } from "../internal/reconciler.js"
|
import { reconciler } from "../internal/reconciler.js"
|
||||||
import type { Renderer } from "../internal/renderers/renderer.js"
|
import type { Renderer } from "../internal/renderers/renderer.js"
|
||||||
import { InstanceProvider } from "./instance-context.js"
|
import { InstanceProvider, MessageProvider } from "./instance-context.js"
|
||||||
import type { ReacordInstance } from "./instance.js"
|
import type { ReacordInstance } from "./instance.js"
|
||||||
|
|
||||||
/** @category Core */
|
/** @category Core */
|
||||||
@@ -54,7 +54,11 @@ export abstract class Reacord {
|
|||||||
const instance: ReacordInstance = {
|
const instance: ReacordInstance = {
|
||||||
render: (content: ReactNode) => {
|
render: (content: ReactNode) => {
|
||||||
reconciler.updateContainer(
|
reconciler.updateContainer(
|
||||||
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
<InstanceProvider value={instance}>
|
||||||
|
<MessageProvider value={renderer.messageStore}>
|
||||||
|
{content}
|
||||||
|
</MessageProvider>
|
||||||
|
</InstanceProvider>,
|
||||||
container,
|
container,
|
||||||
)
|
)
|
||||||
return instance
|
return instance
|
||||||
|
|||||||
22
packages/reacord/library/internal/message-store.ts
Normal file
22
packages/reacord/library/internal/message-store.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ComponentEventMessage } from "../core/component-event"
|
||||||
|
|
||||||
|
export class MessageStore {
|
||||||
|
private value: ComponentEventMessage | undefined
|
||||||
|
private listeners = new Set<() => void>()
|
||||||
|
|
||||||
|
getSnapshot = () => this.value
|
||||||
|
|
||||||
|
subscribe = (listener: () => void) => {
|
||||||
|
this.listeners.add(listener)
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(value: ComponentEventMessage | undefined) {
|
||||||
|
this.value = value
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ComponentEventMessage } from "../core/component-event"
|
||||||
import type { EmbedOptions } from "../core/components/embed-options"
|
import type { EmbedOptions } from "../core/components/embed-options"
|
||||||
import type { SelectProps } from "../core/components/select"
|
import type { SelectProps } from "../core/components/select"
|
||||||
import { last } from "@reacord/helpers/last"
|
import { last } from "@reacord/helpers/last"
|
||||||
@@ -47,6 +48,7 @@ export interface MessageSelectOptionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
|
data?: ComponentEventMessage
|
||||||
edit(options: MessageOptions): Promise<void>
|
edit(options: MessageOptions): Promise<void>
|
||||||
delete(): Promise<void>
|
delete(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const config: HostConfig<
|
|||||||
never, // SuspenseInstance,
|
never, // SuspenseInstance,
|
||||||
never, // HydratableInstance,
|
never, // HydratableInstance,
|
||||||
never, // PublicInstance,
|
never, // PublicInstance,
|
||||||
never, // HostContext,
|
null, // HostContext,
|
||||||
true, // UpdatePayload,
|
true, // UpdatePayload,
|
||||||
never, // ChildSet,
|
never, // ChildSet,
|
||||||
number, // TimeoutHandle,
|
number, // TimeoutHandle,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Container } from "../container.js"
|
import { Container } from "../container.js"
|
||||||
import type { ComponentInteraction } from "../interaction"
|
import type { ComponentInteraction } from "../interaction"
|
||||||
|
import { MessageStore } from "../message-store.js"
|
||||||
import type { Message, MessageOptions } from "../message"
|
import type { Message, MessageOptions } from "../message"
|
||||||
import type { Node } from "../node.js"
|
import type { Node } from "../node.js"
|
||||||
import { Subject } from "rxjs"
|
import { Subject } from "rxjs"
|
||||||
@@ -12,6 +13,7 @@ type UpdatePayload =
|
|||||||
|
|
||||||
export abstract class Renderer {
|
export abstract class Renderer {
|
||||||
readonly nodes = new Container<Node<unknown>>()
|
readonly nodes = new Container<Node<unknown>>()
|
||||||
|
readonly messageStore = new MessageStore()
|
||||||
private componentInteraction?: ComponentInteraction
|
private componentInteraction?: ComponentInteraction
|
||||||
private message?: Message
|
private message?: Message
|
||||||
private active = true
|
private active = true
|
||||||
@@ -75,6 +77,7 @@ export abstract class Renderer {
|
|||||||
private async updateMessage(payload: UpdatePayload) {
|
private async updateMessage(payload: UpdatePayload) {
|
||||||
if (payload.action === "destroy") {
|
if (payload.action === "destroy") {
|
||||||
this.updateSubscription.unsubscribe()
|
this.updateSubscription.unsubscribe()
|
||||||
|
this.messageStore.set(undefined)
|
||||||
await this.message?.delete()
|
await this.message?.delete()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -113,5 +116,6 @@ export abstract class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.message = await this.createMessage(payload.options)
|
this.message = await this.createMessage(payload.options)
|
||||||
|
this.messageStore.set(this.message.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export * from "./core/components/link"
|
|||||||
export * from "./core/components/option"
|
export * from "./core/components/option"
|
||||||
export * from "./core/components/select"
|
export * from "./core/components/select"
|
||||||
export * from "./core/instance"
|
export * from "./core/instance"
|
||||||
export { useInstance } from "./core/instance-context"
|
export { useInstance, useMessage } from "./core/instance-context"
|
||||||
export * from "./core/reacord"
|
export * from "./core/reacord"
|
||||||
export * from "./core/reacord-discord-js"
|
export * from "./core/reacord-discord-js"
|
||||||
|
|||||||
@@ -36,22 +36,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node18 --format cjs,esm --sourcemap --dts --dts-resolve",
|
"build": "rm -rf dist && mkdir -p dist && cp ../../README.md ../../LICENSE . && bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --external react --external react/jsx-runtime --external react/jsx-dev-runtime && bun build library/main.ts --target=node --outdir=dist --format=cjs --sourcemap=external --entry-naming=main.cjs --external react --external react/jsx-runtime --external react/jsx-dev-runtime && tsc -p tsconfig.build.json",
|
||||||
"build-watch": "pnpm build -- --watch",
|
"build-watch": "bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --watch --external react --external react/jsx-runtime --external react/jsx-dev-runtime",
|
||||||
"test": "vitest --coverage --no-watch",
|
"test": "vitest --coverage --no-watch",
|
||||||
"test-dev": "vitest",
|
"test-dev": "vitest",
|
||||||
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
||||||
"typecheck": "tsc -b"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.8.4",
|
|
||||||
"@types/react": "^18.2.27",
|
"@types/react": "^18.2.27",
|
||||||
"@types/react-reconciler": "^0.28.5",
|
"@types/react-reconciler": "^0.28.5",
|
||||||
"react-reconciler": "^0.29.0",
|
"react-reconciler": "^0.29.0",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"discord.js": "^14",
|
"discord.js": "^14.25.1",
|
||||||
"react": ">=17"
|
"react": ">=17"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
@@ -63,15 +62,13 @@
|
|||||||
"@reacord/helpers": "workspace:*",
|
"@reacord/helpers": "workspace:*",
|
||||||
"@types/lodash-es": "^4.17.9",
|
"@types/lodash-es": "^4.17.9",
|
||||||
"c8": "^8.0.1",
|
"c8": "^8.0.1",
|
||||||
"cpy-cli": "^5.0.0",
|
"discord.js": "^14.25.1",
|
||||||
"discord.js": "^14.13.0",
|
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"pretty-ms": "^8.0.0",
|
"pretty-ms": "^8.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tsup": "^7.2.0",
|
|
||||||
"tsx": "^3.13.0",
|
"tsx": "^3.13.0",
|
||||||
"type-fest": "^4.4.0"
|
"type-fest": "^4.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { spawnSync } from "node:child_process"
|
import { spawnSync } from "node:child_process"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { dirname } from "node:path"
|
||||||
import { createRequire } from "node:module"
|
import { createRequire } from "node:module"
|
||||||
import { beforeAll, expect, test } from "vitest"
|
import { beforeAll, expect, test } from "vitest"
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
spawnSync("pnpm", ["run", "build"])
|
const cwd = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||||
|
spawnSync("bun", ["run", "build"], { cwd })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can require commonjs", () => {
|
test("can require commonjs", () => {
|
||||||
|
|||||||
10
packages/reacord/tsconfig.build.json
Normal file
10
packages/reacord/tsconfig.build.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
11
packages/website/.gitignore
vendored
11
packages/website/.gitignore
vendored
@@ -1,11 +0,0 @@
|
|||||||
node_modules
|
|
||||||
/.cache
|
|
||||||
/build
|
|
||||||
/public/build
|
|
||||||
.env
|
|
||||||
/public/api
|
|
||||||
cypress/videos
|
|
||||||
cypress/screenshots
|
|
||||||
*.out.css
|
|
||||||
/api
|
|
||||||
.astro
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# website
|
|
||||||
|
|
||||||
## 0.4.7
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [11153df]
|
|
||||||
- Updated dependencies [fb0a997]
|
|
||||||
- reacord@0.6.0
|
|
||||||
|
|
||||||
## 0.4.6
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [ced48a3]
|
|
||||||
- reacord@0.5.5
|
|
||||||
|
|
||||||
## 0.4.5
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [41c87e3]
|
|
||||||
- reacord@0.5.4
|
|
||||||
|
|
||||||
## 0.4.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [104b175]
|
|
||||||
- Updated dependencies [156cf90]
|
|
||||||
- Updated dependencies [0bab505]
|
|
||||||
- Updated dependencies [d76f316]
|
|
||||||
- reacord@0.5.3
|
|
||||||
|
|
||||||
## 0.4.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [9813a01]
|
|
||||||
- reacord@0.5.2
|
|
||||||
|
|
||||||
## 0.4.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [72f4a4a]
|
|
||||||
- Updated dependencies [7536bde]
|
|
||||||
- Updated dependencies [e335165]
|
|
||||||
- reacord@0.5.1
|
|
||||||
|
|
||||||
## 0.4.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [aa65da5]
|
|
||||||
- reacord@0.5.0
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-nocheck
|
|
||||||
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({
|
|
||||||
applyBaseStyles: false,
|
|
||||||
}),
|
|
||||||
react(),
|
|
||||||
prefetch(),
|
|
||||||
],
|
|
||||||
markdown: {
|
|
||||||
shikiConfig: {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "module",
|
|
||||||
"name": "website",
|
|
||||||
"version": "0.4.7",
|
|
||||||
"private": true,
|
|
||||||
"sideEffects": false,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "run-p --race --print-label dev:*",
|
|
||||||
"dev:typedoc": "typedoc --watch",
|
|
||||||
"dev:astro": "astro dev",
|
|
||||||
"start": "astro preview",
|
|
||||||
"build": "typedoc && astro build",
|
|
||||||
"typecheck": "astro check && tsc -b"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/prefetch": "^0.3.0",
|
|
||||||
"@astrojs/react": "^2.3.2",
|
|
||||||
"@fontsource/jetbrains-mono": "^4.5.12",
|
|
||||||
"@fontsource/rubik": "^4.5.14",
|
|
||||||
"@heroicons/react": "^2.0.18",
|
|
||||||
"@reacord/helpers": "workspace:^",
|
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
|
||||||
"astro": "^2.10.15",
|
|
||||||
"clsx": "^2.0.0",
|
|
||||||
"reacord": "workspace:*",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"tailwind-merge": "^1.14.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@astrojs/tailwind": "^4.0.0",
|
|
||||||
"@total-typescript/ts-reset": "^0.5.1",
|
|
||||||
"@types/node": "^20.8.4",
|
|
||||||
"@types/react": "^18.2.27",
|
|
||||||
"@types/react-dom": "^18.2.12",
|
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"tailwindcss": "^3.3.3",
|
|
||||||
"typedoc": "^0.25.2",
|
|
||||||
"wait-on": "^7.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg width="53" height="35" viewBox="0 0 53 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="3" cy="3" r="1" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 146 B |
Binary file not shown.
|
Before Width: | Height: | Size: 658 B |
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
import { HeartIcon } from "@heroicons/react/20/solid"
|
|
||||||
import { twMerge } from "tailwind-merge"
|
|
||||||
import ExternalLink from "./external-link.astro"
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
class?: string
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<footer class={twMerge("text-xs opacity-75", Astro.props.class)}>
|
|
||||||
<address class="not-italic">
|
|
||||||
© {new Date().getFullYear()}
|
|
||||||
<ExternalLink class="link" href="https://github.com/itsMapleLeaf">
|
|
||||||
itsMapleLeaf
|
|
||||||
</ExternalLink>
|
|
||||||
</address>
|
|
||||||
<p>
|
|
||||||
Coded with <HeartIcon className="inline w-4 align-sub" /> using{" "}
|
|
||||||
<ExternalLink class="link" href="https://astro.build">Astro</ExternalLink>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
export type Props = astroHTML.JSX.AnchorHTMLAttributes
|
|
||||||
---
|
|
||||||
|
|
||||||
<a rel="noopener noreferrer" target="_blank" {...Astro.props}>
|
|
||||||
<slot />
|
|
||||||
</a>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
import { getCollection } from "astro:content"
|
|
||||||
import Layout from "./layout.astro"
|
|
||||||
import MainNavigation from "./main-navigation.astro"
|
|
||||||
|
|
||||||
const guides = await getCollection("guides")
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<div class="isolate">
|
|
||||||
<header
|
|
||||||
class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<MainNavigation />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="container mt-8 flex items-start gap-4">
|
|
||||||
<nav class="sticky top-24 hidden w-48 md:block">
|
|
||||||
<h2 class="text-2xl">Guides</h2>
|
|
||||||
<ul class="mt-3 flex flex-col items-start gap-2">
|
|
||||||
{
|
|
||||||
guides.map((guide) => (
|
|
||||||
<li>
|
|
||||||
<a class="link" href={`/guides/${guide.slug}`}>
|
|
||||||
{guide.data.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<section class="prose prose-invert min-w-0 flex-1 pb-8">
|
|
||||||
<slot />
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
import clsx from "clsx"
|
|
||||||
import { useEffect, useRef, useState } from "react"
|
|
||||||
import blobComfyUrl from "~/assets/blob-comfy.png"
|
|
||||||
import cursorIbeamUrl from "~/assets/cursor-ibeam.png"
|
|
||||||
import cursorUrl from "~/assets/cursor.png"
|
|
||||||
import { raise } from "@reacord/helpers/raise.ts"
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
chatInputText: "",
|
|
||||||
chatInputCursorVisible: true,
|
|
||||||
messageVisible: false,
|
|
||||||
count: 0,
|
|
||||||
cursorLeft: "25%",
|
|
||||||
cursorBottom: "-15px",
|
|
||||||
}
|
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
|
|
||||||
const animationFrame = () =>
|
|
||||||
new Promise((resolve) => requestAnimationFrame(resolve))
|
|
||||||
|
|
||||||
export function LandingAnimation() {
|
|
||||||
const [state, setState] = useState(defaultState)
|
|
||||||
const chatInputRef = useRef<HTMLDivElement>(null)
|
|
||||||
const addRef = useRef<HTMLDivElement>(null)
|
|
||||||
const deleteRef = useRef<HTMLDivElement>(null)
|
|
||||||
const cursorRef = useRef<HTMLImageElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const animateClick = (element: HTMLElement) =>
|
|
||||||
element.animate(
|
|
||||||
[{ transform: `translateY(2px)` }, { transform: `translateY(0px)` }],
|
|
||||||
300,
|
|
||||||
)
|
|
||||||
|
|
||||||
let running = true
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
while (running) {
|
|
||||||
setState(defaultState)
|
|
||||||
await delay(700)
|
|
||||||
|
|
||||||
for (const letter of "/counter") {
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
chatInputText: state.chatInputText + letter,
|
|
||||||
}))
|
|
||||||
await delay(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
await delay(1000)
|
|
||||||
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
messageVisible: true,
|
|
||||||
chatInputText: "",
|
|
||||||
}))
|
|
||||||
await delay(1000)
|
|
||||||
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
cursorLeft: "70px",
|
|
||||||
cursorBottom: "40px",
|
|
||||||
}))
|
|
||||||
await delay(1500)
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
count: state.count + 1,
|
|
||||||
chatInputCursorVisible: false,
|
|
||||||
}))
|
|
||||||
animateClick(addRef.current ?? raise("addRef is null"))
|
|
||||||
await delay(700)
|
|
||||||
}
|
|
||||||
|
|
||||||
await delay(500)
|
|
||||||
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
cursorLeft: "140px",
|
|
||||||
}))
|
|
||||||
await delay(1000)
|
|
||||||
|
|
||||||
animateClick(deleteRef.current ?? raise("deleteRef is null"))
|
|
||||||
setState((state) => ({ ...state, messageVisible: false }))
|
|
||||||
await delay(1000)
|
|
||||||
|
|
||||||
setState(() => ({
|
|
||||||
...defaultState,
|
|
||||||
chatInputCursorVisible: false,
|
|
||||||
}))
|
|
||||||
await delay(500)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
running = false
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let running = true
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
while (running) {
|
|
||||||
const cursor = cursorRef.current ?? raise("cursorRef is null")
|
|
||||||
const chatInput = chatInputRef.current ?? raise("chatInputRef is null")
|
|
||||||
|
|
||||||
// check if the cursor is in the input
|
|
||||||
const cursorRect = cursor.getBoundingClientRect()
|
|
||||||
const chatInputRect = chatInput.getBoundingClientRect()
|
|
||||||
|
|
||||||
const isOverInput =
|
|
||||||
cursorRef.current &&
|
|
||||||
chatInputRef.current &&
|
|
||||||
cursorRect.top + cursorRect.height / 2 > chatInputRect.top
|
|
||||||
|
|
||||||
cursor.src = isOverInput ? cursorIbeamUrl : cursorUrl
|
|
||||||
|
|
||||||
await animationFrame()
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
running = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="animate-fade-in pointer-events-none relative grid select-none gap-2"
|
|
||||||
role="presentation"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
"rounded-lg bg-slate-800 p-4 shadow transition",
|
|
||||||
state.messageVisible ? "opacity-100" : "-translate-y-2 opacity-0",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="h-12 w-12 rounded-full bg-black/25 bg-contain bg-no-repeat p-2">
|
|
||||||
<img
|
|
||||||
src={blobComfyUrl}
|
|
||||||
alt=""
|
|
||||||
className="h-full w-full scale-90 object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-bold">comfybot</p>
|
|
||||||
<p>this button was clicked {state.count} times</p>
|
|
||||||
<div className="mt-2 flex flex-row gap-3">
|
|
||||||
<div
|
|
||||||
ref={addRef}
|
|
||||||
className="rounded bg-emerald-700 px-3 py-1.5 text-sm text-white"
|
|
||||||
>
|
|
||||||
+1
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref={deleteRef}
|
|
||||||
className="rounded bg-red-700 px-3 py-1.5 text-sm text-white"
|
|
||||||
>
|
|
||||||
🗑 delete
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="rounded-lg bg-slate-700 px-4 pb-2 pt-1.5 shadow"
|
|
||||||
ref={chatInputRef}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
"text-sm after:relative after:-left-[2px] after:-top-px after:content-[attr(data-after)]",
|
|
||||||
state.chatInputCursorVisible
|
|
||||||
? "after:opacity-100"
|
|
||||||
: "after:opacity-0",
|
|
||||||
)}
|
|
||||||
data-after="|"
|
|
||||||
>
|
|
||||||
{state.chatInputText || (
|
|
||||||
<span className="absolute block translate-y-1 opacity-50">
|
|
||||||
Message #showing-off-reacord
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img
|
|
||||||
src={cursorUrl}
|
|
||||||
alt=""
|
|
||||||
className="absolute scale-75 bg-transparent transition-all duration-500"
|
|
||||||
style={{ left: state.cursorLeft, bottom: state.cursorBottom }}
|
|
||||||
ref={cursorRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
import "@fontsource/jetbrains-mono/500.css"
|
|
||||||
import "@fontsource/rubik/variable.css"
|
|
||||||
import packageJson from "reacord/package.json"
|
|
||||||
import bannerUrl from "~/assets/banner.png"
|
|
||||||
import faviconUrl from "~/assets/favicon.png"
|
|
||||||
import "~/styles/tailwind.css"
|
|
||||||
---
|
|
||||||
|
|
||||||
<!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.0" />
|
|
||||||
|
|
||||||
<meta name="description" content={packageJson.description} />
|
|
||||||
<meta name="theme-color" content="#21754b" />
|
|
||||||
<meta property="og:url" content="https://reacord.mapleleaf.dev/" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:title" content="Reacord" />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="Create interactive Discord messages using React"
|
|
||||||
/>
|
|
||||||
<meta property="og:image" content={bannerUrl} />
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:domain" content="reacord.mapleleaf.dev" />
|
|
||||||
<meta name="twitter:url" content="https://reacord.mapleleaf.dev/" />
|
|
||||||
<meta name="twitter:title" content="Reacord" />
|
|
||||||
<meta
|
|
||||||
name="twitter:description"
|
|
||||||
content="Create interactive Discord messages using React"
|
|
||||||
/>
|
|
||||||
<meta name="twitter:image" content={bannerUrl} />
|
|
||||||
|
|
||||||
<title>Reacord</title>
|
|
||||||
|
|
||||||
<link rel="icon" href={faviconUrl} />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<slot />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
---
|
|
||||||
import {
|
|
||||||
ArrowTopRightOnSquareIcon,
|
|
||||||
CodeBracketIcon,
|
|
||||||
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"
|
|
||||||
import Menu from "./menu.astro"
|
|
||||||
|
|
||||||
const links = [
|
|
||||||
{
|
|
||||||
href: "/guides/getting-started",
|
|
||||||
label: "Guides",
|
|
||||||
icon: DocumentTextIcon,
|
|
||||||
component: "a",
|
|
||||||
prefetch: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "/api/",
|
|
||||||
label: "API Reference",
|
|
||||||
icon: CodeBracketIcon,
|
|
||||||
component: "a",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "https://github.com/itsMapleLeaf/reacord",
|
|
||||||
label: "GitHub",
|
|
||||||
icon: ArrowTopRightOnSquareIcon,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
component: ExternalLink,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const guides = await getCollection("guides")
|
|
||||||
---
|
|
||||||
|
|
||||||
<nav class="flex h-16 items-center justify-between">
|
|
||||||
<a href="/">
|
|
||||||
<AppLogo class="w-32" />
|
|
||||||
<span class="sr-only">Home</span>
|
|
||||||
</a>
|
|
||||||
<div class="hidden gap-4 md:flex">
|
|
||||||
{
|
|
||||||
links.map((link) => (
|
|
||||||
<link.component
|
|
||||||
href={link.href}
|
|
||||||
class="link inline-flex items-center gap-1"
|
|
||||||
rel={link.prefetch ? "prefetch" : undefined}
|
|
||||||
>
|
|
||||||
<link.icon className="inline-icon" />
|
|
||||||
{link.label}
|
|
||||||
</link.component>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Menu>
|
|
||||||
<Fragment slot="button">
|
|
||||||
<Bars3Icon className="w-6" />
|
|
||||||
<span class="sr-only">Menu</span>
|
|
||||||
</Fragment>
|
|
||||||
{
|
|
||||||
links.map((link) => (
|
|
||||||
<link.component href={link.href}>
|
|
||||||
<MenuItem icon={link.icon} label={link.label} />
|
|
||||||
</link.component>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
<hr class="border-black/25" />
|
|
||||||
{
|
|
||||||
guides.map((guide) => (
|
|
||||||
<a href={`/guides/${guide.slug}`} rel="prefetch">
|
|
||||||
<MenuItem icon={DocumentTextIcon} label={guide.data.title} />
|
|
||||||
</a>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Menu>
|
|
||||||
</nav>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
export interface Props {
|
|
||||||
icon: (props: { class?: string; className?: string }) => unknown
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex w-full items-center gap-1 px-3 py-2 text-left font-medium opacity-50 transition hover:text-emerald-500 hover:opacity-100"
|
|
||||||
>
|
|
||||||
<Astro.props.icon class="inline-icon" className="inline-icon" />
|
|
||||||
<span class="flex-1">{Astro.props.label}</span>
|
|
||||||
</div>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<details class="relative md:hidden" data-menu>
|
|
||||||
<summary
|
|
||||||
class="-m-2 cursor-pointer list-none p-2 transition hover:text-emerald-500"
|
|
||||||
>
|
|
||||||
<slot name="button" />
|
|
||||||
</summary>
|
|
||||||
<div
|
|
||||||
class="absolute right-0 top-[calc(100%+8px)] z-10 max-h-[calc(100vh-5rem)] w-48 overflow-y-auto overflow-x-hidden rounded-lg bg-slate-800 shadow"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
for (const menu of document.querySelectorAll<HTMLDetailsElement>(
|
|
||||||
"[data-menu]",
|
|
||||||
)) {
|
|
||||||
window.addEventListener("click", (event) => {
|
|
||||||
if (!menu.contains(event.target as Node)) {
|
|
||||||
menu.open = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
menu.addEventListener("keydown", (event) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
menu.open = false
|
|
||||||
menu.querySelector("summary")!.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
export type Props = astroHTML.JSX.AnchorHTMLAttributes & {
|
|
||||||
href: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeTrailingSlash = (str: string) => str.replace(/\/$/, "")
|
|
||||||
|
|
||||||
const linkUrl = new URL(Astro.props.href, Astro.url)
|
|
||||||
|
|
||||||
const isActive =
|
|
||||||
removeTrailingSlash(Astro.url.pathname) ===
|
|
||||||
removeTrailingSlash(linkUrl.pathname)
|
|
||||||
---
|
|
||||||
|
|
||||||
<a {...Astro.props} data-active={isActive || undefined}>
|
|
||||||
<slot />
|
|
||||||
</a>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { defineCollection, z } from "astro:content"
|
|
||||||
|
|
||||||
export const collections = {
|
|
||||||
guides: defineCollection({
|
|
||||||
schema: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
title: Getting Started
|
|
||||||
description: Learn how to get started with Reacord.
|
|
||||||
slug: 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.
|
|
||||||
|
|
||||||
## 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 -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
|
|
||||||
```
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
client.on("ready", () => {
|
|
||||||
const channel = await client.channels.fetch("abc123deadbeef")
|
|
||||||
reacord.createChannelMessage(channel).render("Hello, world!")
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
The `.createChannelMessage()` 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", () => {
|
|
||||||
const instance = reacord.createChannelMessage(channel)
|
|
||||||
instance.render(<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.createChannelMessage(channel)
|
|
||||||
instance.render(<Hello subject="World" />)
|
|
||||||
instance.render(<Hello subject="Moon" />)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
You can specify various options for the message:
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
const instance = reacord.createChannelMessage(channel, {
|
|
||||||
tts: true,
|
|
||||||
reply: {
|
|
||||||
messageReference: someMessage.id,
|
|
||||||
},
|
|
||||||
flags: [MessageFlags.SuppressNotifications],
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [Discord.js docs](https://discord.js.org/#/docs/discord.js/main/typedef/MessageCreateOptions) for all of the available options.
|
|
||||||
|
|
||||||
## 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>
|
|
||||||
This section also applies to other kinds of application commands, such as context menu commands.
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
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
|
|
||||||
import { Client } from "discord.js"
|
|
||||||
import { Button, ReacordDiscordJs } from "reacord"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
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 createInteractionReply() function instead of createChannelMessage
|
|
||||||
reacord.createInteractionReply(interaction).render(<>pong!</>)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
client.login(process.env.DISCORD_TOKEN)
|
|
||||||
```
|
|
||||||
|
|
||||||
<aside>
|
|
||||||
This example uses <a href="https://discord.com/developers/docs/interactions/application-commands#registering-a-command">global commands</a>, so the command might take a while to show up 😅
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
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.createInteractionReply(interaction).render(<>pong!</>)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hi",
|
|
||||||
description: "say hi",
|
|
||||||
run: (interaction) => {
|
|
||||||
reacord.createInteractionReply(interaction).render(<>hi</>)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ephemeral Command Replies
|
|
||||||
|
|
||||||
Ephemeral replies are replies that only appear for one user. To create them, use the `.createInteractionReply()` function and provide `ephemeral` option.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
handleCommands(client, [
|
|
||||||
{
|
|
||||||
name: "pong",
|
|
||||||
description: "pong, but in secret",
|
|
||||||
run: (interaction) => {
|
|
||||||
reacord
|
|
||||||
.createInteractionReply(interaction, { ephemeral: true })
|
|
||||||
.render(<>(pong)</>)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
## Text-to-Speech Command Replies
|
|
||||||
|
|
||||||
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
|
|
||||||
handleCommands(client, [
|
|
||||||
{
|
|
||||||
name: "pong",
|
|
||||||
description: "pong, but converted into audio",
|
|
||||||
run: (interaction) => {
|
|
||||||
reacord
|
|
||||||
.createInteractionReply(interaction, { tts: true })
|
|
||||||
.render(<>pong!</>)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
---
|
|
||||||
title: Embeds
|
|
||||||
description: Using embed components
|
|
||||||
slug: embeds
|
|
||||||
---
|
|
||||||
|
|
||||||
# Embeds
|
|
||||||
|
|
||||||
Reacord comes with an `<Embed />` component for sending rich embeds.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { Embed } from "reacord"
|
|
||||||
|
|
||||||
function FancyMessage({ title, description }) {
|
|
||||||
return (
|
|
||||||
<Embed
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
color={0x00ff00}
|
|
||||||
timestamp={Date.now()}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
reacord
|
|
||||||
.createChannelMessage(channel)
|
|
||||||
.render(<FancyMessage title="Hello" description="World" />)
|
|
||||||
```
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<EmbedTitle>{title}</EmbedTitle>
|
|
||||||
{/* embed descriptions are just text */}
|
|
||||||
{description}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FancyMessage({ children }) {
|
|
||||||
return (
|
|
||||||
<Embed color={0x00ff00} timestamp={Date.now()}>
|
|
||||||
{children}
|
|
||||||
</Embed>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
reacord.createChannelMessage(channel).render(
|
|
||||||
<FancyMessage>
|
|
||||||
<FancyDetails title="Hello" description="World" />
|
|
||||||
</FancyMessage>,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [API Reference](/api/index.html#EmbedAuthorProps) for the full list of embed components.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
title: Buttons
|
|
||||||
description: Using button components
|
|
||||||
slug: buttons
|
|
||||||
---
|
|
||||||
|
|
||||||
# Buttons
|
|
||||||
|
|
||||||
Use the `<Button />` component to create a message with a button, and use the `onClick` callback to respond to button clicks.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { Button } from "reacord"
|
|
||||||
|
|
||||||
function Counter() {
|
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
You've clicked the button {count} times.
|
|
||||||
<Button label="+1" onClick={() => setCount(count + 1)} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
import { Button } from "reacord"
|
|
||||||
|
|
||||||
function TheButton() {
|
|
||||||
function handleClick(event) {
|
|
||||||
const name = event.guild.member.displayName || event.user.username
|
|
||||||
|
|
||||||
const publicReply = event.reply(`${name} clicked the button. wow`)
|
|
||||||
setTimeout(() => publicReply.destroy(), 3000)
|
|
||||||
|
|
||||||
const privateReply = event.reply("good job, you clicked it", {
|
|
||||||
ephemeral: true,
|
|
||||||
})
|
|
||||||
privateReply.deactivate() // we don't need to listen to updates on this
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Button label="click me i dare you" onClick={handleClick} />
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [API reference](/api/index.html#ButtonProps) for more information.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
title: Links
|
|
||||||
description: Using link components
|
|
||||||
slug: links
|
|
||||||
---
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { Link } from "reacord"
|
|
||||||
|
|
||||||
function AwesomeLinks() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Link label="look at this" url="https://google.com" />
|
|
||||||
<Link label="wow" url="https://youtube.com/watch?v=dQw4w9WgXcQ" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [API reference](/api/index.html#Link) for more information.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
---
|
|
||||||
title: Select Menus
|
|
||||||
description: Using select menu components
|
|
||||||
slug: select-menus
|
|
||||||
---
|
|
||||||
|
|
||||||
# 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`.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
export function FruitSelect({ onConfirm }) {
|
|
||||||
const [value, setValue] = useState()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Select
|
|
||||||
placeholder="choose a fruit"
|
|
||||||
value={value}
|
|
||||||
onChangeValue={setValue}
|
|
||||||
>
|
|
||||||
<Option value="🍎" />
|
|
||||||
<Option value="🍌" />
|
|
||||||
<Option value="🍒" />
|
|
||||||
</Select>
|
|
||||||
<Button
|
|
||||||
label="confirm"
|
|
||||||
disabled={value == undefined}
|
|
||||||
onClick={() => {
|
|
||||||
if (value) onConfirm(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
const instance = reacord.createChannelMessage(channel).render(
|
|
||||||
<FruitSelect
|
|
||||||
onConfirm={(value) => {
|
|
||||||
instance.render(`you chose ${value}`)
|
|
||||||
instance.deactivate()
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
For a multi-select, use the `multiple` prop, then you can use `values` and `onChangeMultiple` to handle multiple values.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
export function FruitSelect({ onConfirm }) {
|
|
||||||
const [values, setValues] = useState([])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
placeholder="choose a fruit"
|
|
||||||
values={values}
|
|
||||||
onChangeMultiple={setValues}
|
|
||||||
>
|
|
||||||
<Option value="🍎" />
|
|
||||||
<Option value="🍌" />
|
|
||||||
<Option value="🍒" />
|
|
||||||
</Select>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The Select component accepts a variety of props beyond what's shown here. See the [API reference](/api/index.html#SelectChangeEvent) for more information.
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
title: useInstance
|
|
||||||
description: Using useInstance to get the current instance within a component
|
|
||||||
slug: use-instance
|
|
||||||
---
|
|
||||||
|
|
||||||
# useInstance
|
|
||||||
|
|
||||||
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
|
|
||||||
import { Button, useInstance } from "reacord"
|
|
||||||
|
|
||||||
function SelfDestruct() {
|
|
||||||
const instance = useInstance()
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
style="danger"
|
|
||||||
label="delete this"
|
|
||||||
onClick={() => instance.destroy()}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
reacord.createChannelMessage(channel).render(<SelfDestruct />)
|
|
||||||
```
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
title: Using Reacord with other libraries
|
|
||||||
description: Adapting Reacord to another Discord library
|
|
||||||
slug: custom-adapters
|
|
||||||
---
|
|
||||||
|
|
||||||
# Using Reacord with other libraries
|
|
||||||
|
|
||||||
Reacord's core is built to be library agnostic, and can be adapted to libraries other than Discord.js. However, Discord.js is the only built-in adapter at the moment, and the adapter API is still a work in progress.
|
|
||||||
|
|
||||||
If you're interested in creating a custom adapter, [see the code for the Discord.js adapter as an example](https://github.com/itsMapleLeaf/reacord/blob/main/packages/reacord/library/core/reacord-discord-js.ts). Feel free to [create an issue on GitHub](https://github.com/itsMapleLeaf/reacord/issues/new) if you run into issues.
|
|
||||||
3
packages/website/src/env.d.ts
vendored
3
packages/website/src/env.d.ts
vendored
@@ -1,3 +0,0 @@
|
|||||||
/// <reference types="astro/client" />
|
|
||||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
|
||||||
/// <reference path="../.astro/types.d.ts" />
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
---
|
|
||||||
import type { GetStaticPaths } from "astro"
|
|
||||||
import { getCollection, type CollectionEntry } from "astro:content"
|
|
||||||
import AppFooter from "~/components/app-footer.astro"
|
|
||||||
import Layout from "~/components/layout.astro"
|
|
||||||
import MainNavigation from "~/components/main-navigation.astro"
|
|
||||||
import NavLink from "~/components/nav-link.astro"
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
guide: CollectionEntry<"guides">
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
|
||||||
const guides = await getCollection("guides")
|
|
||||||
return guides.map((guide) => ({
|
|
||||||
params: { slug: guide.slug },
|
|
||||||
props: { guide },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const guides = await getCollection("guides")
|
|
||||||
const { Content } = await Astro.props.guide.render()
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<div class="isolate">
|
|
||||||
<header
|
|
||||||
class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
|
|
||||||
>
|
|
||||||
<div class="container">
|
|
||||||
<MainNavigation />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="container mt-8 flex items-start gap-4">
|
|
||||||
<nav
|
|
||||||
class="sticky top-24 hidden h-[calc(100vh-theme(spacing.28))] w-48 flex-col gap-3 md:flex"
|
|
||||||
>
|
|
||||||
<h2 class="text-2xl">Guides</h2>
|
|
||||||
<ul class="flex flex-col items-start gap-2">
|
|
||||||
{
|
|
||||||
guides.map((guide) => (
|
|
||||||
<li>
|
|
||||||
<NavLink
|
|
||||||
class="link data-[active]:link-active"
|
|
||||||
href={`/guides/${guide.slug}`}
|
|
||||||
rel="prefetch"
|
|
||||||
>
|
|
||||||
{guide.data.title}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<AppFooter class="mt-auto" />
|
|
||||||
</nav>
|
|
||||||
<article class="-mt-8 min-w-0 max-w-none flex-1 pb-8">
|
|
||||||
<Content />
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
<AppFooter class="mx-auto mb-4 text-center md:hidden" />
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
article :global(:where(h1, h2, h3, h4, h5, h6)) {
|
|
||||||
@apply mb-3 mt-8 font-light;
|
|
||||||
}
|
|
||||||
article :global(h1) {
|
|
||||||
@apply text-3xl lg:text-4xl;
|
|
||||||
}
|
|
||||||
article :global(h2) {
|
|
||||||
@apply text-2xl;
|
|
||||||
}
|
|
||||||
article :global(h3) {
|
|
||||||
@apply text-xl;
|
|
||||||
}
|
|
||||||
article :global(p) {
|
|
||||||
@apply my-3;
|
|
||||||
}
|
|
||||||
article :global(a) {
|
|
||||||
@apply font-medium text-emerald-400 hover:no-underline;
|
|
||||||
}
|
|
||||||
article :global(strong) {
|
|
||||||
@apply font-medium text-emerald-400;
|
|
||||||
}
|
|
||||||
article :global(code) {
|
|
||||||
@apply rounded border border-slate-800 bg-slate-950 px-1 py-0.5 leading-none text-slate-300;
|
|
||||||
}
|
|
||||||
article :global(pre) {
|
|
||||||
@apply my-4 overflow-x-auto rounded-md border border-slate-800 !bg-slate-950 px-4 py-3 font-monospace;
|
|
||||||
}
|
|
||||||
article :global(pre code) {
|
|
||||||
@apply border-none bg-transparent p-0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
---
|
|
||||||
import dotsBackgroundUrl from "~/assets/dots-background.svg"
|
|
||||||
import AppFooter from "~/components/app-footer.astro"
|
|
||||||
import AppLogo from "~/components/app-logo.astro"
|
|
||||||
import { LandingAnimation } from "~/components/landing-animation"
|
|
||||||
import Layout from "~/components/layout.astro"
|
|
||||||
import MainNavigation from "~/components/main-navigation.astro"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 rotate-6 scale-125 opacity-20"
|
|
||||||
style={{ backgroundImage: `url(${dotsBackgroundUrl})` }}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="relative flex min-h-screen min-w-0 flex-col gap-4 pb-4">
|
|
||||||
<header class="container">
|
|
||||||
<MainNavigation />
|
|
||||||
</header>
|
|
||||||
<div class="my-auto flex flex-col gap-4 px-4">
|
|
||||||
<AppLogo class="mx-auto w-full max-w-lg" />
|
|
||||||
|
|
||||||
<div class="isolate mx-auto h-44 w-full max-w-md">
|
|
||||||
<LandingAnimation client:only />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="-mb-1 text-center text-lg font-light">
|
|
||||||
Create interactive Discord messages with React.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex gap-4 self-center">
|
|
||||||
<a href="/guides/getting-started" class="button button-solid">
|
|
||||||
Get Started
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- <UncontrolledModal
|
|
||||||
button={(button) => (
|
|
||||||
<button {...button} class={buttonClass({ variant: "semiblack" })}>
|
|
||||||
Show Code
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div class="text-sm sm:text-base">
|
|
||||||
<LandingCode />
|
|
||||||
</div>
|
|
||||||
</UncontrolledModal> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container text-center">
|
|
||||||
<AppFooter />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
:focus {
|
|
||||||
@apply outline-none;
|
|
||||||
}
|
|
||||||
:focus-visible {
|
|
||||||
@apply ring-2 ring-inset ring-emerald-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre,
|
|
||||||
code {
|
|
||||||
font-variant-ligatures: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
.container {
|
|
||||||
@apply mx-auto w-full max-w-screen-lg px-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-icon {
|
|
||||||
@apply inline w-5 align-sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
@apply relative inline-block font-medium opacity-60 transition-opacity hover:opacity-100;
|
|
||||||
}
|
|
||||||
.link::after {
|
|
||||||
@apply absolute bottom-[-2px] block h-px w-full translate-y-[3px] bg-current opacity-0 transition content-[''];
|
|
||||||
}
|
|
||||||
.link:hover::after {
|
|
||||||
@apply -translate-y-px opacity-50;
|
|
||||||
}
|
|
||||||
.link-active {
|
|
||||||
@apply text-emerald-500 opacity-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
@apply mt-4 inline-block rounded-lg bg-black/25 px-4 py-2.5 text-xl transition hover:-translate-y-0.5 hover:bg-black/40 hover:shadow active:translate-y-0 active:transition-none;
|
|
||||||
}
|
|
||||||
.button-solid {
|
|
||||||
@apply bg-emerald-700 hover:bg-emerald-800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-fade-in {
|
|
||||||
animation: fade-in 0.5s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { Config } from "tailwindcss"
|
|
||||||
import config from "../../tailwind.config.ts"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
...config,
|
|
||||||
content: ["./src/**/*.{ts,tsx,md,astro}"],
|
|
||||||
} satisfies Config
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.base",
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"jsxImportSource": "react",
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"~/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist", "public/api"]
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"entryPoints": ["../reacord/library/main.ts"],
|
|
||||||
"out": ["public/api"],
|
|
||||||
"tsconfig": "../reacord/tsconfig.json",
|
|
||||||
"excludeInternal": true,
|
|
||||||
"excludePrivate": true,
|
|
||||||
"excludeProtected": true,
|
|
||||||
"categorizeByGroup": false,
|
|
||||||
"preserveWatchOutput": true,
|
|
||||||
"githubPages": false,
|
|
||||||
"readme": "none",
|
|
||||||
"categoryOrder": [
|
|
||||||
"Core",
|
|
||||||
"Embed",
|
|
||||||
"Button",
|
|
||||||
"Link",
|
|
||||||
"Select",
|
|
||||||
"Action Row",
|
|
||||||
"Component Event",
|
|
||||||
"*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
9699
pnpm-lock.yaml
generated
9699
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
|||||||
packages:
|
ignoredBuiltDependencies:
|
||||||
- packages/*
|
- esbuild
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
export default {
|
|
||||||
theme: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: ["RubikVariable", "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: {},
|
|
||||||
},
|
|
||||||
corePlugins: {
|
|
||||||
container: false,
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@itsmapleleaf/configs/tsconfig"
|
"extends": "@itsmapleleaf/configs/tsconfig.bundler.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"github": {
|
|
||||||
"silent": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user