feat: multiple improvements to library functionality (#50)
Node.js CI / CI (push) Successful in 1m18s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m17s

## Summary

This PR implements several improvements to the library application:

- Added start and finish date tracking for media items
- Added "Retired" category for abandoned media
- Implemented avatar-based user menu with dropdown navigation
- Added automatic background token refresh to prevent session expiry
- Created centralised logging system with frontend-to-API log forwarding
- Added toast notifications for error handling

## Changes

### Media Tracking (#41)
- Added `dateStarted` and `dateFinished` fields to Books, Games, Manga, Music, and Shows
- Updated TypeScript types, Prisma schema, and API services
- Added manual date input fields to frontend forms
- Properly converts HTML date strings to Date objects before API submission

### Retired Category (#43)
- Added `RETIRED` status to all media type enums
- Updated Prisma schema, frontend dropdowns, and filter buttons
- Added status label handling for retired items

### User Menu (#46)
- Replaced username text with avatar image in header
- Created dropdown menu with navigation items (Users, Audit, Suggestions)
- Added logout button to menu
- Implemented keyboard accessibility (tabindex, role, keyup handlers)

### Token Refresh (#44)
- Implemented automatic token refresh every 13 minutes in background
- Added proactive refresh to prevent token expiry during form filling
- Prevents users from losing form data due to expired sessions

### Centralised Logging (#1)
- Created `/log` endpoint on API to receive frontend logs
- Replaced API console.log calls with @nhcarrigan/logger
- Created ConsoleLoggerService to intercept all console methods on frontend
- Added global error handlers (window.error, unhandledrejection) on frontend
- Added process error handlers (uncaughtException, unhandledRejection, SIGTERM, SIGINT) on API
- All frontend console activity now forwarded to centralised logging

### Error Handling
- Created ToastService and ToastComponent for displaying errors
- Integrated with GlobalErrorHandler and HTTP interceptor
- Added accessibility features (keyboard navigation, ARIA attributes)
- Set toast opacity to 40% for optimal readability

### Testing & Build
- Fixed pre-existing test failure for GET / route (now returns version info)
- Added ESM module mocking (jsdom, marked, dompurify, @nhcarrigan/logger)
- Configured Jest with isolatedModules to handle TypeScript errors
- Excluded test-setup.ts from production build
- All tests passing (123 total)
- Build passing with no errors

## Test Plan

- [x] All tests pass (123 tests)
- [x] Build passes without errors
- [x] Lint passes (only pre-existing warnings)
- [x] Date fields work correctly on all media types
- [x] Retired status displays and filters properly
- [x] Avatar menu opens/closes correctly with keyboard and mouse
- [x] Token refresh prevents session expiry
- [x] Toast notifications appear for errors
- [x] Frontend logs forward to API successfully
- [x] Root route returns version information

Closes #41
Closes #43
Closes #44
Closes #46
Closes #1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Hikari <hikari@nhcarrigan.com>
Reviewed-on: #50
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #50.
This commit is contained in:
2026-02-19 16:52:43 -08:00
committed by Naomi Carrigan
parent 9caf74945a
commit 7579f1ec97
93 changed files with 4297 additions and 645 deletions
+22 -19
View File
@@ -1,30 +1,33 @@
/**
* @copyright 2026 NHCarrigan
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Link } from "./common.types";
import type { Link } from "./common.types";
export interface Art {
id: string;
title: string;
artist: string;
interface Art {
id: string;
title: string;
artist: string;
description?: string;
imageUrl: string;
tags: string[];
links: Link[];
dateAdded: Date;
createdAt: Date;
updatedAt: Date;
imageUrl: string;
tags: Array<string>;
links: Array<Link>;
dateAdded: Date;
createdAt: Date;
updatedAt: Date;
}
export interface CreateArtDto {
title: string;
artist: string;
interface CreateArtDto {
title: string;
artist: string;
description?: string;
imageUrl: string;
tags?: string[];
links?: Link[];
imageUrl: string;
tags?: Array<string>;
links?: Array<Link>;
}
export interface UpdateArtDto extends Partial<CreateArtDto> {}
type UpdateArtDto = Partial<CreateArtDto>;
export type { Art, CreateArtDto, UpdateArtDto };
+59 -45
View File
@@ -1,58 +1,72 @@
export enum AuditAction {
LOGIN = "LOGIN",
LOGOUT = "LOGOUT",
LOGIN_FAILED = "LOGIN_FAILED",
COMMENT_CREATE = "COMMENT_CREATE",
COMMENT_UPDATE = "COMMENT_UPDATE",
COMMENT_DELETE = "COMMENT_DELETE",
ENTRY_CREATE = "ENTRY_CREATE",
ENTRY_UPDATE = "ENTRY_UPDATE",
ENTRY_DELETE = "ENTRY_DELETE",
LIKE = "LIKE",
UNLIKE = "UNLIKE",
USER_BAN = "USER_BAN",
USER_UNBAN = "USER_UNBAN",
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED",
CSRF_VALIDATION_FAILED = "CSRF_VALIDATION_FAILED",
UNAUTHORIZED_ACCESS = "UNAUTHORIZED_ACCESS",
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
enum AuditAction {
login = "LOGIN",
logout = "LOGOUT",
loginFailed = "LOGIN_FAILED",
commentCreate = "COMMENT_CREATE",
commentUpdate = "COMMENT_UPDATE",
commentDelete = "COMMENT_DELETE",
entryCreate = "ENTRY_CREATE",
entryUpdate = "ENTRY_UPDATE",
entryDelete = "ENTRY_DELETE",
like = "LIKE",
unlike = "UNLIKE",
userBan = "USER_BAN",
userUnban = "USER_UNBAN",
rateLimitExceeded = "RATE_LIMIT_EXCEEDED",
csrfValidationFailed = "CSRF_VALIDATION_FAILED",
unauthorizedAccess = "UNAUTHORIZED_ACCESS",
}
export enum AuditCategory {
AUTH = "AUTH",
CONTENT = "CONTENT",
ADMIN = "ADMIN",
SECURITY = "SECURITY",
enum AuditCategory {
auth = "AUTH",
content = "CONTENT",
admin = "ADMIN",
security = "SECURITY",
}
export interface AuditLogUser {
id: string;
interface AuditLogUser {
id: string;
username: string;
avatar?: string;
avatar?: string;
}
export interface AuditLog {
id: string;
action: AuditAction;
category: AuditCategory;
userId?: string;
user?: AuditLogUser;
interface AuditLog {
id: string;
action: AuditAction;
category: AuditCategory;
userId?: string;
user?: AuditLogUser;
targetUserId?: string;
targetUser?: AuditLogUser;
targetUser?: AuditLogUser;
resourceType?: string;
resourceId?: string;
details?: string;
userAgent?: string;
success: boolean;
createdAt: Date;
resourceId?: string;
details?: string;
userAgent?: string;
success: boolean;
createdAt: Date;
}
export interface AuditLogFilters {
action?: AuditAction;
category?: AuditCategory;
userId?: string;
success?: boolean;
interface AuditLogFilters {
action?: AuditAction;
category?: AuditCategory;
userId?: string;
success?: boolean;
startDate?: Date;
endDate?: Date;
page?: number;
limit?: number;
endDate?: Date;
page?: number;
limit?: number;
}
export {
AuditAction,
AuditCategory,
type AuditLog,
type AuditLogFilters,
type AuditLogUser,
};
+22 -19
View File
@@ -4,33 +4,36 @@
* @author Naomi Carrigan
*/
export interface User {
id: string;
email: string;
username: string;
avatar?: string;
interface User {
id: string;
email: string;
username: string;
avatar?: string;
discordId: string;
isAdmin: boolean;
isBanned: boolean;
isAdmin: boolean;
isBanned: boolean;
inDiscord: boolean;
isVip: boolean;
isMod: boolean;
isStaff: boolean;
isVip: boolean;
isMod: boolean;
isStaff: boolean;
}
export interface JwtPayload {
interface JwtPayload {
/**
* User id.
*/
sub: string;
email: string;
sub: string;
email: string;
username: string;
isAdmin: boolean;
iat?: number;
exp?: number;
isAdmin: boolean;
iat?: number;
exp?: number;
}
export interface AuthResponse {
interface AuthResponse {
accessToken: string;
user: User;
}
user: User;
}
export type { AuthResponse, JwtPayload, User };
+26 -19
View File
@@ -1,46 +1,53 @@
/**
* @copyright 2026 NHCarrigan
* @copyright 2026 NHCarrigan
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export enum BookStatus {
import type { Link } from "./common.types";
enum BookStatus {
reading = "READING",
finished = "FINISHED",
toRead = "TO_READ",
retired = "RETIRED",
}
import { Link } from "./common.types";
export interface Book {
interface Book {
id: string;
title: string;
author: string;
isbn?: string;
status: BookStatus;
dateAdded: Date;
dateStarted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverImage?: string;
tags: string[];
links: Link[];
tags: Array<string>;
links: Array<Link>;
createdAt: Date;
updatedAt: Date;
}
export interface CreateBookDto {
title: string;
author: string;
isbn?: string;
status: BookStatus;
rating?: number;
notes?: string;
coverImage?: string;
tags?: string[];
links?: Link[];
interface CreateBookDto {
title: string;
author: string;
isbn?: string;
status: BookStatus;
dateStarted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverImage?: string;
tags?: Array<string>;
links?: Array<Link>;
}
export interface UpdateBookDto extends Partial<CreateBookDto> {
interface UpdateBookDto extends Partial<CreateBookDto> {
dateStarted?: Date;
dateFinished?: Date;
}
export { type Book, BookStatus, type CreateBookDto, type UpdateBookDto };
+23 -21
View File
@@ -4,32 +4,34 @@
* @author Naomi Carrigan
*/
export interface CommentUser {
id: string;
username: string;
avatar?: string;
interface CommentUser {
id: string;
username: string;
avatar?: string;
inDiscord?: boolean;
isVip?: boolean;
isMod?: boolean;
isStaff?: boolean;
isVip?: boolean;
isMod?: boolean;
isStaff?: boolean;
}
export interface Comment {
id: string;
content: string;
interface Comment {
id: string;
content: string;
rawContent?: string;
userId: string;
user: CommentUser;
gameId?: string;
bookId?: string;
musicId?: string;
artId?: string;
showId?: string;
mangaId?: string;
createdAt: Date;
updatedAt: Date;
userId: string;
user: CommentUser;
gameId?: string;
bookId?: string;
musicId?: string;
artId?: string;
showId?: string;
mangaId?: string;
createdAt: Date;
updatedAt: Date;
}
export interface CreateCommentDto {
interface CreateCommentDto {
content: string;
}
export type { Comment, CommentUser, CreateCommentDto };
+7 -1
View File
@@ -1,4 +1,10 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Link {
title: string;
url: string;
url: string;
}
+27 -18
View File
@@ -1,44 +1,53 @@
/**
* @copyright 2026 NHCarrigan
* @copyright 2026 NHCarrigan
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export enum GameStatus {
import type { Link } from "./common.types";
enum GameStatus {
playing = "PLAYING",
completed = "COMPLETED",
backlog = "BACKLOG",
retired = "RETIRED",
}
import { Link } from "./common.types";
export interface Game {
interface Game {
id: string;
title: string;
platform?: string;
status: GameStatus;
dateAdded: Date;
dateStarted?: Date;
dateCompleted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverImage?: string;
tags: string[];
links: Link[];
tags: Array<string>;
links: Array<Link>;
createdAt: Date;
updatedAt: Date;
}
export interface CreateGameDto {
title: string;
platform?: string;
status: GameStatus;
rating?: number;
notes?: string;
coverImage?: string;
tags?: string[];
links?: Link[];
interface CreateGameDto {
title: string;
platform?: string;
status: GameStatus;
dateStarted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverImage?: string;
tags?: Array<string>;
links?: Array<Link>;
}
export interface UpdateGameDto extends Partial<CreateGameDto> {
interface UpdateGameDto extends Partial<CreateGameDto> {
dateStarted?: Date;
dateCompleted?: Date;
dateFinished?: Date;
}
export { type CreateGameDto, type Game, GameStatus, type UpdateGameDto };
+21 -15
View File
@@ -1,32 +1,38 @@
/**
* @copyright 2026 NHCarrigan
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Like {
id: string;
userId: string;
entityType: 'book' | 'game' | 'show' | 'manga' | 'music' | 'art';
entityId: string;
createdAt: Date;
interface Like {
id: string;
userId: string;
entityType: "book" | "game" | "show" | "manga" | "music" | "art";
entityId: string;
createdAt: Date;
}
export type CreateLikeDto = Pick<Like, 'entityType' | 'entityId'>;
type CreateLikeDto = Pick<Like, "entityType" | "entityId">;
export interface LikeCountDto {
entityId: string;
interface LikeCountDto {
entityId: string;
entityType: string;
count: number;
count: number;
}
export interface LikedItemDto {
interface LikedItemDto {
like: Like;
item: any; // This will be the actual entity (Book, Game, etc.)
/**
* This will be the actual entity (Book, Game, etc.).
*/
item: unknown;
}
// Response types
export interface LikeResponse {
interface LikeResponse {
liked: boolean;
count: number;
}
}
export type { CreateLikeDto, Like, LikeCountDto, LikedItemDto, LikeResponse };
+27 -17
View File
@@ -1,44 +1,54 @@
/**
* @copyright 2026 NHCarrigan
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export enum MangaStatus {
import type { Link } from "./common.types";
enum MangaStatus {
reading = "READING",
completed = "COMPLETED",
wantToRead = "WANT_TO_READ",
retired = "RETIRED",
}
import { Link } from "./common.types";
export interface Manga {
interface Manga {
id: string;
title: string;
author: string;
status: MangaStatus;
dateAdded: Date;
dateStarted?: Date;
dateCompleted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverImage?: string;
tags: string[];
links: Link[];
tags: Array<string>;
links: Array<Link>;
createdAt: Date;
updatedAt: Date;
}
export interface CreateMangaDto {
title: string;
author: string;
status: MangaStatus;
rating?: number;
notes?: string;
coverImage?: string;
tags?: string[];
links?: Link[];
interface CreateMangaDto {
title: string;
author: string;
status: MangaStatus;
dateStarted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverImage?: string;
tags?: Array<string>;
links?: Array<Link>;
}
export interface UpdateMangaDto extends Partial<CreateMangaDto> {
interface UpdateMangaDto extends Partial<CreateMangaDto> {
dateStarted?: Date;
dateCompleted?: Date;
dateFinished?: Date;
}
export { MangaStatus };
export type { CreateMangaDto, Manga, UpdateMangaDto };
+30 -20
View File
@@ -1,52 +1,62 @@
/**
* @copyright 2026 NHCarrigan
* @copyright 2026 NHCarrigan
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export enum MusicType {
import type { Link } from "./common.types";
enum MusicType {
album = "ALBUM",
single = "SINGLE",
ep = "EP",
}
export enum MusicStatus {
enum MusicStatus {
listening = "LISTENING",
completed = "COMPLETED",
wantToListen = "WANT_TO_LISTEN",
retired = "RETIRED",
}
import { Link } from "./common.types";
export interface Music {
interface Music {
id: string;
title: string;
artist: string;
type: MusicType;
status: MusicStatus;
dateAdded: Date;
dateStarted?: Date;
dateCompleted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverArt?: string;
tags: string[];
links: Link[];
tags: Array<string>;
links: Array<Link>;
createdAt: Date;
updatedAt: Date;
}
export interface CreateMusicDto {
title: string;
artist: string;
type: MusicType;
status: MusicStatus;
rating?: number;
notes?: string;
coverArt?: string;
tags?: string[];
links?: Link[];
interface CreateMusicDto {
title: string;
artist: string;
type: MusicType;
status: MusicStatus;
dateStarted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverArt?: string;
tags?: Array<string>;
links?: Array<Link>;
}
export interface UpdateMusicDto extends Partial<CreateMusicDto> {
interface UpdateMusicDto extends Partial<CreateMusicDto> {
dateStarted?: Date;
dateCompleted?: Date;
dateFinished?: Date;
}
export { MusicStatus, MusicType };
export type { CreateMusicDto, Music, UpdateMusicDto };
+28 -18
View File
@@ -1,51 +1,61 @@
/**
* @copyright 2026 NHCarrigan
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export enum ShowType {
import type { Link } from "./common.types";
enum ShowType {
tvSeries = "TV_SERIES",
anime = "ANIME",
film = "FILM",
documentary = "DOCUMENTARY",
}
export enum ShowStatus {
enum ShowStatus {
watching = "WATCHING",
completed = "COMPLETED",
wantToWatch = "WANT_TO_WATCH",
retired = "RETIRED",
}
import { Link } from "./common.types";
export interface Show {
interface Show {
id: string;
title: string;
type: ShowType;
status: ShowStatus;
dateAdded: Date;
dateStarted?: Date;
dateCompleted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverImage?: string;
tags: string[];
links: Link[];
tags: Array<string>;
links: Array<Link>;
createdAt: Date;
updatedAt: Date;
}
export interface CreateShowDto {
title: string;
type: ShowType;
status: ShowStatus;
rating?: number;
notes?: string;
coverImage?: string;
tags?: string[];
links?: Link[];
interface CreateShowDto {
title: string;
type: ShowType;
status: ShowStatus;
dateStarted?: Date;
dateFinished?: Date;
rating?: number;
notes?: string;
coverImage?: string;
tags?: Array<string>;
links?: Array<Link>;
}
export interface UpdateShowDto extends Partial<CreateShowDto> {
interface UpdateShowDto extends Partial<CreateShowDto> {
dateStarted?: Date;
dateCompleted?: Date;
dateFinished?: Date;
}
export { ShowStatus, ShowType };
export type { CreateShowDto, Show, UpdateShowDto };
+106 -85
View File
@@ -1,104 +1,110 @@
import type { CreateGameDto } from "./game.types";
import type { CreateBookDto } from "./book.types";
import type { CreateMusicDto } from "./music.types";
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { CreateArtDto } from "./art.types";
import type { CreateShowDto } from "./show.types";
import type { CreateBookDto } from "./book.types";
import type { CreateGameDto } from "./game.types";
import type { CreateMangaDto } from "./manga.types";
import type { CreateMusicDto } from "./music.types";
import type { CreateShowDto } from "./show.types";
export enum SuggestionEntity {
GAME = "GAME",
BOOK = "BOOK",
MUSIC = "MUSIC",
ART = "ART",
SHOW = "SHOW",
MANGA = "MANGA",
enum SuggestionEntity {
game = "GAME",
book = "BOOK",
music = "MUSIC",
art = "ART",
show = "SHOW",
manga = "MANGA",
}
export enum SuggestionStatus {
UNREVIEWED = "UNREVIEWED",
ACCEPTED = "ACCEPTED",
DECLINED = "DECLINED",
enum SuggestionStatus {
unreviewed = "UNREVIEWED",
accepted = "ACCEPTED",
declined = "DECLINED",
}
export interface SuggestionUser {
id: string;
username: string;
avatar?: string;
interface SuggestionUser {
id: string;
username: string;
avatar?: string;
inDiscord: boolean;
isVip: boolean;
isMod: boolean;
isStaff: boolean;
isVip: boolean;
isMod: boolean;
isStaff: boolean;
}
export interface Suggestion {
id: string;
userId: string;
user: SuggestionUser;
entityType: SuggestionEntity;
status: SuggestionStatus;
interface Suggestion {
id: string;
userId: string;
user: SuggestionUser;
entityType: SuggestionEntity;
status: SuggestionStatus;
declineReason?: string;
title: string;
gameData?: Omit<CreateGameDto, "rating">;
bookData?: Omit<CreateBookDto, "rating">;
musicData?: Omit<CreateMusicDto, "rating">;
artData?: CreateArtDto;
showData?: Omit<CreateShowDto, "rating">;
mangaData?: Omit<CreateMangaDto, "rating">;
createdAt: Date;
updatedAt: Date;
title: string;
gameData?: Omit<CreateGameDto, "rating">;
bookData?: Omit<CreateBookDto, "rating">;
musicData?: Omit<CreateMusicDto, "rating">;
artData?: CreateArtDto;
showData?: Omit<CreateShowDto, "rating">;
mangaData?: Omit<CreateMangaDto, "rating">;
createdAt: Date;
updatedAt: Date;
}
export interface CreateGameSuggestionDto {
entityType: SuggestionEntity.GAME;
title: string;
platform?: string;
notes?: string;
interface CreateGameSuggestionDto {
entityType: SuggestionEntity.game;
title: string;
platform?: string;
notes?: string;
coverImage?: string;
}
export interface CreateBookSuggestionDto {
entityType: SuggestionEntity.BOOK;
title: string;
author: string;
isbn?: string;
notes?: string;
interface CreateBookSuggestionDto {
entityType: SuggestionEntity.book;
title: string;
author: string;
isbn?: string;
notes?: string;
coverImage?: string;
}
export interface CreateMusicSuggestionDto {
entityType: SuggestionEntity.MUSIC;
title: string;
artist: string;
type: string;
notes?: string;
coverArt?: string;
interface CreateMusicSuggestionDto {
entityType: SuggestionEntity.music;
title: string;
artist: string;
type: string;
notes?: string;
coverArt?: string;
}
export interface CreateArtSuggestionDto {
entityType: SuggestionEntity.ART;
title: string;
artist: string;
interface CreateArtSuggestionDto {
entityType: SuggestionEntity.art;
title: string;
artist: string;
description?: string;
imageUrl: string;
imageUrl: string;
}
export interface CreateShowSuggestionDto {
entityType: SuggestionEntity.SHOW;
title: string;
type: string;
notes?: string;
interface CreateShowSuggestionDto {
entityType: SuggestionEntity.show;
title: string;
type: string;
notes?: string;
coverImage?: string;
}
export interface CreateMangaSuggestionDto {
entityType: SuggestionEntity.MANGA;
title: string;
author: string;
notes?: string;
interface CreateMangaSuggestionDto {
entityType: SuggestionEntity.manga;
title: string;
author: string;
notes?: string;
coverImage?: string;
}
export type CreateSuggestionDto =
type CreateSuggestionDto =
| CreateGameSuggestionDto
| CreateBookSuggestionDto
| CreateMusicSuggestionDto
@@ -106,22 +112,37 @@ export type CreateSuggestionDto =
| CreateShowSuggestionDto
| CreateMangaSuggestionDto;
export interface DeclineSuggestionDto {
interface DeclineSuggestionDto {
reason?: string;
}
export interface AcceptWithEditsDto {
title?: string;
platform?: string;
author?: string;
artist?: string;
isbn?: string;
type?: string;
notes?: string;
interface AcceptWithEditsDto {
title?: string;
platform?: string;
author?: string;
artist?: string;
isbn?: string;
type?: string;
notes?: string;
description?: string;
coverImage?: string;
coverArt?: string;
imageUrl?: string;
tags?: string[];
links?: Array<{ label: string; url: string }>;
coverImage?: string;
coverArt?: string;
imageUrl?: string;
tags?: Array<string>;
links?: Array<{ label: string; url: string }>;
}
export { SuggestionEntity, SuggestionStatus };
export type {
AcceptWithEditsDto,
CreateArtSuggestionDto,
CreateBookSuggestionDto,
CreateGameSuggestionDto,
CreateMangaSuggestionDto,
CreateMusicSuggestionDto,
CreateShowSuggestionDto,
CreateSuggestionDto,
DeclineSuggestionDto,
Suggestion,
SuggestionUser,
};