-
- {name}
-
+
setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+
+
+
+
+
+ {formatFileType(fileContent.fileType)}
+
+
+
+ {isEditing && isHovered && (
+
+
+
+
+ )}
+
- );
-}
+ )
+}
\ No newline at end of file
diff --git a/packages/editor/src/components/blocks/HeadingBlock.tsx b/packages/editor/src/components/blocks/HeadingBlock.tsx
index a93c186..3dfe36c 100644
--- a/packages/editor/src/components/blocks/HeadingBlock.tsx
+++ b/packages/editor/src/components/blocks/HeadingBlock.tsx
@@ -40,6 +40,7 @@ export function HeadingTitle({ id, level, children, align = 'left', styles, isEd
className={cn(
`font-bold mb-2 py-4 ${sizeClasses[level as keyof typeof sizeClasses]} ${alignClass}`,
'focus:outline-none rounded-md',
+ 'max-w-4xl break-words',
styles?.italic && 'italic',
styles?.underline && 'underline'
)}
@@ -53,7 +54,8 @@ export function HeadingTitle({ id, level, children, align = 'left', styles, isEd
{children}
diff --git a/packages/editor/src/components/blocks/ImageBlock.tsx b/packages/editor/src/components/blocks/ImageBlock.tsx
index 1c465d1..ff73a68 100644
--- a/packages/editor/src/components/blocks/ImageBlock.tsx
+++ b/packages/editor/src/components/blocks/ImageBlock.tsx
@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { ImagePlus } from 'lucide-react'
import { useState } from 'react'
+import { saveImageToPublic } from '@/lib/fileUtils'
interface ImageBlockProps {
content: string
@@ -26,17 +27,21 @@ export function ImageBlock({ content, id, onUpdate, isEditing, metadata }: Image
const file = e.target.files?.[0]
if (!file) return
- const url = URL.createObjectURL(file)
-
- const imageContent = JSON.stringify({
- url,
- alt: metadata?.alt || file.name,
- caption: metadata?.caption,
- alignment: metadata?.alignment || 'center',
- size: metadata?.size || 'medium'
- })
+ try {
+ const filename = await saveImageToPublic(file, content)
+
+ const imageContent = JSON.stringify({
+ url: `/${filename}`,
+ alt: metadata?.alt || file.name,
+ caption: metadata?.caption,
+ alignment: metadata?.alignment || 'center',
+ size: metadata?.size || 'medium'
+ })
- onUpdate?.(imageContent)
+ onUpdate?.(imageContent)
+ } catch (error) {
+ console.error('Failed to upload image:', error)
+ }
}
// Helper function to parse content
@@ -97,7 +102,7 @@ export function ImageBlock({ content, id, onUpdate, isEditing, metadata }: Image
return (
)
-}
+}
\ No newline at end of file
diff --git a/packages/editor/src/components/blocks/ListBlock.tsx b/packages/editor/src/components/blocks/ListBlock.tsx
index dd74009..e42f1f3 100644
--- a/packages/editor/src/components/blocks/ListBlock.tsx
+++ b/packages/editor/src/components/blocks/ListBlock.tsx
@@ -234,7 +234,7 @@ export function List({
);
@@ -90,7 +91,8 @@ export function Paragraph({
align === 'right' && 'text-right',
styles.bold && 'font-bold',
styles.italic && 'italic',
- styles.underline && 'underline'
+ styles.underline && 'underline',
+ 'max-w-4xl break-words',
)}>
{content}
diff --git a/packages/editor/src/components/blocks/SortableBlock.tsx b/packages/editor/src/components/blocks/SortableBlock.tsx
index 9eb3de9..9afa175 100644
--- a/packages/editor/src/components/blocks/SortableBlock.tsx
+++ b/packages/editor/src/components/blocks/SortableBlock.tsx
@@ -55,6 +55,11 @@ interface ImageBlockContent {
size?: 'small' | 'medium' | 'large' | 'full'
}
+interface AudioBlockContent {
+ url: string;
+ caption?: string;
+ alignment?: 'left' | 'center' | 'right';
+}
export function SortableBlock({
block,
@@ -195,6 +200,74 @@ export function SortableBlock({
const [isRewriting, setIsRewriting] = useState(false)
+ const getVideoContent = () => {
+ if (!block.content) {
+ return {
+ url: '',
+ caption: '',
+ alignment: 'center',
+ size: 'medium'
+ }
+ }
+
+ try {
+ return typeof block.content === 'string'
+ ? JSON.parse(block.content)
+ : block.content
+ } catch {
+ return {
+ url: block.content,
+ caption: '',
+ alignment: 'center',
+ size: 'medium'
+ }
+ }
+ }
+
+ const getAudioContent = () => {
+ if (!block.content) {
+ return {
+ url: '',
+ caption: '',
+ alignment: 'center'
+ }
+ }
+
+ try {
+ return typeof block.content === 'string'
+ ? JSON.parse(block.content)
+ : block.content;
+ } catch {
+ return {
+ url: block.content,
+ caption: '',
+ alignment: 'center'
+ };
+ }
+ }
+
+ const getFileContent = () => {
+ if (!block.content) {
+ return {
+ url: '',
+ name: '',
+ fileType: ''
+ }
+ }
+
+ try {
+ return typeof block.content === 'string'
+ ? JSON.parse(block.content)
+ : block.content
+ } catch {
+ return {
+ url: block.content,
+ name: '',
+ fileType: ''
+ }
+ }
+ }
+
return showPreview ? (
) : (
@@ -267,7 +340,15 @@ export function SortableBlock({
{
+ try {
+ return typeof block.content === 'string'
+ ? JSON.parse(block.content)
+ : block.content;
+ } catch {
+ return {
+ url: block.content,
+ caption: '',
+ alignment: 'center',
+ size: 'medium'
+ };
+ }
+ })() : undefined}
+ onVideoMetadataChange={(metadata) => {
+ if (block.type === 'video') {
+ const currentContent = (() => {
+ try {
+ return typeof block.content === 'string'
+ ? JSON.parse(block.content)
+ : block.content;
+ } catch {
+ return {
+ url: block.content,
+ caption: '',
+ alignment: 'center',
+ size: 'medium'
+ };
+ }
+ })();
+
+ const updatedContent = {
+ ...currentContent,
+ ...metadata
+ };
+ updateBlock(block.id, JSON.stringify(updatedContent));
+ }
+ }}
+ showAudioControls={block.type === 'audio'}
+ audioContent={block.type === 'audio' ? getAudioContent() : undefined}
+ onAudioMetadataChange={(metadata) => {
+ if (block.type === 'audio') {
+ const currentContent = getAudioContent();
+ const updatedContent = {
+ ...currentContent,
+ ...metadata
+ };
+ updateBlock(block.id, JSON.stringify(updatedContent));
+ }
+ }}
/>
)}
void;
}
-export function Table({ id, headers, rows, align = 'left', styles }: TableProps) {
+export function Table({ id, headers, rows, align = 'left', styles, isEditing = false, onChange }: TableProps) {
+ const [isFocused, setIsFocused] = useState(false);
+
const alignClass = align === 'center' ? 'mx-auto' : align === 'right' ? 'ml-auto' : '';
+
+ const handleCellChange = (rowIndex: number, colIndex: number, value: string) => {
+ if (!onChange) return;
+
+ if (rowIndex === -1) {
+ const newHeaders = [...headers];
+ newHeaders[colIndex] = value;
+ onChange(newHeaders, rows);
+ } else {
+ const newRows = rows.map((row, i) =>
+ i === rowIndex ? row.map((cell, j) => (j === colIndex ? value : cell)) : row
+ );
+ onChange(headers, newRows);
+ }
+ };
+
+ const addColumn = () => {
+ if (!onChange) return;
+ const newHeaders = [...headers, 'New Column'];
+ const newRows = rows.map(row => [...row, '']);
+ onChange(newHeaders, newRows);
+ };
+
+ const addRow = () => {
+ if (!onChange) return;
+ const newRow = new Array(headers.length).fill('');
+ onChange(headers, [...rows, newRow]);
+ };
+
+ const removeColumn = (colIndex: number) => {
+ if (!onChange || headers.length <= 1) return;
+ const newHeaders = headers.filter((_, i) => i !== colIndex);
+ const newRows = rows.map(row => row.filter((_, i) => i !== colIndex));
+ onChange(newHeaders, newRows);
+ };
+
+ const removeRow = (rowIndex: number) => {
+ if (!onChange || rows.length <= 1) return;
+ const newRows = rows.filter((_, i) => i !== rowIndex);
+ onChange(headers, newRows);
+ };
+
return (
-
+
setIsFocused(true)}
+ onBlur={() => setIsFocused(false)}
+ tabIndex={0}
+ >
- {headers.map((header, index) => (
- |
- {header}
+ {headers.map((header, colIndex) => (
+ |
+
+ {isEditing ? (
+ handleCellChange(-1, colIndex, e.target.value)}
+ className="w-full bg-transparent focus:outline-none"
+ />
+ ) : (
+ {header}
+ )}
+ {isEditing && isFocused && headers.length > 1 && (
+
+ )}
+
|
))}
+ {isEditing && isFocused && (
+
+
+ |
+ )}
{rows.map((row, rowIndex) => (
- {row.map((cell, cellIndex) => (
- |
- {cell}
+ {row.map((cell, colIndex) => (
+ |
+ {isEditing ? (
+ handleCellChange(rowIndex, colIndex, e.target.value)}
+ className="w-full bg-transparent focus:outline-none"
+ />
+ ) : (
+ {cell}
+ )}
+ {isEditing && isFocused && colIndex === row.length - 1 && rows.length > 1 && (
+
+ )}
|
))}
+ {isEditing && isFocused && (
+
+ {rowIndex === rows.length - 1 ? (
+
+ ) : null}
+ |
+ )}
))}
diff --git a/packages/editor/src/components/blocks/VideoBlock.tsx b/packages/editor/src/components/blocks/VideoBlock.tsx
index fc7f8f7..2ff7053 100644
--- a/packages/editor/src/components/blocks/VideoBlock.tsx
+++ b/packages/editor/src/components/blocks/VideoBlock.tsx
@@ -1,32 +1,233 @@
-import React from 'react';
-import { cn } from "@/lib/utils";
+"use client"
-interface VideoProps {
- id?: string;
- src: string;
+import { cn } from '@/lib/utils'
+import { Button } from '@/components/ui/button'
+import { VideoIcon, Pause, Play } from 'lucide-react'
+import { useState, useRef, useEffect } from 'react'
+import { saveVideoToPublic } from '@/lib/fileUtils'
+
+interface VideoBlockProps {
+ content: string
+ id: string
+ onUpdate?: (content: string) => void
+ isEditing?: boolean
+ metadata?: {
+ caption?: string
+ alignment?: 'left' | 'center' | 'right'
+ size?: 'small' | 'medium' | 'large' | 'full'
+ }
+}
+
+interface VideoBlockContent {
+ url: string;
caption?: string;
- align?: 'left' | 'center' | 'right';
- styles?: {
- bold?: boolean;
- italic?: boolean;
- underline?: boolean;
- };
+ alignment?: 'left' | 'center' | 'right';
+ size?: 'small' | 'medium' | 'large' | 'full';
}
-export function Video({ id, src, caption, align = 'left', styles }: VideoProps) {
- const alignClass = align === 'center' ? 'mx-auto' : align === 'right' ? 'ml-auto' : '';
- return (
-
-
- {caption &&
{caption}
}
+export function VideoBlock({ content, id, onUpdate, isEditing, metadata }: VideoBlockProps) {
+ const [isHovered, setIsHovered] = useState(false)
+ const [isPlaying, setIsPlaying] = useState(false)
+ const videoRef = useRef
(null)
+
+ const handlePlayPause = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ if (videoRef.current) {
+ if (isPlaying) {
+ videoRef.current.pause()
+ } else {
+ videoRef.current.play()
+ }
+ setIsPlaying(!isPlaying)
+ }
+ }
+
+ // Add event listeners to sync the isPlaying state with video events
+ useEffect(() => {
+ const video = videoRef.current
+ if (!video) return
+
+ const handlePlay = () => setIsPlaying(true)
+ const handlePause = () => setIsPlaying(false)
+ const handleEnded = () => setIsPlaying(false)
+
+ video.addEventListener('play', handlePlay)
+ video.addEventListener('pause', handlePause)
+ video.addEventListener('ended', handleEnded)
+
+ return () => {
+ video.removeEventListener('play', handlePlay)
+ video.removeEventListener('pause', handlePause)
+ video.removeEventListener('ended', handleEnded)
+ }
+ }, [])
+
+ const handleFileUpload = async (e: React.ChangeEvent) => {
+ e.stopPropagation()
+ const file = e.target.files?.[0]
+ if (!file) return
+
+ try {
+ const filename = await saveVideoToPublic(file, content)
+
+ const videoContent = JSON.stringify({
+ url: `/${filename}`,
+ caption: metadata?.caption,
+ alignment: metadata?.alignment || 'center',
+ size: metadata?.size || 'medium'
+ })
+
+ onUpdate?.(videoContent)
+ } catch (error) {
+ console.error('Failed to upload video:', error)
+ }
+ }
+
+ const parseVideoContent = (content: string) => {
+ try {
+ return typeof content === 'string' ? JSON.parse(content) : content
+ } catch {
+ return {
+ url: content,
+ caption: metadata?.caption || '',
+ alignment: metadata?.alignment || 'center',
+ size: metadata?.size || 'medium'
+ }
+ }
+ }
+
+ const getVideoContent = (content: string): VideoBlockContent => {
+ try {
+ return typeof content === 'string'
+ ? JSON.parse(content)
+ : content;
+ } catch {
+ return {
+ url: content,
+ caption: '',
+ alignment: 'center',
+ size: 'medium'
+ };
+ }
+ };
+
+ const UploadButton = () => (
+
+
+
+
+
- );
-}
+ )
+
+ if (!content && isEditing) {
+ return (
+
+
+
+ )
+ }
+
+ const videoContent = parseVideoContent(content)
+ const alignment = videoContent.alignment || metadata?.alignment || 'center'
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+
+ {isEditing && isHovered && (
+
e.stopPropagation()}
+ >
+
+
+
+
+ )}
+
+ {videoContent.caption && (
+
+ {videoContent.caption}
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/packages/editor/src/components/editor/AIRewriteButton.tsx b/packages/editor/src/components/editor/AIRewriteButton.tsx
index 29d2759..eb1c654 100644
--- a/packages/editor/src/components/editor/AIRewriteButton.tsx
+++ b/packages/editor/src/components/editor/AIRewriteButton.tsx
@@ -130,6 +130,69 @@ const blockStyles = {
label: 'Detailed',
prompt: 'Expand the toggle content with more detailed explanations. Keep the toggle structure. Output only the toggle content.'
}
+ ],
+
+ table: [
+ {
+ value: 'simplify',
+ label: 'Simplify',
+ prompt: 'Rewrite this table to simplify its content and structure. Output only the table data in a simplified format.'
+ },
+ {
+ value: 'detailed',
+ label: 'Detailed',
+ prompt: 'Rewrite this table to add more detailed information and context. Output only the table data with added details.'
+ }
+ ],
+ video: [
+ {
+ value: 'descriptive',
+ label: 'Descriptive',
+ prompt: 'Rewrite the video caption to be more descriptive and engaging.'
+ },
+ {
+ value: 'concise',
+ label: 'Concise',
+ prompt: 'Rewrite the video caption to be more concise and clear.'
+ }
+ ],
+ audio: [
+ {
+ value: 'descriptive',
+ label: 'Descriptive',
+ prompt: 'Rewrite the audio caption to be more descriptive and engaging.'
+ },
+ {
+ value: 'concise',
+ label: 'Concise',
+ prompt: 'Rewrite the audio caption to be more concise and clear.'
+ }
+ ],
+ file: [
+ {
+ value: 'descriptive',
+ label: 'Descriptive',
+ prompt: 'Rewrite the file caption to be more descriptive and engaging.'
+ }
+ ],
+ checkList: [
+ {
+ value: 'descriptive',
+ label: 'Descriptive',
+ prompt: 'Rewrite the checklist to be more descriptive and engaging.'
+ },
+ {
+ value: 'concise',
+ label: 'Concise',
+ prompt: 'Rewrite the checklist to be more concise and clear.'
+ }
+ ],
+ apiReference: [
+ {
+ value: 'descriptive',
+ label: 'Descriptive',
+ prompt: 'Rewrite the API reference to be more descriptive and engaging.'
+ }
]
} as const;
diff --git a/packages/editor/src/components/editor/AddBlockButton.tsx b/packages/editor/src/components/editor/AddBlockButton.tsx
index b3dba5b..00a1770 100644
--- a/packages/editor/src/components/editor/AddBlockButton.tsx
+++ b/packages/editor/src/components/editor/AddBlockButton.tsx
@@ -14,7 +14,7 @@ import {
Image,
List,
Minus,
- // Table,
+ Table,
Quote,
// ToggleLeft,
// CheckSquare,
@@ -25,6 +25,10 @@ import {
AlertCircle,
Plus,
// Search,
+ Video,
+ Music,
+ File,
+ CheckSquare,
} from 'lucide-react'
interface AddBlockButtonProps {
@@ -49,6 +53,7 @@ export const AddBlockButton = forwardRef<
HTMLButtonElement,
AddBlockButtonProps
>(({ onAddBlock, onChangeType, mode, isActive, onOpenChange, type, open }, ref) => {
+ console.debug(isActive)
const [searchTerm, setSearchTerm] = useState('')
const searchInputRef = useRef(null)
@@ -65,7 +70,7 @@ export const AddBlockButton = forwardRef<
{ type: 'code', icon: , label: 'Code', description: 'Capture a code snippet.', group: 'Basic' },
{ type: 'image', icon: , label: 'Image', description: 'Upload or embed with a link.', group: 'Media' },
{ type: 'divider', icon: , label: 'Divider', description: 'Visually divide your page.', group: 'Basic' },
- // { type: 'table', icon: , label: 'Table', description: 'Add a table to your page.', group: 'Advanced' },
+ { type: 'table', icon: , label: 'Table', description: 'Add a table to your page.', group: 'Advanced' },
{ type: 'blockquote', icon:
, label: 'Quote', description: 'Capture a quote.', group: 'Basic' },
// { type: 'toggleList', icon: , label: 'Toggle', description: 'Toggleable content.', group: 'Advanced' },
// { type: 'checkList', icon: , label: 'To-do list', description: 'Track tasks with a to-do list.', group: 'Basic' },
@@ -74,6 +79,10 @@ export const AddBlockButton = forwardRef<
// { type: 'file', icon: , label: 'File', description: 'Upload or link to a file.', group: 'Media' },
// { type: 'emoji', icon: , label: 'Emoji', description: 'Add an emoji to your page.', group: 'Basic' },
{ type: 'callout', icon: , label: 'Callout', description: 'Make writing stand out.', group: 'Advanced' },
+ { type: 'video', icon: , label: 'Video', description: 'Upload or embed a video.', group: 'Media' },
+ { type: 'audio', icon: , label: 'Audio', description: 'Embed audio content.', group: 'Media' },
+ { type: 'file', icon: , label: 'File', description: 'Upload or link to a file.', group: 'Media' },
+ { type: 'checkList', icon: , label: 'To-do list', description: 'Track tasks with a to-do list.', group: 'Basic' },
]
const filteredOptions = blockOptions.filter((option) =>
diff --git a/packages/editor/src/components/editor/BlockFormatToolbar.tsx b/packages/editor/src/components/editor/BlockFormatToolbar.tsx
index 2bdbf3d..8a70ee5 100644
--- a/packages/editor/src/components/editor/BlockFormatToolbar.tsx
+++ b/packages/editor/src/components/editor/BlockFormatToolbar.tsx
@@ -30,7 +30,7 @@ interface BlockFormatToolbarProps {
onLanguageChange?: (language: string) => void
onFilenameChange?: (filename: string) => void
onShowLineNumbersChange?: (show: boolean) => void
- showCodeControls?: boolean
+ showCodeControls?: boolean;
showImageControls?: boolean;
imageContent?: {
url: string;
@@ -54,6 +54,28 @@ interface BlockFormatToolbarProps {
onAiRewrite?: (style: string) => Promise
isAiRewriting?: boolean
blockType?: BlockType
+ showVideoControls?: boolean;
+ videoContent?: {
+ url: string;
+ caption?: string;
+ alignment?: 'left' | 'center' | 'right';
+ size?: 'small' | 'medium' | 'large' | 'full';
+ };
+ onVideoMetadataChange?: (metadata: Partial<{
+ caption: string;
+ alignment: 'left' | 'center' | 'right';
+ size: 'small' | 'medium' | 'large' | 'full';
+ }>) => void;
+ showAudioControls?: boolean;
+ audioContent?: {
+ url: string;
+ caption?: string;
+ alignment?: 'left' | 'center' | 'right';
+ };
+ onAudioMetadataChange?: (metadata: Partial<{
+ caption: string;
+ alignment: 'left' | 'center' | 'right';
+ }>) => void;
}
export function BlockFormatToolbar({
@@ -91,7 +113,17 @@ export function BlockFormatToolbar({
onAiRewrite,
isAiRewriting,
blockType,
+ showVideoControls = false,
+ videoContent,
+ onVideoMetadataChange,
+ showAudioControls = false,
+ audioContent,
+ onAudioMetadataChange,
}: BlockFormatToolbarProps) {
+ if (blockType === 'file') {
+ return null;
+ }
+
return (
- {!showImageControls && (
+ {!showImageControls && !showCodeControls && !showVideoControls && !showAudioControls && blockType !== 'table' && (
<>
-
-
-
+ {blockType !== 'heading' && (
+
+
+
+ )}
@@ -125,26 +159,26 @@ export function BlockFormatToolbar({
-
+ {blockType !== 'blockquote' && blockType !== 'list' && blockType !== 'checkList' &&
}
>
)}
-
value && onAlignChange(value as 'left' | 'center' | 'right')} className="flex gap-0.5">
-
-
+ {!showCodeControls && !showCalloutControls && blockType !== 'blockquote' && blockType !== 'list' && blockType !== 'checkList' && blockType !== 'table' && (
+ value && onAlignChange(value as 'left' | 'center' | 'right')} className="flex gap-0.5">
+
+
-
-
+
+
+ )}
{showCodeControls && (
<>
-
-
onLanguageChange?.(e.target.value)}
@@ -282,18 +316,62 @@ export function BlockFormatToolbar({
/>
>
)}
-
+
+ {showVideoControls && (
+ <>
+
+
+
onVideoMetadataChange?.({ caption: e.target.value })}
+ placeholder="Caption"
+ className="h-7 w-32 text-xs"
+ />
+
+
+ >
+ )}
+
+ {showAudioControls && (
+ <>
+
- {/* Only show AI rewrite button if not an image block */}
- {!showImageControls && (
-
-
{})}
- isRewriting={isAiRewriting}
- />
-
- )}
+
onAudioMetadataChange?.({ caption: e.target.value })}
+ placeholder="Caption"
+ className="h-7 w-32 text-xs"
+ />
+ >
+ )}
+
+ {!showImageControls && !showVideoControls && !showAudioControls && (
+ <>
+ {!showCalloutControls && blockType !== 'table' &&
}
+
+
{})}
+ isRewriting={isAiRewriting}
+ />
+
+ >
+ )}
);
}
\ No newline at end of file
diff --git a/packages/editor/src/lib/fileUtils.ts b/packages/editor/src/lib/fileUtils.ts
new file mode 100644
index 0000000..3f74648
--- /dev/null
+++ b/packages/editor/src/lib/fileUtils.ts
@@ -0,0 +1,174 @@
+export async function saveImageToPublic(file: File, oldContent?: string): Promise {
+ if (oldContent) {
+ const oldFilename = extractFilenameFromContent(oldContent)
+ if (oldFilename) {
+ try {
+ await deleteImageFromPublic(oldFilename)
+ } catch (error) {
+ console.error('Error deleting old file:', error)
+ }
+ }
+ }
+
+ const formData = new FormData()
+ formData.append('file', file)
+
+ try {
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData,
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to upload image')
+ }
+
+ const data = await response.json()
+ return data.filename
+ } catch (error) {
+ console.error('Error uploading file:', error)
+ throw error
+ }
+}
+
+export async function deleteImageFromPublic(filename: string): Promise {
+ try {
+ const response = await fetch(`/api/upload?filename=${encodeURIComponent(filename)}`, {
+ method: 'DELETE',
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to delete image')
+ }
+ } catch (error) {
+ console.error('Error deleting image:', error)
+ throw error
+ }
+}
+
+export async function moveImageToRoot(filename: string): Promise {
+ try {
+ const response = await fetch(`/api/upload/move-to-root?filename=${encodeURIComponent(filename)}`, {
+ method: 'POST',
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to move image to root')
+ }
+ } catch (error) {
+ console.error('Error moving image:', error)
+ throw error
+ }
+}
+
+export async function saveVideoToPublic(file: File, oldContent?: string): Promise {
+ if (oldContent) {
+ const oldFilename = extractFilenameFromContent(oldContent)
+ if (oldFilename) {
+ try {
+ await deleteImageFromPublic(oldFilename)
+ } catch (error) {
+ console.error('Error deleting old file:', error)
+ }
+ }
+ }
+
+ const formData = new FormData()
+ formData.append('file', file)
+
+ try {
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData,
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to upload video')
+ }
+
+ const data = await response.json()
+ return data.filename
+ } catch (error) {
+ console.error('Error uploading file:', error)
+ throw error
+ }
+}
+
+export async function saveAudioToPublic(file: File, oldContent?: string): Promise {
+ if (oldContent) {
+ const oldFilename = extractFilenameFromContent(oldContent)
+ if (oldFilename) {
+ try {
+ await deleteImageFromPublic(oldFilename)
+ } catch (error) {
+ console.error('Error deleting old file:', error)
+ }
+ }
+ }
+
+ const formData = new FormData()
+ formData.append('file', file)
+
+ try {
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData,
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to upload audio')
+ }
+
+ const data = await response.json()
+ return data.filename
+ } catch (error) {
+ console.error('Error uploading file:', error)
+ throw error
+ }
+}
+
+export async function saveFileToPublic(file: File, oldContent?: string): Promise {
+ if (oldContent) {
+ const oldFilename = extractFilenameFromContent(oldContent)
+ if (oldFilename) {
+ try {
+ await deleteImageFromPublic(oldFilename)
+ } catch (error) {
+ console.error('Error deleting old file:', error)
+ }
+ }
+ }
+
+ const formData = new FormData()
+ formData.append('file', file)
+
+ try {
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData,
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to upload file')
+ }
+
+ const data = await response.json()
+ return data.filename
+ } catch (error) {
+ console.error('Error uploading file:', error)
+ throw error
+ }
+}
+
+export function extractFilenameFromContent(content: string): string | null {
+ try {
+ const parsed = JSON.parse(content)
+ if (parsed.url) {
+ return parsed.url.split('/').pop() || null
+ }
+ } catch {
+ // If content is a direct URL
+ return content.split('/').pop() || null
+ }
+ return null
+}
\ No newline at end of file
diff --git a/packages/editor/src/lib/renderers/BlockRenderer.tsx b/packages/editor/src/lib/renderers/BlockRenderer.tsx
index c139e4a..ba5d2a9 100644
--- a/packages/editor/src/lib/renderers/BlockRenderer.tsx
+++ b/packages/editor/src/lib/renderers/BlockRenderer.tsx
@@ -9,7 +9,7 @@ import { CodeBlock } from "@/components/blocks/CodeBlock"
// import { Image } from '../blocks/Image'
// import { Table } from '../blocks/Table'
// import { ToggleList } from '../blocks/ToggleList'
-// import { CheckList } from '../blocks/CheckList'
+import { CheckList } from "@/components/blocks/CheckListBlock"
// import { Video } from '../blocks/Video'
// import { Audio } from '../blocks/Audio'
// import { File } from '../blocks/File'
@@ -18,6 +18,10 @@ 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"
interface ImageBlockContent {
url: string;
@@ -119,20 +123,19 @@ export function BlockRenderer({ block, isEditing, onUpdate }: BlockRendererProps
onUpdate={(content) => onUpdate?.(block.id, content)}
/>
);
- // case 'table':
- // return ;
- // case 'toggleList':
- // return ;
- // case 'checkList':
- // return ;
- // case 'video':
- // return ;
- // case 'audio':
- // return ;
- // case 'file':
- // return ;
- // case 'emoji':
- // return ;
+ case 'table':
+ return (
+ {
+ onUpdate?.(block.id, JSON.stringify({ headers, rows }));
+ }}
+ align={block.metadata?.align}
+ styles={block.metadata?.styles}
+ />
+ );
case 'callout':
return (
;
+ case 'video':
+ const videoContent = (() => {
+ try {
+ return typeof block.content === 'string'
+ ? JSON.parse(block.content)
+ : block.content;
+ } catch {
+ return {
+ url: block.content,
+ caption: '',
+ alignment: 'center',
+ size: 'medium'
+ };
+ }
+ })();
+
+ return (
+ onUpdate?.(block.id, content)}
+ />
+ );
+ case 'audio':
+ const audioContent = (() => {
+ try {
+ return typeof block.content === 'string'
+ ? JSON.parse(block.content)
+ : block.content;
+ } catch {
+ return {
+ url: block.content,
+ caption: '',
+ alignment: 'center'
+ };
+ }
+ })();
+
+ return (
+ onUpdate?.(block.id, content)}
+ />
+ );
+ case 'file':
+ return (
+ onUpdate?.(block.id, content)}
+ />
+ );
+ case 'checkList':
+ return (
+ onUpdate?.(block.id, content)}
+ />
+ );
default:
return null
}
diff --git a/packages/editor/src/types/Block.ts b/packages/editor/src/types/Block.ts
index 27db31b..61f1b7a 100644
--- a/packages/editor/src/types/Block.ts
+++ b/packages/editor/src/types/Block.ts
@@ -6,41 +6,42 @@ export type BlockType =
| 'list'
| 'blockquote'
| 'divider'
- // | 'table'
+ | 'table'
// | 'toggleList'
- // | 'checkList'
- // | 'video'
- // | 'audio'
- // | 'file'
+ | 'checkList'
+ | 'video'
+ | 'audio'
+ | 'file'
// | 'emoji'
- | 'callout';
+ | 'callout'
+ | 'apiReference';
export interface Block {
id: string;
type: BlockType;
content: string;
metadata?: {
- level?: number; // For headings
- styles?: {
- bold?: boolean;
- italic?: boolean;
- underline?: boolean;
- };
- language?: string; // For code blocks
- alt?: string; // For images
- caption?: string; // For images, videos, and audio
- listType?: 'ordered' | 'unordered'; // For lists
- size?: 'small' | 'medium' | 'large' | 'full'; // for images
- position?: 'left' | 'center' | 'right'; // for images
- headers?: string[]; // For tables
- rows?: string[][]; // For tables
- items?: { title: string; content: string }[]; // For toggle lists
- checkedItems?: { text: string; checked: boolean }[]; // For check lists
- name?: string; // For files
- label?: string; // For emojis
- filename?: string; // For code blocks
- showLineNumbers?: boolean; // For code blocks
- align?: 'left' | 'center' | 'right'; // For alignment
+ level?: number; // For headings
+ styles?: {
+ bold?: boolean;
+ italic?: boolean;
+ underline?: boolean;
+ };
+ language?: string; // For code blocks
+ alt?: string; // For images
+ caption?: string; // For images, videos, and audio
+ listType?: 'ordered' | 'unordered'; // For lists
+ size?: 'small' | 'medium' | 'large' | 'full'; // for images
+ position?: 'left' | 'center' | 'right'; // for images
+ headers?: string[]; // For tables
+ rows?: string[][]; // For tables
+ items?: { title: string; content: string }[]; // For toggle lists
+ checkedItems?: { text: string; checked: boolean }[]; // For check lists
+ name?: string; // For files
+ label?: string; // For emojis
+ filename?: string; // For code blocks
+ showLineNumbers?: boolean; // For code blocks
+ align?: 'left' | 'center' | 'right'; // For alignment
type?: 'info' | 'warning' | 'success' | 'error'; // For callouts
title?: string; // For callouts
};
@@ -48,6 +49,7 @@ export interface Block {
export interface Post {
id: string;
+ slug?: string;
title: string;
description: string;
author: string;
@@ -57,4 +59,24 @@ export interface Post {
category: string;
keywords: string[];
blocks: Block[];
+ imageUrl?: string;
+}
+
+export interface APIReferenceBlock extends Block {
+ type: 'apiReference';
+ metadata: Block['metadata'] & {
+ endpoint: string;
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
+ parameters?: {
+ name: string;
+ type: string;
+ required: boolean;
+ description: string;
+ }[];
+ responses?: {
+ code: number;
+ description: string;
+ example?: any;
+ }[];
+ };
}