feat: replace welcome screen with responsive homepage grid view
This commit is contained in:
parent
7253a1988f
commit
a9e0214b09
144
frontend/app.js
144
frontend/app.js
|
|
@ -92,6 +92,108 @@ function noteApp() {
|
|||
// Dropdown state
|
||||
showNewDropdown: false,
|
||||
|
||||
// Homepage folder navigation
|
||||
selectedHomepageFolder: '',
|
||||
|
||||
// Computed properties for homepage (using getters for Alpine reactivity)
|
||||
get homepageNotes() {
|
||||
// Safety check - ensure notes is an array
|
||||
if (!Array.isArray(this.notes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter notes based on selected folder
|
||||
if (!this.selectedHomepageFolder) {
|
||||
// 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 [];
|
||||
}
|
||||
|
||||
// Safety check - ensure notes is an array for counting
|
||||
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() {
|
||||
// Always return at least Home breadcrumb
|
||||
// Safety check - ensure we always return an array
|
||||
try {
|
||||
const breadcrumb = [{ name: 'Home', path: '' }];
|
||||
|
||||
if (!this.selectedHomepageFolder) {
|
||||
return breadcrumb;
|
||||
}
|
||||
|
||||
const parts = (this.selectedHomepageFolder || '').split('/').filter(part => part.trim() !== '');
|
||||
|
||||
let currentPath = '';
|
||||
parts.forEach(part => {
|
||||
currentPath = currentPath ? currentPath + '/' + part : part;
|
||||
breadcrumb.push({ name: part, path: currentPath });
|
||||
});
|
||||
|
||||
return breadcrumb;
|
||||
} catch (error) {
|
||||
// Fallback to just Home if anything goes wrong
|
||||
return [{ name: 'Home', path: '' }];
|
||||
}
|
||||
},
|
||||
|
||||
// Mermaid state cache
|
||||
lastMermaidTheme: null,
|
||||
|
||||
|
|
@ -116,9 +218,15 @@ function noteApp() {
|
|||
// Parse URL and load specific note if provided
|
||||
this.loadNoteFromURL();
|
||||
|
||||
// Set initial homepage state if no note is loaded
|
||||
if (!this.currentNote) {
|
||||
window.history.replaceState({ homepageFolder: '' }, '', '/');
|
||||
}
|
||||
|
||||
// Listen for browser back/forward navigation
|
||||
window.addEventListener('popstate', (e) => {
|
||||
if (e.state && e.state.notePath) {
|
||||
// Navigating to a note
|
||||
const searchQuery = e.state.searchQuery || '';
|
||||
this.loadNote(e.state.notePath, false, searchQuery); // false = don't update history
|
||||
|
||||
|
|
@ -131,6 +239,24 @@ function noteApp() {
|
|||
this.searchResults = [];
|
||||
this.clearSearchHighlights();
|
||||
}
|
||||
} else {
|
||||
// Navigating back to homepage
|
||||
this.currentNote = '';
|
||||
this.noteContent = '';
|
||||
this.currentNoteName = '';
|
||||
|
||||
// Restore homepage folder state if it was saved
|
||||
if (e.state && e.state.homepageFolder !== undefined) {
|
||||
this.selectedHomepageFolder = e.state.homepageFolder || '';
|
||||
} else {
|
||||
// No folder state in history, go to root
|
||||
this.selectedHomepageFolder = '';
|
||||
}
|
||||
|
||||
// Clear search
|
||||
this.searchQuery = '';
|
||||
this.searchResults = [];
|
||||
this.clearSearchHighlights();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -916,7 +1042,7 @@ function noteApp() {
|
|||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
// Note not found - silently redirect to home
|
||||
window.history.replaceState({}, '', '/');
|
||||
window.history.replaceState({ homepageFolder: this.selectedHomepageFolder || '' }, '', '/');
|
||||
this.currentNote = '';
|
||||
this.noteContent = '';
|
||||
return;
|
||||
|
|
@ -947,7 +1073,11 @@ function noteApp() {
|
|||
url += `?search=${encodeURIComponent(searchQuery)}`;
|
||||
}
|
||||
window.history.pushState(
|
||||
{ notePath: notePath, searchQuery: searchQuery },
|
||||
{
|
||||
notePath: notePath,
|
||||
searchQuery: searchQuery,
|
||||
homepageFolder: this.selectedHomepageFolder || '' // Save current folder state
|
||||
},
|
||||
'',
|
||||
url
|
||||
);
|
||||
|
|
@ -2550,6 +2680,16 @@ function noteApp() {
|
|||
console.error('HTML export failed:', error);
|
||||
alert(`Failed to export HTML: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Homepage folder navigation methods
|
||||
goToHomepageFolder(folderPath) {
|
||||
// Navigate to folder (empty string = root)
|
||||
this.selectedHomepageFolder = folderPath || '';
|
||||
|
||||
// Update browser history
|
||||
const state = { homepageFolder: folderPath || '' };
|
||||
window.history.pushState(state, '', '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -997,17 +997,72 @@
|
|||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
|
||||
<template x-if="!currentNote">
|
||||
<!-- Welcome Screen -->
|
||||
<div class="flex-1 flex items-center justify-center" style="background-color: var(--bg-primary);">
|
||||
<div class="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>
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);" x-text="appName"></h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary);" x-text="appTagline"></p>
|
||||
<!-- Notes Homepage - Grid View -->
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar" style="background-color: var(--bg-primary);">
|
||||
<div class="max-w-7xl mx-auto p-6 md:p-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold mb-2" style="color: var(--text-primary);" x-text="appName"></h1>
|
||||
<p class="text-lg" style="color: var(--text-secondary);" x-text="appTagline"></p>
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<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-for="(crumb, index) in homepageBreadcrumb" :key="index">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
x-show="index > 0"
|
||||
class="text-xs"
|
||||
style="color: var(--text-tertiary);"
|
||||
>/</span>
|
||||
<span
|
||||
@click="goToHomepageFolder(crumb.path)"
|
||||
class="cursor-pointer hover:underline"
|
||||
:style="crumb.path === selectedHomepageFolder ? 'color: var(--accent-primary); font-weight: 600;' : 'color: var(--text-secondary);'"
|
||||
x-text="crumb.name"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Stats Bar - Only show when there are notes or folders -->
|
||||
<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="flex flex-wrap items-center gap-4 text-sm" style="color: var(--text-secondary);">
|
||||
<span class="font-semibold" style="color: var(--text-primary);">
|
||||
<span x-text="homepageNotes.length"></span>
|
||||
<span x-text="homepageNotes.length === 1 ? 'note' : 'notes'"></span>
|
||||
</span>
|
||||
<span x-show="homepageFolders.length > 0">
|
||||
<span x-text="homepageFolders.length"></span>
|
||||
<span x-text="homepageFolders.length === 1 ? 'folder' : 'folders'"></span>
|
||||
</span>
|
||||
<button
|
||||
@click="createNote()"
|
||||
class="px-6 py-3 text-sm font-medium text-white rounded-lg"
|
||||
class="ml-auto px-4 py-2 text-sm 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)'"
|
||||
>
|
||||
+ New Note
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<template x-if="homepageNotes.length === 0 && homepageFolders.length === 0">
|
||||
<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">
|
||||
<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>
|
||||
<h2 class="text-2xl font-bold mb-2" style="color: var(--text-primary);">No notes yet</h2>
|
||||
<p class="mb-6" style="color: var(--text-secondary);">Create your first note to get started</p>
|
||||
<button
|
||||
@click="createNote()"
|
||||
class="px-6 py-3 text-sm 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)'"
|
||||
|
|
@ -1015,6 +1070,70 @@
|
|||
Create Your First Note
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Folders and Notes Grid -->
|
||||
<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">
|
||||
<!-- Folders First -->
|
||||
<template x-for="folder in homepageFolders" :key="folder.path">
|
||||
<div
|
||||
@click="goToHomepageFolder(folder.path)"
|
||||
class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--accent-primary); min-height: 140px; display: flex; flex-direction: column; border-width: 2px;"
|
||||
onmouseover="this.style.borderColor='var(--accent-hover)'; this.style.transform='translateY(-2px)'"
|
||||
onmouseout="this.style.borderColor='var(--accent-primary)'; this.style.transform='translateY(0)'"
|
||||
>
|
||||
<!-- Folder Icon -->
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<svg class="w-6 h-6" style="color: var(--accent-primary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
</svg>
|
||||
<h3 class="font-semibold text-base" style="color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical;" x-text="folder.name"></h3>
|
||||
</div>
|
||||
|
||||
<!-- Folder Metadata -->
|
||||
<div class="flex items-center justify-between text-xs mt-auto pt-2" style="color: var(--text-tertiary);">
|
||||
<span x-text="folder.noteCount + ' ' + (folder.noteCount === 1 ? 'note' : 'notes')"></span>
|
||||
<svg class="w-4 h-4" style="color: var(--accent-primary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Notes -->
|
||||
<template x-for="note in homepageNotes.slice(0, 50)" :key="note.path">
|
||||
<div
|
||||
@click="loadNote(note.path)"
|
||||
class="p-4 rounded-lg border-2 cursor-pointer transition-all hover:shadow-lg"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border-primary); min-height: 140px; display: flex; flex-direction: column;"
|
||||
onmouseover="this.style.borderColor='var(--accent-primary)'; this.style.transform='translateY(-2px)'"
|
||||
onmouseout="this.style.borderColor='var(--border-primary)'; this.style.transform='translateY(0)'"
|
||||
>
|
||||
<!-- Note Title -->
|
||||
<h3 class="font-semibold text-base mb-2" style="color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical;" x-text="note.name"></h3>
|
||||
|
||||
<!-- Metadata -->
|
||||
<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="(note.size / 1024).toFixed(1) + ' KB'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Show More Indicator -->
|
||||
<div x-show="homepageNotes.length > 50" class="mt-6 text-center">
|
||||
<p class="text-sm" style="color: var(--text-secondary);">
|
||||
Showing 50 of <span x-text="homepageNotes.length"></span> notes.
|
||||
<span class="cursor-pointer underline" style="color: var(--accent-primary);" @click="goToHomepageFolder('')">
|
||||
View all in sidebar
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue