feat: Memento avec dates, Markdown, reminders et auth
Tests Playwright validés ✅:
- Création de notes: OK
- Modification titre: OK
- Modification contenu: OK
- Markdown éditable avec preview: OK
Fonctionnalités:
- date-fns: dates relatives sur cards
- react-markdown + remark-gfm
- Markdown avec toggle edit/preview
- Recherche améliorée (titre/contenu/labels/checkItems)
- Reminder recurrence/location (schema)
- NextAuth.js + User/Account/Session
- userId dans Note (optionnel)
- 4 migrations créées
Ready for production + auth integration
This commit is contained in:
@@ -18,9 +18,11 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { X, Plus, Palette, Tag, Image as ImageIcon } from 'lucide-react'
|
||||
import { X, Plus, Palette, Tag, Image as ImageIcon, Bell, FileText, Eye } from 'lucide-react'
|
||||
import { updateNote } from '@/app/actions/notes'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useToast } from '@/components/ui/toast'
|
||||
import { MarkdownContent } from './markdown-content'
|
||||
|
||||
interface NoteEditorProps {
|
||||
note: Note
|
||||
@@ -28,6 +30,7 @@ interface NoteEditorProps {
|
||||
}
|
||||
|
||||
export function NoteEditor({ note, onClose }: NoteEditorProps) {
|
||||
const { addToast } = useToast()
|
||||
const [title, setTitle] = useState(note.title || '')
|
||||
const [content, setContent] = useState(note.content)
|
||||
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
|
||||
@@ -36,7 +39,15 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
|
||||
const [newLabel, setNewLabel] = useState('')
|
||||
const [color, setColor] = useState(note.color)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isMarkdown, setIsMarkdown] = useState(note.isMarkdown || false)
|
||||
const [showMarkdownPreview, setShowMarkdownPreview] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Reminder state
|
||||
const [showReminderDialog, setShowReminderDialog] = useState(false)
|
||||
const [reminderDate, setReminderDate] = useState('')
|
||||
const [reminderTime, setReminderTime] = useState('')
|
||||
const [currentReminder, setCurrentReminder] = useState<Date | null>(note.reminder)
|
||||
|
||||
const colorClasses = NOTE_COLORS[color as NoteColor] || NOTE_COLORS.default
|
||||
|
||||
@@ -57,6 +68,49 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
|
||||
setImages(images.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleReminderOpen = () => {
|
||||
if (currentReminder) {
|
||||
const date = new Date(currentReminder)
|
||||
setReminderDate(date.toISOString().split('T')[0])
|
||||
setReminderTime(date.toTimeString().slice(0, 5))
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
const handleRemoveReminder = () => {
|
||||
setCurrentReminder(null)
|
||||
setShowReminderDialog(false)
|
||||
addToast('Reminder removed', 'success')
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
@@ -67,6 +121,8 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
|
||||
labels,
|
||||
images,
|
||||
color,
|
||||
reminder: currentReminder,
|
||||
isMarkdown,
|
||||
})
|
||||
onClose()
|
||||
} catch (error) {
|
||||
@@ -158,12 +214,58 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
|
||||
|
||||
{/* Content or Checklist */}
|
||||
{note.type === 'text' ? (
|
||||
<Textarea
|
||||
placeholder="Take a note..."
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="min-h-[200px] border-0 focus-visible:ring-0 px-0 bg-transparent resize-none"
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
{/* Markdown controls */}
|
||||
<div className="flex items-center justify-between gap-2 pb-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsMarkdown(!isMarkdown)
|
||||
if (isMarkdown) setShowMarkdownPreview(false)
|
||||
}}
|
||||
className={cn("h-7 text-xs", isMarkdown && "text-blue-600")}
|
||||
>
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
{isMarkdown ? 'Markdown ON' : 'Markdown OFF'}
|
||||
</Button>
|
||||
|
||||
{isMarkdown && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowMarkdownPreview(!showMarkdownPreview)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
{showMarkdownPreview ? (
|
||||
<>
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
Edit
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="h-3 w-3 mr-1" />
|
||||
Preview
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showMarkdownPreview && isMarkdown ? (
|
||||
<MarkdownContent
|
||||
content={content || '*No content*'}
|
||||
className="min-h-[200px] p-3 rounded-md border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50"
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
placeholder={isMarkdown ? "Take a note... (Markdown supported)" : "Take a note..."}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="min-h-[200px] border-0 focus-visible:ring-0 px-0 bg-transparent resize-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{checkItems.map((item) => (
|
||||
@@ -221,6 +323,17 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Reminder Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReminderOpen}
|
||||
title="Set reminder"
|
||||
className={currentReminder ? "text-blue-600" : ""}
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Add Image Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -303,6 +416,58 @@ export function NoteEditor({ note, onClose }: NoteEditorProps) {
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
{/* Reminder Dialog */}
|
||||
<Dialog open={showReminderDialog} onOpenChange={setShowReminderDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Set Reminder</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="reminder-date" className="text-sm font-medium">
|
||||
Date
|
||||
</label>
|
||||
<Input
|
||||
id="reminder-date"
|
||||
type="date"
|
||||
value={reminderDate}
|
||||
onChange={(e) => setReminderDate(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="reminder-time" className="text-sm font-medium">
|
||||
Time
|
||||
</label>
|
||||
<Input
|
||||
id="reminder-time"
|
||||
type="time"
|
||||
value={reminderTime}
|
||||
onChange={(e) => setReminderTime(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
{currentReminder && (
|
||||
<Button variant="outline" onClick={handleRemoveReminder}>
|
||||
Remove Reminder
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={() => setShowReminderDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleReminderSave}>
|
||||
Set Reminder
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user