Compare commits

...

23 Commits

Author SHA1 Message Date
minori ed5746bea9 deps: update jest-util to 30.3.0
Node.js CI / CI (pull_request) Successful in 1m46s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m40s
2026-03-20 07:03:33 -07:00
minori 037993804d Merge pull request 'deps: update @swc/helpers to 0.5.19' (#76) from dependencies/update--swc-helpers into main
Node.js CI / CI (push) Failing after 2m30s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 3m36s
2026-03-06 07:04:03 -08:00
hikari 7d8c6bf21c fix: load Google Fonts correctly with strict CSP (#77)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m42s
Node.js CI / CI (push) Successful in 1m47s
## Summary

- Allows `fonts.googleapis.com` in `style-src` and `fonts.gstatic.com` in `font-src` so the browser can load Google Fonts
- Adds preconnect hints and the Google Fonts import (Griffy, Kalam, Creepster, Henny Penny) to `index.html`
- Sets the body font to Kalam and heading font to Griffy, with utility classes for Creepster and Henny Penny
- Disables Angular's `inlineCritical` optimisation, which was causing the stylesheet to be deferred via `onload="this.media='all'"` — an inline event handler blocked by the strict `script-src` CSP, preventing the heading font rules from ever applying to screen media

## Test plan

- [ ] Rebuild and reload the app
- [ ] Verify headings render in Griffy
- [ ] Verify body text renders in Kalam
- [ ] Check DevTools Styles tab confirms the `h1-h6` font-family rule is matched

✨ This PR was created with help from Hikari~ 🌸

Reviewed-on: #77
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-03-05 10:32:19 -08:00
minori c769c81207 Merge pull request 'deps: update @typescript-eslint/utils to 8.56.0' (#75) from dependencies/update--typescript-eslint-utils into main
Node.js CI / CI (push) Successful in 2m30s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m44s
2026-03-05 07:05:07 -08:00
minori 6dd0ec7db0 deps: update @swc/helpers to 0.5.19
Node.js CI / CI (pull_request) Successful in 2m23s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 2m26s
2026-03-05 07:05:01 -08:00
minori b821ab7a6e Merge pull request 'deps: update marked to 17.0.3' (#74) from dependencies/update-marked into main
Node.js CI / CI (push) Has been cancelled
Security Scan and Upload / Security & DefectDojo Upload (push) Has started running
2026-03-05 07:04:34 -08:00
minori d3e91bfcb1 deps: update @typescript-eslint/utils to 8.56.0
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 2m20s
Node.js CI / CI (pull_request) Successful in 2m25s
2026-03-04 07:04:53 -08:00
minori 8f51c75f0a deps: update marked to 17.0.3
Node.js CI / CI (pull_request) Successful in 2m12s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 2m19s
2026-03-04 07:04:31 -08:00
minori 163738867b Merge pull request 'deps: update typescript-eslint to 8.56.0' (#72) from dependencies/update-typescript-eslint into main
Node.js CI / CI (push) Successful in 2m10s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 2m31s
2026-02-28 07:06:04 -08:00
minori 84fee6afcb deps: update typescript-eslint to 8.56.0
Node.js CI / CI (pull_request) Successful in 2m26s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m30s
2026-02-27 07:05:37 -08:00
minori 0cc515971a Merge pull request 'deps: update jsdom to 28.1.0' (#70) from dependencies/update-jsdom into main
Node.js CI / CI (push) Successful in 1m40s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m42s
2026-02-26 07:05:19 -08:00
minori e3fae2f8bb deps: update jsdom to 28.1.0
Node.js CI / CI (pull_request) Successful in 1m48s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m43s
2026-02-25 07:06:16 -08:00
minori 2f9e623af6 Merge pull request 'deps: update typescript-eslint to 8.55.0' (#63) from dependencies/update-typescript-eslint into main
Node.js CI / CI (push) Successful in 1m45s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m39s
2026-02-23 20:51:16 -08:00
minori 1af0e0d7de Merge pull request 'deps: update @typescript-eslint/utils to 8.55.0' (#62) from dependencies/update--typescript-eslint-utils into main
Node.js CI / CI (push) Has been cancelled
Security Scan and Upload / Security & DefectDojo Upload (push) Has been cancelled
2026-02-23 20:50:50 -08:00
minori 2dc553ee1c Merge pull request 'deps: update marked to 17.0.2' (#68) from dependencies/update-marked into main
Node.js CI / CI (push) Has been cancelled
Security Scan and Upload / Security & DefectDojo Upload (push) Has been cancelled
2026-02-23 20:50:15 -08:00
minori 21af80181f Merge pull request 'deps: update @fastify/oauth2 to 8.2.0' (#61) from dependencies/update--fastify-oauth2 into main
Node.js CI / CI (push) Has been cancelled
Security Scan and Upload / Security & DefectDojo Upload (push) Has been cancelled
2026-02-23 20:49:58 -08:00
naomi 3fecd548a4 release: v1.1.1
Node.js CI / CI (push) Successful in 1m46s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m47s
2026-02-23 20:38:25 -08:00
hikari 6d5b0581a5 fix: base64 uploads, audit log noise, and stale chunk reloads (#69)
Node.js CI / CI (push) Has been cancelled
Security Scan and Upload / Security & DefectDojo Upload (push) Has been cancelled
## Summary

- **Base64 cover image uploads broken for books, shows, manga, and music** — a premature `validateStringLength` check ran before the data URL detection, rejecting all base64 images with a 2,048-char URL limit error. Also fixed the size calculation to extract only the base64 portion after the comma (matching the correct pattern already in `game.service.ts`).
- **Audit log flooded with expected 401s on `/api/auth/me`** — these occur during normal token refresh flow and are not genuine security events. Excluded this URL from the global 401/403 audit log handler.
- **ChunkLoadError spam after deployments** — when Angular lazy-loaded chunks are missing (stale cache after a redeploy), the global error handler now detects `ChunkLoadError` and silently reloads the page instead of logging the error and sending it to the API/Discord.

## Test plan

- [ ] Upload a base64 cover image for a book, show, manga, and music item — should succeed
- [ ] Verify `/api/auth/me` 401s no longer appear in the audit log
- [ ] Deploy a new build and confirm stale-chunk users are silently reloaded

✨ This PR was created with help from Hikari~ 🌸

Reviewed-on: #69
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-02-23 20:37:52 -08:00
minori d7cd3ccd99 deps: update marked to 17.0.2
Node.js CI / CI (pull_request) Successful in 2m8s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m53s
2026-02-22 07:04:07 -08:00
hikari ff0ae73fa7 feat: themed avatars and branding updates (#67)
Node.js CI / CI (push) Successful in 1m44s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 2m16s
## 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
minori b245f1984e deps: update typescript-eslint to 8.55.0
Node.js CI / CI (pull_request) Successful in 1m52s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m56s
2026-02-20 07:15:40 -08:00
minori 6545f46ba6 deps: update @typescript-eslint/utils to 8.55.0
Node.js CI / CI (pull_request) Successful in 1m45s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m47s
2026-02-20 07:14:51 -08:00
minori 8215fda5ff deps: update @fastify/oauth2 to 8.2.0
Node.js CI / CI (pull_request) Successful in 1m15s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m7s
2026-02-20 07:12:23 -08:00
38 changed files with 798 additions and 440 deletions
+2 -2
View File
@@ -22,8 +22,8 @@ export async function app(fastify: FastifyInstance, opts: AppOptions) {
});
}
// Log unauthorized access attempts
if (error.statusCode === 401 || error.statusCode === 403) {
// Log unauthorized access attempts (exclude /api/auth/me as 401s there are expected during token refresh)
if ((error.statusCode === 401 || error.statusCode === 403) && request.url !== '/api/auth/me') {
await AuditService.log({
action: AuditAction.unauthorizedAccess,
category: AuditCategory.security,
+2 -2
View File
@@ -14,11 +14,11 @@ const helmetPlugin: FastifyPluginAsync = async (app) => {
directives: {
defaultSrc: ["'self'"],
// Angular uses inline styles for component encapsulation, so we need to allow them
styleSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"],
connectSrc: ["'self'", process.env.FRONTEND_URL ?? "http://localhost:4200"],
fontSrc: ["'self'", "data:"],
fontSrc: ["'self'", "data:", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
+5 -5
View File
@@ -36,10 +36,6 @@ export class BookService {
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
}
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
}
// Validate rating
if (!validateRating(data.rating)) {
throw new Error("Rating must be an integer between 0 and 10.");
@@ -47,7 +43,11 @@ export class BookService {
if (data.coverImage) {
if (data.coverImage.startsWith("data:")) {
const sizeInBytes = data.coverImage.length * 0.75;
const base64Data = data.coverImage.split(",")[1];
if (!base64Data) {
throw new Error("Invalid image data URL format.");
}
const sizeInBytes = base64Data.length * 0.75;
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
throw new Error("Cover image must be under 5MB.");
}
+5 -5
View File
@@ -33,10 +33,6 @@ export class MangaService {
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
}
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
}
// Validate rating
if (!validateRating(data.rating)) {
throw new Error("Rating must be an integer between 0 and 10.");
@@ -45,7 +41,11 @@ export class MangaService {
// Validate cover image URL
if (data.coverImage) {
if (data.coverImage.startsWith("data:")) {
const sizeInBytes = data.coverImage.length * 0.75;
const base64Data = data.coverImage.split(",")[1];
if (!base64Data) {
throw new Error("Invalid image data URL format.");
}
const sizeInBytes = base64Data.length * 0.75;
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
throw new Error("Cover image must be under 5MB.");
}
+5 -5
View File
@@ -33,10 +33,6 @@ export class MusicService {
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
}
if (!validateStringLength(data.coverArt, MAX_LENGTHS.URL)) {
throw new Error(`Cover art URL must be ${MAX_LENGTHS.URL} characters or less.`);
}
// Validate rating
if (data.rating !== undefined && !validateRating(data.rating)) {
throw new Error("Rating must be an integer between 0 and 10.");
@@ -45,7 +41,11 @@ export class MusicService {
// Validate cover art URL
if (data.coverArt) {
if (data.coverArt.startsWith("data:")) {
const sizeInBytes = data.coverArt.length * 0.75;
const base64Data = data.coverArt.split(",")[1];
if (!base64Data) {
throw new Error("Invalid image data URL format.");
}
const sizeInBytes = base64Data.length * 0.75;
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
throw new Error("Cover image must be under 5MB.");
}
+5 -5
View File
@@ -30,10 +30,6 @@ export class ShowService {
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
}
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
}
// Validate rating
if (!validateRating(data.rating)) {
throw new Error("Rating must be an integer between 0 and 10.");
@@ -42,7 +38,11 @@ export class ShowService {
// Validate cover image URL
if (data.coverImage) {
if (data.coverImage.startsWith("data:")) {
const sizeInBytes = data.coverImage.length * 0.75;
const base64Data = data.coverImage.split(",")[1];
if (!base64Data) {
throw new Error("Invalid image data URL format.");
}
const sizeInBytes = base64Data.length * 0.75;
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
throw new Error("Cover image must be under 5MB.");
}
+5
View File
@@ -25,6 +25,11 @@
},
"configurations": {
"production": {
"optimization": {
"styles": {
"inlineCritical": false
}
},
"budgets": [
{
"type": "initial",
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -23,6 +23,10 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
imports: [CommonModule, FormsModule, RouterModule, PaginationComponent, LikeButtonComponent],
template: `
<div class="container">
<div class="page-hero">
<img src="/assets/avatars/art-avatar.jpg" alt="Art avatar" class="page-avatar" />
</div>
<div class="header-section">
<h2>Art Gallery</h2>
<p class="subtitle">Artwork of Naomi</p>
@@ -458,6 +462,26 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
padding: 2rem;
}
.page-hero {
text-align: center;
margin-bottom: 2rem;
}
.page-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #fdcb6e;
box-shadow: 0 4px 12px rgba(253, 203, 110, 0.3);
transition: all 0.3s;
}
.page-avatar:hover {
transform: scale(1.05);
box-shadow: 0 8px 16px rgba(253, 203, 110, 0.5);
}
.header-section {
display: flex;
flex-direction: column;
@@ -23,6 +23,10 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
imports: [CommonModule, FormsModule, RouterLink, PaginationComponent, LikeButtonComponent],
template: `
<div class="container">
<div class="page-hero">
<img src="/assets/avatars/books-avatar.jpg" alt="Books avatar" class="page-avatar" />
</div>
<div class="header-section">
<h2>My Book Collection</h2>
@if (authService.isAdmin()) {
@@ -701,6 +705,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
padding: 2rem;
}
.page-hero {
text-align: center;
margin-bottom: 2rem;
}
.page-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #8b6f47;
box-shadow: 0 4px 12px rgba(139, 111, 71, 0.3);
transition: all 0.3s;
}
.page-avatar:hover {
transform: scale(1.05);
box-shadow: 0 8px 16px rgba(139, 111, 71, 0.5);
}
.header-section {
display: flex;
justify-content: space-between;
@@ -23,6 +23,10 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
imports: [CommonModule, FormsModule, RouterModule, PaginationComponent, LikeButtonComponent],
template: `
<div class="container">
<div class="page-hero">
<img src="/assets/avatars/games-avatar.jpg" alt="Gaming avatar" class="page-avatar" />
</div>
<div class="header-section">
<h2>My Game Collection</h2>
@if (authService.isAdmin()) {
@@ -684,6 +688,26 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
padding: 2rem;
}
.page-hero {
text-align: center;
margin-bottom: 2rem;
}
.page-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #ff6b6b;
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
transition: all 0.3s;
}
.page-avatar:hover {
transform: scale(1.05);
box-shadow: 0 8px 16px rgba(255, 107, 107, 0.5);
}
.header-section {
display: flex;
justify-content: space-between;
@@ -18,7 +18,7 @@ import { ApiService } from '../../services/api.service';
<header class="header">
<nav class="navbar" aria-label="Main navigation">
<div class="nav-brand">
<img src="/assets/icons/icon-72x72.png" alt="" class="brand-icon" role="presentation" />
<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>
@@ -23,6 +23,7 @@ import { Game, GameStatus, Book, BookStatus, Music, MusicType, Manga, MangaStatu
<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>
@@ -190,10 +191,28 @@ import { Game, GameStatus, Book, BookStatus, Music, MusicType, Manga, MangaStatu
.hero h1 {
font-size: 2.5rem;
margin-bottom: 0.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);
@@ -23,6 +23,10 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
imports: [CommonModule, FormsModule, RouterLink, PaginationComponent, LikeButtonComponent],
template: `
<div class="container">
<div class="page-hero">
<img src="/assets/avatars/manga-avatar.jpg" alt="Manga avatar" class="page-avatar" />
</div>
<div class="header-section">
<h2>My Manga Collection</h2>
@if (authService.isAdmin()) {
@@ -619,6 +623,26 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
padding: 2rem;
}
.page-hero {
text-align: center;
margin-bottom: 2rem;
}
.page-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #00b894;
box-shadow: 0 4px 12px rgba(0, 184, 148, 0.3);
transition: all 0.3s;
}
.page-avatar:hover {
transform: scale(1.05);
box-shadow: 0 8px 16px rgba(0, 184, 148, 0.5);
}
.header-section {
display: flex;
justify-content: space-between;
@@ -23,6 +23,10 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
imports: [CommonModule, FormsModule, RouterLink, PaginationComponent, LikeButtonComponent],
template: `
<div class="container">
<div class="page-hero">
<img src="/assets/avatars/music-avatar.jpg" alt="Music avatar" class="page-avatar" />
</div>
<div class="header-section">
<h2>My Music Collection</h2>
@if (authService.isAdmin()) {
@@ -689,6 +693,26 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
padding: 2rem;
}
.page-hero {
text-align: center;
margin-bottom: 2rem;
}
.page-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #74b9ff;
box-shadow: 0 4px 12px rgba(116, 185, 255, 0.3);
transition: all 0.3s;
}
.page-avatar:hover {
transform: scale(1.05);
box-shadow: 0 8px 16px rgba(116, 185, 255, 0.5);
}
.header-section {
display: flex;
justify-content: space-between;
@@ -23,6 +23,10 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
imports: [CommonModule, RouterLink, FormsModule, PaginationComponent, LikeButtonComponent],
template: `
<div class="container">
<div class="page-hero">
<img src="/assets/avatars/shows-avatar.jpg" alt="Shows avatar" class="page-avatar" />
</div>
<div class="header-section">
<h2>My Shows &amp; Films</h2>
@if (authService.isAdmin()) {
@@ -615,6 +619,26 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
padding: 2rem;
}
.page-hero {
text-align: center;
margin-bottom: 2rem;
}
.page-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #e84393;
box-shadow: 0 4px 12px rgba(232, 67, 147, 0.3);
transition: all 0.3s;
}
.page-avatar:hover {
transform: scale(1.05);
box-shadow: 0 8px 16px rgba(232, 67, 147, 0.5);
}
.header-section {
display: flex;
justify-content: space-between;
@@ -12,6 +12,11 @@ export class GlobalErrorHandler implements ErrorHandler {
private toast = inject(ToastService);
handleError(error: Error): void {
if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) {
window.location.reload();
return;
}
console.error('Global error caught:', error);
// Show user-friendly error message
+3
View File
@@ -8,6 +8,9 @@
<meta name="description" content="Naomi's curated collection of games, books, music, shows, manga, and art. Browse, engage, and suggest new additions!" />
<meta name="theme-color" content="#9d4edd" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Creepster&family=Griffy&family=Henny+Penny&family=Kalam:wght@300;400;700&display=swap" rel="stylesheet" />
<script defer src="https://analytics.nhcarrigan.com/js/pa-YUXAn1vhhRttySUAw_LMN.js"></script>
<script>
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
+22 -1
View File
@@ -38,7 +38,7 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-family: 'Kalam', cursive;
font-size: 14pt;
line-height: 1.6;
color: var(--foreground);
@@ -75,10 +75,30 @@ body::after {
pointer-events: none;
}
@keyframes wiggle {
0%, 100% { transform: rotate(-2deg); }
50% { transform: rotate(2deg); }
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
line-height: 1.2;
color: var(--witch-purple);
font-family: 'Griffy', cursive;
}
h1 {
animation: wiggle 4s ease-in-out infinite;
display: inline-block;
}
.witchy-accent,
.spooky-title {
font-family: 'Creepster', cursive;
}
.mystical-text {
font-family: 'Henny Penny', cursive;
}
a {
@@ -121,6 +141,7 @@ select:focus {
// Button base styles
button {
font-family: inherit;
transition: all 0.3s;
}
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@library/source",
"version": "1.1.0",
"version": "1.1.1",
"license": "MIT",
"scripts": {
"dev": "nx run-many --target=build --all && NODE_ENV=production op run --env-file=dev.env -- node dist/api/main.js",
@@ -31,7 +31,7 @@
"@fastify/csrf-protection": "7.1.0",
"@fastify/helmet": "13.0.2",
"@fastify/jwt": "10.0.0",
"@fastify/oauth2": "8.1.2",
"@fastify/oauth2": "8.2.0",
"@fastify/rate-limit": "10.3.0",
"@fastify/sensible": "6.0.4",
"@fastify/static": "9.0.0",
@@ -44,8 +44,8 @@
"dompurify": "3.3.1",
"fastify": "5.7.3",
"fastify-plugin": "5.0.1",
"jsdom": "28.0.0",
"marked": "17.0.1",
"jsdom": "28.1.0",
"marked": "17.0.3",
"rxjs": "7.8.2"
},
"devDependencies": {
@@ -70,13 +70,13 @@
"@schematics/angular": "21.1.2",
"@swc-node/register": "1.9.2",
"@swc/core": "1.5.29",
"@swc/helpers": "0.5.18",
"@swc/helpers": "0.5.19",
"@types/dompurify": "3.2.0",
"@types/jest": "30.0.0",
"@types/jsdom": "27.0.0",
"@types/jsonwebtoken": "9.0.10",
"@types/node": "20.19.9",
"@typescript-eslint/utils": "8.54.0",
"@typescript-eslint/utils": "8.56.0",
"angular-eslint": "21.1.0",
"cypress": "15.9.0",
"esbuild": "0.19.12",
@@ -84,13 +84,13 @@
"eslint-plugin-cypress": "3.6.0",
"jest": "30.2.0",
"jest-environment-node": "30.2.0",
"jest-util": "30.2.0",
"jest-util": "30.3.0",
"nx": "22.4.4",
"prisma": "6.19.2",
"ts-jest": "29.4.6",
"ts-node": "10.9.1",
"tslib": "2.8.1",
"typescript": "5.9.3",
"typescript-eslint": "8.54.0"
"typescript-eslint": "8.56.0"
}
}
+566 -405
View File
File diff suppressed because it is too large Load Diff