From cbd6499079f8c78ef884f69939613fdd1f9d1747 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Wed, 4 Feb 2026 13:40:52 -0800 Subject: [PATCH] feat: add art component --- api/prisma/schema.prisma | 14 + api/src/app/routes/art/index.ts | 123 +++ api/src/app/services/art.service.ts | 94 ++ api/src/app/services/comment.service.ts | 27 + apps/frontend/src/app/app.routes.ts | 4 + .../components/art/art-gallery.component.ts | 824 ++++++++++++++++++ .../app/components/header/header.component.ts | 1 + apps/frontend/src/app/services/art.service.ts | 37 + .../src/app/services/comments.service.ts | 12 + shared-types/src/index.ts | 1 + shared-types/src/lib/art.types.ts | 24 + shared-types/src/lib/comment.types.ts | 1 + 12 files changed, 1162 insertions(+) create mode 100644 api/src/app/routes/art/index.ts create mode 100644 api/src/app/services/art.service.ts create mode 100644 apps/frontend/src/app/components/art/art-gallery.component.ts create mode 100644 apps/frontend/src/app/services/art.service.ts create mode 100644 shared-types/src/lib/art.types.ts diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index df0e3bc..95e59f9 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -84,6 +84,18 @@ enum MusicStatus { WANT_TO_LISTEN } +model Art { + id String @id @default(auto()) @map("_id") @db.ObjectId + title String + artist String + description String? + imageUrl String + dateAdded DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + comments Comment[] +} + model User { id String @id @default(auto()) @map("_id") @db.ObjectId discordId String @unique @@ -107,6 +119,8 @@ model Comment { book Book? @relation(fields: [bookId], references: [id]) musicId String? @db.ObjectId music Music? @relation(fields: [musicId], references: [id]) + artId String? @db.ObjectId + art Art? @relation(fields: [artId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/api/src/app/routes/art/index.ts b/api/src/app/routes/art/index.ts new file mode 100644 index 0000000..3adeb9d --- /dev/null +++ b/api/src/app/routes/art/index.ts @@ -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; diff --git a/api/src/app/services/art.service.ts b/api/src/app/services/art.service.ts new file mode 100644 index 0000000..6f3cb0f --- /dev/null +++ b/api/src/app/services/art.service.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + await this.prisma.art.delete({ + where: { id }, + }); + } +} diff --git a/api/src/app/services/comment.service.ts b/api/src/app/services/comment.service.ts index 70fae25..4d777e0 100644 --- a/api/src/app/services/comment.service.ts +++ b/api/src/app/services/comment.service.ts @@ -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 { + 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 { + 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 { await this.prisma.comment.delete({ where: { id: commentId }, diff --git a/apps/frontend/src/app/app.routes.ts b/apps/frontend/src/app/app.routes.ts index e047fbf..140a48e 100644 --- a/apps/frontend/src/app/app.routes.ts +++ b/apps/frontend/src/app/app.routes.ts @@ -17,6 +17,10 @@ export const appRoutes: Route[] = [ path: 'music', loadComponent: () => import('./components/music/music-list.component').then(m => m.MusicListComponent) }, + { + path: 'art', + loadComponent: () => import('./components/art/art-gallery.component').then(m => m.ArtGalleryComponent) + }, { path: '**', redirectTo: '' diff --git a/apps/frontend/src/app/components/art/art-gallery.component.ts b/apps/frontend/src/app/components/art/art-gallery.component.ts new file mode 100644 index 0000000..811fbd4 --- /dev/null +++ b/apps/frontend/src/app/components/art/art-gallery.component.ts @@ -0,0 +1,824 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { Component, OnInit, inject, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ArtService } from '../../services/art.service'; +import { AuthService } from '../../services/auth.service'; +import { CommentsService } from '../../services/comments.service'; +import { Art, CreateArtDto, UpdateArtDto, Comment } from '@library/shared-types'; + +@Component({ + selector: 'app-art-gallery', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+
+

Art Gallery

+

Artwork of Naomi

+ @if (authService.isAdmin()) { + + } +
+ +
+

+ A note on AI-generated art: Yes, we understand the negative impacts of AI art on working artists. + However, seeing ourselves represented gives us gender euphoria and dopamine hits in an otherwise shitty and depressing world. + If you want to yell at someone about AI art, go find someone who's actually profiting from their AI slop instead of bothering us. Thanks! +

+

+ That said, if you'd like to draw Naomi and send it our way, we'd be absolutely delighted to display it here! +

+
+ + @if (showAddForm() && authService.isAdmin()) { +
+

Add New Artwork

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + @if (newArt.imageUrl) { +
+

Preview:

+ +
+ } + +
+ + +
+
+ } + + @if (editingArt() && authService.isAdmin()) { +
+

Edit Artwork

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + @if (editArt.imageUrl) { +
+

Preview:

+ +
+ } + +
+ + +
+
+ } + + @if (loading()) { +
Loading gallery...
+ } @else if (artPieces().length === 0) { +
+

No artwork in the gallery yet.

+
+ } @else { + + } + + @if (lightboxArt()) { + + } +
+ `, + styles: [` + .container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; + } + + .header-section { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 2rem; + gap: 0.5rem; + } + + .header-section h2 { + margin: 0; + } + + .subtitle { + color: var(--witch-mauve); + margin: 0; + font-style: italic; + } + + .disclaimer { + background: linear-gradient(135deg, var(--witch-lavender) 0%, var(--witch-mauve) 100%); + border: 2px solid var(--witch-plum); + border-radius: 8px; + padding: 1rem 1.5rem; + margin-bottom: 2rem; + max-width: 800px; + margin-left: auto; + margin-right: auto; + } + + .disclaimer p { + margin: 0; + color: var(--witch-purple); + font-size: 0.95rem; + line-height: 1.6; + text-align: center; + } + + .disclaimer p.callout { + margin-top: 0.75rem; + font-style: italic; + color: var(--witch-plum); + } + + .disclaimer strong { + color: var(--witch-plum); + } + + .add-form { + background: rgba(255, 255, 255, 0.95); + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + border: 2px solid var(--witch-lavender); + backdrop-filter: blur(10px); + max-width: 600px; + margin-left: auto; + margin-right: auto; + } + + .form-group { + margin-bottom: 1rem; + } + + .form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--witch-plum); + } + + .form-group input, + .form-group textarea { + width: 100%; + padding: 0.75rem; + border: 2px solid var(--witch-lavender); + border-radius: 4px; + font-size: 1rem; + background-color: var(--witch-moon); + color: var(--witch-purple); + transition: border-color 0.3s; + } + + .form-group input:focus, + .form-group textarea:focus { + outline: none; + border-color: var(--witch-rose); + } + + .image-preview { + margin: 1rem 0; + text-align: center; + } + + .image-preview img { + max-width: 300px; + max-height: 200px; + border-radius: 8px; + border: 2px solid var(--witch-lavender); + } + + .form-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + } + + .loading, .empty-state { + text-align: center; + padding: 3rem; + color: var(--witch-mauve); + } + + .gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 2rem; + } + + .art-card { + background: rgba(255, 255, 255, 0.95); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px var(--witch-shadow); + border: 2px solid var(--witch-lavender); + transition: transform 0.3s, box-shadow 0.3s; + } + + .art-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px var(--witch-shadow); + } + + .art-image-container { + width: 100%; + aspect-ratio: 1; + overflow: hidden; + cursor: pointer; + } + + .art-image { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s; + } + + .art-image:hover { + transform: scale(1.05); + } + + .art-info { + padding: 1rem; + } + + .art-info h3 { + margin: 0 0 0.5rem 0; + color: var(--witch-plum); + } + + .artist { + color: var(--witch-mauve); + font-style: italic; + margin: 0 0 0.5rem 0; + } + + .date-added { + font-size: 0.85rem; + color: var(--witch-mauve); + margin: 0 0 1rem 0; + } + + .actions { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; + } + + .btn { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s; + } + + .btn:hover { + opacity: 0.9; + transform: translateY(-2px); + } + + .btn-primary { + background: var(--witch-rose); + color: var(--witch-moon); + } + + .btn-secondary { + background: var(--witch-mauve); + color: var(--witch-purple); + } + + .btn-danger { + background: var(--witch-plum); + color: var(--witch-moon); + } + + .btn-sm { + padding: 0.25rem 0.75rem; + font-size: 0.85rem; + } + + .btn-xs { + padding: 0.15rem 0.5rem; + font-size: 0.75rem; + } + + /* Lightbox */ + .lightbox { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + .lightbox-content { + position: relative; + max-width: 90vw; + max-height: 90vh; + text-align: center; + } + + .lightbox-content img { + max-width: 100%; + max-height: 80vh; + border-radius: 8px; + } + + .lightbox-close { + position: absolute; + top: -40px; + right: 0; + background: none; + border: none; + color: white; + font-size: 2rem; + cursor: pointer; + } + + .lightbox-info { + color: white; + margin-top: 1rem; + } + + .lightbox-info h3 { + margin: 0; + color: white; + } + + .lightbox-info p { + margin: 0.5rem 0 0 0; + opacity: 0.8; + } + + /* Comments */ + .comments-section { + margin-top: 1rem; + border-top: 1px solid var(--witch-lavender); + padding-top: 1rem; + } + + .comments-toggle { + width: 100%; + } + + .comments-container { + margin-top: 1rem; + } + + .comment-form { + margin-bottom: 1rem; + } + + .comment-form textarea { + width: 100%; + padding: 0.5rem; + border: 2px solid var(--witch-lavender); + border-radius: 4px; + font-size: 0.9rem; + background-color: var(--witch-moon); + color: var(--witch-purple); + resize: vertical; + margin-bottom: 0.5rem; + } + + .comment-form textarea:focus { + outline: none; + border-color: var(--witch-rose); + } + + .comment { + background: var(--witch-moon); + border: 1px solid var(--witch-lavender); + border-radius: 4px; + padding: 0.75rem; + margin-bottom: 0.5rem; + } + + .comment-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; + } + + .comment-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + } + + .comment-author { + font-weight: 500; + color: var(--witch-plum); + } + + .comment-date { + font-size: 0.75rem; + color: var(--witch-mauve); + } + + .comment-content { + font-size: 0.9rem; + color: var(--witch-purple); + } + + .comments-loading, + .no-comments { + text-align: center; + padding: 1rem; + color: var(--witch-mauve); + font-size: 0.9rem; + } + `] +}) +export class ArtGalleryComponent implements OnInit { + artService = inject(ArtService); + authService = inject(AuthService); + commentsService = inject(CommentsService); + + artPieces = signal([]); + loading = signal(true); + showAddForm = signal(false); + editingArt = signal(null); + lightboxArt = signal(null); + + // Comments state + comments = signal>({}); + commentsLoading = signal>({}); + expandedComments = signal>({}); + newCommentContent: Record = {}; + + newArt: Partial = { + title: '', + artist: '', + imageUrl: '', + description: '' + }; + + editArt: Partial = {}; + + ngOnInit() { + this.loadArt(); + } + + loadArt() { + this.loading.set(true); + this.artService.getAllArt().subscribe({ + next: (art) => { + this.artPieces.set(art); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + } + }); + } + + toggleAddForm() { + this.showAddForm.update(v => !v); + if (!this.showAddForm()) { + this.resetForm(); + } + } + + resetForm() { + this.newArt = { + title: '', + artist: '', + imageUrl: '', + description: '' + }; + } + + addArt() { + if (!this.newArt.title || !this.newArt.artist || !this.newArt.imageUrl) return; + + const artToAdd: CreateArtDto = { + title: this.newArt.title, + artist: this.newArt.artist, + imageUrl: this.newArt.imageUrl, + description: this.newArt.description + }; + + this.artService.createArt(artToAdd).subscribe(() => { + this.loadArt(); + this.toggleAddForm(); + }); + } + + deleteArt(art: Art) { + if (confirm(`Are you sure you want to delete "${art.title}"?`)) { + this.artService.deleteArt(art.id).subscribe(() => { + this.loadArt(); + }); + } + } + + startEdit(art: Art) { + this.editingArt.set(art); + this.editArt = { + title: art.title, + artist: art.artist, + imageUrl: art.imageUrl, + description: art.description + }; + this.showAddForm.set(false); + } + + cancelEdit() { + this.editingArt.set(null); + this.editArt = {}; + } + + saveEdit() { + const art = this.editingArt(); + if (!art || !this.editArt.title || !this.editArt.artist || !this.editArt.imageUrl) return; + + this.artService.updateArt(art.id, this.editArt).subscribe(() => { + this.loadArt(); + this.cancelEdit(); + }); + } + + openLightbox(art: Art) { + this.lightboxArt.set(art); + } + + closeLightbox() { + this.lightboxArt.set(null); + } + + onImageError(event: Event) { + const img = event.target as HTMLImageElement; + img.style.display = 'none'; + } + + formatDate(date: Date | string): string { + return new Date(date).toLocaleDateString(); + } + + // Comments methods + toggleComments(artId: string) { + const expanded = this.expandedComments(); + const isCurrentlyExpanded = expanded[artId]; + + this.expandedComments.set({ + ...expanded, + [artId]: !isCurrentlyExpanded + }); + + if (!isCurrentlyExpanded && !this.comments()[artId]) { + this.loadComments(artId); + } + } + + loadComments(artId: string) { + this.commentsLoading.set({ + ...this.commentsLoading(), + [artId]: true + }); + + this.commentsService.getCommentsForArt(artId).subscribe({ + next: (comments) => { + this.comments.set({ + ...this.comments(), + [artId]: comments + }); + this.commentsLoading.set({ + ...this.commentsLoading(), + [artId]: false + }); + }, + error: () => { + this.commentsLoading.set({ + ...this.commentsLoading(), + [artId]: false + }); + } + }); + } + + getCommentCount(artId: string): number { + return this.comments()[artId]?.length || 0; + } + + addComment(artId: string) { + const content = this.newCommentContent[artId]; + if (!content?.trim()) return; + + this.commentsService.addCommentToArt(artId, { content }).subscribe({ + next: (comment) => { + this.comments.set({ + ...this.comments(), + [artId]: [comment, ...(this.comments()[artId] || [])] + }); + this.newCommentContent[artId] = ''; + } + }); + } + + deleteComment(artId: string, commentId: string) { + if (!confirm('Are you sure you want to delete this comment?')) return; + + this.commentsService.deleteCommentFromArt(artId, commentId).subscribe({ + next: () => { + this.comments.set({ + ...this.comments(), + [artId]: (this.comments()[artId] || []).filter(c => c.id !== commentId) + }); + } + }); + } +} diff --git a/apps/frontend/src/app/components/header/header.component.ts b/apps/frontend/src/app/components/header/header.component.ts index d3c6bf0..48a3f00 100644 --- a/apps/frontend/src/app/components/header/header.component.ts +++ b/apps/frontend/src/app/components/header/header.component.ts @@ -24,6 +24,7 @@ import { AuthService } from '../../services/auth.service';
  • Games
  • Books
  • Music
  • +
  • Art
  • diff --git a/apps/frontend/src/app/services/art.service.ts b/apps/frontend/src/app/services/art.service.ts new file mode 100644 index 0000000..1824b97 --- /dev/null +++ b/apps/frontend/src/app/services/art.service.ts @@ -0,0 +1,37 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ApiService } from './api.service'; +import { Art, CreateArtDto, UpdateArtDto } from '@library/shared-types'; + +@Injectable({ + providedIn: 'root' +}) +export class ArtService { + constructor(private api: ApiService) {} + + getAllArt(): Observable { + return this.api.get('/art'); + } + + getArtById(id: string): Observable { + return this.api.get(`/art/${id}`); + } + + createArt(art: CreateArtDto): Observable { + return this.api.post('/art', art); + } + + updateArt(id: string, art: UpdateArtDto): Observable { + return this.api.put(`/art/${id}`, art); + } + + deleteArt(id: string): Observable<{ success: boolean }> { + return this.api.delete<{ success: boolean }>(`/art/${id}`); + } +} diff --git a/apps/frontend/src/app/services/comments.service.ts b/apps/frontend/src/app/services/comments.service.ts index 120378f..0bb755b 100644 --- a/apps/frontend/src/app/services/comments.service.ts +++ b/apps/frontend/src/app/services/comments.service.ts @@ -50,4 +50,16 @@ export class CommentsService { deleteCommentFromMusic(musicId: string, commentId: string): Observable<{ success: boolean }> { return this.api.delete<{ success: boolean }>(`/music/${musicId}/comments/${commentId}`); } + + getCommentsForArt(artId: string): Observable { + return this.api.get(`/art/${artId}/comments`); + } + + addCommentToArt(artId: string, comment: CreateCommentDto): Observable { + return this.api.post(`/art/${artId}/comments`, comment); + } + + deleteCommentFromArt(artId: string, commentId: string): Observable<{ success: boolean }> { + return this.api.delete<{ success: boolean }>(`/art/${artId}/comments/${commentId}`); + } } diff --git a/shared-types/src/index.ts b/shared-types/src/index.ts index 7ba5b4f..7e980f2 100644 --- a/shared-types/src/index.ts +++ b/shared-types/src/index.ts @@ -6,5 +6,6 @@ export * from "./lib/game.types"; export * from "./lib/book.types"; export * from "./lib/music.types"; +export * from "./lib/art.types"; export type * from "./lib/auth.types"; export * from "./lib/comment.types"; \ No newline at end of file diff --git a/shared-types/src/lib/art.types.ts b/shared-types/src/lib/art.types.ts new file mode 100644 index 0000000..9f1515e --- /dev/null +++ b/shared-types/src/lib/art.types.ts @@ -0,0 +1,24 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + */ + +export interface Art { + id: string; + title: string; + artist: string; + description?: string; + imageUrl: string; + dateAdded: Date; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateArtDto { + title: string; + artist: string; + description?: string; + imageUrl: string; +} + +export interface UpdateArtDto extends Partial {} diff --git a/shared-types/src/lib/comment.types.ts b/shared-types/src/lib/comment.types.ts index ac21191..b2440af 100644 --- a/shared-types/src/lib/comment.types.ts +++ b/shared-types/src/lib/comment.types.ts @@ -18,6 +18,7 @@ export interface Comment { gameId?: string; bookId?: string; musicId?: string; + artId?: string; createdAt: Date; updatedAt: Date; }