Files
library/apps/frontend/src/app/components/header/header.component.ts
T
hikari ff0ae73fa7
Node.js CI / CI (push) Successful in 1m44s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 2m16s
feat: themed avatars and branding updates (#67)
## Summary

Added comprehensive avatar and branding updates across the library application:

### 🌸 Updated Main Branding
- New library-themed avatar (with playful "shh" gesture) for navigation icon
- Updated favicon and all PWA icons (10 sizes from 72x72 to 512x512)
- Added hero avatar to home page between title and subtitle
- All branding uses consistent circular styling with elegant hover effects

### 🎨 Media-Specific Avatars
Added unique themed avatars to each media list page:

- **🎮 Games**: Gaming setup with controller and LED lights (red #ff6b6b border)
- **📚 Books**: Reading in cozy library setting (brown #8b6f47 border)
- **🎵 Music**: Joyful with headphones and urban nightscape (blue #74b9ff border)
- **📺 Shows**: Relaxing with remote and theater curtains (pink #e84393 border)
- **📖 Manga**: Reading manga with shelves background (teal #00b894 border)
- **🎨 Art**: Art studio with paintbrush (yellow #fdcb6e border)

###  Features
- 120x120px circular avatars with themed colour borders
- Smooth hover animations (scale + shadow effects)
- Centered hero sections at top of each list view
- Consistent styling across all media types
- Perfect integration with existing colour themes

### 📊 Technical Details
- All icons generated from source images at multiple resolutions
- Static assets served with correct MIME types
- Optimised image formats for performance
- Responsive design with proper accessibility attributes

## Test Plan

- [x] Verify navigation icon displays correctly in header
- [x] Check favicon appears in browser tabs
- [x] Test PWA icons on mobile devices
- [x] Confirm home page hero avatar renders properly
- [x] Verify all 6 media list avatars display with correct borders
- [x] Test hover animations on all avatars
- [x] Verify build succeeds
- [x] Check static assets serve with correct MIME types

🌸 Created with love by Hikari 💖

Reviewed-on: #67
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-02-20 21:11:05 -08:00

363 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Component, inject, signal, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-header',
standalone: true,
imports: [CommonModule, RouterModule],
template: `
<header class="header">
<nav class="navbar" aria-label="Main navigation">
<div class="nav-brand">
<img src="/assets/nav-icon.jpg" alt="" class="brand-icon" role="presentation" />
<h1><a routerLink="/">Naomi's Library</a></h1>
@if (version()) {
<span class="version" aria-label="Version {{ version() }}">v{{ version() }}</span>
}
</div>
<ul class="nav-links" role="list">
<li><a routerLink="/games" routerLinkActive="active" [attr.aria-current]="isCurrentRoute('/games') ? 'page' : null">Games</a></li>
<li><a routerLink="/books" routerLinkActive="active" [attr.aria-current]="isCurrentRoute('/books') ? 'page' : null">Books</a></li>
<li><a routerLink="/music" routerLinkActive="active" [attr.aria-current]="isCurrentRoute('/music') ? 'page' : null">Music</a></li>
<li><a routerLink="/shows" routerLinkActive="active" [attr.aria-current]="isCurrentRoute('/shows') ? 'page' : null">Shows</a></li>
<li><a routerLink="/manga" routerLinkActive="active" [attr.aria-current]="isCurrentRoute('/manga') ? 'page' : null">Manga</a></li>
<li><a routerLink="/art" routerLinkActive="active" [attr.aria-current]="isCurrentRoute('/art') ? 'page' : null">Art</a></li>
</ul>
<div class="auth-section">
@if (authService.user(); as user) {
<div class="user-menu">
@if (user.avatar) {
<button
class="user-avatar-button"
[attr.aria-label]="'User menu for ' + user.username"
[attr.aria-expanded]="showDropdown()"
aria-haspopup="true"
(click)="toggleDropdown()"
(keydown.escape)="closeDropdown()"
>
<img
[src]="user.avatar"
[alt]="'Avatar for ' + user.username"
class="user-avatar"
/>
</button>
}
@if (showDropdown()) {
<div
class="dropdown-menu"
role="menu"
aria-label="User menu"
tabindex="-1"
(keydown.escape)="closeDropdown()"
>
<a [routerLink]="['/profile', user.slug || user.id]" class="dropdown-item" role="menuitem" (click)="closeDropdown()">My Profile</a>
<a routerLink="/settings" class="dropdown-item" role="menuitem" (click)="closeDropdown()">Settings</a>
<a routerLink="/achievements" class="dropdown-item" role="menuitem" (click)="closeDropdown()"><span aria-hidden="true">🏆</span> Achievements</a>
<a routerLink="/leaderboard" class="dropdown-item" role="menuitem" (click)="closeDropdown()"><span aria-hidden="true">🏆</span> Leaderboard</a>
<a routerLink="/activity" class="dropdown-item" role="menuitem" (click)="closeDropdown()"><span aria-hidden="true">📰</span> Activity Feed</a>
<a routerLink="/about" class="dropdown-item" role="menuitem" (click)="closeDropdown()"><span aria-hidden="true">️</span> About</a>
@if (!user.isAdmin) {
<a routerLink="/my-suggestions" class="dropdown-item" role="menuitem" (click)="closeDropdown()">My Suggestions</a>
}
<a routerLink="/my-likes" class="dropdown-item" role="menuitem" (click)="closeDropdown()">My Likes</a>
@if (user.isAdmin) {
<a routerLink="/admin/users" class="dropdown-item" role="menuitem" (click)="closeDropdown()">Users</a>
<a routerLink="/admin/audit" class="dropdown-item" role="menuitem" (click)="closeDropdown()">Audit</a>
<a routerLink="/admin/suggestions" class="dropdown-item" role="menuitem" (click)="closeDropdown()">Suggestions</a>
<a routerLink="/admin/reports" class="dropdown-item" role="menuitem" (click)="closeDropdown()">Reports</a>
}
<button (click)="logout()" class="dropdown-item logout-btn" role="menuitem">Logout</button>
</div>
}
</div>
} @else {
<button (click)="login()" class="btn btn-primary">Login with Discord</button>
}
</div>
</nav>
</header>
`,
styles: [`
.header {
background-color: var(--witch-purple);
color: var(--witch-moon);
padding: 0;
box-shadow: 0 2px 8px var(--witch-shadow);
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.nav-brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.brand-icon {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--witch-purple);
box-shadow: 0 2px 8px rgba(157, 78, 221, 0.3);
transition: transform 0.2s, box-shadow 0.2s;
}
.brand-icon:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(157, 78, 221, 0.5);
}
.nav-brand h1 {
margin: 0;
font-size: 1.5rem;
}
.nav-brand a {
color: var(--witch-moon);
text-decoration: none;
font-weight: 600;
}
.version {
font-size: 0.7rem;
color: var(--witch-lavender);
opacity: 0.8;
margin-left: 0.5rem;
}
.nav-links {
display: flex;
list-style: none;
gap: 1rem;
margin: 0;
padding: 0;
flex-wrap: wrap;
}
.nav-links a {
color: var(--witch-lavender);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: all 0.3s;
}
.nav-links a:hover,
.nav-links a.active {
background-color: var(--witch-plum);
color: var(--witch-moon);
}
.auth-section {
display: flex;
align-items: center;
gap: 1rem;
}
.welcome {
font-size: 0.9rem;
color: var(--witch-lavender);
}
.user-menu {
position: relative;
}
.user-avatar-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid var(--witch-lavender);
transition: all 0.3s;
}
.user-avatar-button:hover .user-avatar,
.user-avatar-button:focus .user-avatar {
border-color: var(--witch-moon);
transform: scale(1.1);
}
.user-avatar-button:focus-visible {
outline: 3px solid var(--witch-rose);
outline-offset: 2px;
border-radius: 50%;
}
.dropdown-menu {
position: absolute;
top: 50px;
right: 0;
background-color: var(--witch-purple);
border: 2px solid var(--witch-lavender);
border-radius: 8px;
padding: 0.5rem 0;
min-width: 180px;
box-shadow: 0 4px 12px var(--witch-shadow);
z-index: 1000;
animation: fadeIn 0.2s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdown-item {
display: block;
width: 100%;
padding: 0.75rem 1rem;
color: var(--witch-lavender);
text-decoration: none;
background: none;
border: none;
text-align: left;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.dropdown-item:hover {
background-color: var(--witch-plum);
color: var(--witch-moon);
}
.logout-btn {
border-top: 1px solid var(--witch-lavender);
margin-top: 0.5rem;
padding-top: 0.75rem;
font-weight: 500;
}
.admin-badge {
background-color: var(--witch-rose);
color: var(--witch-moon);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
text-decoration: none;
cursor: pointer;
transition: all 0.3s;
}
.admin-badge:hover {
background-color: var(--witch-plum);
transform: translateY(-2px);
}
.user-link {
color: var(--witch-lavender);
text-decoration: none;
font-size: 0.9rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: all 0.3s;
}
.user-link:hover {
background-color: var(--witch-plum);
color: var(--witch-moon);
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--witch-shadow);
}
.btn-primary {
background-color: var(--witch-rose);
color: var(--witch-moon);
}
.btn-primary:hover {
background-color: var(--witch-plum);
}
.btn-secondary {
background-color: var(--witch-mauve);
color: var(--witch-purple);
}
.btn-secondary:hover {
background-color: var(--witch-rose);
color: var(--witch-moon);
}
`]
})
export class HeaderComponent implements OnInit {
authService = inject(AuthService);
private apiService = inject(ApiService);
private router = inject(Router);
version = signal<string | null>(null);
showDropdown = signal<boolean>(false);
ngOnInit() {
this.apiService.get<{ version: string }>('/version').subscribe({
next: (response) => this.version.set(response.version),
error: () => this.version.set(null)
});
}
isCurrentRoute(route: string): boolean {
return this.router.url.startsWith(route);
}
toggleDropdown() {
this.showDropdown.update(v => !v);
}
closeDropdown() {
this.showDropdown.set(false);
}
login() {
this.authService.login();
}
logout() {
this.closeDropdown();
this.authService.logout().subscribe();
}
}