From c12172cda2028bec6385bc9b40f220d74811826a Mon Sep 17 00:00:00 2001 From: TheMonDon <11539895+TheMonDon@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:20:25 -0500 Subject: [PATCH 1/6] Initial commit of components v2 --- src/generator/index.tsx | 242 ++++---- src/generator/renderers/attachment.tsx | 154 +++--- src/generator/renderers/components.tsx | 160 ++++-- .../renderers/components/Container.tsx | 24 + .../renderers/components/Media Gallery.tsx | 54 ++ .../renderers/components/Select Menu.tsx | 56 ++ .../renderers/components/Spacing.tsx | 17 + .../renderers/components/section/Section.tsx | 31 ++ .../components/section/SectionAccessory.tsx | 25 + .../components/section/SectionContent.tsx | 21 + src/generator/renderers/components/styles.ts | 83 +++ src/generator/renderers/components/utils.ts | 170 ++++++ src/generator/renderers/content.tsx | 522 +++++++++--------- src/generator/renderers/embed.tsx | 136 ++--- src/generator/renderers/message.tsx | 244 ++++---- src/generator/renderers/reply.tsx | 90 +-- src/generator/renderers/systemMessage.tsx | 246 ++++----- src/generator/transcript.tsx | 144 ++--- 18 files changed, 1491 insertions(+), 928 deletions(-) create mode 100644 src/generator/renderers/components/Container.tsx create mode 100644 src/generator/renderers/components/Media Gallery.tsx create mode 100644 src/generator/renderers/components/Select Menu.tsx create mode 100644 src/generator/renderers/components/Spacing.tsx create mode 100644 src/generator/renderers/components/section/Section.tsx create mode 100644 src/generator/renderers/components/section/SectionAccessory.tsx create mode 100644 src/generator/renderers/components/section/SectionContent.tsx create mode 100644 src/generator/renderers/components/styles.ts create mode 100644 src/generator/renderers/components/utils.ts diff --git a/src/generator/index.tsx b/src/generator/index.tsx index 402d4171..b9a436c1 100644 --- a/src/generator/index.tsx +++ b/src/generator/index.tsx @@ -1,121 +1,121 @@ -import { type Awaitable, type Channel, type Message, type Role, type User } from 'discord.js'; -import ReactDOMServer from 'react-dom/server'; -import React from 'react'; -import { buildProfiles } from '../utils/buildProfiles'; -import { revealSpoiler, scrollToMessage } from '../static/client'; -import { readFileSync } from 'fs'; -import path from 'path'; -import { renderToString } from '@derockdev/discord-components-core/hydrate'; -import { streamToString } from '../utils/utils'; -import DiscordMessages from './transcript'; -import type { ResolveImageCallback } from '../downloader/images'; - -// read the package.json file and get the @derockdev/discord-components-core version -let discordComponentsVersion = '^3.6.1'; - -try { - const packagePath = path.join(__dirname, '..', '..', 'package.json'); - const packageJSON = JSON.parse(readFileSync(packagePath, 'utf8')); - discordComponentsVersion = packageJSON.dependencies['@derockdev/discord-components-core'] ?? discordComponentsVersion; - // eslint-disable-next-line no-empty -} catch {} // ignore errors - -export type RenderMessageContext = { - messages: Message[]; - channel: Channel; - - callbacks: { - resolveImageSrc: ResolveImageCallback; - resolveChannel: (channelId: string) => Awaitable; - resolveUser: (userId: string) => Awaitable; - resolveRole: (roleId: string) => Awaitable; - }; - - poweredBy?: boolean; - footerText?: string; - saveImages: boolean; - favicon: 'guild' | string; - hydrate: boolean; -}; - -export default async function render({ messages, channel, callbacks, ...options }: RenderMessageContext) { - const profiles = buildProfiles(messages); - - // NOTE: this renders a STATIC site with no interactivity - // if interactivity is needed, switch to renderToPipeableStream and use hydrateRoot on client. - const stream = ReactDOMServer.renderToStaticNodeStream( - - - - - - {/* favicon */} - - - {/* title */} - {channel.isDMBased() ? 'Direct Messages' : channel.name} - - {/* message reference handler */} - - {/* component library */} - - - )} - - - - - - - {/* Make sure the script runs after the DOM has loaded */} - {options.hydrate && } - - ); - - const markup = await streamToString(stream); - - if (options.hydrate) { - const result = await renderToString(markup, { - beforeHydrate: async (document) => { - document.defaultView.$discordMessage = { - profiles: await profiles, - }; - }, - }); - - return result.html; - } - - return markup; -} +import { type Awaitable, type Channel, type Message, type Role, type User } from 'discord.js'; +import ReactDOMServer from 'react-dom/server'; +import React from 'react'; +import { buildProfiles } from '../utils/buildProfiles'; +import { revealSpoiler, scrollToMessage } from '../static/client'; +import { readFileSync } from 'fs'; +import path from 'path'; +import { renderToString } from '@derockdev/discord-components-core/hydrate'; +import { streamToString } from '../utils/utils'; +import DiscordMessages from './transcript'; +import type { ResolveImageCallback } from '../downloader/images'; + +// read the package.json file and get the @derockdev/discord-components-core version +let discordComponentsVersion = '^3.6.1'; + +try { + const packagePath = path.join(__dirname, '..', '..', 'package.json'); + const packageJSON = JSON.parse(readFileSync(packagePath, 'utf8')); + discordComponentsVersion = packageJSON.dependencies['@derockdev/discord-components-core'] ?? discordComponentsVersion; + // eslint-disable-next-line no-empty +} catch {} // ignore errors + +export type RenderMessageContext = { + messages: Message[]; + channel: Channel; + + callbacks: { + resolveImageSrc: ResolveImageCallback; + resolveChannel: (channelId: string) => Awaitable; + resolveUser: (userId: string) => Awaitable; + resolveRole: (roleId: string) => Awaitable; + }; + + poweredBy?: boolean; + footerText?: string; + saveImages: boolean; + favicon: 'guild' | string; + hydrate: boolean; +}; + +export default async function render({ messages, channel, callbacks, ...options }: RenderMessageContext) { + const profiles = buildProfiles(messages); + + // NOTE: this renders a STATIC site with no interactivity + // if interactivity is needed, switch to renderToPipeableStream and use hydrateRoot on client. + const stream = ReactDOMServer.renderToStaticNodeStream( + + + + + + {/* favicon */} + + + {/* title */} + {channel.isDMBased() ? 'Direct Messages' : channel.name} + + {/* message reference handler */} + + {/* component library */} + + + )} + + + + + + + {/* Make sure the script runs after the DOM has loaded */} + {options.hydrate && } + + ); + + const markup = await streamToString(stream); + + if (options.hydrate) { + const result = await renderToString(markup, { + beforeHydrate: async (document) => { + document.defaultView.$discordMessage = { + profiles: await profiles, + }; + }, + }); + + return result.html; + } + + return markup; +} diff --git a/src/generator/renderers/attachment.tsx b/src/generator/renderers/attachment.tsx index b63fed1a..be139491 100644 --- a/src/generator/renderers/attachment.tsx +++ b/src/generator/renderers/attachment.tsx @@ -1,77 +1,77 @@ -import { DiscordAttachment, DiscordAttachments } from '@derockdev/discord-components-react'; -import React from 'react'; -import type { APIAttachment, APIMessage, Attachment as AttachmentType, Message } from 'discord.js'; -import type { RenderMessageContext } from '..'; -import type { AttachmentTypes } from '../../types'; -import { formatBytes } from '../../utils/utils'; - -/** - * Renders all attachments for a message - * @param message - * @param context - * @returns - */ -export async function Attachments(props: { message: Message; context: RenderMessageContext }) { - if (props.message.attachments.size === 0) return <>; - - return ( - - {props.message.attachments.map((attachment, id) => ( - - ))} - - ); -} - -// "audio" | "video" | "image" | "file" -function getAttachmentType(attachment: AttachmentType): AttachmentTypes { - const type = attachment.contentType?.split('/')?.[0] ?? 'unknown'; - if (['audio', 'video', 'image'].includes(type)) return type as AttachmentTypes; - return 'file'; -} - -/** - * Renders one Discord Attachment - * @param props - the attachment and rendering context - */ -export async function Attachment({ - attachment, - context, - message, -}: { - attachment: AttachmentType; - context: RenderMessageContext; - message: Message; -}) { - let url = attachment.url; - const name = attachment.name; - const width = attachment.width; - const height = attachment.height; - - const type = getAttachmentType(attachment); - - // if the attachment is an image, download it to a data url - if (type === 'image') { - const downloaded = await context.callbacks.resolveImageSrc( - attachment.toJSON() as APIAttachment, - message.toJSON() as APIMessage - ); - - if (downloaded !== null) { - url = downloaded ?? url; - } - } - - return ( - - ); -} +import { DiscordAttachment, DiscordAttachments } from '@derockdev/discord-components-react'; +import React from 'react'; +import type { APIAttachment, APIMessage, Attachment as AttachmentType, Message } from 'discord.js'; +import type { RenderMessageContext } from '..'; +import type { AttachmentTypes } from '../../types'; +import { formatBytes } from '../../utils/utils'; + +/** + * Renders all attachments for a message + * @param message + * @param context + * @returns + */ +export async function Attachments(props: { message: Message; context: RenderMessageContext }) { + if (props.message.attachments.size === 0) return <>; + + return ( + + {props.message.attachments.map((attachment, id) => ( + + ))} + + ); +} + +// "audio" | "video" | "image" | "file" +function getAttachmentType(attachment: AttachmentType): AttachmentTypes { + const type = attachment.contentType?.split('/')?.[0] ?? 'unknown'; + if (['audio', 'video', 'image'].includes(type)) return type as AttachmentTypes; + return 'file'; +} + +/** + * Renders one Discord Attachment + * @param props - the attachment and rendering context + */ +export async function Attachment({ + attachment, + context, + message, +}: { + attachment: AttachmentType; + context: RenderMessageContext; + message: Message; +}) { + let url = attachment.url; + const name = attachment.name; + const width = attachment.width; + const height = attachment.height; + + const type = getAttachmentType(attachment); + + // if the attachment is an image, download it to a data url + if (type === 'image') { + const downloaded = await context.callbacks.resolveImageSrc( + attachment.toJSON() as APIAttachment, + message.toJSON() as APIMessage + ); + + if (downloaded !== null) { + url = downloaded ?? url; + } + } + + return ( + + ); +} diff --git a/src/generator/renderers/components.tsx b/src/generator/renderers/components.tsx index dd1065f4..b93a6918 100644 --- a/src/generator/renderers/components.tsx +++ b/src/generator/renderers/components.tsx @@ -1,39 +1,121 @@ -import { DiscordActionRow, DiscordButton } from '@derockdev/discord-components-react'; -import { ButtonStyle, ComponentType, type MessageActionRowComponent, type ActionRow } from 'discord.js'; -import React from 'react'; -import { parseDiscordEmoji } from '../../utils/utils'; - -export default function ComponentRow({ row, id }: { row: ActionRow; id: number }) { - return ( - - {row.components.map((component, id) => ( - - ))} - - ); -} - -const ButtonStyleMapping = { - [ButtonStyle.Primary]: 'primary', - [ButtonStyle.Secondary]: 'secondary', - [ButtonStyle.Success]: 'success', - [ButtonStyle.Danger]: 'destructive', - [ButtonStyle.Link]: 'secondary', -} as const; - -export function Component({ component, id }: { component: MessageActionRowComponent; id: number }) { - if (component.type === ComponentType.Button) { - return ( - - {component.label} - - ); - } - - return undefined; -} +import { DiscordActionRow, DiscordAttachment, DiscordButton, DiscordSpoiler } from '@derockdev/discord-components-react'; +import { ButtonStyle, ComponentType, type ThumbnailComponent, type MessageActionRowComponent, type TopLevelComponent } from 'discord.js'; +import React from 'react'; +import { parseDiscordEmoji } from '../../utils/utils'; +import DiscordSelectMenu from './components/Select Menu'; +import DiscordContainer from './components/Container'; +import DiscordSection from './components/section/Section'; +import DiscordMediaGallery from './components/Media Gallery'; +import DiscordSeperator from './components/Spacing'; +import MessageContent from './content'; +import { RenderType } from './content'; +import type { RenderMessageContext } from '..'; + +export default function ComponentRow({ component, id, context }: { component: TopLevelComponent; id: number; context: RenderMessageContext }) { + if (component.type === ComponentType.ActionRow) { + return ( + + <> + {component.components.map((nestedComponent, id) => ( + + ))} + + + ); + } + + if (component.type === ComponentType.Container) { + return ( + + <> + {component.components.map((nestedComponent, id) => ( + + ))} + + + ); + } + + if (component.type === ComponentType.File) { + return ( + <> + {component.spoiler ? + + + + : + + } + + ); + } + + if (component.type === ComponentType.MediaGallery) { + return ( + + ); + } + + if (component.type === ComponentType.Section) { + return ( + + {component.components.map((nestedComponent, id) => ( + + ))} + + ); + } + + if (component.type === ComponentType.Separator) { + return ( + + ); + } + + if (component.type === ComponentType.TextDisplay) { + return ( + + ); + } +} + +const ButtonStyleMapping = { + [ButtonStyle.Primary]: 'primary', + [ButtonStyle.Secondary]: 'secondary', + [ButtonStyle.Success]: 'success', + [ButtonStyle.Danger]: 'destructive', + [ButtonStyle.Link]: 'secondary', +} as const; + +export function Component({ component, id }: { component: MessageActionRowComponent | ThumbnailComponent; id: number }) { + if (component.type === ComponentType.Button) { + return ( + + {component.label} + + ); + } + + if (component.type === ComponentType.StringSelect || + component.type === ComponentType.UserSelect || + component.type === ComponentType.RoleSelect || + component.type === ComponentType.MentionableSelect || + component.type === ComponentType.ChannelSelect) { + return ( + + ); + } + + return undefined; +} diff --git a/src/generator/renderers/components/Container.tsx b/src/generator/renderers/components/Container.tsx new file mode 100644 index 00000000..ec013a8c --- /dev/null +++ b/src/generator/renderers/components/Container.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +const DiscordContainer: React.FC<{ children: React.ReactNode; }> = ({ children }) => { + return ( +
+ {children} +
+ ); +} + +export default DiscordContainer; \ No newline at end of file diff --git a/src/generator/renderers/components/Media Gallery.tsx b/src/generator/renderers/components/Media Gallery.tsx new file mode 100644 index 00000000..703f84c6 --- /dev/null +++ b/src/generator/renderers/components/Media Gallery.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import type { MediaGalleryComponent } from 'discord.js'; +import { getGalleryLayout, getImageStyle } from './utils'; + + +const DiscordMediaGallery: React.FC<{ component: MediaGalleryComponent }> = ({ component }) => { + if (!component.items || component.items.length === 0) { + return null; + } + + const count = component.items.length; + const imagesToShow = component.items.slice(0, 10); + const hasMore = component.items.length > 10; + + return ( +
+ {imagesToShow.map((media, idx) => ( +
+ {media.description + {hasMore && idx === imagesToShow.length - 1 && ( +
+ +{component.items.length - 10} +
+ )} +
+ ))} +
+ ); +}; + +export default DiscordMediaGallery; diff --git a/src/generator/renderers/components/Select Menu.tsx b/src/generator/renderers/components/Select Menu.tsx new file mode 100644 index 00000000..94e63adc --- /dev/null +++ b/src/generator/renderers/components/Select Menu.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { type MessageActionRowComponent, ComponentType } from 'discord.js'; +import { parseDiscordEmoji } from '../../../utils/utils'; +import { getSelectTypeLabel } from './utils'; +import { selectMenuStyle } from './styles'; + +const DiscordSelectMenu: React.FC<{ component: Exclude }> = ({ component }) => { + const isStringSelect = component.type === ComponentType.StringSelect; + const placeholder = component.placeholder || getSelectTypeLabel(component.type); + + return ( +
+
{placeholder}
+
+ + + +
+ {isStringSelect && component.options && component.options.length > 0 && ( +
+ {component.options.map((option, idx) => ( +
+ {option.emoji && {parseDiscordEmoji(option.emoji)}} + {option.label} +
+ ))} +
+ )} +
+ ); +}; + +export default DiscordSelectMenu; diff --git a/src/generator/renderers/components/Spacing.tsx b/src/generator/renderers/components/Spacing.tsx new file mode 100644 index 00000000..4259f09a --- /dev/null +++ b/src/generator/renderers/components/Spacing.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { SeparatorSpacingSize } from "discord.js"; + +const DiscordSeperator: React.FC<{ divider: boolean; spacing: SeparatorSpacingSize; }> = ({ divider, spacing }) => { + return ( +
+ ); +} + +export default DiscordSeperator; \ No newline at end of file diff --git a/src/generator/renderers/components/section/Section.tsx b/src/generator/renderers/components/section/Section.tsx new file mode 100644 index 00000000..4e12c032 --- /dev/null +++ b/src/generator/renderers/components/section/Section.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import type { ButtonComponent, ThumbnailComponent } from 'discord.js'; +import { Component } from '../../components'; +import SectionContent from './SectionContent'; +import SectionAccessory from './SectionAccessory'; + +interface DiscordSectionProps { + children: React.ReactNode; + accessory?: ButtonComponent | ThumbnailComponent; + id: number; +} + +const DiscordSection: React.FC = ({ children, accessory, id }) => { + return ( +
+ {children} + + {accessory && } + +
+ ); +}; + +export default DiscordSection; diff --git a/src/generator/renderers/components/section/SectionAccessory.tsx b/src/generator/renderers/components/section/SectionAccessory.tsx new file mode 100644 index 00000000..27602937 --- /dev/null +++ b/src/generator/renderers/components/section/SectionAccessory.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +interface SectionAccessoryProps { + children?: React.ReactNode; +} + +const SectionAccessory: React.FC = ({ children }) => { + if (!children) return null; + + return ( +
+ {children} +
+ ); +}; + +export default SectionAccessory; diff --git a/src/generator/renderers/components/section/SectionContent.tsx b/src/generator/renderers/components/section/SectionContent.tsx new file mode 100644 index 00000000..ac5a038a --- /dev/null +++ b/src/generator/renderers/components/section/SectionContent.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +interface SectionContentProps { + children: React.ReactNode; +} + +const SectionContent: React.FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export default SectionContent; diff --git a/src/generator/renderers/components/styles.ts b/src/generator/renderers/components/styles.ts new file mode 100644 index 00000000..5cb271b4 --- /dev/null +++ b/src/generator/renderers/components/styles.ts @@ -0,0 +1,83 @@ +import type { CSSProperties } from 'react'; +import { ButtonStyle } from 'discord.js'; + +// Container styles +export const containerStyle: CSSProperties = { + display: 'grid', + gap: '4px', + width: '100%', + maxWidth: '500px', + borderRadius: '8px', + overflow: 'hidden', +}; + +// Base image style +export const baseImageStyle: CSSProperties = { + overflow: 'hidden', + position: 'relative', + background: '#2b2d31', +}; + +// Button style mapping +export const ButtonStyleMapping = { + [ButtonStyle.Primary]: 'primary', + [ButtonStyle.Secondary]: 'secondary', + [ButtonStyle.Success]: 'success', + [ButtonStyle.Danger]: 'destructive', + [ButtonStyle.Link]: 'secondary', +} as const; + +// Get button style based on type +export const getButtonStyle = (type: string): CSSProperties => ({ + backgroundColor: + type === 'primary' + ? 'hsl(234.935 calc(1*85.556%) 64.706% /1)' + : type === 'secondary' + ? 'hsl(240 calc(1*4%) 60.784% /0.12156862745098039)' + : type === 'success' + ? 'hsl(145.97 calc(1*100%) 26.275% /1)' + : type === 'destructive' + ? 'hsl(355.636 calc(1*64.706%) 50% /1)' + : '#2b2d31', + color: '#ffffff', + padding: '2px 16px', + borderRadius: '8px', + textDecoration: 'none', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '14px', + fontWeight: '500', + height: '32px', + minHeight: '32px', + minWidth: '60px', + cursor: 'pointer', + fontFamily: 'Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif', + textAlign: 'center', + boxSizing: 'border-box', + border: 'none', + outline: 'none', + transition: 'background-color 0.2s ease', +}); + +// Select menu styles +export const selectMenuStyle: CSSProperties = { + marginTop: '2px', + marginBottom: '2px', + position: 'relative', + width: '100%', + maxWidth: '500px', + height: '40px', + backgroundColor: '#2b2d31', + borderRadius: '4px', + color: '#b5bac1', + cursor: 'pointer', + fontFamily: 'Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif', + fontSize: '14px', + display: 'flex', + alignItems: 'center', + padding: '0 8px', + justifyContent: 'space-between', + boxSizing: 'border-box', + border: '1px solid #1e1f22', +}; diff --git a/src/generator/renderers/components/utils.ts b/src/generator/renderers/components/utils.ts new file mode 100644 index 00000000..48e773ca --- /dev/null +++ b/src/generator/renderers/components/utils.ts @@ -0,0 +1,170 @@ +import { ComponentType } from 'discord.js'; +import type { CSSProperties } from 'react'; +import { baseImageStyle, containerStyle } from './styles'; + +/** + * Gets the appropriate label for different select menu types + */ +export const getSelectTypeLabel = (type: ComponentType): string => { + switch (type) { + case ComponentType.UserSelect: + return 'Select User'; + case ComponentType.RoleSelect: + return 'Select Role'; + case ComponentType.MentionableSelect: + return 'Select Mentionable'; + case ComponentType.ChannelSelect: + return 'Select Channel'; + case ComponentType.StringSelect: + return 'Make a Selection'; + default: + return 'Select Option'; + } +}; + +/** + * Gets the grid layout for media galleries based on count + */ +export const getGalleryLayout = (count: number): CSSProperties => { + if (count === 1) { + return { + ...containerStyle, + gridTemplateColumns: '1fr', + gridTemplateRows: 'auto', + }; + } else if (count === 2) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: 'auto', + }; + } else if (count === 3 || count === 4) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: '1fr 1fr', + }; + } else if (count === 5) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto auto', + }; + } else if (count >= 7) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto auto auto auto', + }; + } else { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto', + }; + } +}; + +/** + * Gets the style for an individual image based on its position and total count + */ +export const getImageStyle = (idx: number, count: number): CSSProperties => { + if (count === 3 && idx === 0) { + return { + ...baseImageStyle, + gridRow: '1 / span 2', + gridColumn: '1', + aspectRatio: '1/2', + }; + } + + if (count === 5) { + if (idx < 2) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: idx === 0 ? '1 / span 2' : '3', + }; + } else { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 2 + 1}`, + }; + } + } + + if (count === 7) { + if (idx === 0) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: '1 / span 3', + }; + } else if (idx <= 3) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 0}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 3}`, + }; + } + } + + if (count === 8) { + if (idx < 2) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: idx === 0 ? '1 / span 2' : '3', + }; + } else if (idx < 5) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 2 + 1}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 5 + 1}`, + }; + } + } + + if (count === 10) { + if (idx === 0) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: '1 / span 3', + }; + } else if (idx <= 3) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 0}`, + }; + } else if (idx <= 6) { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 3}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '4', + gridColumn: `${idx - 6}`, + }; + } + } + + return baseImageStyle; +}; diff --git a/src/generator/renderers/content.tsx b/src/generator/renderers/content.tsx index b09ca692..25af2ef3 100644 --- a/src/generator/renderers/content.tsx +++ b/src/generator/renderers/content.tsx @@ -1,261 +1,261 @@ -import { - DiscordBold, - DiscordCodeBlock, - DiscordCustomEmoji, - DiscordInlineCode, - DiscordItalic, - DiscordMention, - DiscordQuote, - DiscordSpoiler, - DiscordTime, - DiscordUnderlined, -} from '@derockdev/discord-components-react'; -import parse, { type RuleTypesExtended } from 'discord-markdown-parser'; -import { ChannelType, type APIMessageComponentEmoji } from 'discord.js'; -import React from 'react'; -import type { ASTNode } from 'simple-markdown'; -import { ASTNode as MessageASTNodes } from 'simple-markdown'; -import type { SingleASTNode } from 'simple-markdown'; -import type { RenderMessageContext } from '../'; -import { parseDiscordEmoji } from '../../utils/utils'; - -export enum RenderType { - EMBED, - REPLY, - NORMAL, - WEBHOOK, -} - -type RenderContentContext = RenderMessageContext & { - type: RenderType; - - _internal?: { - largeEmojis?: boolean; - }; -}; - -/** - * Renders discord markdown content - * @param content - The content to render - * @param context - The context to render the content in - * @returns - */ -export default async function MessageContent({ content, context }: { content: string; context: RenderContentContext }) { - if (context.type === RenderType.REPLY && content.length > 180) content = content.slice(0, 180) + '...'; - - // parse the markdown - const parsed = parse( - content, - context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal' - ); - - // check if the parsed content is only emojis - const isOnlyEmojis = parsed.every( - (node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0) - ); - if (isOnlyEmojis) { - // now check if there are less than or equal to 25 emojis - const emojis = parsed.filter((node) => ['emoji', 'twemoji'].includes(node.type)); - if (emojis.length <= 25) { - context._internal = { - largeEmojis: true, - }; - } - } - - return ; -} - -// This function can probably be combined into the MessageSingleASTNode function -async function MessageASTNodes({ - nodes, - context, -}: { - nodes: ASTNode; - context: RenderContentContext; -}): Promise { - if (Array.isArray(nodes)) { - return ( - <> - {nodes.map((node, i) => ( - - ))} - - ); - } else { - return ; - } -} - -export async function MessageSingleASTNode({ node, context }: { node: SingleASTNode; context: RenderContentContext }) { - if (!node) return null; - - const type = node.type as RuleTypesExtended; - - switch (type) { - case 'text': - return node.content; - - case 'link': - return ( - - - - ); - - case 'url': - case 'autolink': - return ( - - - - ); - - case 'blockQuote': - if (context.type === RenderType.REPLY) { - return ; - } - - return ( - - - - ); - - case 'br': - case 'newline': - if (context.type === RenderType.REPLY) return ' '; - return
; - - case 'channel': { - const id = node.id as string; - const channel = await context.callbacks.resolveChannel(id); - - return ( - - {channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `<#${id}>`} - - ); - } - - case 'role': { - const id = node.id as string; - const role = await context.callbacks.resolveRole(id); - - return ( - - {role ? role.name : `<@&${id}>`} - - ); - } - - case 'user': { - const id = node.id as string; - const user = await context.callbacks.resolveUser(id); - - return {user ? user.displayName ?? user.username : `<@${id}>`}; - } - - case 'here': - case 'everyone': - return ( - - {`@${type}`} - - ); - - case 'codeBlock': - if (context.type !== RenderType.REPLY) { - return ; - } - return {node.content}; - - case 'inlineCode': - return {node.content}; - - case 'em': - return ( - - - - ); - - case 'strong': - return ( - - - - ); - - case 'underline': - return ( - - - - ); - - case 'strikethrough': - return ( - - - - ); - - case 'emoticon': - return typeof node.content === 'string' ? ( - node.content - ) : ( - - ); - - case 'spoiler': - return ( - - - - ); - - case 'emoji': - case 'twemoji': - return ( - - ); - - case 'timestamp': - return ; - - default: { - console.log(`Unknown node type: ${type}`, node); - return typeof node.content === 'string' ? ( - node.content - ) : ( - - ); - } - } -} - -export function getChannelType(channelType: ChannelType): 'channel' | 'voice' | 'thread' | 'forum' { - switch (channelType) { - case ChannelType.GuildCategory: - case ChannelType.GuildAnnouncement: - case ChannelType.GuildText: - return 'channel'; - case ChannelType.GuildVoice: - case ChannelType.GuildStageVoice: - return 'voice'; - case ChannelType.PublicThread: - case ChannelType.PrivateThread: - case ChannelType.AnnouncementThread: - return 'thread'; - case ChannelType.GuildForum: - return 'forum'; - default: - return 'channel'; - } -} +import { + DiscordBold, + DiscordCodeBlock, + DiscordCustomEmoji, + DiscordInlineCode, + DiscordItalic, + DiscordMention, + DiscordQuote, + DiscordSpoiler, + DiscordTime, + DiscordUnderlined, +} from '@derockdev/discord-components-react'; +import parse, { type RuleTypesExtended } from 'discord-markdown-parser'; +import { ChannelType, type APIMessageComponentEmoji } from 'discord.js'; +import React from 'react'; +import type { ASTNode } from 'simple-markdown'; +import { ASTNode as MessageASTNodes } from 'simple-markdown'; +import type { SingleASTNode } from 'simple-markdown'; +import type { RenderMessageContext } from '../'; +import { parseDiscordEmoji } from '../../utils/utils'; + +export enum RenderType { + EMBED, + REPLY, + NORMAL, + WEBHOOK, +} + +type RenderContentContext = RenderMessageContext & { + type: RenderType; + + _internal?: { + largeEmojis?: boolean; + }; +}; + +/** + * Renders discord markdown content + * @param content - The content to render + * @param context - The context to render the content in + * @returns + */ +export default async function MessageContent({ content, context }: { content: string; context: RenderContentContext }) { + if (context.type === RenderType.REPLY && content.length > 180) content = content.slice(0, 180) + '...'; + + // parse the markdown + const parsed = parse( + content, + context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal' + ); + + // check if the parsed content is only emojis + const isOnlyEmojis = parsed.every( + (node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0) + ); + if (isOnlyEmojis) { + // now check if there are less than or equal to 25 emojis + const emojis = parsed.filter((node) => ['emoji', 'twemoji'].includes(node.type)); + if (emojis.length <= 25) { + context._internal = { + largeEmojis: true, + }; + } + } + + return ; +} + +// This function can probably be combined into the MessageSingleASTNode function +async function MessageASTNodes({ + nodes, + context, +}: { + nodes: ASTNode; + context: RenderContentContext; +}): Promise { + if (Array.isArray(nodes)) { + return ( + <> + {nodes.map((node, i) => ( + + ))} + + ); + } else { + return ; + } +} + +export async function MessageSingleASTNode({ node, context }: { node: SingleASTNode; context: RenderContentContext }) { + if (!node) return null; + + const type = node.type as RuleTypesExtended; + + switch (type) { + case 'text': + return node.content; + + case 'link': + return ( + + + + ); + + case 'url': + case 'autolink': + return ( + + + + ); + + case 'blockQuote': + if (context.type === RenderType.REPLY) { + return ; + } + + return ( + + + + ); + + case 'br': + case 'newline': + if (context.type === RenderType.REPLY) return ' '; + return
; + + case 'channel': { + const id = node.id as string; + const channel = await context.callbacks.resolveChannel(id); + + return ( + + {channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `<#${id}>`} + + ); + } + + case 'role': { + const id = node.id as string; + const role = await context.callbacks.resolveRole(id); + + return ( + + {role ? role.name : `<@&${id}>`} + + ); + } + + case 'user': { + const id = node.id as string; + const user = await context.callbacks.resolveUser(id); + + return {user ? user.displayName ?? user.username : `<@${id}>`}; + } + + case 'here': + case 'everyone': + return ( + + {`@${type}`} + + ); + + case 'codeBlock': + if (context.type !== RenderType.REPLY) { + return ; + } + return {node.content}; + + case 'inlineCode': + return {node.content}; + + case 'em': + return ( + + + + ); + + case 'strong': + return ( + + + + ); + + case 'underline': + return ( + + + + ); + + case 'strikethrough': + return ( + + + + ); + + case 'emoticon': + return typeof node.content === 'string' ? ( + node.content + ) : ( + + ); + + case 'spoiler': + return ( + + + + ); + + case 'emoji': + case 'twemoji': + return ( + + ); + + case 'timestamp': + return ; + + default: { + console.log(`Unknown node type: ${type}`, node); + return typeof node.content === 'string' ? ( + node.content + ) : ( + + ); + } + } +} + +export function getChannelType(channelType: ChannelType): 'channel' | 'voice' | 'thread' | 'forum' { + switch (channelType) { + case ChannelType.GuildCategory: + case ChannelType.GuildAnnouncement: + case ChannelType.GuildText: + return 'channel'; + case ChannelType.GuildVoice: + case ChannelType.GuildStageVoice: + return 'voice'; + case ChannelType.PublicThread: + case ChannelType.PrivateThread: + case ChannelType.AnnouncementThread: + return 'thread'; + case ChannelType.GuildForum: + return 'forum'; + default: + return 'channel'; + } +} diff --git a/src/generator/renderers/embed.tsx b/src/generator/renderers/embed.tsx index 433d2b9b..0daa42f9 100644 --- a/src/generator/renderers/embed.tsx +++ b/src/generator/renderers/embed.tsx @@ -1,68 +1,68 @@ -import { - DiscordEmbed as DiscordEmbedComponent, - DiscordEmbedDescription, - DiscordEmbedField, - DiscordEmbedFields, - DiscordEmbedFooter, -} from '@derockdev/discord-components-react'; -import type { Embed, Message } from 'discord.js'; -import React from 'react'; -import type { RenderMessageContext } from '..'; -import { calculateInlineIndex } from '../../utils/embeds'; -import MessageContent, { RenderType } from './content'; - -type RenderEmbedContext = RenderMessageContext & { - index: number; - message: Message; -}; - -export async function DiscordEmbed({ embed, context }: { embed: Embed; context: RenderEmbedContext }) { - return ( - - {/* Description */} - {embed.description && ( - - - - )} - - {/* Fields */} - {embed.fields.length > 0 && ( - - {embed.fields.map(async (field, id) => ( - - - - ))} - - )} - - {/* Footer */} - {embed.footer && ( - - {embed.footer.text} - - )} - - ); -} +import { + DiscordEmbed as DiscordEmbedComponent, + DiscordEmbedDescription, + DiscordEmbedField, + DiscordEmbedFields, + DiscordEmbedFooter, +} from '@derockdev/discord-components-react'; +import type { Embed, Message } from 'discord.js'; +import React from 'react'; +import type { RenderMessageContext } from '..'; +import { calculateInlineIndex } from '../../utils/embeds'; +import MessageContent, { RenderType } from './content'; + +type RenderEmbedContext = RenderMessageContext & { + index: number; + message: Message; +}; + +export async function DiscordEmbed({ embed, context }: { embed: Embed; context: RenderEmbedContext }) { + return ( + + {/* Description */} + {embed.description && ( + + + + )} + + {/* Fields */} + {embed.fields.length > 0 && ( + + {embed.fields.map(async (field, id) => ( + + + + ))} + + )} + + {/* Footer */} + {embed.footer && ( + + {embed.footer.text} + + )} + + ); +} diff --git a/src/generator/renderers/message.tsx b/src/generator/renderers/message.tsx index 90f478ff..b4698a8f 100644 --- a/src/generator/renderers/message.tsx +++ b/src/generator/renderers/message.tsx @@ -1,122 +1,122 @@ -import { - DiscordAttachments, - DiscordCommand, - DiscordMessage as DiscordMessageComponent, - DiscordReaction, - DiscordReactions, - DiscordThread, - DiscordThreadMessage, -} from '@derockdev/discord-components-react'; -import type { Message as MessageType } from 'discord.js'; -import React from 'react'; -import type { RenderMessageContext } from '..'; -import { parseDiscordEmoji } from '../../utils/utils'; -import { Attachments } from './attachment'; -import ComponentRow from './components'; -import MessageContent, { RenderType } from './content'; -import { DiscordEmbed } from './embed'; -import MessageReply from './reply'; -import DiscordSystemMessage from './systemMessage'; - -export default async function DiscordMessage({ - message, - context, -}: { - message: MessageType; - context: RenderMessageContext; -}) { - if (message.system) return ; - - const isCrosspost = message.reference && message.reference.guildId !== message.guild?.id; - - return ( - - {/* reply */} - - - {/* slash command */} - {message.interaction && ( - - )} - - {/* message content */} - {message.content && ( - - )} - - {/* attachments */} - - - {/* message embeds */} - {message.embeds.map((embed, id) => ( - - ))} - - {/* components */} - {message.components.length > 0 && ( - - {message.components.map((component, id) => ( - - ))} - - )} - - {/* reactions */} - {message.reactions.cache.size > 0 && ( - - {message.reactions.cache.map((reaction, id) => ( - - ))} - - )} - - {/* threads */} - {message.hasThread && message.thread && ( - 1 ? 's' : ''}` - : 'View Thread' - } - > - {message.thread.lastMessage ? ( - - 128 - ? message.thread.lastMessage.content.substring(0, 125) + '...' - : message.thread.lastMessage.content - } - context={{ ...context, type: RenderType.REPLY }} - /> - - ) : ( - `Thread messages not saved.` - )} - - )} - - ); -} +import { + DiscordAttachments, + DiscordCommand, + DiscordMessage as DiscordMessageComponent, + DiscordReaction, + DiscordReactions, + DiscordThread, + DiscordThreadMessage, +} from '@derockdev/discord-components-react'; +import type { Message as MessageType } from 'discord.js'; +import React from 'react'; +import type { RenderMessageContext } from '..'; +import { parseDiscordEmoji } from '../../utils/utils'; +import { Attachments } from './attachment'; +import ComponentRow from './components'; +import MessageContent, { RenderType } from './content'; +import { DiscordEmbed } from './embed'; +import MessageReply from './reply'; +import DiscordSystemMessage from './systemMessage'; + +export default async function DiscordMessage({ + message, + context, +}: { + message: MessageType; + context: RenderMessageContext; +}) { + if (message.system) return ; + + const isCrosspost = message.reference && message.reference.guildId !== message.guild?.id; + + return ( + + {/* reply */} + + + {/* slash command */} + {message.interaction && ( + + )} + + {/* message content */} + {message.content && ( + + )} + + {/* attachments */} + + + {/* message embeds */} + {message.embeds.map((embed, id) => ( + + ))} + + {/* components */} + {message.components.length > 0 && ( + + {message.components.map((component, id) => ( + + ))} + + )} + + {/* reactions */} + {message.reactions.cache.size > 0 && ( + + {message.reactions.cache.map((reaction, id) => ( + + ))} + + )} + + {/* threads */} + {message.hasThread && message.thread && ( + 1 ? 's' : ''}` + : 'View Thread' + } + > + {message.thread.lastMessage ? ( + + 128 + ? message.thread.lastMessage.content.substring(0, 125) + '...' + : message.thread.lastMessage.content + } + context={{ ...context, type: RenderType.REPLY }} + /> + + ) : ( + `Thread messages not saved.` + )} + + )} + + ); +} diff --git a/src/generator/renderers/reply.tsx b/src/generator/renderers/reply.tsx index 0d2016be..c71e3276 100644 --- a/src/generator/renderers/reply.tsx +++ b/src/generator/renderers/reply.tsx @@ -1,45 +1,45 @@ -import { DiscordReply } from '@derockdev/discord-components-react'; -import { type Message, UserFlags } from 'discord.js'; -import type { RenderMessageContext } from '..'; -import React from 'react'; -import MessageContent, { RenderType } from './content'; - -export default async function MessageReply({ message, context }: { message: Message; context: RenderMessageContext }) { - if (!message.reference) return null; - if (message.reference.guildId !== message.guild?.id) return null; - - const referencedMessage = context.messages.find((m) => m.id === message.reference!.messageId); - - if (!referencedMessage) return Message could not be loaded.; - - const isCrosspost = referencedMessage.reference && referencedMessage.reference.guildId !== message.guild?.id; - const isCommand = referencedMessage.interaction !== null; - - return ( - 0} - author={ - referencedMessage.member?.nickname ?? referencedMessage.author.displayName ?? referencedMessage.author.username - } - avatar={referencedMessage.author.avatarURL({ size: 32 }) ?? undefined} - roleColor={referencedMessage.member?.displayHexColor ?? undefined} - bot={!isCrosspost && referencedMessage.author.bot} - verified={referencedMessage.author.flags?.has(UserFlags.VerifiedBot)} - op={message?.channel?.isThread?.() && referencedMessage.author.id === message?.channel?.ownerId} - server={isCrosspost ?? undefined} - command={isCommand} - > - {referencedMessage.content ? ( - - - - ) : isCommand ? ( - Click to see command. - ) : ( - Click to see attachment. - )} - - ); -} +import { DiscordReply } from '@derockdev/discord-components-react'; +import { type Message, UserFlags } from 'discord.js'; +import type { RenderMessageContext } from '..'; +import React from 'react'; +import MessageContent, { RenderType } from './content'; + +export default async function MessageReply({ message, context }: { message: Message; context: RenderMessageContext }) { + if (!message.reference) return null; + if (message.reference.guildId !== message.guild?.id) return null; + + const referencedMessage = context.messages.find((m) => m.id === message.reference!.messageId); + + if (!referencedMessage) return Message could not be loaded.; + + const isCrosspost = referencedMessage.reference && referencedMessage.reference.guildId !== message.guild?.id; + const isCommand = referencedMessage.interaction !== null; + + return ( + 0} + author={ + referencedMessage.member?.nickname ?? referencedMessage.author.displayName ?? referencedMessage.author.username + } + avatar={referencedMessage.author.avatarURL({ size: 32 }) ?? undefined} + roleColor={referencedMessage.member?.displayHexColor ?? undefined} + bot={!isCrosspost && referencedMessage.author.bot} + verified={referencedMessage.author.flags?.has(UserFlags.VerifiedBot)} + op={message?.channel?.isThread?.() && referencedMessage.author.id === message?.channel?.ownerId} + server={isCrosspost ?? undefined} + command={isCommand} + > + {referencedMessage.content ? ( + + + + ) : isCommand ? ( + Click to see command. + ) : ( + Click to see attachment. + )} + + ); +} diff --git a/src/generator/renderers/systemMessage.tsx b/src/generator/renderers/systemMessage.tsx index 75760700..66e64164 100644 --- a/src/generator/renderers/systemMessage.tsx +++ b/src/generator/renderers/systemMessage.tsx @@ -1,123 +1,123 @@ -import { DiscordReaction, DiscordReactions, DiscordSystemMessage } from '@derockdev/discord-components-react'; -import { MessageType, type GuildMember, type Message, type User } from 'discord.js'; -import React from 'react'; -import { parseDiscordEmoji } from '../../utils/utils'; - -export default async function SystemMessage({ message }: { message: Message }) { - switch (message.type) { - case MessageType.RecipientAdd: - case MessageType.UserJoin: - return ( - - - - ); - - case MessageType.ChannelPinnedMessage: - return ( - - - {message.author.displayName ?? message.author.username} - {' '} - pinned a message to this channel. - {/* reactions */} - {message.reactions.cache.size > 0 && ( - - {message.reactions.cache.map((reaction, id) => ( - - ))} - - )} - - ); - - case MessageType.GuildBoost: - case MessageType.GuildBoostTier1: - case MessageType.GuildBoostTier2: - case MessageType.GuildBoostTier3: - return ( - - - {message.author.displayName ?? message.author.username} - {' '} - boosted the server! - - ); - - case MessageType.ThreadStarterMessage: - return ( - - - {message.author.displayName ?? message.author.username} - {' '} - started a thread: {message.content} - - ); - - default: - return undefined; - } -} - -export function Highlight({ children, color }: { children: React.ReactNode; color?: string }) { - return {children}; -} - -const allJoinMessages = [ - '{user} just joined the server - glhf!', - '{user} just joined. Everyone, look busy!', - '{user} just joined. Can I get a heal?', - '{user} joined your party.', - '{user} joined. You must construct additional pylons.', - 'Ermagherd. {user} is here.', - 'Welcome, {user}. Stay awhile and listen.', - 'Welcome, {user}. We were expecting you ( ͡° ͜ʖ ͡°)', - 'Welcome, {user}. We hope you brought pizza.', - 'Welcome {user}. Leave your weapons by the door.', - 'A wild {user} appeared.', - 'Swoooosh. {user} just landed.', - 'Brace yourselves {user} just joined the server.', - '{user} just joined. Hide your bananas.', - '{user} just arrived. Seems OP - please nerf.', - '{user} just slid into the server.', - 'A {user} has spawned in the server.', - 'Big {user} showed up!', - "Where's {user}? In the server!", - '{user} hopped into the server. Kangaroo!!', - '{user} just showed up. Hold my beer.', - 'Challenger approaching - {user} has appeared!', - "It's a bird! It's a plane! Nevermind, it's just {user}.", - "It's {user}! Praise the sun! \\\\[T]/", - 'Never gonna give {user} up. Never gonna let {user} down.', - 'Ha! {user} has joined! You activated my trap card!', - 'Cheers, love! {user} is here!', - 'Hey! Listen! {user} has joined!', - "We've been expecting you {user}", - "It's dangerous to go alone, take {user}!", - "{user} has joined the server! It's super effective!", - 'Cheers, love! {user} is here!', - '{user} is here, as the prophecy foretold.', - "{user} has arrived. Party's over.", - 'Ready player {user}', - '{user} is here to kick butt and chew bubblegum. And {user} is all out of gum.', - "Hello. Is it {user} you're looking for?", -]; - -export function JoinMessage({ member, fallbackUser }: { member: GuildMember | null; fallbackUser: User }) { - const randomMessage = allJoinMessages[Math.floor(Math.random() * allJoinMessages.length)]; - - return randomMessage - .split('{user}') - .flatMap((item, i) => [ - item, - - {member?.nickname ?? fallbackUser.displayName ?? fallbackUser.username} - , - ]) - .slice(0, -1); -} +import { DiscordReaction, DiscordReactions, DiscordSystemMessage } from '@derockdev/discord-components-react'; +import { MessageType, type GuildMember, type Message, type User } from 'discord.js'; +import React from 'react'; +import { parseDiscordEmoji } from '../../utils/utils'; + +export default async function SystemMessage({ message }: { message: Message }) { + switch (message.type) { + case MessageType.RecipientAdd: + case MessageType.UserJoin: + return ( + + + + ); + + case MessageType.ChannelPinnedMessage: + return ( + + + {message.author.displayName ?? message.author.username} + {' '} + pinned a message to this channel. + {/* reactions */} + {message.reactions.cache.size > 0 && ( + + {message.reactions.cache.map((reaction, id) => ( + + ))} + + )} + + ); + + case MessageType.GuildBoost: + case MessageType.GuildBoostTier1: + case MessageType.GuildBoostTier2: + case MessageType.GuildBoostTier3: + return ( + + + {message.author.displayName ?? message.author.username} + {' '} + boosted the server! + + ); + + case MessageType.ThreadStarterMessage: + return ( + + + {message.author.displayName ?? message.author.username} + {' '} + started a thread: {message.content} + + ); + + default: + return undefined; + } +} + +export function Highlight({ children, color }: { children: React.ReactNode; color?: string }) { + return {children}; +} + +const allJoinMessages = [ + '{user} just joined the server - glhf!', + '{user} just joined. Everyone, look busy!', + '{user} just joined. Can I get a heal?', + '{user} joined your party.', + '{user} joined. You must construct additional pylons.', + 'Ermagherd. {user} is here.', + 'Welcome, {user}. Stay awhile and listen.', + 'Welcome, {user}. We were expecting you ( ͡° ͜ʖ ͡°)', + 'Welcome, {user}. We hope you brought pizza.', + 'Welcome {user}. Leave your weapons by the door.', + 'A wild {user} appeared.', + 'Swoooosh. {user} just landed.', + 'Brace yourselves {user} just joined the server.', + '{user} just joined. Hide your bananas.', + '{user} just arrived. Seems OP - please nerf.', + '{user} just slid into the server.', + 'A {user} has spawned in the server.', + 'Big {user} showed up!', + "Where's {user}? In the server!", + '{user} hopped into the server. Kangaroo!!', + '{user} just showed up. Hold my beer.', + 'Challenger approaching - {user} has appeared!', + "It's a bird! It's a plane! Nevermind, it's just {user}.", + "It's {user}! Praise the sun! \\\\[T]/", + 'Never gonna give {user} up. Never gonna let {user} down.', + 'Ha! {user} has joined! You activated my trap card!', + 'Cheers, love! {user} is here!', + 'Hey! Listen! {user} has joined!', + "We've been expecting you {user}", + "It's dangerous to go alone, take {user}!", + "{user} has joined the server! It's super effective!", + 'Cheers, love! {user} is here!', + '{user} is here, as the prophecy foretold.', + "{user} has arrived. Party's over.", + 'Ready player {user}', + '{user} is here to kick butt and chew bubblegum. And {user} is all out of gum.', + "Hello. Is it {user} you're looking for?", +]; + +export function JoinMessage({ member, fallbackUser }: { member: GuildMember | null; fallbackUser: User }) { + const randomMessage = allJoinMessages[Math.floor(Math.random() * allJoinMessages.length)]; + + return randomMessage + .split('{user}') + .flatMap((item, i) => [ + item, + + {member?.nickname ?? fallbackUser.displayName ?? fallbackUser.username} + , + ]) + .slice(0, -1); +} diff --git a/src/generator/transcript.tsx b/src/generator/transcript.tsx index 7a03c2d0..070cc2f6 100644 --- a/src/generator/transcript.tsx +++ b/src/generator/transcript.tsx @@ -1,72 +1,72 @@ -import { DiscordHeader, DiscordMessages as DiscordMessagesComponent } from '@derockdev/discord-components-react'; -import { ChannelType } from 'discord.js'; -import React from 'react'; -import type { RenderMessageContext } from '.'; -import MessageContent, { RenderType } from './renderers/content'; -import DiscordMessage from './renderers/message'; - -/** - * The core transcript component. - * Expects window.$discordMessage.profiles to be set for profile information. - * - * @param props Messages, channel details, callbacks, etc. - * @returns - */ -export default async function DiscordMessages({ messages, channel, callbacks, ...options }: RenderMessageContext) { - return ( - - {/* header */} - - {channel.isThread() ? ( - `Thread channel in ${channel.parent?.name ?? 'Unknown Channel'}` - ) : channel.isDMBased() ? ( - `Direct Messages` - ) : channel.isVoiceBased() ? ( - `Voice Text Channel for ${channel.name}` - ) : channel.type === ChannelType.GuildCategory ? ( - `Category Channel` - ) : 'topic' in channel && channel.topic ? ( - - ) : ( - `This is the start of #${channel.name} channel.` - )} - - - {/* body */} - {messages.map((message) => ( - - ))} - - {/* footer */} -
- {options.footerText - ? options.footerText - .replaceAll('{number}', messages.length.toString()) - .replaceAll('{s}', messages.length > 1 ? 's' : '') - : `Exported ${messages.length} message${messages.length > 1 ? 's' : ''}.`}{' '} - {options.poweredBy ? ( - - Powered by{' '} - - discord-html-transcripts - - . - - ) : null} -
-
- ); -} +import { DiscordHeader, DiscordMessages as DiscordMessagesComponent } from '@derockdev/discord-components-react'; +import { ChannelType } from 'discord.js'; +import React from 'react'; +import type { RenderMessageContext } from '.'; +import MessageContent, { RenderType } from './renderers/content'; +import DiscordMessage from './renderers/message'; + +/** + * The core transcript component. + * Expects window.$discordMessage.profiles to be set for profile information. + * + * @param props Messages, channel details, callbacks, etc. + * @returns + */ +export default async function DiscordMessages({ messages, channel, callbacks, ...options }: RenderMessageContext) { + return ( + + {/* header */} + + {channel.isThread() ? ( + `Thread channel in ${channel.parent?.name ?? 'Unknown Channel'}` + ) : channel.isDMBased() ? ( + `Direct Messages` + ) : channel.isVoiceBased() ? ( + `Voice Text Channel for ${channel.name}` + ) : channel.type === ChannelType.GuildCategory ? ( + `Category Channel` + ) : 'topic' in channel && channel.topic ? ( + + ) : ( + `This is the start of #${channel.name} channel.` + )} + + + {/* body */} + {messages.map((message) => ( + + ))} + + {/* footer */} +
+ {options.footerText + ? options.footerText + .replaceAll('{number}', messages.length.toString()) + .replaceAll('{s}', messages.length > 1 ? 's' : '') + : `Exported ${messages.length} message${messages.length > 1 ? 's' : ''}.`}{' '} + {options.poweredBy ? ( + + Powered by{' '} + + discord-html-transcripts + + . + + ) : null} +
+
+ ); +} From 08db6f63f26350dc7702cf013403e08a028c7c91 Mon Sep 17 00:00:00 2001 From: TheMonDon <11539895+TheMonDon@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:23:09 -0500 Subject: [PATCH 2/6] Run lint lol --- src/generator/index.tsx | 242 ++++---- src/generator/renderers/attachment.tsx | 154 +++--- src/generator/renderers/components.tsx | 255 +++++---- .../renderers/components/Container.tsx | 48 +- .../renderers/components/Media Gallery.tsx | 107 ++-- .../renderers/components/Select Menu.tsx | 114 ++-- .../renderers/components/Spacing.tsx | 34 +- .../renderers/components/section/Section.tsx | 60 +- .../components/section/SectionAccessory.tsx | 50 +- .../components/section/SectionContent.tsx | 42 +- src/generator/renderers/components/styles.ts | 166 +++--- src/generator/renderers/components/utils.ts | 340 ++++++------ src/generator/renderers/content.tsx | 522 +++++++++--------- src/generator/renderers/embed.tsx | 136 ++--- src/generator/renderers/message.tsx | 244 ++++---- src/generator/renderers/reply.tsx | 90 +-- src/generator/renderers/systemMessage.tsx | 246 ++++----- src/generator/transcript.tsx | 144 ++--- 18 files changed, 1503 insertions(+), 1491 deletions(-) diff --git a/src/generator/index.tsx b/src/generator/index.tsx index b9a436c1..402d4171 100644 --- a/src/generator/index.tsx +++ b/src/generator/index.tsx @@ -1,121 +1,121 @@ -import { type Awaitable, type Channel, type Message, type Role, type User } from 'discord.js'; -import ReactDOMServer from 'react-dom/server'; -import React from 'react'; -import { buildProfiles } from '../utils/buildProfiles'; -import { revealSpoiler, scrollToMessage } from '../static/client'; -import { readFileSync } from 'fs'; -import path from 'path'; -import { renderToString } from '@derockdev/discord-components-core/hydrate'; -import { streamToString } from '../utils/utils'; -import DiscordMessages from './transcript'; -import type { ResolveImageCallback } from '../downloader/images'; - -// read the package.json file and get the @derockdev/discord-components-core version -let discordComponentsVersion = '^3.6.1'; - -try { - const packagePath = path.join(__dirname, '..', '..', 'package.json'); - const packageJSON = JSON.parse(readFileSync(packagePath, 'utf8')); - discordComponentsVersion = packageJSON.dependencies['@derockdev/discord-components-core'] ?? discordComponentsVersion; - // eslint-disable-next-line no-empty -} catch {} // ignore errors - -export type RenderMessageContext = { - messages: Message[]; - channel: Channel; - - callbacks: { - resolveImageSrc: ResolveImageCallback; - resolveChannel: (channelId: string) => Awaitable; - resolveUser: (userId: string) => Awaitable; - resolveRole: (roleId: string) => Awaitable; - }; - - poweredBy?: boolean; - footerText?: string; - saveImages: boolean; - favicon: 'guild' | string; - hydrate: boolean; -}; - -export default async function render({ messages, channel, callbacks, ...options }: RenderMessageContext) { - const profiles = buildProfiles(messages); - - // NOTE: this renders a STATIC site with no interactivity - // if interactivity is needed, switch to renderToPipeableStream and use hydrateRoot on client. - const stream = ReactDOMServer.renderToStaticNodeStream( - - - - - - {/* favicon */} - - - {/* title */} - {channel.isDMBased() ? 'Direct Messages' : channel.name} - - {/* message reference handler */} - - {/* component library */} - - - )} - - - - - - - {/* Make sure the script runs after the DOM has loaded */} - {options.hydrate && } - - ); - - const markup = await streamToString(stream); - - if (options.hydrate) { - const result = await renderToString(markup, { - beforeHydrate: async (document) => { - document.defaultView.$discordMessage = { - profiles: await profiles, - }; - }, - }); - - return result.html; - } - - return markup; -} +import { type Awaitable, type Channel, type Message, type Role, type User } from 'discord.js'; +import ReactDOMServer from 'react-dom/server'; +import React from 'react'; +import { buildProfiles } from '../utils/buildProfiles'; +import { revealSpoiler, scrollToMessage } from '../static/client'; +import { readFileSync } from 'fs'; +import path from 'path'; +import { renderToString } from '@derockdev/discord-components-core/hydrate'; +import { streamToString } from '../utils/utils'; +import DiscordMessages from './transcript'; +import type { ResolveImageCallback } from '../downloader/images'; + +// read the package.json file and get the @derockdev/discord-components-core version +let discordComponentsVersion = '^3.6.1'; + +try { + const packagePath = path.join(__dirname, '..', '..', 'package.json'); + const packageJSON = JSON.parse(readFileSync(packagePath, 'utf8')); + discordComponentsVersion = packageJSON.dependencies['@derockdev/discord-components-core'] ?? discordComponentsVersion; + // eslint-disable-next-line no-empty +} catch {} // ignore errors + +export type RenderMessageContext = { + messages: Message[]; + channel: Channel; + + callbacks: { + resolveImageSrc: ResolveImageCallback; + resolveChannel: (channelId: string) => Awaitable; + resolveUser: (userId: string) => Awaitable; + resolveRole: (roleId: string) => Awaitable; + }; + + poweredBy?: boolean; + footerText?: string; + saveImages: boolean; + favicon: 'guild' | string; + hydrate: boolean; +}; + +export default async function render({ messages, channel, callbacks, ...options }: RenderMessageContext) { + const profiles = buildProfiles(messages); + + // NOTE: this renders a STATIC site with no interactivity + // if interactivity is needed, switch to renderToPipeableStream and use hydrateRoot on client. + const stream = ReactDOMServer.renderToStaticNodeStream( + + + + + + {/* favicon */} + + + {/* title */} + {channel.isDMBased() ? 'Direct Messages' : channel.name} + + {/* message reference handler */} + + {/* component library */} + + + )} + + + + + + + {/* Make sure the script runs after the DOM has loaded */} + {options.hydrate && } + + ); + + const markup = await streamToString(stream); + + if (options.hydrate) { + const result = await renderToString(markup, { + beforeHydrate: async (document) => { + document.defaultView.$discordMessage = { + profiles: await profiles, + }; + }, + }); + + return result.html; + } + + return markup; +} diff --git a/src/generator/renderers/attachment.tsx b/src/generator/renderers/attachment.tsx index be139491..b63fed1a 100644 --- a/src/generator/renderers/attachment.tsx +++ b/src/generator/renderers/attachment.tsx @@ -1,77 +1,77 @@ -import { DiscordAttachment, DiscordAttachments } from '@derockdev/discord-components-react'; -import React from 'react'; -import type { APIAttachment, APIMessage, Attachment as AttachmentType, Message } from 'discord.js'; -import type { RenderMessageContext } from '..'; -import type { AttachmentTypes } from '../../types'; -import { formatBytes } from '../../utils/utils'; - -/** - * Renders all attachments for a message - * @param message - * @param context - * @returns - */ -export async function Attachments(props: { message: Message; context: RenderMessageContext }) { - if (props.message.attachments.size === 0) return <>; - - return ( - - {props.message.attachments.map((attachment, id) => ( - - ))} - - ); -} - -// "audio" | "video" | "image" | "file" -function getAttachmentType(attachment: AttachmentType): AttachmentTypes { - const type = attachment.contentType?.split('/')?.[0] ?? 'unknown'; - if (['audio', 'video', 'image'].includes(type)) return type as AttachmentTypes; - return 'file'; -} - -/** - * Renders one Discord Attachment - * @param props - the attachment and rendering context - */ -export async function Attachment({ - attachment, - context, - message, -}: { - attachment: AttachmentType; - context: RenderMessageContext; - message: Message; -}) { - let url = attachment.url; - const name = attachment.name; - const width = attachment.width; - const height = attachment.height; - - const type = getAttachmentType(attachment); - - // if the attachment is an image, download it to a data url - if (type === 'image') { - const downloaded = await context.callbacks.resolveImageSrc( - attachment.toJSON() as APIAttachment, - message.toJSON() as APIMessage - ); - - if (downloaded !== null) { - url = downloaded ?? url; - } - } - - return ( - - ); -} +import { DiscordAttachment, DiscordAttachments } from '@derockdev/discord-components-react'; +import React from 'react'; +import type { APIAttachment, APIMessage, Attachment as AttachmentType, Message } from 'discord.js'; +import type { RenderMessageContext } from '..'; +import type { AttachmentTypes } from '../../types'; +import { formatBytes } from '../../utils/utils'; + +/** + * Renders all attachments for a message + * @param message + * @param context + * @returns + */ +export async function Attachments(props: { message: Message; context: RenderMessageContext }) { + if (props.message.attachments.size === 0) return <>; + + return ( + + {props.message.attachments.map((attachment, id) => ( + + ))} + + ); +} + +// "audio" | "video" | "image" | "file" +function getAttachmentType(attachment: AttachmentType): AttachmentTypes { + const type = attachment.contentType?.split('/')?.[0] ?? 'unknown'; + if (['audio', 'video', 'image'].includes(type)) return type as AttachmentTypes; + return 'file'; +} + +/** + * Renders one Discord Attachment + * @param props - the attachment and rendering context + */ +export async function Attachment({ + attachment, + context, + message, +}: { + attachment: AttachmentType; + context: RenderMessageContext; + message: Message; +}) { + let url = attachment.url; + const name = attachment.name; + const width = attachment.width; + const height = attachment.height; + + const type = getAttachmentType(attachment); + + // if the attachment is an image, download it to a data url + if (type === 'image') { + const downloaded = await context.callbacks.resolveImageSrc( + attachment.toJSON() as APIAttachment, + message.toJSON() as APIMessage + ); + + if (downloaded !== null) { + url = downloaded ?? url; + } + } + + return ( + + ); +} diff --git a/src/generator/renderers/components.tsx b/src/generator/renderers/components.tsx index b93a6918..a56cc571 100644 --- a/src/generator/renderers/components.tsx +++ b/src/generator/renderers/components.tsx @@ -1,121 +1,134 @@ -import { DiscordActionRow, DiscordAttachment, DiscordButton, DiscordSpoiler } from '@derockdev/discord-components-react'; -import { ButtonStyle, ComponentType, type ThumbnailComponent, type MessageActionRowComponent, type TopLevelComponent } from 'discord.js'; -import React from 'react'; -import { parseDiscordEmoji } from '../../utils/utils'; -import DiscordSelectMenu from './components/Select Menu'; -import DiscordContainer from './components/Container'; -import DiscordSection from './components/section/Section'; -import DiscordMediaGallery from './components/Media Gallery'; -import DiscordSeperator from './components/Spacing'; -import MessageContent from './content'; -import { RenderType } from './content'; -import type { RenderMessageContext } from '..'; - -export default function ComponentRow({ component, id, context }: { component: TopLevelComponent; id: number; context: RenderMessageContext }) { - if (component.type === ComponentType.ActionRow) { - return ( - - <> - {component.components.map((nestedComponent, id) => ( - - ))} - - - ); - } - - if (component.type === ComponentType.Container) { - return ( - - <> - {component.components.map((nestedComponent, id) => ( - - ))} - - - ); - } - - if (component.type === ComponentType.File) { - return ( - <> - {component.spoiler ? - - - - : - - } - - ); - } - - if (component.type === ComponentType.MediaGallery) { - return ( - - ); - } - - if (component.type === ComponentType.Section) { - return ( - - {component.components.map((nestedComponent, id) => ( - - ))} - - ); - } - - if (component.type === ComponentType.Separator) { - return ( - - ); - } - - if (component.type === ComponentType.TextDisplay) { - return ( - - ); - } -} - -const ButtonStyleMapping = { - [ButtonStyle.Primary]: 'primary', - [ButtonStyle.Secondary]: 'secondary', - [ButtonStyle.Success]: 'success', - [ButtonStyle.Danger]: 'destructive', - [ButtonStyle.Link]: 'secondary', -} as const; - -export function Component({ component, id }: { component: MessageActionRowComponent | ThumbnailComponent; id: number }) { - if (component.type === ComponentType.Button) { - return ( - - {component.label} - - ); - } - - if (component.type === ComponentType.StringSelect || - component.type === ComponentType.UserSelect || - component.type === ComponentType.RoleSelect || - component.type === ComponentType.MentionableSelect || - component.type === ComponentType.ChannelSelect) { - return ( - - ); - } - - return undefined; -} +import { + DiscordActionRow, + DiscordAttachment, + DiscordButton, + DiscordSpoiler, +} from '@derockdev/discord-components-react'; +import { + ButtonStyle, + ComponentType, + type ThumbnailComponent, + type MessageActionRowComponent, + type TopLevelComponent, +} from 'discord.js'; +import React from 'react'; +import { parseDiscordEmoji } from '../../utils/utils'; +import DiscordSelectMenu from './components/Select Menu'; +import DiscordContainer from './components/Container'; +import DiscordSection from './components/section/Section'; +import DiscordMediaGallery from './components/Media Gallery'; +import DiscordSeperator from './components/Spacing'; +import MessageContent from './content'; +import { RenderType } from './content'; +import type { RenderMessageContext } from '..'; + +export default function ComponentRow({ + component, + id, + context, +}: { + component: TopLevelComponent; + id: number; + context: RenderMessageContext; +}) { + if (component.type === ComponentType.ActionRow) { + return ( + + <> + {component.components.map((nestedComponent, id) => ( + + ))} + + + ); + } + + if (component.type === ComponentType.Container) { + return ( + + <> + {component.components.map((nestedComponent, id) => ( + + ))} + + + ); + } + + if (component.type === ComponentType.File) { + return ( + <> + {component.spoiler ? ( + + + + ) : ( + + )} + + ); + } + + if (component.type === ComponentType.MediaGallery) { + return ; + } + + if (component.type === ComponentType.Section) { + return ( + + {component.components.map((nestedComponent, id) => ( + + ))} + + ); + } + + if (component.type === ComponentType.Separator) { + return ; + } + + if (component.type === ComponentType.TextDisplay) { + return ; + } +} + +const ButtonStyleMapping = { + [ButtonStyle.Primary]: 'primary', + [ButtonStyle.Secondary]: 'secondary', + [ButtonStyle.Success]: 'success', + [ButtonStyle.Danger]: 'destructive', + [ButtonStyle.Link]: 'secondary', +} as const; + +export function Component({ + component, + id, +}: { + component: MessageActionRowComponent | ThumbnailComponent; + id: number; +}) { + if (component.type === ComponentType.Button) { + return ( + + {component.label} + + ); + } + + if ( + component.type === ComponentType.StringSelect || + component.type === ComponentType.UserSelect || + component.type === ComponentType.RoleSelect || + component.type === ComponentType.MentionableSelect || + component.type === ComponentType.ChannelSelect + ) { + return ; + } + + return undefined; +} diff --git a/src/generator/renderers/components/Container.tsx b/src/generator/renderers/components/Container.tsx index ec013a8c..e8d2f725 100644 --- a/src/generator/renderers/components/Container.tsx +++ b/src/generator/renderers/components/Container.tsx @@ -1,24 +1,24 @@ -import React from 'react'; - -const DiscordContainer: React.FC<{ children: React.ReactNode; }> = ({ children }) => { - return ( -
- {children} -
- ); -} - -export default DiscordContainer; \ No newline at end of file +import React from 'react'; + +const DiscordContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export default DiscordContainer; diff --git a/src/generator/renderers/components/Media Gallery.tsx b/src/generator/renderers/components/Media Gallery.tsx index 703f84c6..e4caa4d9 100644 --- a/src/generator/renderers/components/Media Gallery.tsx +++ b/src/generator/renderers/components/Media Gallery.tsx @@ -1,54 +1,53 @@ -import React from 'react'; -import type { MediaGalleryComponent } from 'discord.js'; -import { getGalleryLayout, getImageStyle } from './utils'; - - -const DiscordMediaGallery: React.FC<{ component: MediaGalleryComponent }> = ({ component }) => { - if (!component.items || component.items.length === 0) { - return null; - } - - const count = component.items.length; - const imagesToShow = component.items.slice(0, 10); - const hasMore = component.items.length > 10; - - return ( -
- {imagesToShow.map((media, idx) => ( -
- {media.description - {hasMore && idx === imagesToShow.length - 1 && ( -
- +{component.items.length - 10} -
- )} -
- ))} -
- ); -}; - -export default DiscordMediaGallery; +import React from 'react'; +import type { MediaGalleryComponent } from 'discord.js'; +import { getGalleryLayout, getImageStyle } from './utils'; + +const DiscordMediaGallery: React.FC<{ component: MediaGalleryComponent }> = ({ component }) => { + if (!component.items || component.items.length === 0) { + return null; + } + + const count = component.items.length; + const imagesToShow = component.items.slice(0, 10); + const hasMore = component.items.length > 10; + + return ( +
+ {imagesToShow.map((media, idx) => ( +
+ {media.description + {hasMore && idx === imagesToShow.length - 1 && ( +
+ +{component.items.length - 10} +
+ )} +
+ ))} +
+ ); +}; + +export default DiscordMediaGallery; diff --git a/src/generator/renderers/components/Select Menu.tsx b/src/generator/renderers/components/Select Menu.tsx index 94e63adc..d0747392 100644 --- a/src/generator/renderers/components/Select Menu.tsx +++ b/src/generator/renderers/components/Select Menu.tsx @@ -1,56 +1,58 @@ -import React from 'react'; -import { type MessageActionRowComponent, ComponentType } from 'discord.js'; -import { parseDiscordEmoji } from '../../../utils/utils'; -import { getSelectTypeLabel } from './utils'; -import { selectMenuStyle } from './styles'; - -const DiscordSelectMenu: React.FC<{ component: Exclude }> = ({ component }) => { - const isStringSelect = component.type === ComponentType.StringSelect; - const placeholder = component.placeholder || getSelectTypeLabel(component.type); - - return ( -
-
{placeholder}
-
- - - -
- {isStringSelect && component.options && component.options.length > 0 && ( -
- {component.options.map((option, idx) => ( -
- {option.emoji && {parseDiscordEmoji(option.emoji)}} - {option.label} -
- ))} -
- )} -
- ); -}; - -export default DiscordSelectMenu; +import React from 'react'; +import { type MessageActionRowComponent, ComponentType } from 'discord.js'; +import { parseDiscordEmoji } from '../../../utils/utils'; +import { getSelectTypeLabel } from './utils'; +import { selectMenuStyle } from './styles'; + +const DiscordSelectMenu: React.FC<{ + component: Exclude; +}> = ({ component }) => { + const isStringSelect = component.type === ComponentType.StringSelect; + const placeholder = component.placeholder || getSelectTypeLabel(component.type); + + return ( +
+
{placeholder}
+
+ + + +
+ {isStringSelect && component.options && component.options.length > 0 && ( +
+ {component.options.map((option, idx) => ( +
+ {option.emoji && {parseDiscordEmoji(option.emoji)}} + {option.label} +
+ ))} +
+ )} +
+ ); +}; + +export default DiscordSelectMenu; diff --git a/src/generator/renderers/components/Spacing.tsx b/src/generator/renderers/components/Spacing.tsx index 4259f09a..9e175786 100644 --- a/src/generator/renderers/components/Spacing.tsx +++ b/src/generator/renderers/components/Spacing.tsx @@ -1,17 +1,17 @@ -import React from "react"; -import { SeparatorSpacingSize } from "discord.js"; - -const DiscordSeperator: React.FC<{ divider: boolean; spacing: SeparatorSpacingSize; }> = ({ divider, spacing }) => { - return ( -
- ); -} - -export default DiscordSeperator; \ No newline at end of file +import React from 'react'; +import { SeparatorSpacingSize } from 'discord.js'; + +const DiscordSeperator: React.FC<{ divider: boolean; spacing: SeparatorSpacingSize }> = ({ divider, spacing }) => { + return ( +
+ ); +}; + +export default DiscordSeperator; diff --git a/src/generator/renderers/components/section/Section.tsx b/src/generator/renderers/components/section/Section.tsx index 4e12c032..0a4a9e01 100644 --- a/src/generator/renderers/components/section/Section.tsx +++ b/src/generator/renderers/components/section/Section.tsx @@ -1,31 +1,29 @@ -import React from 'react'; -import type { ButtonComponent, ThumbnailComponent } from 'discord.js'; -import { Component } from '../../components'; -import SectionContent from './SectionContent'; -import SectionAccessory from './SectionAccessory'; - -interface DiscordSectionProps { - children: React.ReactNode; - accessory?: ButtonComponent | ThumbnailComponent; - id: number; -} - -const DiscordSection: React.FC = ({ children, accessory, id }) => { - return ( -
- {children} - - {accessory && } - -
- ); -}; - -export default DiscordSection; +import React from 'react'; +import type { ButtonComponent, ThumbnailComponent } from 'discord.js'; +import { Component } from '../../components'; +import SectionContent from './SectionContent'; +import SectionAccessory from './SectionAccessory'; + +interface DiscordSectionProps { + children: React.ReactNode; + accessory?: ButtonComponent | ThumbnailComponent; + id: number; +} + +const DiscordSection: React.FC = ({ children, accessory, id }) => { + return ( +
+ {children} + {accessory && } +
+ ); +}; + +export default DiscordSection; diff --git a/src/generator/renderers/components/section/SectionAccessory.tsx b/src/generator/renderers/components/section/SectionAccessory.tsx index 27602937..65fa4429 100644 --- a/src/generator/renderers/components/section/SectionAccessory.tsx +++ b/src/generator/renderers/components/section/SectionAccessory.tsx @@ -1,25 +1,25 @@ -import React from 'react'; - -interface SectionAccessoryProps { - children?: React.ReactNode; -} - -const SectionAccessory: React.FC = ({ children }) => { - if (!children) return null; - - return ( -
- {children} -
- ); -}; - -export default SectionAccessory; +import React from 'react'; + +interface SectionAccessoryProps { + children?: React.ReactNode; +} + +const SectionAccessory: React.FC = ({ children }) => { + if (!children) return null; + + return ( +
+ {children} +
+ ); +}; + +export default SectionAccessory; diff --git a/src/generator/renderers/components/section/SectionContent.tsx b/src/generator/renderers/components/section/SectionContent.tsx index ac5a038a..ccf9d1e3 100644 --- a/src/generator/renderers/components/section/SectionContent.tsx +++ b/src/generator/renderers/components/section/SectionContent.tsx @@ -1,21 +1,21 @@ -import React from 'react'; - -interface SectionContentProps { - children: React.ReactNode; -} - -const SectionContent: React.FC = ({ children }) => { - return ( -
- {children} -
- ); -}; - -export default SectionContent; +import React from 'react'; + +interface SectionContentProps { + children: React.ReactNode; +} + +const SectionContent: React.FC = ({ children }) => { + return ( +
+ {children} +
+ ); +}; + +export default SectionContent; diff --git a/src/generator/renderers/components/styles.ts b/src/generator/renderers/components/styles.ts index 5cb271b4..1d52d9a8 100644 --- a/src/generator/renderers/components/styles.ts +++ b/src/generator/renderers/components/styles.ts @@ -1,83 +1,83 @@ -import type { CSSProperties } from 'react'; -import { ButtonStyle } from 'discord.js'; - -// Container styles -export const containerStyle: CSSProperties = { - display: 'grid', - gap: '4px', - width: '100%', - maxWidth: '500px', - borderRadius: '8px', - overflow: 'hidden', -}; - -// Base image style -export const baseImageStyle: CSSProperties = { - overflow: 'hidden', - position: 'relative', - background: '#2b2d31', -}; - -// Button style mapping -export const ButtonStyleMapping = { - [ButtonStyle.Primary]: 'primary', - [ButtonStyle.Secondary]: 'secondary', - [ButtonStyle.Success]: 'success', - [ButtonStyle.Danger]: 'destructive', - [ButtonStyle.Link]: 'secondary', -} as const; - -// Get button style based on type -export const getButtonStyle = (type: string): CSSProperties => ({ - backgroundColor: - type === 'primary' - ? 'hsl(234.935 calc(1*85.556%) 64.706% /1)' - : type === 'secondary' - ? 'hsl(240 calc(1*4%) 60.784% /0.12156862745098039)' - : type === 'success' - ? 'hsl(145.97 calc(1*100%) 26.275% /1)' - : type === 'destructive' - ? 'hsl(355.636 calc(1*64.706%) 50% /1)' - : '#2b2d31', - color: '#ffffff', - padding: '2px 16px', - borderRadius: '8px', - textDecoration: 'none', - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: '14px', - fontWeight: '500', - height: '32px', - minHeight: '32px', - minWidth: '60px', - cursor: 'pointer', - fontFamily: 'Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif', - textAlign: 'center', - boxSizing: 'border-box', - border: 'none', - outline: 'none', - transition: 'background-color 0.2s ease', -}); - -// Select menu styles -export const selectMenuStyle: CSSProperties = { - marginTop: '2px', - marginBottom: '2px', - position: 'relative', - width: '100%', - maxWidth: '500px', - height: '40px', - backgroundColor: '#2b2d31', - borderRadius: '4px', - color: '#b5bac1', - cursor: 'pointer', - fontFamily: 'Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif', - fontSize: '14px', - display: 'flex', - alignItems: 'center', - padding: '0 8px', - justifyContent: 'space-between', - boxSizing: 'border-box', - border: '1px solid #1e1f22', -}; +import type { CSSProperties } from 'react'; +import { ButtonStyle } from 'discord.js'; + +// Container styles +export const containerStyle: CSSProperties = { + display: 'grid', + gap: '4px', + width: '100%', + maxWidth: '500px', + borderRadius: '8px', + overflow: 'hidden', +}; + +// Base image style +export const baseImageStyle: CSSProperties = { + overflow: 'hidden', + position: 'relative', + background: '#2b2d31', +}; + +// Button style mapping +export const ButtonStyleMapping = { + [ButtonStyle.Primary]: 'primary', + [ButtonStyle.Secondary]: 'secondary', + [ButtonStyle.Success]: 'success', + [ButtonStyle.Danger]: 'destructive', + [ButtonStyle.Link]: 'secondary', +} as const; + +// Get button style based on type +export const getButtonStyle = (type: string): CSSProperties => ({ + backgroundColor: + type === 'primary' + ? 'hsl(234.935 calc(1*85.556%) 64.706% /1)' + : type === 'secondary' + ? 'hsl(240 calc(1*4%) 60.784% /0.12156862745098039)' + : type === 'success' + ? 'hsl(145.97 calc(1*100%) 26.275% /1)' + : type === 'destructive' + ? 'hsl(355.636 calc(1*64.706%) 50% /1)' + : '#2b2d31', + color: '#ffffff', + padding: '2px 16px', + borderRadius: '8px', + textDecoration: 'none', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '14px', + fontWeight: '500', + height: '32px', + minHeight: '32px', + minWidth: '60px', + cursor: 'pointer', + fontFamily: 'Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif', + textAlign: 'center', + boxSizing: 'border-box', + border: 'none', + outline: 'none', + transition: 'background-color 0.2s ease', +}); + +// Select menu styles +export const selectMenuStyle: CSSProperties = { + marginTop: '2px', + marginBottom: '2px', + position: 'relative', + width: '100%', + maxWidth: '500px', + height: '40px', + backgroundColor: '#2b2d31', + borderRadius: '4px', + color: '#b5bac1', + cursor: 'pointer', + fontFamily: 'Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif', + fontSize: '14px', + display: 'flex', + alignItems: 'center', + padding: '0 8px', + justifyContent: 'space-between', + boxSizing: 'border-box', + border: '1px solid #1e1f22', +}; diff --git a/src/generator/renderers/components/utils.ts b/src/generator/renderers/components/utils.ts index 48e773ca..9eda3fb8 100644 --- a/src/generator/renderers/components/utils.ts +++ b/src/generator/renderers/components/utils.ts @@ -1,170 +1,170 @@ -import { ComponentType } from 'discord.js'; -import type { CSSProperties } from 'react'; -import { baseImageStyle, containerStyle } from './styles'; - -/** - * Gets the appropriate label for different select menu types - */ -export const getSelectTypeLabel = (type: ComponentType): string => { - switch (type) { - case ComponentType.UserSelect: - return 'Select User'; - case ComponentType.RoleSelect: - return 'Select Role'; - case ComponentType.MentionableSelect: - return 'Select Mentionable'; - case ComponentType.ChannelSelect: - return 'Select Channel'; - case ComponentType.StringSelect: - return 'Make a Selection'; - default: - return 'Select Option'; - } -}; - -/** - * Gets the grid layout for media galleries based on count - */ -export const getGalleryLayout = (count: number): CSSProperties => { - if (count === 1) { - return { - ...containerStyle, - gridTemplateColumns: '1fr', - gridTemplateRows: 'auto', - }; - } else if (count === 2) { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr', - gridTemplateRows: 'auto', - }; - } else if (count === 3 || count === 4) { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr', - gridTemplateRows: '1fr 1fr', - }; - } else if (count === 5) { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr 1fr', - gridTemplateRows: 'auto auto', - }; - } else if (count >= 7) { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr 1fr', - gridTemplateRows: 'auto auto auto auto', - }; - } else { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr 1fr', - gridTemplateRows: 'auto', - }; - } -}; - -/** - * Gets the style for an individual image based on its position and total count - */ -export const getImageStyle = (idx: number, count: number): CSSProperties => { - if (count === 3 && idx === 0) { - return { - ...baseImageStyle, - gridRow: '1 / span 2', - gridColumn: '1', - aspectRatio: '1/2', - }; - } - - if (count === 5) { - if (idx < 2) { - return { - ...baseImageStyle, - gridRow: '1', - gridColumn: idx === 0 ? '1 / span 2' : '3', - }; - } else { - return { - ...baseImageStyle, - gridRow: '2', - gridColumn: `${idx - 2 + 1}`, - }; - } - } - - if (count === 7) { - if (idx === 0) { - return { - ...baseImageStyle, - gridRow: '1', - gridColumn: '1 / span 3', - }; - } else if (idx <= 3) { - return { - ...baseImageStyle, - gridRow: '2', - gridColumn: `${idx - 0}`, - }; - } else { - return { - ...baseImageStyle, - gridRow: '3', - gridColumn: `${idx - 3}`, - }; - } - } - - if (count === 8) { - if (idx < 2) { - return { - ...baseImageStyle, - gridRow: '1', - gridColumn: idx === 0 ? '1 / span 2' : '3', - }; - } else if (idx < 5) { - return { - ...baseImageStyle, - gridRow: '2', - gridColumn: `${idx - 2 + 1}`, - }; - } else { - return { - ...baseImageStyle, - gridRow: '3', - gridColumn: `${idx - 5 + 1}`, - }; - } - } - - if (count === 10) { - if (idx === 0) { - return { - ...baseImageStyle, - gridRow: '1', - gridColumn: '1 / span 3', - }; - } else if (idx <= 3) { - return { - ...baseImageStyle, - gridRow: '2', - gridColumn: `${idx - 0}`, - }; - } else if (idx <= 6) { - return { - ...baseImageStyle, - gridRow: '3', - gridColumn: `${idx - 3}`, - }; - } else { - return { - ...baseImageStyle, - gridRow: '4', - gridColumn: `${idx - 6}`, - }; - } - } - - return baseImageStyle; -}; +import { ComponentType } from 'discord.js'; +import type { CSSProperties } from 'react'; +import { baseImageStyle, containerStyle } from './styles'; + +/** + * Gets the appropriate label for different select menu types + */ +export const getSelectTypeLabel = (type: ComponentType): string => { + switch (type) { + case ComponentType.UserSelect: + return 'Select User'; + case ComponentType.RoleSelect: + return 'Select Role'; + case ComponentType.MentionableSelect: + return 'Select Mentionable'; + case ComponentType.ChannelSelect: + return 'Select Channel'; + case ComponentType.StringSelect: + return 'Make a Selection'; + default: + return 'Select Option'; + } +}; + +/** + * Gets the grid layout for media galleries based on count + */ +export const getGalleryLayout = (count: number): CSSProperties => { + if (count === 1) { + return { + ...containerStyle, + gridTemplateColumns: '1fr', + gridTemplateRows: 'auto', + }; + } else if (count === 2) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: 'auto', + }; + } else if (count === 3 || count === 4) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: '1fr 1fr', + }; + } else if (count === 5) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto auto', + }; + } else if (count >= 7) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto auto auto auto', + }; + } else { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto', + }; + } +}; + +/** + * Gets the style for an individual image based on its position and total count + */ +export const getImageStyle = (idx: number, count: number): CSSProperties => { + if (count === 3 && idx === 0) { + return { + ...baseImageStyle, + gridRow: '1 / span 2', + gridColumn: '1', + aspectRatio: '1/2', + }; + } + + if (count === 5) { + if (idx < 2) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: idx === 0 ? '1 / span 2' : '3', + }; + } else { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 2 + 1}`, + }; + } + } + + if (count === 7) { + if (idx === 0) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: '1 / span 3', + }; + } else if (idx <= 3) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 0}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 3}`, + }; + } + } + + if (count === 8) { + if (idx < 2) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: idx === 0 ? '1 / span 2' : '3', + }; + } else if (idx < 5) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 2 + 1}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 5 + 1}`, + }; + } + } + + if (count === 10) { + if (idx === 0) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: '1 / span 3', + }; + } else if (idx <= 3) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 0}`, + }; + } else if (idx <= 6) { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 3}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '4', + gridColumn: `${idx - 6}`, + }; + } + } + + return baseImageStyle; +}; diff --git a/src/generator/renderers/content.tsx b/src/generator/renderers/content.tsx index 25af2ef3..b09ca692 100644 --- a/src/generator/renderers/content.tsx +++ b/src/generator/renderers/content.tsx @@ -1,261 +1,261 @@ -import { - DiscordBold, - DiscordCodeBlock, - DiscordCustomEmoji, - DiscordInlineCode, - DiscordItalic, - DiscordMention, - DiscordQuote, - DiscordSpoiler, - DiscordTime, - DiscordUnderlined, -} from '@derockdev/discord-components-react'; -import parse, { type RuleTypesExtended } from 'discord-markdown-parser'; -import { ChannelType, type APIMessageComponentEmoji } from 'discord.js'; -import React from 'react'; -import type { ASTNode } from 'simple-markdown'; -import { ASTNode as MessageASTNodes } from 'simple-markdown'; -import type { SingleASTNode } from 'simple-markdown'; -import type { RenderMessageContext } from '../'; -import { parseDiscordEmoji } from '../../utils/utils'; - -export enum RenderType { - EMBED, - REPLY, - NORMAL, - WEBHOOK, -} - -type RenderContentContext = RenderMessageContext & { - type: RenderType; - - _internal?: { - largeEmojis?: boolean; - }; -}; - -/** - * Renders discord markdown content - * @param content - The content to render - * @param context - The context to render the content in - * @returns - */ -export default async function MessageContent({ content, context }: { content: string; context: RenderContentContext }) { - if (context.type === RenderType.REPLY && content.length > 180) content = content.slice(0, 180) + '...'; - - // parse the markdown - const parsed = parse( - content, - context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal' - ); - - // check if the parsed content is only emojis - const isOnlyEmojis = parsed.every( - (node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0) - ); - if (isOnlyEmojis) { - // now check if there are less than or equal to 25 emojis - const emojis = parsed.filter((node) => ['emoji', 'twemoji'].includes(node.type)); - if (emojis.length <= 25) { - context._internal = { - largeEmojis: true, - }; - } - } - - return ; -} - -// This function can probably be combined into the MessageSingleASTNode function -async function MessageASTNodes({ - nodes, - context, -}: { - nodes: ASTNode; - context: RenderContentContext; -}): Promise { - if (Array.isArray(nodes)) { - return ( - <> - {nodes.map((node, i) => ( - - ))} - - ); - } else { - return ; - } -} - -export async function MessageSingleASTNode({ node, context }: { node: SingleASTNode; context: RenderContentContext }) { - if (!node) return null; - - const type = node.type as RuleTypesExtended; - - switch (type) { - case 'text': - return node.content; - - case 'link': - return ( - - - - ); - - case 'url': - case 'autolink': - return ( - - - - ); - - case 'blockQuote': - if (context.type === RenderType.REPLY) { - return ; - } - - return ( - - - - ); - - case 'br': - case 'newline': - if (context.type === RenderType.REPLY) return ' '; - return
; - - case 'channel': { - const id = node.id as string; - const channel = await context.callbacks.resolveChannel(id); - - return ( - - {channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `<#${id}>`} - - ); - } - - case 'role': { - const id = node.id as string; - const role = await context.callbacks.resolveRole(id); - - return ( - - {role ? role.name : `<@&${id}>`} - - ); - } - - case 'user': { - const id = node.id as string; - const user = await context.callbacks.resolveUser(id); - - return {user ? user.displayName ?? user.username : `<@${id}>`}; - } - - case 'here': - case 'everyone': - return ( - - {`@${type}`} - - ); - - case 'codeBlock': - if (context.type !== RenderType.REPLY) { - return ; - } - return {node.content}; - - case 'inlineCode': - return {node.content}; - - case 'em': - return ( - - - - ); - - case 'strong': - return ( - - - - ); - - case 'underline': - return ( - - - - ); - - case 'strikethrough': - return ( - - - - ); - - case 'emoticon': - return typeof node.content === 'string' ? ( - node.content - ) : ( - - ); - - case 'spoiler': - return ( - - - - ); - - case 'emoji': - case 'twemoji': - return ( - - ); - - case 'timestamp': - return ; - - default: { - console.log(`Unknown node type: ${type}`, node); - return typeof node.content === 'string' ? ( - node.content - ) : ( - - ); - } - } -} - -export function getChannelType(channelType: ChannelType): 'channel' | 'voice' | 'thread' | 'forum' { - switch (channelType) { - case ChannelType.GuildCategory: - case ChannelType.GuildAnnouncement: - case ChannelType.GuildText: - return 'channel'; - case ChannelType.GuildVoice: - case ChannelType.GuildStageVoice: - return 'voice'; - case ChannelType.PublicThread: - case ChannelType.PrivateThread: - case ChannelType.AnnouncementThread: - return 'thread'; - case ChannelType.GuildForum: - return 'forum'; - default: - return 'channel'; - } -} +import { + DiscordBold, + DiscordCodeBlock, + DiscordCustomEmoji, + DiscordInlineCode, + DiscordItalic, + DiscordMention, + DiscordQuote, + DiscordSpoiler, + DiscordTime, + DiscordUnderlined, +} from '@derockdev/discord-components-react'; +import parse, { type RuleTypesExtended } from 'discord-markdown-parser'; +import { ChannelType, type APIMessageComponentEmoji } from 'discord.js'; +import React from 'react'; +import type { ASTNode } from 'simple-markdown'; +import { ASTNode as MessageASTNodes } from 'simple-markdown'; +import type { SingleASTNode } from 'simple-markdown'; +import type { RenderMessageContext } from '../'; +import { parseDiscordEmoji } from '../../utils/utils'; + +export enum RenderType { + EMBED, + REPLY, + NORMAL, + WEBHOOK, +} + +type RenderContentContext = RenderMessageContext & { + type: RenderType; + + _internal?: { + largeEmojis?: boolean; + }; +}; + +/** + * Renders discord markdown content + * @param content - The content to render + * @param context - The context to render the content in + * @returns + */ +export default async function MessageContent({ content, context }: { content: string; context: RenderContentContext }) { + if (context.type === RenderType.REPLY && content.length > 180) content = content.slice(0, 180) + '...'; + + // parse the markdown + const parsed = parse( + content, + context.type === RenderType.EMBED || context.type === RenderType.WEBHOOK ? 'extended' : 'normal' + ); + + // check if the parsed content is only emojis + const isOnlyEmojis = parsed.every( + (node) => ['emoji', 'twemoji'].includes(node.type) || (node.type === 'text' && node.content.trim().length === 0) + ); + if (isOnlyEmojis) { + // now check if there are less than or equal to 25 emojis + const emojis = parsed.filter((node) => ['emoji', 'twemoji'].includes(node.type)); + if (emojis.length <= 25) { + context._internal = { + largeEmojis: true, + }; + } + } + + return ; +} + +// This function can probably be combined into the MessageSingleASTNode function +async function MessageASTNodes({ + nodes, + context, +}: { + nodes: ASTNode; + context: RenderContentContext; +}): Promise { + if (Array.isArray(nodes)) { + return ( + <> + {nodes.map((node, i) => ( + + ))} + + ); + } else { + return ; + } +} + +export async function MessageSingleASTNode({ node, context }: { node: SingleASTNode; context: RenderContentContext }) { + if (!node) return null; + + const type = node.type as RuleTypesExtended; + + switch (type) { + case 'text': + return node.content; + + case 'link': + return ( + + + + ); + + case 'url': + case 'autolink': + return ( + + + + ); + + case 'blockQuote': + if (context.type === RenderType.REPLY) { + return ; + } + + return ( + + + + ); + + case 'br': + case 'newline': + if (context.type === RenderType.REPLY) return ' '; + return
; + + case 'channel': { + const id = node.id as string; + const channel = await context.callbacks.resolveChannel(id); + + return ( + + {channel ? (channel.isDMBased() ? 'DM Channel' : channel.name) : `<#${id}>`} + + ); + } + + case 'role': { + const id = node.id as string; + const role = await context.callbacks.resolveRole(id); + + return ( + + {role ? role.name : `<@&${id}>`} + + ); + } + + case 'user': { + const id = node.id as string; + const user = await context.callbacks.resolveUser(id); + + return {user ? user.displayName ?? user.username : `<@${id}>`}; + } + + case 'here': + case 'everyone': + return ( + + {`@${type}`} + + ); + + case 'codeBlock': + if (context.type !== RenderType.REPLY) { + return ; + } + return {node.content}; + + case 'inlineCode': + return {node.content}; + + case 'em': + return ( + + + + ); + + case 'strong': + return ( + + + + ); + + case 'underline': + return ( + + + + ); + + case 'strikethrough': + return ( + + + + ); + + case 'emoticon': + return typeof node.content === 'string' ? ( + node.content + ) : ( + + ); + + case 'spoiler': + return ( + + + + ); + + case 'emoji': + case 'twemoji': + return ( + + ); + + case 'timestamp': + return ; + + default: { + console.log(`Unknown node type: ${type}`, node); + return typeof node.content === 'string' ? ( + node.content + ) : ( + + ); + } + } +} + +export function getChannelType(channelType: ChannelType): 'channel' | 'voice' | 'thread' | 'forum' { + switch (channelType) { + case ChannelType.GuildCategory: + case ChannelType.GuildAnnouncement: + case ChannelType.GuildText: + return 'channel'; + case ChannelType.GuildVoice: + case ChannelType.GuildStageVoice: + return 'voice'; + case ChannelType.PublicThread: + case ChannelType.PrivateThread: + case ChannelType.AnnouncementThread: + return 'thread'; + case ChannelType.GuildForum: + return 'forum'; + default: + return 'channel'; + } +} diff --git a/src/generator/renderers/embed.tsx b/src/generator/renderers/embed.tsx index 0daa42f9..433d2b9b 100644 --- a/src/generator/renderers/embed.tsx +++ b/src/generator/renderers/embed.tsx @@ -1,68 +1,68 @@ -import { - DiscordEmbed as DiscordEmbedComponent, - DiscordEmbedDescription, - DiscordEmbedField, - DiscordEmbedFields, - DiscordEmbedFooter, -} from '@derockdev/discord-components-react'; -import type { Embed, Message } from 'discord.js'; -import React from 'react'; -import type { RenderMessageContext } from '..'; -import { calculateInlineIndex } from '../../utils/embeds'; -import MessageContent, { RenderType } from './content'; - -type RenderEmbedContext = RenderMessageContext & { - index: number; - message: Message; -}; - -export async function DiscordEmbed({ embed, context }: { embed: Embed; context: RenderEmbedContext }) { - return ( - - {/* Description */} - {embed.description && ( - - - - )} - - {/* Fields */} - {embed.fields.length > 0 && ( - - {embed.fields.map(async (field, id) => ( - - - - ))} - - )} - - {/* Footer */} - {embed.footer && ( - - {embed.footer.text} - - )} - - ); -} +import { + DiscordEmbed as DiscordEmbedComponent, + DiscordEmbedDescription, + DiscordEmbedField, + DiscordEmbedFields, + DiscordEmbedFooter, +} from '@derockdev/discord-components-react'; +import type { Embed, Message } from 'discord.js'; +import React from 'react'; +import type { RenderMessageContext } from '..'; +import { calculateInlineIndex } from '../../utils/embeds'; +import MessageContent, { RenderType } from './content'; + +type RenderEmbedContext = RenderMessageContext & { + index: number; + message: Message; +}; + +export async function DiscordEmbed({ embed, context }: { embed: Embed; context: RenderEmbedContext }) { + return ( + + {/* Description */} + {embed.description && ( + + + + )} + + {/* Fields */} + {embed.fields.length > 0 && ( + + {embed.fields.map(async (field, id) => ( + + + + ))} + + )} + + {/* Footer */} + {embed.footer && ( + + {embed.footer.text} + + )} + + ); +} diff --git a/src/generator/renderers/message.tsx b/src/generator/renderers/message.tsx index b4698a8f..a4f2d826 100644 --- a/src/generator/renderers/message.tsx +++ b/src/generator/renderers/message.tsx @@ -1,122 +1,122 @@ -import { - DiscordAttachments, - DiscordCommand, - DiscordMessage as DiscordMessageComponent, - DiscordReaction, - DiscordReactions, - DiscordThread, - DiscordThreadMessage, -} from '@derockdev/discord-components-react'; -import type { Message as MessageType } from 'discord.js'; -import React from 'react'; -import type { RenderMessageContext } from '..'; -import { parseDiscordEmoji } from '../../utils/utils'; -import { Attachments } from './attachment'; -import ComponentRow from './components'; -import MessageContent, { RenderType } from './content'; -import { DiscordEmbed } from './embed'; -import MessageReply from './reply'; -import DiscordSystemMessage from './systemMessage'; - -export default async function DiscordMessage({ - message, - context, -}: { - message: MessageType; - context: RenderMessageContext; -}) { - if (message.system) return ; - - const isCrosspost = message.reference && message.reference.guildId !== message.guild?.id; - - return ( - - {/* reply */} - - - {/* slash command */} - {message.interaction && ( - - )} - - {/* message content */} - {message.content && ( - - )} - - {/* attachments */} - - - {/* message embeds */} - {message.embeds.map((embed, id) => ( - - ))} - - {/* components */} - {message.components.length > 0 && ( - - {message.components.map((component, id) => ( - - ))} - - )} - - {/* reactions */} - {message.reactions.cache.size > 0 && ( - - {message.reactions.cache.map((reaction, id) => ( - - ))} - - )} - - {/* threads */} - {message.hasThread && message.thread && ( - 1 ? 's' : ''}` - : 'View Thread' - } - > - {message.thread.lastMessage ? ( - - 128 - ? message.thread.lastMessage.content.substring(0, 125) + '...' - : message.thread.lastMessage.content - } - context={{ ...context, type: RenderType.REPLY }} - /> - - ) : ( - `Thread messages not saved.` - )} - - )} - - ); -} +import { + DiscordAttachments, + DiscordCommand, + DiscordMessage as DiscordMessageComponent, + DiscordReaction, + DiscordReactions, + DiscordThread, + DiscordThreadMessage, +} from '@derockdev/discord-components-react'; +import type { Message as MessageType } from 'discord.js'; +import React from 'react'; +import type { RenderMessageContext } from '..'; +import { parseDiscordEmoji } from '../../utils/utils'; +import { Attachments } from './attachment'; +import ComponentRow from './components'; +import MessageContent, { RenderType } from './content'; +import { DiscordEmbed } from './embed'; +import MessageReply from './reply'; +import DiscordSystemMessage from './systemMessage'; + +export default async function DiscordMessage({ + message, + context, +}: { + message: MessageType; + context: RenderMessageContext; +}) { + if (message.system) return ; + + const isCrosspost = message.reference && message.reference.guildId !== message.guild?.id; + + return ( + + {/* reply */} + + + {/* slash command */} + {message.interaction && ( + + )} + + {/* message content */} + {message.content && ( + + )} + + {/* attachments */} + + + {/* message embeds */} + {message.embeds.map((embed, id) => ( + + ))} + + {/* components */} + {message.components.length > 0 && ( + + {message.components.map((component, id) => ( + + ))} + + )} + + {/* reactions */} + {message.reactions.cache.size > 0 && ( + + {message.reactions.cache.map((reaction, id) => ( + + ))} + + )} + + {/* threads */} + {message.hasThread && message.thread && ( + 1 ? 's' : ''}` + : 'View Thread' + } + > + {message.thread.lastMessage ? ( + + 128 + ? message.thread.lastMessage.content.substring(0, 125) + '...' + : message.thread.lastMessage.content + } + context={{ ...context, type: RenderType.REPLY }} + /> + + ) : ( + `Thread messages not saved.` + )} + + )} + + ); +} diff --git a/src/generator/renderers/reply.tsx b/src/generator/renderers/reply.tsx index c71e3276..0d2016be 100644 --- a/src/generator/renderers/reply.tsx +++ b/src/generator/renderers/reply.tsx @@ -1,45 +1,45 @@ -import { DiscordReply } from '@derockdev/discord-components-react'; -import { type Message, UserFlags } from 'discord.js'; -import type { RenderMessageContext } from '..'; -import React from 'react'; -import MessageContent, { RenderType } from './content'; - -export default async function MessageReply({ message, context }: { message: Message; context: RenderMessageContext }) { - if (!message.reference) return null; - if (message.reference.guildId !== message.guild?.id) return null; - - const referencedMessage = context.messages.find((m) => m.id === message.reference!.messageId); - - if (!referencedMessage) return Message could not be loaded.; - - const isCrosspost = referencedMessage.reference && referencedMessage.reference.guildId !== message.guild?.id; - const isCommand = referencedMessage.interaction !== null; - - return ( - 0} - author={ - referencedMessage.member?.nickname ?? referencedMessage.author.displayName ?? referencedMessage.author.username - } - avatar={referencedMessage.author.avatarURL({ size: 32 }) ?? undefined} - roleColor={referencedMessage.member?.displayHexColor ?? undefined} - bot={!isCrosspost && referencedMessage.author.bot} - verified={referencedMessage.author.flags?.has(UserFlags.VerifiedBot)} - op={message?.channel?.isThread?.() && referencedMessage.author.id === message?.channel?.ownerId} - server={isCrosspost ?? undefined} - command={isCommand} - > - {referencedMessage.content ? ( - - - - ) : isCommand ? ( - Click to see command. - ) : ( - Click to see attachment. - )} - - ); -} +import { DiscordReply } from '@derockdev/discord-components-react'; +import { type Message, UserFlags } from 'discord.js'; +import type { RenderMessageContext } from '..'; +import React from 'react'; +import MessageContent, { RenderType } from './content'; + +export default async function MessageReply({ message, context }: { message: Message; context: RenderMessageContext }) { + if (!message.reference) return null; + if (message.reference.guildId !== message.guild?.id) return null; + + const referencedMessage = context.messages.find((m) => m.id === message.reference!.messageId); + + if (!referencedMessage) return Message could not be loaded.; + + const isCrosspost = referencedMessage.reference && referencedMessage.reference.guildId !== message.guild?.id; + const isCommand = referencedMessage.interaction !== null; + + return ( + 0} + author={ + referencedMessage.member?.nickname ?? referencedMessage.author.displayName ?? referencedMessage.author.username + } + avatar={referencedMessage.author.avatarURL({ size: 32 }) ?? undefined} + roleColor={referencedMessage.member?.displayHexColor ?? undefined} + bot={!isCrosspost && referencedMessage.author.bot} + verified={referencedMessage.author.flags?.has(UserFlags.VerifiedBot)} + op={message?.channel?.isThread?.() && referencedMessage.author.id === message?.channel?.ownerId} + server={isCrosspost ?? undefined} + command={isCommand} + > + {referencedMessage.content ? ( + + + + ) : isCommand ? ( + Click to see command. + ) : ( + Click to see attachment. + )} + + ); +} diff --git a/src/generator/renderers/systemMessage.tsx b/src/generator/renderers/systemMessage.tsx index 66e64164..75760700 100644 --- a/src/generator/renderers/systemMessage.tsx +++ b/src/generator/renderers/systemMessage.tsx @@ -1,123 +1,123 @@ -import { DiscordReaction, DiscordReactions, DiscordSystemMessage } from '@derockdev/discord-components-react'; -import { MessageType, type GuildMember, type Message, type User } from 'discord.js'; -import React from 'react'; -import { parseDiscordEmoji } from '../../utils/utils'; - -export default async function SystemMessage({ message }: { message: Message }) { - switch (message.type) { - case MessageType.RecipientAdd: - case MessageType.UserJoin: - return ( - - - - ); - - case MessageType.ChannelPinnedMessage: - return ( - - - {message.author.displayName ?? message.author.username} - {' '} - pinned a message to this channel. - {/* reactions */} - {message.reactions.cache.size > 0 && ( - - {message.reactions.cache.map((reaction, id) => ( - - ))} - - )} - - ); - - case MessageType.GuildBoost: - case MessageType.GuildBoostTier1: - case MessageType.GuildBoostTier2: - case MessageType.GuildBoostTier3: - return ( - - - {message.author.displayName ?? message.author.username} - {' '} - boosted the server! - - ); - - case MessageType.ThreadStarterMessage: - return ( - - - {message.author.displayName ?? message.author.username} - {' '} - started a thread: {message.content} - - ); - - default: - return undefined; - } -} - -export function Highlight({ children, color }: { children: React.ReactNode; color?: string }) { - return {children}; -} - -const allJoinMessages = [ - '{user} just joined the server - glhf!', - '{user} just joined. Everyone, look busy!', - '{user} just joined. Can I get a heal?', - '{user} joined your party.', - '{user} joined. You must construct additional pylons.', - 'Ermagherd. {user} is here.', - 'Welcome, {user}. Stay awhile and listen.', - 'Welcome, {user}. We were expecting you ( ͡° ͜ʖ ͡°)', - 'Welcome, {user}. We hope you brought pizza.', - 'Welcome {user}. Leave your weapons by the door.', - 'A wild {user} appeared.', - 'Swoooosh. {user} just landed.', - 'Brace yourselves {user} just joined the server.', - '{user} just joined. Hide your bananas.', - '{user} just arrived. Seems OP - please nerf.', - '{user} just slid into the server.', - 'A {user} has spawned in the server.', - 'Big {user} showed up!', - "Where's {user}? In the server!", - '{user} hopped into the server. Kangaroo!!', - '{user} just showed up. Hold my beer.', - 'Challenger approaching - {user} has appeared!', - "It's a bird! It's a plane! Nevermind, it's just {user}.", - "It's {user}! Praise the sun! \\\\[T]/", - 'Never gonna give {user} up. Never gonna let {user} down.', - 'Ha! {user} has joined! You activated my trap card!', - 'Cheers, love! {user} is here!', - 'Hey! Listen! {user} has joined!', - "We've been expecting you {user}", - "It's dangerous to go alone, take {user}!", - "{user} has joined the server! It's super effective!", - 'Cheers, love! {user} is here!', - '{user} is here, as the prophecy foretold.', - "{user} has arrived. Party's over.", - 'Ready player {user}', - '{user} is here to kick butt and chew bubblegum. And {user} is all out of gum.', - "Hello. Is it {user} you're looking for?", -]; - -export function JoinMessage({ member, fallbackUser }: { member: GuildMember | null; fallbackUser: User }) { - const randomMessage = allJoinMessages[Math.floor(Math.random() * allJoinMessages.length)]; - - return randomMessage - .split('{user}') - .flatMap((item, i) => [ - item, - - {member?.nickname ?? fallbackUser.displayName ?? fallbackUser.username} - , - ]) - .slice(0, -1); -} +import { DiscordReaction, DiscordReactions, DiscordSystemMessage } from '@derockdev/discord-components-react'; +import { MessageType, type GuildMember, type Message, type User } from 'discord.js'; +import React from 'react'; +import { parseDiscordEmoji } from '../../utils/utils'; + +export default async function SystemMessage({ message }: { message: Message }) { + switch (message.type) { + case MessageType.RecipientAdd: + case MessageType.UserJoin: + return ( + + + + ); + + case MessageType.ChannelPinnedMessage: + return ( + + + {message.author.displayName ?? message.author.username} + {' '} + pinned a message to this channel. + {/* reactions */} + {message.reactions.cache.size > 0 && ( + + {message.reactions.cache.map((reaction, id) => ( + + ))} + + )} + + ); + + case MessageType.GuildBoost: + case MessageType.GuildBoostTier1: + case MessageType.GuildBoostTier2: + case MessageType.GuildBoostTier3: + return ( + + + {message.author.displayName ?? message.author.username} + {' '} + boosted the server! + + ); + + case MessageType.ThreadStarterMessage: + return ( + + + {message.author.displayName ?? message.author.username} + {' '} + started a thread: {message.content} + + ); + + default: + return undefined; + } +} + +export function Highlight({ children, color }: { children: React.ReactNode; color?: string }) { + return {children}; +} + +const allJoinMessages = [ + '{user} just joined the server - glhf!', + '{user} just joined. Everyone, look busy!', + '{user} just joined. Can I get a heal?', + '{user} joined your party.', + '{user} joined. You must construct additional pylons.', + 'Ermagherd. {user} is here.', + 'Welcome, {user}. Stay awhile and listen.', + 'Welcome, {user}. We were expecting you ( ͡° ͜ʖ ͡°)', + 'Welcome, {user}. We hope you brought pizza.', + 'Welcome {user}. Leave your weapons by the door.', + 'A wild {user} appeared.', + 'Swoooosh. {user} just landed.', + 'Brace yourselves {user} just joined the server.', + '{user} just joined. Hide your bananas.', + '{user} just arrived. Seems OP - please nerf.', + '{user} just slid into the server.', + 'A {user} has spawned in the server.', + 'Big {user} showed up!', + "Where's {user}? In the server!", + '{user} hopped into the server. Kangaroo!!', + '{user} just showed up. Hold my beer.', + 'Challenger approaching - {user} has appeared!', + "It's a bird! It's a plane! Nevermind, it's just {user}.", + "It's {user}! Praise the sun! \\\\[T]/", + 'Never gonna give {user} up. Never gonna let {user} down.', + 'Ha! {user} has joined! You activated my trap card!', + 'Cheers, love! {user} is here!', + 'Hey! Listen! {user} has joined!', + "We've been expecting you {user}", + "It's dangerous to go alone, take {user}!", + "{user} has joined the server! It's super effective!", + 'Cheers, love! {user} is here!', + '{user} is here, as the prophecy foretold.', + "{user} has arrived. Party's over.", + 'Ready player {user}', + '{user} is here to kick butt and chew bubblegum. And {user} is all out of gum.', + "Hello. Is it {user} you're looking for?", +]; + +export function JoinMessage({ member, fallbackUser }: { member: GuildMember | null; fallbackUser: User }) { + const randomMessage = allJoinMessages[Math.floor(Math.random() * allJoinMessages.length)]; + + return randomMessage + .split('{user}') + .flatMap((item, i) => [ + item, + + {member?.nickname ?? fallbackUser.displayName ?? fallbackUser.username} + , + ]) + .slice(0, -1); +} diff --git a/src/generator/transcript.tsx b/src/generator/transcript.tsx index 070cc2f6..7a03c2d0 100644 --- a/src/generator/transcript.tsx +++ b/src/generator/transcript.tsx @@ -1,72 +1,72 @@ -import { DiscordHeader, DiscordMessages as DiscordMessagesComponent } from '@derockdev/discord-components-react'; -import { ChannelType } from 'discord.js'; -import React from 'react'; -import type { RenderMessageContext } from '.'; -import MessageContent, { RenderType } from './renderers/content'; -import DiscordMessage from './renderers/message'; - -/** - * The core transcript component. - * Expects window.$discordMessage.profiles to be set for profile information. - * - * @param props Messages, channel details, callbacks, etc. - * @returns - */ -export default async function DiscordMessages({ messages, channel, callbacks, ...options }: RenderMessageContext) { - return ( - - {/* header */} - - {channel.isThread() ? ( - `Thread channel in ${channel.parent?.name ?? 'Unknown Channel'}` - ) : channel.isDMBased() ? ( - `Direct Messages` - ) : channel.isVoiceBased() ? ( - `Voice Text Channel for ${channel.name}` - ) : channel.type === ChannelType.GuildCategory ? ( - `Category Channel` - ) : 'topic' in channel && channel.topic ? ( - - ) : ( - `This is the start of #${channel.name} channel.` - )} - - - {/* body */} - {messages.map((message) => ( - - ))} - - {/* footer */} -
- {options.footerText - ? options.footerText - .replaceAll('{number}', messages.length.toString()) - .replaceAll('{s}', messages.length > 1 ? 's' : '') - : `Exported ${messages.length} message${messages.length > 1 ? 's' : ''}.`}{' '} - {options.poweredBy ? ( - - Powered by{' '} - - discord-html-transcripts - - . - - ) : null} -
-
- ); -} +import { DiscordHeader, DiscordMessages as DiscordMessagesComponent } from '@derockdev/discord-components-react'; +import { ChannelType } from 'discord.js'; +import React from 'react'; +import type { RenderMessageContext } from '.'; +import MessageContent, { RenderType } from './renderers/content'; +import DiscordMessage from './renderers/message'; + +/** + * The core transcript component. + * Expects window.$discordMessage.profiles to be set for profile information. + * + * @param props Messages, channel details, callbacks, etc. + * @returns + */ +export default async function DiscordMessages({ messages, channel, callbacks, ...options }: RenderMessageContext) { + return ( + + {/* header */} + + {channel.isThread() ? ( + `Thread channel in ${channel.parent?.name ?? 'Unknown Channel'}` + ) : channel.isDMBased() ? ( + `Direct Messages` + ) : channel.isVoiceBased() ? ( + `Voice Text Channel for ${channel.name}` + ) : channel.type === ChannelType.GuildCategory ? ( + `Category Channel` + ) : 'topic' in channel && channel.topic ? ( + + ) : ( + `This is the start of #${channel.name} channel.` + )} + + + {/* body */} + {messages.map((message) => ( + + ))} + + {/* footer */} +
+ {options.footerText + ? options.footerText + .replaceAll('{number}', messages.length.toString()) + .replaceAll('{s}', messages.length > 1 ? 's' : '') + : `Exported ${messages.length} message${messages.length > 1 ? 's' : ''}.`}{' '} + {options.poweredBy ? ( + + Powered by{' '} + + discord-html-transcripts + + . + + ) : null} +
+
+ ); +} From e98cada9e6268abd987a3e2f5a637d00900ac659 Mon Sep 17 00:00:00 2001 From: TheMonDon <11539895+TheMonDon@users.noreply.github.com> Date: Sat, 6 Sep 2025 12:21:16 -0500 Subject: [PATCH 3/6] Requested changes + bug fixes --- src/generator/renderers/components.tsx | 173 +++++------ src/generator/renderers/components/Button.tsx | 37 +++ .../renderers/components/Select Menu.tsx | 3 +- .../renderers/components/Thumbnail.tsx | 18 ++ src/generator/renderers/components/styles.ts | 135 +++++---- src/generator/renderers/components/utils.ts | 284 +++++++++--------- src/generator/transcript.tsx | 5 +- 7 files changed, 364 insertions(+), 291 deletions(-) create mode 100644 src/generator/renderers/components/Button.tsx create mode 100644 src/generator/renderers/components/Thumbnail.tsx diff --git a/src/generator/renderers/components.tsx b/src/generator/renderers/components.tsx index a56cc571..7c91b75a 100644 --- a/src/generator/renderers/components.tsx +++ b/src/generator/renderers/components.tsx @@ -1,11 +1,5 @@ +import { DiscordActionRow, DiscordAttachment, DiscordSpoiler } from '@derockdev/discord-components-react'; import { - DiscordActionRow, - DiscordAttachment, - DiscordButton, - DiscordSpoiler, -} from '@derockdev/discord-components-react'; -import { - ButtonStyle, ComponentType, type ThumbnailComponent, type MessageActionRowComponent, @@ -18,9 +12,12 @@ import DiscordContainer from './components/Container'; import DiscordSection from './components/section/Section'; import DiscordMediaGallery from './components/Media Gallery'; import DiscordSeperator from './components/Spacing'; +import DiscordButton from './components/Button'; +import DiscordThumbnail from './components/Thumbnail'; import MessageContent from './content'; import { RenderType } from './content'; import type { RenderMessageContext } from '..'; +import { ButtonStyleMapping } from './components/styles'; export default function ComponentRow({ component, @@ -31,75 +28,77 @@ export default function ComponentRow({ id: number; context: RenderMessageContext; }) { - if (component.type === ComponentType.ActionRow) { - return ( - + switch (component.type) { + case ComponentType.ActionRow: + return ( + + <> + {component.components.map((nestedComponent, id) => ( + + ))} + + + ); + + case ComponentType.Container: + return ( + + <> + {component.components.map((nestedComponent, id) => ( + + ))} + + + ); + + case ComponentType.File: + return ( <> - {component.components.map((nestedComponent, id) => ( - - ))} + {component.spoiler ? ( + + + + ) : ( + + )} - - ); - } + ); - if (component.type === ComponentType.Container) { - return ( - - <> + case ComponentType.MediaGallery: + return ; + + case ComponentType.Section: + return ( + {component.components.map((nestedComponent, id) => ( ))} - - - ); - } + + ); - if (component.type === ComponentType.File) { - return ( - <> - {component.spoiler ? ( - - - - ) : ( - - )} - - ); - } + case ComponentType.Separator: + return ; - if (component.type === ComponentType.MediaGallery) { - return ; - } - - if (component.type === ComponentType.Section) { - return ( - - {component.components.map((nestedComponent, id) => ( - - ))} - - ); - } - - if (component.type === ComponentType.Separator) { - return ; - } + case ComponentType.TextDisplay: + return ; - if (component.type === ComponentType.TextDisplay) { - return ; + default: + return null; } } -const ButtonStyleMapping = { - [ButtonStyle.Primary]: 'primary', - [ButtonStyle.Secondary]: 'secondary', - [ButtonStyle.Success]: 'success', - [ButtonStyle.Danger]: 'destructive', - [ButtonStyle.Link]: 'secondary', -} as const; - export function Component({ component, id, @@ -107,28 +106,30 @@ export function Component({ component: MessageActionRowComponent | ThumbnailComponent; id: number; }) { - if (component.type === ComponentType.Button) { - return ( - - {component.label} - - ); - } + switch (component.type) { + case ComponentType.Button: + return ( + + {component.label} + + ); - if ( - component.type === ComponentType.StringSelect || - component.type === ComponentType.UserSelect || - component.type === ComponentType.RoleSelect || - component.type === ComponentType.MentionableSelect || - component.type === ComponentType.ChannelSelect - ) { - return ; - } + case ComponentType.StringSelect: + case ComponentType.UserSelect: + case ComponentType.RoleSelect: + case ComponentType.MentionableSelect: + case ComponentType.ChannelSelect: + return ; - return undefined; + case ComponentType.Thumbnail: + return ; + + default: + return undefined; + } } diff --git a/src/generator/renderers/components/Button.tsx b/src/generator/renderers/components/Button.tsx new file mode 100644 index 00000000..e278a4b2 --- /dev/null +++ b/src/generator/renderers/components/Button.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +interface DiscordButtonProps { + type: string; + url?: string; + emoji?: string; + children: React.ReactNode; +} + +export const DiscordButton: React.FC = ({ type, url, emoji, children }) => { + return ( + + {emoji && ( + + emoji + + )} + {children} + {url && ( + + + + + + + )} + + ); +}; + +export default DiscordButton; diff --git a/src/generator/renderers/components/Select Menu.tsx b/src/generator/renderers/components/Select Menu.tsx index d0747392..7ff481f1 100644 --- a/src/generator/renderers/components/Select Menu.tsx +++ b/src/generator/renderers/components/Select Menu.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { type MessageActionRowComponent, ComponentType } from 'discord.js'; import { parseDiscordEmoji } from '../../../utils/utils'; import { getSelectTypeLabel } from './utils'; -import { selectMenuStyle } from './styles'; const DiscordSelectMenu: React.FC<{ component: Exclude; @@ -11,7 +10,7 @@ const DiscordSelectMenu: React.FC<{ const placeholder = component.placeholder || getSelectTypeLabel(component.type); return ( -
+
{placeholder}
diff --git a/src/generator/renderers/components/Thumbnail.tsx b/src/generator/renderers/components/Thumbnail.tsx new file mode 100644 index 00000000..08ee2f5d --- /dev/null +++ b/src/generator/renderers/components/Thumbnail.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const DiscordThumbnail: React.FC<{ url: string }> = ({ url }) => { + return ( + Thumbnail + ); +}; + +export default DiscordThumbnail; diff --git a/src/generator/renderers/components/styles.ts b/src/generator/renderers/components/styles.ts index 1d52d9a8..25593b2e 100644 --- a/src/generator/renderers/components/styles.ts +++ b/src/generator/renderers/components/styles.ts @@ -2,21 +2,21 @@ import type { CSSProperties } from 'react'; import { ButtonStyle } from 'discord.js'; // Container styles -export const containerStyle: CSSProperties = { +export const containerStyle = { display: 'grid', gap: '4px', width: '100%', maxWidth: '500px', borderRadius: '8px', overflow: 'hidden', -}; +} satisfies CSSProperties; // Base image style -export const baseImageStyle: CSSProperties = { +export const baseImageStyle = { overflow: 'hidden', position: 'relative', background: '#2b2d31', -}; +} satisfies CSSProperties; // Button style mapping export const ButtonStyleMapping = { @@ -27,57 +27,78 @@ export const ButtonStyleMapping = { [ButtonStyle.Link]: 'secondary', } as const; -// Get button style based on type -export const getButtonStyle = (type: string): CSSProperties => ({ - backgroundColor: - type === 'primary' - ? 'hsl(234.935 calc(1*85.556%) 64.706% /1)' - : type === 'secondary' - ? 'hsl(240 calc(1*4%) 60.784% /0.12156862745098039)' - : type === 'success' - ? 'hsl(145.97 calc(1*100%) 26.275% /1)' - : type === 'destructive' - ? 'hsl(355.636 calc(1*64.706%) 50% /1)' - : '#2b2d31', - color: '#ffffff', - padding: '2px 16px', - borderRadius: '8px', - textDecoration: 'none', - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: '14px', - fontWeight: '500', - height: '32px', - minHeight: '32px', - minWidth: '60px', - cursor: 'pointer', - fontFamily: 'Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif', - textAlign: 'center', - boxSizing: 'border-box', - border: 'none', - outline: 'none', - transition: 'background-color 0.2s ease', -}); +export const globalStyles = ` + .discord-container { + display: grid; + gap: 4px; + width: 100%; + max-width: 500px; + border-radius: 8px; + overflow: hidden; + } -// Select menu styles -export const selectMenuStyle: CSSProperties = { - marginTop: '2px', - marginBottom: '2px', - position: 'relative', - width: '100%', - maxWidth: '500px', - height: '40px', - backgroundColor: '#2b2d31', - borderRadius: '4px', - color: '#b5bac1', - cursor: 'pointer', - fontFamily: 'Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif', - fontSize: '14px', - display: 'flex', - alignItems: 'center', - padding: '0 8px', - justifyContent: 'space-between', - boxSizing: 'border-box', - border: '1px solid #1e1f22', -}; + .discord-base-image { + overflow: hidden; + position: relative; + background: #2b2d31; + } + + .discord-button { + color: #ffffff !important; + padding: 2px 16px; + border-radius: 8px; + text-decoration: none !important; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 500; + height: 32px; + min-height: 32px; + min-width: 60px; + cursor: pointer; + font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif; + text-align: center; + box-sizing: border-box; + border: none; + outline: none; + transition: background-color 0.2s ease; + } + + .discord-button-primary { + background-color: hsl(234.935 calc(1*85.556%) 64.706% /1); + } + + .discord-button-secondary { + background-color: hsl(240 calc(1*4%) 60.784% /0.12156862745098039); + } + + .discord-button-success { + background-color: hsl(145.97 calc(1*100%) 26.275% /1); + } + + .discord-button-destructive { + background-color: hsl(355.636 calc(1*64.706%) 50% /1); + } + + .discord-select-menu { + margin-top: 2px; + margin-bottom: 2px; + position: relative; + width: 100%; + max-width: 500px; + height: 40px; + background-color: #2b2d31; + border-radius: 4px; + color: #b5bac1; + cursor: pointer; + font-family: Whitney, "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + display: flex; + align-items: center; + padding: 0 8px; + justify-content: space-between; + box-sizing: border-box; + border: 1px solid #1e1f22; + } +`; diff --git a/src/generator/renderers/components/utils.ts b/src/generator/renderers/components/utils.ts index 9eda3fb8..8f48f775 100644 --- a/src/generator/renderers/components/utils.ts +++ b/src/generator/renderers/components/utils.ts @@ -1,169 +1,167 @@ import { ComponentType } from 'discord.js'; -import type { CSSProperties } from 'react'; import { baseImageStyle, containerStyle } from './styles'; /** * Gets the appropriate label for different select menu types */ +const SELECT_LABEL_MAP = { + [ComponentType.UserSelect]: 'Select User', + [ComponentType.RoleSelect]: 'Select Role', + [ComponentType.MentionableSelect]: 'Select Mentionable', + [ComponentType.ChannelSelect]: 'Select Channel', + [ComponentType.StringSelect]: 'Make a Selection', +} as const; + export const getSelectTypeLabel = (type: ComponentType): string => { - switch (type) { - case ComponentType.UserSelect: - return 'Select User'; - case ComponentType.RoleSelect: - return 'Select Role'; - case ComponentType.MentionableSelect: - return 'Select Mentionable'; - case ComponentType.ChannelSelect: - return 'Select Channel'; - case ComponentType.StringSelect: - return 'Make a Selection'; - default: - return 'Select Option'; - } + return SELECT_LABEL_MAP[type as keyof typeof SELECT_LABEL_MAP] ?? 'Select Option'; }; /** * Gets the grid layout for media galleries based on count */ -export const getGalleryLayout = (count: number): CSSProperties => { - if (count === 1) { - return { - ...containerStyle, - gridTemplateColumns: '1fr', - gridTemplateRows: 'auto', - }; - } else if (count === 2) { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr', - gridTemplateRows: 'auto', - }; - } else if (count === 3 || count === 4) { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr', - gridTemplateRows: '1fr 1fr', - }; - } else if (count === 5) { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr 1fr', - gridTemplateRows: 'auto auto', - }; - } else if (count >= 7) { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr 1fr', - gridTemplateRows: 'auto auto auto auto', - }; - } else { - return { - ...containerStyle, - gridTemplateColumns: '1fr 1fr 1fr', - gridTemplateRows: 'auto', - }; - } -}; - -/** - * Gets the style for an individual image based on its position and total count - */ -export const getImageStyle = (idx: number, count: number): CSSProperties => { - if (count === 3 && idx === 0) { - return { - ...baseImageStyle, - gridRow: '1 / span 2', - gridColumn: '1', - aspectRatio: '1/2', - }; - } - - if (count === 5) { - if (idx < 2) { - return { - ...baseImageStyle, - gridRow: '1', - gridColumn: idx === 0 ? '1 / span 2' : '3', - }; - } else { +export const getGalleryLayout = (count: number) => { + switch (count) { + case 1: return { - ...baseImageStyle, - gridRow: '2', - gridColumn: `${idx - 2 + 1}`, + ...containerStyle, + gridTemplateColumns: '1fr', + gridTemplateRows: 'auto', }; - } - } - - if (count === 7) { - if (idx === 0) { + case 2: return { - ...baseImageStyle, - gridRow: '1', - gridColumn: '1 / span 3', + ...containerStyle, + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: 'auto', }; - } else if (idx <= 3) { + case 3: + case 4: return { - ...baseImageStyle, - gridRow: '2', - gridColumn: `${idx - 0}`, + ...containerStyle, + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: '1fr 1fr', }; - } else { + case 5: return { - ...baseImageStyle, - gridRow: '3', - gridColumn: `${idx - 3}`, + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto auto', }; - } + default: + if (count >= 7) { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto auto auto auto', + }; + } else { + return { + ...containerStyle, + gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateRows: 'auto', + }; + } } +}; - if (count === 8) { - if (idx < 2) { - return { - ...baseImageStyle, - gridRow: '1', - gridColumn: idx === 0 ? '1 / span 2' : '3', - }; - } else if (idx < 5) { - return { - ...baseImageStyle, - gridRow: '2', - gridColumn: `${idx - 2 + 1}`, - }; - } else { - return { - ...baseImageStyle, - gridRow: '3', - gridColumn: `${idx - 5 + 1}`, - }; - } - } +/** + * Gets the style for an individual image based on its position and total count + */ +export const getImageStyle = (idx: number, count: number) => { + switch (count) { + case 3: + if (idx === 0) { + return { + ...baseImageStyle, + gridRow: '1 / span 2', + gridColumn: '1', + aspectRatio: '1/2', + }; + } + break; - if (count === 10) { - if (idx === 0) { - return { - ...baseImageStyle, - gridRow: '1', - gridColumn: '1 / span 3', - }; - } else if (idx <= 3) { - return { - ...baseImageStyle, - gridRow: '2', - gridColumn: `${idx - 0}`, - }; - } else if (idx <= 6) { - return { - ...baseImageStyle, - gridRow: '3', - gridColumn: `${idx - 3}`, - }; - } else { - return { - ...baseImageStyle, - gridRow: '4', - gridColumn: `${idx - 6}`, - }; - } + case 5: + if (idx < 2) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: idx === 0 ? '1 / span 2' : '3', + }; + } else { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 2 + 1}`, + }; + } + + case 7: + if (idx === 0) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: '1 / span 3', + }; + } else if (idx <= 3) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 0}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 3}`, + }; + } + + case 8: + if (idx < 2) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: idx === 0 ? '1 / span 2' : '3', + }; + } else if (idx < 5) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 2 + 1}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 5 + 1}`, + }; + } + + case 10: + if (idx === 0) { + return { + ...baseImageStyle, + gridRow: '1', + gridColumn: '1 / span 3', + }; + } else if (idx <= 3) { + return { + ...baseImageStyle, + gridRow: '2', + gridColumn: `${idx - 0}`, + }; + } else if (idx <= 6) { + return { + ...baseImageStyle, + gridRow: '3', + gridColumn: `${idx - 3}`, + }; + } else { + return { + ...baseImageStyle, + gridRow: '4', + gridColumn: `${idx - 6}`, + }; + } } return baseImageStyle; diff --git a/src/generator/transcript.tsx b/src/generator/transcript.tsx index 7a03c2d0..f2122da2 100644 --- a/src/generator/transcript.tsx +++ b/src/generator/transcript.tsx @@ -4,6 +4,7 @@ import React from 'react'; import type { RenderMessageContext } from '.'; import MessageContent, { RenderType } from './renderers/content'; import DiscordMessage from './renderers/message'; +import { globalStyles } from './renderers/components/styles'; /** * The core transcript component. @@ -15,7 +16,7 @@ import DiscordMessage from './renderers/message'; export default async function DiscordMessages({ messages, channel, callbacks, ...options }: RenderMessageContext) { return ( - {/* header */} +