generated from nhcarrigan/template
feat: add tags and links
This commit is contained in:
@@ -13,6 +13,11 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Link {
|
||||||
|
title String
|
||||||
|
url String
|
||||||
|
}
|
||||||
|
|
||||||
model Game {
|
model Game {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||||
title String
|
title String
|
||||||
@@ -23,6 +28,8 @@ model Game {
|
|||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverImage String?
|
coverImage String?
|
||||||
|
tags String[]
|
||||||
|
links Link[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
@@ -45,6 +52,8 @@ model Book {
|
|||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverImage String?
|
coverImage String?
|
||||||
|
tags String[]
|
||||||
|
links Link[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
@@ -67,6 +76,8 @@ model Music {
|
|||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverArt String?
|
coverArt String?
|
||||||
|
tags String[]
|
||||||
|
links Link[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
@@ -90,6 +101,8 @@ model Art {
|
|||||||
artist String
|
artist String
|
||||||
description String?
|
description String?
|
||||||
imageUrl String
|
imageUrl String
|
||||||
|
tags String[]
|
||||||
|
links Link[]
|
||||||
dateAdded DateTime @default(now())
|
dateAdded DateTime @default(now())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -106,6 +119,8 @@ model Show {
|
|||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverImage String?
|
coverImage String?
|
||||||
|
tags String[]
|
||||||
|
links Link[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
@@ -134,6 +149,8 @@ model Manga {
|
|||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverImage String?
|
coverImage String?
|
||||||
|
tags String[]
|
||||||
|
links Link[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export class ArtService {
|
|||||||
return artPieces.map((art) => ({
|
return artPieces.map((art) => ({
|
||||||
...art,
|
...art,
|
||||||
description: art.description || undefined,
|
description: art.description || undefined,
|
||||||
|
tags: art.tags ?? [],
|
||||||
|
links: art.links ?? [],
|
||||||
dateAdded: art.dateAdded,
|
dateAdded: art.dateAdded,
|
||||||
createdAt: art.createdAt,
|
createdAt: art.createdAt,
|
||||||
updatedAt: art.updatedAt,
|
updatedAt: art.updatedAt,
|
||||||
@@ -42,6 +44,8 @@ export class ArtService {
|
|||||||
return {
|
return {
|
||||||
...art,
|
...art,
|
||||||
description: art.description || undefined,
|
description: art.description || undefined,
|
||||||
|
tags: art.tags ?? [],
|
||||||
|
links: art.links ?? [],
|
||||||
dateAdded: art.dateAdded,
|
dateAdded: art.dateAdded,
|
||||||
createdAt: art.createdAt,
|
createdAt: art.createdAt,
|
||||||
updatedAt: art.updatedAt,
|
updatedAt: art.updatedAt,
|
||||||
@@ -59,6 +63,8 @@ export class ArtService {
|
|||||||
return {
|
return {
|
||||||
...art,
|
...art,
|
||||||
description: art.description || undefined,
|
description: art.description || undefined,
|
||||||
|
tags: art.tags ?? [],
|
||||||
|
links: art.links ?? [],
|
||||||
dateAdded: art.dateAdded,
|
dateAdded: art.dateAdded,
|
||||||
createdAt: art.createdAt,
|
createdAt: art.createdAt,
|
||||||
updatedAt: art.updatedAt,
|
updatedAt: art.updatedAt,
|
||||||
@@ -77,6 +83,8 @@ export class ArtService {
|
|||||||
return {
|
return {
|
||||||
...art,
|
...art,
|
||||||
description: art.description || undefined,
|
description: art.description || undefined,
|
||||||
|
tags: art.tags ?? [],
|
||||||
|
links: art.links ?? [],
|
||||||
dateAdded: art.dateAdded,
|
dateAdded: art.dateAdded,
|
||||||
createdAt: art.createdAt,
|
createdAt: art.createdAt,
|
||||||
updatedAt: art.updatedAt,
|
updatedAt: art.updatedAt,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export class BookService {
|
|||||||
status: book.status as unknown as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
|
tags: book.tags ?? [],
|
||||||
|
links: book.links ?? [],
|
||||||
createdAt: book.createdAt,
|
createdAt: book.createdAt,
|
||||||
updatedAt: book.updatedAt,
|
updatedAt: book.updatedAt,
|
||||||
}));
|
}));
|
||||||
@@ -45,6 +47,8 @@ export class BookService {
|
|||||||
status: book.status as unknown as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
|
tags: book.tags ?? [],
|
||||||
|
links: book.links ?? [],
|
||||||
createdAt: book.createdAt,
|
createdAt: book.createdAt,
|
||||||
updatedAt: book.updatedAt,
|
updatedAt: book.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -66,6 +70,8 @@ export class BookService {
|
|||||||
status: book.status as unknown as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
|
tags: book.tags ?? [],
|
||||||
|
links: book.links ?? [],
|
||||||
createdAt: book.createdAt,
|
createdAt: book.createdAt,
|
||||||
updatedAt: book.updatedAt,
|
updatedAt: book.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -90,6 +96,8 @@ export class BookService {
|
|||||||
status: book.status as unknown as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
|
tags: book.tags ?? [],
|
||||||
|
links: book.links ?? [],
|
||||||
createdAt: book.createdAt,
|
createdAt: book.createdAt,
|
||||||
updatedAt: book.updatedAt,
|
updatedAt: book.updatedAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export class GameService {
|
|||||||
status: game.status as unknown as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
|
tags: game.tags ?? [],
|
||||||
|
links: game.links ?? [],
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
updatedAt: game.updatedAt,
|
updatedAt: game.updatedAt,
|
||||||
}));
|
}));
|
||||||
@@ -45,6 +47,8 @@ export class GameService {
|
|||||||
status: game.status as unknown as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
|
tags: game.tags ?? [],
|
||||||
|
links: game.links ?? [],
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
updatedAt: game.updatedAt,
|
updatedAt: game.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -66,6 +70,8 @@ export class GameService {
|
|||||||
status: game.status as unknown as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
|
tags: game.tags ?? [],
|
||||||
|
links: game.links ?? [],
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
updatedAt: game.updatedAt,
|
updatedAt: game.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -90,6 +96,8 @@ export class GameService {
|
|||||||
status: game.status as unknown as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
|
tags: game.tags ?? [],
|
||||||
|
links: game.links ?? [],
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
updatedAt: game.updatedAt,
|
updatedAt: game.updatedAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export class MangaService {
|
|||||||
status: m.status as unknown as MangaStatus,
|
status: m.status as unknown as MangaStatus,
|
||||||
dateAdded: m.dateAdded,
|
dateAdded: m.dateAdded,
|
||||||
dateCompleted: m.dateCompleted || undefined,
|
dateCompleted: m.dateCompleted || undefined,
|
||||||
|
tags: m.tags ?? [],
|
||||||
|
links: m.links ?? [],
|
||||||
createdAt: m.createdAt,
|
createdAt: m.createdAt,
|
||||||
updatedAt: m.updatedAt,
|
updatedAt: m.updatedAt,
|
||||||
}));
|
}));
|
||||||
@@ -39,6 +41,8 @@ export class MangaService {
|
|||||||
status: manga.status as unknown as MangaStatus,
|
status: manga.status as unknown as MangaStatus,
|
||||||
dateAdded: manga.dateAdded,
|
dateAdded: manga.dateAdded,
|
||||||
dateCompleted: manga.dateCompleted || undefined,
|
dateCompleted: manga.dateCompleted || undefined,
|
||||||
|
tags: manga.tags ?? [],
|
||||||
|
links: manga.links ?? [],
|
||||||
createdAt: manga.createdAt,
|
createdAt: manga.createdAt,
|
||||||
updatedAt: manga.updatedAt,
|
updatedAt: manga.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -57,6 +61,8 @@ export class MangaService {
|
|||||||
status: manga.status as unknown as MangaStatus,
|
status: manga.status as unknown as MangaStatus,
|
||||||
dateAdded: manga.dateAdded,
|
dateAdded: manga.dateAdded,
|
||||||
dateCompleted: manga.dateCompleted || undefined,
|
dateCompleted: manga.dateCompleted || undefined,
|
||||||
|
tags: manga.tags ?? [],
|
||||||
|
links: manga.links ?? [],
|
||||||
createdAt: manga.createdAt,
|
createdAt: manga.createdAt,
|
||||||
updatedAt: manga.updatedAt,
|
updatedAt: manga.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -78,6 +84,8 @@ export class MangaService {
|
|||||||
status: manga.status as unknown as MangaStatus,
|
status: manga.status as unknown as MangaStatus,
|
||||||
dateAdded: manga.dateAdded,
|
dateAdded: manga.dateAdded,
|
||||||
dateCompleted: manga.dateCompleted || undefined,
|
dateCompleted: manga.dateCompleted || undefined,
|
||||||
|
tags: manga.tags ?? [],
|
||||||
|
links: manga.links ?? [],
|
||||||
createdAt: manga.createdAt,
|
createdAt: manga.createdAt,
|
||||||
updatedAt: manga.updatedAt,
|
updatedAt: manga.updatedAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export class MusicService {
|
|||||||
status: music.status as unknown as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
|
tags: music.tags ?? [],
|
||||||
|
links: music.links ?? [],
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
updatedAt: music.updatedAt,
|
updatedAt: music.updatedAt,
|
||||||
}));
|
}));
|
||||||
@@ -47,6 +49,8 @@ export class MusicService {
|
|||||||
status: music.status as unknown as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
|
tags: music.tags ?? [],
|
||||||
|
links: music.links ?? [],
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
updatedAt: music.updatedAt,
|
updatedAt: music.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -70,6 +74,8 @@ export class MusicService {
|
|||||||
status: music.status as unknown as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
|
tags: music.tags ?? [],
|
||||||
|
links: music.links ?? [],
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
updatedAt: music.updatedAt,
|
updatedAt: music.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -98,6 +104,8 @@ export class MusicService {
|
|||||||
status: music.status as unknown as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
|
tags: music.tags ?? [],
|
||||||
|
links: music.links ?? [],
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
updatedAt: music.updatedAt,
|
updatedAt: music.updatedAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export class ShowService {
|
|||||||
status: show.status as unknown as ShowStatus,
|
status: show.status as unknown as ShowStatus,
|
||||||
dateAdded: show.dateAdded,
|
dateAdded: show.dateAdded,
|
||||||
dateCompleted: show.dateCompleted || undefined,
|
dateCompleted: show.dateCompleted || undefined,
|
||||||
|
tags: show.tags ?? [],
|
||||||
|
links: show.links ?? [],
|
||||||
createdAt: show.createdAt,
|
createdAt: show.createdAt,
|
||||||
updatedAt: show.updatedAt,
|
updatedAt: show.updatedAt,
|
||||||
}));
|
}));
|
||||||
@@ -41,6 +43,8 @@ export class ShowService {
|
|||||||
status: show.status as unknown as ShowStatus,
|
status: show.status as unknown as ShowStatus,
|
||||||
dateAdded: show.dateAdded,
|
dateAdded: show.dateAdded,
|
||||||
dateCompleted: show.dateCompleted || undefined,
|
dateCompleted: show.dateCompleted || undefined,
|
||||||
|
tags: show.tags ?? [],
|
||||||
|
links: show.links ?? [],
|
||||||
createdAt: show.createdAt,
|
createdAt: show.createdAt,
|
||||||
updatedAt: show.updatedAt,
|
updatedAt: show.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -61,6 +65,8 @@ export class ShowService {
|
|||||||
status: show.status as unknown as ShowStatus,
|
status: show.status as unknown as ShowStatus,
|
||||||
dateAdded: show.dateAdded,
|
dateAdded: show.dateAdded,
|
||||||
dateCompleted: show.dateCompleted || undefined,
|
dateCompleted: show.dateCompleted || undefined,
|
||||||
|
tags: show.tags ?? [],
|
||||||
|
links: show.links ?? [],
|
||||||
createdAt: show.createdAt,
|
createdAt: show.createdAt,
|
||||||
updatedAt: show.updatedAt,
|
updatedAt: show.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -86,6 +92,8 @@ export class ShowService {
|
|||||||
status: show.status as unknown as ShowStatus,
|
status: show.status as unknown as ShowStatus,
|
||||||
dateAdded: show.dateAdded,
|
dateAdded: show.dateAdded,
|
||||||
dateCompleted: show.dateCompleted || undefined,
|
dateCompleted: show.dateCompleted || undefined,
|
||||||
|
tags: show.tags ?? [],
|
||||||
|
links: show.links ?? [],
|
||||||
createdAt: show.createdAt,
|
createdAt: show.createdAt,
|
||||||
updatedAt: show.updatedAt,
|
updatedAt: show.updatedAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.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({
|
@Component({
|
||||||
selector: 'app-art-gallery',
|
selector: 'app-art-gallery',
|
||||||
@@ -102,6 +102,52 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of newArt.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'new')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newTagInput"
|
||||||
|
name="newTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('new'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of newArt.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'new')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newLinkTitle"
|
||||||
|
name="newLinkTitle"
|
||||||
|
placeholder="Link title (e.g., DeviantArt)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="newLinkUrl"
|
||||||
|
name="newLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('new')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Artwork</button>
|
<button type="submit" class="btn btn-primary">Add Artwork</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -231,6 +277,52 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of editArt.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'edit')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editTagInput"
|
||||||
|
name="editTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('edit'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of editArt.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'edit')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editLinkTitle"
|
||||||
|
name="editLinkTitle"
|
||||||
|
placeholder="Link title (e.g., DeviantArt)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="editLinkUrl"
|
||||||
|
name="editLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('edit')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -262,6 +354,24 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
|
|||||||
<p class="artist">by {{ art.artist }}</p>
|
<p class="artist">by {{ art.artist }}</p>
|
||||||
<p class="date-added">Added: {{ formatDate(art.dateAdded) }}</p>
|
<p class="date-added">Added: {{ formatDate(art.dateAdded) }}</p>
|
||||||
|
|
||||||
|
@if (art.tags && art.tags.length > 0) {
|
||||||
|
<div class="tags-display">
|
||||||
|
@for (tag of art.tags; track tag) {
|
||||||
|
<span class="tag-chip">{{ tag }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (art.links && art.links.length > 0) {
|
||||||
|
<div class="links-display">
|
||||||
|
@for (link of art.links; track link.url) {
|
||||||
|
<a [href]="link.url" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||||
|
{{ link.title }} ↗
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (authService.isAdmin()) {
|
@if (authService.isAdmin()) {
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button (click)="startEdit(art)" class="btn btn-secondary btn-sm">
|
<button (click)="startEdit(art)" class="btn btn-secondary btn-sm">
|
||||||
@@ -812,6 +922,116 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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: #fdcb6e;
|
||||||
|
color: #333;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #333;
|
||||||
|
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(253, 203, 110, 0.2);
|
||||||
|
color: #b38f00;
|
||||||
|
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: #b38f00;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
background: rgba(253, 203, 110, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-link:hover {
|
||||||
|
background: rgba(253, 203, 110, 0.2);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class ArtGalleryComponent implements OnInit {
|
export class ArtGalleryComponent implements OnInit {
|
||||||
@@ -848,11 +1068,21 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
title: '',
|
title: '',
|
||||||
artist: '',
|
artist: '',
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
|
|
||||||
editArt: Partial<UpdateArtDto> = {};
|
editArt: Partial<UpdateArtDto> = {};
|
||||||
|
|
||||||
|
// Tags and links input state
|
||||||
|
newTagInput = '';
|
||||||
|
editTagInput = '';
|
||||||
|
newLinkTitle = '';
|
||||||
|
newLinkUrl = '';
|
||||||
|
editLinkTitle = '';
|
||||||
|
editLinkUrl = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadArt();
|
this.loadArt();
|
||||||
}
|
}
|
||||||
@@ -882,8 +1112,58 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
title: '',
|
title: '',
|
||||||
artist: '',
|
artist: '',
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
|
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.newArt.tags = [...(this.newArt.tags || []), input];
|
||||||
|
this.newTagInput = '';
|
||||||
|
} else {
|
||||||
|
this.editArt.tags = [...(this.editArt.tags || []), input];
|
||||||
|
this.editTagInput = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(index: number, target: 'new' | 'edit') {
|
||||||
|
if (target === 'new') {
|
||||||
|
this.newArt.tags = (this.newArt.tags || []).filter((_, i) => i !== index);
|
||||||
|
} else {
|
||||||
|
this.editArt.tags = (this.editArt.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.newArt.links = [...(this.newArt.links || []), { title, url }];
|
||||||
|
this.newLinkTitle = '';
|
||||||
|
this.newLinkUrl = '';
|
||||||
|
} else {
|
||||||
|
this.editArt.links = [...(this.editArt.links || []), { title, url }];
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLink(index: number, target: 'new' | 'edit') {
|
||||||
|
if (target === 'new') {
|
||||||
|
this.newArt.links = (this.newArt.links || []).filter((_, i) => i !== index);
|
||||||
|
} else {
|
||||||
|
this.editArt.links = (this.editArt.links || []).filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addArt() {
|
addArt() {
|
||||||
@@ -893,7 +1173,9 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
title: this.newArt.title,
|
title: this.newArt.title,
|
||||||
artist: this.newArt.artist,
|
artist: this.newArt.artist,
|
||||||
imageUrl: this.newArt.imageUrl,
|
imageUrl: this.newArt.imageUrl,
|
||||||
description: this.newArt.description
|
description: this.newArt.description,
|
||||||
|
tags: this.newArt.tags || [],
|
||||||
|
links: this.newArt.links || []
|
||||||
};
|
};
|
||||||
|
|
||||||
this.artService.createArt(artToAdd).subscribe(() => {
|
this.artService.createArt(artToAdd).subscribe(() => {
|
||||||
@@ -916,14 +1198,22 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
title: art.title,
|
title: art.title,
|
||||||
artist: art.artist,
|
artist: art.artist,
|
||||||
imageUrl: art.imageUrl,
|
imageUrl: art.imageUrl,
|
||||||
description: art.description
|
description: art.description,
|
||||||
|
tags: [...(art.tags || [])],
|
||||||
|
links: [...(art.links || [])]
|
||||||
};
|
};
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
this.editingArt.set(null);
|
this.editingArt.set(null);
|
||||||
this.editArt = {};
|
this.editArt = {};
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEntity } from '@library/shared-types';
|
import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-books-list',
|
selector: 'app-books-list',
|
||||||
@@ -123,6 +123,52 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of newBook.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'new')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newTagInput"
|
||||||
|
name="newTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('new'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of newBook.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'new')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newLinkTitle"
|
||||||
|
name="newLinkTitle"
|
||||||
|
placeholder="Link title (e.g., Goodreads)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="newLinkUrl"
|
||||||
|
name="newLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('new')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Book</button>
|
<button type="submit" class="btn btn-primary">Add Book</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -220,6 +266,52 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of editBook.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'edit')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editTagInput"
|
||||||
|
name="editTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('edit'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of editBook.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'edit')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editLinkTitle"
|
||||||
|
name="editLinkTitle"
|
||||||
|
placeholder="Link title (e.g., Goodreads)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="editLinkUrl"
|
||||||
|
name="editLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('edit')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -375,6 +467,24 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
<p class="notes">{{ book.notes }}</p>
|
<p class="notes">{{ book.notes }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (book.tags && book.tags.length > 0) {
|
||||||
|
<div class="tags-display">
|
||||||
|
@for (tag of book.tags; track tag) {
|
||||||
|
<span class="tag-chip">{{ tag }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (book.links && book.links.length > 0) {
|
||||||
|
<div class="links-display">
|
||||||
|
@for (link of book.links; track link.url) {
|
||||||
|
<a [href]="link.url" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||||
|
{{ link.title }} ↗
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (book.dateFinished) {
|
@if (book.dateFinished) {
|
||||||
<p class="date-finished">
|
<p class="date-finished">
|
||||||
Finished: {{ formatDate(book.dateFinished) }}
|
Finished: {{ formatDate(book.dateFinished) }}
|
||||||
@@ -938,6 +1048,116 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
input[type="file"]:hover {
|
input[type="file"]:hover {
|
||||||
border-color: var(--witch-rose);
|
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 {
|
export class BooksListComponent implements OnInit {
|
||||||
@@ -1000,11 +1220,21 @@ export class BooksListComponent implements OnInit {
|
|||||||
isbn: '',
|
isbn: '',
|
||||||
status: BookStatus.toRead,
|
status: BookStatus.toRead,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: ''
|
notes: '',
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
|
|
||||||
editBook: Partial<UpdateBookDto> = {};
|
editBook: Partial<UpdateBookDto> = {};
|
||||||
|
|
||||||
|
// Tags and links input state
|
||||||
|
newTagInput = '';
|
||||||
|
editTagInput = '';
|
||||||
|
newLinkTitle = '';
|
||||||
|
newLinkUrl = '';
|
||||||
|
editLinkTitle = '';
|
||||||
|
editLinkUrl = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadBooks();
|
this.loadBooks();
|
||||||
}
|
}
|
||||||
@@ -1049,10 +1279,60 @@ export class BooksListComponent implements OnInit {
|
|||||||
status: BookStatus.toRead,
|
status: BookStatus.toRead,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverImage: undefined
|
coverImage: undefined,
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
this.newBookImagePreview.set(null);
|
this.newBookImagePreview.set(null);
|
||||||
this.imageError.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() {
|
addBook() {
|
||||||
@@ -1065,7 +1345,9 @@ export class BooksListComponent implements OnInit {
|
|||||||
status: this.newBook.status,
|
status: this.newBook.status,
|
||||||
rating: this.newBook.rating,
|
rating: this.newBook.rating,
|
||||||
notes: this.newBook.notes,
|
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(() => {
|
this.booksService.createBook(bookToAdd).subscribe(() => {
|
||||||
@@ -1091,11 +1373,16 @@ export class BooksListComponent implements OnInit {
|
|||||||
status: book.status,
|
status: book.status,
|
||||||
rating: book.rating,
|
rating: book.rating,
|
||||||
notes: book.notes,
|
notes: book.notes,
|
||||||
coverImage: book.coverImage
|
coverImage: book.coverImage,
|
||||||
|
tags: [...(book.tags || [])],
|
||||||
|
links: [...(book.links || [])]
|
||||||
};
|
};
|
||||||
this.editBookImagePreview.set(book.coverImage || null);
|
this.editBookImagePreview.set(book.coverImage || null);
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
@@ -1103,6 +1390,9 @@ export class BooksListComponent implements OnInit {
|
|||||||
this.editBook = {};
|
this.editBook = {};
|
||||||
this.editBookImagePreview.set(null);
|
this.editBookImagePreview.set(null);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.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({
|
@Component({
|
||||||
selector: 'app-games-list',
|
selector: 'app-games-list',
|
||||||
@@ -111,6 +111,52 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of newGame.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'new')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newTagInput"
|
||||||
|
name="newTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('new'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of newGame.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'new')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newLinkTitle"
|
||||||
|
name="newLinkTitle"
|
||||||
|
placeholder="Link title (e.g., Steam)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="newLinkUrl"
|
||||||
|
name="newLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('new')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Game</button>
|
<button type="submit" class="btn btn-primary">Add Game</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -196,6 +242,52 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of editGame.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'edit')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editTagInput"
|
||||||
|
name="editTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('edit'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of editGame.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'edit')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editLinkTitle"
|
||||||
|
name="editLinkTitle"
|
||||||
|
placeholder="Link title (e.g., Steam)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="editLinkUrl"
|
||||||
|
name="editLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('edit')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -334,6 +426,24 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
<p class="notes">{{ game.notes }}</p>
|
<p class="notes">{{ game.notes }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (game.tags && game.tags.length > 0) {
|
||||||
|
<div class="tags-display">
|
||||||
|
@for (tag of game.tags; track tag) {
|
||||||
|
<span class="tag-chip">{{ tag }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (game.links && game.links.length > 0) {
|
||||||
|
<div class="links-display">
|
||||||
|
@for (link of game.links; track link.url) {
|
||||||
|
<a [href]="link.url" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||||
|
{{ link.title }} ↗
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (authService.isAdmin()) {
|
@if (authService.isAdmin()) {
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button (click)="startEdit(game)" class="btn btn-secondary btn-sm">
|
<button (click)="startEdit(game)" class="btn btn-secondary btn-sm">
|
||||||
@@ -747,6 +857,112 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tags and Links Styling */
|
||||||
|
.tags-input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-input-container input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove:hover {
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-display {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip {
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links-list {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #ff6b6b;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border: 1px solid #ff6b6b;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-link:hover {
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.comment-edit-form {
|
.comment-edit-form {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -858,11 +1074,21 @@ export class GamesListComponent implements OnInit {
|
|||||||
platform: '',
|
platform: '',
|
||||||
status: GameStatus.backlog,
|
status: GameStatus.backlog,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: ''
|
notes: '',
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
|
|
||||||
editGame: Partial<UpdateGameDto> = {};
|
editGame: Partial<UpdateGameDto> = {};
|
||||||
|
|
||||||
|
// Tags and links input state
|
||||||
|
newTagInput = '';
|
||||||
|
editTagInput = '';
|
||||||
|
newLinkTitle = '';
|
||||||
|
newLinkUrl = '';
|
||||||
|
editLinkTitle = '';
|
||||||
|
editLinkUrl = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadGames();
|
this.loadGames();
|
||||||
}
|
}
|
||||||
@@ -906,10 +1132,60 @@ export class GamesListComponent implements OnInit {
|
|||||||
status: GameStatus.backlog,
|
status: GameStatus.backlog,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverImage: undefined
|
coverImage: undefined,
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
this.newGameImagePreview.set(null);
|
this.newGameImagePreview.set(null);
|
||||||
this.imageError.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.newGame.tags = [...(this.newGame.tags || []), input];
|
||||||
|
this.newTagInput = '';
|
||||||
|
} else {
|
||||||
|
this.editGame.tags = [...(this.editGame.tags || []), input];
|
||||||
|
this.editTagInput = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(index: number, target: 'new' | 'edit') {
|
||||||
|
if (target === 'new') {
|
||||||
|
this.newGame.tags = (this.newGame.tags || []).filter((_, i) => i !== index);
|
||||||
|
} else {
|
||||||
|
this.editGame.tags = (this.editGame.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.newGame.links = [...(this.newGame.links || []), { title, url }];
|
||||||
|
this.newLinkTitle = '';
|
||||||
|
this.newLinkUrl = '';
|
||||||
|
} else {
|
||||||
|
this.editGame.links = [...(this.editGame.links || []), { title, url }];
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLink(index: number, target: 'new' | 'edit') {
|
||||||
|
if (target === 'new') {
|
||||||
|
this.newGame.links = (this.newGame.links || []).filter((_, i) => i !== index);
|
||||||
|
} else {
|
||||||
|
this.editGame.links = (this.editGame.links || []).filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addGame() {
|
addGame() {
|
||||||
@@ -921,7 +1197,9 @@ export class GamesListComponent implements OnInit {
|
|||||||
status: this.newGame.status,
|
status: this.newGame.status,
|
||||||
rating: this.newGame.rating,
|
rating: this.newGame.rating,
|
||||||
notes: this.newGame.notes,
|
notes: this.newGame.notes,
|
||||||
coverImage: this.newGame.coverImage
|
coverImage: this.newGame.coverImage,
|
||||||
|
tags: this.newGame.tags || [],
|
||||||
|
links: this.newGame.links || []
|
||||||
};
|
};
|
||||||
|
|
||||||
this.gamesService.createGame(gameToAdd).subscribe(() => {
|
this.gamesService.createGame(gameToAdd).subscribe(() => {
|
||||||
@@ -946,11 +1224,16 @@ export class GamesListComponent implements OnInit {
|
|||||||
status: game.status,
|
status: game.status,
|
||||||
rating: game.rating,
|
rating: game.rating,
|
||||||
notes: game.notes,
|
notes: game.notes,
|
||||||
coverImage: game.coverImage
|
coverImage: game.coverImage,
|
||||||
|
tags: [...(game.tags || [])],
|
||||||
|
links: [...(game.links || [])]
|
||||||
};
|
};
|
||||||
this.editGameImagePreview.set(game.coverImage || null);
|
this.editGameImagePreview.set(game.coverImage || null);
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
@@ -958,6 +1241,9 @@ export class GamesListComponent implements OnInit {
|
|||||||
this.editGame = {};
|
this.editGame = {};
|
||||||
this.editGameImagePreview.set(null);
|
this.editGameImagePreview.set(null);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, SuggestionEntity } from '@library/shared-types';
|
import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-manga-list',
|
selector: 'app-manga-list',
|
||||||
@@ -112,6 +112,52 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of newManga.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'new')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newTagInput"
|
||||||
|
name="newTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('new'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of newManga.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'new')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newLinkTitle"
|
||||||
|
name="newLinkTitle"
|
||||||
|
placeholder="Link title (e.g., MyAnimeList)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="newLinkUrl"
|
||||||
|
name="newLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('new')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Manga</button>
|
<button type="submit" class="btn btn-primary">Add Manga</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -198,6 +244,52 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of editManga.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'edit')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editTagInput"
|
||||||
|
name="editTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('edit'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of editManga.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'edit')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editLinkTitle"
|
||||||
|
name="editLinkTitle"
|
||||||
|
placeholder="Link title (e.g., MyAnimeList)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="editLinkUrl"
|
||||||
|
name="editLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('edit')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -335,6 +427,24 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
<p class="notes">{{ manga.notes }}</p>
|
<p class="notes">{{ manga.notes }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (manga.tags && manga.tags.length > 0) {
|
||||||
|
<div class="tags-display">
|
||||||
|
@for (tag of manga.tags; track tag) {
|
||||||
|
<span class="tag-chip">{{ tag }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (manga.links && manga.links.length > 0) {
|
||||||
|
<div class="links-display">
|
||||||
|
@for (link of manga.links; track link.url) {
|
||||||
|
<a [href]="link.url" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||||
|
{{ link.title }} ↗
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (authService.isAdmin()) {
|
@if (authService.isAdmin()) {
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button (click)="startEdit(manga)" class="btn btn-secondary btn-sm">
|
<button (click)="startEdit(manga)" class="btn btn-secondary btn-sm">
|
||||||
@@ -801,6 +911,116 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
input[type="file"]:hover {
|
input[type="file"]:hover {
|
||||||
border-color: #ec4899;
|
border-color: #ec4899;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #00b894;
|
||||||
|
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(0, 184, 148, 0.2);
|
||||||
|
color: #00b894;
|
||||||
|
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: #f9fafb;
|
||||||
|
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: #00b894;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
background: rgba(0, 184, 148, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-link:hover {
|
||||||
|
background: rgba(0, 184, 148, 0.2);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class MangaListComponent implements OnInit {
|
export class MangaListComponent implements OnInit {
|
||||||
@@ -857,11 +1077,21 @@ export class MangaListComponent implements OnInit {
|
|||||||
author: '',
|
author: '',
|
||||||
status: MangaStatus.wantToRead,
|
status: MangaStatus.wantToRead,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: ''
|
notes: '',
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
|
|
||||||
editManga: Partial<UpdateMangaDto> = {};
|
editManga: Partial<UpdateMangaDto> = {};
|
||||||
|
|
||||||
|
// Tags and links input state
|
||||||
|
newTagInput = '';
|
||||||
|
editTagInput = '';
|
||||||
|
newLinkTitle = '';
|
||||||
|
newLinkUrl = '';
|
||||||
|
editLinkTitle = '';
|
||||||
|
editLinkUrl = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadManga();
|
this.loadManga();
|
||||||
}
|
}
|
||||||
@@ -905,10 +1135,60 @@ export class MangaListComponent implements OnInit {
|
|||||||
status: MangaStatus.wantToRead,
|
status: MangaStatus.wantToRead,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverImage: undefined
|
coverImage: undefined,
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
this.newMangaImagePreview.set(null);
|
this.newMangaImagePreview.set(null);
|
||||||
this.imageError.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.newManga.tags = [...(this.newManga.tags || []), input];
|
||||||
|
this.newTagInput = '';
|
||||||
|
} else {
|
||||||
|
this.editManga.tags = [...(this.editManga.tags || []), input];
|
||||||
|
this.editTagInput = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(index: number, target: 'new' | 'edit') {
|
||||||
|
if (target === 'new') {
|
||||||
|
this.newManga.tags = (this.newManga.tags || []).filter((_, i) => i !== index);
|
||||||
|
} else {
|
||||||
|
this.editManga.tags = (this.editManga.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.newManga.links = [...(this.newManga.links || []), { title, url }];
|
||||||
|
this.newLinkTitle = '';
|
||||||
|
this.newLinkUrl = '';
|
||||||
|
} else {
|
||||||
|
this.editManga.links = [...(this.editManga.links || []), { title, url }];
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLink(index: number, target: 'new' | 'edit') {
|
||||||
|
if (target === 'new') {
|
||||||
|
this.newManga.links = (this.newManga.links || []).filter((_, i) => i !== index);
|
||||||
|
} else {
|
||||||
|
this.editManga.links = (this.editManga.links || []).filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addManga() {
|
addManga() {
|
||||||
@@ -920,7 +1200,9 @@ export class MangaListComponent implements OnInit {
|
|||||||
status: this.newManga.status,
|
status: this.newManga.status,
|
||||||
rating: this.newManga.rating,
|
rating: this.newManga.rating,
|
||||||
notes: this.newManga.notes,
|
notes: this.newManga.notes,
|
||||||
coverImage: this.newManga.coverImage
|
coverImage: this.newManga.coverImage,
|
||||||
|
tags: this.newManga.tags || [],
|
||||||
|
links: this.newManga.links || []
|
||||||
};
|
};
|
||||||
|
|
||||||
this.mangaService.createManga(mangaToAdd).subscribe(() => {
|
this.mangaService.createManga(mangaToAdd).subscribe(() => {
|
||||||
@@ -945,11 +1227,16 @@ export class MangaListComponent implements OnInit {
|
|||||||
status: manga.status,
|
status: manga.status,
|
||||||
rating: manga.rating,
|
rating: manga.rating,
|
||||||
notes: manga.notes,
|
notes: manga.notes,
|
||||||
coverImage: manga.coverImage
|
coverImage: manga.coverImage,
|
||||||
|
tags: [...(manga.tags || [])],
|
||||||
|
links: [...(manga.links || [])]
|
||||||
};
|
};
|
||||||
this.editMangaImagePreview.set(manga.coverImage || null);
|
this.editMangaImagePreview.set(manga.coverImage || null);
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
@@ -957,6 +1244,9 @@ export class MangaListComponent implements OnInit {
|
|||||||
this.editManga = {};
|
this.editManga = {};
|
||||||
this.editMangaImagePreview.set(null);
|
this.editMangaImagePreview.set(null);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.service';
|
import { SuggestionService } from '../../services/suggestion.service';
|
||||||
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, SuggestionEntity } from '@library/shared-types';
|
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-music-list',
|
selector: 'app-music-list',
|
||||||
@@ -121,6 +121,52 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of newMusic.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'new')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newTagInput"
|
||||||
|
name="newTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('new'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of newMusic.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'new')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newLinkTitle"
|
||||||
|
name="newLinkTitle"
|
||||||
|
placeholder="Link title (e.g., Spotify)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="newLinkUrl"
|
||||||
|
name="newLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('new')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Music</button>
|
<button type="submit" class="btn btn-primary">Add Music</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -216,6 +262,52 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of editMusicData.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'edit')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editTagInput"
|
||||||
|
name="editTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('edit'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of editMusicData.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'edit')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editLinkTitle"
|
||||||
|
name="editLinkTitle"
|
||||||
|
placeholder="Link title (e.g., Spotify)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="editLinkUrl"
|
||||||
|
name="editLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('edit')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -411,6 +503,24 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
<p class="notes">{{ music.notes }}</p>
|
<p class="notes">{{ music.notes }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (music.tags && music.tags.length > 0) {
|
||||||
|
<div class="tags-display">
|
||||||
|
@for (tag of music.tags; track tag) {
|
||||||
|
<span class="tag-chip">{{ tag }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (music.links && music.links.length > 0) {
|
||||||
|
<div class="links-display">
|
||||||
|
@for (link of music.links; track link.url) {
|
||||||
|
<a [href]="link.url" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||||
|
{{ link.title }} ↗
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (music.dateCompleted) {
|
@if (music.dateCompleted) {
|
||||||
<p class="date-completed">
|
<p class="date-completed">
|
||||||
Completed: {{ formatDate(music.dateCompleted) }}
|
Completed: {{ formatDate(music.dateCompleted) }}
|
||||||
@@ -1008,6 +1118,116 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
input[type="file"]:hover {
|
input[type="file"]:hover {
|
||||||
border-color: var(--witch-rose);
|
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 {
|
export class MusicListComponent implements OnInit {
|
||||||
@@ -1085,11 +1305,21 @@ export class MusicListComponent implements OnInit {
|
|||||||
type: MusicType.album,
|
type: MusicType.album,
|
||||||
status: MusicStatus.wantToListen,
|
status: MusicStatus.wantToListen,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: ''
|
notes: '',
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
|
|
||||||
editMusicData: Partial<UpdateMusicDto> = {};
|
editMusicData: Partial<UpdateMusicDto> = {};
|
||||||
|
|
||||||
|
// Tags and links input state
|
||||||
|
newTagInput = '';
|
||||||
|
editTagInput = '';
|
||||||
|
newLinkTitle = '';
|
||||||
|
newLinkUrl = '';
|
||||||
|
editLinkTitle = '';
|
||||||
|
editLinkUrl = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadMusic();
|
this.loadMusic();
|
||||||
}
|
}
|
||||||
@@ -1146,10 +1376,60 @@ export class MusicListComponent implements OnInit {
|
|||||||
status: MusicStatus.wantToListen,
|
status: MusicStatus.wantToListen,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverArt: undefined
|
coverArt: undefined,
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
this.newMusicImagePreview.set(null);
|
this.newMusicImagePreview.set(null);
|
||||||
this.imageError.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() {
|
addMusic() {
|
||||||
@@ -1162,7 +1442,9 @@ export class MusicListComponent implements OnInit {
|
|||||||
status: this.newMusic.status,
|
status: this.newMusic.status,
|
||||||
rating: this.newMusic.rating,
|
rating: this.newMusic.rating,
|
||||||
notes: this.newMusic.notes,
|
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(() => {
|
this.musicService.createMusic(musicToAdd).subscribe(() => {
|
||||||
@@ -1188,11 +1470,16 @@ export class MusicListComponent implements OnInit {
|
|||||||
status: music.status,
|
status: music.status,
|
||||||
rating: music.rating,
|
rating: music.rating,
|
||||||
notes: music.notes,
|
notes: music.notes,
|
||||||
coverArt: music.coverArt
|
coverArt: music.coverArt,
|
||||||
|
tags: [...(music.tags || [])],
|
||||||
|
links: [...(music.links || [])]
|
||||||
};
|
};
|
||||||
this.editMusicImagePreview.set(music.coverArt || null);
|
this.editMusicImagePreview.set(music.coverArt || null);
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
@@ -1200,6 +1487,9 @@ export class MusicListComponent implements OnInit {
|
|||||||
this.editMusicData = {};
|
this.editMusicData = {};
|
||||||
this.editMusicImagePreview.set(null);
|
this.editMusicImagePreview.set(null);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service';
|
|||||||
import { CommentsService } from '../../services/comments.service';
|
import { CommentsService } from '../../services/comments.service';
|
||||||
import { SanitizeService } from '../../services/sanitize.service';
|
import { SanitizeService } from '../../services/sanitize.service';
|
||||||
import { SuggestionService } from '../../services/suggestion.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({
|
@Component({
|
||||||
selector: 'app-shows-list',
|
selector: 'app-shows-list',
|
||||||
@@ -110,6 +110,52 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of newShow.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'new')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newTagInput"
|
||||||
|
name="newTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('new'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of newShow.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'new')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="newLinkTitle"
|
||||||
|
name="newLinkTitle"
|
||||||
|
placeholder="Link title (e.g., IMDB)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="newLinkUrl"
|
||||||
|
name="newLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('new')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Show</button>
|
<button type="submit" class="btn btn-primary">Add Show</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -194,6 +240,52 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tags</label>
|
||||||
|
<div class="tags-input-container">
|
||||||
|
@for (tag of editShow.tags; track tag; let i = $index) {
|
||||||
|
<span class="tag">
|
||||||
|
{{ tag }}
|
||||||
|
<button type="button" (click)="removeTag(i, 'edit')" class="tag-remove">×</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editTagInput"
|
||||||
|
name="editTagInput"
|
||||||
|
placeholder="Add a tag and press Enter"
|
||||||
|
(keydown.enter)="addTag('edit'); $event.preventDefault()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>External Links</label>
|
||||||
|
<div class="links-list">
|
||||||
|
@for (link of editShow.links; track link.url; let i = $index) {
|
||||||
|
<div class="link-item">
|
||||||
|
<span>{{ link.title }}: {{ link.url }}</span>
|
||||||
|
<button type="button" (click)="removeLink(i, 'edit')" class="btn btn-danger btn-sm">×</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="link-add-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="editLinkTitle"
|
||||||
|
name="editLinkTitle"
|
||||||
|
placeholder="Link title (e.g., IMDB)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
[(ngModel)]="editLinkUrl"
|
||||||
|
name="editLinkUrl"
|
||||||
|
placeholder="https://..."
|
||||||
|
>
|
||||||
|
<button type="button" (click)="addLink('edit')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -329,6 +421,24 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
<p class="notes">{{ show.notes }}</p>
|
<p class="notes">{{ show.notes }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (show.tags && show.tags.length > 0) {
|
||||||
|
<div class="tags-display">
|
||||||
|
@for (tag of show.tags; track tag) {
|
||||||
|
<span class="tag-chip">{{ tag }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (show.links && show.links.length > 0) {
|
||||||
|
<div class="links-display">
|
||||||
|
@for (link of show.links; track link.url) {
|
||||||
|
<a [href]="link.url" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||||
|
{{ link.title }} ↗
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (authService.isAdmin()) {
|
@if (authService.isAdmin()) {
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button (click)="startEdit(show)" class="btn btn-secondary btn-sm">
|
<button (click)="startEdit(show)" class="btn btn-secondary btn-sm">
|
||||||
@@ -794,6 +904,116 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
input[type="file"]:hover {
|
input[type="file"]:hover {
|
||||||
border-color: #8b5cf6;
|
border-color: #8b5cf6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #e84393;
|
||||||
|
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(232, 67, 147, 0.2);
|
||||||
|
color: #e84393;
|
||||||
|
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: #f9fafb;
|
||||||
|
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: #e84393;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
background: rgba(232, 67, 147, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-link:hover {
|
||||||
|
background: rgba(232, 67, 147, 0.2);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class ShowsListComponent implements OnInit {
|
export class ShowsListComponent implements OnInit {
|
||||||
@@ -851,11 +1071,21 @@ export class ShowsListComponent implements OnInit {
|
|||||||
type: ShowType.tvSeries,
|
type: ShowType.tvSeries,
|
||||||
status: ShowStatus.wantToWatch,
|
status: ShowStatus.wantToWatch,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: ''
|
notes: '',
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
|
|
||||||
editShow: Partial<UpdateShowDto> = {};
|
editShow: Partial<UpdateShowDto> = {};
|
||||||
|
|
||||||
|
// Tags and links input state
|
||||||
|
newTagInput = '';
|
||||||
|
editTagInput = '';
|
||||||
|
newLinkTitle = '';
|
||||||
|
newLinkUrl = '';
|
||||||
|
editLinkTitle = '';
|
||||||
|
editLinkUrl = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadShows();
|
this.loadShows();
|
||||||
}
|
}
|
||||||
@@ -908,10 +1138,60 @@ export class ShowsListComponent implements OnInit {
|
|||||||
status: ShowStatus.wantToWatch,
|
status: ShowStatus.wantToWatch,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverImage: undefined
|
coverImage: undefined,
|
||||||
|
tags: [],
|
||||||
|
links: []
|
||||||
};
|
};
|
||||||
this.newShowImagePreview.set(null);
|
this.newShowImagePreview.set(null);
|
||||||
this.imageError.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.newShow.tags = [...(this.newShow.tags || []), input];
|
||||||
|
this.newTagInput = '';
|
||||||
|
} else {
|
||||||
|
this.editShow.tags = [...(this.editShow.tags || []), input];
|
||||||
|
this.editTagInput = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTag(index: number, target: 'new' | 'edit') {
|
||||||
|
if (target === 'new') {
|
||||||
|
this.newShow.tags = (this.newShow.tags || []).filter((_, i) => i !== index);
|
||||||
|
} else {
|
||||||
|
this.editShow.tags = (this.editShow.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.newShow.links = [...(this.newShow.links || []), { title, url }];
|
||||||
|
this.newLinkTitle = '';
|
||||||
|
this.newLinkUrl = '';
|
||||||
|
} else {
|
||||||
|
this.editShow.links = [...(this.editShow.links || []), { title, url }];
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLink(index: number, target: 'new' | 'edit') {
|
||||||
|
if (target === 'new') {
|
||||||
|
this.newShow.links = (this.newShow.links || []).filter((_, i) => i !== index);
|
||||||
|
} else {
|
||||||
|
this.editShow.links = (this.editShow.links || []).filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addShow() {
|
addShow() {
|
||||||
@@ -923,7 +1203,9 @@ export class ShowsListComponent implements OnInit {
|
|||||||
status: this.newShow.status,
|
status: this.newShow.status,
|
||||||
rating: this.newShow.rating,
|
rating: this.newShow.rating,
|
||||||
notes: this.newShow.notes,
|
notes: this.newShow.notes,
|
||||||
coverImage: this.newShow.coverImage
|
coverImage: this.newShow.coverImage,
|
||||||
|
tags: this.newShow.tags || [],
|
||||||
|
links: this.newShow.links || []
|
||||||
};
|
};
|
||||||
|
|
||||||
this.showsService.createShow(showToAdd).subscribe(() => {
|
this.showsService.createShow(showToAdd).subscribe(() => {
|
||||||
@@ -948,11 +1230,16 @@ export class ShowsListComponent implements OnInit {
|
|||||||
status: show.status,
|
status: show.status,
|
||||||
rating: show.rating,
|
rating: show.rating,
|
||||||
notes: show.notes,
|
notes: show.notes,
|
||||||
coverImage: show.coverImage
|
coverImage: show.coverImage,
|
||||||
|
tags: [...(show.tags || [])],
|
||||||
|
links: [...(show.links || [])]
|
||||||
};
|
};
|
||||||
this.editShowImagePreview.set(show.coverImage || null);
|
this.editShowImagePreview.set(show.coverImage || null);
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
@@ -960,6 +1247,9 @@ export class ShowsListComponent implements OnInit {
|
|||||||
this.editShow = {};
|
this.editShow = {};
|
||||||
this.editShowImagePreview.set(null);
|
this.editShowImagePreview.set(null);
|
||||||
this.imageError.set(null);
|
this.imageError.set(null);
|
||||||
|
this.editTagInput = '';
|
||||||
|
this.editLinkTitle = '';
|
||||||
|
this.editLinkUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ export type * from "./lib/auth.types";
|
|||||||
export * from "./lib/comment.types";
|
export * from "./lib/comment.types";
|
||||||
export * from "./lib/audit.types";
|
export * from "./lib/audit.types";
|
||||||
export * from "./lib/suggestion.types";
|
export * from "./lib/suggestion.types";
|
||||||
|
export * from "./lib/common.types";
|
||||||
@@ -3,12 +3,16 @@
|
|||||||
* @license Naomi's Public License
|
* @license Naomi's Public License
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Link } from "./common.types";
|
||||||
|
|
||||||
export interface Art {
|
export interface Art {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
|
tags: string[];
|
||||||
|
links: Link[];
|
||||||
dateAdded: Date;
|
dateAdded: Date;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -19,6 +23,8 @@ export interface CreateArtDto {
|
|||||||
artist: string;
|
artist: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
|
tags?: string[];
|
||||||
|
links?: Link[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateArtDto extends Partial<CreateArtDto> {}
|
export interface UpdateArtDto extends Partial<CreateArtDto> {}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export enum BookStatus {
|
|||||||
toRead = "TO_READ",
|
toRead = "TO_READ",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { Link } from "./common.types";
|
||||||
|
|
||||||
export interface Book {
|
export interface Book {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -21,6 +23,8 @@ export interface Book {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
tags: string[];
|
||||||
|
links: Link[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -33,6 +37,8 @@ export interface CreateBookDto {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
tags?: string[];
|
||||||
|
links?: Link[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateBookDto extends Partial<CreateBookDto> {
|
export interface UpdateBookDto extends Partial<CreateBookDto> {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Link {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ export enum GameStatus {
|
|||||||
backlog = "BACKLOG",
|
backlog = "BACKLOG",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { Link } from "./common.types";
|
||||||
|
|
||||||
export interface Game {
|
export interface Game {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -20,6 +22,8 @@ export interface Game {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
tags: string[];
|
||||||
|
links: Link[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -31,6 +35,8 @@ export interface CreateGameDto {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
tags?: string[];
|
||||||
|
links?: Link[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateGameDto extends Partial<CreateGameDto> {
|
export interface UpdateGameDto extends Partial<CreateGameDto> {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export enum MangaStatus {
|
|||||||
wantToRead = "WANT_TO_READ",
|
wantToRead = "WANT_TO_READ",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { Link } from "./common.types";
|
||||||
|
|
||||||
export interface Manga {
|
export interface Manga {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -20,6 +22,8 @@ export interface Manga {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
tags: string[];
|
||||||
|
links: Link[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -31,6 +35,8 @@ export interface CreateMangaDto {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
tags?: string[];
|
||||||
|
links?: Link[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateMangaDto extends Partial<CreateMangaDto> {
|
export interface UpdateMangaDto extends Partial<CreateMangaDto> {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export enum MusicStatus {
|
|||||||
wantToListen = "WANT_TO_LISTEN",
|
wantToListen = "WANT_TO_LISTEN",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { Link } from "./common.types";
|
||||||
|
|
||||||
export interface Music {
|
export interface Music {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -27,6 +29,8 @@ export interface Music {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverArt?: string;
|
coverArt?: string;
|
||||||
|
tags: string[];
|
||||||
|
links: Link[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -39,6 +43,8 @@ export interface CreateMusicDto {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverArt?: string;
|
coverArt?: string;
|
||||||
|
tags?: string[];
|
||||||
|
links?: Link[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateMusicDto extends Partial<CreateMusicDto> {
|
export interface UpdateMusicDto extends Partial<CreateMusicDto> {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export enum ShowStatus {
|
|||||||
wantToWatch = "WANT_TO_WATCH",
|
wantToWatch = "WANT_TO_WATCH",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { Link } from "./common.types";
|
||||||
|
|
||||||
export interface Show {
|
export interface Show {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -27,6 +29,8 @@ export interface Show {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
tags: string[];
|
||||||
|
links: Link[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -38,6 +42,8 @@ export interface CreateShowDto {
|
|||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
tags?: string[];
|
||||||
|
links?: Link[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateShowDto extends Partial<CreateShowDto> {
|
export interface UpdateShowDto extends Partial<CreateShowDto> {
|
||||||
|
|||||||
Reference in New Issue
Block a user