feat: add note templates system with 10 pre-built templates

- Create noteTemplates.ts with 10 default templates (Meeting, Daily Journal, Project Planning, etc.)
- Add TemplateSelector component with search and category filtering
- Integrate template selector in desktop and mobile note creation flows
- Support automatic todo creation from templates
- Organize templates by category (Travail, Personnel, Créativité, Autre)
This commit is contained in:
Claude committed 2025-11-27 13:43:22 +00:00
1 parent c4c17264f4
commit 0a22cc784f
4 files changed
+550 -7

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { Search, Sparkles } from "lucide-react";
import { DEFAULT_TEMPLATES, getTemplatesByCategory, searchTemplates, type NoteTemplate } from "@/utils/noteTemplates";
interface TemplateSelectorProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSelectTemplate: (template: NoteTemplate) => void;
}
export function TemplateSelector({ open, onOpenChange, onSelectTemplate }: TemplateSelectorProps) {
const [searchQuery, setSearchQuery] = useState("");
const templates = searchQuery.trim()
? searchTemplates(searchQuery)
: DEFAULT_TEMPLATES;
const templatesByCategory = getTemplatesByCategory();
const handleSelectTemplate = (template: NoteTemplate) => {
onSelectTemplate(template);
onOpenChange(false);
setSearchQuery("");
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
Choisir un template
</DialogTitle>
<DialogDescription>
Démarrez rapidement avec un modèle prédéfini
</DialogDescription>
</DialogHeader>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Rechercher un template..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
{/* Templates Grid */}
<ScrollArea className="h-[400px] pr-4">
{searchQuery.trim() ? (
// Search Results
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{templates.map((template) => (
<TemplateCard
key={template.id}
template={template}
onClick={() => handleSelectTemplate(template)}
/>
))}
{templates.length === 0 && (
<div className="col-span-2 text-center py-8 text-muted-foreground">
Aucun template trouvé
</div>
)}
</div>
) : (
// Templates by Category
<div className="space-y-6">
{Array.from(templatesByCategory.entries()).map(([category, categoryTemplates]) => (
<div key={category}>
<h3 className="font-semibold mb-3 text-sm text-muted-foreground uppercase tracking-wide">
{category}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{categoryTemplates.map((template) => (
<TemplateCard
key={template.id}
template={template}
onClick={() => handleSelectTemplate(template)}
/>
))}
</div>
</div>
))}
</div>
)}
</ScrollArea>
</DialogContent>
</Dialog>
);
}
interface TemplateCardProps {
template: NoteTemplate;
onClick: () => void;
}
function TemplateCard({ template, onClick }: TemplateCardProps) {
return (
<Button
variant="outline"
className="h-auto p-4 flex flex-col items-start gap-2 hover:bg-accent hover:border-primary transition-all"
onClick={onClick}
>
<div className="flex items-center gap-2 w-full">
<span className="text-2xl">{template.icon}</span>
<span className="font-semibold text-left flex-1">{template.name}</span>
</div>
<p className="text-sm text-muted-foreground text-left">
{template.description}
</p>
{template.todos && template.todos.length > 0 && (
<Badge variant="secondary" className="text-xs">
{template.todos.length} tâche{template.todos.length > 1 ? 's' : ''}
</Badge>
)}
</Button>
);
}
+34 -4
View File
@@ -34,6 +34,8 @@ import { AddUserModal } from "@/components/modals/AddUserModal";
import { compressImage, formatFileSize } from "@/utils/imageCompression";
import { ModeToggle } from "@/components/mode-toggle";
import { downloadNoteAsMarkdown, downloadAllNotesAsMarkdown, downloadTodosAsMarkdown } from "@/utils/markdownExport";
import { TemplateSelector } from "@/components/TemplateSelector";
import type { NoteTemplate } from "@/utils/noteTemplates";
// ===== FONCTIONS UTILITAIRES TIMEZONE EUROPE/PARIS =====
@@ -147,6 +149,7 @@ const Index = () => {
const [addUserModal, setAddUserModal] = useState(false);
const [deleteUserModal, setDeleteUserModal] = useState<{ open: boolean, userId?: number }>({ open: false });
const [changePasswordModal, setChangePasswordModal] = useState<{ open: boolean, userId?: number }>({ open: false });
const [templateSelectorOpen, setTemplateSelectorOpen] = useState(false);
const [addNoteTodoModal, setAddNoteTodoModal] = useState(false);
const [addTagModal, setAddTagModal] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
@@ -410,12 +413,32 @@ const Index = () => {
navigate("/login");
};
const handleCreateNote = async () => {
const handleCreateNote = () => {
setTemplateSelectorOpen(true);
};
const handleCreateNoteFromTemplate = async (template: NoteTemplate) => {
try {
const newNote = await NotesService.createNote("Nouvelle note");
const title = template.name === "Note vide" ? "Nouvelle note" : template.name;
const newNote = await NotesService.createNote(title, template.content);
if (newNote) {
setNotes([newNote, ...notes]);
setOpenNote(newNote);
// Add todos if template has any
if (template.todos && template.todos.length > 0) {
for (const todo of template.todos) {
await NotesService.addTodo(newNote.id!, todo.text);
}
// Reload note to get the todos
const notes = await NotesService.getNotes();
const updatedNote = notes.find(n => n.id === newNote.id);
if (updatedNote) {
setNotes([updatedNote, ...notes.filter(n => n.id !== newNote.id)]);
setOpenNote(updatedNote);
}
} else {
setNotes([newNote, ...notes]);
setOpenNote(newNote);
}
showSuccess("Note créée depuis le template");
}
} catch (error) {
showError("Erreur lors de la création de la note");
@@ -2287,6 +2310,13 @@ const Index = () => {
confirmText="Ajouter"
/>
{/* Template Selector */}
<TemplateSelector
open={templateSelectorOpen}
onOpenChange={setTemplateSelectorOpen}
onSelectTemplate={handleCreateNoteFromTemplate}
/>
{/* Add Event Modal */}
<Dialog open={addEventModal} onOpenChange={setAddEventModal}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
+27 -3
View File
@@ -8,6 +8,8 @@ import { MobileCard } from "@/components/mobile/MobileCard";
import { Plus, Search, CheckSquare, Image as ImageIcon, Paperclip, Tag as TagIcon, FileText } from "lucide-react";
import NotesService, { Note } from "@/services/NotesService";
import { showError, showSuccess } from "@/utils/toast";
import { TemplateSelector } from "@/components/TemplateSelector";
import type { NoteTemplate } from "@/utils/noteTemplates";
const NOTES_PER_PAGE = 10;
@@ -16,6 +18,7 @@ export default function NotesPage() {
const [searchQuery, setSearchQuery] = useState("");
const [showArchived, setShowArchived] = useState(false);
const [page, setPage] = useState(0);
const [templateSelectorOpen, setTemplateSelectorOpen] = useState(false);
const navigate = useNavigate();
const loadNotes = async () => {
@@ -48,10 +51,24 @@ export default function NotesPage() {
const totalPages = Math.ceil(filteredNotes.length / NOTES_PER_PAGE);
const paginatedNotes = filteredNotes.slice(page * NOTES_PER_PAGE, (page + 1) * NOTES_PER_PAGE);
const handleCreateNote = async () => {
const handleCreateNote = () => {
setTemplateSelectorOpen(true);
};
const handleCreateNoteFromTemplate = async (template: NoteTemplate) => {
try {
const newNote = await NotesService.createNote("Nouvelle note", "");
navigate(`/mobile/notes/${newNote.id}`);
const title = template.name === "Note vide" ? "Nouvelle note" : template.name;
const newNote = await NotesService.createNote(title, template.content);
if (newNote) {
// Add todos if template has any
if (template.todos && template.todos.length > 0) {
for (const todo of template.todos) {
await NotesService.addTodo(newNote.id!, todo.text);
}
}
showSuccess("Note créée depuis le template");
navigate(`/mobile/notes/${newNote.id}`);
}
} catch (error) {
showError("Erreur lors de la création de la note");
}
@@ -238,6 +255,13 @@ export default function NotesPage() {
{/* FAB */}
<MobileFAB icon={Plus} onClick={handleCreateNote} label="Créer une note" />
{/* Template Selector */}
<TemplateSelector
open={templateSelectorOpen}
onOpenChange={setTemplateSelectorOpen}
onSelectTemplate={handleCreateNoteFromTemplate}
/>
</div>
);
}
+363
View File
@@ -0,0 +1,363 @@
export interface NoteTemplate {
id: string;
name: string;
description: string;
icon: string;
category: string;
content: string;
todos?: Array<{ text: string; completed: boolean }>;
}
export const DEFAULT_TEMPLATES: NoteTemplate[] = [
{
id: 'blank',
name: 'Note vide',
description: 'Commencer avec une page blanche',
icon: '📄',
category: 'Général',
content: '',
todos: []
},
{
id: 'meeting',
name: 'Réunion',
description: 'Notes de réunion structurées',
icon: '🤝',
category: 'Travail',
content: `<h2>Réunion - [Titre]</h2>
<p><strong>Date:</strong> ${new Date().toLocaleDateString('fr-FR')}</p>
<p><strong>Participants:</strong> </p>
<p><strong>Objectif:</strong> </p>
<h3>Points discutés</h3>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<h3>Décisions</h3>
<p>• </p>
<h3>Prochaines étapes</h3>
<p>→ </p>`,
todos: [
{ text: 'Envoyer le compte-rendu', completed: false },
{ text: 'Planifier le suivi', completed: false }
]
},
{
id: 'daily-journal',
name: 'Journal quotidien',
description: 'Réflexions et gratitude du jour',
icon: '📔',
category: 'Personnel',
content: `<h2>Journal - ${new Date().toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</h2>
<h3>🌅 Ce matin je me sens...</h3>
<p></p>
<h3>🎯 Mes priorités aujourd'hui</h3>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<h3>💡 Idées et réflexions</h3>
<p></p>
<h3>🙏 Gratitude</h3>
<p>Aujourd'hui je suis reconnaissant(e) pour:</p>
<p>• </p>
<h3>🌙 Bilan de la journée</h3>
<p></p>`,
todos: []
},
{
id: 'project-planning',
name: 'Planification de projet',
description: 'Organiser un nouveau projet',
icon: '🎯',
category: 'Travail',
content: `<h2>Projet - [Nom du projet]</h2>
<h3>📋 Vue d'ensemble</h3>
<p><strong>Objectif:</strong> </p>
<p><strong>Deadline:</strong> </p>
<p><strong>Responsable:</strong> </p>
<h3>🎯 Objectifs</h3>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<h3>📊 Phases du projet</h3>
<p><strong>Phase 1:</strong> Préparation</p>
<p>• </p>
<p><strong>Phase 2:</strong> Développement</p>
<p>• </p>
<p><strong>Phase 3:</strong> Finalisation</p>
<p>• </p>
<h3>👥 Équipe</h3>
<p>• </p>
<h3>⚠️ Risques identifiés</h3>
<p>• </p>
<h3>📌 Ressources nécessaires</h3>
<p>• </p>`,
todos: [
{ text: 'Définir le scope', completed: false },
{ text: 'Assembler l\'équipe', completed: false },
{ text: 'Créer le planning', completed: false }
]
},
{
id: 'brainstorming',
name: 'Brainstorming',
description: 'Session de génération d\'idées',
icon: '💭',
category: 'Créativité',
content: `<h2>💭 Brainstorming - [Sujet]</h2>
<p><strong>Date:</strong> ${new Date().toLocaleDateString('fr-FR')}</p>
<p><strong>Question centrale:</strong> </p>
<h3>💡 Idées</h3>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<p>4. </p>
<p>5. </p>
<h3>⭐ Idées à creuser</h3>
<p>• </p>
<h3>🚀 Actions possibles</h3>
<p>→ </p>
<h3>🔗 Connexions et insights</h3>
<p></p>`,
todos: []
},
{
id: 'recipe',
name: 'Recette de cuisine',
description: 'Noter une recette',
icon: '👨‍🍳',
category: 'Personnel',
content: `<h2>🍳 Recette - [Nom du plat]</h2>
<p><strong>⏱️ Temps de préparation:</strong> </p>
<p><strong>👥 Portions:</strong> </p>
<p><strong>😋 Difficulté:</strong> Facile / Moyen / Difficile</p>
<h3>📝 Ingrédients</h3>
<p>• </p>
<h3>👨‍🍳 Préparation</h3>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<h3>💡 Astuces</h3>
<p>• </p>
<h3>📌 Notes</h3>
<p></p>`,
todos: []
},
{
id: 'weekly-review',
name: 'Revue hebdomadaire',
description: 'Bilan et planification de la semaine',
icon: '📊',
category: 'Productivité',
content: `<h2>📊 Revue de la semaine</h2>
<p><strong>Semaine du:</strong> ${new Date().toLocaleDateString('fr-FR')}</p>
<h3>✅ Accomplissements</h3>
<p>Cette semaine j'ai réussi à:</p>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<h3>📈 Progrès</h3>
<p>• </p>
<h3>⚠️ Défis rencontrés</h3>
<p>• </p>
<h3>💡 Leçons apprises</h3>
<p>• </p>
<h3>🎯 Objectifs semaine prochaine</h3>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<h3>🔄 Habitudes à améliorer</h3>
<p>• </p>`,
todos: [
{ text: 'Planifier la semaine prochaine', completed: false }
]
},
{
id: 'bug-report',
name: 'Rapport de bug',
description: 'Documenter un problème technique',
icon: '🐛',
category: 'Développement',
content: `<h2>🐛 Bug Report</h2>
<p><strong>Date:</strong> ${new Date().toLocaleDateString('fr-FR')}</p>
<p><strong>Priorité:</strong> 🔴 Critique / 🟠 Haute / 🟡 Moyenne / 🟢 Basse</p>
<p><strong>Statut:</strong> Nouveau / En cours / Résolu</p>
<h3>📋 Description</h3>
<p></p>
<h3>🔄 Étapes pour reproduire</h3>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<h3>✅ Résultat attendu</h3>
<p></p>
<h3>❌ Résultat actuel</h3>
<p></p>
<h3>💻 Environnement</h3>
<p><strong>OS:</strong> </p>
<p><strong>Navigateur:</strong> </p>
<p><strong>Version:</strong> </p>
<h3>📌 Notes additionnelles</h3>
<p></p>
<h3>🔧 Solution proposée</h3>
<p></p>`,
todos: [
{ text: 'Reproduire le bug', completed: false },
{ text: 'Identifier la cause', completed: false },
{ text: 'Implémenter le fix', completed: false },
{ text: 'Tester la solution', completed: false }
]
},
{
id: 'learning-notes',
name: 'Notes d\'apprentissage',
description: 'Apprendre un nouveau sujet',
icon: '📚',
category: 'Éducation',
content: `<h2>📚 Notes d'apprentissage - [Sujet]</h2>
<p><strong>Date:</strong> ${new Date().toLocaleDateString('fr-FR')}</p>
<p><strong>Source:</strong> </p>
<h3>🎯 Objectifs d'apprentissage</h3>
<p>• </p>
<h3>📝 Concepts clés</h3>
<p><strong>Concept 1:</strong> </p>
<p>• </p>
<p><strong>Concept 2:</strong> </p>
<p>• </p>
<h3>💡 Exemples</h3>
<p></p>
<h3>❓ Questions</h3>
<p>• </p>
<h3>🔗 Ressources</h3>
<p>• </p>
<h3>📌 À retenir</h3>
<p>• </p>
<h3>✍️ Pratique</h3>
<p>Exercices à faire:</p>
<p>1. </p>`,
todos: [
{ text: 'Réviser les notes', completed: false },
{ text: 'Faire les exercices', completed: false }
]
},
{
id: 'travel-planning',
name: 'Planification de voyage',
description: 'Organiser un déplacement',
icon: '✈️',
category: 'Personnel',
content: `<h2>✈️ Voyage - [Destination]</h2>
<p><strong>📅 Dates:</strong> du [date] au [date]</p>
<p><strong>👥 Voyageurs:</strong> </p>
<p><strong>💰 Budget:</strong> </p>
<h3>🎒 À emporter</h3>
<p>Vêtements:</p>
<p>• </p>
<p>Documents:</p>
<p>• Passeport</p>
<p>• Billets</p>
<p>• Assurance</p>
<h3>🏨 Hébergement</h3>
<p><strong>Hôtel/Airbnb:</strong> </p>
<p><strong>Adresse:</strong> </p>
<p><strong>Check-in:</strong> </p>
<h3>✈️ Transport</h3>
<p><strong>Vol aller:</strong> </p>
<p><strong>Vol retour:</strong> </p>
<h3>📍 Lieux à visiter</h3>
<p>1. </p>
<p>2. </p>
<p>3. </p>
<h3>🍽️ Restaurants recommandés</h3>
<p>• </p>
<h3>💡 Conseils et infos pratiques</h3>
<p>• </p>`,
todos: [
{ text: 'Réserver les vols', completed: false },
{ text: 'Réserver l\'hébergement', completed: false },
{ text: 'Faire la valise', completed: false }
]
}
];
export function getTemplatesByCategory(): Map<string, NoteTemplate[]> {
const categorized = new Map<string, NoteTemplate[]>();
DEFAULT_TEMPLATES.forEach(template => {
const category = template.category;
if (!categorized.has(category)) {
categorized.set(category, []);
}
categorized.get(category)!.push(template);
});
return categorized;
}
export function getTemplateById(id: string): NoteTemplate | undefined {
return DEFAULT_TEMPLATES.find(t => t.id === id);
}
export function searchTemplates(query: string): NoteTemplate[] {
const lowerQuery = query.toLowerCase();
return DEFAULT_TEMPLATES.filter(template =>
template.name.toLowerCase().includes(lowerQuery) ||
template.description.toLowerCase().includes(lowerQuery) ||
template.category.toLowerCase().includes(lowerQuery)
);
}