Spaces:
Sleeping
Sleeping
| 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'; | |
| ({ | |
| 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(); | |
| } | |
| } |