Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9594542869 |
5
.changeset/five-wolves-destroy.md
Normal file
5
.changeset/five-wolves-destroy.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"reacord": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
breaking: more descriptive component event types
|
||||||
33
.changeset/many-pets-melt.md
Normal file
33
.changeset/many-pets-melt.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
"reacord": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add new descriptive adapter methods
|
||||||
|
|
||||||
|
The reacord instance names have been updated, and the old names are now deprecated.
|
||||||
|
|
||||||
|
- `send` -> `createChannelMessage`
|
||||||
|
- `reply` -> `createInteractionReply`
|
||||||
|
|
||||||
|
These new methods also accept discord JS options. Usage example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// can accept either a channel object or a channel ID
|
||||||
|
reacord.createChannelMessage(channel)
|
||||||
|
reacord.createChannelMessage(channel, {
|
||||||
|
tts: true,
|
||||||
|
})
|
||||||
|
reacord.createChannelMessage(channel, {
|
||||||
|
reply: {
|
||||||
|
messageReference: "123456789012345678",
|
||||||
|
failIfNotExists: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
reacord.createInteractionReply(interaction)
|
||||||
|
reacord.createInteractionReply(interaction, {
|
||||||
|
ephemeral: true,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
These new methods replace the old ones, which are now deprecated.
|
||||||
30
README.md
30
README.md
@@ -1,30 +1,42 @@
|
|||||||
|
<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
|
||||||
# bun
|
# npm
|
||||||
bun add reacord react discord.js
|
npm install 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 { useState } from "react"
|
import * as React from "react"
|
||||||
import { Embed, Button } from "reacord"
|
import { Embed, Button } from "reacord"
|
||||||
|
|
||||||
function Counter() {
|
function Counter() {
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = React.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
|
<Button onClick={() => setCount(count + 1)}>
|
||||||
label="+1"
|
+1
|
||||||
onClick={() => setCount(count + 1)}
|
</Button>
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"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": "bun run --cwd packages/helpers typecheck && bun run --cwd packages/reacord typecheck",
|
"lint:types": "tsc -b & pnpm -r --parallel run typecheck",
|
||||||
|
"astro-sync": "pnpm --filter website exec astro sync",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "bun run --cwd packages/reacord build",
|
"build": "pnpm -r run build",
|
||||||
"release": "bun run build && changeset publish"
|
"build:website": "pnpm --filter website... run build",
|
||||||
|
"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",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -31,7 +33,8 @@
|
|||||||
],
|
],
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,40 +1,5 @@
|
|||||||
# reacord
|
# reacord
|
||||||
|
|
||||||
## 0.6.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- 11153df: breaking: more descriptive component event types
|
|
||||||
- fb0a997: add new descriptive adapter methods
|
|
||||||
|
|
||||||
The reacord instance names have been updated, and the old names are now deprecated.
|
|
||||||
|
|
||||||
- `send` -> `createChannelMessage`
|
|
||||||
- `reply` -> `createInteractionReply`
|
|
||||||
|
|
||||||
These new methods also accept discord JS options. Usage example:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// can accept either a channel object or a channel ID
|
|
||||||
reacord.createChannelMessage(channel)
|
|
||||||
reacord.createChannelMessage(channel, {
|
|
||||||
tts: true,
|
|
||||||
})
|
|
||||||
reacord.createChannelMessage(channel, {
|
|
||||||
reply: {
|
|
||||||
messageReference: "123456789012345678",
|
|
||||||
failIfNotExists: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
reacord.createInteractionReply(interaction)
|
|
||||||
reacord.createInteractionReply(interaction, {
|
|
||||||
ephemeral: true,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
These new methods replace the old ones, which are now deprecated.
|
|
||||||
|
|
||||||
## 0.5.5
|
## 0.5.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -13,13 +13,6 @@ 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
|
||||||
@@ -32,20 +25,6 @@ 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,13 +1,10 @@
|
|||||||
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.
|
||||||
@@ -21,27 +18,3 @@ 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.isAnySelectMenu()) {
|
if (interaction.isButton() || interaction.isStringSelectMenu()) {
|
||||||
this.handleComponentInteraction(
|
this.handleComponentInteraction(
|
||||||
this.createReacordComponentInteraction(interaction),
|
this.createReacordComponentInteraction(interaction),
|
||||||
)
|
)
|
||||||
@@ -154,9 +154,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
raise(`Channel ${channel.id} must be a text channel`)
|
raise(`Channel ${channel.id} must be a text channel`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const textChannel = channel as Discord.TextBasedChannel &
|
const message = await channel.send({
|
||||||
Discord.PartialTextBasedChannelFields
|
|
||||||
const message = await textChannel.send({
|
|
||||||
...getDiscordMessageOptions(messageOptions),
|
...getDiscordMessageOptions(messageOptions),
|
||||||
...messageCreateOptions,
|
...messageCreateOptions,
|
||||||
})
|
})
|
||||||
@@ -214,16 +212,41 @@ 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, ["nick", "avatarUrl", "pending"]),
|
pick(interaction.member, [
|
||||||
|
"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(),
|
||||||
@@ -237,17 +260,15 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
|
|
||||||
const guild: ComponentEventGuild | undefined = interaction.guild
|
const guild: ComponentEventGuild | undefined = interaction.guild
|
||||||
? {
|
? {
|
||||||
id: interaction.guild.id,
|
...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
|
||||||
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 = {
|
||||||
id: interaction.user.id,
|
...pruneNullishValues(
|
||||||
username: interaction.user.username,
|
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
||||||
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,
|
||||||
}
|
}
|
||||||
@@ -310,7 +331,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.isAnySelectMenu()) {
|
if (interaction.isStringSelectMenu()) {
|
||||||
return {
|
return {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
type: "select",
|
type: "select",
|
||||||
@@ -327,7 +348,6 @@ 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))
|
||||||
},
|
},
|
||||||
@@ -337,28 +357,6 @@ 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,
|
||||||
@@ -405,60 +403,16 @@ 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") {
|
||||||
const {
|
|
||||||
menuType,
|
|
||||||
values,
|
|
||||||
options: selectOptions,
|
|
||||||
channelTypes,
|
|
||||||
multiple,
|
|
||||||
...rest
|
|
||||||
} = component
|
|
||||||
|
|
||||||
if (menuType === "string" || menuType == undefined) {
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...component,
|
||||||
type: Discord.ComponentType.StringSelect,
|
type: Discord.ComponentType.SelectMenu,
|
||||||
options: selectOptions.map((option) => ({
|
options: component.options.map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
default: values?.includes(option.value),
|
default: component.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
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid component type ${safeJsonStringify(component)}}`,
|
`Invalid component type ${safeJsonStringify(component)}}`,
|
||||||
|
|||||||
@@ -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, MessageProvider } from "./instance-context.js"
|
import { InstanceProvider } from "./instance-context.js"
|
||||||
import type { ReacordInstance } from "./instance.js"
|
import type { ReacordInstance } from "./instance.js"
|
||||||
|
|
||||||
/** @category Core */
|
/** @category Core */
|
||||||
@@ -54,11 +54,7 @@ export abstract class Reacord {
|
|||||||
const instance: ReacordInstance = {
|
const instance: ReacordInstance = {
|
||||||
render: (content: ReactNode) => {
|
render: (content: ReactNode) => {
|
||||||
reconciler.updateContainer(
|
reconciler.updateContainer(
|
||||||
<InstanceProvider value={instance}>
|
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
||||||
<MessageProvider value={renderer.messageStore}>
|
|
||||||
{content}
|
|
||||||
</MessageProvider>
|
|
||||||
</InstanceProvider>,
|
|
||||||
container,
|
container,
|
||||||
)
|
)
|
||||||
return instance
|
return instance
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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,4 +1,3 @@
|
|||||||
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"
|
||||||
@@ -48,7 +47,6 @@ 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,
|
||||||
null, // HostContext,
|
never, // HostContext,
|
||||||
true, // UpdatePayload,
|
true, // UpdatePayload,
|
||||||
never, // ChildSet,
|
never, // ChildSet,
|
||||||
number, // TimeoutHandle,
|
number, // TimeoutHandle,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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"
|
||||||
@@ -13,7 +12,6 @@ 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
|
||||||
@@ -77,7 +75,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -116,6 +113,5 @@ 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, useMessage } from "./core/instance-context"
|
export { useInstance } from "./core/instance-context"
|
||||||
export * from "./core/reacord"
|
export * from "./core/reacord"
|
||||||
export * from "./core/reacord-discord-js"
|
export * from "./core/reacord-discord-js"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "reacord",
|
"name": "reacord",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Create interactive Discord messages using React.",
|
"description": "Create interactive Discord messages using React.",
|
||||||
"version": "0.6.0",
|
"version": "0.5.5",
|
||||||
"homepage": "https://reacord.mapleleaf.dev",
|
"homepage": "https://reacord.mapleleaf.dev",
|
||||||
"repository": "https://github.com/itsMapleLeaf/reacord.git",
|
"repository": "https://github.com/itsMapleLeaf/reacord.git",
|
||||||
"changelog": "https://github.com/itsMapleLeaf/reacord/releases",
|
"changelog": "https://github.com/itsMapleLeaf/reacord/releases",
|
||||||
@@ -36,21 +36,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"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": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node18 --format cjs,esm --sourcemap --dts --dts-resolve",
|
||||||
"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",
|
"build-watch": "pnpm build -- --watch",
|
||||||
"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.25.1",
|
"discord.js": "^14",
|
||||||
"react": ">=17"
|
"react": ">=17"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
@@ -62,13 +63,15 @@
|
|||||||
"@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",
|
||||||
"discord.js": "^14.25.1",
|
"cpy-cli": "^5.0.0",
|
||||||
|
"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,8 +1,6 @@
|
|||||||
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,
|
||||||
@@ -13,6 +11,7 @@ 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 })
|
||||||
@@ -54,57 +53,9 @@ 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) => {
|
||||||
function Counter() {
|
const Counter = () => {
|
||||||
const [count, setCount] = useState(0)
|
const [count, setCount] = React.useState(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
count: {count}
|
count: {count}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
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(() => {
|
||||||
const cwd = dirname(dirname(fileURLToPath(import.meta.url)))
|
spawnSync("pnpm", ["run", "build"])
|
||||||
spawnSync("bun", ["run", "build"], { cwd })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can require commonjs", () => {
|
test("can require commonjs", () => {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": false,
|
|
||||||
"declaration": true,
|
|
||||||
"emitDeclarationOnly": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"outDir": "dist"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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
Normal file
11
packages/website/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules
|
||||||
|
/.cache
|
||||||
|
/build
|
||||||
|
/public/build
|
||||||
|
.env
|
||||||
|
/public/api
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
||||||
|
*.out.css
|
||||||
|
/api
|
||||||
|
.astro
|
||||||
48
packages/website/CHANGELOG.md
Normal file
48
packages/website/CHANGELOG.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# website
|
||||||
|
|
||||||
|
## 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
|
||||||
20
packages/website/astro.config.mjs
Normal file
20
packages/website/astro.config.mjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// 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: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
41
packages/website/package.json
Normal file
41
packages/website/package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"name": "website",
|
||||||
|
"version": "0.4.6",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
packages/website/src/assets/banner.png
Normal file
BIN
packages/website/src/assets/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
BIN
packages/website/src/assets/blob-comfy.png
Normal file
BIN
packages/website/src/assets/blob-comfy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
packages/website/src/assets/cursor-ibeam.png
Normal file
BIN
packages/website/src/assets/cursor-ibeam.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
packages/website/src/assets/cursor.png
Normal file
BIN
packages/website/src/assets/cursor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
3
packages/website/src/assets/dots-background.svg
Normal file
3
packages/website/src/assets/dots-background.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 146 B |
BIN
packages/website/src/assets/favicon.png
Normal file
BIN
packages/website/src/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 658 B |
22
packages/website/src/components/app-footer.astro
Normal file
22
packages/website/src/components/app-footer.astro
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
21
packages/website/src/components/app-logo.astro
Normal file
21
packages/website/src/components/app-logo.astro
Normal file
File diff suppressed because one or more lines are too long
7
packages/website/src/components/external-link.astro
Normal file
7
packages/website/src/components/external-link.astro
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
export type Props = astroHTML.JSX.AnchorHTMLAttributes
|
||||||
|
---
|
||||||
|
|
||||||
|
<a rel="noopener noreferrer" target="_blank" {...Astro.props}>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
38
packages/website/src/components/guide-layout.astro
Normal file
38
packages/website/src/components/guide-layout.astro
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content"
|
||||||
|
import Layout from "./layout.astro"
|
||||||
|
import MainNavigation from "./main-navigation.astro"
|
||||||
|
|
||||||
|
const guides = await getCollection("guides")
|
||||||
|
---
|
||||||
|
|
||||||
|
<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>
|
||||||
201
packages/website/src/components/landing-animation.tsx
Normal file
201
packages/website/src/components/landing-animation.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
packages/website/src/components/layout.astro
Normal file
43
packages/website/src/components/layout.astro
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
81
packages/website/src/components/main-navigation.astro
Normal file
81
packages/website/src/components/main-navigation.astro
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
13
packages/website/src/components/menu-item.astro
Normal file
13
packages/website/src/components/menu-item.astro
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
30
packages/website/src/components/menu.astro
Normal file
30
packages/website/src/components/menu.astro
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<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>
|
||||||
17
packages/website/src/components/nav-link.astro
Normal file
17
packages/website/src/components/nav-link.astro
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
10
packages/website/src/content/config.ts
Normal file
10
packages/website/src/content/config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineCollection, z } from "astro:content"
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
guides: defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
59
packages/website/src/content/guides/0-getting-started.md
Normal file
59
packages/website/src/content/guides/0-getting-started.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
```
|
||||||
198
packages/website/src/content/guides/1-sending-messages.md
Normal file
198
packages/website/src/content/guides/1-sending-messages.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
---
|
||||||
|
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!</>)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
64
packages/website/src/content/guides/2-embeds.md
Normal file
64
packages/website/src/content/guides/2-embeds.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
48
packages/website/src/content/guides/3-buttons.md
Normal file
48
packages/website/src/content/guides/3-buttons.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
24
packages/website/src/content/guides/4-links.md
Normal file
24
packages/website/src/content/guides/4-links.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
69
packages/website/src/content/guides/5-select-menu.md
Normal file
69
packages/website/src/content/guides/5-select-menu.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
26
packages/website/src/content/guides/6-use-instance.md
Normal file
26
packages/website/src/content/guides/6-use-instance.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
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 />)
|
||||||
|
```
|
||||||
11
packages/website/src/content/guides/custom-adapters.md
Normal file
11
packages/website/src/content/guides/custom-adapters.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
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
Normal file
3
packages/website/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="astro/client" />
|
||||||
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
95
packages/website/src/pages/guides/[slug].astro
Normal file
95
packages/website/src/pages/guides/[slug].astro
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
54
packages/website/src/pages/index.astro
Normal file
54
packages/website/src/pages/index.astro
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
59
packages/website/src/styles/tailwind.css
Normal file
59
packages/website/src/styles/tailwind.css
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/website/tailwind.config.ts
Normal file
7
packages/website/tailwind.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Config } from "tailwindcss"
|
||||||
|
import config from "../../tailwind.config.ts"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...config,
|
||||||
|
content: ["./src/**/*.{ts,tsx,md,astro}"],
|
||||||
|
} satisfies Config
|
||||||
12
packages/website/tsconfig.json
Normal file
12
packages/website/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "public/api"]
|
||||||
|
}
|
||||||
22
packages/website/typedoc.json
Normal file
22
packages/website/typedoc.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
9777
pnpm-lock.yaml
generated
9777
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
|||||||
ignoredBuiltDependencies:
|
packages:
|
||||||
- esbuild
|
- packages/*
|
||||||
|
|||||||
16
tailwind.config.ts
Normal file
16
tailwind.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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.bundler.json"
|
"extends": "@itsmapleleaf/configs/tsconfig"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user