Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions frontend/src/components/ArtworkCard.css
Original file line number Diff line number Diff line change
@@ -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;
}
52 changes: 52 additions & 0 deletions frontend/src/components/ArtworkCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="artwork-card__placeholder">
Image unavailable
</div>
)
}

return (
<img
className="artwork-card__image"
src={src}
alt={alt}
loading="lazy"
onError={() => setHasError(true)}
/>
)
}

export default function ArtworkCard({ artwork }: ArtworkCardProps) {
const artist = artwork.artist?.trim() || 'Unknown artist'

return (
<article className="artwork-card">
{artwork.imageUrl ? (
<ArtworkImage
key={artwork.imageUrl}
src={artwork.imageUrl}
alt={artwork.imageAlt || `${artwork.title} by ${artist}`}
/>
) : (
<div className="artwork-card__placeholder">
Image unavailable
</div>
)}

<h3 className="artwork-card__title">{artwork.title}</h3>
<p className="artwork-card__artist">{artist}</p>
</article>
)
}
19 changes: 19 additions & 0 deletions frontend/src/components/ArtworkGrid.css
Original file line number Diff line number Diff line change
@@ -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));
}
}
23 changes: 23 additions & 0 deletions frontend/src/components/ArtworkGrid.tsx
Original file line number Diff line number Diff line change
@@ -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 <p>No artworks to display.</p>
}

return (
<ul className="artwork-grid" aria-label="Artwork results" role="list">
{artworks.map((artwork) => (
<li key={artwork.id}>
<ArtworkCard artwork={artwork} />
</li>
))}
</ul>
)
}
8 changes: 8 additions & 0 deletions frontend/src/types/Artwork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type Artwork = {
id: string | number
title: string
artist: string | null
imageUrl: string | null
imageAlt?: string | null
date?: string | null
}