Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
"test": "vitest run"
},
"dependencies": {
"@apollo/client": "^4.3.3",
"@apollo/client": "^4.3.8",
"@tailwindcss/vite": "^4.3.3",
"graphql": "^16.14.2",
"graphql": "^16.15.0",
"lucide-react": "^1.57.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
Expand Down
389 changes: 295 additions & 94 deletions frontend/src/components/CatalogueControls.tsx

Large diffs are not rendered by default.

133 changes: 20 additions & 113 deletions frontend/src/components/gallery/AddToGalleryModal.tsx
Original file line number Diff line number Diff line change
@@ -1,132 +1,38 @@
import { useEffect, useState } from "react";
import type { Gallery } from "../../types/Gallery";
import { useAuthStore } from "../../store/authStore";
import { useState } from "react";
import { useMyGalleries } from "../../hooks/gallery/useMyGallery";
import { useAddArtworkToGallery } from "../../hooks/gallery/useAddArtworkToGallery";

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<Gallery[]>([]);
const [loading, setLoading] = useState(true);
const [message, setMessage] = useState("");

const token = useAuthStore((state) => state.token);

useEffect(() => {
async function fetchGalleries() {
const query = `
query {
myGalleries {
id
userId
title
artworkIds
}
}
`;

try {
const response = await fetch(`${import.meta.env.BASE_URL}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 { galleries, isLoading, error: galleriesError } = useMyGalleries();

const result = (await response.json()) as GalleriesResponse;
const { addArtwork, adding, error: addError } = useAddArtworkToGallery();

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();
}, [token]);
const [message, setMessage] = useState("");

async function handleAddToGallery(galleryId: string) {
const mutation = `
mutation AddArtworkToGallery($galleryId: ID!, $artworkId: Int!) {
addArtworkToGallery(
galleryId: $galleryId
artworkId: $artworkId
) {
id
title
artworkIds
}
}
`;
if (adding) return;

try {
setMessage("");
setMessage("");

const response = await fetch(`${import.meta.env.BASE_URL}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);
}
const updatedGallery = await addArtwork(galleryId, artworkId);

if (updatedGallery) {
onClose();
} catch (error) {
setMessage(
error instanceof Error ? error.message : "Something went wrong",
);
} else {
setMessage("Could not add artwork to gallery");
}
}

const errorMessage = message || addError || galleriesError;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4">
<div
Expand All @@ -150,11 +56,11 @@ export default function AddToGalleryModal({
</button>
</div>

{loading ? (
{isLoading ? (
<p className="mt-6 text-gray-500 dark:text-gray-400">
Loading galleries...
</p>
) : galleries.length === 0 ? (
) : galleries.length === 0 && !galleriesError ? (
<p className="mt-6 text-gray-500 dark:text-gray-400">
You have not created any galleries yet.
</p>
Expand All @@ -164,18 +70,19 @@ export default function AddToGalleryModal({
<button
key={gallery.id}
type="button"
disabled={adding}
onClick={() => void handleAddToGallery(gallery.id)}
className="rounded-lg border border-gray-200 px-4 py-3 text-left transition hover:bg-gray-100 dark:border-[#504b46] dark:hover:bg-[#35322e]"
className="rounded-lg border border-gray-200 px-4 py-3 text-left transition disabled:opacity-50 hover:bg-gray-100 dark:border-[#504b46] dark:hover:bg-[#35322e]"
>
{gallery.title}
</button>
))}
</div>
)}

{message && (
{errorMessage && (
<p role="alert" className="mt-4 text-sm text-red-600">
{message}
{errorMessage}
</p>
)}
</div>
Expand Down
47 changes: 3 additions & 44 deletions frontend/src/components/gallery/CreateGalleryModal.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,30 @@
import { LucideX } from "lucide-react";
import { useState } from "react";
import { useAuthStore } from "../../store/authStore";
import { useCreateGallery } from "../../hooks/gallery/useCreateGallery";

type CreateGalleryModalProps = {
onClose: () => void;
onCreated: () => void;
};

export default function CreateGalleryModal({
onClose,
onCreated,
}: CreateGalleryModalProps) {
const [title, setTitle] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState("");

const token = useAuthStore((state) => state.token);
const { createGallery, isCreating } = useCreateGallery();

async function handleCreateGallery() {
if (!title.trim() || isCreating) return;

setIsCreating(true);
setError("");

const query = `
mutation CreateGallery($title: String!) {
createGallery(title: $title) {
id
userId
title
artworkIds
}
}
`;

try {
const response = await fetch(`${import.meta.env.BASE_URL}graphql`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token ?? ""}`,
},
body: JSON.stringify({
query,
variables: { title: title.trim() },
}),
});

if (!response.ok) {
throw new Error("Could not create gallery");
}

const result = await response.json();

if (result.errors?.length) {
throw new Error(result.errors[0].message);
}
await createGallery(title);

if (!result.data?.createGallery) {
throw new Error("Gallery was not created");
}
onCreated();
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
} finally {
setIsCreating(false);
}
}

Expand Down
37 changes: 37 additions & 0 deletions frontend/src/hooks/artwork/useArtists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { gql, type TypedDocumentNode } from "@apollo/client";
import { useQuery } from "@apollo/client/react";

type ArtistsData = {
artists: string[];
};

type ArtistsVariables = {
search: string;
limit: number;
offset: number;
};

const GET_ARTISTS: TypedDocumentNode<ArtistsData, ArtistsVariables> = gql`
query ArtistOptions($search: String, $limit: Int!, $offset: Int!) {
artists(search: $search, limit: $limit, offset: $offset)
}
`;

export function useArtists(search = "", enabled = true) {
const { data, loading, error } = useQuery(GET_ARTISTS, {
variables: {
search: search.trim(),
limit: 50,
offset: 0,
},
skip: !enabled,
fetchPolicy: "cache-first",
notifyOnNetworkStatusChange: true,
});

return {
artists: data?.artists ?? [],
loading: enabled && loading,
error: enabled ? (error?.message ?? "") : "",
};
}
42 changes: 42 additions & 0 deletions frontend/src/hooks/artwork/useArtwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { gql, type TypedDocumentNode } from "@apollo/client";
import { useQuery } from "@apollo/client/react";
import type { Artwork } from "../../types/Artwork";

type ArtworkData = {
artwork: Artwork | null;
};

type ArtworkVariables = {
id: number;
};

const GET_ARTWORK: TypedDocumentNode<ArtworkData, ArtworkVariables> = gql`
query GetArtwork($id: Int!) {
artwork(id: $id) {
id
title
artist_display
date_display
image_url
medium
dimensions
country
city
culture
period
}
}
`;

export function useArtwork(id: number) {
const { data, loading, error } = useQuery(GET_ARTWORK, {
variables: { id },
fetchPolicy: "cache-first",
});

return {
artwork: data?.artwork ?? null,
loading,
error: error?.message ?? "",
};
}
Loading