'use client' import { useState, useRef, useEffect } from 'react' import { Card } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Button } from '@/components/ui/button' import { CheckSquare, X, Bell, Image, UserPlus, Palette, Archive, MoreVertical, Undo2, Redo2, FileText, Eye, Link as LinkIcon } from 'lucide-react' import { createNote } from '@/app/actions/notes' import { fetchLinkMetadata } from '@/app/actions/scrape' import { CheckItem, NOTE_COLORS, NoteColor, LinkMetadata } from '@/lib/types' import { Checkbox } from '@/components/ui/checkbox' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { cn } from '@/lib/utils' import { useToast } from '@/components/ui/toast' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { MarkdownContent } from './markdown-content' import { LabelSelector } from './label-selector' import { LabelBadge } from './label-badge' interface HistoryState { title: string content: string } interface NoteState { title: string content: string checkItems: CheckItem[] images: string[] } export function NoteInput() { const { addToast } = useToast() const [isExpanded, setIsExpanded] = useState(false) const [type, setType] = useState<'text' | 'checklist'>('text') const [isSubmitting, setIsSubmitting] = useState(false) const [color, setColor] = useState('default') const [isArchived, setIsArchived] = useState(false) const [selectedLabels, setSelectedLabels] = useState([]) const fileInputRef = useRef(null) // Simple state without complex undo/redo - like Google Keep const [title, setTitle] = useState('') const [content, setContent] = useState('') const [checkItems, setCheckItems] = useState([]) const [images, setImages] = useState([]) const [links, setLinks] = useState([]) const [isMarkdown, setIsMarkdown] = useState(false) const [showMarkdownPreview, setShowMarkdownPreview] = useState(false) // Undo/Redo history (title and content only) const [history, setHistory] = useState([{ title: '', content: '' }]) const [historyIndex, setHistoryIndex] = useState(0) const isUndoingRef = useRef(false) // Reminder dialog const [showReminderDialog, setShowReminderDialog] = useState(false) const [showLinkDialog, setShowLinkDialog] = useState(false) const [linkUrl, setLinkUrl] = useState('') const [reminderDate, setReminderDate] = useState('') const [reminderTime, setReminderTime] = useState('') const [currentReminder, setCurrentReminder] = useState(null) // Save to history after 1 second of inactivity useEffect(() => { if (isUndoingRef.current) { isUndoingRef.current = false return } const timer = setTimeout(() => { const currentState = { title, content } const lastState = history[historyIndex] if (lastState.title !== title || lastState.content !== content) { const newHistory = history.slice(0, historyIndex + 1) newHistory.push(currentState) if (newHistory.length > 50) { newHistory.shift() } else { setHistoryIndex(historyIndex + 1) } setHistory(newHistory) } }, 1000) return () => clearTimeout(timer) }, [title, content, history, historyIndex]) // Undo/Redo functions const handleUndo = () => { if (historyIndex > 0) { isUndoingRef.current = true const newIndex = historyIndex - 1 setHistoryIndex(newIndex) setTitle(history[newIndex].title) setContent(history[newIndex].content) } } const handleRedo = () => { if (historyIndex < history.length - 1) { isUndoingRef.current = true const newIndex = historyIndex + 1 setHistoryIndex(newIndex) setTitle(history[newIndex].title) setContent(history[newIndex].content) } } // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!isExpanded) return if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault() handleUndo() } if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault() handleRedo() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [isExpanded, historyIndex, history]) const handleImageUpload = async (e: React.ChangeEvent) => { const files = e.target.files if (!files) return // Validate file types const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] const maxSize = 5 * 1024 * 1024 // 5MB for (const file of Array.from(files)) { // Validation if (!validTypes.includes(file.type)) { addToast(`Invalid file type: ${file.name}. Only JPEG, PNG, GIF, and WebP allowed.`, 'error') continue } if (file.size > maxSize) { addToast(`File too large: ${file.name}. Maximum size is 5MB.`, 'error') continue } // Upload to server 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('Upload failed') const data = await response.json() setImages(prev => [...prev, data.url]) } catch (error) { console.error('Upload error:', error) addToast(`Failed to upload ${file.name}`, 'error') } } // Reset input e.target.value = '' } const handleAddLink = async () => { if (!linkUrl) return // Optimistic add (or loading state) setShowLinkDialog(false) try { const metadata = await fetchLinkMetadata(linkUrl) if (metadata) { setLinks(prev => [...prev, metadata]) addToast('Link added', 'success') } else { addToast('Could not fetch link metadata', 'warning') // Fallback: just add the url as title setLinks(prev => [...prev, { url: linkUrl, title: linkUrl }]) } } catch (error) { console.error('Failed to add link:', error) addToast('Failed to add link', 'error') } finally { setLinkUrl('') } } const handleRemoveLink = (index: number) => { setLinks(links.filter((_, i) => i !== index)) } const handleReminderOpen = () => { const tomorrow = new Date(Date.now() + 86400000) setReminderDate(tomorrow.toISOString().split('T')[0]) setReminderTime('09:00') setShowReminderDialog(true) } const handleReminderSave = () => { if (!reminderDate || !reminderTime) { addToast('Please enter date and time', 'warning') return } const dateTimeString = `${reminderDate}T${reminderTime}` const date = new Date(dateTimeString) if (isNaN(date.getTime())) { addToast('Invalid date or time', 'error') return } if (date < new Date()) { addToast('Reminder must be in the future', 'error') return } setCurrentReminder(date) addToast(`Reminder set for ${date.toLocaleString()}`, 'success') setShowReminderDialog(false) setReminderDate('') setReminderTime('') } const handleSubmit = async () => { // Validation if (type === 'text' && !content.trim()) { addToast('Please enter some content', 'warning') return } if (type === 'checklist' && checkItems.length === 0) { addToast('Please add at least one item', 'warning') return } if (type === 'checklist' && checkItems.every(item => !item.text.trim())) { addToast('Checklist items cannot be empty', 'warning') return } setIsSubmitting(true) try { await createNote({ title: title.trim() || undefined, content: type === 'text' ? content : '', type, checkItems: type === 'checklist' ? checkItems : undefined, color, isArchived, images: images.length > 0 ? images : undefined, links: links.length > 0 ? links : undefined, reminder: currentReminder, isMarkdown, labels: selectedLabels.length > 0 ? selectedLabels : undefined, }) // Reset form setTitle('') setContent('') setCheckItems([]) setImages([]) setLinks([]) setIsMarkdown(false) setShowMarkdownPreview(false) setHistory([{ title: '', content: '' }]) setHistoryIndex(0) setIsExpanded(false) setType('text') setColor('default') setIsArchived(false) setCurrentReminder(null) setSelectedLabels([]) addToast('Note created successfully', 'success') } catch (error) { console.error('Failed to create note:', error) addToast('Failed to create note', 'error') } finally { setIsSubmitting(false) } } const handleAddCheckItem = () => { setCheckItems([ ...checkItems, { id: Date.now().toString(), text: '', checked: false }, ]) } const handleUpdateCheckItem = (id: string, text: string) => { setCheckItems( checkItems.map(item => (item.id === id ? { ...item, text } : item)) ) } const handleRemoveCheckItem = (id: string) => { setCheckItems(checkItems.filter(item => item.id !== id)) } const handleClose = () => { setIsExpanded(false) setTitle('') setContent('') setCheckItems([]) setImages([]) setLinks([]) setIsMarkdown(false) setShowMarkdownPreview(false) setHistory([{ title: '', content: '' }]) setHistoryIndex(0) setType('text') setColor('default') setIsArchived(false) setCurrentReminder(null) setSelectedLabels([]) } if (!isExpanded) { return (
setIsExpanded(true)} readOnly value="" className="border-0 focus-visible:ring-0 cursor-text" />
) } const colorClasses = NOTE_COLORS[color] || NOTE_COLORS.default return ( <>
setTitle(e.target.value)} className="border-0 focus-visible:ring-0 text-base font-semibold" /> {/* Image Preview */} {images.length > 0 && (
{images.map((img, idx) => (
{`Upload
))}
)} {/* Link Previews */} {links.length > 0 && (
{links.map((link, idx) => (
{link.imageUrl && (
)}

{link.title || link.url}

{link.description &&

{link.description}

} {new URL(link.url).hostname}
))}
)} {type === 'text' ? (
{/* Selected Labels Display */} {selectedLabels.length > 0 && (
{selectedLabels.map(label => ( setSelectedLabels(prev => prev.filter(l => l !== label))} /> ))}
)} {/* Markdown toggle button */} {isMarkdown && (
)} {showMarkdownPreview && isMarkdown ? ( ) : (