feat: Add Projects and Folders UI (SimpleNote v2)
- New Projects view (projects.js): Lists all projects with cards - New ProjectView (projectView.js): Project dashboard with folder tree - Updated API client: Projects and Folders CRUD methods - New modals: NewProjectModal, NewFolderModal, MoveToFolderModal - Edit/Delete project functionality - Updated navigation: ProjectList -> ProjectView -> FolderView - Consistent dark theme styling Changes: - public/js/views/projects.js (NEW) - public/js/views/projectView.js (NEW) - public/js/api.js (added Projects/Folders API methods) - public/js/app.js (added navigation routes) - public/js/components/sidebar.js (added Projects link) - public/css/style.css (added project/folder styles)
This commit is contained in:
@@ -1293,3 +1293,311 @@ ul, ol {
|
|||||||
padding: var(--space-6);
|
padding: var(--space-6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Projects Page === */
|
||||||
|
.projects-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-header {
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-5);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-header {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-name {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-description {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-4);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-meta .meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Breadcrumb Nav === */
|
||||||
|
.breadcrumb-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-left: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-sep {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-current {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Project Sidebar === */
|
||||||
|
.project-sidebar .sidebar-scroll {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-sidebar .sidebar-section {
|
||||||
|
padding: var(--space-3);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-sidebar .section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-sidebar .section-header h3 {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-sidebar .btn-icon-only {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-sidebar .btn-icon-only:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Folder Tree === */
|
||||||
|
.folder-tree {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item:hover {
|
||||||
|
background: var(--color-hover);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item.active {
|
||||||
|
background: var(--color-accent-alpha);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item .icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item .label {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item .count {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
background: var(--color-bg);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item .tree-action {
|
||||||
|
opacity: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item:hover .tree-action {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item .tree-action:hover {
|
||||||
|
background: var(--color-accent);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-children {
|
||||||
|
padding-left: var(--space-4);
|
||||||
|
margin-left: var(--space-3);
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Content Header Extensions === */
|
||||||
|
.content-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header-left h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-count {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-inline {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-inline input {
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
padding-left: 32px;
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-inline input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box-inline .icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Doc Card Actions === */
|
||||||
|
.doc-card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-card-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-3);
|
||||||
|
right: var(--space-3);
|
||||||
|
opacity: 0;
|
||||||
|
transition: var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-card:hover .doc-card-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-card-actions .btn-icon-only {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,6 +115,54 @@ class ApiClient {
|
|||||||
getTags() {
|
getTags() {
|
||||||
return this.get('/tags');
|
return this.get('/tags');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== PROJECTS =====
|
||||||
|
getProjects() {
|
||||||
|
return this.get('/projects');
|
||||||
|
}
|
||||||
|
|
||||||
|
getProject(id) {
|
||||||
|
return this.get(`/projects/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createProject(data) {
|
||||||
|
return this.post('/projects', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProject(id, data) {
|
||||||
|
return this.put(`/projects/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteProject(id) {
|
||||||
|
return this.delete(`/projects/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getProjectTree(id) {
|
||||||
|
return this.get(`/projects/${id}/tree`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== FOLDERS =====
|
||||||
|
getFolders(projectId, parentId = null) {
|
||||||
|
const query = parentId ? `?parentId=${parentId}` : '';
|
||||||
|
return this.get(`/projects/${projectId}/folders${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createFolder(data) {
|
||||||
|
return this.post('/folders', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFolder(id, data) {
|
||||||
|
return this.put(`/folders/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFolder(id) {
|
||||||
|
return this.delete(`/folders/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Move document to folder =====
|
||||||
|
moveDocumentToFolder(documentId, folderId) {
|
||||||
|
return this.put(`/documents/${documentId}`, { folderId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = new ApiClient();
|
export const api = new ApiClient();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
import { renderLogin, initLoginHandlers } from './views/login.js';
|
import { renderLogin, initLoginHandlers } from './views/login.js';
|
||||||
|
import { renderProjects } from './views/projects.js';
|
||||||
|
import { renderProjectView } from './views/projectView.js';
|
||||||
import { renderDashboard } from './views/dashboard.js';
|
import { renderDashboard } from './views/dashboard.js';
|
||||||
import { renderDocument } from './views/document.js';
|
import { renderDocument } from './views/document.js';
|
||||||
import { renderEditor } from './views/editor.js';
|
import { renderEditor } from './views/editor.js';
|
||||||
@@ -11,7 +13,7 @@ class App {
|
|||||||
this.currentView = null;
|
this.currentView = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
token: localStorage.getItem('sn_token'),
|
token: localStorage.getItem('sn_token'),
|
||||||
view: 'dashboard',
|
view: 'projects', // Default to projects view
|
||||||
params: {}
|
params: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -41,7 +43,7 @@ class App {
|
|||||||
try {
|
try {
|
||||||
await api.login(token);
|
await api.login(token);
|
||||||
this.state.token = token;
|
this.state.token = token;
|
||||||
this.state.view = 'dashboard';
|
this.state.view = 'projects';
|
||||||
this.render();
|
this.render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Invalid token';
|
return 'Invalid token';
|
||||||
@@ -53,6 +55,12 @@ class App {
|
|||||||
const app = document.getElementById('app');
|
const app = document.getElementById('app');
|
||||||
|
|
||||||
switch (this.state.view) {
|
switch (this.state.view) {
|
||||||
|
case 'projects':
|
||||||
|
await renderProjects(this);
|
||||||
|
break;
|
||||||
|
case 'project':
|
||||||
|
await renderProjectView(this);
|
||||||
|
break;
|
||||||
case 'dashboard':
|
case 'dashboard':
|
||||||
await renderDashboard(this);
|
await renderDashboard(this);
|
||||||
break;
|
break;
|
||||||
@@ -63,7 +71,7 @@ class App {
|
|||||||
renderEditor(this);
|
renderEditor(this);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
await renderDashboard(this);
|
await renderProjects(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,10 +147,14 @@ app.init();
|
|||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && window.app.state.view === 'editor') {
|
if (e.key === 'Escape' && window.app.state.view === 'editor') {
|
||||||
window.app.navigate('dashboard');
|
window.app.navigate('project', { id: window.app.state.params.projectId });
|
||||||
}
|
}
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n' && window.app.state.view === 'dashboard') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'n' && (window.app.state.view === 'projects' || window.app.state.view === 'project')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.showNewDocModal();
|
if (window.app.state.view === 'project') {
|
||||||
|
window.showNewDocModal(window.app.state.params.id, '');
|
||||||
|
} else {
|
||||||
|
window.showNewProjectModal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export function renderSidebar({ libraries, tags, selectedLibrary, selectedTag, o
|
|||||||
</div>
|
</div>
|
||||||
<div class="quick-links">
|
<div class="quick-links">
|
||||||
<a class="quick-link" data-action="home">📋 All Documents</a>
|
<a class="quick-link" data-action="home">📋 All Documents</a>
|
||||||
|
<a class="quick-link" href="#" onclick="window.app.navigate('projects'); return false;">📂 Projects</a>
|
||||||
<a class="quick-link" href="#" onclick="window.showNewDocModal(); return false;">📄 New Document</a>
|
<a class="quick-link" href="#" onclick="window.showNewDocModal(); return false;">📄 New Document</a>
|
||||||
<a class="quick-link" href="#" onclick="window.showNewLibraryModal(); return false;">📁 New Library</a>
|
<a class="quick-link" href="#" onclick="window.showNewLibraryModal(); return false;">📁 New Library</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
480
public/js/views/projectView.js
Normal file
480
public/js/views/projectView.js
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
// Project View - Shows a single project with folder tree
|
||||||
|
|
||||||
|
import { api } from '../api.js';
|
||||||
|
import { renderSidebar } from '../components/sidebar.js';
|
||||||
|
|
||||||
|
export async function renderProjectView(app) {
|
||||||
|
const projectId = app.state.params.id;
|
||||||
|
let project = null;
|
||||||
|
let folders = [];
|
||||||
|
let documents = [];
|
||||||
|
let tags = [];
|
||||||
|
let selectedFolderId = null;
|
||||||
|
let selectedTag = null;
|
||||||
|
let searchQuery = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [projResult, docsResult, tagsResult] = await Promise.all([
|
||||||
|
api.getProject(projectId),
|
||||||
|
api.getDocuments({ projectId }),
|
||||||
|
api.getTags()
|
||||||
|
]);
|
||||||
|
project = projResult;
|
||||||
|
documents = docsResult.documents || [];
|
||||||
|
tags = tagsResult.tags || [];
|
||||||
|
|
||||||
|
// Get folders
|
||||||
|
try {
|
||||||
|
const foldersResult = await api.getFolders(projectId);
|
||||||
|
folders = foldersResult.folders || [];
|
||||||
|
} catch (e) {
|
||||||
|
folders = [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
app.showToast('Failed to load project', 'error');
|
||||||
|
app.navigate('projects');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appEl = document.getElementById('app');
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
// Build folder tree
|
||||||
|
const folderTree = buildFolderTree(folders, null);
|
||||||
|
|
||||||
|
// Filter documents
|
||||||
|
let filteredDocs = documents;
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
filteredDocs = filteredDocs.filter(d =>
|
||||||
|
d.title.toLowerCase().includes(q) ||
|
||||||
|
(d.content && d.content.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFolderId !== null) {
|
||||||
|
filteredDocs = filteredDocs.filter(d => d.folderId === selectedFolderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTag) {
|
||||||
|
filteredDocs = filteredDocs.filter(d =>
|
||||||
|
d.tags && d.tags.includes(selectedTag)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine current folder name
|
||||||
|
let currentFolderName = 'All Documents';
|
||||||
|
if (selectedFolderId !== null) {
|
||||||
|
const folder = folders.find(f => f.id === selectedFolderId);
|
||||||
|
if (folder) currentFolderName = folder.name;
|
||||||
|
} else if (selectedTag) {
|
||||||
|
currentFolderName = `#${selectedTag}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
appEl.innerHTML = `
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="logo">📝 SimpleNote</div>
|
||||||
|
<div class="breadcrumb-nav">
|
||||||
|
<span class="breadcrumb-link" onclick="window.app.navigate('projects')">Projects</span>
|
||||||
|
<span class="breadcrumb-sep">/</span>
|
||||||
|
<span class="breadcrumb-current">${escapeHtml(project.name)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn btn-ghost" onclick="window.showEditProjectModal('${project.id}')" title="Edit Project">✏️</button>
|
||||||
|
<button class="btn btn-ghost" onclick="window.confirmDeleteProject('${project.id}')" title="Delete Project">🗑️</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="app-layout">
|
||||||
|
<aside class="sidebar project-sidebar">
|
||||||
|
<div class="sidebar-scroll">
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>📁 Folders</h3>
|
||||||
|
<button class="btn btn-ghost btn-icon-only" onclick="window.showNewFolderModal('${projectId}', null)" title="New Folder">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="folder-tree">
|
||||||
|
<div class="tree-item ${selectedFolderId === null && !selectedTag ? 'active' : ''}" data-action="folder" data-folder-id="">
|
||||||
|
<span class="icon">📋</span>
|
||||||
|
<span class="label">All Documents</span>
|
||||||
|
<span class="count">${documents.length}</span>
|
||||||
|
</div>
|
||||||
|
${folderTree}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h3>🏷️ Tags</h3>
|
||||||
|
<div class="tag-list">
|
||||||
|
${tags.map(tag => `
|
||||||
|
<div class="tag-item ${selectedTag === tag.name ? 'active' : ''}" data-action="tag" data-tag="${escapeHtml(tag.name)}">
|
||||||
|
<span>#${escapeHtml(tag.name)}</span>
|
||||||
|
<span class="tag-count">${tag.count}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="quick-links">
|
||||||
|
<a class="quick-link" href="#" onclick="window.app.navigate('projects'); return false;">📋 All Projects</a>
|
||||||
|
<a class="quick-link" href="#" onclick="window.showNewDocModal('${projectId}', '${selectedFolderId || ''}'); return false;">📄 New Document</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="content-header-left">
|
||||||
|
<h1>${escapeHtml(currentFolderName)}</h1>
|
||||||
|
<span class="doc-count">${filteredDocs.length} document${filteredDocs.length !== 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="content-header-right">
|
||||||
|
<div class="search-box-inline">
|
||||||
|
<span class="icon">🔍</span>
|
||||||
|
<input type="text" id="search-input" placeholder="Search documents..." value="${escapeHtml(searchQuery)}">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick="window.showNewDocModal('${projectId}', '${selectedFolderId || ''}')">+ New Document</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content-body">
|
||||||
|
${filteredDocs.length === 0 ? `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="icon">📄</div>
|
||||||
|
<h3>No documents found</h3>
|
||||||
|
<p>${searchQuery || selectedTag ? 'Try adjusting your filters' : 'Create your first document in this project'}</p>
|
||||||
|
${!searchQuery && !selectedTag ? `<button class="btn btn-primary" onclick="window.showNewDocModal('${projectId}', '${selectedFolderId || ''}')">+ Create Document</button>` : ''}
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div class="doc-grid">
|
||||||
|
${filteredDocs.map(doc => renderDocCard(doc, projectId)).join('')}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Attach event listeners
|
||||||
|
attachEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEventListeners() {
|
||||||
|
// Search
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
searchInput.oninput = (e) => {
|
||||||
|
searchQuery = e.target.value;
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Folder selection
|
||||||
|
document.querySelectorAll('[data-action="folder"]').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const folderId = el.getAttribute('data-folder-id');
|
||||||
|
selectedFolderId = folderId === '' ? null : folderId;
|
||||||
|
selectedTag = null;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tag selection
|
||||||
|
document.querySelectorAll('[data-action="tag"]').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedTag = el.getAttribute('data-tag');
|
||||||
|
selectedFolderId = null;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFolderTree(folders, parentId, depth = 0) {
|
||||||
|
return folders
|
||||||
|
.filter(f => f.parentId === parentId)
|
||||||
|
.map(folder => {
|
||||||
|
const children = folders.filter(f => f.parentId === folder.id);
|
||||||
|
const hasChildren = children.length > 0;
|
||||||
|
const isSelected = selectedFolderId === folder.id;
|
||||||
|
const docCount = documents.filter(d => d.folderId === folder.id).length;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="tree-node">
|
||||||
|
<div class="tree-item ${isSelected ? 'active' : ''}" data-action="folder" data-folder-id="${folder.id}">
|
||||||
|
<span class="tree-toggle ${hasChildren ? 'expanded' : ''}" style="padding-left: ${depth * 12}px">
|
||||||
|
${hasChildren ? '▶' : ''}
|
||||||
|
</span>
|
||||||
|
<span class="icon">📁</span>
|
||||||
|
<span class="label">${escapeHtml(folder.name)}</span>
|
||||||
|
<span class="count">${docCount}</span>
|
||||||
|
<button class="tree-action" onclick="event.stopPropagation(); window.showNewFolderModal('${projectId}', '${folder.id}')" title="New subfolder">+</button>
|
||||||
|
</div>
|
||||||
|
${hasChildren ? `<div class="tree-children">${buildFolderTree(folders, folder.id, depth + 1)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDocCard(doc, projectId) {
|
||||||
|
const priorityEmoji = { high: '🔴', medium: '🟡', low: '🟢' };
|
||||||
|
const priority = doc.priority || 'medium';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="doc-card" onclick="window.app.navigate('document', {id: '${doc.id}', projectId: '${projectId}'})">
|
||||||
|
<div class="doc-card-header">
|
||||||
|
<span class="doc-id">${doc.id}</span>
|
||||||
|
<span class="type-badge ${doc.type || 'general'}">${doc.type || 'general'}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="doc-title">${escapeHtml(doc.title)}</h3>
|
||||||
|
${doc.tags && doc.tags.length ? `
|
||||||
|
<div class="doc-tags">
|
||||||
|
${doc.tags.map(t => `<span class="tag-pill">${escapeHtml(t)}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="doc-meta">
|
||||||
|
<span class="doc-meta-item">📅 ${formatDate(doc.createdAt)}</span>
|
||||||
|
<span class="doc-meta-item">👤 ${escapeHtml(doc.author || 'unknown')}</span>
|
||||||
|
<span class="status-badge ${doc.status || 'draft'}">${doc.status || 'draft'}</span>
|
||||||
|
<span class="priority-indicator">${priorityEmoji[priority]}</span>
|
||||||
|
</div>
|
||||||
|
<div class="doc-card-actions">
|
||||||
|
<button class="btn btn-ghost btn-icon-only" onclick="event.stopPropagation(); window.showMoveToFolderModal('${doc.id}')" title="Move to folder">📁</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global function: Show modal to create new folder
|
||||||
|
window.showNewFolderModal = async function(projectId, parentId) {
|
||||||
|
let folders = [];
|
||||||
|
try {
|
||||||
|
const result = await api.getFolders(projectId);
|
||||||
|
folders = result.folders || [];
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'modal-backdrop';
|
||||||
|
backdrop.innerHTML = `
|
||||||
|
<div class="modal" style="min-width: 450px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span>📁</span>
|
||||||
|
<h3>Create New Folder</h3>
|
||||||
|
<button class="modal-close" onclick="this.closest('.modal-backdrop').remove()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-folder-name">Folder Name</label>
|
||||||
|
<input type="text" id="new-folder-name" class="form-control" placeholder="e.g., API Documentation">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 16px;">
|
||||||
|
<label for="new-folder-parent">Parent Folder (optional)</label>
|
||||||
|
<select id="new-folder-parent" class="form-control">
|
||||||
|
<option value="">— Root (no parent) —</option>
|
||||||
|
${folders.map(f => `<option value="${f.id}">📁 ${escapeHtml(f.name)}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-ghost" onclick="this.closest('.modal-backdrop').remove()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="create-folder-btn">Create Folder</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
|
||||||
|
const nameInput = document.getElementById('new-folder-name');
|
||||||
|
const parentSelect = document.getElementById('new-folder-parent');
|
||||||
|
const createBtn = document.getElementById('create-folder-btn');
|
||||||
|
|
||||||
|
// Pre-select parent if provided
|
||||||
|
if (parentId) {
|
||||||
|
parentSelect.value = parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBtn.onclick = async () => {
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
window.app.showToast('Please enter a folder name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.createFolder({
|
||||||
|
name,
|
||||||
|
projectId,
|
||||||
|
parentId: parentSelect.value || null
|
||||||
|
});
|
||||||
|
backdrop.remove();
|
||||||
|
window.app.showToast('Folder created', 'success');
|
||||||
|
window.app.navigate('project', { id: projectId });
|
||||||
|
} catch (e) {
|
||||||
|
window.app.showToast('Failed to create folder: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
backdrop.onclick = (e) => {
|
||||||
|
if (e.target === backdrop) backdrop.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
nameInput.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global function: Edit project modal
|
||||||
|
window.showEditProjectModal = async function(projectId) {
|
||||||
|
let project = null;
|
||||||
|
try {
|
||||||
|
project = await api.getProject(projectId);
|
||||||
|
} catch (e) {
|
||||||
|
window.app.showToast('Failed to load project', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'modal-backdrop';
|
||||||
|
backdrop.innerHTML = `
|
||||||
|
<div class="modal" style="min-width: 450px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span>✏️</span>
|
||||||
|
<h3>Edit Project</h3>
|
||||||
|
<button class="modal-close" onclick="this.closest('.modal-backdrop').remove()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-project-name">Project Name</label>
|
||||||
|
<input type="text" id="edit-project-name" class="form-control" value="${escapeHtml(project.name)}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 16px;">
|
||||||
|
<label for="edit-project-description">Description</label>
|
||||||
|
<textarea id="edit-project-description" class="form-control" rows="3" style="resize: vertical;">${escapeHtml(project.description || '')}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-ghost" onclick="this.closest('.modal-backdrop').remove()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="save-project-btn">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
|
||||||
|
const nameInput = document.getElementById('edit-project-name');
|
||||||
|
const descInput = document.getElementById('edit-project-description');
|
||||||
|
const saveBtn = document.getElementById('save-project-btn');
|
||||||
|
|
||||||
|
saveBtn.onclick = async () => {
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
window.app.showToast('Please enter a project name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.updateProject(projectId, {
|
||||||
|
name,
|
||||||
|
description: descInput.value.trim()
|
||||||
|
});
|
||||||
|
backdrop.remove();
|
||||||
|
window.app.showToast('Project updated', 'success');
|
||||||
|
window.app.navigate('project', { id: projectId });
|
||||||
|
} catch (e) {
|
||||||
|
window.app.showToast('Failed to update project: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
backdrop.onclick = (e) => {
|
||||||
|
if (e.target === backdrop) backdrop.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
nameInput.focus();
|
||||||
|
nameInput.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global function: Confirm delete project
|
||||||
|
window.confirmDeleteProject = async function(projectId) {
|
||||||
|
const confirmed = await window.app.confirmDelete('Delete this project? All documents and folders will be deleted.');
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.deleteProject(projectId);
|
||||||
|
window.app.showToast('Project deleted', 'success');
|
||||||
|
window.app.navigate('projects');
|
||||||
|
} catch (e) {
|
||||||
|
window.app.showToast('Failed to delete project: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global function: Move document to folder modal
|
||||||
|
window.showMoveToFolderModal = async function(documentId) {
|
||||||
|
const currentProjectId = window.app.state.params.id;
|
||||||
|
|
||||||
|
let folders = [];
|
||||||
|
try {
|
||||||
|
const result = await api.getFolders(currentProjectId);
|
||||||
|
folders = result.folders || [];
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Also get current document to show its current folder
|
||||||
|
let currentDoc = null;
|
||||||
|
try {
|
||||||
|
currentDoc = await api.getDocument(documentId);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'modal-backdrop';
|
||||||
|
backdrop.innerHTML = `
|
||||||
|
<div class="modal" style="min-width: 450px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span>📁</span>
|
||||||
|
<h3>Move to Folder</h3>
|
||||||
|
<button class="modal-close" onclick="this.closest('.modal-backdrop').remove()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p style="color: var(--color-text-secondary); margin-bottom: 16px;">Select a folder for this document:</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<select id="move-folder-select" class="form-control">
|
||||||
|
<option value="">— No Folder (Root) —</option>
|
||||||
|
${folders.map(f => `<option value="${f.id}">📁 ${escapeHtml(f.name)}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-ghost" onclick="this.closest('.modal-backdrop').remove()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="move-folder-btn">Move</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
|
||||||
|
const select = document.getElementById('move-folder-select');
|
||||||
|
const moveBtn = document.getElementById('move-folder-btn');
|
||||||
|
|
||||||
|
// Pre-select current folder if any
|
||||||
|
if (currentDoc && currentDoc.folderId) {
|
||||||
|
select.value = currentDoc.folderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBtn.onclick = async () => {
|
||||||
|
try {
|
||||||
|
await api.moveDocumentToFolder(documentId, select.value || null);
|
||||||
|
backdrop.remove();
|
||||||
|
window.app.showToast('Document moved', 'success');
|
||||||
|
window.app.navigate('project', { id: currentProjectId });
|
||||||
|
} catch (e) {
|
||||||
|
window.app.showToast('Failed to move document: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
backdrop.onclick = (e) => {
|
||||||
|
if (e.target === backdrop) backdrop.remove();
|
||||||
|
};
|
||||||
|
};
|
||||||
140
public/js/views/projects.js
Normal file
140
public/js/views/projects.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
// Projects List View - Shows all projects
|
||||||
|
|
||||||
|
import { api } from '../api.js';
|
||||||
|
|
||||||
|
export async function renderProjects(app) {
|
||||||
|
let projects = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.getProjects();
|
||||||
|
projects = result.projects || [];
|
||||||
|
} catch (e) {
|
||||||
|
app.showToast('Failed to load projects', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
const appEl = document.getElementById('app');
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
appEl.innerHTML = `
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="logo">📝 SimpleNote</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn btn-primary" onclick="window.showNewProjectModal()">+ New Project</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="projects-page">
|
||||||
|
<div class="projects-header">
|
||||||
|
<h1>Projects</h1>
|
||||||
|
<p class="text-muted">Organize your documents into projects and folders</p>
|
||||||
|
</div>
|
||||||
|
<div class="projects-grid">
|
||||||
|
${projects.length === 0 ? `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="icon">📋</div>
|
||||||
|
<h3>No projects yet</h3>
|
||||||
|
<p>Create your first project to get started</p>
|
||||||
|
<button class="btn btn-primary" onclick="window.showNewProjectModal()">+ Create Project</button>
|
||||||
|
</div>
|
||||||
|
` : projects.map(project => renderProjectCard(project)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProjectCard(project) {
|
||||||
|
const createdDate = formatDate(project.createdAt);
|
||||||
|
const docCount = project.documentCount || 0;
|
||||||
|
const folderCount = project.folderCount || 0;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="project-card" onclick="window.app.navigate('project', {id: '${project.id}'})">
|
||||||
|
<div class="project-card-header">
|
||||||
|
<div class="project-icon">📋</div>
|
||||||
|
<div class="project-info">
|
||||||
|
<h3 class="project-name">${escapeHtml(project.name)}</h3>
|
||||||
|
${project.description ? `<p class="project-description">${escapeHtml(project.description)}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="project-card-meta">
|
||||||
|
<span class="meta-item">📄 ${docCount} docs</span>
|
||||||
|
<span class="meta-item">📁 ${folderCount} folders</span>
|
||||||
|
<span class="meta-item">📅 ${createdDate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global function: Show modal to create new project
|
||||||
|
window.showNewProjectModal = function() {
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'modal-backdrop';
|
||||||
|
backdrop.innerHTML = `
|
||||||
|
<div class="modal" style="min-width: 450px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span>📋</span>
|
||||||
|
<h3>Create New Project</h3>
|
||||||
|
<button class="modal-close" onclick="this.closest('.modal-backdrop').remove()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-project-name">Project Name</label>
|
||||||
|
<input type="text" id="new-project-name" class="form-control" placeholder="e.g., Backend Requirements">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-top: 16px;">
|
||||||
|
<label for="new-project-description">Description (optional)</label>
|
||||||
|
<textarea id="new-project-description" class="form-control" placeholder="Brief description of the project..." rows="3" style="resize: vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-ghost" onclick="this.closest('.modal-backdrop').remove()">Cancel</button>
|
||||||
|
<button class="btn btn-primary" id="create-project-btn">Create Project</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
|
||||||
|
const nameInput = document.getElementById('new-project-name');
|
||||||
|
const descInput = document.getElementById('new-project-description');
|
||||||
|
const createBtn = document.getElementById('create-project-btn');
|
||||||
|
|
||||||
|
createBtn.onclick = async () => {
|
||||||
|
const name = nameInput.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
window.app.showToast('Please enter a project name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.createProject({
|
||||||
|
name,
|
||||||
|
description: descInput.value.trim()
|
||||||
|
});
|
||||||
|
backdrop.remove();
|
||||||
|
window.app.showToast('Project created successfully', 'success');
|
||||||
|
window.app.navigate('projects');
|
||||||
|
} catch (e) {
|
||||||
|
window.app.showToast('Failed to create project: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
backdrop.onclick = (e) => {
|
||||||
|
if (e.target === backdrop) backdrop.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
nameInput.focus();
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user