generated from nhcarrigan/template
feat: add suggestion feature
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
import { prisma } from "../lib/prisma";
|
||||
import type {
|
||||
Suggestion,
|
||||
SuggestionStatus,
|
||||
SuggestionEntity,
|
||||
CreateSuggestionDto,
|
||||
} from "@library/shared-types";
|
||||
import {
|
||||
GameStatus,
|
||||
BookStatus,
|
||||
MusicType,
|
||||
MusicStatus,
|
||||
ShowType,
|
||||
ShowStatus,
|
||||
MangaStatus,
|
||||
} from "@library/shared-types";
|
||||
import { GameService } from "./game.service";
|
||||
import { BookService } from "./book.service";
|
||||
import { MusicService } from "./music.service";
|
||||
import { ArtService } from "./art.service";
|
||||
import { ShowService } from "./show.service";
|
||||
import { MangaService } from "./manga.service";
|
||||
|
||||
const gameService = new GameService();
|
||||
const bookService = new BookService();
|
||||
const musicService = new MusicService();
|
||||
const artService = new ArtService();
|
||||
const showService = new ShowService();
|
||||
const mangaService = new MangaService();
|
||||
|
||||
interface SuggestionUser {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
inDiscord: boolean;
|
||||
isVip: boolean;
|
||||
isMod: boolean;
|
||||
isStaff: boolean;
|
||||
}
|
||||
|
||||
function mapUser(user: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
inDiscord: boolean;
|
||||
isVip: boolean;
|
||||
isMod: boolean;
|
||||
isStaff: boolean;
|
||||
}): SuggestionUser {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
inDiscord: user.inDiscord,
|
||||
isVip: user.isVip,
|
||||
isMod: user.isMod,
|
||||
isStaff: user.isStaff,
|
||||
};
|
||||
}
|
||||
|
||||
function mapSuggestion(suggestion: {
|
||||
id: string;
|
||||
userId: string;
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
inDiscord: boolean;
|
||||
isVip: boolean;
|
||||
isMod: boolean;
|
||||
isStaff: boolean;
|
||||
};
|
||||
entityType: string;
|
||||
status: string;
|
||||
declineReason: string | null;
|
||||
title: string;
|
||||
gameData: unknown;
|
||||
bookData: unknown;
|
||||
musicData: unknown;
|
||||
artData: unknown;
|
||||
showData: unknown;
|
||||
mangaData: unknown;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}): Suggestion {
|
||||
return {
|
||||
id: suggestion.id,
|
||||
userId: suggestion.userId,
|
||||
user: mapUser(suggestion.user) as Suggestion["user"],
|
||||
entityType: suggestion.entityType as SuggestionEntity,
|
||||
status: suggestion.status as SuggestionStatus,
|
||||
declineReason: suggestion.declineReason ?? undefined,
|
||||
title: suggestion.title,
|
||||
gameData: suggestion.gameData as Suggestion["gameData"],
|
||||
bookData: suggestion.bookData as Suggestion["bookData"],
|
||||
musicData: suggestion.musicData as Suggestion["musicData"],
|
||||
artData: suggestion.artData as Suggestion["artData"],
|
||||
showData: suggestion.showData as Suggestion["showData"],
|
||||
mangaData: suggestion.mangaData as Suggestion["mangaData"],
|
||||
createdAt: suggestion.createdAt,
|
||||
updatedAt: suggestion.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function getMusicType(type?: string): MusicType {
|
||||
switch (type) {
|
||||
case "ALBUM":
|
||||
return MusicType.album;
|
||||
case "SINGLE":
|
||||
return MusicType.single;
|
||||
case "EP":
|
||||
return MusicType.ep;
|
||||
default:
|
||||
return MusicType.album;
|
||||
}
|
||||
}
|
||||
|
||||
function getShowType(type?: string): ShowType {
|
||||
switch (type) {
|
||||
case "TV_SERIES":
|
||||
return ShowType.tvSeries;
|
||||
case "ANIME":
|
||||
return ShowType.anime;
|
||||
case "FILM":
|
||||
return ShowType.film;
|
||||
case "DOCUMENTARY":
|
||||
return ShowType.documentary;
|
||||
default:
|
||||
return ShowType.tvSeries;
|
||||
}
|
||||
}
|
||||
|
||||
export const SuggestionService = {
|
||||
async getAllSuggestions(filters?: {
|
||||
status?: SuggestionStatus;
|
||||
entityType?: SuggestionEntity;
|
||||
}): Promise<Suggestion[]> {
|
||||
const suggestions = await prisma.suggestion.findMany({
|
||||
where: {
|
||||
...(filters?.status && { status: filters.status }),
|
||||
...(filters?.entityType && { entityType: filters.entityType }),
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return suggestions.map(mapSuggestion);
|
||||
},
|
||||
|
||||
async getUserSuggestions(userId: string): Promise<Suggestion[]> {
|
||||
const suggestions = await prisma.suggestion.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return suggestions.map(mapSuggestion);
|
||||
},
|
||||
|
||||
async getSuggestionById(id: string): Promise<Suggestion | null> {
|
||||
const suggestion = await prisma.suggestion.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!suggestion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mapSuggestion(suggestion);
|
||||
},
|
||||
|
||||
async createSuggestion(
|
||||
userId: string,
|
||||
data: CreateSuggestionDto
|
||||
): Promise<Suggestion> {
|
||||
const pendingCount = await prisma.suggestion.count({
|
||||
where: {
|
||||
userId,
|
||||
status: "UNREVIEWED",
|
||||
},
|
||||
});
|
||||
|
||||
if (pendingCount >= 5) {
|
||||
throw new Error("You can only have 5 pending suggestions at a time");
|
||||
}
|
||||
|
||||
const entityDataField = `${data.entityType.toLowerCase()}Data`;
|
||||
|
||||
const suggestionData: Record<string, unknown> = {
|
||||
userId,
|
||||
entityType: data.entityType,
|
||||
title: data.title,
|
||||
};
|
||||
|
||||
// Extract the data without entityType and title
|
||||
const { entityType: _et, title: _t, ...entityData } = data;
|
||||
suggestionData[entityDataField] = entityData;
|
||||
|
||||
const suggestion = await prisma.suggestion.create({
|
||||
data: suggestionData as Parameters<typeof prisma.suggestion.create>[0]["data"],
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return mapSuggestion(suggestion);
|
||||
},
|
||||
|
||||
async acceptSuggestion(id: string): Promise<Suggestion> {
|
||||
const suggestion = await prisma.suggestion.findUnique({
|
||||
where: { id },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
if (!suggestion) {
|
||||
throw new Error("Suggestion not found");
|
||||
}
|
||||
|
||||
if (suggestion.status !== "UNREVIEWED") {
|
||||
throw new Error("Suggestion has already been reviewed");
|
||||
}
|
||||
|
||||
// Create the entity based on type with "Want to X" status
|
||||
switch (suggestion.entityType) {
|
||||
case "GAME": {
|
||||
const gameData = suggestion.gameData as {
|
||||
platform?: string;
|
||||
notes?: string;
|
||||
coverImage?: string;
|
||||
} | null;
|
||||
await gameService.createGame({
|
||||
title: suggestion.title,
|
||||
platform: gameData?.platform,
|
||||
status: GameStatus.backlog,
|
||||
notes: gameData?.notes,
|
||||
coverImage: gameData?.coverImage,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "BOOK": {
|
||||
const bookData = suggestion.bookData as {
|
||||
author: string;
|
||||
isbn?: string;
|
||||
notes?: string;
|
||||
coverImage?: string;
|
||||
} | null;
|
||||
await bookService.createBook({
|
||||
title: suggestion.title,
|
||||
author: bookData?.author ?? "Unknown",
|
||||
isbn: bookData?.isbn,
|
||||
status: BookStatus.toRead,
|
||||
notes: bookData?.notes,
|
||||
coverImage: bookData?.coverImage,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "MUSIC": {
|
||||
const musicData = suggestion.musicData as {
|
||||
artist: string;
|
||||
type: string;
|
||||
notes?: string;
|
||||
coverArt?: string;
|
||||
} | null;
|
||||
await musicService.createMusic({
|
||||
title: suggestion.title,
|
||||
artist: musicData?.artist ?? "Unknown",
|
||||
type: getMusicType(musicData?.type),
|
||||
status: MusicStatus.wantToListen,
|
||||
notes: musicData?.notes,
|
||||
coverArt: musicData?.coverArt,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "ART": {
|
||||
const artData = suggestion.artData as {
|
||||
artist: string;
|
||||
description?: string;
|
||||
imageUrl: string;
|
||||
} | null;
|
||||
await artService.createArt({
|
||||
title: suggestion.title,
|
||||
artist: artData?.artist ?? "Unknown",
|
||||
description: artData?.description,
|
||||
imageUrl: artData?.imageUrl ?? "",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "SHOW": {
|
||||
const showData = suggestion.showData as {
|
||||
type: string;
|
||||
notes?: string;
|
||||
coverImage?: string;
|
||||
} | null;
|
||||
await showService.createShow({
|
||||
title: suggestion.title,
|
||||
type: getShowType(showData?.type),
|
||||
status: ShowStatus.wantToWatch,
|
||||
notes: showData?.notes,
|
||||
coverImage: showData?.coverImage,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "MANGA": {
|
||||
const mangaData = suggestion.mangaData as {
|
||||
author: string;
|
||||
notes?: string;
|
||||
coverImage?: string;
|
||||
} | null;
|
||||
await mangaService.createManga({
|
||||
title: suggestion.title,
|
||||
author: mangaData?.author ?? "Unknown",
|
||||
status: MangaStatus.wantToRead,
|
||||
notes: mangaData?.notes,
|
||||
coverImage: mangaData?.coverImage,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update suggestion status
|
||||
const updatedSuggestion = await prisma.suggestion.update({
|
||||
where: { id },
|
||||
data: { status: "ACCEPTED" },
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
return mapSuggestion(updatedSuggestion);
|
||||
},
|
||||
|
||||
async declineSuggestion(id: string, reason?: string): Promise<Suggestion> {
|
||||
const suggestion = await prisma.suggestion.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!suggestion) {
|
||||
throw new Error("Suggestion not found");
|
||||
}
|
||||
|
||||
if (suggestion.status !== "UNREVIEWED") {
|
||||
throw new Error("Suggestion has already been reviewed");
|
||||
}
|
||||
|
||||
const updatedSuggestion = await prisma.suggestion.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: "DECLINED",
|
||||
declineReason: reason,
|
||||
},
|
||||
include: { user: true },
|
||||
});
|
||||
|
||||
return mapSuggestion(updatedSuggestion);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user