From 39fd4219ad330787d911ef6b37526d958afe66e2 Mon Sep 17 00:00:00 2001 From: mina Date: Thu, 29 Oct 2026 20:33:27 +0100 Subject: [PATCH 1/5] feat: add artworks to galleries --- .../src/graphql/resolvers/galleryResolvers.ts | 58 ++++++ backend/src/graphql/resolvers/index.ts | 1 + backend/src/graphql/typeDefs.ts | 1 + frontend/src/components/AddToGalleryModal.tsx | 183 ++++++++++++++++++ frontend/src/pages/ArtworkDetailPage.tsx | 11 ++ 5 files changed, 254 insertions(+) create mode 100644 frontend/src/components/AddToGalleryModal.tsx diff --git a/backend/src/graphql/resolvers/galleryResolvers.ts b/backend/src/graphql/resolvers/galleryResolvers.ts index 3664b25..dfe4e16 100644 --- a/backend/src/graphql/resolvers/galleryResolvers.ts +++ b/backend/src/graphql/resolvers/galleryResolvers.ts @@ -59,5 +59,63 @@ export function createGalleryResolvers(db: Db) { artworkIds: [], }; }, + + addArtworkToGallery: async ( + _parent: unknown, + args: { galleryId: string; artworkId: number }, + context: Context, + ) => { + // Check that the user is logged in + if (!context.userId || !ObjectId.isValid(context.userId)) { + throw new GraphQLError("You must be logged in"); + } + + // Validate the gallery ID before converting it to ObjectId + if (!ObjectId.isValid(args.galleryId)) { + throw new GraphQLError("Invalid gallery"); + } + + const galleryId = new ObjectId(args.galleryId); + const userId = new ObjectId(context.userId); + + // Make sure the gallery exists and belongs to the logged-in user + const gallery = await db.collection("galleries").findOne({ + _id: galleryId, + userId, + }); + + if (!gallery) { + throw new GraphQLError("Gallery not found"); + } + + // Add the artwork ID without creating duplicates + await db.collection("galleries").updateOne( + { + _id: galleryId, + userId, + }, + { + $addToSet: { + artworkIds: args.artworkId, + }, + }, + ); + + // Fetch the updated gallery so it can be returned to the client + const updatedGallery = await db.collection("galleries").findOne({ + _id: galleryId, + }); + + if (!updatedGallery) { + throw new GraphQLError("Gallery not found"); + } + + return { + id: updatedGallery._id.toString(), + userId: updatedGallery.userId.toString(), + title: updatedGallery.title, + artworkIds: updatedGallery.artworkIds, + }; + }, }; } diff --git a/backend/src/graphql/resolvers/index.ts b/backend/src/graphql/resolvers/index.ts index 07169e8..861f644 100644 --- a/backend/src/graphql/resolvers/index.ts +++ b/backend/src/graphql/resolvers/index.ts @@ -20,6 +20,7 @@ export function createResolvers(db: Db) { register: userResolvers.register, updateProfileImage: userResolvers.updateProfileImage, createGallery: galleryResolvers.createGallery, + addArtworkToGallery: galleryResolvers.addArtworkToGallery, updateCoverImage: userResolvers.updateCoverImage, }, }; diff --git a/backend/src/graphql/typeDefs.ts b/backend/src/graphql/typeDefs.ts index d71398f..f3fa358 100644 --- a/backend/src/graphql/typeDefs.ts +++ b/backend/src/graphql/typeDefs.ts @@ -74,6 +74,7 @@ export const typeDefs = `#graphql register(username: String!, password: String!): User! login(username: String!, password: String!): AuthPayload! createGallery(title: String!): Gallery! + addArtworkToGallery(galleryId: ID!, artworkId: Int!): Gallery! updateProfileImage(image: String!): User! updateCoverImage(image: String!): User! } diff --git a/frontend/src/components/AddToGalleryModal.tsx b/frontend/src/components/AddToGalleryModal.tsx new file mode 100644 index 0000000..dde823b --- /dev/null +++ b/frontend/src/components/AddToGalleryModal.tsx @@ -0,0 +1,183 @@ +import { useEffect, useState } from "react"; +import type { Gallery } from "../types/Gallery"; + +type AddToGalleryModalProps = { + artworkId: number; + onClose: () => void; +}; + +type GalleriesResponse = { + data?: { + myGalleries: Gallery[]; + }; + errors?: { message: string }[]; +}; + +type AddArtworkResponse = { + data?: { + addArtworkToGallery: Gallery; + }; + errors?: { message: string }[]; +}; + +export default function AddToGalleryModal({ + artworkId, + onClose, +}: AddToGalleryModalProps) { + const [galleries, setGalleries] = useState([]); + const [loading, setLoading] = useState(true); + const [message, setMessage] = useState(""); + + useEffect(() => { + async function fetchGalleries() { + const query = ` + query { + myGalleries { + id + userId + title + artworkIds + } + } + `; + + try { + const token = sessionStorage.getItem("token"); + + const response = await fetch("/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token ?? ""}`, + }, + body: JSON.stringify({ query }), + }); + + if (!response.ok) { + throw new Error("Could not fetch galleries"); + } + + const result = (await response.json()) as GalleriesResponse; + + if (result.errors?.length) { + throw new Error(result.errors[0].message); + } + + setGalleries(result.data?.myGalleries ?? []); + } catch (error) { + setMessage( + error instanceof Error ? error.message : "Something went wrong", + ); + } finally { + setLoading(false); + } + } + + void fetchGalleries(); + }, []); + + async function handleAddToGallery(galleryId: string) { + const mutation = ` + mutation AddArtworkToGallery($galleryId: ID!, $artworkId: Int!) { + addArtworkToGallery( + galleryId: $galleryId + artworkId: $artworkId + ) { + id + title + artworkIds + } + } + `; + + try { + setMessage(""); + + const token = sessionStorage.getItem("token"); + + const response = await fetch("/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token ?? ""}`, + }, + body: JSON.stringify({ + query: mutation, + variables: { + galleryId, + artworkId, + }, + }), + }); + + if (!response.ok) { + throw new Error("Could not add artwork to gallery"); + } + + const result = (await response.json()) as AddArtworkResponse; + + if (result.errors?.length) { + throw new Error(result.errors[0].message); + } + + onClose(); + } catch (error) { + setMessage( + error instanceof Error ? error.message : "Something went wrong", + ); + } + } + + return ( +
+
+
+ + + +
+ + {loading ? ( +

Loading galleries...

+ ) : galleries.length === 0 ? ( +

+ You have not created any galleries yet. +

+ ) : ( +
+ {galleries.map((gallery) => ( + + ))} +
+ )} + + {message && ( +

+ {message} +

+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/ArtworkDetailPage.tsx b/frontend/src/pages/ArtworkDetailPage.tsx index 517b49b..0cad80c 100644 --- a/frontend/src/pages/ArtworkDetailPage.tsx +++ b/frontend/src/pages/ArtworkDetailPage.tsx @@ -1,5 +1,7 @@ import { Link, useParams } from "react-router-dom"; import { useArtwork } from "../hooks/useArtwork"; +import { useState } from "react"; +import AddToGalleryModal from "../components/AddToGalleryModal"; // ChatGPT was used to assist with the implementation of this page. export default function ArtworkDetailPage() { @@ -7,6 +9,7 @@ export default function ArtworkDetailPage() { const artworkId = Number(id); const { artwork, loading, error } = useArtwork(artworkId); + const [showGalleryModal, setShowGalleryModal] = useState(false); if (loading) { return ( @@ -51,6 +54,7 @@ export default function ArtworkDetailPage() {