23: note templates - localStorage templates (Meeting notes, Daily journal, Brain dump)
This commit is contained in:
@@ -5,6 +5,7 @@ import { Plus, FileText, Link2, GitBranch, Trash2, Pin, Archive, Search, PinOff,
|
|||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { NoteTemplates } from '@/components/notes/note-templates';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -162,6 +163,28 @@ export default function NotesPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createNoteWithContent(content: string) {
|
||||||
|
if (!domainId) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/domains/" + domainId + "/notes", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: 'Untitled note',
|
||||||
|
content,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Unable to create note.');
|
||||||
|
const newNote = await response.json();
|
||||||
|
setNotes((current) => [newNote, ...current]);
|
||||||
|
setSelectedNote(newNote);
|
||||||
|
toast.success('Note created from template');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create note:', error);
|
||||||
|
toast.error('Unable to create note');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createNote() {
|
async function createNote() {
|
||||||
if (!domainId) return;
|
if (!domainId) return;
|
||||||
try {
|
try {
|
||||||
@@ -315,6 +338,9 @@ export default function NotesPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
|
<NoteTemplates onCreateFromTemplate={(content) => {
|
||||||
|
createNoteWithContent(content);
|
||||||
|
}} />
|
||||||
<Button onClick={createNote}>
|
<Button onClick={createNote}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
New note
|
New note
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { FileText, NotebookText, BrainCircuit } from 'lucide-react';
|
||||||
|
|
||||||
|
const TEMPLATES_KEY = 'pe_note_templates';
|
||||||
|
|
||||||
|
interface NoteTemplate {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TEMPLATES: NoteTemplate[] = [
|
||||||
|
{
|
||||||
|
name: 'Meeting notes',
|
||||||
|
icon: 'FileText',
|
||||||
|
content: '<h2>Meeting: [Title]</h2><p><strong>Date:</strong> [Date]</p><p><strong>Attendees:</strong></p><ul><li></li></ul><h3>Agenda</h3><ol><li></li></ol><h3>Notes</h3><p></p><h3>Action Items</h3><ul><li></li></ul>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Daily journal',
|
||||||
|
icon: 'NotebookText',
|
||||||
|
content: '<h2>[Date]</h2><h3>What I did today</h3><p></p><h3>What I learned</h3><p></p><h3>What I\'m grateful for</h3><ul><li></li></ul>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Brain dump',
|
||||||
|
icon: 'BrainCircuit',
|
||||||
|
content: '<h2>Brain Dump — [Date]</h2><p>Everything on my mind right now:</p><ul><li></li></ul><h3>Priorities</h3><ol><li></li></ol>',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getTemplates(): NoteTemplate[] {
|
||||||
|
if (typeof window === 'undefined') return DEFAULT_TEMPLATES;
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(TEMPLATES_KEY);
|
||||||
|
if (stored) return JSON.parse(stored);
|
||||||
|
} catch {}
|
||||||
|
return DEFAULT_TEMPLATES;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NoteTemplatesProps {
|
||||||
|
onCreateFromTemplate: (content: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NoteTemplates({ onCreateFromTemplate }: NoteTemplatesProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [templates, setTemplates] = useState<NoteTemplate[]>(getTemplates);
|
||||||
|
|
||||||
|
const handleSelect = useCallback(
|
||||||
|
(template: NoteTemplate) => {
|
||||||
|
const content = template.content
|
||||||
|
.replace(/\[Date\]/g, new Date().toLocaleDateString())
|
||||||
|
.replace(/\[Title\]/g, template.name);
|
||||||
|
onCreateFromTemplate(content);
|
||||||
|
setOpen(false);
|
||||||
|
toast.success('Note created from template');
|
||||||
|
},
|
||||||
|
[onCreateFromTemplate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconMap: Record<string, React.ReactNode> = {
|
||||||
|
FileText: <FileText className="h-5 w-5" />,
|
||||||
|
NotebookText: <NotebookText className="h-5 w-5" />,
|
||||||
|
BrainCircuit: <BrainCircuit className="h-5 w-5" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(true)}>
|
||||||
|
<FileText className="mr-2 h-4 w-4" />
|
||||||
|
+ From template
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Choose a template</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Start with a pre-formatted note template.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<button
|
||||||
|
key={template.name}
|
||||||
|
onClick={() => handleSelect(template)}
|
||||||
|
className="flex w-full items-center gap-3 rounded-lg border p-3 text-left transition-colors hover:bg-accent"
|
||||||
|
>
|
||||||
|
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||||
|
{iconMap[template.icon] || <FileText className="h-5 w-5" />}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">{template.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{template.name === 'Meeting notes'
|
||||||
|
? 'Structured meeting notes with agenda and action items'
|
||||||
|
: template.name === 'Daily journal'
|
||||||
|
? 'Daily reflection with accomplishments and learnings'
|
||||||
|
: 'Free-form thought capture'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="text-xs text-muted-foreground">
|
||||||
|
Templates are stored locally in your browser.
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user