Compare commits
34 Commits
reacord@0.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 061608323f | |||
| d1611d8f64 | |||
|
|
b641885112 | ||
|
|
2a8ee7885d | ||
|
|
6c71073d10 | ||
|
|
5674e3c1b6 | ||
|
|
a41c825cdd | ||
|
|
a00fbc0631 | ||
|
|
a713f17a5c | ||
|
|
44795cd7cc | ||
|
|
17978a5252 | ||
|
|
95fb342183 | ||
|
|
0772ca4502 | ||
|
|
11153dfe0f | ||
|
|
fb0a997855 | ||
|
|
da1c62f2f0 | ||
|
|
cdc22b7916 | ||
|
|
7fee69c8ae | ||
|
|
c2e5dc04dd | ||
|
|
390da4cab6 | ||
|
|
def0c46f13 | ||
|
|
8b6e283810 | ||
|
|
13fcf7ddc9 | ||
|
|
ce12351a24 | ||
|
|
73bb098774 | ||
|
|
4ee4d4ab91 | ||
|
|
f998a0e09a | ||
|
|
453192cc96 | ||
|
|
d387f669ab | ||
|
|
9aec87ae9f | ||
|
|
65d1d68bb0 | ||
|
|
dfb7562c97 | ||
|
|
9e2be6c2e0 | ||
|
|
d078995516 |
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)
|
||||
|
||||
```console
|
||||
# npm
|
||||
npm install reacord react discord.js
|
||||
|
||||
# yarn
|
||||
yarn add reacord react discord.js
|
||||
|
||||
# pnpm
|
||||
pnpm add reacord react discord.js
|
||||
# bun
|
||||
bun add reacord react discord.js
|
||||
|
||||
```
|
||||
|
||||
## Get Started
|
||||
|
||||
[Visit the docs to get started.](https://reacord.mapleleaf.dev/guides/getting-started)
|
||||
|
||||
## Example
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```tsx
|
||||
import * as React from "react"
|
||||
import { useState } from "react"
|
||||
import { Embed, Button } from "reacord"
|
||||
|
||||
function Counter() {
|
||||
const [count, setCount] = React.useState(0)
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Embed title="Counter">
|
||||
This button has been clicked {count} times.
|
||||
</Embed>
|
||||
<Button onClick={() => setCount(count + 1)}>
|
||||
+1
|
||||
</Button>
|
||||
<Button
|
||||
label="+1"
|
||||
onClick={() => setCount(count + 1)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
22
package.json
22
package.json
@@ -1,21 +1,17 @@
|
||||
{
|
||||
"name": "reacord-monorepo",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "run-s --continue-on-error lint:*",
|
||||
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
|
||||
"lint:prettier": "prettier . --write --cache --list-different",
|
||||
"lint:types": "tsc -b & pnpm -r --parallel run typecheck",
|
||||
"astro-sync": "pnpm --filter website exec astro sync",
|
||||
"format": "run-s --continue-on-error format:*",
|
||||
"format:eslint": "eslint . --report-unused-disable-directives --fix",
|
||||
"format:prettier": "prettier --cache --write . \"**/*.astro\"",
|
||||
"lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different",
|
||||
"lint:types": "bun run --cwd packages/helpers typecheck && bun run --cwd packages/reacord typecheck",
|
||||
"test": "vitest",
|
||||
"build": "pnpm -r run build",
|
||||
"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"
|
||||
"build": "bun run --cwd packages/reacord build",
|
||||
"release": "bun run build && changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.2",
|
||||
@@ -24,7 +20,6 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.0.3",
|
||||
"react": "^18.2.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
@@ -36,8 +31,7 @@
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"packages/website/public/api"
|
||||
"dist"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# 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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -9,41 +9,52 @@ export interface ComponentEvent {
|
||||
*
|
||||
* @see https://discord.com/developers/docs/resources/channel#message-object
|
||||
*/
|
||||
message: MessageInfo
|
||||
message: ComponentEventMessage
|
||||
|
||||
/**
|
||||
* The channel that this event occurred in.
|
||||
*
|
||||
* @see https://discord.com/developers/docs/resources/channel#channel-object
|
||||
*/
|
||||
channel: ChannelInfo
|
||||
channel: ComponentEventChannel
|
||||
|
||||
/**
|
||||
* The user that triggered this event.
|
||||
*
|
||||
* @see https://discord.com/developers/docs/resources/user#user-object
|
||||
*/
|
||||
user: UserInfo
|
||||
user: ComponentEventUser
|
||||
|
||||
/**
|
||||
* The guild that this event occurred in.
|
||||
*
|
||||
* @see https://discord.com/developers/docs/resources/guild#guild-object
|
||||
*/
|
||||
guild?: GuildInfo
|
||||
guild?: ComponentEventGuild
|
||||
|
||||
/** Create a new reply to this event. */
|
||||
reply(content?: ReactNode): ReacordInstance
|
||||
reply(
|
||||
content?: ReactNode,
|
||||
options?: ComponentEventReplyOptions,
|
||||
): ReacordInstance
|
||||
|
||||
/**
|
||||
* Create an ephemeral reply to this event, shown only to the user who
|
||||
* triggered it.
|
||||
*
|
||||
* @deprecated Use event.reply(content, { ephemeral: true })
|
||||
*/
|
||||
ephemeralReply(content?: ReactNode): ReacordInstance
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface ChannelInfo {
|
||||
export interface ComponentEventReplyOptions {
|
||||
ephemeral?: boolean
|
||||
tts?: boolean
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface ComponentEventChannel {
|
||||
id: string
|
||||
name?: string
|
||||
topic?: string
|
||||
@@ -55,11 +66,11 @@ export interface ChannelInfo {
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface MessageInfo {
|
||||
export interface ComponentEventMessage {
|
||||
id: string
|
||||
channelId: string
|
||||
authorId: string
|
||||
member?: GuildMemberInfo
|
||||
member?: ComponentEventGuildMember
|
||||
content: string
|
||||
timestamp: string
|
||||
editedTimestamp?: string
|
||||
@@ -70,14 +81,14 @@ export interface MessageInfo {
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface GuildInfo {
|
||||
export interface ComponentEventGuild {
|
||||
id: string
|
||||
name: string
|
||||
member: GuildMemberInfo
|
||||
member: ComponentEventGuildMember
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface GuildMemberInfo {
|
||||
export interface ComponentEventGuildMember {
|
||||
id: string
|
||||
nick?: string
|
||||
displayName: string
|
||||
@@ -92,7 +103,7 @@ export interface GuildMemberInfo {
|
||||
}
|
||||
|
||||
/** @category Component Event */
|
||||
export interface UserInfo {
|
||||
export interface ComponentEventUser {
|
||||
id: string
|
||||
username: string
|
||||
discriminator: string
|
||||
|
||||
@@ -13,6 +13,13 @@ import type { ComponentEvent } from "../component-event"
|
||||
import { OptionNode } from "./option-node"
|
||||
import { omit } from "@reacord/helpers/omit.js"
|
||||
|
||||
export type SelectMenuType =
|
||||
| "string"
|
||||
| "user"
|
||||
| "role"
|
||||
| "mentionable"
|
||||
| "channel"
|
||||
|
||||
/** @category Select */
|
||||
export interface SelectProps {
|
||||
children?: ReactNode
|
||||
@@ -25,6 +32,20 @@ export interface SelectProps {
|
||||
/** The text shown when no value is selected */
|
||||
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 */
|
||||
multiple?: boolean
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { ReacordInstance } from "./instance.js"
|
||||
import { raise } from "@reacord/helpers/raise.js"
|
||||
import * as React from "react"
|
||||
import type { MessageStore } from "../internal/message-store.js"
|
||||
|
||||
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
||||
const MessageContext = React.createContext<MessageStore | undefined>(undefined)
|
||||
|
||||
export const InstanceProvider = Context.Provider
|
||||
export const MessageProvider = MessageContext.Provider
|
||||
|
||||
/**
|
||||
* 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?")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { ReactNode } from "react"
|
||||
*/
|
||||
export interface ReacordInstance {
|
||||
/** Render some JSX to this instance (edits the message) */
|
||||
render: (content: ReactNode) => void
|
||||
render: (content: ReactNode) => ReacordInstance
|
||||
|
||||
/** Remove this message */
|
||||
destroy: () => void
|
||||
|
||||
@@ -14,11 +14,12 @@ import type {
|
||||
import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer"
|
||||
import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer"
|
||||
import type {
|
||||
ChannelInfo,
|
||||
GuildInfo,
|
||||
GuildMemberInfo,
|
||||
MessageInfo,
|
||||
UserInfo,
|
||||
ComponentEventChannel,
|
||||
ComponentEventGuild,
|
||||
ComponentEventGuildMember,
|
||||
ComponentEventMessage,
|
||||
ComponentEventReplyOptions,
|
||||
ComponentEventUser,
|
||||
} from "./component-event"
|
||||
import type { ReacordInstance } from "./instance"
|
||||
import type { ReacordConfig } from "./reacord"
|
||||
@@ -37,7 +38,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
super(config)
|
||||
|
||||
client.on("interactionCreate", (interaction) => {
|
||||
if (interaction.isButton() || interaction.isStringSelectMenu()) {
|
||||
if (interaction.isButton() || interaction.isAnySelectMenu()) {
|
||||
this.handleComponentInteraction(
|
||||
this.createReacordComponentInteraction(interaction),
|
||||
)
|
||||
@@ -48,14 +49,49 @@ export class ReacordDiscordJs extends Reacord {
|
||||
/**
|
||||
* Sends a message to a channel.
|
||||
*
|
||||
* @param target Discord channel object.
|
||||
* @param [options] Options for the channel message
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
* @see {@link Discord.MessageCreateOptions}
|
||||
*/
|
||||
public createChannelMessage(
|
||||
target: Discord.ChannelResolvable,
|
||||
options: Discord.MessageCreateOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createChannelMessageRenderer(target, options),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replies to a command interaction by sending a message.
|
||||
*
|
||||
* @param interaction Discord command interaction object.
|
||||
* @param [options] Custom options for the interaction reply method.
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
* @see {@link Discord.InteractionReplyOptions}
|
||||
*/
|
||||
public createInteractionReply(
|
||||
interaction: Discord.CommandInteraction,
|
||||
options: Discord.InteractionReplyOptions = {},
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction, options),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to a channel.
|
||||
*
|
||||
* @deprecated Use reacord.createChannelMessage() instead.
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
override send(
|
||||
channelId: string,
|
||||
public send(
|
||||
channel: Discord.ChannelResolvable,
|
||||
initialContent?: React.ReactNode,
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createChannelRenderer(channelId),
|
||||
this.createChannelMessageRenderer(channel, {}),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
@@ -63,14 +99,15 @@ export class ReacordDiscordJs extends Reacord {
|
||||
/**
|
||||
* Sends a message as a reply to a command interaction.
|
||||
*
|
||||
* @deprecated Use reacord.createInteractionReply() instead.
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
override reply(
|
||||
public reply(
|
||||
interaction: Discord.CommandInteraction,
|
||||
initialContent?: React.ReactNode,
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, {}),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
@@ -78,31 +115,51 @@ export class ReacordDiscordJs extends Reacord {
|
||||
/**
|
||||
* Sends an ephemeral message as a reply to a command interaction.
|
||||
*
|
||||
* @deprecated Use reacord.createInteractionReply(interaction, { ephemeral:
|
||||
* true })
|
||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||
*/
|
||||
override ephemeralReply(
|
||||
public ephemeralReply(
|
||||
interaction: Discord.CommandInteraction,
|
||||
initialContent?: React.ReactNode,
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, {
|
||||
ephemeral: true,
|
||||
}),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
|
||||
private createChannelRenderer(channelId: string) {
|
||||
private createChannelMessageRenderer(
|
||||
channelResolvable: Discord.ChannelResolvable,
|
||||
messageCreateOptions?: Discord.MessageCreateOptions,
|
||||
) {
|
||||
return new ChannelMessageRenderer({
|
||||
send: async (options) => {
|
||||
const channel =
|
||||
this.client.channels.cache.get(channelId) ??
|
||||
(await this.client.channels.fetch(channelId)) ??
|
||||
raise(`Channel ${channelId} not found`)
|
||||
|
||||
if (!channel.isTextBased()) {
|
||||
raise(`Channel ${channelId} is not a text channel`)
|
||||
send: async (messageOptions) => {
|
||||
let channel = this.client.channels.resolve(channelResolvable)
|
||||
if (!channel && typeof channelResolvable === "string") {
|
||||
channel = await this.client.channels.fetch(channelResolvable)
|
||||
}
|
||||
|
||||
const message = await channel.send(getDiscordMessageOptions(options))
|
||||
if (!channel) {
|
||||
const id =
|
||||
typeof channelResolvable === "string"
|
||||
? channelResolvable
|
||||
: channelResolvable.id
|
||||
raise(`Channel ${id} not found`)
|
||||
}
|
||||
|
||||
if (!channel.isTextBased()) {
|
||||
raise(`Channel ${channel.id} must be a text channel`)
|
||||
}
|
||||
|
||||
const textChannel = channel as Discord.TextBasedChannel &
|
||||
Discord.PartialTextBasedChannelFields
|
||||
const message = await textChannel.send({
|
||||
...getDiscordMessageOptions(messageOptions),
|
||||
...messageCreateOptions,
|
||||
})
|
||||
return createReacordMessage(message)
|
||||
},
|
||||
})
|
||||
@@ -112,20 +169,22 @@ export class ReacordDiscordJs extends Reacord {
|
||||
interaction:
|
||||
| Discord.CommandInteraction
|
||||
| Discord.MessageComponentInteraction,
|
||||
interactionReplyOptions: Discord.InteractionReplyOptions,
|
||||
) {
|
||||
return new InteractionReplyRenderer({
|
||||
type: "command",
|
||||
id: interaction.id,
|
||||
reply: async (options) => {
|
||||
interactionId: interaction.id,
|
||||
reply: async (messageOptions) => {
|
||||
const message = await interaction.reply({
|
||||
...getDiscordMessageOptions(options),
|
||||
...getDiscordMessageOptions(messageOptions),
|
||||
...interactionReplyOptions,
|
||||
fetchReply: true,
|
||||
})
|
||||
return createReacordMessage(message)
|
||||
},
|
||||
followUp: async (options) => {
|
||||
followUp: async (messageOptions) => {
|
||||
const message = await interaction.followUp({
|
||||
...getDiscordMessageOptions(options),
|
||||
...getDiscordMessageOptions(messageOptions),
|
||||
...interactionReplyOptions,
|
||||
fetchReply: true,
|
||||
})
|
||||
return createReacordMessage(message)
|
||||
@@ -133,36 +192,11 @@ export class ReacordDiscordJs extends Reacord {
|
||||
})
|
||||
}
|
||||
|
||||
private createEphemeralInteractionReplyRenderer(
|
||||
interaction:
|
||||
| Discord.CommandInteraction
|
||||
| Discord.MessageComponentInteraction,
|
||||
) {
|
||||
return new InteractionReplyRenderer({
|
||||
type: "command",
|
||||
id: interaction.id,
|
||||
reply: async (options) => {
|
||||
await interaction.reply({
|
||||
...getDiscordMessageOptions(options),
|
||||
ephemeral: true,
|
||||
})
|
||||
return createEphemeralReacordMessage()
|
||||
},
|
||||
followUp: async (options) => {
|
||||
await interaction.followUp({
|
||||
...getDiscordMessageOptions(options),
|
||||
ephemeral: true,
|
||||
})
|
||||
return createEphemeralReacordMessage()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private createReacordComponentInteraction(
|
||||
interaction: Discord.MessageComponentInteraction,
|
||||
): ComponentInteraction {
|
||||
// todo please dear god clean this up
|
||||
const channel: ChannelInfo = interaction.channel
|
||||
const channel: ComponentEventChannel = interaction.channel
|
||||
? {
|
||||
...pruneNullishValues(
|
||||
pick(interaction.channel, [
|
||||
@@ -178,43 +212,18 @@ export class ReacordDiscordJs extends Reacord {
|
||||
}
|
||||
: raise("Non-channel interactions are not supported")
|
||||
|
||||
const message: MessageInfo =
|
||||
const message: ComponentEventMessage =
|
||||
interaction.message instanceof Discord.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,
|
||||
}
|
||||
? createComponentEventMessage(interaction.message)
|
||||
: raise("Message not found")
|
||||
|
||||
const member: GuildMemberInfo | undefined =
|
||||
const member: ComponentEventGuildMember | undefined =
|
||||
interaction.member instanceof Discord.GuildMember
|
||||
? {
|
||||
...pruneNullishValues(
|
||||
pick(interaction.member, [
|
||||
"id",
|
||||
"nick",
|
||||
"displayName",
|
||||
"avatarUrl",
|
||||
"displayAvatarUrl",
|
||||
"color",
|
||||
"pending",
|
||||
]),
|
||||
pick(interaction.member, ["nick", "avatarUrl", "pending"]),
|
||||
),
|
||||
id: interaction.member.id,
|
||||
displayName: interaction.member.displayName,
|
||||
roles: interaction.member.roles.cache.map((role) => role.id),
|
||||
joinedAt: interaction.member.joinedAt?.toISOString(),
|
||||
@@ -226,17 +235,19 @@ export class ReacordDiscordJs extends Reacord {
|
||||
}
|
||||
: undefined
|
||||
|
||||
const guild: GuildInfo | 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"),
|
||||
}
|
||||
: undefined
|
||||
|
||||
const user: UserInfo = {
|
||||
...pruneNullishValues(
|
||||
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
||||
),
|
||||
const user: ComponentEventUser = {
|
||||
id: interaction.user.id,
|
||||
username: interaction.user.username,
|
||||
discriminator: interaction.user.discriminator,
|
||||
tag: interaction.user.tag,
|
||||
avatarUrl: interaction.user.avatarURL(),
|
||||
accentColor: interaction.user.accentColor ?? undefined,
|
||||
}
|
||||
@@ -275,15 +286,18 @@ export class ReacordDiscordJs extends Reacord {
|
||||
user,
|
||||
guild,
|
||||
|
||||
reply: (content?: ReactNode) =>
|
||||
reply: (content?: ReactNode, options?: ComponentEventReplyOptions) =>
|
||||
this.createInstance(
|
||||
this.createInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, options ?? {}),
|
||||
content,
|
||||
),
|
||||
|
||||
/** @deprecated Use event.reply(content, { ephemeral: true }) */
|
||||
ephemeralReply: (content: ReactNode) =>
|
||||
this.createInstance(
|
||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
||||
this.createInteractionReplyRenderer(interaction, {
|
||||
ephemeral: true,
|
||||
}),
|
||||
content,
|
||||
),
|
||||
},
|
||||
@@ -296,7 +310,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.isStringSelectMenu()) {
|
||||
if (interaction.isAnySelectMenu()) {
|
||||
return {
|
||||
...baseProps,
|
||||
type: "select",
|
||||
@@ -313,6 +327,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
|
||||
function createReacordMessage(message: Discord.Message): Message {
|
||||
return {
|
||||
data: createComponentEventMessage(message),
|
||||
edit: async (options) => {
|
||||
await message.edit(getDiscordMessageOptions(options))
|
||||
},
|
||||
@@ -322,16 +337,25 @@ function createReacordMessage(message: Discord.Message): Message {
|
||||
}
|
||||
}
|
||||
|
||||
function createEphemeralReacordMessage(): Message {
|
||||
function createComponentEventMessage(
|
||||
message: Discord.Message,
|
||||
): ComponentEventMessage {
|
||||
return {
|
||||
edit: () => {
|
||||
console.warn("Ephemeral messages can't be edited")
|
||||
return Promise.resolve()
|
||||
},
|
||||
delete: () => {
|
||||
console.warn("Ephemeral messages can't be deleted")
|
||||
return Promise.resolve()
|
||||
},
|
||||
...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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,14 +405,58 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
|
||||
// future proofing
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (component.type === "select") {
|
||||
return {
|
||||
...component,
|
||||
type: Discord.ComponentType.SelectMenu,
|
||||
options: component.options.map((option) => ({
|
||||
...option,
|
||||
default: component.values?.includes(option.value),
|
||||
})),
|
||||
const {
|
||||
menuType,
|
||||
values,
|
||||
options: selectOptions,
|
||||
channelTypes,
|
||||
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
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
|
||||
import type { ComponentInteraction } from "../internal/interaction.js"
|
||||
import { reconciler } from "../internal/reconciler.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"
|
||||
|
||||
/** @category Core */
|
||||
@@ -23,10 +23,6 @@ export abstract class Reacord {
|
||||
|
||||
constructor(private readonly config: ReacordConfig = {}) {}
|
||||
|
||||
abstract send(...args: unknown[]): ReacordInstance
|
||||
abstract reply(...args: unknown[]): ReacordInstance
|
||||
abstract ephemeralReply(...args: unknown[]): ReacordInstance
|
||||
|
||||
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
||||
for (const renderer of this.renderers) {
|
||||
if (renderer.handleComponentInteraction(interaction)) return
|
||||
@@ -58,9 +54,14 @@ export abstract class Reacord {
|
||||
const instance: ReacordInstance = {
|
||||
render: (content: ReactNode) => {
|
||||
reconciler.updateContainer(
|
||||
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
||||
<InstanceProvider value={instance}>
|
||||
<MessageProvider value={renderer.messageStore}>
|
||||
{content}
|
||||
</MessageProvider>
|
||||
</InstanceProvider>,
|
||||
container,
|
||||
)
|
||||
return instance
|
||||
},
|
||||
deactivate: () => {
|
||||
this.deactivate(renderer)
|
||||
|
||||
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 { SelectProps } from "../core/components/select"
|
||||
import { last } from "@reacord/helpers/last"
|
||||
@@ -47,6 +48,7 @@ export interface MessageSelectOptionOptions {
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
data?: ComponentEventMessage
|
||||
edit(options: MessageOptions): Promise<void>
|
||||
delete(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const config: HostConfig<
|
||||
never, // SuspenseInstance,
|
||||
never, // HydratableInstance,
|
||||
never, // PublicInstance,
|
||||
never, // HostContext,
|
||||
null, // HostContext,
|
||||
true, // UpdatePayload,
|
||||
never, // ChildSet,
|
||||
number, // TimeoutHandle,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interaction } from "../interaction"
|
||||
import type { Message, MessageOptions } from "../message"
|
||||
import { Renderer } from "./renderer"
|
||||
|
||||
@@ -6,17 +5,23 @@ import { Renderer } from "./renderer"
|
||||
// so we know whether to call reply() or followUp()
|
||||
const repliedInteractionIds = new Set<string>()
|
||||
|
||||
export type InteractionReplyRendererImplementation = {
|
||||
interactionId: string
|
||||
reply: (options: MessageOptions) => Promise<Message>
|
||||
followUp: (options: MessageOptions) => Promise<Message>
|
||||
}
|
||||
|
||||
export class InteractionReplyRenderer extends Renderer {
|
||||
constructor(private interaction: Interaction) {
|
||||
constructor(private implementation: InteractionReplyRendererImplementation) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected createMessage(options: MessageOptions): Promise<Message> {
|
||||
if (repliedInteractionIds.has(this.interaction.id)) {
|
||||
return this.interaction.followUp(options)
|
||||
if (repliedInteractionIds.has(this.implementation.interactionId)) {
|
||||
return this.implementation.followUp(options)
|
||||
}
|
||||
|
||||
repliedInteractionIds.add(this.interaction.id)
|
||||
return this.interaction.reply(options)
|
||||
repliedInteractionIds.add(this.implementation.interactionId)
|
||||
return this.implementation.reply(options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Container } from "../container.js"
|
||||
import type { ComponentInteraction } from "../interaction"
|
||||
import { MessageStore } from "../message-store.js"
|
||||
import type { Message, MessageOptions } from "../message"
|
||||
import type { Node } from "../node.js"
|
||||
import { Subject } from "rxjs"
|
||||
@@ -12,6 +13,7 @@ type UpdatePayload =
|
||||
|
||||
export abstract class Renderer {
|
||||
readonly nodes = new Container<Node<unknown>>()
|
||||
readonly messageStore = new MessageStore()
|
||||
private componentInteraction?: ComponentInteraction
|
||||
private message?: Message
|
||||
private active = true
|
||||
@@ -75,6 +77,7 @@ export abstract class Renderer {
|
||||
private async updateMessage(payload: UpdatePayload) {
|
||||
if (payload.action === "destroy") {
|
||||
this.updateSubscription.unsubscribe()
|
||||
this.messageStore.set(undefined)
|
||||
await this.message?.delete()
|
||||
return
|
||||
}
|
||||
@@ -113,5 +116,6 @@ export abstract class Renderer {
|
||||
}
|
||||
|
||||
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/select"
|
||||
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-discord-js"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "reacord",
|
||||
"type": "module",
|
||||
"description": "Create interactive Discord messages using React.",
|
||||
"version": "0.5.5",
|
||||
"version": "0.6.0",
|
||||
"homepage": "https://reacord.mapleleaf.dev",
|
||||
"repository": "https://github.com/itsMapleLeaf/reacord.git",
|
||||
"changelog": "https://github.com/itsMapleLeaf/reacord/releases",
|
||||
@@ -36,22 +36,21 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --sourcemap --dts --dts-resolve",
|
||||
"build-watch": "pnpm build -- --watch",
|
||||
"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": "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-dev": "vitest",
|
||||
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
||||
"typecheck": "tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/react": "^18.2.27",
|
||||
"@types/react-reconciler": "^0.28.5",
|
||||
"react-reconciler": "^0.29.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"discord.js": "^14",
|
||||
"discord.js": "^14.25.1",
|
||||
"react": ">=17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -63,15 +62,13 @@
|
||||
"@reacord/helpers": "workspace:*",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"c8": "^8.0.1",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"discord.js": "^14.13.0",
|
||||
"discord.js": "^14.25.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
"pretty-ms": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
"tsup": "^7.2.0",
|
||||
"tsx": "^3.13.0",
|
||||
"type-fest": "^4.4.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { raise } from "@reacord/helpers/raise.js"
|
||||
import {
|
||||
Button,
|
||||
Embed,
|
||||
EmbedField,
|
||||
Link,
|
||||
Option,
|
||||
ReacordDiscordJs,
|
||||
@@ -11,7 +13,6 @@ import type { TextChannel } from "discord.js"
|
||||
import { ChannelType, Client, IntentsBitField } from "discord.js"
|
||||
import "dotenv/config"
|
||||
import { kebabCase } from "lodash-es"
|
||||
import * as React from "react"
|
||||
import { useState } from "react"
|
||||
|
||||
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
||||
@@ -50,12 +51,60 @@ const createTest = async (
|
||||
}
|
||||
|
||||
await createTest("basic", (channel) => {
|
||||
reacord.send(channel.id, "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) => {
|
||||
const Counter = () => {
|
||||
const [count, setCount] = React.useState(0)
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
count: {count}
|
||||
@@ -73,7 +122,7 @@ await createTest("counter", (channel) => {
|
||||
</>
|
||||
)
|
||||
}
|
||||
reacord.send(channel.id, <Counter />)
|
||||
reacord.createChannelMessage(channel).render(<Counter />)
|
||||
})
|
||||
|
||||
await createTest("select", (channel) => {
|
||||
@@ -102,8 +151,7 @@ await createTest("select", (channel) => {
|
||||
)
|
||||
}
|
||||
|
||||
const instance = reacord.send(
|
||||
channel.id,
|
||||
const instance = reacord.createChannelMessage(channel).render(
|
||||
<FruitSelect
|
||||
onConfirm={(value) => {
|
||||
instance.render(`you chose ${value}`)
|
||||
@@ -114,8 +162,7 @@ await createTest("select", (channel) => {
|
||||
})
|
||||
|
||||
await createTest("ephemeral button", (channel) => {
|
||||
reacord.send(
|
||||
channel.id,
|
||||
reacord.createChannelMessage(channel).render(
|
||||
<>
|
||||
<Button
|
||||
label="public clic"
|
||||
@@ -125,7 +172,7 @@ await createTest("ephemeral button", (channel) => {
|
||||
/>
|
||||
<Button
|
||||
label="clic"
|
||||
onClick={(event) => event.ephemeralReply("you clic")}
|
||||
onClick={(event) => event.reply("you clic", { ephemeral: true })}
|
||||
/>
|
||||
</>,
|
||||
)
|
||||
@@ -136,9 +183,11 @@ await createTest("delete this", (channel) => {
|
||||
const instance = useInstance()
|
||||
return <Button label="delete this" onClick={() => instance.destroy()} />
|
||||
}
|
||||
reacord.send(channel.id, <DeleteThis />)
|
||||
reacord.createChannelMessage(channel).render(<DeleteThis />)
|
||||
})
|
||||
|
||||
await createTest("link", (channel) => {
|
||||
reacord.send(channel.id, <Link label="hi" url="https://mapleleaf.dev" />)
|
||||
reacord
|
||||
.createChannelMessage(channel)
|
||||
.render(<Link label="hi" url="https://mapleleaf.dev" />)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { dirname } from "node:path"
|
||||
import { createRequire } from "node:module"
|
||||
import { beforeAll, expect, test } from "vitest"
|
||||
|
||||
beforeAll(() => {
|
||||
spawnSync("pnpm", ["run", "build"])
|
||||
const cwd = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
spawnSync("bun", ["run", "build"], { cwd })
|
||||
})
|
||||
|
||||
test("can require commonjs", () => {
|
||||
|
||||
@@ -6,8 +6,9 @@ import { test } from "vitest"
|
||||
test("rendering behavior", async () => {
|
||||
const tester = new ReacordTester()
|
||||
|
||||
const reply = tester.reply()
|
||||
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
||||
const reply = tester
|
||||
.createInteractionReply()
|
||||
.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
||||
|
||||
await tester.assertMessages([
|
||||
{
|
||||
@@ -244,8 +245,7 @@ test("rendering behavior", async () => {
|
||||
test("delete", async () => {
|
||||
const tester = new ReacordTester()
|
||||
|
||||
const reply = tester.reply()
|
||||
reply.render(
|
||||
const reply = tester.createInteractionReply().render(
|
||||
<>
|
||||
some text
|
||||
<Embed>some embed</Embed>
|
||||
|
||||
@@ -53,9 +53,7 @@ test("single select", async () => {
|
||||
])
|
||||
}
|
||||
|
||||
const reply = tester.reply()
|
||||
|
||||
reply.render(<TestSelect />)
|
||||
tester.createInteractionReply().render(<TestSelect />)
|
||||
await assertSelect([])
|
||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||
|
||||
@@ -119,9 +117,7 @@ test("multiple select", async () => {
|
||||
])
|
||||
}
|
||||
|
||||
const reply = tester.reply()
|
||||
|
||||
reply.render(<TestSelect />)
|
||||
tester.createInteractionReply().render(<TestSelect />)
|
||||
await assertSelect([])
|
||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||
|
||||
@@ -148,7 +144,7 @@ test("multiple select", async () => {
|
||||
|
||||
test("optional onSelect + unknown value", async () => {
|
||||
const tester = new ReacordTester()
|
||||
tester.reply().render(<Select placeholder="select" />)
|
||||
tester.createInteractionReply().render(<Select placeholder="select" />)
|
||||
await tester.findSelectByPlaceholder("select").select("something")
|
||||
await tester.assertMessages([
|
||||
{
|
||||
|
||||
@@ -8,10 +8,11 @@ import { setTimeout } from "node:timers/promises"
|
||||
import type { ReactNode } from "react"
|
||||
import { expect } from "vitest"
|
||||
import type {
|
||||
ChannelInfo,
|
||||
GuildInfo,
|
||||
MessageInfo,
|
||||
UserInfo,
|
||||
ComponentEventChannel,
|
||||
ComponentEventGuild,
|
||||
ComponentEventMessage,
|
||||
ComponentEventReplyOptions,
|
||||
ComponentEventUser,
|
||||
} from "../library/core/component-event"
|
||||
import type { ButtonClickEvent } from "../library/core/components/button"
|
||||
import type { SelectChangeEvent } from "../library/core/components/select"
|
||||
@@ -21,12 +22,14 @@ import type { Channel } from "../library/internal/channel"
|
||||
import { Container } from "../library/internal/container"
|
||||
import type {
|
||||
ButtonInteraction,
|
||||
CommandInteraction,
|
||||
SelectInteraction,
|
||||
} from "../library/internal/interaction"
|
||||
import type { Message, MessageOptions } from "../library/internal/message"
|
||||
import { ChannelMessageRenderer } from "../library/internal/renderers/channel-message-renderer"
|
||||
import { InteractionReplyRenderer } from "../library/internal/renderers/interaction-reply-renderer"
|
||||
import {
|
||||
InteractionReplyRenderer,
|
||||
type InteractionReplyRendererImplementation,
|
||||
} from "../library/internal/renderers/interaction-reply-renderer"
|
||||
|
||||
export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0]
|
||||
|
||||
@@ -42,26 +45,28 @@ export class ReacordTester extends Reacord {
|
||||
return [...this.messageContainer]
|
||||
}
|
||||
|
||||
override send(initialContent?: ReactNode): ReacordInstance {
|
||||
public createChannelMessage(): ReacordInstance {
|
||||
return this.createInstance(
|
||||
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
|
||||
override reply(initialContent?: ReactNode): ReacordInstance {
|
||||
public createMessageReply(): ReacordInstance {
|
||||
return this.createInstance(
|
||||
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
||||
)
|
||||
}
|
||||
|
||||
public createInteractionReply(
|
||||
_options?: ComponentEventReplyOptions,
|
||||
): ReacordInstance {
|
||||
return this.createInstance(
|
||||
new InteractionReplyRenderer(
|
||||
new TestCommandInteraction(this.messageContainer),
|
||||
),
|
||||
initialContent,
|
||||
)
|
||||
}
|
||||
|
||||
override ephemeralReply(initialContent?: ReactNode): ReacordInstance {
|
||||
return this.reply(initialContent)
|
||||
}
|
||||
|
||||
assertMessages(expected: MessageSample[]) {
|
||||
return waitFor(() => {
|
||||
expect(this.sampleMessages()).toEqual(expected)
|
||||
@@ -69,7 +74,7 @@ export class ReacordTester extends Reacord {
|
||||
}
|
||||
|
||||
async assertRender(content: ReactNode, expected: MessageSample[]) {
|
||||
const instance = this.reply()
|
||||
const instance = this.createInteractionReply()
|
||||
instance.render(content)
|
||||
await this.assertMessages(expected)
|
||||
instance.destroy()
|
||||
@@ -171,9 +176,8 @@ class TestMessage implements Message {
|
||||
}
|
||||
}
|
||||
|
||||
class TestCommandInteraction implements CommandInteraction {
|
||||
readonly type = "command"
|
||||
readonly id = "test-command-interaction"
|
||||
class TestCommandInteraction implements InteractionReplyRendererImplementation {
|
||||
readonly interactionId = "test-command-interaction"
|
||||
readonly channelId = "test-channel-id"
|
||||
|
||||
constructor(private messageContainer: Container<TestMessage>) {}
|
||||
@@ -248,17 +252,19 @@ class TestSelectInteraction
|
||||
class TestComponentEvent {
|
||||
constructor(private tester: ReacordTester) {}
|
||||
|
||||
message: MessageInfo = {} as MessageInfo // todo
|
||||
channel: ChannelInfo = {} as ChannelInfo // todo
|
||||
user: UserInfo = {} as UserInfo // todo
|
||||
guild: GuildInfo = {} as GuildInfo // todo
|
||||
message: ComponentEventMessage = {} as ComponentEventMessage // todo
|
||||
channel: ComponentEventChannel = {} as ComponentEventChannel // todo
|
||||
user: ComponentEventUser = {} as ComponentEventUser // todo
|
||||
guild: ComponentEventGuild = {} as ComponentEventGuild // todo
|
||||
|
||||
reply(content?: ReactNode): ReacordInstance {
|
||||
return this.tester.reply(content)
|
||||
return this.tester.createInteractionReply().render(content)
|
||||
}
|
||||
|
||||
ephemeralReply(content?: ReactNode): ReacordInstance {
|
||||
return this.tester.ephemeralReply(content)
|
||||
return this.tester
|
||||
.createInteractionReply({ ephemeral: true })
|
||||
.render(content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ describe("useInstance", () => {
|
||||
}
|
||||
|
||||
const tester = new ReacordTester()
|
||||
const instance = tester.send(<TestComponent name="parent" />)
|
||||
const instance = tester
|
||||
.createChannelMessage()
|
||||
.render(<TestComponent name="parent" />)
|
||||
|
||||
await tester.assertMessages([messageOutput("parent")])
|
||||
expect(instanceFromHook).toBe(instance)
|
||||
|
||||
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",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"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,48 +0,0 @@
|
||||
# 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
|
||||
@@ -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.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"
|
||||
}
|
||||
}
|
||||
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="bg-slate-700/30 shadow sticky top-0 backdrop-blur-sm transition z-10 flex"
|
||||
>
|
||||
<div class="container">
|
||||
<MainNavigation />
|
||||
</div>
|
||||
</header>
|
||||
<main class="container mt-8 flex items-start gap-4">
|
||||
<nav class="w-48 sticky top-24 hidden md:block">
|
||||
<h2 class="text-2xl">Guides</h2>
|
||||
<ul class="mt-3 flex flex-col gap-2 items-start">
|
||||
{
|
||||
guides.map((guide) => (
|
||||
<li>
|
||||
<a class="link" href={`/guides/${guide.slug}`}>
|
||||
{guide.data.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="prose prose-invert pb-8 flex-1 min-w-0">
|
||||
<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="md:hidden relative" data-menu>
|
||||
<summary
|
||||
class="list-none p-2 -m-2 cursor-pointer hover:text-emerald-500 transition"
|
||||
>
|
||||
<slot name="button" />
|
||||
</summary>
|
||||
<div
|
||||
class="w-48 max-h-[calc(100vh-5rem)] bg-slate-800 shadow rounded-lg overflow-x-hidden overflow-y-auto top-[calc(100%+8px)] right-0 absolute z-10"
|
||||
>
|
||||
<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,52 +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, [React](https://reactjs.org), [Discord.js](https://discord.js.org) and the [Discord API](https://discord.dev). Keep these pages as reference if you need it.
|
||||
|
||||
## Setup from template
|
||||
|
||||
[Use this starter template](https://github.com/itsMapleLeaf/reacord-starter) to get off the ground quickly.
|
||||
|
||||
## Adding to an existing project
|
||||
|
||||
Install Reacord and dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install reacord react discord.js
|
||||
|
||||
# yarn
|
||||
yarn add reacord react discord.js
|
||||
|
||||
# pnpm
|
||||
pnpm add reacord react discord.js
|
||||
```
|
||||
|
||||
Create a Discord.js client and a Reacord instance:
|
||||
|
||||
```js
|
||||
// main.jsx
|
||||
import { Client } from "discord.js"
|
||||
import { ReacordDiscordJs } from "reacord"
|
||||
|
||||
const client = new Client()
|
||||
const reacord = new ReacordDiscordJs(client)
|
||||
|
||||
client.on("ready", () => {
|
||||
console.log("Ready!")
|
||||
})
|
||||
|
||||
await client.login(process.env.BOT_TOKEN)
|
||||
```
|
||||
|
||||
To use JSX in your code, run it with [tsx](https://npm.im/tsx):
|
||||
|
||||
```bash
|
||||
npm install tsx
|
||||
tsx main.tsx
|
||||
```
|
||||
@@ -1,166 +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
|
||||
const channelId = "abc123deadbeef"
|
||||
|
||||
client.on("ready", () => {
|
||||
reacord.send(channelId, "Hello, world!")
|
||||
})
|
||||
```
|
||||
|
||||
The `.send()` function creates a **Reacord instance**. You can pass strings, numbers, or anything that can be rendered by React, such as JSX!
|
||||
|
||||
Components rendered through this instance can include state and effects, and the message on Discord will update automatically.
|
||||
|
||||
```jsx
|
||||
function Uptime() {
|
||||
const [startTime] = useState(Date.now())
|
||||
const [currentTime, setCurrentTime] = useState(Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(Date.now())
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return <>this message has been shown for {currentTime - startTime}ms</>
|
||||
}
|
||||
|
||||
client.on("ready", () => {
|
||||
reacord.send(channelId, <Uptime />)
|
||||
})
|
||||
```
|
||||
|
||||
The instance can be rendered to multiple times, which will update the message each time.
|
||||
|
||||
```jsx
|
||||
const Hello = ({ subject }) => <>Hello, {subject}!</>
|
||||
|
||||
client.on("ready", () => {
|
||||
const instance = reacord.send(channel)
|
||||
instance.render(<Hello subject="World" />)
|
||||
instance.render(<Hello subject="Moon" />)
|
||||
})
|
||||
```
|
||||
|
||||
## Cleaning Up Instances
|
||||
|
||||
If you no longer want to use the instance, you can clean it up in a few ways:
|
||||
|
||||
- `instance.destroy()` - This will remove the message.
|
||||
- `instance.deactivate()` - This will keep the message, but it will disable the components on the message, and no longer listen to user interactions.
|
||||
|
||||
By default, Reacord has a max limit on the number of active instances, and deactivates older instances to conserve memory. This can be configured through the Reacord options:
|
||||
|
||||
```js
|
||||
const reacord = new ReacordDiscordJs(client, {
|
||||
// after sending four messages,
|
||||
// the first one will be deactivated
|
||||
maxInstances: 3,
|
||||
})
|
||||
```
|
||||
|
||||
## Discord Slash Commands
|
||||
|
||||
<aside>
|
||||
This section also applies to other kinds of application commands, such as context menu commands.
|
||||
</aside>
|
||||
|
||||
To reply to a command interaction, use the `.reply()` function. This function returns an instance that works the same way as the one from `.send()`. Here's an example:
|
||||
|
||||
```jsx
|
||||
import { Client } from "discord.js"
|
||||
import { 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 reply() function instead of send
|
||||
reacord.reply(interaction, <>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.reply(interaction, <>pong!</>)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hi",
|
||||
description: "say hi",
|
||||
run: (interaction) => {
|
||||
reacord.reply(interaction, <>hi</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
## Ephemeral Command Replies
|
||||
|
||||
Ephemeral replies are replies that only appear for one user. To create them, use the `.ephemeralReply()` function.
|
||||
|
||||
```tsx
|
||||
handleCommands(client, [
|
||||
{
|
||||
name: "pong",
|
||||
description: "pong, but in secret",
|
||||
run: (interaction) => {
|
||||
reacord.ephemeralReply(interaction, <>(pong)</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
The `ephemeralReply` function also returns an instance, but ephemeral replies cannot be updated via `instance.render()`. You can `.deactivate()` them, but `.destroy()` will not delete the message; only the user can hide it from view.
|
||||
@@ -1,63 +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.send(channelId, <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.send(
|
||||
channelId,
|
||||
<FancyMessage>
|
||||
<FancyDetails title="Hello" description="World" />
|
||||
</FancyMessage>,
|
||||
)
|
||||
```
|
||||
|
||||
See the [API Reference](/api/index.html#EmbedAuthorProps) for the full list of embed components.
|
||||
@@ -1,46 +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.ephemeralReply("good job, you clicked it")
|
||||
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,70 +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.send(
|
||||
channelId,
|
||||
<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.
|
||||
|
||||
```tsx
|
||||
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.send(channelId, <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:
|
||||
- packages/*
|
||||
ignoredBuiltDependencies:
|
||||
- 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