medicodeapp / frontend /src /app /features /shell /shell.component.ts
Denisijcu's picture
upload files
c98875e
import { Component, inject, signal } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
import { CommonModule } from '@angular/common';
import { AuthService } from '../../core/services/auth.service';
import { FooterComponent } from '../footer/footer.component';
@Component({
selector: 'app-shell',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive, CommonModule, FooterComponent],
template: `
<div class="shell">
<!-- Sidebar -->
<aside class="sidebar" [class.collapsed]="collapsed()">
<div class="sidebar-header">
<div class="brand">
<svg viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#0EA5E9"/>
<path d="M16 6v20M6 16h20" stroke="white" stroke-width="3" stroke-linecap="round"/>
<circle cx="16" cy="16" r="4" fill="white" fill-opacity="0.3"/>
</svg>
@if (!collapsed()) { <span class="brand-name">MediCode</span> }
</div>
<button class="collapse-btn" (click)="toggleCollapsed()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 18l6-6-6-6"/>
</svg>
</button>
</div>
<nav class="sidebar-nav">
<a routerLink="/dashboard" routerLinkActive="active" class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
@if (!collapsed()) { <span>Dashboard</span> }
</a>
<a routerLink="/codes" routerLinkActive="active" class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
@if (!collapsed()) { <span>Code Search</span> }
</a>
<a routerLink="/patients" routerLinkActive="active" class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
@if (!collapsed()) { <span>Patients</span> }
</a>
<a routerLink="/reports" routerLinkActive="active" class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>
</svg>
@if (!collapsed()) { <span>Reports</span> }
</a>
</nav>
<div class="sidebar-footer">
<div class="user-info">
<div class="avatar">{{ initials() }}</div>
@if (!collapsed()) {
<div class="user-meta">
<span class="user-name">{{ auth.currentUser()?.name }}</span>
<span class="user-role">{{ auth.currentUser()?.role }}</span>
</div>
}
</div>
<button class="logout-btn" (click)="auth.logout()" title="Logout">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/>
</svg>
</button>
</div>
</aside>
<!-- Main -->
<!-- Main -->
<main class="main-content">
<div class="page-wrapper">
<router-outlet />
</div>
<app-footer />
</main>
</div>
`,
styles: [`
.shell { display:flex; min-height:100vh; background:#f1f5f9; font-family:'Inter',system-ui,sans-serif; }
.sidebar {
width:240px; background:#0f172a; display:flex; flex-direction:column;
transition:width 0.2s ease; flex-shrink:0;
}
.sidebar.collapsed { width:64px; }
.sidebar-header {
display:flex; align-items:center; justify-content:space-between;
padding:1.25rem 1rem; border-bottom:1px solid rgba(255,255,255,0.07);
}
.brand { display:flex; align-items:center; gap:0.625rem; overflow:hidden; }
.brand svg { width:32px; height:32px; flex-shrink:0; }
.brand-name { color:white; font-weight:700; font-size:1.125rem; white-space:nowrap; }
.collapse-btn {
background:none; border:none; cursor:pointer; color:#64748b; padding:0.25rem;
border-radius:4px; display:flex; transition:color 0.15s;
}
.collapse-btn:hover { color:#94a3b8; }
.collapse-btn svg { width:16px; height:16px; }
.sidebar-nav { flex:1; padding:1rem 0.5rem; display:flex; flex-direction:column; gap:0.25rem; }
.nav-item {
display:flex; align-items:center; gap:0.75rem; padding:0.7rem 0.75rem;
border-radius:8px; color:#94a3b8; text-decoration:none; font-size:0.9rem;
font-weight:500; transition:background 0.15s, color 0.15s; white-space:nowrap; overflow:hidden;
}
.nav-item svg { width:20px; height:20px; flex-shrink:0; }
.nav-item:hover { background:rgba(255,255,255,0.07); color:#e2e8f0; }
.nav-item.active { background:rgba(14,165,233,0.15); color:#38bdf8; }
.sidebar-footer {
padding:0.75rem 0.5rem; border-top:1px solid rgba(255,255,255,0.07);
display:flex; align-items:center; justify-content:space-between; gap:0.5rem;
}
.user-info { display:flex; align-items:center; gap:0.625rem; overflow:hidden; min-width:0; }
.avatar {
width:32px; height:32px; border-radius:50%; background:#0EA5E9; color:white;
font-size:0.75rem; font-weight:700; display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
.user-meta { display:flex; flex-direction:column; overflow:hidden; }
.user-name { color:#e2e8f0; font-size:0.8rem; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.user-role { color:#64748b; font-size:0.7rem; text-transform:capitalize; }
.logout-btn {
background:none; border:none; cursor:pointer; color:#64748b; padding:0.4rem;
border-radius:6px; display:flex; flex-shrink:0; transition:color 0.15s;
}
.logout-btn:hover { color:#f87171; }
.logout-btn svg { width:18px; height:18px; }
/* ── Fix footer positioning ── */
.main-content {
flex: 1;
overflow: auto;
display: flex;
flex-direction: column; /* ← key: stack page + footer vertically */
}
.page-wrapper {
flex: 1; /* ← key: page content expands, footer stays at bottom */
}
`],
})
export class ShellComponent {
auth = inject(AuthService);
collapsed = signal(false);
toggleCollapsed() {
this.collapsed.update(v => !v);
}
initials() {
const name = this.auth.currentUser()?.name || '';
return name.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase();
}
}