feat(ui-phase-15): project switcher — localStorage CRUD, first-run modal
Projects stored in localStorage (key: migrator_projects). CRUD: create, list, setActive, delete. Switcher modal opens automatically on first run when no projects exist. Active project name displayed in nav footer and project button. Deleting a project requires confirmation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
516af313a1
commit
85f82eaabf
|
|
@ -0,0 +1,197 @@
|
|||
// Project / customer context — localStorage CRUD + switcher modal
|
||||
|
||||
import { escHtml, uuid, formatDate } from './utils.js';
|
||||
|
||||
const STORAGE_KEY = 'migrator_projects';
|
||||
|
||||
// ── Data model ─────────────────────────────────────────────────────────────
|
||||
// localStorage schema:
|
||||
// { active: string|null, projects: Array<{ id, name, createdAt }> }
|
||||
|
||||
function load() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { active: null, projects: [] };
|
||||
} catch {
|
||||
return { active: null, projects: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function save(data) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function listProjects() {
|
||||
return load().projects;
|
||||
}
|
||||
|
||||
export function getActive() {
|
||||
const data = load();
|
||||
return data.projects.find(p => p.id === data.active) || null;
|
||||
}
|
||||
|
||||
export function createProject(name) {
|
||||
const data = load();
|
||||
const project = { id: uuid(), name: name.trim(), createdAt: new Date().toISOString() };
|
||||
data.projects.push(project);
|
||||
if (!data.active) data.active = project.id;
|
||||
save(data);
|
||||
return project;
|
||||
}
|
||||
|
||||
export function deleteProject(id) {
|
||||
const data = load();
|
||||
data.projects = data.projects.filter(p => p.id !== id);
|
||||
if (data.active === id) {
|
||||
data.active = data.projects[0]?.id || null;
|
||||
}
|
||||
save(data);
|
||||
}
|
||||
|
||||
export function setActive(id) {
|
||||
const data = load();
|
||||
if (data.projects.find(p => p.id === id)) {
|
||||
data.active = id;
|
||||
save(data);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Init: called on app startup ─────────────────────────────────────────────
|
||||
// onUpdate callback is called whenever the active project changes
|
||||
|
||||
let _onUpdate = null;
|
||||
|
||||
export function initProject(onUpdate) {
|
||||
_onUpdate = onUpdate;
|
||||
onUpdate();
|
||||
// Show project modal on first run if no projects exist
|
||||
if (listProjects().length === 0) {
|
||||
showProjectModal(onUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Project switcher modal ─────────────────────────────────────────────────
|
||||
|
||||
export function showProjectModal(onUpdate) {
|
||||
if (onUpdate) _onUpdate = onUpdate;
|
||||
|
||||
const existing = document.getElementById('project-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.id = 'project-modal';
|
||||
wrapper.innerHTML = `
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-box modal-sm">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">Switch Project</span>
|
||||
<button class="modal-close" id="pm-close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding-bottom:0">
|
||||
<div class="project-list" id="pm-project-list"></div>
|
||||
<div class="new-project-form">
|
||||
<h4>New Project</h4>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-input" id="pm-new-name"
|
||||
placeholder="Customer name (e.g. Acme Corp)" maxlength="60" />
|
||||
<div class="form-error" id="pm-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="pm-cancel">Cancel</button>
|
||||
<button class="btn btn-primary" id="pm-create">Create Project</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(wrapper);
|
||||
|
||||
renderProjectList();
|
||||
|
||||
document.getElementById('pm-close').onclick = () => wrapper.remove();
|
||||
document.getElementById('pm-cancel').onclick = () => wrapper.remove();
|
||||
document.getElementById('pm-create').onclick = handleCreate;
|
||||
document.getElementById('pm-new-name').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') handleCreate();
|
||||
});
|
||||
|
||||
// Focus name input
|
||||
setTimeout(() => document.getElementById('pm-new-name')?.focus(), 50);
|
||||
}
|
||||
|
||||
function renderProjectList() {
|
||||
const list = document.getElementById('pm-project-list');
|
||||
const active = getActive();
|
||||
const projects = listProjects();
|
||||
|
||||
if (!projects.length) {
|
||||
list.innerHTML = `<p style="font-size:13px;color:var(--text-muted);margin-bottom:8px">
|
||||
No projects yet. Create one below to get started.
|
||||
</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = projects.map(p => `
|
||||
<div class="project-row ${p.id === active?.id ? 'active' : ''}" data-id="${escHtml(p.id)}">
|
||||
<div class="project-row-icon">${escHtml(p.name.slice(0, 2).toUpperCase())}</div>
|
||||
<div style="flex:1">
|
||||
<div class="project-row-name">${escHtml(p.name)}</div>
|
||||
<div class="project-row-sub">Created ${formatDate(p.createdAt)}</div>
|
||||
</div>
|
||||
${p.id === active?.id
|
||||
? '<span class="project-row-active-badge">● Active</span>'
|
||||
: `<button class="btn btn-secondary btn-xs pm-delete-btn" data-id="${escHtml(p.id)}" title="Delete project">✕</button>`
|
||||
}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Activate on row click
|
||||
list.querySelectorAll('.project-row').forEach(row => {
|
||||
row.addEventListener('click', e => {
|
||||
if (e.target.classList.contains('pm-delete-btn')) return;
|
||||
activateProject(row.dataset.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Delete buttons
|
||||
list.querySelectorAll('.pm-delete-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
handleDelete(btn.dataset.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function activateProject(id) {
|
||||
setActive(id);
|
||||
if (_onUpdate) _onUpdate();
|
||||
const modal = document.getElementById('project-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
function handleDelete(id) {
|
||||
const project = listProjects().find(p => p.id === id);
|
||||
if (!project) return;
|
||||
if (!confirm(`Delete project "${project.name}"? This cannot be undone.`)) return;
|
||||
deleteProject(id);
|
||||
if (_onUpdate) _onUpdate();
|
||||
renderProjectList();
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
const input = document.getElementById('pm-new-name');
|
||||
const errorEl = document.getElementById('pm-error');
|
||||
const name = input?.value.trim();
|
||||
|
||||
if (!name) {
|
||||
if (errorEl) errorEl.textContent = 'Project name is required.';
|
||||
input?.focus();
|
||||
return;
|
||||
}
|
||||
if (errorEl) errorEl.textContent = '';
|
||||
|
||||
const project = createProject(name);
|
||||
setActive(project.id);
|
||||
if (_onUpdate) _onUpdate();
|
||||
const modal = document.getElementById('project-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
Loading…
Reference in New Issue