Add user database migration, core reusable components, and layout structure
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { getUser, clearToken } from '../../lib/auth.mjs';
|
||||
|
||||
const BREADCRUMBS = {
|
||||
'/': ['Dashboard'],
|
||||
'/work-orders': ['Work Orders'],
|
||||
'/work-orders/new': ['Work Orders', 'New'],
|
||||
'/registry/people': ['Resources', 'People'],
|
||||
'/registry/vehicles': ['Resources', 'Vehicles'],
|
||||
'/registry/equipment': ['Resources', 'Equipment'],
|
||||
'/registry/materials': ['Resources', 'Materials'],
|
||||
'/reports': ['Reports'],
|
||||
'/users': ['Admin', 'Users'],
|
||||
'/settings': ['Admin', 'Settings'],
|
||||
};
|
||||
|
||||
class AppTopbar extends HTMLElement {
|
||||
#menuOpen = false;
|
||||
|
||||
connectedCallback() {
|
||||
this.#render();
|
||||
window.addEventListener('hashchange', () => this.#render());
|
||||
document.addEventListener('click', e => {
|
||||
if (!this.contains(e.target) && this.#menuOpen) {
|
||||
this.#menuOpen = false;
|
||||
this.#render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#render() {
|
||||
const user = getUser();
|
||||
const path = decodeURIComponent(location.hash.slice(1)) || '/';
|
||||
const crumb = BREADCRUMBS[path] || [path.split('/').filter(Boolean).map(s => s.replace(/-/g, ' ')).join(' › ')];
|
||||
const initials = (user?.displayName || user?.username || 'U')
|
||||
.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
||||
|
||||
this.innerHTML = `
|
||||
<style>
|
||||
app-topbar { position: relative; }
|
||||
.breadcrumb { flex: 1; display: flex; align-items: center; gap: .4rem; font-size: .875rem; }
|
||||
.crumb { color: var(--text-muted); }
|
||||
.crumb:last-child { color: var(--text); font-weight: 600; }
|
||||
.sep { color: var(--border); }
|
||||
.right { display: flex; align-items: center; gap: .5rem; margin-left: auto; }
|
||||
.avatar-btn { background: none; border: none; cursor: pointer; display: flex; align-items: center; gap: .5rem; padding: .3rem .5rem; border-radius: var(--radius); transition: background .15s; }
|
||||
.avatar-btn:hover { background: var(--surface-2); }
|
||||
.avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--teal); color: #fff; display: flex; align-items: center; justify-content: center; font-size: .75rem; font-weight: 700; flex-shrink: 0; }
|
||||
.user-name { font-size: .813rem; font-weight: 500; color: var(--text); }
|
||||
.dropdown { position: absolute; top: calc(100% + .25rem); right: 1.25rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-md); min-width: 180px; z-index: 100; overflow: hidden; }
|
||||
.dd-item { display: flex; align-items: center; gap: .6rem; padding: .65rem 1rem; font-size: .875rem; color: var(--text); cursor: pointer; border: none; background: none; width: 100%; text-align: left; transition: background .15s; }
|
||||
.dd-item:hover { background: var(--surface-2); }
|
||||
.dd-item.danger { color: var(--danger); }
|
||||
.dd-divider { height: 1px; background: var(--border); margin: .25rem 0; }
|
||||
.mobile-menu-btn { display: none; background: none; border: none; cursor: pointer; color: var(--text); padding: .4rem; }
|
||||
@media (max-width: 768px) { .mobile-menu-btn { display: flex; } .breadcrumb { font-size: .813rem; } }
|
||||
</style>
|
||||
|
||||
<button class="mobile-menu-btn" id="mobile-menu">
|
||||
<i data-lucide="menu" style="width:22px;height:22px"></i>
|
||||
</button>
|
||||
|
||||
<nav class="breadcrumb">
|
||||
${crumb.map((c, i) => `
|
||||
<span class="crumb">${c}</span>
|
||||
${i < crumb.length - 1 ? '<span class="sep">/</span>' : ''}
|
||||
`).join('')}
|
||||
</nav>
|
||||
|
||||
<div class="right">
|
||||
<button class="avatar-btn" id="user-menu-btn">
|
||||
<div class="avatar">${initials}</div>
|
||||
<span class="user-name">${user?.displayName || user?.username || ''}</span>
|
||||
<i data-lucide="chevron-down" style="width:14px;height:14px;color:var(--text-muted)"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${this.#menuOpen ? `
|
||||
<div class="dropdown" id="dropdown">
|
||||
<button class="dd-item">
|
||||
<i data-lucide="user" style="width:15px;height:15px"></i> My Profile
|
||||
</button>
|
||||
<div class="dd-divider"></div>
|
||||
<button class="dd-item danger" id="dd-logout">
|
||||
<i data-lucide="log-out" style="width:15px;height:15px"></i> Sign Out
|
||||
</button>
|
||||
</div>` : ''}`;
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [this] });
|
||||
|
||||
this.querySelector('#user-menu-btn')?.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
this.#menuOpen = !this.#menuOpen;
|
||||
this.#render();
|
||||
});
|
||||
this.querySelector('#dd-logout')?.addEventListener('click', () => {
|
||||
clearToken();
|
||||
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||
});
|
||||
this.querySelector('#mobile-menu')?.addEventListener('click', () => {
|
||||
window.dispatchEvent(new CustomEvent('sidebar:toggle'));
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('app-topbar', AppTopbar);
|
||||
Reference in New Issue
Block a user