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

This commit is contained in:
2026-05-01 01:11:03 +02:00
parent 7053e242d2
commit 1345403a31
31 changed files with 3222 additions and 150 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { Note, NOTE_COLORS, NoteColor } from '@/lib/types'
import { Note, NOTE_COLORS, NoteColor, NoteType } from '@/lib/types'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -20,7 +20,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote, LucideIcon, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2, LogOut, Trash2 } from 'lucide-react'
import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote, LucideIcon, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2, LogOut, Trash2, AlignLeft, FileCode2, PenLine, ListChecks } from 'lucide-react'
import { useState, useEffect, useTransition, useOptimistic, memo } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter, useSearchParams } from 'next/navigation'
@@ -83,6 +83,13 @@ function getDateLocale(language: string): Locale {
return localeMap[language] || enUS
}
const NOTE_TYPE_ICONS: Record<NoteType, LucideIcon> = {
text: AlignLeft,
markdown: FileCode2,
richtext: PenLine,
checklist: ListChecks,
}
// Map icon names to lucide-react components
const ICON_MAP: Record<string, LucideIcon> = {
'folder': Folder,
@@ -536,8 +543,12 @@ export const NoteCard = memo(function NoteCard({
{/* Title */}
{optimisticNote.title && (
<h3 dir="auto" className="text-lg font-heading font-semibold mb-2 pr-20 text-foreground leading-tight tracking-tight">
{optimisticNote.title}
<h3 dir="auto" className="text-lg font-heading font-semibold mb-2 pr-20 text-foreground leading-tight tracking-tight flex items-center gap-2">
{(() => {
const TypeIcon = NOTE_TYPE_ICONS[optimisticNote.type] || AlignLeft
return <TypeIcon className="h-4 w-4 shrink-0 text-muted-foreground/50" />
})()}
<span className="min-w-0 truncate">{optimisticNote.title}</span>
</h3>
)}
@@ -608,18 +619,20 @@ export const NoteCard = memo(function NoteCard({
)}
{/* Content */}
{optimisticNote.type === 'text' ? (
{optimisticNote.type === 'checklist' ? (
<NoteChecklist
items={optimisticNote.checkItems || []}
onToggleItem={handleCheckItem}
/>
) : optimisticNote.type === 'richtext' ? (
<div className="text-sm text-foreground line-clamp-10 rt-preview" dangerouslySetInnerHTML={{ __html: optimisticNote.content || '' }} />
) : (
<div className="text-sm text-foreground line-clamp-10">
<MarkdownContent
content={optimisticNote.content}
className="prose-h1:text-xl prose-h1:font-semibold prose-h1:leading-snug prose-h1:mt-1 prose-h1:mb-2 prose-h2:text-lg prose-h2:font-medium prose-h3:text-base prose-p:text-sm prose-p:leading-relaxed"
/>
</div>
) : (
<NoteChecklist
items={optimisticNote.checkItems || []}
onToggleItem={handleCheckItem}
/>
)}
{/* Labels - using shared LabelBadge component */}