Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 107 additions & 12 deletions src/generator/renderers/components.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,95 @@
import { DiscordActionRow, DiscordButton } from '@derockdev/discord-components-react';
import { ButtonStyle, ComponentType, type MessageActionRowComponent, type ActionRow } from 'discord.js';
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({ row, id }: { row: ActionRow<MessageActionRowComponent>; id: number }) {
return (
<DiscordActionRow key={id}>
{row.components.map((component, id) => (
<Component component={component} id={id} key={id} />
))}
</DiscordActionRow>
);
export default function ComponentRow({
component,
id,
context,
}: {
component: TopLevelComponent;
id: number;
context: RenderMessageContext;
}) {
if (component.type === ComponentType.ActionRow) {
return (
<DiscordActionRow key={id}>
<>
{component.components.map((nestedComponent, id) => (
<Component component={nestedComponent} id={id} key={id} />
))}
</>
</DiscordActionRow>
);
}

if (component.type === ComponentType.Container) {
return (
<DiscordContainer key={id}>
<>
{component.components.map((nestedComponent, id) => (
<ComponentRow component={nestedComponent} id={id} key={id} context={context} />
))}
</>
</DiscordContainer>
);
}

if (component.type === ComponentType.File) {
return (
<>
{component.spoiler ? (
<DiscordSpoiler key={component.id} slot="attachment">
<DiscordAttachment type="file" key={component.id} slot="attachment" url={component.file.url} />
</DiscordSpoiler>
) : (
<DiscordAttachment type="file" key={component.id} slot="attachment" url={component.file.url} />
)}
</>
);
}

if (component.type === ComponentType.MediaGallery) {
return <DiscordMediaGallery component={component} key={id} />;
}

if (component.type === ComponentType.Section) {
return (
<DiscordSection key={id} accessory={component.accessory} id={id}>
{component.components.map((nestedComponent, id) => (
<ComponentRow component={nestedComponent} id={id} key={id} context={context} />
))}
</DiscordSection>
);
}

if (component.type === ComponentType.Separator) {
return <DiscordSeperator key={id} spacing={component.spacing} divider={component.divider} />;
}

if (component.type === ComponentType.TextDisplay) {
return <MessageContent key={id} content={component.content} context={{ ...context, type: RenderType.NORMAL }} />;
}
}
Comment thread
TheMonDon marked this conversation as resolved.

const ButtonStyleMapping = {
Expand All @@ -21,12 +100,18 @@ const ButtonStyleMapping = {
[ButtonStyle.Link]: 'secondary',
} as const;

export function Component({ component, id }: { component: MessageActionRowComponent; id: number }) {
export function Component({
component,
id,
}: {
component: MessageActionRowComponent | ThumbnailComponent;
id: number;
}) {
if (component.type === ComponentType.Button) {
return (
<DiscordButton
key={id}
type={ButtonStyleMapping[component.style]}
type={ButtonStyleMapping[component.style as keyof typeof ButtonStyleMapping]}
url={component.url ?? undefined}
emoji={component.emoji ? parseDiscordEmoji(component.emoji) : undefined}
>
Expand All @@ -35,5 +120,15 @@ export function Component({ component, id }: { component: MessageActionRowCompon
);
}

if (
component.type === ComponentType.StringSelect ||
component.type === ComponentType.UserSelect ||
component.type === ComponentType.RoleSelect ||
component.type === ComponentType.MentionableSelect ||
component.type === ComponentType.ChannelSelect
) {
return <DiscordSelectMenu key={id} component={component} />;
}

Comment thread
TheMonDon marked this conversation as resolved.
Outdated
return undefined;
}
24 changes: 24 additions & 0 deletions src/generator/renderers/components/Container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';

const DiscordContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div
style={{
display: 'flex',
width: '500px',
flexDirection: 'column',
backgroundColor: '#3f4248',
padding: '16px',
border: '1px solid #4f5359',
marginTop: '2px',
marginBottom: '2px',
borderRadius: '10px',
gap: '8px',
}}
>
{children}
</div>
);
};

export default DiscordContainer;
53 changes: 53 additions & 0 deletions src/generator/renderers/components/Media Gallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +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 (
<div style={getGalleryLayout(count)}>
{imagesToShow.map((media, idx) => (
<div key={idx} style={getImageStyle(idx, count)}>
<img
src={media.media.url}
alt={media.description || 'Media content'}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
{hasMore && idx === imagesToShow.length - 1 && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
color: 'white',
fontSize: '20px',
fontWeight: 'bold',
}}
>
+{component.items.length - 10}
</div>
)}
</div>
))}
</div>
);
};

export default DiscordMediaGallery;
58 changes: 58 additions & 0 deletions src/generator/renderers/components/Select Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +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<{
Comment thread
TheMonDon marked this conversation as resolved.
Outdated
component: Exclude<MessageActionRowComponent, { type: ComponentType.Button }>;
}> = ({ component }) => {
const isStringSelect = component.type === ComponentType.StringSelect;
const placeholder = component.placeholder || getSelectTypeLabel(component.type);

return (
<div style={selectMenuStyle}>
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{placeholder}</div>
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '8px' }}>
<svg width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M7 10L12 15L17 10H7Z" />
</svg>
</div>
{isStringSelect && component.options && component.options.length > 0 && (
<div
style={{
display: 'none',
position: 'absolute',
top: '44px',
left: '0',
width: '100%',
backgroundColor: '#2b2d31',
borderRadius: '4px',
zIndex: 10,
border: '1px solid #1e1f22',
maxHeight: '320px',
overflowY: 'auto',
}}
>
{component.options.map((option, idx) => (
<div
key={idx}
style={{
padding: '8px 12px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
borderBottom: idx < component.options.length - 1 ? '1px solid #1e1f22' : 'none',
}}
>
{option.emoji && <span style={{ marginRight: '8px' }}>{parseDiscordEmoji(option.emoji)}</span>}
<span>{option.label}</span>
</div>
))}
</div>
)}
</div>
);
};

export default DiscordSelectMenu;
17 changes: 17 additions & 0 deletions src/generator/renderers/components/Spacing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { SeparatorSpacingSize } from 'discord.js';

const DiscordSeperator: React.FC<{ divider: boolean; spacing: SeparatorSpacingSize }> = ({ divider, spacing }) => {
return (
<div
style={{
width: '100%',
height: divider ? '1px' : '0px',
backgroundColor: '#4f5359',
margin: spacing === SeparatorSpacingSize.Large ? '8px 0' : '0',
}}
/>
);
};

export default DiscordSeperator;
29 changes: 29 additions & 0 deletions src/generator/renderers/components/section/Section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +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<DiscordSectionProps> = ({ children, accessory, id }) => {
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
maxWidth: '500px',
}}
>
<SectionContent>{children}</SectionContent>
<SectionAccessory>{accessory && <Component component={accessory} id={id} />}</SectionAccessory>
</div>
);
};

export default DiscordSection;
25 changes: 25 additions & 0 deletions src/generator/renderers/components/section/SectionAccessory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';

interface SectionAccessoryProps {
children?: React.ReactNode;
}

const SectionAccessory: React.FC<SectionAccessoryProps> = ({ children }) => {
if (!children) return null;

return (
<div
style={{
display: 'flex',
width: '100%',
maxWidth: '500px',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
{children}
</div>
);
};

export default SectionAccessory;
21 changes: 21 additions & 0 deletions src/generator/renderers/components/section/SectionContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

interface SectionContentProps {
children: React.ReactNode;
}

const SectionContent: React.FC<SectionContentProps> = ({ children }) => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
}}
>
{children}
</div>
);
};

export default SectionContent;
Loading
Loading