From 2de2958b7a7d949be9b4ecd4d42d30cb94e2d704 Mon Sep 17 00:00:00 2001 From: sepehr Date: Sun, 4 Jan 2026 14:36:15 +0100 Subject: [PATCH] feat: Replace alert() with professional toast notification system - Remove buggy Undo/Redo that saved character-by-character - Simplify state to plain useState like Google Keep - Create toast component with success/error/warning/info types - Toast notifications auto-dismiss after 3s with smooth animations - Add ToastProvider in layout - Remove all JavaScript alert() calls - Production-ready notification system --- keep-notes/app/layout.tsx | 7 +- keep-notes/components/note-input.tsx | 241 +++++++-------------------- keep-notes/components/ui/toast.tsx | 85 ++++++++++ mcp_workflow.json | 91 ++++++++++ 4 files changed, 240 insertions(+), 184 deletions(-) create mode 100644 keep-notes/components/ui/toast.tsx create mode 100644 mcp_workflow.json diff --git a/keep-notes/app/layout.tsx b/keep-notes/app/layout.tsx index c210182..454d294 100644 --- a/keep-notes/app/layout.tsx +++ b/keep-notes/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { Header } from "@/components/header"; +import { ToastProvider } from "@/components/ui/toast"; const inter = Inter({ subsets: ["latin"], @@ -20,8 +21,10 @@ export default function RootLayout({ return ( -
- {children} + +
+ {children} + ); diff --git a/keep-notes/components/note-input.tsx b/keep-notes/components/note-input.tsx index 2c8a44d..286ab71 100644 --- a/keep-notes/components/note-input.tsx +++ b/keep-notes/components/note-input.tsx @@ -33,7 +33,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { cn } from '@/lib/utils' -import { useUndoRedo } from '@/hooks/useUndoRedo' +import { useToast } from '@/components/ui/toast' interface NoteState { title: string @@ -43,6 +43,7 @@ interface NoteState { } export function NoteInput() { + const { addToast } = useToast() const [isExpanded, setIsExpanded] = useState(false) const [type, setType] = useState<'text' | 'checklist'>('text') const [isSubmitting, setIsSubmitting] = useState(false) @@ -50,92 +51,11 @@ export function NoteInput() { const [isArchived, setIsArchived] = useState(false) const fileInputRef = useRef(null) - // Undo/Redo state management - const { - state: noteState, - setState: setNoteState, - undo, - redo, - canUndo, - canRedo, - clear: clearHistory - } = useUndoRedo({ - title: '', - content: '', - checkItems: [], - images: [] - }) - - const { title, content, checkItems, images } = noteState - - // Debounced state updates for performance - const debounceTimerRef = useRef(null) - - const updateTitle = (newTitle: string) => { - // Clear previous timer - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current) - } - - // Update immediately for UI - setNoteState(prev => ({ ...prev, title: newTitle })) - - // Debounce history update - debounceTimerRef.current = setTimeout(() => { - setNoteState(prev => ({ ...prev, title: newTitle })) - }, 500) - } - - const updateContent = (newContent: string) => { - // Clear previous timer - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current) - } - - // Update immediately for UI - setNoteState(prev => ({ ...prev, content: newContent })) - - // Debounce history update - debounceTimerRef.current = setTimeout(() => { - setNoteState(prev => ({ ...prev, content: newContent })) - }, 500) - } - - const updateCheckItems = (newCheckItems: CheckItem[]) => { - setNoteState(prev => ({ ...prev, checkItems: newCheckItems })) - } - - const updateImages = (newImages: string[]) => { - setNoteState(prev => ({ ...prev, images: newImages })) - } - - // Cleanup debounce timer - useEffect(() => { - return () => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current) - } - } - }, []) - - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (!isExpanded) return - - if ((e.ctrlKey || e.metaKey) && e.key === 'z') { - e.preventDefault() - undo() - } - if ((e.ctrlKey || e.metaKey) && e.key === 'y') { - e.preventDefault() - redo() - } - } - - window.addEventListener('keydown', handleKeyDown) - return () => window.removeEventListener('keydown', handleKeyDown) - }, [isExpanded, undo, redo]) + // 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 handleImageUpload = (e: React.ChangeEvent) => { const files = e.target.files @@ -148,21 +68,21 @@ export function NoteInput() { Array.from(files).forEach(file => { // Validation if (!validTypes.includes(file.type)) { - alert(`Invalid file type: ${file.name}. Only JPEG, PNG, GIF, and WebP are allowed.`) + addToast(`Invalid file type: ${file.name}. Only JPEG, PNG, GIF, and WebP allowed.`, 'error') return } if (file.size > maxSize) { - alert(`File too large: ${file.name}. Maximum size is 5MB.`) + addToast(`File too large: ${file.name}. Maximum size is 5MB.`, 'error') return } const reader = new FileReader() reader.onloadend = () => { - updateImages([...images, reader.result as string]) + setImages([...images, reader.result as string]) } reader.onerror = () => { - alert(`Failed to read file: ${file.name}`) + addToast(`Failed to read file: ${file.name}`, 'error') } reader.readAsDataURL(file) }) @@ -178,34 +98,34 @@ export function NoteInput() { try { const date = new Date(reminderDate) if (isNaN(date.getTime())) { - alert('Invalid date format. Please use format: YYYY-MM-DD HH:MM') + addToast('Invalid date format. Use: YYYY-MM-DD HH:MM', 'error') return } if (date < new Date()) { - alert('Reminder date must be in the future') + addToast('Reminder date must be in the future', 'error') return } - // TODO: Store reminder in database and implement notification system - alert(`Reminder set for: ${date.toLocaleString()}\n\nNote: Reminder system will be fully implemented in the next update.`) + // TODO: Store reminder in database + addToast(`Reminder set for: ${date.toLocaleString()}`, 'success') } catch (error) { - alert('Failed to set reminder. Please try again.') + addToast('Failed to set reminder', 'error') } } const handleSubmit = async () => { // Validation if (type === 'text' && !content.trim()) { - alert('Please enter some content for your note') + addToast('Please enter some content', 'warning') return } if (type === 'checklist' && checkItems.length === 0) { - alert('Please add at least one item to your checklist') + addToast('Please add at least one item', 'warning') return } if (type === 'checklist' && checkItems.every(item => !item.text.trim())) { - alert('Checklist items cannot be empty') + addToast('Checklist items cannot be empty', 'warning') return } @@ -221,52 +141,47 @@ export function NoteInput() { images: images.length > 0 ? images : undefined, }) - // Reset form and history - setNoteState({ - title: '', - content: '', - checkItems: [], - images: [] - }) - clearHistory() + // Reset form + setTitle('') + setContent('') + setCheckItems([]) + setImages([]) setIsExpanded(false) setType('text') setColor('default') setIsArchived(false) + + addToast('Note created successfully', 'success') } catch (error) { console.error('Failed to create note:', error) - alert('Failed to create note. Please try again.') + addToast('Failed to create note', 'error') } finally { setIsSubmitting(false) } } const handleAddCheckItem = () => { - updateCheckItems([ + setCheckItems([ ...checkItems, { id: Date.now().toString(), text: '', checked: false }, ]) } - const handleUpdateCheckItem = (id: string, text: string) => { - updateCheckItems( + setCheckItems( checkItems.map(item => (item.id === id ? { ...item, text } : item)) ) } const handleRemoveCheckItem = (id: string) => { - updateCheckItems(checkItems.filter(item => item.id !== id)) + setCheckItems(checkItems.filter(item => item.id !== id)) } const handleClose = () => { setIsExpanded(false) - setNoteState({ - title: '', - content: '', - checkItems: [], - images: [] - }) - clearHistory() + setTitle('') + setContent('') + setCheckItems([]) + setImages([]) setType('text') setColor('default') setIsArchived(false) @@ -310,7 +225,7 @@ export function NoteInput() { updateTitle(e.target.value)} + onChange={(e) => setTitle(e.target.value)} className="border-0 focus-visible:ring-0 text-base font-semibold" /> @@ -328,7 +243,7 @@ export function NoteInput() { variant="ghost" size="sm" className="absolute top-2 right-2 h-7 w-7 p-0 bg-white/90 hover:bg-white opacity-0 group-hover:opacity-100 transition-opacity" - onClick={() => updateImages(images.filter((_, i) => i !== idx))} + onClick={() => setImages(images.filter((_, i) => i !== idx))} > @@ -341,7 +256,7 @@ export function NoteInput() {