refactor: enhance homepage functionality with improved folder and note handling

- Added file size formatting for notes and improved breadcrumb navigation.
This commit is contained in:
sudo-Harshk 2025-11-20 20:01:36 +05:30
parent a9e0214b09
commit da5d82c258
2 changed files with 252 additions and 191 deletions

View File

@ -95,81 +95,64 @@ function noteApp() {
// Homepage folder navigation // Homepage folder navigation
selectedHomepageFolder: '', selectedHomepageFolder: '',
// Computed properties for homepage (using getters for Alpine reactivity) // Computed-like helpers for homepage (use methods for safer evaluation)
get homepageNotes() { homepageNotes() {
// Safety check - ensure notes is an array if (!this.folderTree || typeof this.folderTree !== 'object') {
if (!Array.isArray(this.notes)) {
return []; return [];
} }
// Filter notes based on selected folder const folderNode = this.getFolderNode(this.selectedHomepageFolder || '');
if (!this.selectedHomepageFolder) { if (!folderNode || !Array.isArray(folderNode.notes)) {
// Root level - show notes without folder
return this.notes.filter(note => !note.folder);
} else {
// Show notes in selected folder (exact match)
return this.notes.filter(note => note.folder === this.selectedHomepageFolder);
}
},
get homepageFolders() {
// Safety check - ensure allFolders is an array
if (!Array.isArray(this.allFolders)) {
return []; return [];
} }
// Safety check - ensure notes is an array for counting return folderNode.notes;
const notesArray = Array.isArray(this.notes) ? this.notes : [];
// Get folders for current view
if (!this.selectedHomepageFolder) {
// Root level - show top-level folders
return this.allFolders
.filter(folder => !folder.includes('/'))
.map(folder => {
// Count notes in this folder and all subfolders
const noteCount = notesArray.filter(note =>
note.folder === folder || (note.folder && note.folder.startsWith(folder + '/'))
).length;
return {
name: folder,
path: folder,
noteCount: noteCount
};
});
} else {
// Get subfolders of current folder
const prefix = this.selectedHomepageFolder + '/';
const subfolders = this.allFolders
.filter(folder => folder.startsWith(prefix) && folder !== this.selectedHomepageFolder)
.map(folder => {
// Extract subfolder name (next level after current folder)
const relativePath = folder.substring(this.selectedHomepageFolder.length + 1);
const subfolderName = relativePath.split('/')[0];
const subfolderPath = this.selectedHomepageFolder + '/' + subfolderName;
// Count notes in this subfolder and all nested subfolders
const noteCount = notesArray.filter(note =>
note.folder === subfolderPath || (note.folder && note.folder.startsWith(subfolderPath + '/'))
).length;
return {
name: subfolderName,
path: subfolderPath,
noteCount: noteCount
};
})
.filter((folder, index, self) =>
// Remove duplicates (same subfolder name)
index === self.findIndex(f => f.path === folder.path)
);
return subfolders;
}
}, },
get homepageBreadcrumb() { // Helper: Format file size nicely
formatSize(bytes) {
if (bytes === 0 || !bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
},
homepageFolders() {
if (!this.folderTree || typeof this.folderTree !== 'object') {
return [];
}
let childFolders = [];
if (!this.selectedHomepageFolder) {
childFolders = Object.entries(this.folderTree)
.filter(([key]) => key !== '__root__')
.map(([, folder]) => folder);
} else {
const parentFolder = this.getFolderNode(this.selectedHomepageFolder);
if (!parentFolder || !parentFolder.children) {
return [];
}
childFolders = Object.values(parentFolder.children);
}
if (!Array.isArray(childFolders) || childFolders.length === 0) {
return [];
}
return childFolders
.map(folder => ({
name: folder.name,
path: folder.path,
noteCount: this.calculateFolderNoteCount(folder)
}))
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
},
homepageBreadcrumb() {
// Always return at least Home breadcrumb // Always return at least Home breadcrumb
// Safety check - ensure we always return an array // Safety check - ensure we always return an array
try { try {
@ -194,6 +177,56 @@ function noteApp() {
} }
}, },
getFolderNode(folderPath = '') {
if (!this.folderTree || typeof this.folderTree !== 'object') {
return null;
}
if (!folderPath) {
if (this.folderTree['__root__']) {
return this.folderTree['__root__'];
}
return { name: '', path: '', children: {}, notes: [] };
}
const parts = folderPath.split('/').filter(Boolean);
let currentLevel = this.folderTree;
let node = null;
for (const part of parts) {
if (!currentLevel[part]) {
return null;
}
node = currentLevel[part];
currentLevel = node.children || {};
}
return node;
},
calculateFolderNoteCount(folderNode) {
if (!folderNode || typeof folderNode !== 'object') {
return 0;
}
const directNotes = Array.isArray(folderNode.notes) ? folderNode.notes.length : 0;
if (!folderNode.children || Object.keys(folderNode.children).length === 0) {
return directNotes;
}
return Object.values(folderNode.children).reduce(
(total, child) => total + this.calculateFolderNoteCount(child),
directNotes
);
},
// Check if app is empty (no notes and no folders)
get isAppEmpty() {
const notesArray = Array.isArray(this.notes) ? this.notes : [];
const foldersArray = Array.isArray(this.allFolders) ? this.allFolders : [];
return notesArray.length === 0 && foldersArray.length === 0;
},
// Mermaid state cache // Mermaid state cache
lastMermaidTheme: null, lastMermaidTheme: null,
@ -1676,6 +1709,7 @@ function noteApp() {
const note = this.notes.find(n => n.path === this.currentNote); const note = this.notes.find(n => n.path === this.currentNote);
if (note) { if (note) {
note.modified = new Date().toISOString(); note.modified = new Date().toISOString();
note.size = new Blob([this.noteContent]).size;
} }
// Hide "saved" indicator // Hide "saved" indicator

View File

@ -720,10 +720,15 @@
> >
<!-- Header --> <!-- Header -->
<div class="p-4 border-b" style="border-color: var(--border-primary);"> <div class="p-4 border-b" style="border-color: var(--border-primary);">
<div class="flex items-center gap-2 mb-3"> <button
class="flex items-center gap-2 mb-3 hover:opacity-80 transition"
@click="goToHomepageFolder(''); currentNote = ''; currentNoteName = ''; noteContent = ''; mobileSidebarOpen = false"
aria-label="Go to homepage"
style="background: none; border: none; outline: none; cursor: pointer;"
>
<img src="/static/logo.svg" alt="NoteDiscovery Logo" class="w-8 h-8"> <img src="/static/logo.svg" alt="NoteDiscovery Logo" class="w-8 h-8">
<h1 class="text-xl font-bold" style="color: var(--text-primary);" x-text="appName"></h1> <h1 class="text-xl font-bold" style="color: var(--text-primary);" x-text="appName"></h1>
</div> </button>
<!-- Theme Selector --> <!-- Theme Selector -->
<div class="mb-3"> <div class="mb-3">
@ -992,13 +997,34 @@
@mouseout="if(!isResizing) $el.style.backgroundColor='transparent'" @mouseout="if(!isResizing) $el.style.backgroundColor='transparent'"
></div> ></div>
<!-- Main Content Area --> <!-- Main Content Area -->
<div class="flex-1 flex flex-col overflow-hidden"> <div class="flex-1 flex flex-col overflow-hidden">
<template x-if="!currentNote"> <template x-if="!currentNote">
<!-- Notes Homepage - Grid View --> <!-- Notes Homepage -->
<div class="flex-1 overflow-y-auto custom-scrollbar" style="background-color: var(--bg-primary);"> <div class="flex-1 overflow-y-auto custom-scrollbar" style="background-color: var(--bg-primary);">
<!-- Welcome Hero - Show when app is empty -->
<template x-if="isAppEmpty">
<div class="flex flex-col items-center justify-center h-full py-16 text-center">
<svg class="mx-auto h-16 w-16 mb-4" style="color: var(--text-tertiary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<h1 class="text-4xl font-bold mb-4" style="color: var(--text-primary);" x-text="appName"></h1>
<p class="text-xl mb-8" style="color: var(--text-secondary);" x-text="appTagline"></p>
<button
@click="createNote()"
class="px-8 py-4 text-base font-medium text-white rounded-lg transition-colors"
style="background-color: var(--accent-primary);"
onmouseover="this.style.backgroundColor='var(--accent-hover)'"
onmouseout="this.style.backgroundColor='var(--accent-primary)'"
>
Create Your First Note
</button>
</div>
</template>
<!-- Grid View - Show when app has content -->
<template x-if="!isAppEmpty">
<div class="max-w-7xl mx-auto p-6 md:p-8"> <div class="max-w-7xl mx-auto p-6 md:p-8">
<!-- Header --> <!-- Header -->
<div class="mb-6"> <div class="mb-6">
@ -1008,8 +1034,8 @@
<!-- Breadcrumb Navigation --> <!-- Breadcrumb Navigation -->
<div class="mb-4 flex items-center gap-2 text-sm" style="color: var(--text-secondary);"> <div class="mb-4 flex items-center gap-2 text-sm" style="color: var(--text-secondary);">
<template x-if="homepageBreadcrumb && Array.isArray(homepageBreadcrumb)"> <template x-if="Array.isArray(homepageBreadcrumb())">
<template x-for="(crumb, index) in homepageBreadcrumb" :key="index"> <template x-for="(crumb, index) in homepageBreadcrumb()" :key="index">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span <span
x-show="index > 0" x-show="index > 0"
@ -1028,16 +1054,16 @@
</div> </div>
<!-- Stats Bar - Only show when there are notes or folders --> <!-- Stats Bar - Only show when there are notes or folders -->
<template x-if="homepageNotes.length > 0 || homepageFolders.length > 0"> <template x-if="homepageNotes().length > 0 || homepageFolders().length > 0">
<div class="mb-6 p-4 rounded-lg" style="background-color: var(--bg-secondary); border: 1px solid var(--border-primary);"> <div class="mb-6 p-4 rounded-lg" style="background-color: var(--bg-secondary); border: 1px solid var(--border-primary);">
<div class="flex flex-wrap items-center gap-4 text-sm" style="color: var(--text-secondary);"> <div class="flex flex-wrap items-center gap-4 text-sm" style="color: var(--text-secondary);">
<span class="font-semibold" style="color: var(--text-primary);"> <span class="font-semibold" style="color: var(--text-primary);">
<span x-text="homepageNotes.length"></span> <span x-text="homepageNotes().length"></span>
<span x-text="homepageNotes.length === 1 ? 'note' : 'notes'"></span> <span x-text="homepageNotes().length === 1 ? 'note' : 'notes'"></span>
</span> </span>
<span x-show="homepageFolders.length > 0"> <span x-show="homepageFolders().length > 0">
<span x-text="homepageFolders.length"></span> <span x-text="homepageFolders().length"></span>
<span x-text="homepageFolders.length === 1 ? 'folder' : 'folders'"></span> <span x-text="homepageFolders().length === 1 ? 'folder' : 'folders'"></span>
</span> </span>
<button <button
@click="createNote()" @click="createNote()"
@ -1052,8 +1078,8 @@
</div> </div>
</template> </template>
<!-- Empty State --> <!-- Empty State (for folder view) -->
<template x-if="homepageNotes.length === 0 && homepageFolders.length === 0"> <template x-if="homepageNotes().length === 0 && homepageFolders().length === 0">
<div class="flex flex-col items-center justify-center py-16 text-center"> <div class="flex flex-col items-center justify-center py-16 text-center">
<svg class="mx-auto h-24 w-24 mb-4" style="color: var(--text-tertiary); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto h-24 w-24 mb-4" style="color: var(--text-tertiary); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
@ -1073,10 +1099,10 @@
</template> </template>
<!-- Folders and Notes Grid --> <!-- Folders and Notes Grid -->
<template x-if="homepageNotes.length > 0 || homepageFolders.length > 0"> <template x-if="homepageNotes().length > 0 || homepageFolders().length > 0">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
<!-- Folders First --> <!-- Folders First -->
<template x-for="folder in homepageFolders" :key="folder.path"> <template x-for="folder in homepageFolders()" :key="folder.path">
<div <div
@click="goToHomepageFolder(folder.path)" @click="goToHomepageFolder(folder.path)"
class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg" class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
@ -1103,7 +1129,7 @@
</template> </template>
<!-- Notes --> <!-- Notes -->
<template x-for="note in homepageNotes.slice(0, 50)" :key="note.path"> <template x-for="note in homepageNotes().slice(0, 50)" :key="note.path">
<div <div
@click="loadNote(note.path)" @click="loadNote(note.path)"
class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg" class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
@ -1117,7 +1143,7 @@
<!-- Metadata --> <!-- Metadata -->
<div class="flex items-center justify-between text-xs mt-auto pt-2" style="color: var(--text-tertiary);"> <div class="flex items-center justify-between text-xs mt-auto pt-2" style="color: var(--text-tertiary);">
<span x-text="new Date(note.modified).toLocaleDateString()"></span> <span x-text="new Date(note.modified).toLocaleDateString()"></span>
<span x-text="(note.size / 1024).toFixed(1) + ' KB'"></span> <span x-text="formatSize(note.size)"></span>
</div> </div>
</div> </div>
</template> </template>
@ -1125,15 +1151,16 @@
</template> </template>
<!-- Show More Indicator --> <!-- Show More Indicator -->
<div x-show="homepageNotes.length > 50" class="mt-6 text-center"> <div x-show="homepageNotes().length > 50" class="mt-6 text-center">
<p class="text-sm" style="color: var(--text-secondary);"> <p class="text-sm" style="color: var(--text-secondary);">
Showing 50 of <span x-text="homepageNotes.length"></span> notes. Showing 50 of <span x-text="homepageNotes().length"></span> notes.
<span class="cursor-pointer underline" style="color: var(--accent-primary);" @click="goToHomepageFolder('')"> <span class="cursor-pointer underline" style="color: var(--accent-primary);" @click="goToHomepageFolder('')">
View all in sidebar View all in sidebar
</span> </span>
</p> </p>
</div> </div>
</div> </div>
</template>
</div> </div>
</template> </template>