generated from nhcarrigan/template
ff0ae73fa7
## 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>
469 lines
12 KiB
TypeScript
469 lines
12 KiB
TypeScript
/**
|
|
* @copyright 2026 NHCarrigan
|
|
* @license Naomi's Public License
|
|
* @author Naomi Carrigan
|
|
*/
|
|
|
|
import { Component, OnInit, inject, signal } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { RouterModule } from '@angular/router';
|
|
import { GamesService } from '../../services/games.service';
|
|
import { BooksService } from '../../services/books.service';
|
|
import { MusicService } from '../../services/music.service';
|
|
import { MangaService } from '../../services/manga.service';
|
|
import { ShowsService } from '../../services/shows.service';
|
|
import { ArtService } from '../../services/art.service';
|
|
import { Game, GameStatus, Book, BookStatus, Music, MusicType, Manga, MangaStatus, Show, ShowStatus, ShowType, Art } from '@library/shared-types';
|
|
|
|
@Component({
|
|
selector: 'app-home',
|
|
standalone: true,
|
|
imports: [CommonModule, RouterModule],
|
|
template: `
|
|
<div class="container">
|
|
<div class="hero">
|
|
<h1>Welcome to Naomi's Library</h1>
|
|
<img src="/assets/nav-icon.jpg" alt="Naomi's avatar" class="hero-avatar" />
|
|
<p class="tagline">A personal collection of games, books, music, manga, shows, and art</p>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<a routerLink="/games" class="stat-card games-card">
|
|
<div class="icon">🎮</div>
|
|
<div class="stat-info">
|
|
<h3>Games</h3>
|
|
<div class="count">{{ gamesCount() }}</div>
|
|
<p>{{ currentlyPlayingCount() }} currently playing</p>
|
|
</div>
|
|
</a>
|
|
|
|
<a routerLink="/books" class="stat-card books-card">
|
|
<div class="icon">📚</div>
|
|
<div class="stat-info">
|
|
<h3>Books</h3>
|
|
<div class="count">{{ booksCount() }}</div>
|
|
<p>{{ currentlyReadingCount() }} currently reading</p>
|
|
</div>
|
|
</a>
|
|
|
|
<a routerLink="/music" class="stat-card music-card">
|
|
<div class="icon">🎵</div>
|
|
<div class="stat-info">
|
|
<h3>Music</h3>
|
|
<div class="count">{{ musicCount() }}</div>
|
|
<p>{{ albumsCount() }} albums, {{ singlesCount() }} singles</p>
|
|
</div>
|
|
</a>
|
|
|
|
<a routerLink="/manga" class="stat-card manga-card">
|
|
<div class="icon">📖</div>
|
|
<div class="stat-info">
|
|
<h3>Manga</h3>
|
|
<div class="count">{{ mangaCount() }}</div>
|
|
<p>{{ currentlyReadingMangaCount() }} currently reading</p>
|
|
</div>
|
|
</a>
|
|
|
|
<a routerLink="/shows" class="stat-card shows-card">
|
|
<div class="icon">📺</div>
|
|
<div class="stat-info">
|
|
<h3>Shows</h3>
|
|
<div class="count">{{ showsCount() }}</div>
|
|
<p>{{ animeCount() }} anime, {{ filmsCount() }} films</p>
|
|
</div>
|
|
</a>
|
|
|
|
<a routerLink="/art" class="stat-card art-card">
|
|
<div class="icon">🎨</div>
|
|
<div class="stat-info">
|
|
<h3>Art</h3>
|
|
<div class="count">{{ artCount() }}</div>
|
|
<p>commissioned pieces</p>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="recent-section">
|
|
<h2>Recent Additions</h2>
|
|
|
|
<div class="recent-grid">
|
|
@if (recentGames().length > 0) {
|
|
<div class="recent-category">
|
|
<h3>🎮 Latest Games</h3>
|
|
<ul class="recent-list">
|
|
@for (game of recentGames(); track game.id) {
|
|
<li>
|
|
<a [routerLink]="['/games', game.id]">{{ game.title }}</a>
|
|
@if (game.platform) {
|
|
<span class="platform">({{ game.platform }})</span>
|
|
}
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
|
|
@if (recentBooks().length > 0) {
|
|
<div class="recent-category">
|
|
<h3>📚 Latest Books</h3>
|
|
<ul class="recent-list">
|
|
@for (book of recentBooks(); track book.id) {
|
|
<li>
|
|
<a [routerLink]="['/books', book.id]">{{ book.title }}</a>
|
|
<span class="author">by {{ book.author }}</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
|
|
@if (recentMusic().length > 0) {
|
|
<div class="recent-category">
|
|
<h3>🎵 Latest Music</h3>
|
|
<ul class="recent-list">
|
|
@for (music of recentMusic(); track music.id) {
|
|
<li>
|
|
<a [routerLink]="['/music', music.id]">{{ music.title }}</a>
|
|
<span class="artist">by {{ music.artist }}</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
|
|
@if (recentManga().length > 0) {
|
|
<div class="recent-category">
|
|
<h3>📖 Latest Manga</h3>
|
|
<ul class="recent-list">
|
|
@for (manga of recentManga(); track manga.id) {
|
|
<li>
|
|
<a [routerLink]="['/manga', manga.id]">{{ manga.title }}</a>
|
|
<span class="author">by {{ manga.author }}</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
|
|
@if (recentShows().length > 0) {
|
|
<div class="recent-category">
|
|
<h3>📺 Latest Shows</h3>
|
|
<ul class="recent-list">
|
|
@for (show of recentShows(); track show.id) {
|
|
<li>
|
|
<a [routerLink]="['/shows', show.id]">{{ show.title }}</a>
|
|
<span class="show-type">{{ formatShowType(show.type) }}</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
|
|
@if (recentArt().length > 0) {
|
|
<div class="recent-category">
|
|
<h3>🎨 Latest Art</h3>
|
|
<ul class="recent-list">
|
|
@for (art of recentArt(); track art.id) {
|
|
<li>
|
|
<a [routerLink]="['/art', art.id]">{{ art.title }}</a>
|
|
<span class="artist">by {{ art.artist }}</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
styles: [`
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.hero {
|
|
text-align: center;
|
|
padding: 3rem 0;
|
|
margin-bottom: 3rem;
|
|
}
|
|
|
|
.hero h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 1rem;
|
|
color: var(--witch-purple);
|
|
}
|
|
|
|
.hero-avatar {
|
|
width: 150px;
|
|
height: 150px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 4px solid var(--witch-lavender);
|
|
box-shadow: 0 4px 12px var(--witch-shadow);
|
|
margin: 1rem auto;
|
|
display: block;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.hero-avatar:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 8px 16px rgba(157, 78, 221, 0.5);
|
|
border-color: var(--witch-rose);
|
|
}
|
|
|
|
.tagline {
|
|
font-size: 1.2rem;
|
|
color: var(--witch-plum);
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 3rem;
|
|
}
|
|
|
|
.stat-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.5rem;
|
|
padding: 1.5rem;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border: 2px solid var(--witch-mauve);
|
|
border-radius: 12px;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
transition: all 0.3s;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 8px 16px var(--witch-shadow);
|
|
background: var(--witch-moon);
|
|
}
|
|
|
|
.games-card:hover { border-color: #ff6b6b; }
|
|
.books-card:hover { border-color: #8b6f47; }
|
|
.music-card:hover { border-color: #74b9ff; }
|
|
.manga-card:hover { border-color: #00b894; }
|
|
.shows-card:hover { border-color: #e84393; }
|
|
.art-card:hover { border-color: #fdcb6e; }
|
|
|
|
.icon {
|
|
font-size: 3rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.stat-info h3 {
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 1.1rem;
|
|
color: var(--witch-plum);
|
|
}
|
|
|
|
.count {
|
|
font-size: 2.5rem;
|
|
font-weight: bold;
|
|
line-height: 1;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.games-card .count { color: #ff6b6b; }
|
|
.books-card .count { color: #8b6f47; }
|
|
.music-card .count { color: #74b9ff; }
|
|
.manga-card .count { color: #00b894; }
|
|
.shows-card .count { color: #e84393; }
|
|
.art-card .count { color: #fdcb6e; }
|
|
|
|
.stat-info p {
|
|
margin: 0;
|
|
color: var(--witch-plum);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.recent-section {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 2rem;
|
|
border-radius: 12px;
|
|
border: 2px solid var(--witch-lavender);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.recent-section h2 {
|
|
margin: 0 0 1.5rem 0;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.recent-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 2rem;
|
|
}
|
|
|
|
.recent-category h3 {
|
|
margin: 0 0 1rem 0;
|
|
font-size: 1.1rem;
|
|
color: var(--witch-purple);
|
|
}
|
|
|
|
.recent-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.recent-list li {
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid var(--witch-lavender);
|
|
}
|
|
|
|
.recent-list li:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.recent-list a {
|
|
color: var(--witch-rose);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.recent-list a:hover {
|
|
color: var(--witch-plum);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.platform,
|
|
.author,
|
|
.artist,
|
|
.show-type {
|
|
display: block;
|
|
font-size: 0.85rem;
|
|
color: var(--witch-plum);
|
|
margin-top: 0.25rem;
|
|
}
|
|
`]
|
|
})
|
|
export class HomeComponent implements OnInit {
|
|
gamesService = inject(GamesService);
|
|
booksService = inject(BooksService);
|
|
musicService = inject(MusicService);
|
|
mangaService = inject(MangaService);
|
|
showsService = inject(ShowsService);
|
|
artService = inject(ArtService);
|
|
|
|
games = signal<Game[]>([]);
|
|
books = signal<Book[]>([]);
|
|
music = signal<Music[]>([]);
|
|
manga = signal<Manga[]>([]);
|
|
shows = signal<Show[]>([]);
|
|
art = signal<Art[]>([]);
|
|
|
|
ngOnInit() {
|
|
// Load all data
|
|
this.gamesService.getAllGames().subscribe(games => this.games.set(games));
|
|
this.booksService.getAllBooks().subscribe(books => this.books.set(books));
|
|
this.musicService.getAllMusic().subscribe(music => this.music.set(music));
|
|
this.mangaService.getAllManga().subscribe(manga => this.manga.set(manga));
|
|
this.showsService.getAllShows().subscribe(shows => this.shows.set(shows));
|
|
this.artService.getAllArt().subscribe(art => this.art.set(art));
|
|
}
|
|
|
|
// Games stats
|
|
gamesCount() {
|
|
return this.games().length;
|
|
}
|
|
|
|
currentlyPlayingCount() {
|
|
return this.games().filter(g => g.status === GameStatus.playing).length;
|
|
}
|
|
|
|
recentGames() {
|
|
return this.games().slice(0, 5);
|
|
}
|
|
|
|
// Books stats
|
|
booksCount() {
|
|
return this.books().length;
|
|
}
|
|
|
|
currentlyReadingCount() {
|
|
return this.books().filter(b => b.status === BookStatus.reading).length;
|
|
}
|
|
|
|
recentBooks() {
|
|
return this.books().slice(0, 5);
|
|
}
|
|
|
|
// Music stats
|
|
musicCount() {
|
|
return this.music().length;
|
|
}
|
|
|
|
albumsCount() {
|
|
return this.music().filter(m => m.type === MusicType.album).length;
|
|
}
|
|
|
|
singlesCount() {
|
|
return this.music().filter(m => m.type === MusicType.single).length;
|
|
}
|
|
|
|
recentMusic() {
|
|
return this.music().slice(0, 5);
|
|
}
|
|
|
|
// Manga stats
|
|
mangaCount() {
|
|
return this.manga().length;
|
|
}
|
|
|
|
currentlyReadingMangaCount() {
|
|
return this.manga().filter(m => m.status === MangaStatus.reading).length;
|
|
}
|
|
|
|
recentManga() {
|
|
return this.manga().slice(0, 5);
|
|
}
|
|
|
|
// Shows stats
|
|
showsCount() {
|
|
return this.shows().length;
|
|
}
|
|
|
|
animeCount() {
|
|
return this.shows().filter(s => s.type === ShowType.anime).length;
|
|
}
|
|
|
|
filmsCount() {
|
|
return this.shows().filter(s => s.type === ShowType.film).length;
|
|
}
|
|
|
|
recentShows() {
|
|
return this.shows().slice(0, 5);
|
|
}
|
|
|
|
formatShowType(type: ShowType): string {
|
|
switch (type) {
|
|
case ShowType.tvSeries:
|
|
return 'TV Series';
|
|
case ShowType.anime:
|
|
return 'Anime';
|
|
case ShowType.film:
|
|
return 'Film';
|
|
case ShowType.documentary:
|
|
return 'Documentary';
|
|
default:
|
|
return type;
|
|
}
|
|
}
|
|
|
|
// Art stats
|
|
artCount() {
|
|
return this.art().length;
|
|
}
|
|
|
|
recentArt() {
|
|
return this.art().slice(0, 5);
|
|
}
|
|
} |