From b9f33bc055095bcc197eda17951cfa2b3be5fb20 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Wed, 4 Feb 2026 19:49:27 -0800 Subject: [PATCH] feat: add tags and links --- api/prisma/schema.prisma | 17 + api/src/app/services/art.service.ts | 8 + api/src/app/services/book.service.ts | 8 + api/src/app/services/game.service.ts | 8 + api/src/app/services/manga.service.ts | 8 + api/src/app/services/music.service.ts | 8 + api/src/app/services/show.service.ts | 8 + .../components/art/art-gallery.component.ts | 300 +++++++++++++++++- .../components/books/books-list.component.ts | 300 +++++++++++++++++- .../components/games/games-list.component.ts | 296 ++++++++++++++++- .../components/manga/manga-list.component.ts | 300 +++++++++++++++++- .../components/music/music-list.component.ts | 300 +++++++++++++++++- .../components/shows/shows-list.component.ts | 300 +++++++++++++++++- shared-types/src/index.ts | 3 +- shared-types/src/lib/art.types.ts | 6 + shared-types/src/lib/book.types.ts | 6 + shared-types/src/lib/common.types.ts | 4 + shared-types/src/lib/game.types.ts | 6 + shared-types/src/lib/manga.types.ts | 6 + shared-types/src/lib/music.types.ts | 6 + shared-types/src/lib/show.types.ts | 6 + 21 files changed, 1873 insertions(+), 31 deletions(-) create mode 100644 shared-types/src/lib/common.types.ts diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index b28fc59..b6154ba 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -13,6 +13,11 @@ datasource db { url = env("DATABASE_URL") } +type Link { + title String + url String +} + model Game { id String @id @default(auto()) @map("_id") @db.ObjectId title String @@ -23,6 +28,8 @@ model Game { rating Int? @db.Int @default(0) notes String? coverImage String? + tags String[] + links Link[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments Comment[] @@ -45,6 +52,8 @@ model Book { rating Int? @db.Int @default(0) notes String? coverImage String? + tags String[] + links Link[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments Comment[] @@ -67,6 +76,8 @@ model Music { rating Int? @db.Int @default(0) notes String? coverArt String? + tags String[] + links Link[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments Comment[] @@ -90,6 +101,8 @@ model Art { artist String description String? imageUrl String + tags String[] + links Link[] dateAdded DateTime @default(now()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -106,6 +119,8 @@ model Show { rating Int? @db.Int @default(0) notes String? coverImage String? + tags String[] + links Link[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments Comment[] @@ -134,6 +149,8 @@ model Manga { rating Int? @db.Int @default(0) notes String? coverImage String? + tags String[] + links Link[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments Comment[] diff --git a/api/src/app/services/art.service.ts b/api/src/app/services/art.service.ts index 6f3cb0f..5b8649c 100644 --- a/api/src/app/services/art.service.ts +++ b/api/src/app/services/art.service.ts @@ -23,6 +23,8 @@ export class ArtService { return artPieces.map((art) => ({ ...art, description: art.description || undefined, + tags: art.tags ?? [], + links: art.links ?? [], dateAdded: art.dateAdded, createdAt: art.createdAt, updatedAt: art.updatedAt, @@ -42,6 +44,8 @@ export class ArtService { return { ...art, description: art.description || undefined, + tags: art.tags ?? [], + links: art.links ?? [], dateAdded: art.dateAdded, createdAt: art.createdAt, updatedAt: art.updatedAt, @@ -59,6 +63,8 @@ export class ArtService { return { ...art, description: art.description || undefined, + tags: art.tags ?? [], + links: art.links ?? [], dateAdded: art.dateAdded, createdAt: art.createdAt, updatedAt: art.updatedAt, @@ -77,6 +83,8 @@ export class ArtService { return { ...art, description: art.description || undefined, + tags: art.tags ?? [], + links: art.links ?? [], dateAdded: art.dateAdded, createdAt: art.createdAt, updatedAt: art.updatedAt, diff --git a/api/src/app/services/book.service.ts b/api/src/app/services/book.service.ts index fda295b..de13a29 100644 --- a/api/src/app/services/book.service.ts +++ b/api/src/app/services/book.service.ts @@ -25,6 +25,8 @@ export class BookService { status: book.status as unknown as BookStatus, dateAdded: book.dateAdded, dateFinished: book.dateFinished || undefined, + tags: book.tags ?? [], + links: book.links ?? [], createdAt: book.createdAt, updatedAt: book.updatedAt, })); @@ -45,6 +47,8 @@ export class BookService { status: book.status as unknown as BookStatus, dateAdded: book.dateAdded, dateFinished: book.dateFinished || undefined, + tags: book.tags ?? [], + links: book.links ?? [], createdAt: book.createdAt, updatedAt: book.updatedAt, }; @@ -66,6 +70,8 @@ export class BookService { status: book.status as unknown as BookStatus, dateAdded: book.dateAdded, dateFinished: book.dateFinished || undefined, + tags: book.tags ?? [], + links: book.links ?? [], createdAt: book.createdAt, updatedAt: book.updatedAt, }; @@ -90,6 +96,8 @@ export class BookService { status: book.status as unknown as BookStatus, dateAdded: book.dateAdded, dateFinished: book.dateFinished || undefined, + tags: book.tags ?? [], + links: book.links ?? [], createdAt: book.createdAt, updatedAt: book.updatedAt, }; diff --git a/api/src/app/services/game.service.ts b/api/src/app/services/game.service.ts index e500b1e..d763807 100644 --- a/api/src/app/services/game.service.ts +++ b/api/src/app/services/game.service.ts @@ -25,6 +25,8 @@ export class GameService { status: game.status as unknown as GameStatus, dateAdded: game.dateAdded, dateCompleted: game.dateCompleted || undefined, + tags: game.tags ?? [], + links: game.links ?? [], createdAt: game.createdAt, updatedAt: game.updatedAt, })); @@ -45,6 +47,8 @@ export class GameService { status: game.status as unknown as GameStatus, dateAdded: game.dateAdded, dateCompleted: game.dateCompleted || undefined, + tags: game.tags ?? [], + links: game.links ?? [], createdAt: game.createdAt, updatedAt: game.updatedAt, }; @@ -66,6 +70,8 @@ export class GameService { status: game.status as unknown as GameStatus, dateAdded: game.dateAdded, dateCompleted: game.dateCompleted || undefined, + tags: game.tags ?? [], + links: game.links ?? [], createdAt: game.createdAt, updatedAt: game.updatedAt, }; @@ -90,6 +96,8 @@ export class GameService { status: game.status as unknown as GameStatus, dateAdded: game.dateAdded, dateCompleted: game.dateCompleted || undefined, + tags: game.tags ?? [], + links: game.links ?? [], createdAt: game.createdAt, updatedAt: game.updatedAt, }; diff --git a/api/src/app/services/manga.service.ts b/api/src/app/services/manga.service.ts index 95a1d2e..bc7dbfd 100644 --- a/api/src/app/services/manga.service.ts +++ b/api/src/app/services/manga.service.ts @@ -22,6 +22,8 @@ export class MangaService { status: m.status as unknown as MangaStatus, dateAdded: m.dateAdded, dateCompleted: m.dateCompleted || undefined, + tags: m.tags ?? [], + links: m.links ?? [], createdAt: m.createdAt, updatedAt: m.updatedAt, })); @@ -39,6 +41,8 @@ export class MangaService { status: manga.status as unknown as MangaStatus, dateAdded: manga.dateAdded, dateCompleted: manga.dateCompleted || undefined, + tags: manga.tags ?? [], + links: manga.links ?? [], createdAt: manga.createdAt, updatedAt: manga.updatedAt, }; @@ -57,6 +61,8 @@ export class MangaService { status: manga.status as unknown as MangaStatus, dateAdded: manga.dateAdded, dateCompleted: manga.dateCompleted || undefined, + tags: manga.tags ?? [], + links: manga.links ?? [], createdAt: manga.createdAt, updatedAt: manga.updatedAt, }; @@ -78,6 +84,8 @@ export class MangaService { status: manga.status as unknown as MangaStatus, dateAdded: manga.dateAdded, dateCompleted: manga.dateCompleted || undefined, + tags: manga.tags ?? [], + links: manga.links ?? [], createdAt: manga.createdAt, updatedAt: manga.updatedAt, }; diff --git a/api/src/app/services/music.service.ts b/api/src/app/services/music.service.ts index 22d8ce8..b7c8e03 100644 --- a/api/src/app/services/music.service.ts +++ b/api/src/app/services/music.service.ts @@ -26,6 +26,8 @@ export class MusicService { status: music.status as unknown as MusicStatus, dateAdded: music.dateAdded, dateCompleted: music.dateCompleted || undefined, + tags: music.tags ?? [], + links: music.links ?? [], createdAt: music.createdAt, updatedAt: music.updatedAt, })); @@ -47,6 +49,8 @@ export class MusicService { status: music.status as unknown as MusicStatus, dateAdded: music.dateAdded, dateCompleted: music.dateCompleted || undefined, + tags: music.tags ?? [], + links: music.links ?? [], createdAt: music.createdAt, updatedAt: music.updatedAt, }; @@ -70,6 +74,8 @@ export class MusicService { status: music.status as unknown as MusicStatus, dateAdded: music.dateAdded, dateCompleted: music.dateCompleted || undefined, + tags: music.tags ?? [], + links: music.links ?? [], createdAt: music.createdAt, updatedAt: music.updatedAt, }; @@ -98,6 +104,8 @@ export class MusicService { status: music.status as unknown as MusicStatus, dateAdded: music.dateAdded, dateCompleted: music.dateCompleted || undefined, + tags: music.tags ?? [], + links: music.links ?? [], createdAt: music.createdAt, updatedAt: music.updatedAt, }; diff --git a/api/src/app/services/show.service.ts b/api/src/app/services/show.service.ts index 09fdbeb..57d7206 100644 --- a/api/src/app/services/show.service.ts +++ b/api/src/app/services/show.service.ts @@ -23,6 +23,8 @@ export class ShowService { status: show.status as unknown as ShowStatus, dateAdded: show.dateAdded, dateCompleted: show.dateCompleted || undefined, + tags: show.tags ?? [], + links: show.links ?? [], createdAt: show.createdAt, updatedAt: show.updatedAt, })); @@ -41,6 +43,8 @@ export class ShowService { status: show.status as unknown as ShowStatus, dateAdded: show.dateAdded, dateCompleted: show.dateCompleted || undefined, + tags: show.tags ?? [], + links: show.links ?? [], createdAt: show.createdAt, updatedAt: show.updatedAt, }; @@ -61,6 +65,8 @@ export class ShowService { status: show.status as unknown as ShowStatus, dateAdded: show.dateAdded, dateCompleted: show.dateCompleted || undefined, + tags: show.tags ?? [], + links: show.links ?? [], createdAt: show.createdAt, updatedAt: show.updatedAt, }; @@ -86,6 +92,8 @@ export class ShowService { status: show.status as unknown as ShowStatus, dateAdded: show.dateAdded, dateCompleted: show.dateCompleted || undefined, + tags: show.tags ?? [], + links: show.links ?? [], createdAt: show.createdAt, updatedAt: show.updatedAt, }; diff --git a/apps/frontend/src/app/components/art/art-gallery.component.ts b/apps/frontend/src/app/components/art/art-gallery.component.ts index 6ef84d4..b6ff755 100644 --- a/apps/frontend/src/app/components/art/art-gallery.component.ts +++ b/apps/frontend/src/app/components/art/art-gallery.component.ts @@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service'; import { CommentsService } from '../../services/comments.service'; import { SanitizeService } from '../../services/sanitize.service'; import { SuggestionService } from '../../services/suggestion.service'; -import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@library/shared-types'; +import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from '@library/shared-types'; @Component({ selector: 'app-art-gallery', @@ -102,6 +102,52 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib } +
+ +
+ @for (tag of newArt.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -231,6 +277,52 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
} +
+ +
+ @for (tag of editArt.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -262,6 +354,24 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib

by {{ art.artist }}

Added: {{ formatDate(art.dateAdded) }}

+ @if (art.tags && art.tags.length > 0) { +
+ @for (tag of art.tags; track tag) { + {{ tag }} + } +
+ } + + @if (art.links && art.links.length > 0) { + + } + @if (authService.isAdmin()) {
+
+ +
+ @for (tag of newBook.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -220,6 +266,52 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti }
+
+ +
+ @for (tag of editBook.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -375,6 +467,24 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti

{{ book.notes }}

} + @if (book.tags && book.tags.length > 0) { +
+ @for (tag of book.tags; track tag) { + {{ tag }} + } +
+ } + + @if (book.links && book.links.length > 0) { + + } + @if (book.dateFinished) {

Finished: {{ formatDate(book.dateFinished) }} @@ -938,6 +1048,116 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti input[type="file"]:hover { border-color: var(--witch-rose); } + + .tags-input-container { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.5rem; + border: 2px solid var(--witch-lavender); + border-radius: 4px; + background: var(--witch-moon); + } + + .tags-input-container input { + flex: 1; + min-width: 150px; + border: none; + padding: 0.25rem; + font-size: 0.9rem; + background: transparent; + } + + .tags-input-container input:focus { + outline: none; + } + + .tag { + display: inline-flex; + align-items: center; + gap: 0.25rem; + background: #8b6f47; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.85rem; + } + + .tag-remove { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 0; + font-size: 1rem; + line-height: 1; + } + + .tag-remove:hover { + opacity: 0.8; + } + + .tags-display { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0.5rem 0; + } + + .tag-chip { + background: rgba(139, 111, 71, 0.2); + color: #8b6f47; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + } + + .links-list { + margin-bottom: 0.5rem; + } + + .link-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; + background: var(--witch-moon); + border-radius: 4px; + margin-bottom: 0.25rem; + } + + .link-add-form { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + + .link-add-form input { + flex: 1; + min-width: 120px; + } + + .links-display { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0.5rem 0; + } + + .external-link { + color: #8b6f47; + text-decoration: none; + font-size: 0.85rem; + padding: 0.2rem 0.5rem; + background: rgba(139, 111, 71, 0.1); + border-radius: 4px; + transition: background 0.2s; + } + + .external-link:hover { + background: rgba(139, 111, 71, 0.2); + text-decoration: underline; + } `] }) export class BooksListComponent implements OnInit { @@ -1000,11 +1220,21 @@ export class BooksListComponent implements OnInit { isbn: '', status: BookStatus.toRead, rating: undefined, - notes: '' + notes: '', + tags: [], + links: [] }; editBook: Partial = {}; + // Tags and links input state + newTagInput = ''; + editTagInput = ''; + newLinkTitle = ''; + newLinkUrl = ''; + editLinkTitle = ''; + editLinkUrl = ''; + ngOnInit() { this.loadBooks(); } @@ -1049,10 +1279,60 @@ export class BooksListComponent implements OnInit { status: BookStatus.toRead, rating: undefined, notes: '', - coverImage: undefined + coverImage: undefined, + tags: [], + links: [] }; this.newBookImagePreview.set(null); this.imageError.set(null); + this.newTagInput = ''; + this.newLinkTitle = ''; + this.newLinkUrl = ''; + } + + addTag(target: 'new' | 'edit') { + const input = target === 'new' ? this.newTagInput.trim() : this.editTagInput.trim(); + if (!input) return; + + if (target === 'new') { + this.newBook.tags = [...(this.newBook.tags || []), input]; + this.newTagInput = ''; + } else { + this.editBook.tags = [...(this.editBook.tags || []), input]; + this.editTagInput = ''; + } + } + + removeTag(index: number, target: 'new' | 'edit') { + if (target === 'new') { + this.newBook.tags = (this.newBook.tags || []).filter((_, i) => i !== index); + } else { + this.editBook.tags = (this.editBook.tags || []).filter((_, i) => i !== index); + } + } + + addLink(target: 'new' | 'edit') { + const title = target === 'new' ? this.newLinkTitle.trim() : this.editLinkTitle.trim(); + const url = target === 'new' ? this.newLinkUrl.trim() : this.editLinkUrl.trim(); + if (!title || !url) return; + + if (target === 'new') { + this.newBook.links = [...(this.newBook.links || []), { title, url }]; + this.newLinkTitle = ''; + this.newLinkUrl = ''; + } else { + this.editBook.links = [...(this.editBook.links || []), { title, url }]; + this.editLinkTitle = ''; + this.editLinkUrl = ''; + } + } + + removeLink(index: number, target: 'new' | 'edit') { + if (target === 'new') { + this.newBook.links = (this.newBook.links || []).filter((_, i) => i !== index); + } else { + this.editBook.links = (this.editBook.links || []).filter((_, i) => i !== index); + } } addBook() { @@ -1065,7 +1345,9 @@ export class BooksListComponent implements OnInit { status: this.newBook.status, rating: this.newBook.rating, notes: this.newBook.notes, - coverImage: this.newBook.coverImage + coverImage: this.newBook.coverImage, + tags: this.newBook.tags || [], + links: this.newBook.links || [] }; this.booksService.createBook(bookToAdd).subscribe(() => { @@ -1091,11 +1373,16 @@ export class BooksListComponent implements OnInit { status: book.status, rating: book.rating, notes: book.notes, - coverImage: book.coverImage + coverImage: book.coverImage, + tags: [...(book.tags || [])], + links: [...(book.links || [])] }; this.editBookImagePreview.set(book.coverImage || null); this.showAddForm.set(false); this.imageError.set(null); + this.editTagInput = ''; + this.editLinkTitle = ''; + this.editLinkUrl = ''; } cancelEdit() { @@ -1103,6 +1390,9 @@ export class BooksListComponent implements OnInit { this.editBook = {}; this.editBookImagePreview.set(null); this.imageError.set(null); + this.editTagInput = ''; + this.editLinkTitle = ''; + this.editLinkUrl = ''; } saveEdit() { diff --git a/apps/frontend/src/app/components/games/games-list.component.ts b/apps/frontend/src/app/components/games/games-list.component.ts index 5285513..96395c0 100644 --- a/apps/frontend/src/app/components/games/games-list.component.ts +++ b/apps/frontend/src/app/components/games/games-list.component.ts @@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service'; import { CommentsService } from '../../services/comments.service'; import { SanitizeService } from '../../services/sanitize.service'; import { SuggestionService } from '../../services/suggestion.service'; -import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEntity } from '@library/shared-types'; +import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEntity, Link } from '@library/shared-types'; @Component({ selector: 'app-games-list', @@ -111,6 +111,52 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti }

+
+ +
+ @for (tag of newGame.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -196,6 +242,52 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti }
+
+ +
+ @for (tag of editGame.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -334,6 +426,24 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti

{{ game.notes }}

} + @if (game.tags && game.tags.length > 0) { +
+ @for (tag of game.tags; track tag) { + {{ tag }} + } +
+ } + + @if (game.links && game.links.length > 0) { + + } + @if (authService.isAdmin()) {
+
+ +
+ @for (tag of newManga.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -198,6 +244,52 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion }
+
+ +
+ @for (tag of editManga.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -335,6 +427,24 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion

{{ manga.notes }}

} + @if (manga.tags && manga.tags.length > 0) { +
+ @for (tag of manga.tags; track tag) { + {{ tag }} + } +
+ } + + @if (manga.links && manga.links.length > 0) { + + } + @if (authService.isAdmin()) {
+
+ +
+ @for (tag of newMusic.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -216,6 +262,52 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, }
+
+ +
+ @for (tag of editMusicData.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -411,6 +503,24 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,

{{ music.notes }}

} + @if (music.tags && music.tags.length > 0) { +
+ @for (tag of music.tags; track tag) { + {{ tag }} + } +
+ } + + @if (music.links && music.links.length > 0) { + + } + @if (music.dateCompleted) {

Completed: {{ formatDate(music.dateCompleted) }} @@ -1008,6 +1118,116 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, input[type="file"]:hover { border-color: var(--witch-rose); } + + .tags-input-container { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.5rem; + border: 2px solid var(--witch-lavender); + border-radius: 4px; + background: var(--witch-moon); + } + + .tags-input-container input { + flex: 1; + min-width: 150px; + border: none; + padding: 0.25rem; + font-size: 0.9rem; + background: transparent; + } + + .tags-input-container input:focus { + outline: none; + } + + .tag { + display: inline-flex; + align-items: center; + gap: 0.25rem; + background: #74b9ff; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.85rem; + } + + .tag-remove { + background: none; + border: none; + color: white; + cursor: pointer; + padding: 0; + font-size: 1rem; + line-height: 1; + } + + .tag-remove:hover { + opacity: 0.8; + } + + .tags-display { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0.5rem 0; + } + + .tag-chip { + background: rgba(116, 185, 255, 0.2); + color: #74b9ff; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + } + + .links-list { + margin-bottom: 0.5rem; + } + + .link-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; + background: var(--witch-moon); + border-radius: 4px; + margin-bottom: 0.25rem; + } + + .link-add-form { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + + .link-add-form input { + flex: 1; + min-width: 120px; + } + + .links-display { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0.5rem 0; + } + + .external-link { + color: #74b9ff; + text-decoration: none; + font-size: 0.85rem; + padding: 0.2rem 0.5rem; + background: rgba(116, 185, 255, 0.1); + border-radius: 4px; + transition: background 0.2s; + } + + .external-link:hover { + background: rgba(116, 185, 255, 0.2); + text-decoration: underline; + } `] }) export class MusicListComponent implements OnInit { @@ -1085,11 +1305,21 @@ export class MusicListComponent implements OnInit { type: MusicType.album, status: MusicStatus.wantToListen, rating: undefined, - notes: '' + notes: '', + tags: [], + links: [] }; editMusicData: Partial = {}; + // Tags and links input state + newTagInput = ''; + editTagInput = ''; + newLinkTitle = ''; + newLinkUrl = ''; + editLinkTitle = ''; + editLinkUrl = ''; + ngOnInit() { this.loadMusic(); } @@ -1146,10 +1376,60 @@ export class MusicListComponent implements OnInit { status: MusicStatus.wantToListen, rating: undefined, notes: '', - coverArt: undefined + coverArt: undefined, + tags: [], + links: [] }; this.newMusicImagePreview.set(null); this.imageError.set(null); + this.newTagInput = ''; + this.newLinkTitle = ''; + this.newLinkUrl = ''; + } + + addTag(target: 'new' | 'edit') { + const input = target === 'new' ? this.newTagInput.trim() : this.editTagInput.trim(); + if (!input) return; + + if (target === 'new') { + this.newMusic.tags = [...(this.newMusic.tags || []), input]; + this.newTagInput = ''; + } else { + this.editMusicData.tags = [...(this.editMusicData.tags || []), input]; + this.editTagInput = ''; + } + } + + removeTag(index: number, target: 'new' | 'edit') { + if (target === 'new') { + this.newMusic.tags = (this.newMusic.tags || []).filter((_, i) => i !== index); + } else { + this.editMusicData.tags = (this.editMusicData.tags || []).filter((_, i) => i !== index); + } + } + + addLink(target: 'new' | 'edit') { + const title = target === 'new' ? this.newLinkTitle.trim() : this.editLinkTitle.trim(); + const url = target === 'new' ? this.newLinkUrl.trim() : this.editLinkUrl.trim(); + if (!title || !url) return; + + if (target === 'new') { + this.newMusic.links = [...(this.newMusic.links || []), { title, url }]; + this.newLinkTitle = ''; + this.newLinkUrl = ''; + } else { + this.editMusicData.links = [...(this.editMusicData.links || []), { title, url }]; + this.editLinkTitle = ''; + this.editLinkUrl = ''; + } + } + + removeLink(index: number, target: 'new' | 'edit') { + if (target === 'new') { + this.newMusic.links = (this.newMusic.links || []).filter((_, i) => i !== index); + } else { + this.editMusicData.links = (this.editMusicData.links || []).filter((_, i) => i !== index); + } } addMusic() { @@ -1162,7 +1442,9 @@ export class MusicListComponent implements OnInit { status: this.newMusic.status, rating: this.newMusic.rating, notes: this.newMusic.notes, - coverArt: this.newMusic.coverArt + coverArt: this.newMusic.coverArt, + tags: this.newMusic.tags || [], + links: this.newMusic.links || [] }; this.musicService.createMusic(musicToAdd).subscribe(() => { @@ -1188,11 +1470,16 @@ export class MusicListComponent implements OnInit { status: music.status, rating: music.rating, notes: music.notes, - coverArt: music.coverArt + coverArt: music.coverArt, + tags: [...(music.tags || [])], + links: [...(music.links || [])] }; this.editMusicImagePreview.set(music.coverArt || null); this.showAddForm.set(false); this.imageError.set(null); + this.editTagInput = ''; + this.editLinkTitle = ''; + this.editLinkUrl = ''; } cancelEdit() { @@ -1200,6 +1487,9 @@ export class MusicListComponent implements OnInit { this.editMusicData = {}; this.editMusicImagePreview.set(null); this.imageError.set(null); + this.editTagInput = ''; + this.editLinkTitle = ''; + this.editLinkUrl = ''; } saveEdit() { diff --git a/apps/frontend/src/app/components/shows/shows-list.component.ts b/apps/frontend/src/app/components/shows/shows-list.component.ts index d4ee4e6..fa7b71c 100644 --- a/apps/frontend/src/app/components/shows/shows-list.component.ts +++ b/apps/frontend/src/app/components/shows/shows-list.component.ts @@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service'; import { CommentsService } from '../../services/comments.service'; import { SanitizeService } from '../../services/sanitize.service'; import { SuggestionService } from '../../services/suggestion.service'; -import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, SuggestionEntity } from '@library/shared-types'; +import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, SuggestionEntity, Link } from '@library/shared-types'; @Component({ selector: 'app-shows-list', @@ -110,6 +110,52 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg }

+
+ +
+ @for (tag of newShow.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -194,6 +240,52 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg }
+
+ +
+ @for (tag of editShow.tags; track tag; let i = $index) { + + {{ tag }} + + + } + +
+
+ +
+ + + +
+
@@ -329,6 +421,24 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg

{{ show.notes }}

} + @if (show.tags && show.tags.length > 0) { +
+ @for (tag of show.tags; track tag) { + {{ tag }} + } +
+ } + + @if (show.links && show.links.length > 0) { + + } + @if (authService.isAdmin()) {