feat: Notion-like rich text editor with TipTap, 4 note types, slash commands & bubble menu
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m33s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m33s
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata } from '@/lib/types'
|
||||
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata, NoteType } from '@/lib/types'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -23,7 +23,9 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { X, Plus, Palette, Image as ImageIcon, Bell, FileText, Eye, Link as LinkIcon, Sparkles, Maximize2, Copy, Wand2, LogOut } from 'lucide-react'
|
||||
import { NoteTypeSelector } from '@/components/note-type-selector'
|
||||
import { RichTextEditor } from '@/components/rich-text-editor'
|
||||
import { X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles, Maximize2, Copy, Wand2, LogOut } from 'lucide-react'
|
||||
import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote } from '@/app/actions/notes'
|
||||
import { fetchLinkMetadata } from '@/app/actions/scrape'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -85,8 +87,9 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
const [size, setSize] = useState<NoteSize>(note.size || 'small')
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [removedImageUrls, setRemovedImageUrls] = useState<string[]>([])
|
||||
const [isMarkdown, setIsMarkdown] = useState(note.isMarkdown || false)
|
||||
const [showMarkdownPreview, setShowMarkdownPreview] = useState(note.isMarkdown || false)
|
||||
const [noteType, setNoteType] = useState<NoteType>(note.type)
|
||||
const isMarkdown = noteType === 'markdown'
|
||||
const [showMarkdownPreview, setShowMarkdownPreview] = useState(note.type === 'markdown')
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Update context notebookId when note changes
|
||||
@@ -96,9 +99,9 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
|
||||
// Auto-tagging hook - use local state for live suggestions as user types
|
||||
const { suggestions, isAnalyzing } = useAutoTagging({
|
||||
content: note.type === 'text' ? content : '',
|
||||
content: noteType !== 'checklist' ? content : '',
|
||||
notebookId: note.notebookId,
|
||||
enabled: note.type === 'text' && autoLabelingEnabled
|
||||
enabled: noteType !== 'checklist' && autoLabelingEnabled
|
||||
})
|
||||
|
||||
// Reminder state
|
||||
@@ -466,7 +469,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
|
||||
// Set the transformed markdown content and enable markdown mode
|
||||
setContent(data.transformedText)
|
||||
setIsMarkdown(true)
|
||||
setNoteType('markdown')
|
||||
setShowMarkdownPreview(false)
|
||||
|
||||
toast.success(t('ai.transformSuccess'))
|
||||
@@ -523,14 +526,15 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
try {
|
||||
await updateNote(note.id, {
|
||||
title: title.trim() || null,
|
||||
content: note.type === 'text' ? content : '',
|
||||
checkItems: note.type === 'checklist' ? checkItems : null,
|
||||
content: noteType !== 'checklist' ? content : '',
|
||||
checkItems: noteType === 'checklist' ? checkItems : null,
|
||||
labels,
|
||||
images,
|
||||
links,
|
||||
color,
|
||||
reminder: currentReminder,
|
||||
isMarkdown,
|
||||
isMarkdown: noteType === 'markdown',
|
||||
type: noteType,
|
||||
size,
|
||||
})
|
||||
|
||||
@@ -588,12 +592,12 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
title: `${title || t('notes.untitled')} (${t('notes.copy')})`,
|
||||
content: content,
|
||||
color: color,
|
||||
type: note.type,
|
||||
checkItems: checkItems,
|
||||
labels: labels,
|
||||
images: images,
|
||||
links: links,
|
||||
isMarkdown: isMarkdown,
|
||||
isMarkdown: noteType === 'markdown',
|
||||
type: noteType,
|
||||
size: size,
|
||||
})
|
||||
toast.success(t('notes.copySuccess'))
|
||||
@@ -697,7 +701,13 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
)}
|
||||
|
||||
{/* Content or Checklist */}
|
||||
{note.type === 'text' ? (
|
||||
{noteType === 'richtext' ? (
|
||||
<RichTextEditor
|
||||
content={content}
|
||||
onChange={setContent}
|
||||
className="min-h-[200px]"
|
||||
/>
|
||||
) : noteType === 'text' || noteType === 'markdown' ? (
|
||||
<div className="space-y-2">
|
||||
{showMarkdownPreview && isMarkdown ? (
|
||||
<MarkdownContent
|
||||
@@ -847,18 +857,9 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Markdown toggle */}
|
||||
{note.type === 'text' && (
|
||||
<Button variant="ghost" size="icon"
|
||||
className={cn('h-8 w-8', isMarkdown && 'text-primary bg-primary/10')}
|
||||
onClick={() => { setIsMarkdown(!isMarkdown); if (isMarkdown) setShowMarkdownPreview(false) }}
|
||||
title="Markdown">
|
||||
<FileText className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<NoteTypeSelector value={noteType} onChange={(newType) => { setNoteType(newType); if (newType !== 'markdown') setShowMarkdownPreview(false) }} />
|
||||
|
||||
{/* Markdown preview toggle */}
|
||||
{isMarkdown && (
|
||||
{noteType === 'markdown' && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8"
|
||||
onClick={() => setShowMarkdownPreview(!showMarkdownPreview)}
|
||||
title={showMarkdownPreview ? t('general.edit') : t('notes.preview')}>
|
||||
@@ -867,7 +868,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
||||
)}
|
||||
|
||||
{/* AI Copilot */}
|
||||
{note.type === 'text' && aiAssistantEnabled && (
|
||||
{noteType !== 'checklist' && aiAssistantEnabled && (
|
||||
<Button variant="ghost" size="sm"
|
||||
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-colors', aiOpen && 'bg-primary/10 text-primary')}
|
||||
onClick={() => setAiOpen(!aiOpen)} title="Assistant IA">
|
||||
|
||||
Reference in New Issue
Block a user