feat: added art a day
0ba191e3
1 file(s) · +183 −0
| 1 | + | --- |
|
| 2 | + | export const prerender = false; |
|
| 3 | + | ||
| 4 | + | import PageLayout from "@/layouts/Base.astro"; |
|
| 5 | + | import { getFormattedDate } from "@/utils"; |
|
| 6 | + | import sanitizeHtml from "sanitize-html"; |
|
| 7 | + | ||
| 8 | + | type Artwork = { |
|
| 9 | + | id: number; |
|
| 10 | + | title: string; |
|
| 11 | + | artist_display: string | null; |
|
| 12 | + | artist_title: string | null; |
|
| 13 | + | date_display: string | null; |
|
| 14 | + | medium_display: string | null; |
|
| 15 | + | dimensions: string | null; |
|
| 16 | + | place_of_origin: string | null; |
|
| 17 | + | credit_line: string | null; |
|
| 18 | + | description: string | null; |
|
| 19 | + | short_description: string | null; |
|
| 20 | + | image_id: string | null; |
|
| 21 | + | }; |
|
| 22 | + | ||
| 23 | + | const meta = { |
|
| 24 | + | title: "Art a Day", |
|
| 25 | + | description: |
|
| 26 | + | "A different painting every day from the Art Institute of Chicago.", |
|
| 27 | + | }; |
|
| 28 | + | ||
| 29 | + | const SEARCH_URL = "https://api.artic.edu/api/v1/artworks/search"; |
|
| 30 | + | const FIELDS = [ |
|
| 31 | + | "id", |
|
| 32 | + | "title", |
|
| 33 | + | "artist_display", |
|
| 34 | + | "artist_title", |
|
| 35 | + | "date_display", |
|
| 36 | + | "medium_display", |
|
| 37 | + | "dimensions", |
|
| 38 | + | "place_of_origin", |
|
| 39 | + | "credit_line", |
|
| 40 | + | "description", |
|
| 41 | + | "short_description", |
|
| 42 | + | "image_id", |
|
| 43 | + | ].join(","); |
|
| 44 | + | ||
| 45 | + | const filterParams = { |
|
| 46 | + | query: { |
|
| 47 | + | bool: { |
|
| 48 | + | must: [ |
|
| 49 | + | { term: { is_public_domain: true } }, |
|
| 50 | + | { term: { "classification_title.keyword": "painting" } }, |
|
| 51 | + | { term: { "artwork_type_title.keyword": "Painting" } }, |
|
| 52 | + | { exists: { field: "image_id" } }, |
|
| 53 | + | ], |
|
| 54 | + | }, |
|
| 55 | + | }, |
|
| 56 | + | }; |
|
| 57 | + | const encodedParams = encodeURIComponent(JSON.stringify(filterParams)); |
|
| 58 | + | ||
| 59 | + | const today = new Date(); |
|
| 60 | + | const dayIndex = Math.floor(today.getTime() / 86_400_000); |
|
| 61 | + | ||
| 62 | + | let artwork: Artwork | null = null; |
|
| 63 | + | let error: string | null = null; |
|
| 64 | + | ||
| 65 | + | const aicHeaders = { |
|
| 66 | + | "AIC-User-Agent": "stevedylan.dev (stevedsimkins@gmail.com)", |
|
| 67 | + | "User-Agent": "stevedylan.dev (stevedsimkins@gmail.com)", |
|
| 68 | + | Accept: "application/json", |
|
| 69 | + | }; |
|
| 70 | + | ||
| 71 | + | try { |
|
| 72 | + | const countRes = await fetch( |
|
| 73 | + | `${SEARCH_URL}?params=${encodedParams}&limit=1&fields=id`, |
|
| 74 | + | { headers: aicHeaders }, |
|
| 75 | + | ); |
|
| 76 | + | if (!countRes.ok) throw new Error(`AIC count returned ${countRes.status}`); |
|
| 77 | + | const countJson = await countRes.json(); |
|
| 78 | + | const total: number = countJson?.pagination?.total ?? 0; |
|
| 79 | + | if (total <= 0) throw new Error("no paintings found"); |
|
| 80 | + | // AIC caps from + size at 10000; stay well under. |
|
| 81 | + | const safeTotal = Math.min(total, 9000); |
|
| 82 | + | const page = (dayIndex % safeTotal) + 1; |
|
| 83 | + | ||
| 84 | + | const pickRes = await fetch( |
|
| 85 | + | `${SEARCH_URL}?params=${encodedParams}&limit=1&page=${page}&fields=${FIELDS}`, |
|
| 86 | + | { headers: aicHeaders }, |
|
| 87 | + | ); |
|
| 88 | + | if (!pickRes.ok) throw new Error(`AIC pick returned ${pickRes.status}`); |
|
| 89 | + | const pickJson = await pickRes.json(); |
|
| 90 | + | artwork = pickJson?.data?.[0] ?? null; |
|
| 91 | + | if (!artwork) throw new Error("empty result"); |
|
| 92 | + | } catch (e) { |
|
| 93 | + | error = e instanceof Error ? e.message : "Failed to reach AIC API"; |
|
| 94 | + | } |
|
| 95 | + | ||
| 96 | + | const imageUrl = artwork?.image_id |
|
| 97 | + | ? `https://www.artic.edu/iiif/2/${artwork.image_id}/full/843,/0/default.jpg` |
|
| 98 | + | : null; |
|
| 99 | + | const sourceUrl = artwork |
|
| 100 | + | ? `https://www.artic.edu/artworks/${artwork.id}` |
|
| 101 | + | : null; |
|
| 102 | + | const cleanDescription = artwork?.description |
|
| 103 | + | ? sanitizeHtml(artwork.description, { |
|
| 104 | + | allowedTags: ["p", "em", "strong", "a", "br", "i", "b"], |
|
| 105 | + | allowedAttributes: { a: ["href", "title"] }, |
|
| 106 | + | }) |
|
| 107 | + | : null; |
|
| 108 | + | ||
| 109 | + | Astro.response.headers.set( |
|
| 110 | + | "Cache-Control", |
|
| 111 | + | "public, max-age=3600, s-maxage=86400", |
|
| 112 | + | ); |
|
| 113 | + | --- |
|
| 114 | + | ||
| 115 | + | <PageLayout meta={meta}> |
|
| 116 | + | <div class="flex min-h-screen flex-col items-start justify-start gap-6"> |
|
| 117 | + | <div class="flex flex-col gap-1"> |
|
| 118 | + | <h1 class="title">Art a Day</h1> |
|
| 119 | + | <p class="text-sm opacity-70"> |
|
| 120 | + | One painting every day from the |
|
| 121 | + | <a class="style-link" href="https://www.artic.edu/" target="_blank" rel="noopener">Art Institute of Chicago</a>. |
|
| 122 | + | {getFormattedDate(today)}. |
|
| 123 | + | </p> |
|
| 124 | + | </div> |
|
| 125 | + | {error || !artwork ? ( |
|
| 126 | + | <p class="text-red-400 text-sm">Couldn't load today's painting{error ? `: ${error}` : ""}.</p> |
|
| 127 | + | ) : ( |
|
| 128 | + | <article class="flex flex-col w-full gap-6"> |
|
| 129 | + | {imageUrl && ( |
|
| 130 | + | <img |
|
| 131 | + | src={imageUrl} |
|
| 132 | + | alt={artwork.title} |
|
| 133 | + | loading="eager" |
|
| 134 | + | class="w-full max-w-2xl h-auto" |
|
| 135 | + | /> |
|
| 136 | + | )} |
|
| 137 | + | <div class="flex flex-col gap-1"> |
|
| 138 | + | <h2 class="text-xl italic text-accent-2">{artwork.title}</h2> |
|
| 139 | + | {artwork.artist_display && ( |
|
| 140 | + | <p class="text-sm whitespace-pre-line">{artwork.artist_display}</p> |
|
| 141 | + | )} |
|
| 142 | + | {artwork.date_display && ( |
|
| 143 | + | <p class="text-sm opacity-70">{artwork.date_display}</p> |
|
| 144 | + | )} |
|
| 145 | + | </div> |
|
| 146 | + | <dl class="grid grid-cols-1 sm:grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm"> |
|
| 147 | + | {artwork.place_of_origin && ( |
|
| 148 | + | <> |
|
| 149 | + | <dt class="opacity-50 uppercase tracking-widest text-xs">Origin</dt> |
|
| 150 | + | <dd>{artwork.place_of_origin}</dd> |
|
| 151 | + | </> |
|
| 152 | + | )} |
|
| 153 | + | {artwork.medium_display && ( |
|
| 154 | + | <> |
|
| 155 | + | <dt class="opacity-50 uppercase tracking-widest text-xs">Medium</dt> |
|
| 156 | + | <dd>{artwork.medium_display}</dd> |
|
| 157 | + | </> |
|
| 158 | + | )} |
|
| 159 | + | {artwork.dimensions && ( |
|
| 160 | + | <> |
|
| 161 | + | <dt class="opacity-50 uppercase tracking-widest text-xs">Dimensions</dt> |
|
| 162 | + | <dd>{artwork.dimensions}</dd> |
|
| 163 | + | </> |
|
| 164 | + | )} |
|
| 165 | + | {artwork.credit_line && ( |
|
| 166 | + | <> |
|
| 167 | + | <dt class="opacity-50 uppercase tracking-widest text-xs">Credit</dt> |
|
| 168 | + | <dd>{artwork.credit_line}</dd> |
|
| 169 | + | </> |
|
| 170 | + | )} |
|
| 171 | + | </dl> |
|
| 172 | + | {cleanDescription && ( |
|
| 173 | + | <div class="prose prose-cactus prose-sm max-w-none" set:html={cleanDescription} /> |
|
| 174 | + | )} |
|
| 175 | + | {sourceUrl && ( |
|
| 176 | + | <a class="style-link self-start" href={sourceUrl} target="_blank" rel="noopener"> |
|
| 177 | + | View on artic.edu |
|
| 178 | + | </a> |
|
| 179 | + | )} |
|
| 180 | + | </article> |
|
| 181 | + | )} |
|
| 182 | + | </div> |
|
| 183 | + | </PageLayout> |