generated from nhcarrigan/template
feat: multiple improvements to library functionality #50
@@ -24,7 +24,9 @@ model Game {
|
|||||||
platform String?
|
platform String?
|
||||||
status GameStatus
|
status GameStatus
|
||||||
dateAdded DateTime @default(now())
|
dateAdded DateTime @default(now())
|
||||||
|
dateStarted DateTime?
|
||||||
dateCompleted DateTime?
|
dateCompleted DateTime?
|
||||||
|
dateFinished DateTime?
|
||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverImage String?
|
coverImage String?
|
||||||
@@ -48,6 +50,7 @@ model Book {
|
|||||||
isbn String?
|
isbn String?
|
||||||
status BookStatus
|
status BookStatus
|
||||||
dateAdded DateTime @default(now())
|
dateAdded DateTime @default(now())
|
||||||
|
dateStarted DateTime?
|
||||||
dateFinished DateTime?
|
dateFinished DateTime?
|
||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
@@ -72,7 +75,9 @@ model Music {
|
|||||||
type MusicType
|
type MusicType
|
||||||
status MusicStatus
|
status MusicStatus
|
||||||
dateAdded DateTime @default(now())
|
dateAdded DateTime @default(now())
|
||||||
|
dateStarted DateTime?
|
||||||
dateCompleted DateTime?
|
dateCompleted DateTime?
|
||||||
|
dateFinished DateTime?
|
||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverArt String?
|
coverArt String?
|
||||||
@@ -115,7 +120,9 @@ model Show {
|
|||||||
type ShowType
|
type ShowType
|
||||||
status ShowStatus
|
status ShowStatus
|
||||||
dateAdded DateTime @default(now())
|
dateAdded DateTime @default(now())
|
||||||
|
dateStarted DateTime?
|
||||||
dateCompleted DateTime?
|
dateCompleted DateTime?
|
||||||
|
dateFinished DateTime?
|
||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverImage String?
|
coverImage String?
|
||||||
@@ -145,7 +152,9 @@ model Manga {
|
|||||||
author String
|
author String
|
||||||
status MangaStatus
|
status MangaStatus
|
||||||
dateAdded DateTime @default(now())
|
dateAdded DateTime @default(now())
|
||||||
|
dateStarted DateTime?
|
||||||
dateCompleted DateTime?
|
dateCompleted DateTime?
|
||||||
|
dateFinished DateTime?
|
||||||
rating Int? @db.Int @default(0)
|
rating Int? @db.Int @default(0)
|
||||||
notes String?
|
notes String?
|
||||||
coverImage String?
|
coverImage String?
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export class BookService {
|
|||||||
...book,
|
...book,
|
||||||
status: book.status as unknown as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
|
dateStarted: book.dateStarted || undefined,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
tags: book.tags ?? [],
|
tags: book.tags ?? [],
|
||||||
links: book.links ?? [],
|
links: book.links ?? [],
|
||||||
@@ -46,6 +47,7 @@ export class BookService {
|
|||||||
...book,
|
...book,
|
||||||
status: book.status as unknown as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
|
dateStarted: book.dateStarted || undefined,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
tags: book.tags ?? [],
|
tags: book.tags ?? [],
|
||||||
links: book.links ?? [],
|
links: book.links ?? [],
|
||||||
@@ -69,6 +71,7 @@ export class BookService {
|
|||||||
...book,
|
...book,
|
||||||
status: book.status as unknown as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
|
dateStarted: book.dateStarted || undefined,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
tags: book.tags ?? [],
|
tags: book.tags ?? [],
|
||||||
links: book.links ?? [],
|
links: book.links ?? [],
|
||||||
@@ -95,6 +98,7 @@ export class BookService {
|
|||||||
...book,
|
...book,
|
||||||
status: book.status as unknown as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
|
dateStarted: book.dateStarted || undefined,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
tags: book.tags ?? [],
|
tags: book.tags ?? [],
|
||||||
links: book.links ?? [],
|
links: book.links ?? [],
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export class GameService {
|
|||||||
...game,
|
...game,
|
||||||
status: game.status as unknown as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
|
dateStarted: game.dateStarted || undefined,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
|
dateFinished: game.dateFinished || undefined,
|
||||||
tags: game.tags ?? [],
|
tags: game.tags ?? [],
|
||||||
links: game.links ?? [],
|
links: game.links ?? [],
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
@@ -46,7 +48,9 @@ export class GameService {
|
|||||||
...game,
|
...game,
|
||||||
status: game.status as unknown as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
|
dateStarted: game.dateStarted || undefined,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
|
dateFinished: game.dateFinished || undefined,
|
||||||
tags: game.tags ?? [],
|
tags: game.tags ?? [],
|
||||||
links: game.links ?? [],
|
links: game.links ?? [],
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
@@ -69,7 +73,9 @@ export class GameService {
|
|||||||
...game,
|
...game,
|
||||||
status: game.status as unknown as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
|
dateStarted: game.dateStarted || undefined,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
|
dateFinished: game.dateFinished || undefined,
|
||||||
tags: game.tags ?? [],
|
tags: game.tags ?? [],
|
||||||
links: game.links ?? [],
|
links: game.links ?? [],
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
@@ -95,7 +101,9 @@ export class GameService {
|
|||||||
...game,
|
...game,
|
||||||
status: game.status as unknown as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
|
dateStarted: game.dateStarted || undefined,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
|
dateFinished: game.dateFinished || undefined,
|
||||||
tags: game.tags ?? [],
|
tags: game.tags ?? [],
|
||||||
links: game.links ?? [],
|
links: game.links ?? [],
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ export class MangaService {
|
|||||||
...m,
|
...m,
|
||||||
status: m.status as unknown as MangaStatus,
|
status: m.status as unknown as MangaStatus,
|
||||||
dateAdded: m.dateAdded,
|
dateAdded: m.dateAdded,
|
||||||
|
dateStarted: m.dateStarted || undefined,
|
||||||
dateCompleted: m.dateCompleted || undefined,
|
dateCompleted: m.dateCompleted || undefined,
|
||||||
|
dateFinished: m.dateFinished || undefined,
|
||||||
tags: m.tags ?? [],
|
tags: m.tags ?? [],
|
||||||
links: m.links ?? [],
|
links: m.links ?? [],
|
||||||
createdAt: m.createdAt,
|
createdAt: m.createdAt,
|
||||||
@@ -40,7 +42,9 @@ export class MangaService {
|
|||||||
...manga,
|
...manga,
|
||||||
status: manga.status as unknown as MangaStatus,
|
status: manga.status as unknown as MangaStatus,
|
||||||
dateAdded: manga.dateAdded,
|
dateAdded: manga.dateAdded,
|
||||||
|
dateStarted: manga.dateStarted || undefined,
|
||||||
dateCompleted: manga.dateCompleted || undefined,
|
dateCompleted: manga.dateCompleted || undefined,
|
||||||
|
dateFinished: manga.dateFinished || undefined,
|
||||||
tags: manga.tags ?? [],
|
tags: manga.tags ?? [],
|
||||||
links: manga.links ?? [],
|
links: manga.links ?? [],
|
||||||
createdAt: manga.createdAt,
|
createdAt: manga.createdAt,
|
||||||
@@ -60,7 +64,9 @@ export class MangaService {
|
|||||||
...manga,
|
...manga,
|
||||||
status: manga.status as unknown as MangaStatus,
|
status: manga.status as unknown as MangaStatus,
|
||||||
dateAdded: manga.dateAdded,
|
dateAdded: manga.dateAdded,
|
||||||
|
dateStarted: manga.dateStarted || undefined,
|
||||||
dateCompleted: manga.dateCompleted || undefined,
|
dateCompleted: manga.dateCompleted || undefined,
|
||||||
|
dateFinished: manga.dateFinished || undefined,
|
||||||
tags: manga.tags ?? [],
|
tags: manga.tags ?? [],
|
||||||
links: manga.links ?? [],
|
links: manga.links ?? [],
|
||||||
createdAt: manga.createdAt,
|
createdAt: manga.createdAt,
|
||||||
@@ -83,7 +89,9 @@ export class MangaService {
|
|||||||
...manga,
|
...manga,
|
||||||
status: manga.status as unknown as MangaStatus,
|
status: manga.status as unknown as MangaStatus,
|
||||||
dateAdded: manga.dateAdded,
|
dateAdded: manga.dateAdded,
|
||||||
|
dateStarted: manga.dateStarted || undefined,
|
||||||
dateCompleted: manga.dateCompleted || undefined,
|
dateCompleted: manga.dateCompleted || undefined,
|
||||||
|
dateFinished: manga.dateFinished || undefined,
|
||||||
tags: manga.tags ?? [],
|
tags: manga.tags ?? [],
|
||||||
links: manga.links ?? [],
|
links: manga.links ?? [],
|
||||||
createdAt: manga.createdAt,
|
createdAt: manga.createdAt,
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export class MusicService {
|
|||||||
type: music.type as unknown as MusicType,
|
type: music.type as unknown as MusicType,
|
||||||
status: music.status as unknown as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
|
dateStarted: music.dateStarted || undefined,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
|
dateFinished: music.dateFinished || undefined,
|
||||||
tags: music.tags ?? [],
|
tags: music.tags ?? [],
|
||||||
links: music.links ?? [],
|
links: music.links ?? [],
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
@@ -48,7 +50,9 @@ export class MusicService {
|
|||||||
type: music.type as unknown as MusicType,
|
type: music.type as unknown as MusicType,
|
||||||
status: music.status as unknown as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
|
dateStarted: music.dateStarted || undefined,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
|
dateFinished: music.dateFinished || undefined,
|
||||||
tags: music.tags ?? [],
|
tags: music.tags ?? [],
|
||||||
links: music.links ?? [],
|
links: music.links ?? [],
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
@@ -73,7 +77,9 @@ export class MusicService {
|
|||||||
type: music.type as unknown as MusicType,
|
type: music.type as unknown as MusicType,
|
||||||
status: music.status as unknown as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
|
dateStarted: music.dateStarted || undefined,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
|
dateFinished: music.dateFinished || undefined,
|
||||||
tags: music.tags ?? [],
|
tags: music.tags ?? [],
|
||||||
links: music.links ?? [],
|
links: music.links ?? [],
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
@@ -103,7 +109,9 @@ export class MusicService {
|
|||||||
type: music.type as unknown as MusicType,
|
type: music.type as unknown as MusicType,
|
||||||
status: music.status as unknown as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
|
dateStarted: music.dateStarted || undefined,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
|
dateFinished: music.dateFinished || undefined,
|
||||||
tags: music.tags ?? [],
|
tags: music.tags ?? [],
|
||||||
links: music.links ?? [],
|
links: music.links ?? [],
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ export class ShowService {
|
|||||||
type: show.type as unknown as ShowType,
|
type: show.type as unknown as ShowType,
|
||||||
status: show.status as unknown as ShowStatus,
|
status: show.status as unknown as ShowStatus,
|
||||||
dateAdded: show.dateAdded,
|
dateAdded: show.dateAdded,
|
||||||
|
dateStarted: show.dateStarted || undefined,
|
||||||
dateCompleted: show.dateCompleted || undefined,
|
dateCompleted: show.dateCompleted || undefined,
|
||||||
|
dateFinished: show.dateFinished || undefined,
|
||||||
tags: show.tags ?? [],
|
tags: show.tags ?? [],
|
||||||
links: show.links ?? [],
|
links: show.links ?? [],
|
||||||
createdAt: show.createdAt,
|
createdAt: show.createdAt,
|
||||||
@@ -42,7 +44,9 @@ export class ShowService {
|
|||||||
type: show.type as unknown as ShowType,
|
type: show.type as unknown as ShowType,
|
||||||
status: show.status as unknown as ShowStatus,
|
status: show.status as unknown as ShowStatus,
|
||||||
dateAdded: show.dateAdded,
|
dateAdded: show.dateAdded,
|
||||||
|
dateStarted: show.dateStarted || undefined,
|
||||||
dateCompleted: show.dateCompleted || undefined,
|
dateCompleted: show.dateCompleted || undefined,
|
||||||
|
dateFinished: show.dateFinished || undefined,
|
||||||
tags: show.tags ?? [],
|
tags: show.tags ?? [],
|
||||||
links: show.links ?? [],
|
links: show.links ?? [],
|
||||||
createdAt: show.createdAt,
|
createdAt: show.createdAt,
|
||||||
@@ -64,7 +68,9 @@ export class ShowService {
|
|||||||
type: show.type as unknown as ShowType,
|
type: show.type as unknown as ShowType,
|
||||||
status: show.status as unknown as ShowStatus,
|
status: show.status as unknown as ShowStatus,
|
||||||
dateAdded: show.dateAdded,
|
dateAdded: show.dateAdded,
|
||||||
|
dateStarted: show.dateStarted || undefined,
|
||||||
dateCompleted: show.dateCompleted || undefined,
|
dateCompleted: show.dateCompleted || undefined,
|
||||||
|
dateFinished: show.dateFinished || undefined,
|
||||||
tags: show.tags ?? [],
|
tags: show.tags ?? [],
|
||||||
links: show.links ?? [],
|
links: show.links ?? [],
|
||||||
createdAt: show.createdAt,
|
createdAt: show.createdAt,
|
||||||
@@ -91,7 +97,9 @@ export class ShowService {
|
|||||||
type: show.type as unknown as ShowType,
|
type: show.type as unknown as ShowType,
|
||||||
status: show.status as unknown as ShowStatus,
|
status: show.status as unknown as ShowStatus,
|
||||||
dateAdded: show.dateAdded,
|
dateAdded: show.dateAdded,
|
||||||
|
dateStarted: show.dateStarted || undefined,
|
||||||
dateCompleted: show.dateCompleted || undefined,
|
dateCompleted: show.dateCompleted || undefined,
|
||||||
|
dateFinished: show.dateFinished || undefined,
|
||||||
tags: show.tags ?? [],
|
tags: show.tags ?? [],
|
||||||
links: show.links ?? [],
|
links: show.links ?? [],
|
||||||
createdAt: show.createdAt,
|
createdAt: show.createdAt,
|
||||||
|
|||||||
@@ -39,22 +39,22 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
All ({{ suggestions().length }})
|
All ({{ suggestions().length }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(SuggestionStatus.UNREVIEWED)"
|
(click)="setFilter(SuggestionStatus.unreviewed)"
|
||||||
[class.active]="statusFilter() === SuggestionStatus.UNREVIEWED"
|
[class.active]="statusFilter() === SuggestionStatus.unreviewed"
|
||||||
class="filter-btn pending"
|
class="filter-btn pending"
|
||||||
>
|
>
|
||||||
Pending ({{ unreviewedCount() }})
|
Pending ({{ unreviewedCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(SuggestionStatus.ACCEPTED)"
|
(click)="setFilter(SuggestionStatus.accepted)"
|
||||||
[class.active]="statusFilter() === SuggestionStatus.ACCEPTED"
|
[class.active]="statusFilter() === SuggestionStatus.accepted"
|
||||||
class="filter-btn accepted"
|
class="filter-btn accepted"
|
||||||
>
|
>
|
||||||
Accepted ({{ acceptedCount() }})
|
Accepted ({{ acceptedCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(SuggestionStatus.DECLINED)"
|
(click)="setFilter(SuggestionStatus.declined)"
|
||||||
[class.active]="statusFilter() === SuggestionStatus.DECLINED"
|
[class.active]="statusFilter() === SuggestionStatus.declined"
|
||||||
class="filter-btn declined"
|
class="filter-btn declined"
|
||||||
>
|
>
|
||||||
Declined ({{ declinedCount() }})
|
Declined ({{ declinedCount() }})
|
||||||
@@ -171,7 +171,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (suggestion.status === SuggestionStatus.DECLINED && suggestion.declineReason) {
|
@if (suggestion.status === SuggestionStatus.declined && suggestion.declineReason) {
|
||||||
<div class="decline-reason">
|
<div class="decline-reason">
|
||||||
<strong>Decline reason:</strong> {{ suggestion.declineReason }}
|
<strong>Decline reason:</strong> {{ suggestion.declineReason }}
|
||||||
</div>
|
</div>
|
||||||
@@ -180,7 +180,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
<div class="suggestion-footer">
|
<div class="suggestion-footer">
|
||||||
<span class="date">Suggested on {{ formatDate(suggestion.createdAt) }}</span>
|
<span class="date">Suggested on {{ formatDate(suggestion.createdAt) }}</span>
|
||||||
|
|
||||||
@if (suggestion.status === SuggestionStatus.UNREVIEWED) {
|
@if (suggestion.status === SuggestionStatus.unreviewed) {
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button (click)="acceptSuggestion(suggestion)" class="btn btn-accept">
|
<button (click)="acceptSuggestion(suggestion)" class="btn btn-accept">
|
||||||
Accept
|
Accept
|
||||||
@@ -248,7 +248,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@switch (editingSuggestion()!.entityType) {
|
@switch (editingSuggestion()!.entityType) {
|
||||||
@case ('BOOK') {
|
@case (SuggestionEntity.book) {
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-author">Author</label>
|
<label for="edit-author">Author</label>
|
||||||
<input
|
<input
|
||||||
@@ -269,7 +269,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@case ('GAME') {
|
@case (SuggestionEntity.game) {
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-platform">Platform</label>
|
<label for="edit-platform">Platform</label>
|
||||||
<input
|
<input
|
||||||
@@ -280,7 +280,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@case ('MUSIC') {
|
@case (SuggestionEntity.music) {
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-artist">Artist</label>
|
<label for="edit-artist">Artist</label>
|
||||||
<input
|
<input
|
||||||
@@ -300,7 +300,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@case ('ART') {
|
@case (SuggestionEntity.art) {
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-artist">Artist</label>
|
<label for="edit-artist">Artist</label>
|
||||||
<input
|
<input
|
||||||
@@ -331,7 +331,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@case ('SHOW') {
|
@case (SuggestionEntity.show) {
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-type">Type</label>
|
<label for="edit-type">Type</label>
|
||||||
<select id="edit-type" [(ngModel)]="editedData.type" name="type" required>
|
<select id="edit-type" [(ngModel)]="editedData.type" name="type" required>
|
||||||
@@ -342,7 +342,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@case ('MANGA') {
|
@case (SuggestionEntity.manga) {
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-author">Author</label>
|
<label for="edit-author">Author</label>
|
||||||
<input
|
<input
|
||||||
@@ -366,7 +366,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (editingSuggestion()!.entityType !== 'ART') {
|
@if (editingSuggestion()!.entityType !== SuggestionEntity.art) {
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-coverImage">Cover Image URL</label>
|
<label for="edit-coverImage">Cover Image URL</label>
|
||||||
<input
|
<input
|
||||||
@@ -727,10 +727,11 @@ export class AdminSuggestionsComponent implements OnInit {
|
|||||||
pageSize = signal(25);
|
pageSize = signal(25);
|
||||||
|
|
||||||
SuggestionStatus = SuggestionStatus;
|
SuggestionStatus = SuggestionStatus;
|
||||||
|
SuggestionEntity = SuggestionEntity;
|
||||||
|
|
||||||
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.UNREVIEWED).length;
|
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.unreviewed).length;
|
||||||
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.ACCEPTED).length;
|
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.accepted).length;
|
||||||
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.DECLINED).length;
|
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.declined).length;
|
||||||
|
|
||||||
filteredSuggestions = computed(() => {
|
filteredSuggestions = computed(() => {
|
||||||
const filter = this.statusFilter();
|
const filter = this.statusFilter();
|
||||||
@@ -788,20 +789,20 @@ export class AdminSuggestionsComponent implements OnInit {
|
|||||||
|
|
||||||
getStatusLabel(status: SuggestionStatus): string {
|
getStatusLabel(status: SuggestionStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case SuggestionStatus.UNREVIEWED: return 'Pending';
|
case SuggestionStatus.unreviewed: return 'Pending';
|
||||||
case SuggestionStatus.ACCEPTED: return 'Accepted';
|
case SuggestionStatus.accepted: return 'Accepted';
|
||||||
case SuggestionStatus.DECLINED: return 'Declined';
|
case SuggestionStatus.declined: return 'Declined';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntityIcon(entityType: SuggestionEntity): string {
|
getEntityIcon(entityType: SuggestionEntity): string {
|
||||||
switch (entityType) {
|
switch (entityType) {
|
||||||
case SuggestionEntity.GAME: return '🎮';
|
case SuggestionEntity.game: return '🎮';
|
||||||
case SuggestionEntity.BOOK: return '📚';
|
case SuggestionEntity.book: return '📚';
|
||||||
case SuggestionEntity.MUSIC: return '🎵';
|
case SuggestionEntity.music: return '🎵';
|
||||||
case SuggestionEntity.MANGA: return 'đź“–';
|
case SuggestionEntity.manga: return 'đź“–';
|
||||||
case SuggestionEntity.SHOW: return '📺';
|
case SuggestionEntity.show: return '📺';
|
||||||
case SuggestionEntity.ART: return '🎨';
|
case SuggestionEntity.art: return '🎨';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,39 +823,39 @@ export class AdminSuggestionsComponent implements OnInit {
|
|||||||
|
|
||||||
// Add entity-specific data
|
// Add entity-specific data
|
||||||
switch (suggestion.entityType) {
|
switch (suggestion.entityType) {
|
||||||
case 'BOOK':
|
case SuggestionEntity.book:
|
||||||
const bookData = suggestion.bookData as any;
|
const bookData = suggestion.bookData as any;
|
||||||
this.editedData.author = bookData?.author || '';
|
this.editedData.author = bookData?.author || '';
|
||||||
this.editedData.isbn = bookData?.isbn || '';
|
this.editedData.isbn = bookData?.isbn || '';
|
||||||
this.editedData.notes = bookData?.notes || '';
|
this.editedData.notes = bookData?.notes || '';
|
||||||
this.editedData.coverImage = bookData?.coverImage || '';
|
this.editedData.coverImage = bookData?.coverImage || '';
|
||||||
break;
|
break;
|
||||||
case 'GAME':
|
case SuggestionEntity.game:
|
||||||
const gameData = suggestion.gameData as any;
|
const gameData = suggestion.gameData as any;
|
||||||
this.editedData.platform = gameData?.platform || '';
|
this.editedData.platform = gameData?.platform || '';
|
||||||
this.editedData.notes = gameData?.notes || '';
|
this.editedData.notes = gameData?.notes || '';
|
||||||
this.editedData.coverImage = gameData?.coverImage || '';
|
this.editedData.coverImage = gameData?.coverImage || '';
|
||||||
break;
|
break;
|
||||||
case 'MUSIC':
|
case SuggestionEntity.music:
|
||||||
const musicData = suggestion.musicData as any;
|
const musicData = suggestion.musicData as any;
|
||||||
this.editedData.artist = musicData?.artist || '';
|
this.editedData.artist = musicData?.artist || '';
|
||||||
this.editedData.type = musicData?.type || 'ALBUM';
|
this.editedData.type = musicData?.type || 'ALBUM';
|
||||||
this.editedData.notes = musicData?.notes || '';
|
this.editedData.notes = musicData?.notes || '';
|
||||||
this.editedData.coverArt = musicData?.coverArt || '';
|
this.editedData.coverArt = musicData?.coverArt || '';
|
||||||
break;
|
break;
|
||||||
case 'ART':
|
case SuggestionEntity.art:
|
||||||
const artData = suggestion.artData as any;
|
const artData = suggestion.artData as any;
|
||||||
this.editedData.artist = artData?.artist || '';
|
this.editedData.artist = artData?.artist || '';
|
||||||
this.editedData.description = artData?.description || '';
|
this.editedData.description = artData?.description || '';
|
||||||
this.editedData.imageUrl = artData?.imageUrl || '';
|
this.editedData.imageUrl = artData?.imageUrl || '';
|
||||||
break;
|
break;
|
||||||
case 'SHOW':
|
case SuggestionEntity.show:
|
||||||
const showData = suggestion.showData as any;
|
const showData = suggestion.showData as any;
|
||||||
this.editedData.type = showData?.type || 'TV_SERIES';
|
this.editedData.type = showData?.type || 'TV_SERIES';
|
||||||
this.editedData.notes = showData?.notes || '';
|
this.editedData.notes = showData?.notes || '';
|
||||||
this.editedData.coverImage = showData?.coverImage || '';
|
this.editedData.coverImage = showData?.coverImage || '';
|
||||||
break;
|
break;
|
||||||
case 'MANGA':
|
case SuggestionEntity.manga:
|
||||||
const mangaData = suggestion.mangaData as any;
|
const mangaData = suggestion.mangaData as any;
|
||||||
this.editedData.author = mangaData?.author || '';
|
this.editedData.author = mangaData?.author || '';
|
||||||
this.editedData.notes = mangaData?.notes || '';
|
this.editedData.notes = mangaData?.notes || '';
|
||||||
|
|||||||
@@ -1571,7 +1571,7 @@ export class ArtGalleryComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.suggestionService.createSuggestion({
|
await this.suggestionService.createSuggestion({
|
||||||
entityType: SuggestionEntity.ART,
|
entityType: SuggestionEntity.art,
|
||||||
title: this.suggestedArt.title,
|
title: this.suggestedArt.title,
|
||||||
artist: this.suggestedArt.artist,
|
artist: this.suggestedArt.artist,
|
||||||
imageUrl: this.suggestedArt.imageUrl,
|
imageUrl: this.suggestedArt.imageUrl,
|
||||||
|
|||||||
@@ -82,6 +82,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateStarted"
|
||||||
|
[(ngModel)]="newBook.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateFinished"
|
||||||
|
[(ngModel)]="newBook.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="rating">Rating (1-10)</label>
|
<label for="rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -223,6 +243,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateStarted"
|
||||||
|
[(ngModel)]="editBook.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateFinished"
|
||||||
|
[(ngModel)]="editBook.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-rating">Rating (1-10)</label>
|
<label for="edit-rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -543,6 +583,12 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (book.dateStarted) {
|
||||||
|
<p class="date-started">
|
||||||
|
Started: {{ formatDate(book.dateStarted) }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
@if (book.dateFinished) {
|
@if (book.dateFinished) {
|
||||||
<p class="date-finished">
|
<p class="date-finished">
|
||||||
Finished: {{ formatDate(book.dateFinished) }}
|
Finished: {{ formatDate(book.dateFinished) }}
|
||||||
@@ -984,6 +1030,7 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-started,
|
||||||
.date-finished {
|
.date-finished {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--witch-plum);
|
color: var(--witch-plum);
|
||||||
@@ -1462,11 +1509,13 @@ export class BooksListComponent implements OnInit {
|
|||||||
|
|
||||||
totalFilteredBooks = computed(() => this.filteredBooks().length);
|
totalFilteredBooks = computed(() => this.filteredBooks().length);
|
||||||
|
|
||||||
newBook: Partial<CreateBookDto> = {
|
newBook: Partial<CreateBookDto> & { dateStarted?: Date; dateFinished?: Date } = {
|
||||||
title: '',
|
title: '',
|
||||||
author: '',
|
author: '',
|
||||||
isbn: '',
|
isbn: '',
|
||||||
status: BookStatus.toRead,
|
status: BookStatus.toRead,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -1563,6 +1612,8 @@ export class BooksListComponent implements OnInit {
|
|||||||
author: '',
|
author: '',
|
||||||
isbn: '',
|
isbn: '',
|
||||||
status: BookStatus.toRead,
|
status: BookStatus.toRead,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverImage: undefined,
|
coverImage: undefined,
|
||||||
@@ -1629,6 +1680,8 @@ export class BooksListComponent implements OnInit {
|
|||||||
author: this.newBook.author,
|
author: this.newBook.author,
|
||||||
isbn: this.newBook.isbn,
|
isbn: this.newBook.isbn,
|
||||||
status: this.newBook.status,
|
status: this.newBook.status,
|
||||||
|
dateStarted: this.newBook.dateStarted ? new Date(this.newBook.dateStarted) : undefined,
|
||||||
|
dateFinished: this.newBook.dateFinished ? new Date(this.newBook.dateFinished) : undefined,
|
||||||
rating: this.newBook.rating,
|
rating: this.newBook.rating,
|
||||||
notes: this.newBook.notes,
|
notes: this.newBook.notes,
|
||||||
coverImage: this.newBook.coverImage,
|
coverImage: this.newBook.coverImage,
|
||||||
@@ -1657,6 +1710,8 @@ export class BooksListComponent implements OnInit {
|
|||||||
author: book.author,
|
author: book.author,
|
||||||
isbn: book.isbn,
|
isbn: book.isbn,
|
||||||
status: book.status,
|
status: book.status,
|
||||||
|
dateStarted: book.dateStarted,
|
||||||
|
dateFinished: book.dateFinished,
|
||||||
rating: book.rating,
|
rating: book.rating,
|
||||||
notes: book.notes,
|
notes: book.notes,
|
||||||
coverImage: book.coverImage,
|
coverImage: book.coverImage,
|
||||||
@@ -1685,7 +1740,13 @@ export class BooksListComponent implements OnInit {
|
|||||||
const book = this.editingBook();
|
const book = this.editingBook();
|
||||||
if (!book || !this.editBook.title || !this.editBook.author || !this.editBook.status) return;
|
if (!book || !this.editBook.title || !this.editBook.author || !this.editBook.status) return;
|
||||||
|
|
||||||
this.booksService.updateBook(book.id, this.editBook).subscribe(() => {
|
const updateData = {
|
||||||
|
...this.editBook,
|
||||||
|
dateStarted: this.editBook.dateStarted ? new Date(this.editBook.dateStarted) : undefined,
|
||||||
|
dateFinished: this.editBook.dateFinished ? new Date(this.editBook.dateFinished) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.booksService.updateBook(book.id, updateData).subscribe(() => {
|
||||||
this.loadBooks();
|
this.loadBooks();
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
});
|
});
|
||||||
@@ -1883,7 +1944,7 @@ export class BooksListComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.suggestionService.createSuggestion({
|
await this.suggestionService.createSuggestion({
|
||||||
entityType: SuggestionEntity.BOOK,
|
entityType: SuggestionEntity.book,
|
||||||
title: this.suggestedBook.title,
|
title: this.suggestedBook.title,
|
||||||
author: this.suggestedBook.author,
|
author: this.suggestedBook.author,
|
||||||
isbn: this.suggestedBook.isbn,
|
isbn: this.suggestedBook.isbn,
|
||||||
|
|||||||
@@ -70,6 +70,26 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateStarted"
|
||||||
|
[(ngModel)]="newGame.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateFinished"
|
||||||
|
[(ngModel)]="newGame.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="rating">Rating (1-10)</label>
|
<label for="rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -199,6 +219,26 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateStarted"
|
||||||
|
[(ngModel)]="editGame.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateFinished"
|
||||||
|
[(ngModel)]="editGame.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-rating">Rating (1-10)</label>
|
<label for="edit-rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -500,6 +540,18 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (game.dateStarted) {
|
||||||
|
<p class="date-started">
|
||||||
|
Started: {{ formatDate(game.dateStarted) }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (game.dateFinished) {
|
||||||
|
<p class="date-finished">
|
||||||
|
Finished: {{ formatDate(game.dateFinished) }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
@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">
|
||||||
@@ -854,6 +906,13 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-started,
|
||||||
|
.date-finished {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #4b5563;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
@@ -1257,10 +1316,12 @@ export class GamesListComponent implements OnInit {
|
|||||||
|
|
||||||
totalFilteredGames = computed(() => this.filteredGames().length);
|
totalFilteredGames = computed(() => this.filteredGames().length);
|
||||||
|
|
||||||
newGame: Partial<CreateGameDto> = {
|
newGame: Partial<CreateGameDto> & { dateStarted?: Date; dateFinished?: Date } = {
|
||||||
title: '',
|
title: '',
|
||||||
platform: '',
|
platform: '',
|
||||||
status: GameStatus.backlog,
|
status: GameStatus.backlog,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -1352,6 +1413,8 @@ export class GamesListComponent implements OnInit {
|
|||||||
title: '',
|
title: '',
|
||||||
platform: '',
|
platform: '',
|
||||||
status: GameStatus.backlog,
|
status: GameStatus.backlog,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverImage: undefined,
|
coverImage: undefined,
|
||||||
@@ -1420,6 +1483,8 @@ export class GamesListComponent implements OnInit {
|
|||||||
rating: this.newGame.rating,
|
rating: this.newGame.rating,
|
||||||
notes: this.newGame.notes,
|
notes: this.newGame.notes,
|
||||||
coverImage: this.newGame.coverImage,
|
coverImage: this.newGame.coverImage,
|
||||||
|
dateStarted: this.newGame.dateStarted ? new Date(this.newGame.dateStarted) : undefined,
|
||||||
|
dateFinished: this.newGame.dateFinished ? new Date(this.newGame.dateFinished) : undefined,
|
||||||
tags: this.newGame.tags || [],
|
tags: this.newGame.tags || [],
|
||||||
links: this.newGame.links || []
|
links: this.newGame.links || []
|
||||||
};
|
};
|
||||||
@@ -1444,6 +1509,8 @@ export class GamesListComponent implements OnInit {
|
|||||||
title: game.title,
|
title: game.title,
|
||||||
platform: game.platform,
|
platform: game.platform,
|
||||||
status: game.status,
|
status: game.status,
|
||||||
|
dateStarted: game.dateStarted,
|
||||||
|
dateFinished: game.dateFinished,
|
||||||
rating: game.rating,
|
rating: game.rating,
|
||||||
notes: game.notes,
|
notes: game.notes,
|
||||||
coverImage: game.coverImage,
|
coverImage: game.coverImage,
|
||||||
@@ -1472,7 +1539,13 @@ export class GamesListComponent implements OnInit {
|
|||||||
const game = this.editingGame();
|
const game = this.editingGame();
|
||||||
if (!game || !this.editGame.title || !this.editGame.status) return;
|
if (!game || !this.editGame.title || !this.editGame.status) return;
|
||||||
|
|
||||||
this.gamesService.updateGame(game.id, this.editGame).subscribe(() => {
|
const updateData: UpdateGameDto = {
|
||||||
|
...this.editGame,
|
||||||
|
dateStarted: this.editGame.dateStarted ? new Date(this.editGame.dateStarted) : undefined,
|
||||||
|
dateFinished: this.editGame.dateFinished ? new Date(this.editGame.dateFinished) : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this.gamesService.updateGame(game.id, updateData).subscribe(() => {
|
||||||
this.loadGames();
|
this.loadGames();
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
});
|
});
|
||||||
@@ -1669,7 +1742,7 @@ export class GamesListComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.suggestionService.createSuggestion({
|
await this.suggestionService.createSuggestion({
|
||||||
entityType: SuggestionEntity.GAME,
|
entityType: SuggestionEntity.game,
|
||||||
title: this.suggestedGame.title,
|
title: this.suggestedGame.title,
|
||||||
platform: this.suggestedGame.platform,
|
platform: this.suggestedGame.platform,
|
||||||
notes: this.suggestedGame.notes,
|
notes: this.suggestedGame.notes,
|
||||||
|
|||||||
@@ -71,6 +71,26 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateStarted"
|
||||||
|
[(ngModel)]="newManga.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateFinished"
|
||||||
|
[(ngModel)]="newManga.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="rating">Rating (1-10)</label>
|
<label for="rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -201,6 +221,26 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateStarted"
|
||||||
|
[(ngModel)]="editManga.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateFinished"
|
||||||
|
[(ngModel)]="editManga.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-rating">Rating (1-10)</label>
|
<label for="edit-rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -501,6 +541,18 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (manga.dateStarted) {
|
||||||
|
<p class="date-started">
|
||||||
|
Started: {{ formatDate(manga.dateStarted) }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (manga.dateFinished) {
|
||||||
|
<p class="date-finished">
|
||||||
|
Finished: {{ formatDate(manga.dateFinished) }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
@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">
|
||||||
@@ -857,6 +909,13 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-started,
|
||||||
|
.date-finished {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #4b5563;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
@@ -1260,10 +1319,12 @@ export class MangaListComponent implements OnInit {
|
|||||||
|
|
||||||
totalFilteredManga = computed(() => this.filteredManga().length);
|
totalFilteredManga = computed(() => this.filteredManga().length);
|
||||||
|
|
||||||
newManga: Partial<CreateMangaDto> = {
|
newManga: Partial<CreateMangaDto> & { dateStarted?: Date; dateFinished?: Date } = {
|
||||||
title: '',
|
title: '',
|
||||||
author: '',
|
author: '',
|
||||||
status: MangaStatus.wantToRead,
|
status: MangaStatus.wantToRead,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -1355,6 +1416,8 @@ export class MangaListComponent implements OnInit {
|
|||||||
title: '',
|
title: '',
|
||||||
author: '',
|
author: '',
|
||||||
status: MangaStatus.wantToRead,
|
status: MangaStatus.wantToRead,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverImage: undefined,
|
coverImage: undefined,
|
||||||
@@ -1420,6 +1483,8 @@ export class MangaListComponent implements OnInit {
|
|||||||
title: this.newManga.title,
|
title: this.newManga.title,
|
||||||
author: this.newManga.author,
|
author: this.newManga.author,
|
||||||
status: this.newManga.status,
|
status: this.newManga.status,
|
||||||
|
dateStarted: this.newManga.dateStarted ? new Date(this.newManga.dateStarted) : undefined,
|
||||||
|
dateFinished: this.newManga.dateFinished ? new Date(this.newManga.dateFinished) : undefined,
|
||||||
rating: this.newManga.rating,
|
rating: this.newManga.rating,
|
||||||
notes: this.newManga.notes,
|
notes: this.newManga.notes,
|
||||||
coverImage: this.newManga.coverImage,
|
coverImage: this.newManga.coverImage,
|
||||||
@@ -1447,6 +1512,8 @@ export class MangaListComponent implements OnInit {
|
|||||||
title: manga.title,
|
title: manga.title,
|
||||||
author: manga.author,
|
author: manga.author,
|
||||||
status: manga.status,
|
status: manga.status,
|
||||||
|
dateStarted: manga.dateStarted,
|
||||||
|
dateFinished: manga.dateFinished,
|
||||||
rating: manga.rating,
|
rating: manga.rating,
|
||||||
notes: manga.notes,
|
notes: manga.notes,
|
||||||
coverImage: manga.coverImage,
|
coverImage: manga.coverImage,
|
||||||
@@ -1475,7 +1542,13 @@ export class MangaListComponent implements OnInit {
|
|||||||
const manga = this.editingManga();
|
const manga = this.editingManga();
|
||||||
if (!manga || !this.editManga.title || !this.editManga.author || !this.editManga.status) return;
|
if (!manga || !this.editManga.title || !this.editManga.author || !this.editManga.status) return;
|
||||||
|
|
||||||
this.mangaService.updateManga(manga.id, this.editManga).subscribe(() => {
|
const updateData = {
|
||||||
|
...this.editManga,
|
||||||
|
dateStarted: this.editManga.dateStarted ? new Date(this.editManga.dateStarted) : undefined,
|
||||||
|
dateFinished: this.editManga.dateFinished ? new Date(this.editManga.dateFinished) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mangaService.updateManga(manga.id, updateData).subscribe(() => {
|
||||||
this.loadManga();
|
this.loadManga();
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
});
|
});
|
||||||
@@ -1670,7 +1743,7 @@ export class MangaListComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.suggestionService.createSuggestion({
|
await this.suggestionService.createSuggestion({
|
||||||
entityType: SuggestionEntity.MANGA,
|
entityType: SuggestionEntity.manga,
|
||||||
title: this.suggestedManga.title,
|
title: this.suggestedManga.title,
|
||||||
author: this.suggestedManga.author,
|
author: this.suggestedManga.author,
|
||||||
notes: this.suggestedManga.notes,
|
notes: this.suggestedManga.notes,
|
||||||
|
|||||||
@@ -80,6 +80,26 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateStarted"
|
||||||
|
[(ngModel)]="newMusic.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateFinished"
|
||||||
|
[(ngModel)]="newMusic.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="rating">Rating (1-10)</label>
|
<label for="rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -219,6 +239,26 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateStarted"
|
||||||
|
[(ngModel)]="editMusicData.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateFinished"
|
||||||
|
[(ngModel)]="editMusicData.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-rating">Rating (1-10)</label>
|
<label for="edit-rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -577,9 +617,15 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (music.dateCompleted) {
|
@if (music.dateStarted) {
|
||||||
<p class="date-completed">
|
<p class="date-started">
|
||||||
Completed: {{ formatDate(music.dateCompleted) }}
|
Started: {{ formatDate(music.dateStarted) }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (music.dateFinished) {
|
||||||
|
<p class="date-finished">
|
||||||
|
Finished: {{ formatDate(music.dateFinished) }}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,7 +1059,8 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-completed {
|
.date-started,
|
||||||
|
.date-finished {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--witch-plum);
|
color: var(--witch-plum);
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
@@ -1484,11 +1531,13 @@ export class MusicListComponent implements OnInit {
|
|||||||
|
|
||||||
totalFilteredMusic = computed(() => this.filteredMusic().length);
|
totalFilteredMusic = computed(() => this.filteredMusic().length);
|
||||||
|
|
||||||
newMusic: Partial<CreateMusicDto> = {
|
newMusic: Partial<CreateMusicDto> & { dateStarted?: Date; dateFinished?: Date } = {
|
||||||
title: '',
|
title: '',
|
||||||
artist: '',
|
artist: '',
|
||||||
type: MusicType.album,
|
type: MusicType.album,
|
||||||
status: MusicStatus.wantToListen,
|
status: MusicStatus.wantToListen,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -1595,6 +1644,8 @@ export class MusicListComponent implements OnInit {
|
|||||||
artist: '',
|
artist: '',
|
||||||
type: MusicType.album,
|
type: MusicType.album,
|
||||||
status: MusicStatus.wantToListen,
|
status: MusicStatus.wantToListen,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverArt: undefined,
|
coverArt: undefined,
|
||||||
@@ -1661,6 +1712,8 @@ export class MusicListComponent implements OnInit {
|
|||||||
artist: this.newMusic.artist,
|
artist: this.newMusic.artist,
|
||||||
type: this.newMusic.type,
|
type: this.newMusic.type,
|
||||||
status: this.newMusic.status,
|
status: this.newMusic.status,
|
||||||
|
dateStarted: this.newMusic.dateStarted ? new Date(this.newMusic.dateStarted) : undefined,
|
||||||
|
dateFinished: this.newMusic.dateFinished ? new Date(this.newMusic.dateFinished) : undefined,
|
||||||
rating: this.newMusic.rating,
|
rating: this.newMusic.rating,
|
||||||
notes: this.newMusic.notes,
|
notes: this.newMusic.notes,
|
||||||
coverArt: this.newMusic.coverArt,
|
coverArt: this.newMusic.coverArt,
|
||||||
@@ -1689,6 +1742,8 @@ export class MusicListComponent implements OnInit {
|
|||||||
artist: music.artist,
|
artist: music.artist,
|
||||||
type: music.type,
|
type: music.type,
|
||||||
status: music.status,
|
status: music.status,
|
||||||
|
dateStarted: music.dateStarted,
|
||||||
|
dateFinished: music.dateFinished,
|
||||||
rating: music.rating,
|
rating: music.rating,
|
||||||
notes: music.notes,
|
notes: music.notes,
|
||||||
coverArt: music.coverArt,
|
coverArt: music.coverArt,
|
||||||
@@ -1717,7 +1772,13 @@ export class MusicListComponent implements OnInit {
|
|||||||
const music = this.editingMusic();
|
const music = this.editingMusic();
|
||||||
if (!music || !this.editMusicData.title || !this.editMusicData.artist || !this.editMusicData.type || !this.editMusicData.status) return;
|
if (!music || !this.editMusicData.title || !this.editMusicData.artist || !this.editMusicData.type || !this.editMusicData.status) return;
|
||||||
|
|
||||||
this.musicService.updateMusic(music.id, this.editMusicData).subscribe(() => {
|
const updateData = {
|
||||||
|
...this.editMusicData,
|
||||||
|
dateStarted: this.editMusicData.dateStarted ? new Date(this.editMusicData.dateStarted) : undefined,
|
||||||
|
dateFinished: this.editMusicData.dateFinished ? new Date(this.editMusicData.dateFinished) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.musicService.updateMusic(music.id, updateData).subscribe(() => {
|
||||||
this.loadMusic();
|
this.loadMusic();
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
});
|
});
|
||||||
@@ -1915,7 +1976,7 @@ export class MusicListComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.suggestionService.createSuggestion({
|
await this.suggestionService.createSuggestion({
|
||||||
entityType: SuggestionEntity.MUSIC,
|
entityType: SuggestionEntity.music,
|
||||||
title: this.suggestedMusic.title,
|
title: this.suggestedMusic.title,
|
||||||
artist: this.suggestedMusic.artist,
|
artist: this.suggestedMusic.artist,
|
||||||
type: this.suggestedMusic.type,
|
type: this.suggestedMusic.type,
|
||||||
|
|||||||
@@ -373,12 +373,12 @@ export class MyLikesComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getItemTitle(likedItem: LikedItemDto): string {
|
getItemTitle(likedItem: LikedItemDto): string {
|
||||||
const item = likedItem.item;
|
const item = likedItem.item as any;
|
||||||
return item.title || item.name || 'Untitled';
|
return item.title || item.name || 'Untitled';
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemSubtitle(likedItem: LikedItemDto): string {
|
getItemSubtitle(likedItem: LikedItemDto): string {
|
||||||
const item = likedItem.item;
|
const item = likedItem.item as any;
|
||||||
const type = likedItem.like.entityType;
|
const type = likedItem.like.entityType;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -400,7 +400,7 @@ export class MyLikesComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getItemImage(likedItem: LikedItemDto): string | null {
|
getItemImage(likedItem: LikedItemDto): string | null {
|
||||||
const item = likedItem.item;
|
const item = likedItem.item as any;
|
||||||
return item.coverImage || item.imageUrl || null;
|
return item.coverImage || item.imageUrl || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,22 +43,22 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
All ({{ suggestions().length }})
|
All ({{ suggestions().length }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(SuggestionStatus.UNREVIEWED)"
|
(click)="setFilter(SuggestionStatus.unreviewed)"
|
||||||
[class.active]="statusFilter() === SuggestionStatus.UNREVIEWED"
|
[class.active]="statusFilter() === SuggestionStatus.unreviewed"
|
||||||
class="filter-btn pending"
|
class="filter-btn pending"
|
||||||
>
|
>
|
||||||
Pending ({{ unreviewedCount() }})
|
Pending ({{ unreviewedCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(SuggestionStatus.ACCEPTED)"
|
(click)="setFilter(SuggestionStatus.accepted)"
|
||||||
[class.active]="statusFilter() === SuggestionStatus.ACCEPTED"
|
[class.active]="statusFilter() === SuggestionStatus.accepted"
|
||||||
class="filter-btn accepted"
|
class="filter-btn accepted"
|
||||||
>
|
>
|
||||||
Accepted ({{ acceptedCount() }})
|
Accepted ({{ acceptedCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(SuggestionStatus.DECLINED)"
|
(click)="setFilter(SuggestionStatus.declined)"
|
||||||
[class.active]="statusFilter() === SuggestionStatus.DECLINED"
|
[class.active]="statusFilter() === SuggestionStatus.declined"
|
||||||
class="filter-btn declined"
|
class="filter-btn declined"
|
||||||
>
|
>
|
||||||
Declined ({{ declinedCount() }})
|
Declined ({{ declinedCount() }})
|
||||||
@@ -120,7 +120,7 @@ import { Suggestion, SuggestionStatus, SuggestionEntity } from '@library/shared-
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (suggestion.status === SuggestionStatus.DECLINED && suggestion.declineReason) {
|
@if (suggestion.status === SuggestionStatus.declined && suggestion.declineReason) {
|
||||||
<div class="decline-reason">
|
<div class="decline-reason">
|
||||||
<strong>Reason:</strong> {{ suggestion.declineReason }}
|
<strong>Reason:</strong> {{ suggestion.declineReason }}
|
||||||
</div>
|
</div>
|
||||||
@@ -337,9 +337,9 @@ export class MySuggestionsComponent implements OnInit {
|
|||||||
|
|
||||||
SuggestionStatus = SuggestionStatus;
|
SuggestionStatus = SuggestionStatus;
|
||||||
|
|
||||||
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.UNREVIEWED).length;
|
unreviewedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.unreviewed).length;
|
||||||
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.ACCEPTED).length;
|
acceptedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.accepted).length;
|
||||||
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.DECLINED).length;
|
declinedCount = () => this.suggestions().filter(s => s.status === SuggestionStatus.declined).length;
|
||||||
|
|
||||||
filteredSuggestions = computed(() => {
|
filteredSuggestions = computed(() => {
|
||||||
const filter = this.statusFilter();
|
const filter = this.statusFilter();
|
||||||
@@ -397,20 +397,20 @@ export class MySuggestionsComponent implements OnInit {
|
|||||||
|
|
||||||
getStatusLabel(status: SuggestionStatus): string {
|
getStatusLabel(status: SuggestionStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case SuggestionStatus.UNREVIEWED: return 'Pending Review';
|
case SuggestionStatus.unreviewed: return 'Pending Review';
|
||||||
case SuggestionStatus.ACCEPTED: return 'Accepted';
|
case SuggestionStatus.accepted: return 'Accepted';
|
||||||
case SuggestionStatus.DECLINED: return 'Declined';
|
case SuggestionStatus.declined: return 'Declined';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntityIcon(entityType: SuggestionEntity): string {
|
getEntityIcon(entityType: SuggestionEntity): string {
|
||||||
switch (entityType) {
|
switch (entityType) {
|
||||||
case SuggestionEntity.GAME: return '🎮';
|
case SuggestionEntity.game: return '🎮';
|
||||||
case SuggestionEntity.BOOK: return '📚';
|
case SuggestionEntity.book: return '📚';
|
||||||
case SuggestionEntity.MUSIC: return '🎵';
|
case SuggestionEntity.music: return '🎵';
|
||||||
case SuggestionEntity.MANGA: return 'đź“–';
|
case SuggestionEntity.manga: return 'đź“–';
|
||||||
case SuggestionEntity.SHOW: return '📺';
|
case SuggestionEntity.show: return '📺';
|
||||||
case SuggestionEntity.ART: return '🎨';
|
case SuggestionEntity.art: return '🎨';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,26 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateStarted"
|
||||||
|
[(ngModel)]="newShow.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="dateFinished"
|
||||||
|
[(ngModel)]="newShow.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="rating">Rating (1-10)</label>
|
<label for="rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -197,6 +217,26 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateStarted">Date Started</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateStarted"
|
||||||
|
[(ngModel)]="editShow.dateStarted"
|
||||||
|
name="dateStarted"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-dateFinished">Date Finished</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="edit-dateFinished"
|
||||||
|
[(ngModel)]="editShow.dateFinished"
|
||||||
|
name="dateFinished"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-rating">Rating (1-10)</label>
|
<label for="edit-rating">Rating (1-10)</label>
|
||||||
<input
|
<input
|
||||||
@@ -495,6 +535,18 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (show.dateStarted) {
|
||||||
|
<p class="date-started">
|
||||||
|
Started: {{ formatDate(show.dateStarted) }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (show.dateFinished) {
|
||||||
|
<p class="date-finished">
|
||||||
|
Finished: {{ formatDate(show.dateFinished) }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
@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">
|
||||||
@@ -850,6 +902,13 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-started,
|
||||||
|
.date-finished {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #4b5563;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
@@ -1254,12 +1313,14 @@ export class ShowsListComponent implements OnInit {
|
|||||||
|
|
||||||
totalFilteredShows = computed(() => this.filteredShows().length);
|
totalFilteredShows = computed(() => this.filteredShows().length);
|
||||||
|
|
||||||
newShow: Partial<CreateShowDto> = {
|
newShow: Partial<CreateShowDto> & { dateStarted?: Date; dateFinished?: Date } = {
|
||||||
title: '',
|
title: '',
|
||||||
type: ShowType.tvSeries,
|
type: ShowType.tvSeries,
|
||||||
status: ShowStatus.wantToWatch,
|
status: ShowStatus.wantToWatch,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
tags: [],
|
tags: [],
|
||||||
links: []
|
links: []
|
||||||
};
|
};
|
||||||
@@ -1361,6 +1422,8 @@ export class ShowsListComponent implements OnInit {
|
|||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
coverImage: undefined,
|
coverImage: undefined,
|
||||||
|
dateStarted: undefined,
|
||||||
|
dateFinished: undefined,
|
||||||
tags: [],
|
tags: [],
|
||||||
links: []
|
links: []
|
||||||
};
|
};
|
||||||
@@ -1423,6 +1486,8 @@ export class ShowsListComponent implements OnInit {
|
|||||||
title: this.newShow.title,
|
title: this.newShow.title,
|
||||||
type: this.newShow.type,
|
type: this.newShow.type,
|
||||||
status: this.newShow.status,
|
status: this.newShow.status,
|
||||||
|
dateStarted: this.newShow.dateStarted ? new Date(this.newShow.dateStarted) : undefined,
|
||||||
|
dateFinished: this.newShow.dateFinished ? new Date(this.newShow.dateFinished) : undefined,
|
||||||
rating: this.newShow.rating,
|
rating: this.newShow.rating,
|
||||||
notes: this.newShow.notes,
|
notes: this.newShow.notes,
|
||||||
coverImage: this.newShow.coverImage,
|
coverImage: this.newShow.coverImage,
|
||||||
@@ -1450,6 +1515,8 @@ export class ShowsListComponent implements OnInit {
|
|||||||
title: show.title,
|
title: show.title,
|
||||||
type: show.type,
|
type: show.type,
|
||||||
status: show.status,
|
status: show.status,
|
||||||
|
dateStarted: show.dateStarted,
|
||||||
|
dateFinished: show.dateFinished,
|
||||||
rating: show.rating,
|
rating: show.rating,
|
||||||
notes: show.notes,
|
notes: show.notes,
|
||||||
coverImage: show.coverImage,
|
coverImage: show.coverImage,
|
||||||
@@ -1478,7 +1545,13 @@ export class ShowsListComponent implements OnInit {
|
|||||||
const show = this.editingShow();
|
const show = this.editingShow();
|
||||||
if (!show || !this.editShow.title || !this.editShow.type || !this.editShow.status) return;
|
if (!show || !this.editShow.title || !this.editShow.type || !this.editShow.status) return;
|
||||||
|
|
||||||
this.showsService.updateShow(show.id, this.editShow).subscribe(() => {
|
const updateData = {
|
||||||
|
...this.editShow,
|
||||||
|
dateStarted: this.editShow.dateStarted ? new Date(this.editShow.dateStarted) : undefined,
|
||||||
|
dateFinished: this.editShow.dateFinished ? new Date(this.editShow.dateFinished) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.showsService.updateShow(show.id, updateData).subscribe(() => {
|
||||||
this.loadShows();
|
this.loadShows();
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
});
|
});
|
||||||
@@ -1673,7 +1746,7 @@ export class ShowsListComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.suggestionService.createSuggestion({
|
await this.suggestionService.createSuggestion({
|
||||||
entityType: SuggestionEntity.SHOW,
|
entityType: SuggestionEntity.show,
|
||||||
title: this.suggestedShow.title,
|
title: this.suggestedShow.title,
|
||||||
type: this.suggestedShow.type,
|
type: this.suggestedShow.type,
|
||||||
notes: this.suggestedShow.notes,
|
notes: this.suggestedShow.notes,
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Database
|
||||||
|
DATABASE_URL="op://Environment Variables - Naomi/Library/mongo url"
|
||||||
|
|
||||||
|
# JWT Secret
|
||||||
|
JWT_SECRET="op://Environment Variables - Naomi/Library/jwt secret"
|
||||||
|
|
||||||
|
# Discord OAuth
|
||||||
|
DISCORD_CLIENT_ID="op://Environment Variables - Naomi/Library/discord client id"
|
||||||
|
DISCORD_CLIENT_SECRET="op://Environment Variables - Naomi/Library/discord client secret"
|
||||||
|
|
||||||
|
# Admin Configuration
|
||||||
|
ADMIN_DISCORD_ID="op://Environment Variables - Naomi/Library/admin discord id"
|
||||||
|
|
||||||
|
# Discord Server
|
||||||
|
DISCORD_GUILD_ID="op://Environment Variables - Naomi/Library/discord server id"
|
||||||
|
SPONSOR_ROLE_ID="op://Environment Variables - Naomi/Library/sponsor role id"
|
||||||
|
MOD_ROLE_ID="op://Environment Variables - Naomi/Library/mod role id"
|
||||||
|
STAFF_ROLE_ID="op://Environment Variables - Naomi/Library/staff role id"
|
||||||
|
|
||||||
|
# Application URL
|
||||||
|
BASE_URL="op://Environment Variables - Naomi/Library/localhost url"
|
||||||
+1
-6
@@ -1,8 +1,3 @@
|
|||||||
import nhcarrigan from '@nhcarrigan/eslint-config';
|
import nhcarrigan from '@nhcarrigan/eslint-config';
|
||||||
|
|
||||||
export default [
|
export default nhcarrigan;
|
||||||
...nhcarrigan,
|
|
||||||
{
|
|
||||||
ignores: ['**/dist', '**/out-tsc', 'node_modules'],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "nx run-many --target=build --all && NODE_ENV=production op run --env-file=dev.env -- node dist/api/main.js",
|
||||||
"lint": "nx run-many --target=lint --all",
|
"lint": "nx run-many --target=lint --all",
|
||||||
"build": "nx run-many --target=build --all",
|
"build": "nx run-many --target=build --all",
|
||||||
"test": "nx run-many --target=test --all --passWithNoTests",
|
"test": "nx run-many --target=test --all --passWithNoTests",
|
||||||
|
|||||||
@@ -3,19 +3,113 @@ import { fileURLToPath } from 'url';
|
|||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const nhcarriganArray = Array.isArray(nhcarrigan) ? nhcarrigan : [nhcarrigan];
|
||||||
|
|
||||||
|
// Jest globals that should be available in test files
|
||||||
|
const jestGlobals = {
|
||||||
|
afterAll: 'readonly',
|
||||||
|
afterEach: 'readonly',
|
||||||
|
beforeAll: 'readonly',
|
||||||
|
beforeEach: 'readonly',
|
||||||
|
describe: 'readonly',
|
||||||
|
expect: 'readonly',
|
||||||
|
it: 'readonly',
|
||||||
|
jest: 'readonly',
|
||||||
|
test: 'readonly',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map the nhcarrigan configs to handle shared-types directory structure
|
||||||
|
const mappedConfigs = nhcarriganArray.flatMap(config => {
|
||||||
|
if (!config.files) {
|
||||||
|
return [config];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFiles = config.files
|
||||||
|
.map(pattern => {
|
||||||
|
if (pattern.startsWith('src/')) {
|
||||||
|
return pattern.replace('src/', 'shared-types/src/');
|
||||||
|
} else if (pattern.startsWith('test/')) {
|
||||||
|
return pattern.replace('test/', 'shared-types/test/');
|
||||||
|
}
|
||||||
|
return pattern;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine if this is a test file config
|
||||||
|
const isTestFile = newFiles[0]?.includes('test/');
|
||||||
|
|
||||||
|
// Update configs to handle shared-types directory structure
|
||||||
|
let updatedConfig = { ...config, files: newFiles };
|
||||||
|
|
||||||
|
if (config.languageOptions) {
|
||||||
|
const updatedLanguageOptions = { ...config.languageOptions };
|
||||||
|
|
||||||
|
// Add Jest globals for test files
|
||||||
|
if (isTestFile) {
|
||||||
|
updatedLanguageOptions.globals = { ...updatedLanguageOptions.globals, ...jestGlobals };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update parserOptions to use our tsconfig with proper tsconfigRootDir
|
||||||
|
if (config.languageOptions.parserOptions) {
|
||||||
|
updatedLanguageOptions.parserOptions = {
|
||||||
|
...config.languageOptions.parserOptions,
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedConfig = { ...updatedConfig, languageOptions: updatedLanguageOptions };
|
||||||
|
}
|
||||||
|
|
||||||
|
return [updatedConfig];
|
||||||
|
});
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...nhcarrigan,
|
|
||||||
{
|
{
|
||||||
ignores: ['**/dist', '**/out-tsc', 'node_modules'],
|
ignores: ['dist', 'out-tsc', 'node_modules'],
|
||||||
},
|
},
|
||||||
|
...mappedConfigs,
|
||||||
|
// Disable vitest rules for this Jest project
|
||||||
{
|
{
|
||||||
files: ['**/*.ts'],
|
files: ['shared-types/test/**/*.spec.ts'],
|
||||||
languageOptions: {
|
rules: {
|
||||||
parserOptions: {
|
'vitest/consistent-test-filename': 'off',
|
||||||
project: './tsconfig.lib.json',
|
'vitest/consistent-test-it': 'off',
|
||||||
tsconfigRootDir: __dirname,
|
'vitest/expect-expect': 'off',
|
||||||
},
|
'vitest/no-alias-methods': 'off',
|
||||||
|
'vitest/no-commented-out-tests': 'off',
|
||||||
|
'vitest/no-conditional-expect': 'off',
|
||||||
|
'vitest/no-conditional-in-test': 'off',
|
||||||
|
'vitest/no-conditional-tests': 'off',
|
||||||
|
'vitest/no-disabled-tests': 'off',
|
||||||
|
'vitest/no-duplicate-hooks': 'off',
|
||||||
|
'vitest/no-focused-tests': 'off',
|
||||||
|
'vitest/no-identical-title': 'off',
|
||||||
|
'vitest/no-standalone-expect': 'off',
|
||||||
|
'vitest/no-test-prefixes': 'off',
|
||||||
|
'vitest/no-test-return-statement': 'off',
|
||||||
|
'vitest/prefer-comparison-matcher': 'off',
|
||||||
|
'vitest/prefer-each': 'off',
|
||||||
|
'vitest/prefer-equality-matcher': 'off',
|
||||||
|
'vitest/prefer-expect-assertions': 'off',
|
||||||
|
'vitest/prefer-expect-resolves': 'off',
|
||||||
|
'vitest/prefer-hooks-in-order': 'off',
|
||||||
|
'vitest/prefer-hooks-on-top': 'off',
|
||||||
|
'vitest/prefer-lowercase-title': 'off',
|
||||||
|
'vitest/prefer-mock-promise-shorthand': 'off',
|
||||||
|
'vitest/prefer-spy-on': 'off',
|
||||||
|
'vitest/prefer-strict-equal': 'off',
|
||||||
|
'vitest/prefer-to-be': 'off',
|
||||||
|
'vitest/prefer-to-be-falsy': 'off',
|
||||||
|
'vitest/prefer-to-be-object': 'off',
|
||||||
|
'vitest/prefer-to-be-truthy': 'off',
|
||||||
|
'vitest/prefer-to-contain': 'off',
|
||||||
|
'vitest/prefer-to-have-length': 'off',
|
||||||
|
'vitest/prefer-todo': 'off',
|
||||||
|
'vitest/require-hook': 'off',
|
||||||
|
'vitest/require-to-throw-message': 'off',
|
||||||
|
'vitest/require-top-level-describe': 'off',
|
||||||
|
'vitest/valid-describe-callback': 'off',
|
||||||
|
'vitest/valid-expect': 'off',
|
||||||
|
'vitest/valid-title': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export default {
|
||||||
|
displayName: "shared-types",
|
||||||
|
preset: "../jest.preset.js",
|
||||||
|
testEnvironment: "node",
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ["ts", "js", "html"],
|
||||||
|
coverageDirectory: "../coverage/shared-types",
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 100,
|
||||||
|
functions: 100,
|
||||||
|
lines: 100,
|
||||||
|
statements: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collectCoverageFrom: [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"!src/index.ts", // Just re-exports
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -5,5 +5,20 @@
|
|||||||
"projectType": "library",
|
"projectType": "library",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"// targets": "to see all targets run: nx show project shared-types --web",
|
"// targets": "to see all targets run: nx show project shared-types --web",
|
||||||
"targets": {}
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["shared-types/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nx/jest:jest",
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "shared-types/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
},
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface Book {
|
|||||||
isbn?: string;
|
isbn?: string;
|
||||||
status: BookStatus;
|
status: BookStatus;
|
||||||
dateAdded: Date;
|
dateAdded: Date;
|
||||||
|
dateStarted?: Date;
|
||||||
dateFinished?: Date;
|
dateFinished?: Date;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
@@ -30,18 +31,21 @@ interface Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CreateBookDto {
|
interface CreateBookDto {
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
author: string;
|
||||||
isbn?: string;
|
isbn?: string;
|
||||||
status: BookStatus;
|
status: BookStatus;
|
||||||
rating?: number;
|
dateStarted?: Date;
|
||||||
notes?: string;
|
dateFinished?: Date;
|
||||||
coverImage?: string;
|
rating?: number;
|
||||||
tags?: Array<string>;
|
notes?: string;
|
||||||
links?: Array<Link>;
|
coverImage?: string;
|
||||||
|
tags?: Array<string>;
|
||||||
|
links?: Array<Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateBookDto extends Partial<CreateBookDto> {
|
interface UpdateBookDto extends Partial<CreateBookDto> {
|
||||||
|
dateStarted?: Date;
|
||||||
dateFinished?: Date;
|
dateFinished?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ interface Game {
|
|||||||
platform?: string;
|
platform?: string;
|
||||||
status: GameStatus;
|
status: GameStatus;
|
||||||
dateAdded: Date;
|
dateAdded: Date;
|
||||||
|
dateStarted?: Date;
|
||||||
dateCompleted?: Date;
|
dateCompleted?: Date;
|
||||||
|
dateFinished?: Date;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
@@ -29,18 +31,22 @@ interface Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CreateGameDto {
|
interface CreateGameDto {
|
||||||
title: string;
|
title: string;
|
||||||
platform?: string;
|
platform?: string;
|
||||||
status: GameStatus;
|
status: GameStatus;
|
||||||
rating?: number;
|
dateStarted?: Date;
|
||||||
notes?: string;
|
dateFinished?: Date;
|
||||||
coverImage?: string;
|
rating?: number;
|
||||||
tags?: Array<string>;
|
notes?: string;
|
||||||
links?: Array<Link>;
|
coverImage?: string;
|
||||||
|
tags?: Array<string>;
|
||||||
|
links?: Array<Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateGameDto extends Partial<CreateGameDto> {
|
interface UpdateGameDto extends Partial<CreateGameDto> {
|
||||||
|
dateStarted?: Date;
|
||||||
dateCompleted?: Date;
|
dateCompleted?: Date;
|
||||||
|
dateFinished?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type CreateGameDto, type Game, GameStatus, type UpdateGameDto };
|
export { type CreateGameDto, type Game, GameStatus, type UpdateGameDto };
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ interface Manga {
|
|||||||
author: string;
|
author: string;
|
||||||
status: MangaStatus;
|
status: MangaStatus;
|
||||||
dateAdded: Date;
|
dateAdded: Date;
|
||||||
|
dateStarted?: Date;
|
||||||
dateCompleted?: Date;
|
dateCompleted?: Date;
|
||||||
|
dateFinished?: Date;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
@@ -29,18 +31,22 @@ interface Manga {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CreateMangaDto {
|
interface CreateMangaDto {
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
author: string;
|
||||||
status: MangaStatus;
|
status: MangaStatus;
|
||||||
rating?: number;
|
dateStarted?: Date;
|
||||||
notes?: string;
|
dateFinished?: Date;
|
||||||
coverImage?: string;
|
rating?: number;
|
||||||
tags?: Array<string>;
|
notes?: string;
|
||||||
links?: Array<Link>;
|
coverImage?: string;
|
||||||
|
tags?: Array<string>;
|
||||||
|
links?: Array<Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateMangaDto extends Partial<CreateMangaDto> {
|
interface UpdateMangaDto extends Partial<CreateMangaDto> {
|
||||||
|
dateStarted?: Date;
|
||||||
dateCompleted?: Date;
|
dateCompleted?: Date;
|
||||||
|
dateFinished?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { MangaStatus };
|
export { MangaStatus };
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ interface Music {
|
|||||||
type: MusicType;
|
type: MusicType;
|
||||||
status: MusicStatus;
|
status: MusicStatus;
|
||||||
dateAdded: Date;
|
dateAdded: Date;
|
||||||
|
dateStarted?: Date;
|
||||||
dateCompleted?: Date;
|
dateCompleted?: Date;
|
||||||
|
dateFinished?: Date;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverArt?: string;
|
coverArt?: string;
|
||||||
@@ -36,19 +38,23 @@ interface Music {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CreateMusicDto {
|
interface CreateMusicDto {
|
||||||
title: string;
|
title: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
type: MusicType;
|
type: MusicType;
|
||||||
status: MusicStatus;
|
status: MusicStatus;
|
||||||
rating?: number;
|
dateStarted?: Date;
|
||||||
notes?: string;
|
dateFinished?: Date;
|
||||||
coverArt?: string;
|
rating?: number;
|
||||||
tags?: Array<string>;
|
notes?: string;
|
||||||
links?: Array<Link>;
|
coverArt?: string;
|
||||||
|
tags?: Array<string>;
|
||||||
|
links?: Array<Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateMusicDto extends Partial<CreateMusicDto> {
|
interface UpdateMusicDto extends Partial<CreateMusicDto> {
|
||||||
|
dateStarted?: Date;
|
||||||
dateCompleted?: Date;
|
dateCompleted?: Date;
|
||||||
|
dateFinished?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { MusicStatus, MusicType };
|
export { MusicStatus, MusicType };
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ interface Show {
|
|||||||
type: ShowType;
|
type: ShowType;
|
||||||
status: ShowStatus;
|
status: ShowStatus;
|
||||||
dateAdded: Date;
|
dateAdded: Date;
|
||||||
|
dateStarted?: Date;
|
||||||
dateCompleted?: Date;
|
dateCompleted?: Date;
|
||||||
|
dateFinished?: Date;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
@@ -36,18 +38,22 @@ interface Show {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CreateShowDto {
|
interface CreateShowDto {
|
||||||
title: string;
|
title: string;
|
||||||
type: ShowType;
|
type: ShowType;
|
||||||
status: ShowStatus;
|
status: ShowStatus;
|
||||||
rating?: number;
|
dateStarted?: Date;
|
||||||
notes?: string;
|
dateFinished?: Date;
|
||||||
coverImage?: string;
|
rating?: number;
|
||||||
tags?: Array<string>;
|
notes?: string;
|
||||||
links?: Array<Link>;
|
coverImage?: string;
|
||||||
|
tags?: Array<string>;
|
||||||
|
links?: Array<Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateShowDto extends Partial<CreateShowDto> {
|
interface UpdateShowDto extends Partial<CreateShowDto> {
|
||||||
|
dateStarted?: Date;
|
||||||
dateCompleted?: Date;
|
dateCompleted?: Date;
|
||||||
|
dateFinished?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ShowStatus, ShowType };
|
export { ShowStatus, ShowType };
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Art, CreateArtDto, UpdateArtDto } from "../src/lib/art.types";
|
||||||
|
|
||||||
|
describe("art Types", () => {
|
||||||
|
describe("art interface", () => {
|
||||||
|
it("should accept valid art object with minimal fields", () => {
|
||||||
|
const art: Art = {
|
||||||
|
artist: "Jane Doe",
|
||||||
|
createdAt: new Date(),
|
||||||
|
dateAdded: new Date(),
|
||||||
|
id: "art123",
|
||||||
|
imageUrl: "https://example.com/sunset.jpg",
|
||||||
|
links: [],
|
||||||
|
tags: [],
|
||||||
|
title: "Beautiful Sunset",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(art.description).toBeUndefined();
|
||||||
|
expect(art.tags).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid art object with all fields", () => {
|
||||||
|
const fullArt: Art = {
|
||||||
|
artist: "John Smith",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
dateAdded: new Date("2024-01-01"),
|
||||||
|
description: "A breathtaking view of the mountains",
|
||||||
|
id: "art456",
|
||||||
|
imageUrl: "https://example.com/mountains.jpg",
|
||||||
|
links: [
|
||||||
|
{ title: "Artist Portfolio", url: "https://artist.com" },
|
||||||
|
{ title: "Instagram", url: "https://instagram.com/artist" },
|
||||||
|
],
|
||||||
|
tags: [ "landscape", "nature", "mountains" ],
|
||||||
|
title: "Mountain Landscape",
|
||||||
|
updatedAt: new Date("2024-01-02"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullArt.description).toBe("A breathtaking view of the mountains");
|
||||||
|
expect(fullArt.tags).toHaveLength(3);
|
||||||
|
expect(fullArt.links).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createArtDto interface", () => {
|
||||||
|
it("should accept DTO with required fields only", () => {
|
||||||
|
const createDto: CreateArtDto = {
|
||||||
|
artist: "New Artist",
|
||||||
|
imageUrl: "https://example.com/art.jpg",
|
||||||
|
title: "New Artwork",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.description).toBeUndefined();
|
||||||
|
expect(createDto.tags).toBeUndefined();
|
||||||
|
expect(createDto.links).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept DTO with all fields", () => {
|
||||||
|
const fullCreateDto: CreateArtDto = {
|
||||||
|
artist: "Famous Artist",
|
||||||
|
description: "Detailed description",
|
||||||
|
imageUrl: "https://example.com/complete.jpg",
|
||||||
|
links: [ { title: "Website", url: "https://website.com" } ],
|
||||||
|
tags: [ "tag1", "tag2" ],
|
||||||
|
title: "Complete Artwork",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullCreateDto.tags).toEqual([ "tag1", "tag2" ]);
|
||||||
|
expect(fullCreateDto.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateArtDto type", () => {
|
||||||
|
it("should accept empty update DTO", () => {
|
||||||
|
const emptyUpdate: UpdateArtDto = {};
|
||||||
|
expect(emptyUpdate).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept partial updates", () => {
|
||||||
|
const partialUpdate: UpdateArtDto = {
|
||||||
|
tags: [ "new-tag" ],
|
||||||
|
title: "Updated Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(partialUpdate.title).toBe("Updated Title");
|
||||||
|
expect(partialUpdate.artist).toBeUndefined();
|
||||||
|
expect(partialUpdate.tags).toEqual([ "new-tag" ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept full update", () => {
|
||||||
|
const fullUpdate: UpdateArtDto = {
|
||||||
|
artist: "Different Artist",
|
||||||
|
description: "New description",
|
||||||
|
imageUrl: "https://example.com/new-image.jpg",
|
||||||
|
links: [ { title: "New Link", url: "https://newlink.com" } ],
|
||||||
|
tags: [ "updated", "tags" ],
|
||||||
|
title: "Completely New Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullUpdate.title).toBe("Completely New Title");
|
||||||
|
expect(fullUpdate.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuditAction,
|
||||||
|
AuditCategory,
|
||||||
|
type AuditLog,
|
||||||
|
type AuditLogFilters,
|
||||||
|
type AuditLogUser,
|
||||||
|
} from "../src/lib/audit.types";
|
||||||
|
|
||||||
|
describe("audit Types", () => {
|
||||||
|
describe("auditAction enum", () => {
|
||||||
|
it("should have all expected action values", () => {
|
||||||
|
expect(AuditAction.login).toBe("LOGIN");
|
||||||
|
expect(AuditAction.logout).toBe("LOGOUT");
|
||||||
|
expect(AuditAction.loginFailed).toBe("LOGIN_FAILED");
|
||||||
|
expect(AuditAction.commentCreate).toBe("COMMENT_CREATE");
|
||||||
|
expect(AuditAction.commentUpdate).toBe("COMMENT_UPDATE");
|
||||||
|
expect(AuditAction.commentDelete).toBe("COMMENT_DELETE");
|
||||||
|
expect(AuditAction.entryCreate).toBe("ENTRY_CREATE");
|
||||||
|
expect(AuditAction.entryUpdate).toBe("ENTRY_UPDATE");
|
||||||
|
expect(AuditAction.entryDelete).toBe("ENTRY_DELETE");
|
||||||
|
expect(AuditAction.like).toBe("LIKE");
|
||||||
|
expect(AuditAction.unlike).toBe("UNLIKE");
|
||||||
|
expect(AuditAction.userBan).toBe("USER_BAN");
|
||||||
|
expect(AuditAction.userUnban).toBe("USER_UNBAN");
|
||||||
|
expect(AuditAction.rateLimitExceeded).toBe("RATE_LIMIT_EXCEEDED");
|
||||||
|
expect(AuditAction.csrfValidationFailed).toBe("CSRF_VALIDATION_FAILED");
|
||||||
|
expect(AuditAction.unauthorizedAccess).toBe("UNAUTHORIZED_ACCESS");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("auditCategory enum", () => {
|
||||||
|
it("should have all expected category values", () => {
|
||||||
|
expect(AuditCategory.auth).toBe("AUTH");
|
||||||
|
expect(AuditCategory.content).toBe("CONTENT");
|
||||||
|
expect(AuditCategory.admin).toBe("ADMIN");
|
||||||
|
expect(AuditCategory.security).toBe("SECURITY");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("auditLogUser interface", () => {
|
||||||
|
it("should accept valid user objects", () => {
|
||||||
|
const userWithAvatar: AuditLogUser = {
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
id: "user123",
|
||||||
|
username: "testuser",
|
||||||
|
};
|
||||||
|
|
||||||
|
const userWithoutAvatar: AuditLogUser = {
|
||||||
|
id: "user456",
|
||||||
|
username: "anotheruser",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(userWithAvatar.avatar).toBe("https://example.com/avatar.png");
|
||||||
|
expect(userWithoutAvatar.avatar).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("auditLog interface", () => {
|
||||||
|
it("should accept valid audit log with minimal fields", () => {
|
||||||
|
const minimalLog: AuditLog = {
|
||||||
|
action: AuditAction.login,
|
||||||
|
category: AuditCategory.auth,
|
||||||
|
createdAt: new Date(),
|
||||||
|
id: "log123",
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(minimalLog.userId).toBeUndefined();
|
||||||
|
expect(minimalLog.details).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid audit log with all fields", () => {
|
||||||
|
const fullLog: AuditLog = {
|
||||||
|
action: AuditAction.commentDelete,
|
||||||
|
category: AuditCategory.admin,
|
||||||
|
createdAt: new Date(),
|
||||||
|
details: "Admin deleted inappropriate comment",
|
||||||
|
id: "log456",
|
||||||
|
resourceId: "comment123",
|
||||||
|
resourceType: "comment",
|
||||||
|
success: true,
|
||||||
|
targetUser: {
|
||||||
|
id: "user789",
|
||||||
|
username: "targetuser",
|
||||||
|
},
|
||||||
|
targetUserId: "user789",
|
||||||
|
user: {
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
id: "user123",
|
||||||
|
username: "admin",
|
||||||
|
},
|
||||||
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||||
|
userId: "user123",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullLog.user?.username).toBe("admin");
|
||||||
|
expect(fullLog.targetUser?.username).toBe("targetuser");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("auditLogFilters interface", () => {
|
||||||
|
it("should accept empty filters", () => {
|
||||||
|
const emptyFilters: AuditLogFilters = {};
|
||||||
|
expect(emptyFilters).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept filters with all fields", () => {
|
||||||
|
const fullFilters: AuditLogFilters = {
|
||||||
|
action: AuditAction.login,
|
||||||
|
category: AuditCategory.auth,
|
||||||
|
endDate: new Date("2024-12-31"),
|
||||||
|
limit: 50,
|
||||||
|
page: 1,
|
||||||
|
startDate: new Date("2024-01-01"),
|
||||||
|
success: true,
|
||||||
|
userId: "user123",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullFilters.page).toBe(1);
|
||||||
|
expect(fullFilters.limit).toBe(50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AuthResponse, JwtPayload, User } from "../src/lib/auth.types";
|
||||||
|
|
||||||
|
describe("auth Types", () => {
|
||||||
|
describe("user interface", () => {
|
||||||
|
it("should accept valid user objects", () => {
|
||||||
|
const userWithAvatar: User = {
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
discordId: "discord123",
|
||||||
|
email: "test@example.com",
|
||||||
|
id: "user123",
|
||||||
|
inDiscord: true,
|
||||||
|
isAdmin: true,
|
||||||
|
isBanned: false,
|
||||||
|
isMod: true,
|
||||||
|
isStaff: true,
|
||||||
|
isVip: false,
|
||||||
|
username: "testuser",
|
||||||
|
};
|
||||||
|
|
||||||
|
const userWithoutAvatar: User = {
|
||||||
|
discordId: "discord456",
|
||||||
|
email: "another@example.com",
|
||||||
|
id: "user456",
|
||||||
|
inDiscord: false,
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: false,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: true,
|
||||||
|
username: "anotheruser",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(userWithAvatar.avatar).toBe("https://example.com/avatar.png");
|
||||||
|
expect(userWithoutAvatar.avatar).toBeUndefined();
|
||||||
|
expect(userWithAvatar.isAdmin).toBeTruthy();
|
||||||
|
expect(userWithoutAvatar.isVip).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle all boolean flags correctly", () => {
|
||||||
|
const bannedUser: User = {
|
||||||
|
discordId: "discord789",
|
||||||
|
email: "banned@example.com",
|
||||||
|
id: "banned123",
|
||||||
|
inDiscord: false,
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: true,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: false,
|
||||||
|
username: "banneduser",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(bannedUser.isBanned).toBeTruthy();
|
||||||
|
expect(bannedUser.isAdmin).toBeFalsy();
|
||||||
|
expect(bannedUser.inDiscord).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("jwtPayload interface", () => {
|
||||||
|
it("should accept JWT payload with required fields", () => {
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
email: "test@example.com",
|
||||||
|
isAdmin: false,
|
||||||
|
sub: "user123",
|
||||||
|
username: "testuser",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(payload.sub).toBe("user123");
|
||||||
|
expect(payload.iat).toBeUndefined();
|
||||||
|
expect(payload.exp).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept JWT payload with timestamps", () => {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const payloadWithTimestamps: JwtPayload = {
|
||||||
|
email: "another@example.com",
|
||||||
|
exp: now + 3600,
|
||||||
|
iat: now,
|
||||||
|
isAdmin: true,
|
||||||
|
sub: "user456",
|
||||||
|
username: "anotheruser", // 1 hour
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(payloadWithTimestamps.iat).toBe(now);
|
||||||
|
expect(payloadWithTimestamps.exp).toBe(now + 3600);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authResponse interface", () => {
|
||||||
|
it("should accept valid auth response", () => {
|
||||||
|
const authResponse: AuthResponse = {
|
||||||
|
accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
user: {
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
discordId: "discord123",
|
||||||
|
email: "test@example.com",
|
||||||
|
id: "user123",
|
||||||
|
inDiscord: true,
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: false,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: false,
|
||||||
|
username: "testuser",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(authResponse.accessToken).toContain("eyJ");
|
||||||
|
expect(authResponse.user.username).toBe("testuser");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BookStatus } from "../src/lib/book.types";
|
||||||
|
import type { Book, CreateBookDto, UpdateBookDto } from "../src/lib/book.types";
|
||||||
|
|
||||||
|
describe("book Types", () => {
|
||||||
|
describe("bookStatus enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(BookStatus.reading).toBe("READING");
|
||||||
|
expect(BookStatus.finished).toBe("FINISHED");
|
||||||
|
expect(BookStatus.toRead).toBe("TO_READ");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(BookStatus);
|
||||||
|
expect(values).toHaveLength(3);
|
||||||
|
expect(values).toContain("READING");
|
||||||
|
expect(values).toContain("FINISHED");
|
||||||
|
expect(values).toContain("TO_READ");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("book interface", () => {
|
||||||
|
it("should accept valid book object with minimal fields", () => {
|
||||||
|
const book: Book = {
|
||||||
|
author: "F. Scott Fitzgerald",
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
dateAdded: new Date("2024-01-15"),
|
||||||
|
id: "book123",
|
||||||
|
links: [],
|
||||||
|
status: BookStatus.reading,
|
||||||
|
tags: [],
|
||||||
|
title: "The Great Gatsby",
|
||||||
|
updatedAt: new Date("2024-01-16"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(book.isbn).toBeUndefined();
|
||||||
|
expect(book.dateFinished).toBeUndefined();
|
||||||
|
expect(book.rating).toBeUndefined();
|
||||||
|
expect(book.notes).toBeUndefined();
|
||||||
|
expect(book.coverImage).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid book object with all fields", () => {
|
||||||
|
const fullBook: Book = {
|
||||||
|
author: "George Orwell",
|
||||||
|
coverImage: "https://example.com/1984-cover.jpg",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
dateAdded: new Date("2024-01-01"),
|
||||||
|
dateFinished: new Date("2024-01-20"),
|
||||||
|
id: "book456",
|
||||||
|
isbn: "978-0-452-28423-4",
|
||||||
|
links: [
|
||||||
|
{ title: "Goodreads", url: "https://goodreads.com/book/1984" },
|
||||||
|
{ title: "Wikipedia", url: "https://wikipedia.org/wiki/1984" },
|
||||||
|
],
|
||||||
|
notes: "A dystopian masterpiece that remains relevant",
|
||||||
|
rating: 5,
|
||||||
|
status: BookStatus.finished,
|
||||||
|
tags: [ "dystopian", "classic", "political" ],
|
||||||
|
title: "1984",
|
||||||
|
updatedAt: new Date("2024-01-20"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullBook.isbn).toBe("978-0-452-28423-4");
|
||||||
|
expect(fullBook.rating).toBe(5);
|
||||||
|
expect(fullBook.tags).toHaveLength(3);
|
||||||
|
expect(fullBook.links).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createBookDto interface", () => {
|
||||||
|
it("should accept DTO with required fields only", () => {
|
||||||
|
const createDto: CreateBookDto = {
|
||||||
|
author: "Harper Lee",
|
||||||
|
status: BookStatus.toRead,
|
||||||
|
title: "To Kill a Mockingbird",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.isbn).toBeUndefined();
|
||||||
|
expect(createDto.rating).toBeUndefined();
|
||||||
|
expect(createDto.notes).toBeUndefined();
|
||||||
|
expect(createDto.coverImage).toBeUndefined();
|
||||||
|
expect(createDto.tags).toBeUndefined();
|
||||||
|
expect(createDto.links).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept DTO with all fields", () => {
|
||||||
|
const fullCreateDto: CreateBookDto = {
|
||||||
|
author: "J.R.R. Tolkien",
|
||||||
|
coverImage: "https://example.com/hobbit-cover.jpg",
|
||||||
|
isbn: "978-0-547-92822-7",
|
||||||
|
links: [ { title: "Author Website", url: "https://tolkien.com" } ],
|
||||||
|
notes: "Starting the journey to Middle-earth",
|
||||||
|
rating: 4,
|
||||||
|
status: BookStatus.reading,
|
||||||
|
tags: [ "fantasy", "adventure", "classic" ],
|
||||||
|
title: "The Hobbit",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullCreateDto.isbn).toBe("978-0-547-92822-7");
|
||||||
|
expect(fullCreateDto.rating).toBe(4);
|
||||||
|
expect(fullCreateDto.tags).toEqual([ "fantasy", "adventure", "classic" ]);
|
||||||
|
expect(fullCreateDto.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateBookDto type", () => {
|
||||||
|
it("should accept empty update DTO", () => {
|
||||||
|
const emptyUpdate: UpdateBookDto = {};
|
||||||
|
expect(emptyUpdate).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept partial updates including dateFinished", () => {
|
||||||
|
const partialUpdate: UpdateBookDto = {
|
||||||
|
dateFinished: new Date("2024-02-01"),
|
||||||
|
rating: 5,
|
||||||
|
status: BookStatus.finished,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(partialUpdate.status).toBe(BookStatus.finished);
|
||||||
|
expect(partialUpdate.dateFinished).toEqual(new Date("2024-02-01"));
|
||||||
|
expect(partialUpdate.rating).toBe(5);
|
||||||
|
expect(partialUpdate.title).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept full update", () => {
|
||||||
|
const fullUpdate: UpdateBookDto = {
|
||||||
|
author: "Different Author",
|
||||||
|
coverImage: "https://example.com/new-cover.jpg",
|
||||||
|
dateFinished: new Date("2024-02-15"),
|
||||||
|
isbn: "978-1-234-56789-0",
|
||||||
|
links: [ { title: "New Link", url: "https://newlink.com" } ],
|
||||||
|
notes: "Updated notes after finishing",
|
||||||
|
rating: 3,
|
||||||
|
status: BookStatus.finished,
|
||||||
|
tags: [ "updated", "tags" ],
|
||||||
|
title: "Updated Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullUpdate.title).toBe("Updated Title");
|
||||||
|
expect(fullUpdate.dateFinished).toEqual(new Date("2024-02-15"));
|
||||||
|
expect(fullUpdate.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* @copyright 2026 NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CommentUser, Comment, CreateCommentDto } from "../src/lib/comment.types";
|
||||||
|
|
||||||
|
describe("comment Types", () => {
|
||||||
|
describe("commentUser interface", () => {
|
||||||
|
it("should accept valid user object with minimal fields", () => {
|
||||||
|
const user: CommentUser = {
|
||||||
|
id: "user123",
|
||||||
|
username: "testuser",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(user.avatar).toBeUndefined();
|
||||||
|
expect(user.inDiscord).toBeUndefined();
|
||||||
|
expect(user.isVip).toBeUndefined();
|
||||||
|
expect(user.isMod).toBeUndefined();
|
||||||
|
expect(user.isStaff).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid user object with all fields", () => {
|
||||||
|
const fullUser: CommentUser = {
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
id: "user456",
|
||||||
|
inDiscord: true,
|
||||||
|
isMod: true,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: true,
|
||||||
|
username: "vipmoduser",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullUser.avatar).toBe("https://example.com/avatar.png");
|
||||||
|
expect(fullUser.inDiscord).toBeTruthy();
|
||||||
|
expect(fullUser.isVip).toBeTruthy();
|
||||||
|
expect(fullUser.isMod).toBeTruthy();
|
||||||
|
expect(fullUser.isStaff).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("comment interface", () => {
|
||||||
|
it("should accept valid comment object with minimal fields", () => {
|
||||||
|
const comment: Comment = {
|
||||||
|
content: "This is a great book!",
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
id: "comment123",
|
||||||
|
updatedAt: new Date("2024-01-15"),
|
||||||
|
user: {
|
||||||
|
id: "user123",
|
||||||
|
username: "bookworm",
|
||||||
|
},
|
||||||
|
userId: "user123",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(comment.rawContent).toBeUndefined();
|
||||||
|
expect(comment.gameId).toBeUndefined();
|
||||||
|
expect(comment.bookId).toBeUndefined();
|
||||||
|
expect(comment.musicId).toBeUndefined();
|
||||||
|
expect(comment.artId).toBeUndefined();
|
||||||
|
expect(comment.showId).toBeUndefined();
|
||||||
|
expect(comment.mangaId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept comment for a book", () => {
|
||||||
|
const bookComment: Comment = {
|
||||||
|
bookId: "book123",
|
||||||
|
content: "<p>Amazing read!</p>",
|
||||||
|
createdAt: new Date("2024-01-20"),
|
||||||
|
id: "comment456",
|
||||||
|
rawContent: "Amazing read!",
|
||||||
|
updatedAt: new Date("2024-01-21"),
|
||||||
|
user: {
|
||||||
|
avatar: "https://example.com/reader.png",
|
||||||
|
id: "user789",
|
||||||
|
inDiscord: true,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: false,
|
||||||
|
username: "reader",
|
||||||
|
},
|
||||||
|
userId: "user789",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(bookComment.bookId).toBe("book123");
|
||||||
|
expect(bookComment.rawContent).toBe("Amazing read!");
|
||||||
|
expect(bookComment.gameId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept comment for a game", () => {
|
||||||
|
const gameComment: Comment = {
|
||||||
|
content: "Best game ever!",
|
||||||
|
createdAt: new Date("2024-02-01"),
|
||||||
|
gameId: "game789",
|
||||||
|
id: "comment789",
|
||||||
|
updatedAt: new Date("2024-02-01"),
|
||||||
|
user: {
|
||||||
|
id: "user456",
|
||||||
|
username: "gamer",
|
||||||
|
},
|
||||||
|
userId: "user456",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(gameComment.gameId).toBe("game789");
|
||||||
|
expect(gameComment.bookId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept comment for music", () => {
|
||||||
|
const musicComment: Comment = {
|
||||||
|
content: "Beautiful album",
|
||||||
|
createdAt: new Date("2024-02-10"),
|
||||||
|
id: "comment999",
|
||||||
|
musicId: "music123",
|
||||||
|
updatedAt: new Date("2024-02-10"),
|
||||||
|
user: {
|
||||||
|
id: "user111",
|
||||||
|
username: "musiclover",
|
||||||
|
},
|
||||||
|
userId: "user111",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(musicComment.musicId).toBe("music123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept comment for art", () => {
|
||||||
|
const artComment: Comment = {
|
||||||
|
artId: "art456",
|
||||||
|
content: "Stunning artwork!",
|
||||||
|
createdAt: new Date("2024-02-15"),
|
||||||
|
id: "comment111",
|
||||||
|
updatedAt: new Date("2024-02-15"),
|
||||||
|
user: {
|
||||||
|
id: "user222",
|
||||||
|
username: "artcritic",
|
||||||
|
},
|
||||||
|
userId: "user222",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(artComment.artId).toBe("art456");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept comment for a show", () => {
|
||||||
|
const showComment: Comment = {
|
||||||
|
content: "Great series!",
|
||||||
|
createdAt: new Date("2024-02-20"),
|
||||||
|
id: "comment222",
|
||||||
|
showId: "show789",
|
||||||
|
updatedAt: new Date("2024-02-20"),
|
||||||
|
user: {
|
||||||
|
id: "user333",
|
||||||
|
username: "tvfan",
|
||||||
|
},
|
||||||
|
userId: "user333",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(showComment.showId).toBe("show789");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept comment for manga", () => {
|
||||||
|
const mangaComment: Comment = {
|
||||||
|
content: "Awesome manga!",
|
||||||
|
createdAt: new Date("2024-02-25"),
|
||||||
|
id: "comment333",
|
||||||
|
mangaId: "manga123",
|
||||||
|
updatedAt: new Date("2024-02-25"),
|
||||||
|
user: {
|
||||||
|
id: "user444",
|
||||||
|
username: "mangareader",
|
||||||
|
},
|
||||||
|
userId: "user444",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(mangaComment.mangaId).toBe("manga123");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createCommentDto interface", () => {
|
||||||
|
it("should accept DTO with content", () => {
|
||||||
|
const createDto: CreateCommentDto = {
|
||||||
|
content: "This is my comment",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.content).toBe("This is my comment");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept DTO with formatted content", () => {
|
||||||
|
const createDto: CreateCommentDto = {
|
||||||
|
content: "<p>This is <strong>formatted</strong> content!</p>",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.content).toBe("<p>This is <strong>formatted</strong> content!</p>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Link } from "../src/lib/common.types";
|
||||||
|
|
||||||
|
describe("common Types", () => {
|
||||||
|
describe("link interface", () => {
|
||||||
|
it("should accept valid Link objects", () => {
|
||||||
|
const validLink: Link = {
|
||||||
|
title: "Example Website",
|
||||||
|
url: "https://example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(validLink.title).toBe("Example Website");
|
||||||
|
expect(validLink.url).toBe("https://example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with different URL formats", () => {
|
||||||
|
const links: Array<Link> = [
|
||||||
|
{ title: "HTTP URL", url: "http://example.com" },
|
||||||
|
{ title: "HTTPS URL", url: "https://example.com" },
|
||||||
|
{ title: "Relative URL", url: "/path/to/page" },
|
||||||
|
{ title: "URL with query", url: "https://example.com?query=value" },
|
||||||
|
{ title: "URL with anchor", url: "https://example.com#section" },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(links).toHaveLength(5);
|
||||||
|
for (const link of links) {
|
||||||
|
expect(link).toHaveProperty("title");
|
||||||
|
expect(link).toHaveProperty("url");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GameStatus } from "../src/lib/game.types";
|
||||||
|
import type { Game, CreateGameDto, UpdateGameDto } from "../src/lib/game.types";
|
||||||
|
|
||||||
|
describe("game Types", () => {
|
||||||
|
describe("gameStatus enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(GameStatus.playing).toBe("PLAYING");
|
||||||
|
expect(GameStatus.completed).toBe("COMPLETED");
|
||||||
|
expect(GameStatus.backlog).toBe("BACKLOG");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(GameStatus);
|
||||||
|
expect(values).toHaveLength(3);
|
||||||
|
expect(values).toContain("PLAYING");
|
||||||
|
expect(values).toContain("COMPLETED");
|
||||||
|
expect(values).toContain("BACKLOG");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("game interface", () => {
|
||||||
|
it("should accept valid game object with minimal fields", () => {
|
||||||
|
const game: Game = {
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
dateAdded: new Date("2024-01-15"),
|
||||||
|
id: "game123",
|
||||||
|
links: [],
|
||||||
|
status: GameStatus.playing,
|
||||||
|
tags: [],
|
||||||
|
title: "The Legend of Zelda: Breath of the Wild",
|
||||||
|
updatedAt: new Date("2024-01-16"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(game.platform).toBeUndefined();
|
||||||
|
expect(game.dateCompleted).toBeUndefined();
|
||||||
|
expect(game.rating).toBeUndefined();
|
||||||
|
expect(game.notes).toBeUndefined();
|
||||||
|
expect(game.coverImage).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid game object with all fields", () => {
|
||||||
|
const fullGame: Game = {
|
||||||
|
coverImage: "https://example.com/hades-cover.jpg",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
dateAdded: new Date("2024-01-01"),
|
||||||
|
dateCompleted: new Date("2024-01-20"),
|
||||||
|
id: "game456",
|
||||||
|
links: [
|
||||||
|
{ title: "Steam", url: "https://store.steampowered.com/app/hades" },
|
||||||
|
{ title: "Official Site", url: "https://supergiantgames.com/hades" },
|
||||||
|
],
|
||||||
|
notes: "One of the best roguelikes ever made",
|
||||||
|
platform: "Nintendo Switch",
|
||||||
|
rating: 5,
|
||||||
|
status: GameStatus.completed,
|
||||||
|
tags: [ "roguelike", "indie", "action", "mythology" ],
|
||||||
|
title: "Hades",
|
||||||
|
updatedAt: new Date("2024-01-20"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullGame.platform).toBe("Nintendo Switch");
|
||||||
|
expect(fullGame.rating).toBe(5);
|
||||||
|
expect(fullGame.tags).toHaveLength(4);
|
||||||
|
expect(fullGame.links).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept games on different platforms", () => {
|
||||||
|
const pcGame: Game = {
|
||||||
|
createdAt: new Date("2024-02-01"),
|
||||||
|
dateAdded: new Date("2024-02-01"),
|
||||||
|
id: "game789",
|
||||||
|
links: [],
|
||||||
|
platform: "PC",
|
||||||
|
status: GameStatus.completed,
|
||||||
|
tags: [ "puzzle", "first-person" ],
|
||||||
|
title: "Portal 2",
|
||||||
|
updatedAt: new Date("2024-02-01"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ps5Game: Game = {
|
||||||
|
createdAt: new Date("2024-02-05"),
|
||||||
|
dateAdded: new Date("2024-02-05"),
|
||||||
|
id: "game999",
|
||||||
|
links: [],
|
||||||
|
platform: "PlayStation 5",
|
||||||
|
status: GameStatus.backlog,
|
||||||
|
tags: [ "action", "superhero" ],
|
||||||
|
title: "Spider-Man: Miles Morales",
|
||||||
|
updatedAt: new Date("2024-02-05"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(pcGame.platform).toBe("PC");
|
||||||
|
expect(ps5Game.platform).toBe("PlayStation 5");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createGameDto interface", () => {
|
||||||
|
it("should accept DTO with required fields only", () => {
|
||||||
|
const createDto: CreateGameDto = {
|
||||||
|
status: GameStatus.backlog,
|
||||||
|
title: "Elden Ring",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.platform).toBeUndefined();
|
||||||
|
expect(createDto.rating).toBeUndefined();
|
||||||
|
expect(createDto.notes).toBeUndefined();
|
||||||
|
expect(createDto.coverImage).toBeUndefined();
|
||||||
|
expect(createDto.tags).toBeUndefined();
|
||||||
|
expect(createDto.links).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept DTO with all fields", () => {
|
||||||
|
const fullCreateDto: CreateGameDto = {
|
||||||
|
coverImage: "https://example.com/hollow-knight.jpg",
|
||||||
|
links: [ { title: "Wiki", url: "https://hollowknight.wiki" } ],
|
||||||
|
notes: "Beautiful metroidvania with challenging gameplay",
|
||||||
|
platform: "Nintendo Switch",
|
||||||
|
rating: 4,
|
||||||
|
status: GameStatus.playing,
|
||||||
|
tags: [ "metroidvania", "indie", "platformer" ],
|
||||||
|
title: "Hollow Knight",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullCreateDto.platform).toBe("Nintendo Switch");
|
||||||
|
expect(fullCreateDto.rating).toBe(4);
|
||||||
|
expect(fullCreateDto.tags).toEqual([ "metroidvania", "indie", "platformer" ]);
|
||||||
|
expect(fullCreateDto.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateGameDto type", () => {
|
||||||
|
it("should accept empty update DTO", () => {
|
||||||
|
const emptyUpdate: UpdateGameDto = {};
|
||||||
|
expect(emptyUpdate).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept partial updates including dateCompleted", () => {
|
||||||
|
const partialUpdate: UpdateGameDto = {
|
||||||
|
dateCompleted: new Date("2024-02-10"),
|
||||||
|
rating: 5,
|
||||||
|
status: GameStatus.completed,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(partialUpdate.status).toBe(GameStatus.completed);
|
||||||
|
expect(partialUpdate.dateCompleted).toEqual(new Date("2024-02-10"));
|
||||||
|
expect(partialUpdate.rating).toBe(5);
|
||||||
|
expect(partialUpdate.title).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept full update", () => {
|
||||||
|
const fullUpdate: UpdateGameDto = {
|
||||||
|
coverImage: "https://example.com/new-cover.jpg",
|
||||||
|
dateCompleted: new Date("2024-02-15"),
|
||||||
|
links: [ { title: "New Link", url: "https://newlink.com" } ],
|
||||||
|
notes: "Updated after completion",
|
||||||
|
platform: "Xbox Series X",
|
||||||
|
rating: 3,
|
||||||
|
status: GameStatus.completed,
|
||||||
|
tags: [ "updated", "tags" ],
|
||||||
|
title: "Updated Game Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullUpdate.title).toBe("Updated Game Title");
|
||||||
|
expect(fullUpdate.platform).toBe("Xbox Series X");
|
||||||
|
expect(fullUpdate.dateCompleted).toEqual(new Date("2024-02-15"));
|
||||||
|
expect(fullUpdate.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Like, CreateLikeDto, LikeResponse, LikedItemDto, LikeCountDto } from "../src/lib/like.types";
|
||||||
|
|
||||||
|
describe("like Types", () => {
|
||||||
|
describe("like interface", () => {
|
||||||
|
it("should accept valid like object for a book", () => {
|
||||||
|
const bookLike: Like = {
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
entityId: "book456",
|
||||||
|
entityType: "book",
|
||||||
|
id: "like123",
|
||||||
|
userId: "user123",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(bookLike.entityType).toBe("book");
|
||||||
|
expect(bookLike.entityId).toBe("book456");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid like object for a game", () => {
|
||||||
|
const gameLike: Like = {
|
||||||
|
createdAt: new Date("2024-01-20"),
|
||||||
|
entityId: "game123",
|
||||||
|
entityType: "game",
|
||||||
|
id: "like456",
|
||||||
|
userId: "user789",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(gameLike.entityType).toBe("game");
|
||||||
|
expect(gameLike.entityId).toBe("game123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept likes for all entity types", () => {
|
||||||
|
const showLike: Like = {
|
||||||
|
createdAt: new Date("2024-02-01"),
|
||||||
|
entityId: "show222",
|
||||||
|
entityType: "show",
|
||||||
|
id: "like789",
|
||||||
|
userId: "user111",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mangaLike: Like = {
|
||||||
|
createdAt: new Date("2024-02-05"),
|
||||||
|
entityId: "manga444",
|
||||||
|
entityType: "manga",
|
||||||
|
id: "like999",
|
||||||
|
userId: "user333",
|
||||||
|
};
|
||||||
|
|
||||||
|
const musicLike: Like = {
|
||||||
|
createdAt: new Date("2024-02-10"),
|
||||||
|
entityId: "music666",
|
||||||
|
entityType: "music",
|
||||||
|
id: "like111",
|
||||||
|
userId: "user555",
|
||||||
|
};
|
||||||
|
|
||||||
|
const artLike: Like = {
|
||||||
|
createdAt: new Date("2024-02-15"),
|
||||||
|
entityId: "art888",
|
||||||
|
entityType: "art",
|
||||||
|
id: "like222",
|
||||||
|
userId: "user777",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(showLike.entityType).toBe("show");
|
||||||
|
expect(mangaLike.entityType).toBe("manga");
|
||||||
|
expect(musicLike.entityType).toBe("music");
|
||||||
|
expect(artLike.entityType).toBe("art");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createLikeDto type", () => {
|
||||||
|
it("should pick only entityType and entityId from Like", () => {
|
||||||
|
const createDto: CreateLikeDto = {
|
||||||
|
entityId: "book123",
|
||||||
|
entityType: "book",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.entityType).toBe("book");
|
||||||
|
expect(createDto.entityId).toBe("book123");
|
||||||
|
// Verify it only has these two properties
|
||||||
|
expect(Object.keys(createDto)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept different entity types", () => {
|
||||||
|
const gameDto: CreateLikeDto = {
|
||||||
|
entityId: "game456",
|
||||||
|
entityType: "game",
|
||||||
|
};
|
||||||
|
|
||||||
|
const artDto: CreateLikeDto = {
|
||||||
|
entityId: "art789",
|
||||||
|
entityType: "art",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(gameDto.entityType).toBe("game");
|
||||||
|
expect(artDto.entityType).toBe("art");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("likeCountDto interface", () => {
|
||||||
|
it("should accept valid like count object", () => {
|
||||||
|
const likeCount: LikeCountDto = {
|
||||||
|
count: 42,
|
||||||
|
entityId: "book123",
|
||||||
|
entityType: "book",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(likeCount.count).toBe(42);
|
||||||
|
expect(likeCount.entityType).toBe("book");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept like counts for different entities", () => {
|
||||||
|
const gameLikeCount: LikeCountDto = {
|
||||||
|
count: 15,
|
||||||
|
entityId: "game456",
|
||||||
|
entityType: "game",
|
||||||
|
};
|
||||||
|
|
||||||
|
const showLikeCount: LikeCountDto = {
|
||||||
|
count: 0,
|
||||||
|
entityId: "show789",
|
||||||
|
entityType: "show",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(gameLikeCount.count).toBe(15);
|
||||||
|
expect(showLikeCount.count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("likedItemDto interface", () => {
|
||||||
|
it("should accept liked item with unknown item type", () => {
|
||||||
|
const likedBook: LikedItemDto = {
|
||||||
|
item: {
|
||||||
|
author: "F. Scott Fitzgerald",
|
||||||
|
id: "book456",
|
||||||
|
title: "The Great Gatsby",
|
||||||
|
// ... other book properties
|
||||||
|
},
|
||||||
|
like: {
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
entityId: "book456",
|
||||||
|
entityType: "book",
|
||||||
|
id: "like123",
|
||||||
|
userId: "user123",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(likedBook.like.entityType).toBe("book");
|
||||||
|
expect(likedBook.item).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept liked items for different entity types", () => {
|
||||||
|
const likedGame: LikedItemDto = {
|
||||||
|
item: {
|
||||||
|
id: "game123",
|
||||||
|
platform: "Nintendo Switch",
|
||||||
|
title: "Hades",
|
||||||
|
// ... other game properties
|
||||||
|
},
|
||||||
|
like: {
|
||||||
|
createdAt: new Date("2024-02-01"),
|
||||||
|
entityId: "game123",
|
||||||
|
entityType: "game",
|
||||||
|
id: "like456",
|
||||||
|
userId: "user789",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const likedArt: LikedItemDto = {
|
||||||
|
item: {
|
||||||
|
artist: "Jane Doe",
|
||||||
|
id: "art456",
|
||||||
|
imageUrl: "https://example.com/sunset.jpg",
|
||||||
|
title: "Beautiful Sunset",
|
||||||
|
// ... other art properties
|
||||||
|
},
|
||||||
|
like: {
|
||||||
|
createdAt: new Date("2024-02-10"),
|
||||||
|
entityId: "art456",
|
||||||
|
entityType: "art",
|
||||||
|
id: "like789",
|
||||||
|
userId: "user999",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(likedGame.like.entityType).toBe("game");
|
||||||
|
expect(likedArt.like.entityType).toBe("art");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("likeResponse interface", () => {
|
||||||
|
it("should accept response with liked true", () => {
|
||||||
|
const likedResponse: LikeResponse = {
|
||||||
|
count: 10,
|
||||||
|
liked: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(likedResponse.liked).toBeTruthy();
|
||||||
|
expect(likedResponse.count).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept response with liked false", () => {
|
||||||
|
const notLikedResponse: LikeResponse = {
|
||||||
|
count: 5,
|
||||||
|
liked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(notLikedResponse.liked).toBeFalsy();
|
||||||
|
expect(notLikedResponse.count).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept response with zero count", () => {
|
||||||
|
const zeroResponse: LikeResponse = {
|
||||||
|
count: 0,
|
||||||
|
liked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(zeroResponse.liked).toBeFalsy();
|
||||||
|
expect(zeroResponse.count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MangaStatus } from "../src/lib/manga.types";
|
||||||
|
import type { Manga, CreateMangaDto, UpdateMangaDto } from "../src/lib/manga.types";
|
||||||
|
|
||||||
|
describe("manga Types", () => {
|
||||||
|
describe("mangaStatus enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(MangaStatus.reading).toBe("READING");
|
||||||
|
expect(MangaStatus.completed).toBe("COMPLETED");
|
||||||
|
expect(MangaStatus.wantToRead).toBe("WANT_TO_READ");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(MangaStatus);
|
||||||
|
expect(values).toHaveLength(3);
|
||||||
|
expect(values).toContain("READING");
|
||||||
|
expect(values).toContain("COMPLETED");
|
||||||
|
expect(values).toContain("WANT_TO_READ");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("manga interface", () => {
|
||||||
|
it("should accept valid manga object with minimal fields", () => {
|
||||||
|
const manga: Manga = {
|
||||||
|
author: "Tsugumi Ohba",
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
dateAdded: new Date("2024-01-15"),
|
||||||
|
id: "manga123",
|
||||||
|
links: [],
|
||||||
|
status: MangaStatus.reading,
|
||||||
|
tags: [],
|
||||||
|
title: "Death Note",
|
||||||
|
updatedAt: new Date("2024-01-16"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(manga.dateCompleted).toBeUndefined();
|
||||||
|
expect(manga.rating).toBeUndefined();
|
||||||
|
expect(manga.notes).toBeUndefined();
|
||||||
|
expect(manga.coverImage).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid manga object with all fields", () => {
|
||||||
|
const fullManga: Manga = {
|
||||||
|
author: "Eiichiro Oda",
|
||||||
|
coverImage: "https://example.com/onepiece-cover.jpg",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
dateAdded: new Date("2024-01-01"),
|
||||||
|
dateCompleted: undefined,
|
||||||
|
id: "manga456",
|
||||||
|
links: [
|
||||||
|
{ title: "MyAnimeList", url: "https://myanimelist.net/manga/13" },
|
||||||
|
{ title: "Official Site", url: "https://one-piece.com" },
|
||||||
|
],
|
||||||
|
|
||||||
|
notes: "Epic adventure that keeps getting better",
|
||||||
|
|
||||||
|
// Still ongoing
|
||||||
|
rating: 5,
|
||||||
|
|
||||||
|
status: MangaStatus.reading,
|
||||||
|
|
||||||
|
tags: [ "adventure", "shounen", "pirates", "long-running" ],
|
||||||
|
title: "One Piece",
|
||||||
|
updatedAt: new Date("2024-02-01"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullManga.author).toBe("Eiichiro Oda");
|
||||||
|
expect(fullManga.rating).toBe(5);
|
||||||
|
expect(fullManga.tags).toHaveLength(4);
|
||||||
|
expect(fullManga.links).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept completed manga", () => {
|
||||||
|
const completedManga: Manga = {
|
||||||
|
author: "Hiromu Arakawa",
|
||||||
|
coverImage: "https://example.com/fma-cover.jpg",
|
||||||
|
createdAt: new Date("2023-12-01"),
|
||||||
|
dateAdded: new Date("2023-12-01"),
|
||||||
|
dateCompleted: new Date("2024-01-30"),
|
||||||
|
id: "manga789",
|
||||||
|
links: [],
|
||||||
|
notes: "Perfect from start to finish",
|
||||||
|
rating: 5,
|
||||||
|
status: MangaStatus.completed,
|
||||||
|
tags: [ "shounen", "adventure", "alchemy" ],
|
||||||
|
title: "Fullmetal Alchemist",
|
||||||
|
updatedAt: new Date("2024-01-30"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(completedManga.status).toBe(MangaStatus.completed);
|
||||||
|
expect(completedManga.dateCompleted).toEqual(new Date("2024-01-30"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createMangaDto interface", () => {
|
||||||
|
it("should accept DTO with required fields only", () => {
|
||||||
|
const createDto: CreateMangaDto = {
|
||||||
|
author: "Hajime Isayama",
|
||||||
|
status: MangaStatus.wantToRead,
|
||||||
|
title: "Attack on Titan",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.rating).toBeUndefined();
|
||||||
|
expect(createDto.notes).toBeUndefined();
|
||||||
|
expect(createDto.coverImage).toBeUndefined();
|
||||||
|
expect(createDto.tags).toBeUndefined();
|
||||||
|
expect(createDto.links).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept DTO with all fields", () => {
|
||||||
|
const fullCreateDto: CreateMangaDto = {
|
||||||
|
author: "Kohei Horikoshi",
|
||||||
|
coverImage: "https://example.com/mha-cover.jpg",
|
||||||
|
links: [ { title: "Wiki", url: "https://mha.wiki" } ],
|
||||||
|
notes: "Great superhero manga with unique powers",
|
||||||
|
rating: 4,
|
||||||
|
status: MangaStatus.reading,
|
||||||
|
tags: [ "shounen", "superhero", "school" ],
|
||||||
|
title: "My Hero Academia",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullCreateDto.author).toBe("Kohei Horikoshi");
|
||||||
|
expect(fullCreateDto.rating).toBe(4);
|
||||||
|
expect(fullCreateDto.tags).toEqual([ "shounen", "superhero", "school" ]);
|
||||||
|
expect(fullCreateDto.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateMangaDto type", () => {
|
||||||
|
it("should accept empty update DTO", () => {
|
||||||
|
const emptyUpdate: UpdateMangaDto = {};
|
||||||
|
expect(emptyUpdate).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept partial updates including dateCompleted", () => {
|
||||||
|
const partialUpdate: UpdateMangaDto = {
|
||||||
|
dateCompleted: new Date("2024-02-10"),
|
||||||
|
rating: 4,
|
||||||
|
status: MangaStatus.completed,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(partialUpdate.status).toBe(MangaStatus.completed);
|
||||||
|
expect(partialUpdate.dateCompleted).toEqual(new Date("2024-02-10"));
|
||||||
|
expect(partialUpdate.rating).toBe(4);
|
||||||
|
expect(partialUpdate.title).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept full update", () => {
|
||||||
|
const fullUpdate: UpdateMangaDto = {
|
||||||
|
author: "Different Author",
|
||||||
|
coverImage: "https://example.com/new-manga-cover.jpg",
|
||||||
|
dateCompleted: new Date("2024-02-15"),
|
||||||
|
links: [ { title: "New Link", url: "https://newlink.com" } ],
|
||||||
|
notes: "Updated after finishing the series",
|
||||||
|
rating: 3,
|
||||||
|
status: MangaStatus.completed,
|
||||||
|
tags: [ "updated", "tags" ],
|
||||||
|
title: "Updated Manga Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullUpdate.title).toBe("Updated Manga Title");
|
||||||
|
expect(fullUpdate.author).toBe("Different Author");
|
||||||
|
expect(fullUpdate.dateCompleted).toEqual(new Date("2024-02-15"));
|
||||||
|
expect(fullUpdate.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MusicStatus, MusicType } from "../src/lib/music.types";
|
||||||
|
import type { Music, CreateMusicDto, UpdateMusicDto } from "../src/lib/music.types";
|
||||||
|
|
||||||
|
describe("music Types", () => {
|
||||||
|
describe("musicType enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(MusicType.album).toBe("ALBUM");
|
||||||
|
expect(MusicType.single).toBe("SINGLE");
|
||||||
|
expect(MusicType.ep).toBe("EP");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(MusicType);
|
||||||
|
expect(values).toHaveLength(3);
|
||||||
|
expect(values).toContain("ALBUM");
|
||||||
|
expect(values).toContain("SINGLE");
|
||||||
|
expect(values).toContain("EP");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("musicStatus enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(MusicStatus.listening).toBe("LISTENING");
|
||||||
|
expect(MusicStatus.completed).toBe("COMPLETED");
|
||||||
|
expect(MusicStatus.wantToListen).toBe("WANT_TO_LISTEN");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(MusicStatus);
|
||||||
|
expect(values).toHaveLength(3);
|
||||||
|
expect(values).toContain("LISTENING");
|
||||||
|
expect(values).toContain("COMPLETED");
|
||||||
|
expect(values).toContain("WANT_TO_LISTEN");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("music interface", () => {
|
||||||
|
it("should accept valid music object with minimal fields", () => {
|
||||||
|
const music: Music = {
|
||||||
|
artist: "Pink Floyd",
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
dateAdded: new Date("2024-01-15"),
|
||||||
|
id: "music123",
|
||||||
|
links: [],
|
||||||
|
status: MusicStatus.listening,
|
||||||
|
tags: [],
|
||||||
|
title: "Dark Side of the Moon",
|
||||||
|
type: MusicType.album,
|
||||||
|
updatedAt: new Date("2024-01-16"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(music.dateCompleted).toBeUndefined();
|
||||||
|
expect(music.rating).toBeUndefined();
|
||||||
|
expect(music.notes).toBeUndefined();
|
||||||
|
expect(music.coverArt).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid music object with all fields", () => {
|
||||||
|
const fullMusic: Music = {
|
||||||
|
artist: "Pink Floyd",
|
||||||
|
coverArt: "https://example.com/the-wall-cover.jpg",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
dateAdded: new Date("2024-01-01"),
|
||||||
|
dateCompleted: new Date("2024-01-20"),
|
||||||
|
id: "music456",
|
||||||
|
links: [
|
||||||
|
{ title: "Spotify", url: "https://spotify.com/album/the-wall" },
|
||||||
|
{ title: "Apple Music", url: "https://music.apple.com/album/the-wall" },
|
||||||
|
],
|
||||||
|
notes: "A rock opera masterpiece",
|
||||||
|
rating: 5,
|
||||||
|
status: MusicStatus.completed,
|
||||||
|
tags: [ "progressive rock", "concept album", "classic" ],
|
||||||
|
title: "The Wall",
|
||||||
|
type: MusicType.album,
|
||||||
|
updatedAt: new Date("2024-01-20"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullMusic.artist).toBe("Pink Floyd");
|
||||||
|
expect(fullMusic.rating).toBe(5);
|
||||||
|
expect(fullMusic.tags).toHaveLength(3);
|
||||||
|
expect(fullMusic.links).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept different music types", () => {
|
||||||
|
const single: Music = {
|
||||||
|
artist: "The Weeknd",
|
||||||
|
createdAt: new Date("2024-02-01"),
|
||||||
|
dateAdded: new Date("2024-02-01"),
|
||||||
|
id: "music789",
|
||||||
|
links: [],
|
||||||
|
status: MusicStatus.completed,
|
||||||
|
tags: [ "pop", "synthwave" ],
|
||||||
|
title: "Blinding Lights",
|
||||||
|
type: MusicType.single,
|
||||||
|
updatedAt: new Date("2024-02-01"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ep: Music = {
|
||||||
|
artist: "The Weeknd",
|
||||||
|
createdAt: new Date("2024-02-05"),
|
||||||
|
dateAdded: new Date("2024-02-05"),
|
||||||
|
id: "music999",
|
||||||
|
links: [],
|
||||||
|
status: MusicStatus.wantToListen,
|
||||||
|
tags: [ "r&b", "dark" ],
|
||||||
|
title: "My Dear Melancholy,",
|
||||||
|
type: MusicType.ep,
|
||||||
|
updatedAt: new Date("2024-02-05"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(single.type).toBe(MusicType.single);
|
||||||
|
expect(ep.type).toBe(MusicType.ep);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createMusicDto interface", () => {
|
||||||
|
it("should accept DTO with required fields only", () => {
|
||||||
|
const createDto: CreateMusicDto = {
|
||||||
|
artist: "Fleetwood Mac",
|
||||||
|
status: MusicStatus.wantToListen,
|
||||||
|
title: "Rumours",
|
||||||
|
type: MusicType.album,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.rating).toBeUndefined();
|
||||||
|
expect(createDto.notes).toBeUndefined();
|
||||||
|
expect(createDto.coverArt).toBeUndefined();
|
||||||
|
expect(createDto.tags).toBeUndefined();
|
||||||
|
expect(createDto.links).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept DTO with all fields", () => {
|
||||||
|
const fullCreateDto: CreateMusicDto = {
|
||||||
|
artist: "The Weeknd",
|
||||||
|
coverArt: "https://example.com/after-hours.jpg",
|
||||||
|
links: [ { title: "YouTube", url: "https://youtube.com/album/after-hours" } ],
|
||||||
|
notes: "Dark synthwave vibes",
|
||||||
|
rating: 4,
|
||||||
|
status: MusicStatus.listening,
|
||||||
|
tags: [ "synthwave", "pop", "r&b" ],
|
||||||
|
title: "After Hours",
|
||||||
|
type: MusicType.album,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullCreateDto.artist).toBe("The Weeknd");
|
||||||
|
expect(fullCreateDto.type).toBe(MusicType.album);
|
||||||
|
expect(fullCreateDto.rating).toBe(4);
|
||||||
|
expect(fullCreateDto.tags).toEqual([ "synthwave", "pop", "r&b" ]);
|
||||||
|
expect(fullCreateDto.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateMusicDto type", () => {
|
||||||
|
it("should accept empty update DTO", () => {
|
||||||
|
const emptyUpdate: UpdateMusicDto = {};
|
||||||
|
expect(emptyUpdate).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept partial updates including dateCompleted", () => {
|
||||||
|
const partialUpdate: UpdateMusicDto = {
|
||||||
|
dateCompleted: new Date("2024-02-10"),
|
||||||
|
rating: 5,
|
||||||
|
status: MusicStatus.completed,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(partialUpdate.status).toBe(MusicStatus.completed);
|
||||||
|
expect(partialUpdate.dateCompleted).toEqual(new Date("2024-02-10"));
|
||||||
|
expect(partialUpdate.rating).toBe(5);
|
||||||
|
expect(partialUpdate.title).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept full update", () => {
|
||||||
|
const fullUpdate: UpdateMusicDto = {
|
||||||
|
artist: "Different Artist",
|
||||||
|
coverArt: "https://example.com/new-cover.jpg",
|
||||||
|
dateCompleted: new Date("2024-02-15"),
|
||||||
|
links: [ { title: "New Link", url: "https://newlink.com" } ],
|
||||||
|
notes: "Updated after listening",
|
||||||
|
rating: 3,
|
||||||
|
status: MusicStatus.completed,
|
||||||
|
tags: [ "updated", "tags" ],
|
||||||
|
title: "Updated Album Title",
|
||||||
|
type: MusicType.ep,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullUpdate.title).toBe("Updated Album Title");
|
||||||
|
expect(fullUpdate.artist).toBe("Different Artist");
|
||||||
|
expect(fullUpdate.type).toBe(MusicType.ep);
|
||||||
|
expect(fullUpdate.dateCompleted).toEqual(new Date("2024-02-15"));
|
||||||
|
expect(fullUpdate.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ShowStatus, ShowType } from "../src/lib/show.types";
|
||||||
|
import type { Show, CreateShowDto, UpdateShowDto } from "../src/lib/show.types";
|
||||||
|
|
||||||
|
describe("show Types", () => {
|
||||||
|
describe("showType enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(ShowType.tvSeries).toBe("TV_SERIES");
|
||||||
|
expect(ShowType.anime).toBe("ANIME");
|
||||||
|
expect(ShowType.film).toBe("FILM");
|
||||||
|
expect(ShowType.documentary).toBe("DOCUMENTARY");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(ShowType);
|
||||||
|
expect(values).toHaveLength(4);
|
||||||
|
expect(values).toContain("TV_SERIES");
|
||||||
|
expect(values).toContain("ANIME");
|
||||||
|
expect(values).toContain("FILM");
|
||||||
|
expect(values).toContain("DOCUMENTARY");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("showStatus enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(ShowStatus.watching).toBe("WATCHING");
|
||||||
|
expect(ShowStatus.completed).toBe("COMPLETED");
|
||||||
|
expect(ShowStatus.wantToWatch).toBe("WANT_TO_WATCH");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(ShowStatus);
|
||||||
|
expect(values).toHaveLength(3);
|
||||||
|
expect(values).toContain("WATCHING");
|
||||||
|
expect(values).toContain("COMPLETED");
|
||||||
|
expect(values).toContain("WANT_TO_WATCH");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("show interface", () => {
|
||||||
|
it("should accept valid show object with minimal fields", () => {
|
||||||
|
const show: Show = {
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
dateAdded: new Date("2024-01-15"),
|
||||||
|
id: "show123",
|
||||||
|
links: [],
|
||||||
|
status: ShowStatus.watching,
|
||||||
|
tags: [],
|
||||||
|
title: "Breaking Bad",
|
||||||
|
type: ShowType.tvSeries,
|
||||||
|
updatedAt: new Date("2024-01-16"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(show.dateCompleted).toBeUndefined();
|
||||||
|
expect(show.rating).toBeUndefined();
|
||||||
|
expect(show.notes).toBeUndefined();
|
||||||
|
expect(show.coverImage).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept valid show object with all fields", () => {
|
||||||
|
const fullShow: Show = {
|
||||||
|
coverImage: "https://example.com/aot-cover.jpg",
|
||||||
|
createdAt: new Date("2024-01-01"),
|
||||||
|
dateAdded: new Date("2024-01-01"),
|
||||||
|
dateCompleted: new Date("2024-01-20"),
|
||||||
|
id: "show456",
|
||||||
|
links: [
|
||||||
|
{ title: "MyAnimeList", url: "https://myanimelist.net/anime/16498" },
|
||||||
|
{ title: "Crunchyroll", url: "https://crunchyroll.com/attack-on-titan" },
|
||||||
|
],
|
||||||
|
notes: "Incredible story with amazing animation",
|
||||||
|
rating: 5,
|
||||||
|
status: ShowStatus.completed,
|
||||||
|
tags: [ "action", "dark fantasy", "shounen" ],
|
||||||
|
title: "Attack on Titan",
|
||||||
|
type: ShowType.anime,
|
||||||
|
updatedAt: new Date("2024-01-20"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullShow.type).toBe(ShowType.anime);
|
||||||
|
expect(fullShow.rating).toBe(5);
|
||||||
|
expect(fullShow.tags).toHaveLength(3);
|
||||||
|
expect(fullShow.links).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept different show types", () => {
|
||||||
|
const film: Show = {
|
||||||
|
createdAt: new Date("2024-02-01"),
|
||||||
|
dateAdded: new Date("2024-02-01"),
|
||||||
|
id: "show789",
|
||||||
|
links: [],
|
||||||
|
rating: 5,
|
||||||
|
status: ShowStatus.completed,
|
||||||
|
tags: [ "sci-fi", "thriller" ],
|
||||||
|
title: "Inception",
|
||||||
|
type: ShowType.film,
|
||||||
|
updatedAt: new Date("2024-02-01"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const documentary: Show = {
|
||||||
|
createdAt: new Date("2024-02-05"),
|
||||||
|
dateAdded: new Date("2024-02-05"),
|
||||||
|
id: "show999",
|
||||||
|
links: [],
|
||||||
|
status: ShowStatus.wantToWatch,
|
||||||
|
tags: [ "nature", "wildlife" ],
|
||||||
|
title: "Planet Earth II",
|
||||||
|
type: ShowType.documentary,
|
||||||
|
updatedAt: new Date("2024-02-05"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(film.type).toBe(ShowType.film);
|
||||||
|
expect(documentary.type).toBe(ShowType.documentary);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createShowDto interface", () => {
|
||||||
|
it("should accept DTO with required fields only", () => {
|
||||||
|
const createDto: CreateShowDto = {
|
||||||
|
status: ShowStatus.wantToWatch,
|
||||||
|
title: "Stranger Things",
|
||||||
|
type: ShowType.tvSeries,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(createDto.rating).toBeUndefined();
|
||||||
|
expect(createDto.notes).toBeUndefined();
|
||||||
|
expect(createDto.coverImage).toBeUndefined();
|
||||||
|
expect(createDto.tags).toBeUndefined();
|
||||||
|
expect(createDto.links).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept DTO with all fields", () => {
|
||||||
|
const fullCreateDto: CreateShowDto = {
|
||||||
|
coverImage: "https://example.com/death-note.jpg",
|
||||||
|
links: [ { title: "Netflix", url: "https://netflix.com/death-note" } ],
|
||||||
|
notes: "Psychological thriller with great plot",
|
||||||
|
rating: 4,
|
||||||
|
status: ShowStatus.watching,
|
||||||
|
tags: [ "psychological", "thriller", "supernatural" ],
|
||||||
|
title: "Death Note",
|
||||||
|
type: ShowType.anime,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullCreateDto.type).toBe(ShowType.anime);
|
||||||
|
expect(fullCreateDto.rating).toBe(4);
|
||||||
|
expect(fullCreateDto.tags).toEqual([ "psychological", "thriller", "supernatural" ]);
|
||||||
|
expect(fullCreateDto.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateShowDto type", () => {
|
||||||
|
it("should accept empty update DTO", () => {
|
||||||
|
const emptyUpdate: UpdateShowDto = {};
|
||||||
|
expect(emptyUpdate).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept partial updates including dateCompleted", () => {
|
||||||
|
const partialUpdate: UpdateShowDto = {
|
||||||
|
dateCompleted: new Date("2024-02-10"),
|
||||||
|
rating: 5,
|
||||||
|
status: ShowStatus.completed,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(partialUpdate.status).toBe(ShowStatus.completed);
|
||||||
|
expect(partialUpdate.dateCompleted).toEqual(new Date("2024-02-10"));
|
||||||
|
expect(partialUpdate.rating).toBe(5);
|
||||||
|
expect(partialUpdate.title).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept full update", () => {
|
||||||
|
const fullUpdate: UpdateShowDto = {
|
||||||
|
coverImage: "https://example.com/new-show-cover.jpg",
|
||||||
|
dateCompleted: new Date("2024-02-15"),
|
||||||
|
links: [ { title: "New Link", url: "https://newlink.com" } ],
|
||||||
|
notes: "Updated after watching",
|
||||||
|
rating: 3,
|
||||||
|
status: ShowStatus.completed,
|
||||||
|
tags: [ "updated", "tags" ],
|
||||||
|
title: "Updated Show Title",
|
||||||
|
type: ShowType.documentary,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(fullUpdate.title).toBe("Updated Show Title");
|
||||||
|
expect(fullUpdate.type).toBe(ShowType.documentary);
|
||||||
|
expect(fullUpdate.dateCompleted).toEqual(new Date("2024-02-15"));
|
||||||
|
expect(fullUpdate.links).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
/**
|
||||||
|
* @copyright NHCarrigan
|
||||||
|
* @license Naomi's Public License
|
||||||
|
* @author Naomi Carrigan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GameStatus, BookStatus, MangaStatus, MusicStatus, MusicType, ShowStatus, ShowType } from "../src";
|
||||||
|
import { SuggestionEntity, SuggestionStatus } from "../src/lib/suggestion.types";
|
||||||
|
import type {
|
||||||
|
Suggestion,
|
||||||
|
SuggestionUser,
|
||||||
|
CreateSuggestionDto,
|
||||||
|
DeclineSuggestionDto,
|
||||||
|
CreateGameSuggestionDto,
|
||||||
|
CreateBookSuggestionDto,
|
||||||
|
CreateMusicSuggestionDto,
|
||||||
|
CreateArtSuggestionDto,
|
||||||
|
CreateShowSuggestionDto,
|
||||||
|
CreateMangaSuggestionDto,
|
||||||
|
AcceptWithEditsDto,
|
||||||
|
} from "../src/lib/suggestion.types";
|
||||||
|
|
||||||
|
describe("suggestion Types", () => {
|
||||||
|
describe("suggestionEntity enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(SuggestionEntity.game).toBe("GAME");
|
||||||
|
expect(SuggestionEntity.book).toBe("BOOK");
|
||||||
|
expect(SuggestionEntity.music).toBe("MUSIC");
|
||||||
|
expect(SuggestionEntity.art).toBe("ART");
|
||||||
|
expect(SuggestionEntity.show).toBe("SHOW");
|
||||||
|
expect(SuggestionEntity.manga).toBe("MANGA");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(SuggestionEntity);
|
||||||
|
expect(values).toHaveLength(6);
|
||||||
|
expect(values).toContain("GAME");
|
||||||
|
expect(values).toContain("BOOK");
|
||||||
|
expect(values).toContain("MUSIC");
|
||||||
|
expect(values).toContain("ART");
|
||||||
|
expect(values).toContain("SHOW");
|
||||||
|
expect(values).toContain("MANGA");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("suggestionStatus enum", () => {
|
||||||
|
it("should have the correct values", () => {
|
||||||
|
expect(SuggestionStatus.unreviewed).toBe("UNREVIEWED");
|
||||||
|
expect(SuggestionStatus.accepted).toBe("ACCEPTED");
|
||||||
|
expect(SuggestionStatus.declined).toBe("DECLINED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have all expected enum values", () => {
|
||||||
|
const values = Object.values(SuggestionStatus);
|
||||||
|
expect(values).toHaveLength(3);
|
||||||
|
expect(values).toContain("UNREVIEWED");
|
||||||
|
expect(values).toContain("ACCEPTED");
|
||||||
|
expect(values).toContain("DECLINED");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("suggestionUser interface", () => {
|
||||||
|
it("should accept valid user object", () => {
|
||||||
|
const user: SuggestionUser = {
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
id: "user123",
|
||||||
|
inDiscord: true,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: false,
|
||||||
|
username: "suggester",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(user.inDiscord).toBeTruthy();
|
||||||
|
expect(user.isVip).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept user without avatar", () => {
|
||||||
|
const user: SuggestionUser = {
|
||||||
|
id: "user456",
|
||||||
|
inDiscord: true,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: true,
|
||||||
|
isVip: true,
|
||||||
|
username: "vipuser",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(user.avatar).toBeUndefined();
|
||||||
|
expect(user.isVip).toBeTruthy();
|
||||||
|
expect(user.isStaff).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("suggestion interface", () => {
|
||||||
|
it("should accept game suggestion", () => {
|
||||||
|
const gameSuggestion: Suggestion = {
|
||||||
|
createdAt: new Date("2024-01-15"),
|
||||||
|
entityType: SuggestionEntity.game,
|
||||||
|
gameData: {
|
||||||
|
coverImage: "https://example.com/hades.jpg",
|
||||||
|
notes: "Amazing roguelike",
|
||||||
|
platform: "Nintendo Switch",
|
||||||
|
status: GameStatus.backlog,
|
||||||
|
title: "Hades",
|
||||||
|
},
|
||||||
|
id: "sug123",
|
||||||
|
status: SuggestionStatus.unreviewed,
|
||||||
|
title: "Hades",
|
||||||
|
updatedAt: new Date("2024-01-15"),
|
||||||
|
user: {
|
||||||
|
id: "user123",
|
||||||
|
inDiscord: true,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: false,
|
||||||
|
username: "gamer",
|
||||||
|
},
|
||||||
|
userId: "user123",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(gameSuggestion.entityType).toBe(SuggestionEntity.game);
|
||||||
|
expect(gameSuggestion.gameData).toBeDefined();
|
||||||
|
expect(gameSuggestion.bookData).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept book suggestion", () => {
|
||||||
|
const bookSuggestion: Suggestion = {
|
||||||
|
bookData: {
|
||||||
|
author: "George Orwell",
|
||||||
|
isbn: "978-0-452-28423-4",
|
||||||
|
notes: "Dystopian classic",
|
||||||
|
status: BookStatus.toRead,
|
||||||
|
title: "1984",
|
||||||
|
},
|
||||||
|
createdAt: new Date("2024-01-20"),
|
||||||
|
entityType: SuggestionEntity.book,
|
||||||
|
id: "sug456",
|
||||||
|
status: SuggestionStatus.accepted,
|
||||||
|
title: "1984",
|
||||||
|
updatedAt: new Date("2024-01-21"),
|
||||||
|
user: {
|
||||||
|
id: "user456",
|
||||||
|
inDiscord: false,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: true,
|
||||||
|
username: "reader",
|
||||||
|
},
|
||||||
|
userId: "user456",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(bookSuggestion.entityType).toBe(SuggestionEntity.book);
|
||||||
|
expect(bookSuggestion.bookData).toBeDefined();
|
||||||
|
expect(bookSuggestion.gameData).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept declined suggestion with reason", () => {
|
||||||
|
const declinedSuggestion: Suggestion = {
|
||||||
|
createdAt: new Date("2024-02-01"),
|
||||||
|
declineReason: "Already in the library",
|
||||||
|
entityType: SuggestionEntity.music,
|
||||||
|
id: "sug789",
|
||||||
|
musicData: {
|
||||||
|
artist: "Pink Floyd",
|
||||||
|
status: MusicStatus.wantToListen,
|
||||||
|
title: "Dark Side of the Moon",
|
||||||
|
type: MusicType.album,
|
||||||
|
},
|
||||||
|
status: SuggestionStatus.declined,
|
||||||
|
title: "Dark Side of the Moon",
|
||||||
|
updatedAt: new Date("2024-02-02"),
|
||||||
|
user: {
|
||||||
|
id: "user789",
|
||||||
|
inDiscord: true,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: false,
|
||||||
|
username: "suggester",
|
||||||
|
},
|
||||||
|
userId: "user789",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(declinedSuggestion.status).toBe(SuggestionStatus.declined);
|
||||||
|
expect(declinedSuggestion.declineReason).toBe("Already in the library");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept art suggestion", () => {
|
||||||
|
const artSuggestion: Suggestion = {
|
||||||
|
artData: {
|
||||||
|
artist: "Jane Doe",
|
||||||
|
description: "A stunning sunset painting",
|
||||||
|
imageUrl: "https://example.com/sunset.jpg",
|
||||||
|
title: "Beautiful Sunset",
|
||||||
|
},
|
||||||
|
createdAt: new Date("2024-02-10"),
|
||||||
|
entityType: SuggestionEntity.art,
|
||||||
|
id: "sug999",
|
||||||
|
status: SuggestionStatus.unreviewed,
|
||||||
|
title: "Beautiful Sunset",
|
||||||
|
updatedAt: new Date("2024-02-10"),
|
||||||
|
user: {
|
||||||
|
id: "user999",
|
||||||
|
inDiscord: true,
|
||||||
|
isMod: true,
|
||||||
|
isStaff: false,
|
||||||
|
isVip: false,
|
||||||
|
username: "artlover",
|
||||||
|
},
|
||||||
|
userId: "user999",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(artSuggestion.entityType).toBe(SuggestionEntity.art);
|
||||||
|
expect(artSuggestion.artData).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept show and manga suggestions", () => {
|
||||||
|
const showSuggestion: Suggestion = {
|
||||||
|
createdAt: new Date("2024-02-15"),
|
||||||
|
entityType: SuggestionEntity.show,
|
||||||
|
id: "sug111",
|
||||||
|
showData: {
|
||||||
|
status: ShowStatus.wantToWatch,
|
||||||
|
title: "Breaking Bad",
|
||||||
|
type: ShowType.tvSeries,
|
||||||
|
},
|
||||||
|
status: SuggestionStatus.unreviewed,
|
||||||
|
title: "Breaking Bad",
|
||||||
|
updatedAt: new Date("2024-02-15"),
|
||||||
|
user: {
|
||||||
|
id: "user111",
|
||||||
|
inDiscord: false,
|
||||||
|
isMod: false,
|
||||||
|
isStaff: true,
|
||||||
|
isVip: false,
|
||||||
|
username: "tvfan",
|
||||||
|
},
|
||||||
|
userId: "user111",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mangaSuggestion: Suggestion = {
|
||||||
|
createdAt: new Date("2024-02-20"),
|
||||||
|
entityType: SuggestionEntity.manga,
|
||||||
|
id: "sug222",
|
||||||
|
mangaData: {
|
||||||
|
author: "Eiichiro Oda",
|
||||||
|
status: MangaStatus.reading,
|
||||||
|
title: "One Piece",
|
||||||
|
},
|
||||||
|
status: SuggestionStatus.accepted,
|
||||||
|
title: "One Piece",
|
||||||
|
updatedAt: new Date("2024-02-21"),
|
||||||
|
user: {
|
||||||
|
id: "user222",
|
||||||
|
inDiscord: true,
|
||||||
|
isMod: true,
|
||||||
|
isStaff: true,
|
||||||
|
isVip: true,
|
||||||
|
username: "mangareader",
|
||||||
|
},
|
||||||
|
userId: "user222",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(showSuggestion.entityType).toBe(SuggestionEntity.show);
|
||||||
|
expect(mangaSuggestion.entityType).toBe(SuggestionEntity.manga);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createSuggestionDto types", () => {
|
||||||
|
it("should accept game suggestion DTO", () => {
|
||||||
|
const gameDto: CreateGameSuggestionDto = {
|
||||||
|
coverImage: "https://example.com/hollow.jpg",
|
||||||
|
entityType: SuggestionEntity.game,
|
||||||
|
notes: "Great metroidvania",
|
||||||
|
platform: "PC",
|
||||||
|
title: "Hollow Knight",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(gameDto.entityType).toBe(SuggestionEntity.game);
|
||||||
|
expect(gameDto.platform).toBe("PC");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept book suggestion DTO", () => {
|
||||||
|
const bookDto: CreateBookSuggestionDto = {
|
||||||
|
author: "J.R.R. Tolkien",
|
||||||
|
coverImage: "https://example.com/hobbit.jpg",
|
||||||
|
entityType: SuggestionEntity.book,
|
||||||
|
isbn: "978-0-547-92822-7",
|
||||||
|
notes: "Fantasy classic",
|
||||||
|
title: "The Hobbit",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(bookDto.entityType).toBe(SuggestionEntity.book);
|
||||||
|
expect(bookDto.author).toBe("J.R.R. Tolkien");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept music suggestion DTO", () => {
|
||||||
|
const musicDto: CreateMusicSuggestionDto = {
|
||||||
|
artist: "Pink Floyd",
|
||||||
|
coverArt: "https://example.com/wall.jpg",
|
||||||
|
entityType: SuggestionEntity.music,
|
||||||
|
notes: "Rock opera",
|
||||||
|
title: "The Wall",
|
||||||
|
type: "ALBUM",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(musicDto.entityType).toBe(SuggestionEntity.music);
|
||||||
|
expect(musicDto.type).toBe("ALBUM");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept art suggestion DTO", () => {
|
||||||
|
const artDto: CreateArtSuggestionDto = {
|
||||||
|
artist: "Vincent van Gogh",
|
||||||
|
description: "Famous painting",
|
||||||
|
entityType: SuggestionEntity.art,
|
||||||
|
imageUrl: "https://example.com/starry.jpg",
|
||||||
|
title: "Starry Night",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(artDto.entityType).toBe(SuggestionEntity.art);
|
||||||
|
expect(artDto.imageUrl).toBe("https://example.com/starry.jpg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept show suggestion DTO", () => {
|
||||||
|
const showDto: CreateShowSuggestionDto = {
|
||||||
|
coverImage: "https://example.com/office.jpg",
|
||||||
|
entityType: SuggestionEntity.show,
|
||||||
|
notes: "Comedy series",
|
||||||
|
title: "The Office",
|
||||||
|
type: "TV_SERIES",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(showDto.entityType).toBe(SuggestionEntity.show);
|
||||||
|
expect(showDto.type).toBe("TV_SERIES");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept manga suggestion DTO", () => {
|
||||||
|
const mangaDto: CreateMangaSuggestionDto = {
|
||||||
|
author: "Tsugumi Ohba",
|
||||||
|
coverImage: "https://example.com/deathnote.jpg",
|
||||||
|
entityType: SuggestionEntity.manga,
|
||||||
|
notes: "Psychological thriller",
|
||||||
|
title: "Death Note",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(mangaDto.entityType).toBe(SuggestionEntity.manga);
|
||||||
|
expect(mangaDto.author).toBe("Tsugumi Ohba");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with union type", () => {
|
||||||
|
const suggestions: Array<CreateSuggestionDto> = [
|
||||||
|
{
|
||||||
|
entityType: SuggestionEntity.game,
|
||||||
|
title: "Game Title",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
author: "Author Name",
|
||||||
|
entityType: SuggestionEntity.book,
|
||||||
|
title: "Book Title",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(suggestions).toHaveLength(2);
|
||||||
|
expect(suggestions[0].entityType).toBe(SuggestionEntity.game);
|
||||||
|
expect(suggestions[1].entityType).toBe(SuggestionEntity.book);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("declineSuggestionDto interface", () => {
|
||||||
|
it("should accept empty decline DTO", () => {
|
||||||
|
const declineDto: DeclineSuggestionDto = {};
|
||||||
|
expect(declineDto.reason).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept decline DTO with reason", () => {
|
||||||
|
const declineDto: DeclineSuggestionDto = {
|
||||||
|
reason: "Already exists in the library",
|
||||||
|
};
|
||||||
|
expect(declineDto.reason).toBe("Already exists in the library");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("acceptWithEditsDto interface", () => {
|
||||||
|
it("should accept empty edits DTO", () => {
|
||||||
|
const editsDto: AcceptWithEditsDto = {};
|
||||||
|
expect(editsDto).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept edits for book fields", () => {
|
||||||
|
const editsDto: AcceptWithEditsDto = {
|
||||||
|
author: "Corrected Author",
|
||||||
|
coverImage: "https://example.com/new-cover.jpg",
|
||||||
|
isbn: "978-0-123-45678-9",
|
||||||
|
links: [ { label: "Goodreads", url: "https://goodreads.com" } ],
|
||||||
|
notes: "Updated notes",
|
||||||
|
tags: [ "fiction", "classic" ],
|
||||||
|
title: "Corrected Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(editsDto.title).toBe("Corrected Title");
|
||||||
|
expect(editsDto.author).toBe("Corrected Author");
|
||||||
|
expect(editsDto.tags).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept edits for music fields", () => {
|
||||||
|
const editsDto: AcceptWithEditsDto = {
|
||||||
|
artist: "Artist Name",
|
||||||
|
coverArt: "https://example.com/album.jpg",
|
||||||
|
title: "Album Title",
|
||||||
|
type: "ALBUM",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(editsDto.artist).toBe("Artist Name");
|
||||||
|
expect(editsDto.type).toBe("ALBUM");
|
||||||
|
expect(editsDto.coverArt).toBe("https://example.com/album.jpg");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept edits for game fields", () => {
|
||||||
|
const editsDto: AcceptWithEditsDto = {
|
||||||
|
coverImage: "https://example.com/game.jpg",
|
||||||
|
notes: "Action RPG",
|
||||||
|
platform: "PlayStation 5",
|
||||||
|
title: "Game Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(editsDto.platform).toBe("PlayStation 5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept edits for art fields", () => {
|
||||||
|
const editsDto: AcceptWithEditsDto = {
|
||||||
|
artist: "Artist Name",
|
||||||
|
description: "Beautiful artwork",
|
||||||
|
imageUrl: "https://example.com/art.jpg",
|
||||||
|
title: "Art Title",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(editsDto.description).toBe("Beautiful artwork");
|
||||||
|
expect(editsDto.imageUrl).toBe("https://example.com/art.jpg");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.base.json",
|
"extends": "../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"outDir": "../dist/out-tsc",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -10,11 +11,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noPropertyAccessFromIndexSignature": true
|
"noPropertyAccessFromIndexSignature": true
|
||||||
},
|
},
|
||||||
"files": [],
|
"include": ["src/**/*.ts", "test/**/*.ts"],
|
||||||
"include": [],
|
"exclude": ["test/**/*.spec.ts"]
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.lib.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": ["test/**/*.spec.ts"]
|
||||||
|
}
|
||||||
+1
-1
@@ -18,5 +18,5 @@
|
|||||||
"@library/shared-types": ["shared-types/src/index.ts"]
|
"@library/shared-types": ["shared-types/src/index.ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "tmp"]
|
"exclude": ["node_modules", "tmp", "**/test/**/*.spec.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user