feat: initial prototype works

I can log in and create a book! Woo!
This commit is contained in:
2026-02-04 12:17:05 -08:00
parent e167a17bd9
commit b6d66d34cb
44 changed files with 3695 additions and 493 deletions
@@ -0,0 +1,308 @@
/**
* @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 { Game, GameStatus, Book, BookStatus, Music, MusicType } 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>
<p class="tagline">A personal collection of games, books, and music</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>
</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.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.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.title }}</a>
<span class="artist">by {{ music.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: 0.5rem;
color: var(--witch-purple);
}
.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: var(--witch-rose); }
.books-card:hover { border-color: var(--witch-plum); }
.music-card:hover { border-color: var(--witch-purple); }
.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: var(--witch-rose); }
.books-card .count { color: var(--witch-plum); }
.music-card .count { color: var(--witch-purple); }
.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 {
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);
games = signal<Game[]>([]);
books = signal<Book[]>([]);
music = signal<Music[]>([]);
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));
}
// 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);
}
}