feat: add art component

This commit is contained in:
2026-02-04 13:40:52 -08:00
parent d338c8b52f
commit cbd6499079
12 changed files with 1162 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { FastifyPluginAsync } from "fastify";
import { Art, CreateArtDto, UpdateArtDto, Comment, CreateCommentDto } from "@library/shared-types";
import { ArtService } from "../../services/art.service";
import { CommentService } from "../../services/comment.service";
import { adminGuard } from "../../middleware/admin-guard";
const artRoutes: FastifyPluginAsync = async (app) => {
const artService = new ArtService();
const commentService = new CommentService();
/**
* Get all art (public route).
*/
app.get<{ Reply: Art[] }>("/", async () => {
return artService.getAllArt();
});
/**
* Get single art piece by ID (public route).
*/
app.get<{ Params: { id: string }; Reply: Art | null }>(
"/:id",
async (request) => {
const { id } = request.params;
return artService.getArtById(id);
}
);
/**
* Create new art piece (admin only).
*/
app.post<{ Body: CreateArtDto; Reply: Art }>(
"/",
{
preValidation: [app.authenticate, adminGuard],
},
async (request) => {
return artService.createArt(request.body);
}
);
/**
* Update art by ID (admin only).
*/
app.put<{
Params: { id: string };
Body: UpdateArtDto;
Reply: Art | null;
}>(
"/:id",
{
preValidation: [app.authenticate, adminGuard],
},
async (request) => {
const { id } = request.params;
return artService.updateArt(id, request.body);
}
);
/**
* Delete art by ID (admin only).
*/
app.delete<{ Params: { id: string }; Reply: { success: boolean } }>(
"/:id",
{
preValidation: [app.authenticate, adminGuard],
},
async (request) => {
const { id } = request.params;
await artService.deleteArt(id);
return { success: true };
}
);
/**
* Get comments for an art piece (public route).
*/
app.get<{ Params: { id: string }; Reply: Comment[] }>(
"/:id/comments",
async (request) => {
const { id } = request.params;
return commentService.getCommentsForArt(id);
}
);
/**
* Add comment to an art piece (authenticated users).
*/
app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>(
"/:id/comments",
{
preValidation: [app.authenticate],
},
async (request) => {
const { id } = request.params;
const userId = request.user.id;
return commentService.createCommentForArt(id, userId, request.body);
}
);
/**
* Delete comment (admin only).
*/
app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>(
"/:id/comments/:commentId",
{
preValidation: [app.authenticate, adminGuard],
},
async (request) => {
const { commentId } = request.params;
await commentService.deleteComment(commentId);
return { success: true };
}
);
};
export default artRoutes;
+94
View File
@@ -0,0 +1,94 @@
/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Art, CreateArtDto, UpdateArtDto } from "@library/shared-types";
import { prisma } from "../lib/prisma";
export class ArtService {
private prisma = prisma;
constructor() {}
/**
* Get all art pieces.
*/
async getAllArt(): Promise<Art[]> {
const artPieces = await this.prisma.art.findMany({
orderBy: { createdAt: "desc" },
});
return artPieces.map((art) => ({
...art,
description: art.description || undefined,
dateAdded: art.dateAdded,
createdAt: art.createdAt,
updatedAt: art.updatedAt,
}));
}
/**
* Get art by ID.
*/
async getArtById(id: string): Promise<Art | null> {
const art = await this.prisma.art.findUnique({
where: { id },
});
if (!art) return null;
return {
...art,
description: art.description || undefined,
dateAdded: art.dateAdded,
createdAt: art.createdAt,
updatedAt: art.updatedAt,
};
}
/**
* Create new art piece.
*/
async createArt(data: CreateArtDto): Promise<Art> {
const art = await this.prisma.art.create({
data,
});
return {
...art,
description: art.description || undefined,
dateAdded: art.dateAdded,
createdAt: art.createdAt,
updatedAt: art.updatedAt,
};
}
/**
* Update art by ID.
*/
async updateArt(id: string, data: UpdateArtDto): Promise<Art> {
const art = await this.prisma.art.update({
where: { id },
data,
});
return {
...art,
description: art.description || undefined,
dateAdded: art.dateAdded,
createdAt: art.createdAt,
updatedAt: art.updatedAt,
};
}
/**
* Delete art by ID.
*/
async deleteArt(id: string): Promise<void> {
await this.prisma.art.delete({
where: { id },
});
}
}
+27
View File
@@ -63,6 +63,7 @@ export class CommentService {
gameId: comment.gameId || undefined,
bookId: comment.bookId || undefined,
musicId: comment.musicId || undefined,
artId: comment.artId || undefined,
createdAt: comment.createdAt,
updatedAt: comment.updatedAt,
};
@@ -146,6 +147,32 @@ export class CommentService {
return this.mapComment(comment);
}
async getCommentsForArt(artId: string): Promise<Comment[]> {
const comments = await this.prisma.comment.findMany({
where: { artId },
include: { user: true },
orderBy: { createdAt: "desc" },
});
return comments.map((c) => this.mapComment(c));
}
async createCommentForArt(
artId: string,
userId: string,
data: CreateCommentDto
): Promise<Comment> {
const sanitizedContent = this.sanitizeMarkdown(data.content);
const comment = await this.prisma.comment.create({
data: {
content: sanitizedContent,
userId,
artId,
},
include: { user: true },
});
return this.mapComment(comment);
}
async deleteComment(commentId: string): Promise<void> {
await this.prisma.comment.delete({
where: { id: commentId },