import { createSignal, createEffect, onMount, For, Show } from 'solid-js'; import { Button } from '@/components/ui/Button'; import { SearchTagFilterBar } from '@/components/ui/SearchTagFilterBar'; import { NoteModal } from '@/components/ui/NoteModal'; import { ViewNoteModal } from '@/components/ui/ViewNoteModal'; import { IconPin, IconTrash, IconEdit, IconCopy, IconDownload, IconPaperclip } from '@tabler/icons-solidjs'; import { getMockNotes } from '@/lib/mockData'; import { isDemoMode, shouldUseRealBackend } from '@/lib/demo-mode'; import { getApiV1BaseUrl } from '@/lib/api-url'; const API_BASE_URL = getApiV1BaseUrl(); interface Note { id: number; title: string; content: string; createdAt: string; updatedAt: string; tags: string[]; pinned: boolean; attachments?: Array<{ id: string; name: string; type: string; size: string; url?: string; }>; isMarkdown?: boolean; isHtml?: boolean; } const renderMarkdownPreviewHtml = (content: string, maxBlocks = 4): string => { const html = content .replace(/^# (.*$)/gim, '

$1<\/h1>') .replace(/^## (.*$)/gim, '

$1<\/h2>') .replace(/^### (.*$)/gim, '

$1<\/h3>') .replace(/^#### (.*$)/gim, '

$1<\/h4>') .replace(/\*\*(.*?)\*\*/g, '$1<\/strong>') .replace(/\*(.*?)\*/g, '$1<\/em>') .replace(/`(.*?)`/g, '$1<\/code>') .replace(/```(.*?)\n([\s\S]*?)```/g, '
$2<\/code><\/pre>')
    .replace(/^- \[ \] (.*$)/gim, '
$1
') .replace(/^- \[x\] (.*$)/gim, '
$1
') .replace(/^- (.*$)/gim, '
  • $1<\/li>') .replace(/^\d+\. (.*$)/gim, '
  • $1<\/li>') .replace(/> (.*$)/gim, '
    $1<\/blockquote>') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1<\/a>') .replace(/\n\n+/g, '<\/p>

    '); const parts = html.split('<\/p>

    '); const limited = parts.slice(0, maxBlocks).join('<\/p>

    '); return limited; }; const renderPlainTextPreviewHtml = (content: string): string => { return content .replace(/(https?:\/\/[^\s]+)/g, '$1<\/a>') .replace(/\*\*(.*?)\*\*/g, '$1<\/strong>') .replace(/\*(.*?)\*/g, '$1<\/em>') .replace(/^- \[ \] (.*$)/gim, '

    ') .replace(/^- \[x\] (.*$)/gim, '
    $1
    ') .split('\n') .slice(0, 6) .map((line) => (line ? line : '
    ')) .join('\n'); }; export const Notes = () => { const [notes, setNotes] = createSignal([]); const [isLoading, setIsLoading] = createSignal(true); const [searchTerm, setSearchTerm] = createSignal(''); const [selectedTags, setSelectedTags] = createSignal([]); const [showAddModal, setShowAddModal] = createSignal(false); const [showEditModal, setShowEditModal] = createSignal(false); const [showViewModal, setShowViewModal] = createSignal(false); const [editingNote, setEditingNote] = createSignal(null); const [viewingNote, setViewingNote] = createSignal(null); const [copiedContent, setCopiedContent] = createSignal(false); const [expandedNotes, setExpandedNotes] = createSignal>(new Set()); onMount(async () => { try { let notesData: any[] = []; // Check if we should use demo mode or real API if (isDemoMode() && !shouldUseRealBackend()) { console.log('[Notes] Loading demo notes data'); // Load mock notes data for demo mode const mockNotesData = getMockNotes(); notesData = mockNotesData; } else { console.log('[Notes] Loading notes from real API'); // Load from real API const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/notes`, { headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, }); if (!response.ok) { throw new Error('Failed to load notes'); } notesData = await response.json(); } const adaptedNotes: Note[] = (Array.isArray(notesData) ? notesData : []).map((note: any, index) => { const tags = Array.isArray(note.tags) ? note.tags.map((tag: any) => (typeof tag === 'string' ? tag : tag?.name)).filter(Boolean) : []; const content = note.content || ''; const createdAt = note.created_at || note.createdAt || new Date().toISOString(); const updatedAt = note.updated_at || note.updatedAt || createdAt; return { id: Number(note.id || index + 1), title: note.title || 'Untitled note', content, createdAt, updatedAt, tags, pinned: Boolean(note.pinned ?? note.is_pinned ?? tags.includes('pinned')), attachments: Array.isArray(note.attachments) ? note.attachments.map((att: any, attachmentIndex: number) => ({ id: String(att.id || `att_${attachmentIndex}`), name: att.name || 'attachment', type: att.type || 'file', size: att.size || '', url: att.url, })) : [], isMarkdown: content.includes('#') || content.includes('*'), isHtml: content.includes('<') && content.includes('>'), }; }); setNotes(adaptedNotes); } catch (error) { console.error('Failed to load notes:', error); // Fallback to demo data if API fails if (!isDemoMode()) { console.log('[Notes] API failed, falling back to demo data'); const mockNotesData = getMockNotes(); const adaptedNotes: Note[] = mockNotesData.map((note: any, index) => { const tags = Array.isArray(note.tags) ? note.tags.map((tag: any) => (typeof tag === 'string' ? tag : tag?.name)).filter(Boolean) : []; const content = note.content || ''; const createdAt = note.created_at || note.createdAt || new Date().toISOString(); const updatedAt = note.updated_at || note.updatedAt || createdAt; return { id: Number(note.id || index + 1), title: note.title || 'Untitled note', content, createdAt, updatedAt, tags, pinned: Boolean(note.pinned ?? note.is_pinned ?? tags.includes('pinned')), attachments: Array.isArray(note.attachments) ? note.attachments.map((att: any, attachmentIndex: number) => ({ id: String(att.id || `att_${attachmentIndex}`), name: att.name || 'attachment', type: att.type || 'file', size: att.size || '', url: att.url, })) : [], isMarkdown: content.includes('#') || content.includes('*'), isHtml: content.includes('<') && content.includes('>'), }; }); setNotes(adaptedNotes); } else { setNotes([]); } } finally { setIsLoading(false); } }); const filteredNotes = () => { const term = searchTerm().toLowerCase(); const tags = selectedTags(); return notes().filter(note => { const matchesSearch = note.title.toLowerCase().includes(term) || note.content.toLowerCase().includes(term) || note.tags.some(tag => tag.toLowerCase().includes(term)); const matchesTags = tags.length === 0 || tags.every(tag => note.tags.includes(tag)); return matchesSearch && matchesTags; }).sort((a, b) => { if (a.pinned && !b.pinned) return -1; if (!a.pinned && b.pinned) return 1; return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); }); }; const allTags = () => { const tagSet = new Set(); notes().forEach(note => { note.tags.forEach(tag => tagSet.add(tag)); }); return Array.from(tagSet).sort(); }; const toggleTag = (tag: string) => { const currentTags = selectedTags(); if (currentTags.includes(tag)) { setSelectedTags([]); } else { setSelectedTags([tag]); } }; const handleAddNote = async (noteData: any) => { try { if (isDemoMode() && !shouldUseRealBackend()) { // Demo mode: Add note locally const note: Note = { id: Date.now(), title: noteData.title, content: noteData.content, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), tags: noteData.tags, pinned: false }; setNotes(prev => [note, ...prev]); setShowAddModal(false); } else { // Real API: Create note via API const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/notes`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(noteData), }); if (!response.ok) { throw new Error('Failed to create note'); } const newNote = await response.json(); const adaptedNote: Note = { id: newNote.id, title: newNote.title, content: newNote.content, createdAt: newNote.created_at || newNote.createdAt, updatedAt: newNote.updated_at || newNote.updatedAt, tags: Array.isArray(newNote.tags) ? newNote.tags.map((tag: any) => typeof tag === 'string' ? tag : tag.name) : [], pinned: Boolean(newNote.pinned ?? newNote.is_pinned), attachments: [], isMarkdown: newNote.content.includes('#') || newNote.content.includes('*'), isHtml: newNote.content.includes('<') && newNote.content.includes('>'), }; setNotes(prev => [adaptedNote, ...prev]); setShowAddModal(false); } } catch (error) { console.error('Failed to add note:', error); } }; const handleEditNote = async (noteData: any) => { try { if (isDemoMode() && !shouldUseRealBackend()) { // Demo mode: Update note locally setNotes(prev => prev.map(note => note.id === noteData.id ? { ...note, title: noteData.title, content: noteData.content, tags: noteData.tags, updatedAt: new Date().toISOString() } : note )); setShowEditModal(false); setEditingNote(null); } else { // Real API: Update note via API const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/notes/${noteData.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(noteData), }); if (!response.ok) { throw new Error('Failed to update note'); } const updatedNote = await response.json(); setNotes(prev => prev.map(note => note.id === noteData.id ? { ...note, title: updatedNote.title || noteData.title, content: updatedNote.content || noteData.content, tags: Array.isArray(updatedNote.tags) ? updatedNote.tags.map((tag: any) => typeof tag === 'string' ? tag : tag.name) : noteData.tags, updatedAt: updatedNote.updated_at || updatedNote.updatedAt || new Date().toISOString() } : note )); setShowEditModal(false); setEditingNote(null); } } catch (error) { console.error('Failed to update note:', error); } }; const togglePin = async (noteId: number) => { try { if (isDemoMode() && !shouldUseRealBackend()) { // Demo mode: Toggle pin locally setNotes(prev => prev.map(note => note.id === noteId ? { ...note, pinned: !note.pinned } : note )); } else { // Real API: Toggle pin via API const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token'); const note = notes().find(n => n.id === noteId); const response = await fetch(`${API_BASE_URL}/notes/${noteId}/pin`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ pinned: !note?.pinned }), }); if (!response.ok) { throw new Error('Failed to toggle pin'); } setNotes(prev => prev.map(note => note.id === noteId ? { ...note, pinned: !note.pinned } : note )); } } catch (error) { console.error('Failed to toggle pin:', error); } }; const deleteNote = async (noteId: number) => { try { if (isDemoMode() && !shouldUseRealBackend()) { // Demo mode: Delete note locally setNotes(prev => prev.filter(note => note.id !== noteId)); } else { // Real API: Delete note via API const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/notes/${noteId}`, { method: 'DELETE', headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, }); if (!response.ok) { throw new Error('Failed to delete note'); } setNotes(prev => prev.filter(note => note.id !== noteId)); } } catch (error) { console.error('Failed to delete note:', error); } }; const startEditNote = (note: Note) => { setEditingNote(note); setShowEditModal(true); }; const viewNote = (note: Note) => { console.log('Viewing note:', note.title); setViewingNote(note); setShowViewModal(true); }; const copyNoteContent = async (note: Note) => { try { await navigator.clipboard.writeText(note.content); setCopiedContent(true); setTimeout(() => setCopiedContent(false), 2000); } catch (error) { console.error('Failed to copy content:', error); } }; const toggleNoteExpansion = (noteId: number) => { setExpandedNotes(prev => { const newSet = new Set(prev); if (newSet.has(noteId)) { newSet.delete(noteId); } else { newSet.add(noteId); } return newSet; }); }; const exportNote = (note: Note) => { const content = note.isMarkdown ? `# ${note.title}\n\n${note.content}` : note.content; const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${note.title.replace(/\s+/g, '_')}.md`; a.click(); URL.revokeObjectURL(url); }; // Add this function to handle checkbox changes const updateNoteCheckbox = (noteId: number, checkboxIndex: number, isChecked: boolean) => { setNotes(prev => prev.map(note => { if (note.id === noteId) { const lines = note.content.split('\n'); let checkboxCount = 0; const updatedLines = lines.map(line => { const uncheckedMatch = line.match(/^- \[ \] (.*)$/); const checkedMatch = line.match(/^- \[x\] (.*)$/); if (uncheckedMatch || checkedMatch) { if (checkboxCount === checkboxIndex) { const text = uncheckedMatch ? uncheckedMatch[1] : (checkedMatch ? checkedMatch[1] : ''); return isChecked ? `- [x] ${text}` : `- [ ] ${text}`; } checkboxCount++; } return line; }); return { ...note, content: updatedLines.join('\n'), updatedAt: new Date().toISOString() }; } return note; })); }; // Handler for updating note content from ViewNoteModal const handleUpdateNoteContent = (noteId: number, content: string) => { setNotes(prev => prev.map(note => note.id === noteId ? { ...note, content, updatedAt: new Date().toISOString() } : note )); }; // Make the function available globally for checkbox onchange handlers createEffect(() => { (window as any).updateNoteContent = (checkbox: HTMLInputElement) => { const noteElement = checkbox.closest('[data-note-id]'); if (noteElement) { const noteId = parseInt(noteElement.getAttribute('data-note-id') || '0'); const checkboxElements = noteElement.querySelectorAll('input[type="checkbox"]'); const checkboxIndex = Array.from(checkboxElements).indexOf(checkbox); updateNoteCheckbox(noteId, checkboxIndex, checkbox.checked); } }; }); return (
    {/* Header - Signal/Discord style */}

    Notes

    Your personal notes

    {/* Main Content Area */}
    {/* Sidebar - Discord style */}

    Search & Filter

    setSearchTerm(value)} tagOptions={allTags()} selectedTag={selectedTags()[0] || ''} onTagChange={(value) => setSelectedTags(value ? [value] : [])} onReset={() => { setSearchTerm(''); setSelectedTags([]); }} />

    Quick Stats

    Total Notes {filteredNotes().length}
    Pinned {filteredNotes().filter(n => n.pinned).length}
    Tags {allTags().length}
    {/* Notes List - Signal style chat */}
    Content copied!
    {isLoading() ? (
    {[...Array(3)].map(() => (
    ))}
    ) : ( <> {filteredNotes().length > 0 ? ( filteredNotes().map((note) => (
    viewNote(note)} >

    {note.title}

    {note.pinned && } {note.isMarkdown && MD} {note.isHtml && HTML}
    {/* Note Preview */}
    } >
    $1
  • ') .replace(/^## (.*$)/gim, '

    $1

    ') .replace(/^### (.*$)/gim, '

    $1

    ') .replace(/^#### (.*$)/gim, '

    $1

    ') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/`(.*?)`/g, '$1') .replace(/```(.*?)\n([\s\S]*?)```/g, '
    $2
    ') .replace(/^- \[ \] (.*$)/gim, '
    $1
    ') .replace(/^- \[x\] (.*$)/gim, '
    $1
    ') .replace(/^- (.*$)/gim, '
  • $1
  • ') .replace(/^\d+\. (.*$)/gim, '
  • $1
  • ') .replace(/> (.*$)/gim, '
    $1
    ') .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '
    $1') .replace(/\n\n+/g, '

    ') : note.content.replace(/(https?:\/\/[^\s]+)/g, '$1') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/^- \[ \] (.*$)/gim, '

    $1
    ') .replace(/^- \[x\] (.*$)/gim, '
    $1
    ') .split('\n').map((line) => line ? `

    ${line}

    ` : '
    ').join('') } /> {/* Tags */}
    {(tag) => ( )}
    {/* Attachments */} 0}>
    Attachments ({note.attachments?.length || 0})
    {(attachment) => (
    {attachment.name} ({attachment.size})
    )}

    Updated: {note.updatedAt && !isNaN(new Date(note.updatedAt).getTime()) ? new Date(note.updatedAt).toLocaleDateString() : 'Invalid Date'}

    )) ) : (
    📝

    {searchTerm() || selectedTags().length > 0 ? 'No notes found matching your search or filters.' : 'No notes yet. Add your first note!'}

    {searchTerm() || selectedTags().length > 0 ? 'Try adjusting your search terms or filters.' : 'Click the "Add Note" button to get started.'}

    )} )} {/* Add Note Modal */} setShowAddModal(false)} onSubmit={handleAddNote} availableTags={allTags()} /> {/* Edit Note Modal */} { setShowEditModal(false); setEditingNote(null); }} onSubmit={handleEditNote} note={editingNote()} availableTags={allTags()} /> {/* View Note Modal */} { setShowViewModal(false); setViewingNote(null); }} note={viewingNote()} onEdit={startEditNote} onTogglePin={togglePin} onDelete={deleteNote} onCopyContent={copyNoteContent} onExportNote={exportNote} onUpdateNote={handleUpdateNoteContent} /> ); };