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
}
+
+
+
+
@@ -231,6 +277,52 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
}
+
+
+
+
@@ -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()) {
+
+
+
+
@@ -220,6 +266,52 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
}
+
+
+
+
@@ -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
}
+
+
+
+
@@ -196,6 +242,52 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
}
+
+
+
+
@@ -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()) {
+
+
+
+
@@ -198,6 +244,52 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
}
+
+
+
+
@@ -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()) {
+
+
+
+
@@ -216,6 +262,52 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
}
+
+
+
+
@@ -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
}
+
+
+
+
@@ -194,6 +240,52 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
}
+
+
+
+
@@ -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()) {