chore: added /art-calendar
e8987cc9
3 file(s) · +134 −183
| 58 | 58 | path: "/birds", |
|
| 59 | 59 | }, |
|
| 60 | 60 | { |
|
| 61 | + | title: "Art Calendar", |
|
| 62 | + | path: "/art-calendar", |
|
| 63 | + | }, |
|
| 64 | + | { |
|
| 61 | 65 | title: "Uses", |
|
| 62 | 66 | path: "/uses", |
|
| 63 | 67 | }, |
|
| 97 | 101 | name: "Wine Log", |
|
| 98 | 102 | href: "https://cellar.stevedylan.dev/feed.xml", |
|
| 99 | 103 | description: "Wines I've tasted, logged in the cellar.", |
|
| 104 | + | }, |
|
| 105 | + | { |
|
| 106 | + | name: "Art Calendar", |
|
| 107 | + | href: "https://easel.stevedylan.dev/feed.xml", |
|
| 108 | + | description: "A painting a day from the Art Institute of Chicago.", |
|
| 100 | 109 | }, |
|
| 101 | 110 | ]; |
|
| 102 | 111 | ||
| 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> |
| 1 | + | --- |
|
| 2 | + | export const prerender = false; |
|
| 3 | + | ||
| 4 | + | import PageLayout from "@/layouts/Base.astro"; |
|
| 5 | + | import { getFormattedDate } from "@/utils"; |
|
| 6 | + | ||
| 7 | + | type Artwork = { |
|
| 8 | + | date: string; |
|
| 9 | + | artwork_id: number; |
|
| 10 | + | title: string; |
|
| 11 | + | artist_display: string | null; |
|
| 12 | + | date_display: string | null; |
|
| 13 | + | medium_display: string | null; |
|
| 14 | + | dimensions: string | null; |
|
| 15 | + | place_of_origin: string | null; |
|
| 16 | + | credit_line: string | null; |
|
| 17 | + | short_description: string | null; |
|
| 18 | + | image_id: string | null; |
|
| 19 | + | image_url: string | null; |
|
| 20 | + | source_url: string | null; |
|
| 21 | + | }; |
|
| 22 | + | ||
| 23 | + | const meta = { |
|
| 24 | + | title: "Art Calendar", |
|
| 25 | + | description: |
|
| 26 | + | "A different painting every day from the Art Institute of Chicago.", |
|
| 27 | + | }; |
|
| 28 | + | ||
| 29 | + | const FEED_URL = "https://easel.stevedylan.dev/feed.xml"; |
|
| 30 | + | const today = new Date(); |
|
| 31 | + | ||
| 32 | + | let artwork: Artwork | null = null; |
|
| 33 | + | let error: string | null = null; |
|
| 34 | + | ||
| 35 | + | try { |
|
| 36 | + | const res = await fetch("https://easel.stevedylan.dev/api/today", { |
|
| 37 | + | headers: { Accept: "application/json" }, |
|
| 38 | + | }); |
|
| 39 | + | if (!res.ok) throw new Error(`easel returned ${res.status}`); |
|
| 40 | + | artwork = (await res.json()) as Artwork; |
|
| 41 | + | } catch (e) { |
|
| 42 | + | error = e instanceof Error ? e.message : "Failed to reach easel API"; |
|
| 43 | + | } |
|
| 44 | + | ||
| 45 | + | Astro.response.headers.set( |
|
| 46 | + | "Cache-Control", |
|
| 47 | + | "public, max-age=3600, s-maxage=86400", |
|
| 48 | + | ); |
|
| 49 | + | --- |
|
| 50 | + | ||
| 51 | + | <PageLayout meta={meta}> |
|
| 52 | + | <div class="flex min-h-screen flex-col items-start justify-start gap-6"> |
|
| 53 | + | <div class="flex flex-col gap-1"> |
|
| 54 | + | <h1 class="title">Art Calendar</h1> |
|
| 55 | + | <p class="text-sm opacity-70"> |
|
| 56 | + | One painting every day from the |
|
| 57 | + | <a class="style-link" href="https://www.artic.edu/" target="_blank" rel="noopener">Art Institute of Chicago</a>. |
|
| 58 | + | {getFormattedDate(today)}. |
|
| 59 | + | </p> |
|
| 60 | + | <p class="text-sm opacity-70 flex items-center gap-1"> |
|
| 61 | + | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256" class="shrink-0 text-accent-2" aria-hidden="true"> |
|
| 62 | + | <path fill="currentColor" d="M106.91 149.09A71.53 71.53 0 0 1 128 200a8 8 0 0 1-16 0a56 56 0 0 0-56-56a8 8 0 0 1 0-16a71.53 71.53 0 0 1 50.91 21.09M56 80a8 8 0 0 0 0 16a104 104 0 0 1 104 104a8 8 0 0 0 16 0A120 120 0 0 0 56 80m118.79 1.21A166.9 166.9 0 0 0 56 32a8 8 0 0 0 0 16a151 151 0 0 1 107.48 44.52A151 151 0 0 1 208 200a8 8 0 0 0 16 0a166.9 166.9 0 0 0-49.21-118.79M60 184a12 12 0 1 0 12 12a12 12 0 0 0-12-12"/> |
|
| 63 | + | </svg> |
|
| 64 | + | <a class="style-link" href={FEED_URL} target="_blank" rel="noopener">RSS feed</a> |
|
| 65 | + | </p> |
|
| 66 | + | </div> |
|
| 67 | + | {error || !artwork ? ( |
|
| 68 | + | <p class="text-red-400 text-sm">Couldn't load today's painting{error ? `: ${error}` : ""}.</p> |
|
| 69 | + | ) : ( |
|
| 70 | + | <article class="flex flex-col w-full gap-6"> |
|
| 71 | + | {artwork.image_url && ( |
|
| 72 | + | <img |
|
| 73 | + | src={artwork.image_url} |
|
| 74 | + | alt={artwork.title} |
|
| 75 | + | loading="eager" |
|
| 76 | + | class="w-full max-w-2xl h-auto" |
|
| 77 | + | /> |
|
| 78 | + | )} |
|
| 79 | + | <div class="flex flex-col gap-1"> |
|
| 80 | + | <h2 class="text-xl italic text-accent-2">{artwork.title}</h2> |
|
| 81 | + | {artwork.artist_display && ( |
|
| 82 | + | <p class="text-sm whitespace-pre-line">{artwork.artist_display}</p> |
|
| 83 | + | )} |
|
| 84 | + | {artwork.date_display && ( |
|
| 85 | + | <p class="text-sm opacity-70">{artwork.date_display}</p> |
|
| 86 | + | )} |
|
| 87 | + | </div> |
|
| 88 | + | <dl class="grid grid-cols-1 sm:grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm"> |
|
| 89 | + | {artwork.place_of_origin && ( |
|
| 90 | + | <> |
|
| 91 | + | <dt class="opacity-50 uppercase tracking-widest text-xs">Origin</dt> |
|
| 92 | + | <dd>{artwork.place_of_origin}</dd> |
|
| 93 | + | </> |
|
| 94 | + | )} |
|
| 95 | + | {artwork.medium_display && ( |
|
| 96 | + | <> |
|
| 97 | + | <dt class="opacity-50 uppercase tracking-widest text-xs">Medium</dt> |
|
| 98 | + | <dd>{artwork.medium_display}</dd> |
|
| 99 | + | </> |
|
| 100 | + | )} |
|
| 101 | + | {artwork.dimensions && ( |
|
| 102 | + | <> |
|
| 103 | + | <dt class="opacity-50 uppercase tracking-widest text-xs">Dimensions</dt> |
|
| 104 | + | <dd>{artwork.dimensions}</dd> |
|
| 105 | + | </> |
|
| 106 | + | )} |
|
| 107 | + | {artwork.credit_line && ( |
|
| 108 | + | <> |
|
| 109 | + | <dt class="opacity-50 uppercase tracking-widest text-xs">Credit</dt> |
|
| 110 | + | <dd>{artwork.credit_line}</dd> |
|
| 111 | + | </> |
|
| 112 | + | )} |
|
| 113 | + | </dl> |
|
| 114 | + | {artwork.short_description && ( |
|
| 115 | + | <p class="text-sm">{artwork.short_description}</p> |
|
| 116 | + | )} |
|
| 117 | + | {artwork.source_url && ( |
|
| 118 | + | <a class="style-link self-start" href={artwork.source_url} target="_blank" rel="noopener"> |
|
| 119 | + | View on artic.edu |
|
| 120 | + | </a> |
|
| 121 | + | )} |
|
| 122 | + | </article> |
|
| 123 | + | )} |
|
| 124 | + | </div> |
|
| 125 | + | </PageLayout> |