diff --git a/frontend/src/components/ArtworkCard.css b/frontend/src/components/ArtworkCard.css
new file mode 100644
index 0000000..737ab3f
--- /dev/null
+++ b/frontend/src/components/ArtworkCard.css
@@ -0,0 +1,41 @@
+.artwork-card {
+ min-width: 0;
+ padding: 16px;
+ border: 1px solid #dedede;
+ border-radius: 4px;
+ background: #fff;
+ color: #171717;
+ text-align: left;
+}
+
+.artwork-card__image,
+.artwork-card__placeholder {
+ display: block;
+ width: 100%;
+ aspect-ratio: 4 / 5;
+}
+
+.artwork-card__image {
+ object-fit: contain;
+ background: #f5f4f1;
+}
+
+.artwork-card__placeholder {
+ display: grid;
+ place-items: center;
+ background: #f5f4f1;
+ color: #595959;
+}
+
+.artwork-card__title {
+ margin: 16px 0 8px;
+ font-size: 1.5rem;
+ line-height: 1.2;
+ overflow-wrap: anywhere;
+}
+
+.artwork-card__artist {
+ margin: 0;
+ color: #595959;
+ overflow-wrap: anywhere;
+}
\ No newline at end of file
diff --git a/frontend/src/components/ArtworkCard.tsx b/frontend/src/components/ArtworkCard.tsx
new file mode 100644
index 0000000..897e1c8
--- /dev/null
+++ b/frontend/src/components/ArtworkCard.tsx
@@ -0,0 +1,52 @@
+import { useState } from 'react'
+import type { Artwork } from '../types/Artwork'
+import './ArtworkCard.css'
+
+type ArtworkCardProps = {
+ artwork: Artwork
+}
+
+function ArtworkImage({ src, alt }: { src: string; alt: string }) {
+ const [hasError, setHasError] = useState(false)
+
+ if (hasError) {
+ return (
+
+ Image unavailable
+
+ )
+ }
+
+ return (
+
setHasError(true)}
+ />
+ )
+}
+
+export default function ArtworkCard({ artwork }: ArtworkCardProps) {
+ const artist = artwork.artist?.trim() || 'Unknown artist'
+
+ return (
+
+ {artwork.imageUrl ? (
+
+ ) : (
+
+ Image unavailable
+
+ )}
+
+ {artwork.title}
+ {artist}
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/ArtworkGrid.css b/frontend/src/components/ArtworkGrid.css
new file mode 100644
index 0000000..904c56e
--- /dev/null
+++ b/frontend/src/components/ArtworkGrid.css
@@ -0,0 +1,19 @@
+.artwork-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ align-items: start;
+ gap: 20px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.artwork-grid > li {
+ min-width: 0;
+}
+
+@media (min-width: 1024px) {
+ .artwork-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/components/ArtworkGrid.tsx b/frontend/src/components/ArtworkGrid.tsx
new file mode 100644
index 0000000..d12f73a
--- /dev/null
+++ b/frontend/src/components/ArtworkGrid.tsx
@@ -0,0 +1,23 @@
+import type { Artwork } from '../types/Artwork'
+import ArtworkCard from './ArtworkCard'
+import './ArtworkGrid.css'
+
+type ArtworkGridProps = {
+ artworks: Artwork[]
+}
+
+export default function ArtworkGrid({ artworks }: ArtworkGridProps) {
+ if (artworks.length === 0) {
+ return No artworks to display.
+ }
+
+ return (
+
+ {artworks.map((artwork) => (
+ -
+
+
+ ))}
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/types/Artwork.ts b/frontend/src/types/Artwork.ts
new file mode 100644
index 0000000..f44a2eb
--- /dev/null
+++ b/frontend/src/types/Artwork.ts
@@ -0,0 +1,8 @@
+export type Artwork = {
+ id: string | number
+ title: string
+ artist: string | null
+ imageUrl: string | null
+ imageAlt?: string | null
+ date?: string | null
+}