Skip to content

Commit

Permalink
feat: Button block
Browse files Browse the repository at this point in the history
  • Loading branch information
shreyashkgupta committed Dec 12, 2024
1 parent e93528b commit 35b3c9a
Show file tree
Hide file tree
Showing 15 changed files with 520 additions and 42 deletions.
103 changes: 103 additions & 0 deletions packages/akiradocs/src/components/blocks/ButtonBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import { useState } from "react"

interface ButtonBlockProps {
content: string
metadata?: {
buttonUrl?: string
buttonStyle?: {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
size?: 'default' | 'sm' | 'lg'
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
}
}
isEditing?: boolean
onUpdate?: (content: string) => void
align?: 'left' | 'center' | 'right'
}

export function ButtonBlock({
content,
metadata,
isEditing,
onUpdate,
align = 'left'
}: ButtonBlockProps) {
const [isFocused, setIsFocused] = useState(false);
const buttonStyle = metadata?.buttonStyle || {}
const url = metadata?.buttonUrl || '#'

const buttonClasses = cn(
"relative",
{
'rounded-none': buttonStyle.radius === 'none',
'rounded-sm': buttonStyle.radius === 'sm',
'rounded-md': buttonStyle.radius === 'md',
'rounded-lg': buttonStyle.radius === 'lg',
'rounded-full': buttonStyle.radius === 'full',
'w-full': buttonStyle.size === 'lg',
'w-24': buttonStyle.size === 'sm',
'w-auto': buttonStyle.size === 'default'
}
)

const wrapperClasses = cn(
'w-full',
{
'text-left': align === 'left',
'text-center': align === 'center',
'text-right': align === 'right'
}
)

if (isEditing) {
return (
<div className={wrapperClasses}>
<div className="relative inline-block">
<Button
variant={buttonStyle.variant || 'default'}
size={buttonStyle.size || 'default'}
className={cn(buttonClasses, isFocused && "invisible")}
type="button"
>
{content || 'Button text...'}
</Button>
<input
value={content}
onChange={(e) => onUpdate?.(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
className={cn(
buttonVariants({
variant: buttonStyle.variant || 'default',
size: buttonStyle.size || 'default'
}),
buttonClasses,
"absolute inset-0 border-0 outline-none",
!isFocused && "opacity-0"
)}
placeholder="Button text..."
/>
</div>
</div>
)
}

return (
<div className={wrapperClasses}>
<div className="relative inline-block">
<Button
variant={buttonStyle.variant || 'default'}
size={buttonStyle.size || 'default'}
className={buttonClasses}
asChild
>
<a href={url} target="_blank" rel="noopener noreferrer">
{content}
</a>
</Button>
</div>
</div>
)
}
21 changes: 21 additions & 0 deletions packages/akiradocs/src/components/blocks/SortableBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ interface SortableBlockProps {
align?: 'left' | 'center' | 'right'
type?: 'info' | 'warning' | 'success' | 'error'
title?: string
buttonUrl?: string
buttonStyle?: {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
size?: 'default' | 'sm' | 'lg'
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full'
}
}
}
updateBlock: (id: string, content: string) => void
Expand Down Expand Up @@ -514,6 +520,21 @@ export function SortableBlock({
updateBlock(block.id, JSON.stringify(updatedContent));
}
}}
showButtonControls={block.type === 'button'}
buttonMetadata={{
url: block.metadata?.buttonUrl,
style: block.metadata?.buttonStyle
}}
onButtonMetadataChange={(metadata) => {
updateBlockMetadata(block.id, {
...block.metadata,
buttonUrl: metadata.buttonUrl,
buttonStyle: {
...block.metadata?.buttonStyle,
...metadata.buttonStyle
}
})
}}
/>
)}
<BlockRenderer
Expand Down
12 changes: 12 additions & 0 deletions packages/akiradocs/src/components/editor/AIRewriteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,18 @@ const blockStyles = {
prompt: 'Rewrite the API reference to be more descriptive and engaging.'
}
],
button: [
{
value: 'descriptive',
label: 'Descriptive',
prompt: 'Rewrite the button text to be more descriptive and engaging.'
},
{
value: 'concise',
label: 'Concise',
prompt: 'Rewrite the button text to be more concise and clear.'
}
],
spacer: [
{
value: 'default',
Expand Down
9 changes: 2 additions & 7 deletions packages/akiradocs/src/components/editor/AddBlockButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,14 @@ import {
Minus,
Table,
Quote,
// ToggleLeft,
// CheckSquare,
// Video,
// Music,
// File,
// Smile,
AlertCircle,
Plus,
// Search,
Video,
Music,
File,
CheckSquare,
ArrowUpDown,
Link2,
} from 'lucide-react'

interface AddBlockButtonProps {
Expand Down Expand Up @@ -85,6 +79,7 @@ export const AddBlockButton = forwardRef<
{ type: 'file', icon: <File size={18} />, label: 'File', description: 'Upload or link to a file.', group: 'Media' },
{ type: 'checkList', icon: <CheckSquare size={18} />, label: 'To-do list', description: 'Track tasks with a to-do list.', group: 'Basic' },
{ type: 'spacer', icon: <ArrowUpDown size={18} />, label: 'Spacing', description: 'Add vertical space between blocks.', group: 'Basic' },
{ type: 'button', icon: <Link2 size={18} />, label: 'Button', description: 'Add a clickable button with link.', group: 'Basic' },
]

const filteredOptions = blockOptions.filter((option) =>
Expand Down
97 changes: 96 additions & 1 deletion packages/akiradocs/src/components/editor/BlockFormatToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ interface BlockFormatToolbarProps {
caption: string;
alignment: 'left' | 'center' | 'right';
}>) => void;
showButtonControls?: boolean;
buttonMetadata?: {
url?: string;
style?: {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg';
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
};
};
onButtonMetadataChange?: (metadata: Partial<{
buttonUrl: string;
buttonStyle: {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
size?: 'default' | 'sm' | 'lg';
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
};
}>) => void;
}

export function BlockFormatToolbar({
Expand Down Expand Up @@ -119,6 +136,9 @@ export function BlockFormatToolbar({
showAudioControls = false,
audioContent,
onAudioMetadataChange,
showButtonControls = false,
buttonMetadata,
onButtonMetadataChange,
}: BlockFormatToolbarProps) {
if (blockType === 'file' || blockType === 'spacer') {
return null;
Expand All @@ -133,7 +153,7 @@ export function BlockFormatToolbar({
"bg-popover border shadow-md rounded-md",
className
)}>
{!showImageControls && !showCodeControls && !showVideoControls && !showAudioControls && blockType !== 'table' && (
{!showImageControls && !showCodeControls && !showVideoControls && !showAudioControls && !showButtonControls && blockType !== 'table' && (
<>
<ToggleGroup
type="multiple"
Expand Down Expand Up @@ -360,6 +380,81 @@ export function BlockFormatToolbar({
</>
)}

{showButtonControls && (
<>
<Separator orientation="vertical" className="mx-0.5 h-7" />

<Input
value={buttonMetadata?.url || ''}
onChange={(e) => onButtonMetadataChange?.({ buttonUrl: e.target.value })}
placeholder="URL"
className="h-7 w-32 text-xs"
/>

<Select
value={buttonMetadata?.style?.variant || 'default'}
onValueChange={(value) => onButtonMetadataChange?.({
buttonStyle: {
...buttonMetadata?.style,
variant: value as any
}
})}
>
<SelectTrigger className="h-7 w-24">
<SelectValue placeholder="Style" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="destructive">Destructive</SelectItem>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="secondary">Secondary</SelectItem>
<SelectItem value="ghost">Ghost</SelectItem>
<SelectItem value="link">Link</SelectItem>
</SelectContent>
</Select>

<Select
value={buttonMetadata?.style?.size || 'default'}
onValueChange={(value) => onButtonMetadataChange?.({
buttonStyle: {
...buttonMetadata?.style,
size: value as any
}
})}
>
<SelectTrigger className="h-7 w-20">
<SelectValue placeholder="Size" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="sm">Small</SelectItem>
<SelectItem value="lg">Large</SelectItem>
</SelectContent>
</Select>

<Select
value={buttonMetadata?.style?.radius || 'md'}
onValueChange={(value) => onButtonMetadataChange?.({
buttonStyle: {
...buttonMetadata?.style,
radius: value as any
}
})}
>
<SelectTrigger className="h-7 w-20">
<SelectValue placeholder="Radius" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Square</SelectItem>
<SelectItem value="sm">Small</SelectItem>
<SelectItem value="md">Medium</SelectItem>
<SelectItem value="lg">Large</SelectItem>
<SelectItem value="full">Pill</SelectItem>
</SelectContent>
</Select>
</>
)}

{!showImageControls && !showVideoControls && !showAudioControls && (
<>
{!showCalloutControls && blockType !== 'table' && <Separator orientation="vertical" className="mx-0.5 h-7" />}
Expand Down
28 changes: 13 additions & 15 deletions packages/akiradocs/src/lib/renderers/BlockRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,15 @@ import { List } from "@/components/blocks/ListBlock"
import { Blockquote } from "@/components/blocks/BlockquoteBlock"
import { Divider } from "@/components/blocks/DividerBlock"
import { CodeBlock } from "@/components/blocks/CodeBlock"
// import { Image } from '../blocks/Image'
// import { Table } from '../blocks/Table'
// import { ToggleList } from '../blocks/ToggleList'
import { CheckList } from "@/components/blocks/CheckListBlock"
// import { Video } from '../blocks/Video'
// import { Audio } from '../blocks/Audio'
// import { File } from '../blocks/File'
// import { Emoji } from '../blocks/Emoji'
import { Callout } from "@/components/blocks/CalloutBlock"
import { cn } from '@/lib/utils'
import { ErrorBoundary } from 'react-error-boundary'
import { ImageBlock } from "@/components/blocks/ImageBlock"
import { Table } from '@/components/blocks/TableBlock'
import { VideoBlock } from "@/components/blocks/VideoBlock"
import { AudioBlock } from "@/components/blocks/AudioBlock"
import { FileBlock } from "@/components/blocks/FileBlock"
import { SpacerBlock } from "@/components/blocks/SpacerBlock"
import { ButtonBlock } from "@/components/blocks/ButtonBlock"

interface ImageBlockContent {
url: string;
Expand Down Expand Up @@ -65,20 +57,16 @@ export function BlockRenderer({ block, isEditing, onUpdate }: BlockRendererProps
</Paragraph>
);
case 'heading':
const hasStrong = block.content.includes('<strong>');
return (
<HeadingTitle
{...commonProps}
level={block.metadata?.level || 1}
align={block.metadata?.align}
styles={{
...block.metadata?.styles,
bold: hasStrong ? true : false
}}
styles={block.metadata?.styles}
isEditing={isEditing}
onUpdate={(content) => onUpdate?.(block.id, content)}
>
{hasStrong ? block.content.replace(/<\/?strong>/g, '') : block.content}
{block.content}
</HeadingTitle>
);
case 'list':
Expand Down Expand Up @@ -243,6 +231,16 @@ export function BlockRenderer({ block, isEditing, onUpdate }: BlockRendererProps
onUpdate={(size) => onUpdate?.(block.id, size)}
/>
);
case 'button':
return (
<ButtonBlock
content={block.content}
metadata={block.metadata}
isEditing={isEditing}
onUpdate={(content) => onUpdate?.(block.id, content)}
align={block.metadata?.align}
/>
);
default:
return null
}
Expand Down
Loading

0 comments on commit 35b3c9a

Please sign in to comment.