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)
|
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
||||||
|
|
||||||
```console
|
```console
|
||||||
# npm
|
# bun
|
||||||
npm install reacord react discord.js
|
bun add reacord react discord.js
|
||||||
|
|
||||||
# yarn
|
|
||||||
yarn add reacord react discord.js
|
|
||||||
|
|
||||||
# pnpm
|
|
||||||
pnpm add reacord react discord.js
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Get Started
|
|
||||||
|
|
||||||
[Visit the docs to get started.](https://reacord.mapleleaf.dev/guides/getting-started)
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
```tsx
|
```tsx
|
||||||
import * as React from "react"
|
import { useState } from "react"
|
||||||
import { Embed, Button } from "reacord"
|
import { Embed, Button } from "reacord"
|
||||||
|
|
||||||
function Counter() {
|
function Counter() {
|
||||||
const [count, setCount] = React.useState(0)
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Embed title="Counter">
|
<Embed title="Counter">
|
||||||
This button has been clicked {count} times.
|
This button has been clicked {count} times.
|
||||||
</Embed>
|
</Embed>
|
||||||
<Button onClick={() => setCount(count + 1)}>
|
<Button
|
||||||
+1
|
label="+1"
|
||||||
</Button>
|
onClick={() => setCount(count + 1)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -1,21 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "reacord-monorepo",
|
"name": "reacord-monorepo",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "run-s --continue-on-error lint:*",
|
"lint": "run-s --continue-on-error lint:*",
|
||||||
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
|
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
|
||||||
"lint:prettier": "prettier . --write --cache --list-different",
|
"lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different",
|
||||||
"lint:types": "tsc -b & pnpm -r --parallel run typecheck",
|
"lint:types": "bun run --cwd packages/helpers typecheck && bun run --cwd packages/reacord typecheck",
|
||||||
"astro-sync": "pnpm --filter website exec astro sync",
|
|
||||||
"format": "run-s --continue-on-error format:*",
|
|
||||||
"format:eslint": "eslint . --report-unused-disable-directives --fix",
|
|
||||||
"format:prettier": "prettier --cache --write . \"**/*.astro\"",
|
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "pnpm -r run build",
|
"build": "bun run --cwd packages/reacord build",
|
||||||
"build:website": "pnpm --filter website... run build",
|
"release": "bun run build && changeset publish"
|
||||||
"start": "pnpm -C packages/website run start",
|
|
||||||
"start:website": "pnpm -C packages/website run start",
|
|
||||||
"release": "pnpm -r run build && changeset publish"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.26.2",
|
"@changesets/cli": "^2.26.2",
|
||||||
@@ -24,7 +20,6 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tailwindcss": "^3.3.3",
|
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vitest": "^0.34.6"
|
"vitest": "^0.34.6"
|
||||||
},
|
},
|
||||||
@@ -36,8 +31,7 @@
|
|||||||
],
|
],
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"dist",
|
"dist"
|
||||||
"packages/website/public/api"
|
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
|||||||
@@ -1,5 +1,40 @@
|
|||||||
# 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
|
||||||
|
|||||||
@@ -9,41 +9,52 @@ export interface ComponentEvent {
|
|||||||
*
|
*
|
||||||
* @see https://discord.com/developers/docs/resources/channel#message-object
|
* @see https://discord.com/developers/docs/resources/channel#message-object
|
||||||
*/
|
*/
|
||||||
message: MessageInfo
|
message: ComponentEventMessage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The channel that this event occurred in.
|
* The channel that this event occurred in.
|
||||||
*
|
*
|
||||||
* @see https://discord.com/developers/docs/resources/channel#channel-object
|
* @see https://discord.com/developers/docs/resources/channel#channel-object
|
||||||
*/
|
*/
|
||||||
channel: ChannelInfo
|
channel: ComponentEventChannel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user that triggered this event.
|
* The user that triggered this event.
|
||||||
*
|
*
|
||||||
* @see https://discord.com/developers/docs/resources/user#user-object
|
* @see https://discord.com/developers/docs/resources/user#user-object
|
||||||
*/
|
*/
|
||||||
user: UserInfo
|
user: ComponentEventUser
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The guild that this event occurred in.
|
* The guild that this event occurred in.
|
||||||
*
|
*
|
||||||
* @see https://discord.com/developers/docs/resources/guild#guild-object
|
* @see https://discord.com/developers/docs/resources/guild#guild-object
|
||||||
*/
|
*/
|
||||||
guild?: GuildInfo
|
guild?: ComponentEventGuild
|
||||||
|
|
||||||
/** Create a new reply to this event. */
|
/** 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
|
* Create an ephemeral reply to this event, shown only to the user who
|
||||||
* triggered it.
|
* triggered it.
|
||||||
|
*
|
||||||
|
* @deprecated Use event.reply(content, { ephemeral: true })
|
||||||
*/
|
*/
|
||||||
ephemeralReply(content?: ReactNode): ReacordInstance
|
ephemeralReply(content?: ReactNode): ReacordInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @category Component Event */
|
/** @category Component Event */
|
||||||
export interface ChannelInfo {
|
export interface ComponentEventReplyOptions {
|
||||||
|
ephemeral?: boolean
|
||||||
|
tts?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @category Component Event */
|
||||||
|
export interface ComponentEventChannel {
|
||||||
id: string
|
id: string
|
||||||
name?: string
|
name?: string
|
||||||
topic?: string
|
topic?: string
|
||||||
@@ -55,11 +66,11 @@ export interface ChannelInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @category Component Event */
|
/** @category Component Event */
|
||||||
export interface MessageInfo {
|
export interface ComponentEventMessage {
|
||||||
id: string
|
id: string
|
||||||
channelId: string
|
channelId: string
|
||||||
authorId: string
|
authorId: string
|
||||||
member?: GuildMemberInfo
|
member?: ComponentEventGuildMember
|
||||||
content: string
|
content: string
|
||||||
timestamp: string
|
timestamp: string
|
||||||
editedTimestamp?: string
|
editedTimestamp?: string
|
||||||
@@ -70,14 +81,14 @@ export interface MessageInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @category Component Event */
|
/** @category Component Event */
|
||||||
export interface GuildInfo {
|
export interface ComponentEventGuild {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
member: GuildMemberInfo
|
member: ComponentEventGuildMember
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @category Component Event */
|
/** @category Component Event */
|
||||||
export interface GuildMemberInfo {
|
export interface ComponentEventGuildMember {
|
||||||
id: string
|
id: string
|
||||||
nick?: string
|
nick?: string
|
||||||
displayName: string
|
displayName: string
|
||||||
@@ -92,7 +103,7 @@ export interface GuildMemberInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @category Component Event */
|
/** @category Component Event */
|
||||||
export interface UserInfo {
|
export interface ComponentEventUser {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
discriminator: string
|
discriminator: string
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ import type { ComponentEvent } from "../component-event"
|
|||||||
import { OptionNode } from "./option-node"
|
import { OptionNode } from "./option-node"
|
||||||
import { omit } from "@reacord/helpers/omit.js"
|
import { omit } from "@reacord/helpers/omit.js"
|
||||||
|
|
||||||
|
export type SelectMenuType =
|
||||||
|
| "string"
|
||||||
|
| "user"
|
||||||
|
| "role"
|
||||||
|
| "mentionable"
|
||||||
|
| "channel"
|
||||||
|
|
||||||
/** @category Select */
|
/** @category Select */
|
||||||
export interface SelectProps {
|
export interface SelectProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
@@ -25,6 +32,20 @@ export interface SelectProps {
|
|||||||
/** The text shown when no value is selected */
|
/** The text shown when no value is selected */
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The kind of select menu to render.
|
||||||
|
*
|
||||||
|
* Defaults to `string`.
|
||||||
|
*/
|
||||||
|
menuType?: SelectMenuType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit the channel types shown in a channel select menu.
|
||||||
|
*
|
||||||
|
* This is only used when `menuType` is `channel`.
|
||||||
|
*/
|
||||||
|
channelTypes?: number[]
|
||||||
|
|
||||||
/** Set to true to allow multiple selected values */
|
/** Set to true to allow multiple selected values */
|
||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { ReacordInstance } from "./instance.js"
|
import type { ReacordInstance } from "./instance.js"
|
||||||
import { raise } from "@reacord/helpers/raise.js"
|
import { raise } from "@reacord/helpers/raise.js"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import type { MessageStore } from "../internal/message-store.js"
|
||||||
|
|
||||||
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
||||||
|
const MessageContext = React.createContext<MessageStore | undefined>(undefined)
|
||||||
|
|
||||||
export const InstanceProvider = Context.Provider
|
export const InstanceProvider = Context.Provider
|
||||||
|
export const MessageProvider = MessageContext.Provider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the associated instance for the current component.
|
* Get the associated instance for the current component.
|
||||||
@@ -18,3 +21,27 @@ export function useInstance(): ReacordInstance {
|
|||||||
raise("Could not find instance, was this component rendered via Reacord?")
|
raise("Could not find instance, was this component rendered via Reacord?")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the message that the current component is rendered into.
|
||||||
|
*
|
||||||
|
* @category Core
|
||||||
|
*/
|
||||||
|
export function useMessage() {
|
||||||
|
const store =
|
||||||
|
React.useContext(MessageContext) ??
|
||||||
|
raise("Could not find message store, was this component rendered via Reacord?")
|
||||||
|
|
||||||
|
const getSnapshot = React.useCallback(() => store.getSnapshot(), [store])
|
||||||
|
|
||||||
|
if (React.useSyncExternalStore) {
|
||||||
|
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState(getSnapshot)
|
||||||
|
React.useEffect(() => store.subscribe(() => setValue(getSnapshot())), [
|
||||||
|
store,
|
||||||
|
getSnapshot,
|
||||||
|
])
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ReactNode } from "react"
|
|||||||
*/
|
*/
|
||||||
export interface ReacordInstance {
|
export interface ReacordInstance {
|
||||||
/** Render some JSX to this instance (edits the message) */
|
/** Render some JSX to this instance (edits the message) */
|
||||||
render: (content: ReactNode) => void
|
render: (content: ReactNode) => ReacordInstance
|
||||||
|
|
||||||
/** Remove this message */
|
/** Remove this message */
|
||||||
destroy: () => void
|
destroy: () => void
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import type {
|
|||||||
import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer"
|
import { ChannelMessageRenderer } from "../internal/renderers/channel-message-renderer"
|
||||||
import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer"
|
import { InteractionReplyRenderer } from "../internal/renderers/interaction-reply-renderer"
|
||||||
import type {
|
import type {
|
||||||
ChannelInfo,
|
ComponentEventChannel,
|
||||||
GuildInfo,
|
ComponentEventGuild,
|
||||||
GuildMemberInfo,
|
ComponentEventGuildMember,
|
||||||
MessageInfo,
|
ComponentEventMessage,
|
||||||
UserInfo,
|
ComponentEventReplyOptions,
|
||||||
|
ComponentEventUser,
|
||||||
} from "./component-event"
|
} from "./component-event"
|
||||||
import type { ReacordInstance } from "./instance"
|
import type { ReacordInstance } from "./instance"
|
||||||
import type { ReacordConfig } from "./reacord"
|
import type { ReacordConfig } from "./reacord"
|
||||||
@@ -37,7 +38,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
super(config)
|
super(config)
|
||||||
|
|
||||||
client.on("interactionCreate", (interaction) => {
|
client.on("interactionCreate", (interaction) => {
|
||||||
if (interaction.isButton() || interaction.isStringSelectMenu()) {
|
if (interaction.isButton() || interaction.isAnySelectMenu()) {
|
||||||
this.handleComponentInteraction(
|
this.handleComponentInteraction(
|
||||||
this.createReacordComponentInteraction(interaction),
|
this.createReacordComponentInteraction(interaction),
|
||||||
)
|
)
|
||||||
@@ -48,14 +49,49 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
/**
|
/**
|
||||||
* Sends a message to a channel.
|
* 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
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
*/
|
*/
|
||||||
override send(
|
public send(
|
||||||
channelId: string,
|
channel: Discord.ChannelResolvable,
|
||||||
initialContent?: React.ReactNode,
|
initialContent?: React.ReactNode,
|
||||||
): ReacordInstance {
|
): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
this.createChannelRenderer(channelId),
|
this.createChannelMessageRenderer(channel, {}),
|
||||||
initialContent,
|
initialContent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -63,14 +99,15 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
/**
|
/**
|
||||||
* Sends a message as a reply to a command interaction.
|
* Sends a message as a reply to a command interaction.
|
||||||
*
|
*
|
||||||
|
* @deprecated Use reacord.createInteractionReply() instead.
|
||||||
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
*/
|
*/
|
||||||
override reply(
|
public reply(
|
||||||
interaction: Discord.CommandInteraction,
|
interaction: Discord.CommandInteraction,
|
||||||
initialContent?: React.ReactNode,
|
initialContent?: React.ReactNode,
|
||||||
): ReacordInstance {
|
): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
this.createInteractionReplyRenderer(interaction),
|
this.createInteractionReplyRenderer(interaction, {}),
|
||||||
initialContent,
|
initialContent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -78,31 +115,51 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
/**
|
/**
|
||||||
* Sends an ephemeral message as a reply to a command interaction.
|
* 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
|
* @see https://reacord.mapleleaf.dev/guides/sending-messages
|
||||||
*/
|
*/
|
||||||
override ephemeralReply(
|
public ephemeralReply(
|
||||||
interaction: Discord.CommandInteraction,
|
interaction: Discord.CommandInteraction,
|
||||||
initialContent?: React.ReactNode,
|
initialContent?: React.ReactNode,
|
||||||
): ReacordInstance {
|
): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
this.createInteractionReplyRenderer(interaction, {
|
||||||
|
ephemeral: true,
|
||||||
|
}),
|
||||||
initialContent,
|
initialContent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private createChannelRenderer(channelId: string) {
|
private createChannelMessageRenderer(
|
||||||
|
channelResolvable: Discord.ChannelResolvable,
|
||||||
|
messageCreateOptions?: Discord.MessageCreateOptions,
|
||||||
|
) {
|
||||||
return new ChannelMessageRenderer({
|
return new ChannelMessageRenderer({
|
||||||
send: async (options) => {
|
send: async (messageOptions) => {
|
||||||
const channel =
|
let channel = this.client.channels.resolve(channelResolvable)
|
||||||
this.client.channels.cache.get(channelId) ??
|
if (!channel && typeof channelResolvable === "string") {
|
||||||
(await this.client.channels.fetch(channelId)) ??
|
channel = await this.client.channels.fetch(channelResolvable)
|
||||||
raise(`Channel ${channelId} not found`)
|
|
||||||
|
|
||||||
if (!channel.isTextBased()) {
|
|
||||||
raise(`Channel ${channelId} is not a text channel`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
return createReacordMessage(message)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -112,20 +169,22 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
interaction:
|
interaction:
|
||||||
| Discord.CommandInteraction
|
| Discord.CommandInteraction
|
||||||
| Discord.MessageComponentInteraction,
|
| Discord.MessageComponentInteraction,
|
||||||
|
interactionReplyOptions: Discord.InteractionReplyOptions,
|
||||||
) {
|
) {
|
||||||
return new InteractionReplyRenderer({
|
return new InteractionReplyRenderer({
|
||||||
type: "command",
|
interactionId: interaction.id,
|
||||||
id: interaction.id,
|
reply: async (messageOptions) => {
|
||||||
reply: async (options) => {
|
|
||||||
const message = await interaction.reply({
|
const message = await interaction.reply({
|
||||||
...getDiscordMessageOptions(options),
|
...getDiscordMessageOptions(messageOptions),
|
||||||
|
...interactionReplyOptions,
|
||||||
fetchReply: true,
|
fetchReply: true,
|
||||||
})
|
})
|
||||||
return createReacordMessage(message)
|
return createReacordMessage(message)
|
||||||
},
|
},
|
||||||
followUp: async (options) => {
|
followUp: async (messageOptions) => {
|
||||||
const message = await interaction.followUp({
|
const message = await interaction.followUp({
|
||||||
...getDiscordMessageOptions(options),
|
...getDiscordMessageOptions(messageOptions),
|
||||||
|
...interactionReplyOptions,
|
||||||
fetchReply: true,
|
fetchReply: true,
|
||||||
})
|
})
|
||||||
return createReacordMessage(message)
|
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(
|
private createReacordComponentInteraction(
|
||||||
interaction: Discord.MessageComponentInteraction,
|
interaction: Discord.MessageComponentInteraction,
|
||||||
): ComponentInteraction {
|
): ComponentInteraction {
|
||||||
// todo please dear god clean this up
|
// todo please dear god clean this up
|
||||||
const channel: ChannelInfo = interaction.channel
|
const channel: ComponentEventChannel = interaction.channel
|
||||||
? {
|
? {
|
||||||
...pruneNullishValues(
|
...pruneNullishValues(
|
||||||
pick(interaction.channel, [
|
pick(interaction.channel, [
|
||||||
@@ -178,43 +212,18 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
}
|
}
|
||||||
: raise("Non-channel interactions are not supported")
|
: raise("Non-channel interactions are not supported")
|
||||||
|
|
||||||
const message: MessageInfo =
|
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: GuildMemberInfo | undefined =
|
const member: ComponentEventGuildMember | undefined =
|
||||||
interaction.member instanceof Discord.GuildMember
|
interaction.member instanceof Discord.GuildMember
|
||||||
? {
|
? {
|
||||||
...pruneNullishValues(
|
...pruneNullishValues(
|
||||||
pick(interaction.member, [
|
pick(interaction.member, ["nick", "avatarUrl", "pending"]),
|
||||||
"id",
|
|
||||||
"nick",
|
|
||||||
"displayName",
|
|
||||||
"avatarUrl",
|
|
||||||
"displayAvatarUrl",
|
|
||||||
"color",
|
|
||||||
"pending",
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
|
id: interaction.member.id,
|
||||||
displayName: interaction.member.displayName,
|
displayName: interaction.member.displayName,
|
||||||
roles: interaction.member.roles.cache.map((role) => role.id),
|
roles: interaction.member.roles.cache.map((role) => role.id),
|
||||||
joinedAt: interaction.member.joinedAt?.toISOString(),
|
joinedAt: interaction.member.joinedAt?.toISOString(),
|
||||||
@@ -226,17 +235,19 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
}
|
}
|
||||||
: undefined
|
: 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"),
|
member: member ?? raise("unexpected: member is undefined"),
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const user: UserInfo = {
|
const user: ComponentEventUser = {
|
||||||
...pruneNullishValues(
|
id: interaction.user.id,
|
||||||
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
username: interaction.user.username,
|
||||||
),
|
discriminator: interaction.user.discriminator,
|
||||||
|
tag: interaction.user.tag,
|
||||||
avatarUrl: interaction.user.avatarURL(),
|
avatarUrl: interaction.user.avatarURL(),
|
||||||
accentColor: interaction.user.accentColor ?? undefined,
|
accentColor: interaction.user.accentColor ?? undefined,
|
||||||
}
|
}
|
||||||
@@ -275,15 +286,18 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
user,
|
user,
|
||||||
guild,
|
guild,
|
||||||
|
|
||||||
reply: (content?: ReactNode) =>
|
reply: (content?: ReactNode, options?: ComponentEventReplyOptions) =>
|
||||||
this.createInstance(
|
this.createInstance(
|
||||||
this.createInteractionReplyRenderer(interaction),
|
this.createInteractionReplyRenderer(interaction, options ?? {}),
|
||||||
content,
|
content,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/** @deprecated Use event.reply(content, { ephemeral: true }) */
|
||||||
ephemeralReply: (content: ReactNode) =>
|
ephemeralReply: (content: ReactNode) =>
|
||||||
this.createInstance(
|
this.createInstance(
|
||||||
this.createEphemeralInteractionReplyRenderer(interaction),
|
this.createInteractionReplyRenderer(interaction, {
|
||||||
|
ephemeral: true,
|
||||||
|
}),
|
||||||
content,
|
content,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -296,7 +310,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.isStringSelectMenu()) {
|
if (interaction.isAnySelectMenu()) {
|
||||||
return {
|
return {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
type: "select",
|
type: "select",
|
||||||
@@ -313,6 +327,7 @@ export class ReacordDiscordJs extends Reacord {
|
|||||||
|
|
||||||
function createReacordMessage(message: Discord.Message): Message {
|
function createReacordMessage(message: Discord.Message): Message {
|
||||||
return {
|
return {
|
||||||
|
data: createComponentEventMessage(message),
|
||||||
edit: async (options) => {
|
edit: async (options) => {
|
||||||
await message.edit(getDiscordMessageOptions(options))
|
await message.edit(getDiscordMessageOptions(options))
|
||||||
},
|
},
|
||||||
@@ -322,16 +337,25 @@ function createReacordMessage(message: Discord.Message): Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEphemeralReacordMessage(): Message {
|
function createComponentEventMessage(
|
||||||
|
message: Discord.Message,
|
||||||
|
): ComponentEventMessage {
|
||||||
return {
|
return {
|
||||||
edit: () => {
|
...pick(message, [
|
||||||
console.warn("Ephemeral messages can't be edited")
|
"id",
|
||||||
return Promise.resolve()
|
"channelId",
|
||||||
},
|
"authorId",
|
||||||
delete: () => {
|
"content",
|
||||||
console.warn("Ephemeral messages can't be deleted")
|
"tts",
|
||||||
return Promise.resolve()
|
"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
|
// future proofing
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (component.type === "select") {
|
if (component.type === "select") {
|
||||||
return {
|
const {
|
||||||
...component,
|
menuType,
|
||||||
type: Discord.ComponentType.SelectMenu,
|
values,
|
||||||
options: component.options.map((option) => ({
|
options: selectOptions,
|
||||||
...option,
|
channelTypes,
|
||||||
default: component.values?.includes(option.value),
|
multiple,
|
||||||
})),
|
...rest
|
||||||
|
} = component
|
||||||
|
|
||||||
|
if (menuType === "string" || menuType == undefined) {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.StringSelect,
|
||||||
|
options: selectOptions.map((option) => ({
|
||||||
|
...option,
|
||||||
|
default: values?.includes(option.value),
|
||||||
|
})),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (menuType === "user") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.UserSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuType === "role") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.RoleSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuType === "mentionable") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.MentionableSelect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuType === "channel") {
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
type: Discord.ComponentType.ChannelSelect,
|
||||||
|
channelTypes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
raise(
|
||||||
|
`Unsupported select menu type: ${menuType ?? "string"}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
component satisfies never
|
component satisfies never
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
|
|||||||
import type { ComponentInteraction } from "../internal/interaction.js"
|
import type { ComponentInteraction } from "../internal/interaction.js"
|
||||||
import { reconciler } from "../internal/reconciler.js"
|
import { reconciler } from "../internal/reconciler.js"
|
||||||
import type { Renderer } from "../internal/renderers/renderer.js"
|
import type { Renderer } from "../internal/renderers/renderer.js"
|
||||||
import { InstanceProvider } from "./instance-context.js"
|
import { InstanceProvider, MessageProvider } from "./instance-context.js"
|
||||||
import type { ReacordInstance } from "./instance.js"
|
import type { ReacordInstance } from "./instance.js"
|
||||||
|
|
||||||
/** @category Core */
|
/** @category Core */
|
||||||
@@ -23,10 +23,6 @@ export abstract class Reacord {
|
|||||||
|
|
||||||
constructor(private readonly config: ReacordConfig = {}) {}
|
constructor(private readonly config: ReacordConfig = {}) {}
|
||||||
|
|
||||||
abstract send(...args: unknown[]): ReacordInstance
|
|
||||||
abstract reply(...args: unknown[]): ReacordInstance
|
|
||||||
abstract ephemeralReply(...args: unknown[]): ReacordInstance
|
|
||||||
|
|
||||||
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
protected handleComponentInteraction(interaction: ComponentInteraction) {
|
||||||
for (const renderer of this.renderers) {
|
for (const renderer of this.renderers) {
|
||||||
if (renderer.handleComponentInteraction(interaction)) return
|
if (renderer.handleComponentInteraction(interaction)) return
|
||||||
@@ -58,9 +54,14 @@ export abstract class Reacord {
|
|||||||
const instance: ReacordInstance = {
|
const instance: ReacordInstance = {
|
||||||
render: (content: ReactNode) => {
|
render: (content: ReactNode) => {
|
||||||
reconciler.updateContainer(
|
reconciler.updateContainer(
|
||||||
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
<InstanceProvider value={instance}>
|
||||||
|
<MessageProvider value={renderer.messageStore}>
|
||||||
|
{content}
|
||||||
|
</MessageProvider>
|
||||||
|
</InstanceProvider>,
|
||||||
container,
|
container,
|
||||||
)
|
)
|
||||||
|
return instance
|
||||||
},
|
},
|
||||||
deactivate: () => {
|
deactivate: () => {
|
||||||
this.deactivate(renderer)
|
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 { EmbedOptions } from "../core/components/embed-options"
|
||||||
import type { SelectProps } from "../core/components/select"
|
import type { SelectProps } from "../core/components/select"
|
||||||
import { last } from "@reacord/helpers/last"
|
import { last } from "@reacord/helpers/last"
|
||||||
@@ -47,6 +48,7 @@ export interface MessageSelectOptionOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
|
data?: ComponentEventMessage
|
||||||
edit(options: MessageOptions): Promise<void>
|
edit(options: MessageOptions): Promise<void>
|
||||||
delete(): Promise<void>
|
delete(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const config: HostConfig<
|
|||||||
never, // SuspenseInstance,
|
never, // SuspenseInstance,
|
||||||
never, // HydratableInstance,
|
never, // HydratableInstance,
|
||||||
never, // PublicInstance,
|
never, // PublicInstance,
|
||||||
never, // HostContext,
|
null, // HostContext,
|
||||||
true, // UpdatePayload,
|
true, // UpdatePayload,
|
||||||
never, // ChildSet,
|
never, // ChildSet,
|
||||||
number, // TimeoutHandle,
|
number, // TimeoutHandle,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { Interaction } from "../interaction"
|
|
||||||
import type { Message, MessageOptions } from "../message"
|
import type { Message, MessageOptions } from "../message"
|
||||||
import { Renderer } from "./renderer"
|
import { Renderer } from "./renderer"
|
||||||
|
|
||||||
@@ -6,17 +5,23 @@ import { Renderer } from "./renderer"
|
|||||||
// so we know whether to call reply() or followUp()
|
// so we know whether to call reply() or followUp()
|
||||||
const repliedInteractionIds = new Set<string>()
|
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 {
|
export class InteractionReplyRenderer extends Renderer {
|
||||||
constructor(private interaction: Interaction) {
|
constructor(private implementation: InteractionReplyRendererImplementation) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createMessage(options: MessageOptions): Promise<Message> {
|
protected createMessage(options: MessageOptions): Promise<Message> {
|
||||||
if (repliedInteractionIds.has(this.interaction.id)) {
|
if (repliedInteractionIds.has(this.implementation.interactionId)) {
|
||||||
return this.interaction.followUp(options)
|
return this.implementation.followUp(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
repliedInteractionIds.add(this.interaction.id)
|
repliedInteractionIds.add(this.implementation.interactionId)
|
||||||
return this.interaction.reply(options)
|
return this.implementation.reply(options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Container } from "../container.js"
|
import { Container } from "../container.js"
|
||||||
import type { ComponentInteraction } from "../interaction"
|
import type { ComponentInteraction } from "../interaction"
|
||||||
|
import { MessageStore } from "../message-store.js"
|
||||||
import type { Message, MessageOptions } from "../message"
|
import type { Message, MessageOptions } from "../message"
|
||||||
import type { Node } from "../node.js"
|
import type { Node } from "../node.js"
|
||||||
import { Subject } from "rxjs"
|
import { Subject } from "rxjs"
|
||||||
@@ -12,6 +13,7 @@ type UpdatePayload =
|
|||||||
|
|
||||||
export abstract class Renderer {
|
export abstract class Renderer {
|
||||||
readonly nodes = new Container<Node<unknown>>()
|
readonly nodes = new Container<Node<unknown>>()
|
||||||
|
readonly messageStore = new MessageStore()
|
||||||
private componentInteraction?: ComponentInteraction
|
private componentInteraction?: ComponentInteraction
|
||||||
private message?: Message
|
private message?: Message
|
||||||
private active = true
|
private active = true
|
||||||
@@ -75,6 +77,7 @@ export abstract class Renderer {
|
|||||||
private async updateMessage(payload: UpdatePayload) {
|
private async updateMessage(payload: UpdatePayload) {
|
||||||
if (payload.action === "destroy") {
|
if (payload.action === "destroy") {
|
||||||
this.updateSubscription.unsubscribe()
|
this.updateSubscription.unsubscribe()
|
||||||
|
this.messageStore.set(undefined)
|
||||||
await this.message?.delete()
|
await this.message?.delete()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -113,5 +116,6 @@ export abstract class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.message = await this.createMessage(payload.options)
|
this.message = await this.createMessage(payload.options)
|
||||||
|
this.messageStore.set(this.message.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export * from "./core/components/link"
|
|||||||
export * from "./core/components/option"
|
export * from "./core/components/option"
|
||||||
export * from "./core/components/select"
|
export * from "./core/components/select"
|
||||||
export * from "./core/instance"
|
export * from "./core/instance"
|
||||||
export { useInstance } from "./core/instance-context"
|
export { useInstance, useMessage } from "./core/instance-context"
|
||||||
export * from "./core/reacord"
|
export * from "./core/reacord"
|
||||||
export * from "./core/reacord-discord-js"
|
export * from "./core/reacord-discord-js"
|
||||||
|
|||||||
@@ -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.5.5",
|
"version": "0.6.0",
|
||||||
"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,22 +36,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node16 --format cjs,esm --sourcemap --dts --dts-resolve",
|
"build": "rm -rf dist && mkdir -p dist && cp ../../README.md ../../LICENSE . && bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --external react --external react/jsx-runtime --external react/jsx-dev-runtime && bun build library/main.ts --target=node --outdir=dist --format=cjs --sourcemap=external --entry-naming=main.cjs --external react --external react/jsx-runtime --external react/jsx-dev-runtime && tsc -p tsconfig.build.json",
|
||||||
"build-watch": "pnpm build -- --watch",
|
"build-watch": "bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --watch --external react --external react/jsx-runtime --external react/jsx-dev-runtime",
|
||||||
"test": "vitest --coverage --no-watch",
|
"test": "vitest --coverage --no-watch",
|
||||||
"test-dev": "vitest",
|
"test-dev": "vitest",
|
||||||
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
||||||
"typecheck": "tsc -b"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^20.8.4",
|
|
||||||
"@types/react": "^18.2.27",
|
"@types/react": "^18.2.27",
|
||||||
"@types/react-reconciler": "^0.28.5",
|
"@types/react-reconciler": "^0.28.5",
|
||||||
"react-reconciler": "^0.29.0",
|
"react-reconciler": "^0.29.0",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"discord.js": "^14",
|
"discord.js": "^14.25.1",
|
||||||
"react": ">=17"
|
"react": ">=17"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
@@ -63,15 +62,13 @@
|
|||||||
"@reacord/helpers": "workspace:*",
|
"@reacord/helpers": "workspace:*",
|
||||||
"@types/lodash-es": "^4.17.9",
|
"@types/lodash-es": "^4.17.9",
|
||||||
"c8": "^8.0.1",
|
"c8": "^8.0.1",
|
||||||
"cpy-cli": "^5.0.0",
|
"discord.js": "^14.25.1",
|
||||||
"discord.js": "^14.13.0",
|
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"pretty-ms": "^8.0.0",
|
"pretty-ms": "^8.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"tsup": "^7.2.0",
|
|
||||||
"tsx": "^3.13.0",
|
"tsx": "^3.13.0",
|
||||||
"type-fest": "^4.4.0"
|
"type-fest": "^4.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { raise } from "@reacord/helpers/raise.js"
|
import { raise } from "@reacord/helpers/raise.js"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Embed,
|
||||||
|
EmbedField,
|
||||||
Link,
|
Link,
|
||||||
Option,
|
Option,
|
||||||
ReacordDiscordJs,
|
ReacordDiscordJs,
|
||||||
@@ -11,7 +13,6 @@ import type { TextChannel } from "discord.js"
|
|||||||
import { ChannelType, Client, IntentsBitField } from "discord.js"
|
import { ChannelType, Client, IntentsBitField } from "discord.js"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
import { kebabCase } from "lodash-es"
|
import { kebabCase } from "lodash-es"
|
||||||
import * as React from "react"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
const client = new Client({ intents: IntentsBitField.Flags.Guilds })
|
||||||
@@ -50,12 +51,60 @@ const createTest = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createTest("basic", (channel) => {
|
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) => {
|
await createTest("counter", (channel) => {
|
||||||
const Counter = () => {
|
function Counter() {
|
||||||
const [count, setCount] = React.useState(0)
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
count: {count}
|
count: {count}
|
||||||
@@ -73,7 +122,7 @@ await createTest("counter", (channel) => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
reacord.send(channel.id, <Counter />)
|
reacord.createChannelMessage(channel).render(<Counter />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("select", (channel) => {
|
await createTest("select", (channel) => {
|
||||||
@@ -102,8 +151,7 @@ await createTest("select", (channel) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = reacord.send(
|
const instance = reacord.createChannelMessage(channel).render(
|
||||||
channel.id,
|
|
||||||
<FruitSelect
|
<FruitSelect
|
||||||
onConfirm={(value) => {
|
onConfirm={(value) => {
|
||||||
instance.render(`you chose ${value}`)
|
instance.render(`you chose ${value}`)
|
||||||
@@ -114,8 +162,7 @@ await createTest("select", (channel) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await createTest("ephemeral button", (channel) => {
|
await createTest("ephemeral button", (channel) => {
|
||||||
reacord.send(
|
reacord.createChannelMessage(channel).render(
|
||||||
channel.id,
|
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
label="public clic"
|
label="public clic"
|
||||||
@@ -125,7 +172,7 @@ await createTest("ephemeral button", (channel) => {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="clic"
|
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()
|
const instance = useInstance()
|
||||||
return <Button label="delete this" onClick={() => instance.destroy()} />
|
return <Button label="delete this" onClick={() => instance.destroy()} />
|
||||||
}
|
}
|
||||||
reacord.send(channel.id, <DeleteThis />)
|
reacord.createChannelMessage(channel).render(<DeleteThis />)
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTest("link", (channel) => {
|
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 { spawnSync } from "node:child_process"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { dirname } from "node:path"
|
||||||
import { createRequire } from "node:module"
|
import { createRequire } from "node:module"
|
||||||
import { beforeAll, expect, test } from "vitest"
|
import { beforeAll, expect, test } from "vitest"
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
spawnSync("pnpm", ["run", "build"])
|
const cwd = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||||
|
spawnSync("bun", ["run", "build"], { cwd })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can require commonjs", () => {
|
test("can require commonjs", () => {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { test } from "vitest"
|
|||||||
test("rendering behavior", async () => {
|
test("rendering behavior", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
const reply = tester.reply()
|
const reply = tester
|
||||||
reply.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
.createInteractionReply()
|
||||||
|
.render(<KitchenSinkCounter onDeactivate={() => reply.deactivate()} />)
|
||||||
|
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
@@ -244,8 +245,7 @@ test("rendering behavior", async () => {
|
|||||||
test("delete", async () => {
|
test("delete", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
|
|
||||||
const reply = tester.reply()
|
const reply = tester.createInteractionReply().render(
|
||||||
reply.render(
|
|
||||||
<>
|
<>
|
||||||
some text
|
some text
|
||||||
<Embed>some embed</Embed>
|
<Embed>some embed</Embed>
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ test("single select", async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = tester.reply()
|
tester.createInteractionReply().render(<TestSelect />)
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
@@ -119,9 +117,7 @@ test("multiple select", async () => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = tester.reply()
|
tester.createInteractionReply().render(<TestSelect />)
|
||||||
|
|
||||||
reply.render(<TestSelect />)
|
|
||||||
await assertSelect([])
|
await assertSelect([])
|
||||||
expect(onSelect).toHaveBeenCalledTimes(0)
|
expect(onSelect).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
@@ -148,7 +144,7 @@ test("multiple select", async () => {
|
|||||||
|
|
||||||
test("optional onSelect + unknown value", async () => {
|
test("optional onSelect + unknown value", async () => {
|
||||||
const tester = new ReacordTester()
|
const tester = new ReacordTester()
|
||||||
tester.reply().render(<Select placeholder="select" />)
|
tester.createInteractionReply().render(<Select placeholder="select" />)
|
||||||
await tester.findSelectByPlaceholder("select").select("something")
|
await tester.findSelectByPlaceholder("select").select("something")
|
||||||
await tester.assertMessages([
|
await tester.assertMessages([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { setTimeout } from "node:timers/promises"
|
|||||||
import type { ReactNode } from "react"
|
import type { ReactNode } from "react"
|
||||||
import { expect } from "vitest"
|
import { expect } from "vitest"
|
||||||
import type {
|
import type {
|
||||||
ChannelInfo,
|
ComponentEventChannel,
|
||||||
GuildInfo,
|
ComponentEventGuild,
|
||||||
MessageInfo,
|
ComponentEventMessage,
|
||||||
UserInfo,
|
ComponentEventReplyOptions,
|
||||||
|
ComponentEventUser,
|
||||||
} from "../library/core/component-event"
|
} from "../library/core/component-event"
|
||||||
import type { ButtonClickEvent } from "../library/core/components/button"
|
import type { ButtonClickEvent } from "../library/core/components/button"
|
||||||
import type { SelectChangeEvent } from "../library/core/components/select"
|
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 { Container } from "../library/internal/container"
|
||||||
import type {
|
import type {
|
||||||
ButtonInteraction,
|
ButtonInteraction,
|
||||||
CommandInteraction,
|
|
||||||
SelectInteraction,
|
SelectInteraction,
|
||||||
} from "../library/internal/interaction"
|
} from "../library/internal/interaction"
|
||||||
import type { Message, MessageOptions } from "../library/internal/message"
|
import type { Message, MessageOptions } from "../library/internal/message"
|
||||||
import { ChannelMessageRenderer } from "../library/internal/renderers/channel-message-renderer"
|
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]
|
export type MessageSample = ReturnType<ReacordTester["sampleMessages"]>[0]
|
||||||
|
|
||||||
@@ -42,26 +45,28 @@ export class ReacordTester extends Reacord {
|
|||||||
return [...this.messageContainer]
|
return [...this.messageContainer]
|
||||||
}
|
}
|
||||||
|
|
||||||
override send(initialContent?: ReactNode): ReacordInstance {
|
public createChannelMessage(): ReacordInstance {
|
||||||
return this.createInstance(
|
return this.createInstance(
|
||||||
new ChannelMessageRenderer(new TestChannel(this.messageContainer)),
|
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(
|
return this.createInstance(
|
||||||
new InteractionReplyRenderer(
|
new InteractionReplyRenderer(
|
||||||
new TestCommandInteraction(this.messageContainer),
|
new TestCommandInteraction(this.messageContainer),
|
||||||
),
|
),
|
||||||
initialContent,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override ephemeralReply(initialContent?: ReactNode): ReacordInstance {
|
|
||||||
return this.reply(initialContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertMessages(expected: MessageSample[]) {
|
assertMessages(expected: MessageSample[]) {
|
||||||
return waitFor(() => {
|
return waitFor(() => {
|
||||||
expect(this.sampleMessages()).toEqual(expected)
|
expect(this.sampleMessages()).toEqual(expected)
|
||||||
@@ -69,7 +74,7 @@ export class ReacordTester extends Reacord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async assertRender(content: ReactNode, expected: MessageSample[]) {
|
async assertRender(content: ReactNode, expected: MessageSample[]) {
|
||||||
const instance = this.reply()
|
const instance = this.createInteractionReply()
|
||||||
instance.render(content)
|
instance.render(content)
|
||||||
await this.assertMessages(expected)
|
await this.assertMessages(expected)
|
||||||
instance.destroy()
|
instance.destroy()
|
||||||
@@ -171,9 +176,8 @@ class TestMessage implements Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestCommandInteraction implements CommandInteraction {
|
class TestCommandInteraction implements InteractionReplyRendererImplementation {
|
||||||
readonly type = "command"
|
readonly interactionId = "test-command-interaction"
|
||||||
readonly id = "test-command-interaction"
|
|
||||||
readonly channelId = "test-channel-id"
|
readonly channelId = "test-channel-id"
|
||||||
|
|
||||||
constructor(private messageContainer: Container<TestMessage>) {}
|
constructor(private messageContainer: Container<TestMessage>) {}
|
||||||
@@ -248,17 +252,19 @@ class TestSelectInteraction
|
|||||||
class TestComponentEvent {
|
class TestComponentEvent {
|
||||||
constructor(private tester: ReacordTester) {}
|
constructor(private tester: ReacordTester) {}
|
||||||
|
|
||||||
message: MessageInfo = {} as MessageInfo // todo
|
message: ComponentEventMessage = {} as ComponentEventMessage // todo
|
||||||
channel: ChannelInfo = {} as ChannelInfo // todo
|
channel: ComponentEventChannel = {} as ComponentEventChannel // todo
|
||||||
user: UserInfo = {} as UserInfo // todo
|
user: ComponentEventUser = {} as ComponentEventUser // todo
|
||||||
guild: GuildInfo = {} as GuildInfo // todo
|
guild: ComponentEventGuild = {} as ComponentEventGuild // todo
|
||||||
|
|
||||||
reply(content?: ReactNode): ReacordInstance {
|
reply(content?: ReactNode): ReacordInstance {
|
||||||
return this.tester.reply(content)
|
return this.tester.createInteractionReply().render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
ephemeralReply(content?: ReactNode): ReacordInstance {
|
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 tester = new ReacordTester()
|
||||||
const instance = tester.send(<TestComponent name="parent" />)
|
const instance = tester
|
||||||
|
.createChannelMessage()
|
||||||
|
.render(<TestComponent name="parent" />)
|
||||||
|
|
||||||
await tester.assertMessages([messageOutput("parent")])
|
await tester.assertMessages([messageOutput("parent")])
|
||||||
expect(instanceFromHook).toBe(instance)
|
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",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
11
packages/website/.gitignore
vendored
11
packages/website/.gitignore
vendored
@@ -1,11 +0,0 @@
|
|||||||
node_modules
|
|
||||||
/.cache
|
|
||||||
/build
|
|
||||||
/public/build
|
|
||||||
.env
|
|
||||||
/public/api
|
|
||||||
cypress/videos
|
|
||||||
cypress/screenshots
|
|
||||||
*.out.css
|
|
||||||
/api
|
|
||||||
.astro
|
|
||||||
@@ -1,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:
|
ignoredBuiltDependencies:
|
||||||
- packages/*
|
- esbuild
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
export default {
|
|
||||||
theme: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: ["RubikVariable", "sans-serif"],
|
|
||||||
monospace: ["'JetBrains Mono'", "monospace"],
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
DEFAULT: "0 2px 9px 0 rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3)",
|
|
||||||
},
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
corePlugins: {
|
|
||||||
container: false,
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@itsmapleleaf/configs/tsconfig"
|
"extends": "@itsmapleleaf/configs/tsconfig.bundler.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"github": {
|
|
||||||
"silent": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user