diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b5ded3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Testing +coverage/ + +# Next.js +.next/ +out/ +build/ + +# Production +dist/ + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env files +.env +.env*.local + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts + +# Prisma +prisma/dev.db +prisma/dev.db-journal +prisma/*.db +prisma/*.db-journal + +# MCP server logs +mcp-server/*.log diff --git a/COMPLETED-FEATURES.md b/COMPLETED-FEATURES.md index 0c069c2..314c5f4 100644 --- a/COMPLETED-FEATURES.md +++ b/COMPLETED-FEATURES.md @@ -106,14 +106,14 @@ export const NOTE_COLORS = { **Fichiers**: `components/note-input.tsx`, `components/note-editor.tsx` **Icônes implémentées**: -1. **Bell** - Remind me (⚠️ non fonctionnel) +1. **Bell** - Remind me ✅ **FONCTIONNEL** 2. **Image** - Ajouter image ✅ 3. **UserPlus** - Collaborateur (⚠️ non fonctionnel) 4. **Palette** - Changer couleur ✅ 5. **Archive** - Archiver note ✅ -6. **MoreVertical** - Plus d'options (⚠️ non fonctionnel) -7. **Undo2** - Annuler (⚠️ non fonctionnel) -8. **Redo2** - Rétablir (⚠️ non fonctionnel) +6. **MoreVertical** - Plus d'options ✅ +7. **Undo2** - Annuler ✅ **FONCTIONNEL** +8. **Redo2** - Rétablir ✅ **FONCTIONNEL** 9. **CheckSquare** - Mode checklist ✅ --- @@ -176,11 +176,45 @@ function parseNote(dbNote: any): Note { - Temps réel avec debouncing ### ✅ Drag-and-drop pour réorganiser -**Fichiers**: `components/note-card.tsx`, `app/actions/notes.ts` -- Utilisation de `@hello-pangea/dnd` (fork de react-beautiful-dnd) +**Fichiers**: `components/note-card.tsx`, `app/actions/notes.ts`, `components/note-grid.tsx` +- Utilisation du drag-and-drop HTML5 natif - Champ `order` dans la DB pour persister l'ordre -- Réorganisation visuelle fluide -- `updateNoteOrder()` pour sauvegarder les changements +- Réorganisation visuelle fluide avec feedback (opacity-30 pendant le drag) +- `reorderNotes()` pour sauvegarder les changements +- Fonctionne séparément pour les notes épinglées et non-épinglées +- Persistance après rechargement de page + +### ✅ Undo/Redo dans note-input +**Fichiers**: `components/note-input.tsx`, `hooks/useUndoRedo.ts` +- Historique de 50 états maximum +- Sauvegarde automatique après 1 seconde d'inactivité +- Boutons Undo/Redo dans la toolbar +- Raccourcis clavier: + - `Ctrl+Z` ou `Cmd+Z` → Undo + - `Ctrl+Y` ou `Cmd+Y` ou `Ctrl+Shift+Z` → Redo +- Gestion des états title et content +- Reset de l'historique après création de note +- Tests Playwright complets dans `tests/undo-redo.spec.ts` + +### ✅ Système de Reminders +**Fichiers**: `components/note-input.tsx`, `components/note-editor.tsx`, `components/note-card.tsx`, `prisma/schema.prisma` +- **Champ reminder** ajouté au schema Prisma (DateTime nullable) +- **Dialog de reminder** avec date et time pickers +- **Valeurs par défaut**: Demain à 9h00 +- **Validation**: + - Date et heure requises + - Date doit être dans le futur + - Format date/time valide +- **Fonctionnalités**: + - Définir reminder sur nouvelle note (note-input.tsx) + - Définir reminder sur note existante (note-editor.tsx) + - Modifier reminder existant + - Supprimer reminder + - Indicateur visuel (icône Bell bleue) sur les notes avec reminder actif +- **Persistance**: Reminder sauvegardé en base de données +- **Tests**: Tests Playwright complets dans `tests/reminder-dialog.spec.ts` +- **Toast notifications**: Confirmation lors de la définition/suppression +- **Migration**: `20260104140638_add_reminder` --- @@ -479,14 +513,12 @@ const handleImageUpload = (e: React.ChangeEvent) => { - Documentation README complète ### ⚠️ Partiellement Complété (10%) -- Toolbar: 4/9 boutons fonctionnels +- Toolbar: UserPlus (Collaborateur) non fonctionnel ### ❌ À Implémenter (5%) -- Bell (Remind me) - Système de rappels -- UserPlus (Collaborator) - Collaboration -- MoreVertical (More options) - Menu additionnel -- Undo2 - Annulation -- Redo2 - Rétablissement +- UserPlus (Collaborator) - Collaboration temps réel +- Système de notification pour les reminders actifs +- Dark mode complet --- diff --git a/MCP-LIGHTWEIGHT-TEST.md b/MCP-LIGHTWEIGHT-TEST.md new file mode 100644 index 0000000..9b4345f --- /dev/null +++ b/MCP-LIGHTWEIGHT-TEST.md @@ -0,0 +1,168 @@ +# Test MCP Server - Lightweight Mode + +## Test 1: Get Notes (Lightweight - Default) +```powershell +$body = @{ + jsonrpc = "2.0" + id = 1 + method = "tools/call" + params = @{ + name = "get_notes" + arguments = @{ + fullDetails = $false + } + } +} | ConvertTo-Json -Depth 10 + +$response = Invoke-RestMethod -Uri "http://localhost:3001/sse" -Method POST -Body $body -ContentType "application/json" +$response.result.content[0].text | ConvertFrom-Json | ConvertTo-Json -Depth 10 +``` + +**Résultat attendu :** +- ✅ Titres des notes +- ✅ Contenu tronqué (200 caractères max) +- ✅ Métadonnées (hasImages, imageCount, etc.) +- ❌ PAS d'images base64 (économie de payload) + +## Test 2: Get Notes (Full Details) +```powershell +$body = @{ + jsonrpc = "2.0" + id = 2 + method = "tools/call" + params = @{ + name = "get_notes" + arguments = @{ + fullDetails = $true + } + } +} | ConvertTo-Json -Depth 10 + +$response = Invoke-RestMethod -Uri "http://localhost:3001/sse" -Method POST -Body $body -ContentType "application/json" +$response.result.content[0].text | ConvertFrom-Json | Select-Object -First 1 | ConvertTo-Json -Depth 10 +``` + +**Résultat attendu :** +- ✅ Toutes les données complètes +- ✅ Images base64 incluses +- ⚠️ Payload très lourd + +## Test 3: Create Note +```powershell +$body = @{ + jsonrpc = "2.0" + id = 3 + method = "tools/call" + params = @{ + name = "create_note" + arguments = @{ + title = "Test MCP Lightweight" + content = "Cette note teste le mode lightweight du serveur MCP" + color = "green" + labels = @("Test", "MCP") + } + } +} | ConvertTo-Json -Depth 10 + +$response = Invoke-RestMethod -Uri "http://localhost:3001/sse" -Method POST -Body $body -ContentType "application/json" +$response.result.content[0].text | ConvertFrom-Json | ConvertTo-Json +``` + +## Test 4: Search Notes (Lightweight) +```powershell +$body = @{ + jsonrpc = "2.0" + id = 4 + method = "tools/call" + params = @{ + name = "get_notes" + arguments = @{ + search = "test" + fullDetails = $false + } + } +} | ConvertTo-Json -Depth 10 + +$response = Invoke-RestMethod -Uri "http://localhost:3001/sse" -Method POST -Body $body -ContentType "application/json" +$response.result.content[0].text | ConvertFrom-Json | ConvertTo-Json -Depth 10 +``` + +## Comparaison de Taille de Payload + +### Mode Lightweight +```json +{ + "id": "abc123", + "title": "Note avec images", + "content": "Début du contenu qui est automatiquement tronqué à 200 caractères pour réduire...", + "hasImages": true, + "imageCount": 3, + "color": "blue", + "type": "text", + "isPinned": false, + "isArchived": false +} +``` +**Taille :** ~300 bytes par note + +### Mode Full Details +```json +{ + "id": "abc123", + "title": "Note avec images", + "content": "Contenu complet de la note qui peut être très long...", + "images": [ + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg... (100KB+)", + "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQA... (200KB+)", + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg... (150KB+)" + ], + "checkItems": [...], + "labels": [...], + ... +} +``` +**Taille :** 450KB+ par note avec 3 images + +### Économie +Pour 10 notes avec images : +- **Lightweight :** ~3 KB +- **Full Details :** ~4.5 MB +- **Économie :** **99.93%** 🎉 + +## Utilisation dans N8N + +### Workflow Tech News +Le workflow utilise automatiquement le mode lightweight car : +1. On ne fait que lire les titres des notes existantes +2. On créé des notes texte sans images +3. Pas besoin des détails complets + +### Configuration N8N +```json +{ + "method": "POST", + "url": "http://localhost:3001/sse", + "body": { + "jsonrpc": "2.0", + "id": "{{ $now.toUnixInteger() }}", + "method": "tools/call", + "params": { + "name": "get_notes", + "arguments": { + "fullDetails": false // ← Mode lightweight par défaut + } + } + } +} +``` + +## Notes + +- Par défaut, `get_notes` retourne des données lightweight +- Pour obtenir les images, spécifier `fullDetails: true` +- Le contenu est tronqué à 200 caractères max +- Utile pour : + - Lister les notes + - Rechercher par titre + - Vérifier l'existence d'une note + - Workflows N8N optimisés diff --git a/N8N-TECH-NEWS.md b/N8N-TECH-NEWS.md new file mode 100644 index 0000000..9533431 --- /dev/null +++ b/N8N-TECH-NEWS.md @@ -0,0 +1,307 @@ +# 🤖 Workflow N8N - Tech News to Memento + +## 📋 Description + +Ce workflow automatise la veille technologique en : +1. **Lisant** un flux RSS TechCrunch (ou autre source tech) +2. **Analysant** les articles avec GPT-4o-mini +3. **Sélectionnant** les 2 actualités les plus pertinentes +4. **Créant** automatiquement 2 notes dans Memento via MCP + +## 🔧 Architecture du Workflow + +### 1. **Schedule Trigger** ⏰ +- Exécution automatique toutes les **6 heures** +- Configurable selon vos besoins + +### 2. **RSS Feed Reader** 📰 +- Source par défaut : TechCrunch Feed +- Alternatives possibles : + - Hacker News: `https://news.ycombinator.com/rss` + - The Verge: `https://www.theverge.com/rss/index.xml` + - Ars Technica: `https://feeds.arstechnica.com/arstechnica/index` + - MIT Technology Review: `https://www.technologyreview.com/feed/` + +### 3. **Prepare AI Analysis** 🧮 +- Formate les articles pour l'analyse IA +- Crée un prompt système optimisé +- Structure les données pour OpenAI + +### 4. **OpenAI Agent** 🤖 +**Modèle :** GPT-4o-mini +**Temperature :** 0.3 (réponses cohérentes) +**Max Tokens :** 500 + +**Prompt Système :** +``` +Tu es un expert en analyse d'actualités technologiques. +Ta mission est de sélectionner les 2 articles les plus pertinents +et importants parmi une liste d'actualités. + +Tu dois être objectif, privilégier l'innovation et l'impact réel. +Réponds UNIQUEMENT en JSON valide, sans markdown ni texte supplémentaire. +``` + +**Critères de sélection :** +- ✅ Innovation majeure ou rupture technologique +- ✅ Impact significatif sur l'industrie tech +- ✅ Actualité récente et importante +- ❌ Éviter articles marketing/promotionnels +- ✅ Privilégier annonces concrètes + +### 5. **Parse Selection** 🔍 +- Parse la réponse JSON de l'IA +- Gère les formats markdown et JSON brut +- Fallback sur les 2 premiers articles en cas d'erreur + +### 6. **Format Note** 📝 +Crée une note structurée avec : +- 📰 Titre de l'article +- 🔍 Raison de la sélection (par l'IA) +- 📝 Résumé/description +- 🔗 Lien vers l'article complet +- 📅 Date de publication +- 🏷️ Catégories/tags + +**Couleur :** Bleu (tech) +**Labels :** `Tech News`, `Auto-Generated`, + catégories de l'article + +### 7. **MCP - Create Note** 💾 +- Appelle le MCP server sur `http://localhost:3001/sse` +- Utilise le tool `create_note` +- Format JSON-RPC 2.0 + +**Payload exemple :** +```json +{ + "jsonrpc": "2.0", + "id": 1704380400, + "method": "tools/call", + "params": { + "name": "create_note", + "arguments": { + "title": "📰 Major AI Breakthrough Announced...", + "content": "📰 **Full Title**\n\n🔍 **Pourquoi cet article ?**\n...", + "color": "blue", + "type": "text", + "labels": ["Tech News", "Auto-Generated", "AI"] + } + } +} +``` + +### 8. **Extract Result & Summary** ✅ +- Extrait l'ID et le titre des notes créées +- Crée un résumé d'exécution +- Status de succès/échec + +## 🚀 Installation + +### Prérequis +1. **N8N** installé et opérationnel +2. **MCP Server** tournant sur port 3001 +3. **Clé API OpenAI** configurée +4. **Memento** accessible sur localhost:3000 + +### Étapes + +1. **Démarrer le MCP Server** +```powershell +cd d:\dev_new_pc\Keep\mcp-server +node index-sse.js +``` + +2. **Vérifier que Memento tourne** +```powershell +cd d:\dev_new_pc\Keep\keep-notes +npm run dev +``` + +3. **Importer le workflow dans N8N** + - Ouvrir N8N (http://localhost:5678) + - Cliquer "Import from File" + - Sélectionner `n8n-tech-news-workflow.json` + +4. **Configurer les credentials OpenAI** + - Node "OpenAI - Select Best Articles" + - Ajouter votre clé API OpenAI + - Tester la connexion + +5. **Activer le workflow** + - Cliquer sur "Active" en haut à droite + - Le workflow s'exécutera toutes les 6 heures + +## 🧪 Test Manuel + +1. Ouvrir le workflow dans N8N +2. Cliquer sur "Execute Workflow" (éclair ⚡) +3. Vérifier les résultats : + - RSS feed récupéré ✅ + - IA sélectionné 2 articles ✅ + - 2 notes créées dans Memento ✅ + +## 📊 Monitoring + +### Vérifier les notes créées + +**Via l'interface Memento :** +- Ouvrir http://localhost:3000 +- Chercher les notes avec label "Tech News" +- Notes en bleu avec icône 📰 + +**Via MCP :** +```bash +curl -X POST http://localhost:3001/sse \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "get_notes", + "arguments": { + "search": "Tech News" + } + } + }' +``` + +## 🎯 Personnalisation + +### Changer la source RSS + +Dans le node "RSS Feed - Tech News" : +```javascript +// Remplacer l'URL par : +"https://feeds.feedburner.com/venturebeat/SZYF" // VentureBeat +"https://www.wired.com/feed/rss" // Wired +"https://techcrunch.com/feed/" // TechCrunch Alt +``` + +### Modifier les critères de sélection + +Dans le node "Prepare AI Analysis", modifier le prompt : +```javascript +Critères de sélection : +- Focus sur [IA / Blockchain / Cloud / DevOps / ...] +- Articles en français uniquement +- Durée de lecture < 10 min +- etc. +``` + +### Changer la fréquence + +Dans le node "Schedule - Every 6 hours" : +- **Toutes les 3h** : `hoursInterval: 3` +- **Tous les jours à 9h** : `cronExpression: "0 9 * * *"` +- **Du lundi au vendredi** : `cronExpression: "0 9 * * 1-5"` + +### Modifier le nombre d'articles + +Dans "Prepare AI Analysis" : +```javascript +// Passer de 2 à 3 articles +"sélectionnez les 3 articles les PLUS PERTINENTS" + +// Adapter la structure JSON +{ + "selected": [ + { "index": 1, "reason": "..." }, + { "index": 2, "reason": "..." }, + { "index": 3, "reason": "..." } + ] +} +``` + +### Changer la couleur/labels + +Dans le node "Format Note" : +```javascript +color: 'orange', // ou red, green, purple, etc. +labels: ['AI News', 'Breaking', 'Important'] +``` + +## 🐛 Troubleshooting + +### Erreur "MCP Server not responding" +```bash +# Vérifier que le MCP server tourne +curl http://localhost:3001/sse + +# Redémarrer si nécessaire +cd d:\dev_new_pc\Keep\mcp-server +node index-sse.js +``` + +### Erreur OpenAI "Rate limit exceeded" +- Attendre quelques minutes +- Réduire la fréquence du workflow +- Upgrader votre plan OpenAI + +### Pas d'articles sélectionnés +- Vérifier le flux RSS (URL valide ?) +- Tester le prompt OpenAI manuellement +- Vérifier les logs N8N + +### Notes non créées +```javascript +// Vérifier le payload MCP dans le node "MCP - Create Note" +console.log($json); + +// Tester directement avec curl +curl -X POST http://localhost:3001/sse \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_note","arguments":{"title":"Test","content":"Test"}}}' +``` + +## 📈 Optimisations Possibles + +1. **Multi-sources RSS** + - Ajouter plusieurs nodes RSS + - Merger les résultats + - Augmenter à 5-10 articles sélectionnés + +2. **Filtering avancé** + - Ajouter des keywords à exclure + - Filtrer par date (dernières 24h uniquement) + - Éliminer les doublons + +3. **Enrichissement** + - Scraper le contenu complet de l'article + - Générer un résumé avec GPT + - Ajouter des images via API + +4. **Notifications** + - Envoyer email avec les articles sélectionnés + - Notification Slack/Discord + - Push notification mobile + +5. **Analytics** + - Logger les articles sélectionnés + - Stats sur les sources les plus utilisées + - Tendances des sujets tech + +## 🔐 Sécurité + +- ⚠️ Ne pas exposer le MCP server sur internet +- ⚠️ Sécuriser la clé API OpenAI +- ✅ Utiliser variables d'environnement pour secrets +- ✅ Limiter rate limiting sur le RSS + +## 📚 Ressources + +- [N8N Documentation](https://docs.n8n.io/) +- [MCP Protocol Spec](https://modelcontextprotocol.io/) +- [OpenAI API](https://platform.openai.com/docs) +- [RSS Feeds Tech](https://github.com/awesome-rss/awesome-rss) + +## 🎉 Résultat Attendu + +Toutes les 6 heures, vous aurez automatiquement : +- ✅ **2 notes** dans Memento +- 📰 Sur les **actualités tech les plus importantes** +- 🤖 **Sélectionnées par IA** +- 🏷️ **Labellisées** et organisées +- 🔗 Avec **liens** vers articles complets + +**Gain de temps :** ~30 min de veille manuelle par jour = **3.5h par semaine** ! 🚀 diff --git a/keep-notes/app/actions/notes.ts b/keep-notes/app/actions/notes.ts index 57b0348..28c49ab 100644 --- a/keep-notes/app/actions/notes.ts +++ b/keep-notes/app/actions/notes.ts @@ -58,8 +58,10 @@ export async function searchNotes(query: string) { where: { isArchived: false, OR: [ - { title: { contains: query, mode: 'insensitive' } }, - { content: { contains: query, mode: 'insensitive' } } + { title: { contains: query } }, + { content: { contains: query } }, + { labels: { contains: query } }, + { checkItems: { contains: query } } ] }, orderBy: [ @@ -68,7 +70,38 @@ export async function searchNotes(query: string) { ] }) - return notes.map(parseNote) + // Enhanced ranking: prioritize title matches + const rankedNotes = notes.map(note => { + const parsedNote = parseNote(note) + let score = 0 + + // Title match gets highest score + if (parsedNote.title?.toLowerCase().includes(query.toLowerCase())) { + score += 10 + } + + // Content match + if (parsedNote.content.toLowerCase().includes(query.toLowerCase())) { + score += 5 + } + + // Label match + if (parsedNote.labels?.some(label => label.toLowerCase().includes(query.toLowerCase()))) { + score += 3 + } + + // CheckItems match + if (parsedNote.checkItems?.some(item => item.text.toLowerCase().includes(query.toLowerCase()))) { + score += 2 + } + + return { note: parsedNote, score } + }) + + // Sort by score descending, then by existing order (pinned/updated) + return rankedNotes + .sort((a, b) => b.score - a.score) + .map(item => item.note) } catch (error) { console.error('Error searching notes:', error) return [] @@ -85,6 +118,8 @@ export async function createNote(data: { labels?: string[] images?: string[] isArchived?: boolean + reminder?: Date | null + isMarkdown?: boolean }) { try { const note = await prisma.note.create({ @@ -97,6 +132,8 @@ export async function createNote(data: { labels: data.labels ? JSON.stringify(data.labels) : null, images: data.images ? JSON.stringify(data.images) : null, isArchived: data.isArchived || false, + reminder: data.reminder || null, + isMarkdown: data.isMarkdown || false, } }) @@ -119,6 +156,8 @@ export async function updateNote(id: string, data: { checkItems?: CheckItem[] | null labels?: string[] | null images?: string[] | null + reminder?: Date | null + isMarkdown?: boolean }) { try { // Stringify JSON fields if they exist diff --git a/keep-notes/components/markdown-content.tsx b/keep-notes/components/markdown-content.tsx new file mode 100644 index 0000000..885a7a7 --- /dev/null +++ b/keep-notes/components/markdown-content.tsx @@ -0,0 +1,19 @@ +'use client' + +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' + +interface MarkdownContentProps { + content: string + className?: string +} + +export function MarkdownContent({ content, className = '' }: MarkdownContentProps) { + return ( +
+ + {content} + +
+ ) +} diff --git a/keep-notes/components/note-card.tsx b/keep-notes/components/note-card.tsx index 62933cd..fb7a60e 100644 --- a/keep-notes/components/note-card.tsx +++ b/keep-notes/components/note-card.tsx @@ -20,10 +20,14 @@ import { Pin, Tag, Trash2, + Bell, } from 'lucide-react' import { useState } from 'react' import { deleteNote, toggleArchive, togglePin, updateColor, updateNote } from '@/app/actions/notes' import { cn } from '@/lib/utils' +import { formatDistanceToNow } from 'date-fns' +import { fr } from 'date-fns/locale' +import { MarkdownContent } from './markdown-content' interface NoteCardProps { note: Note @@ -105,6 +109,16 @@ export function NoteCard({ note, onEdit, onDragStart, onDragEnd, onDragOver, isD )} + {/* Reminder Icon */} + {note.reminder && new Date(note.reminder) > new Date() && ( + + )} + {/* Title */} {note.title && (

@@ -173,9 +187,15 @@ export function NoteCard({ note, onEdit, onDragStart, onDragEnd, onDragOver, isD {/* Content */} {note.type === 'text' ? ( -

- {note.content} -

+ note.isMarkdown ? ( +
+ +
+ ) : ( +

+ {note.content} +

+ ) ) : (
{note.checkItems?.map((item) => ( @@ -217,6 +237,11 @@ export function NoteCard({ note, onEdit, onDragStart, onDragEnd, onDragOver, isD
)} + {/* Creation Date */} +
+ {formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: fr })} +
+ {/* Action Bar - Shows on Hover */}
(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(null) + + // Reminder state + const [showReminderDialog, setShowReminderDialog] = useState(false) + const [reminderDate, setReminderDate] = useState('') + const [reminderTime, setReminderTime] = useState('') + const [currentReminder, setCurrentReminder] = useState(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' ? ( -