diff --git a/backend/src/index.ts b/backend/src/index.ts index 7b743d1..781d8db 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -15,9 +15,14 @@ const typeDefs = `#graphql image_url: String } + type ArtworkPage { + artworks: [Artwork!]! + totalCount: Int! + } + type Query { hello: String! - artworks: [Artwork!]! + artworks(limit: Int!, offset: Int!): ArtworkPage! } `; @@ -26,8 +31,25 @@ const resolvers = { Query: { hello: () => "Hello from Artex backend!", - artworks: async () => { - return await db.collection("met_artworks").find({}).toArray(); + artworks: async ( + _: unknown, + { limit, offset }: { limit: number; offset: number }, + ) => { + if (limit < 1 || limit > 100 || offset < 0) { + throw new Error("Invalid pagination parameters"); + } + const collection = db.collection("met_artworks"); + + const [artworks, totalCount] = await Promise.all([ + collection.find({}).sort({ id: 1 }).skip(offset).limit(limit).toArray(), + + collection.countDocuments({}), + ]); + + return { + artworks, + totalCount, + }; }, }, }; diff --git a/frontend/index.html b/frontend/index.html index 0fca6f0..241c301 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - frontend + Artex
diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000..28a265f Binary files /dev/null and b/frontend/public/favicon.png differ diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/frontend/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg deleted file mode 100644 index e952219..0000000 --- a/frontend/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx new file mode 100644 index 0000000..00c4237 --- /dev/null +++ b/frontend/src/components/Pagination.tsx @@ -0,0 +1,42 @@ +type PaginationProps = { + page: number; + totalPages: number; + onPageChange: (page: number) => void; +}; + +export default function Pagination({ + page, + totalPages, + onPageChange, +}: PaginationProps) { + if (totalPages <= 1) return null; + + return ( + + ); +} diff --git a/frontend/src/components/PlaceHolderComp.tsx b/frontend/src/components/PlaceHolderComp.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/hooks/useArtworks.ts b/frontend/src/hooks/useArtworks.ts index 1826db4..430c333 100644 --- a/frontend/src/hooks/useArtworks.ts +++ b/frontend/src/hooks/useArtworks.ts @@ -1,32 +1,47 @@ import { useEffect, useState } from "react"; import type { Artwork } from "../types/Artwork"; -export function useArtworks() { +export function useArtworks(page: number, pageSize = 12) { const [artworks, setArtworks] = useState([]); + const [totalCount, setTotalCount] = useState(0); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); useEffect(() => { + const controller = new AbortController(); + async function fetchArtworks() { + setLoading(true); + setError(""); + try { const response = await fetch("/graphql", { method: "POST", headers: { "Content-Type": "application/json", }, + signal: controller.signal, body: JSON.stringify({ query: ` - query { - artworks { - id - title - artist_display - image_url - } - } - `, + query GetArtworks($limit: Int!, $offset: Int!) { + artworks(limit: $limit, offset: $offset) { + totalCount + artworks { + id + title + artist_display + image_url + } + } + } + `, + variables: { + limit: pageSize, + offset: (page - 1) * pageSize, + }, }), }); + if (!response.ok) { throw new Error(`HTTP ${response.status}`); } @@ -37,16 +52,23 @@ export function useArtworks() { throw new Error(result.errors[0].message); } - setArtworks(result.data.artworks.slice(0, 12)); + setArtworks(result.data.artworks.artworks); + setTotalCount(result.data.artworks.totalCount); } catch (err) { + if (controller.signal.aborted) return; + setError(err instanceof Error ? err.message : "Unknown error"); } finally { - setLoading(false); + if (!controller.signal.aborted) { + setLoading(false); + } } } fetchArtworks(); - }, []); - return { artworks, loading, error }; + return () => controller.abort(); + }, [page, pageSize]); + + return { artworks, totalCount, loading, error }; } diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 698d708..acb6b66 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,17 +1,39 @@ +import { useState } from "react"; import ArtworkGrid from "../components/ArtworkGrid"; import Hero from "../components/Hero"; +import Pagination from "../components/Pagination"; import { useArtworks } from "../hooks/useArtworks"; +const PAGE_SIZE = 12; + export default function HomePage() { - const { artworks, loading, error } = useArtworks(); + const [page, setPage] = useState(1); + + const { artworks, totalCount, loading, error } = useArtworks(page, PAGE_SIZE); - if (loading) return

Loading artworks...

; - if (error) return

Error: {error}

; + const totalPages = Math.ceil(totalCount / PAGE_SIZE); return (
- + +
+ {error &&

Error: {error}

} + + {loading &&

Loading artworks...

} + +
+ +
+ + {!error && ( + + )} +
); }