this repo has no description
at main 201 lines 5.3 kB view raw
1--- 2import type { InferEntrySchema } from "astro:content"; 3import { compareDesc, formatISO } from "date-fns"; 4import { setCssColors } from "../colors"; 5import { imageAttrs } from "../pages/works/media"; 6import Translated from "./Translated.astro"; 7import WIPIndicator from "./WIPIndicator.astro"; 8 9const EXCLUDED_WORKS = ["calen7drier"]; 10 11interface Props { 12 works: InferEntrySchema<"works">[]; 13} 14 15const isoDate = (date: Date) => formatISO(date, { representation: "date" }); 16const preferredDate = ({ 17 started, 18 finished, 19 additionalMetadata, 20}: { 21 started: Date | null; 22 finished: Date | null; 23 additionalMetadata?: { created?: Date | null | undefined }; 24}) => { 25 if (started && !finished && !additionalMetadata?.created) { 26 // Ongoing works 27 return new Date(); 28 } 29 return finished ?? started ?? additionalMetadata?.created; 30}; 31 32function groupBy<T, K extends string | number>( 33 items: T[], 34 key: (item: T) => K, 35): Record<K, T[]> { 36 return items.reduce( 37 (acc, current) => { 38 if (!acc[key(current)]) { 39 acc[key(current)] = []; 40 } 41 acc[key(current)].push(current); 42 return acc; 43 }, 44 {} as Record<K, T[]>, 45 ); 46} 47 48function findThumbnailBlock( 49 content: InferEntrySchema<"works">["content"], 50 sourcePath: string, 51) { 52 const sourcemap = Object.entries(content) 53 .flatMap(([_, { blocks }]) => blocks) 54 .find( 55 (block) => block.type === "media" && block.relativeSource === sourcePath, 56 ); 57 58 if (!sourcemap) 59 return Object.values(content) 60 .flatMap(({ blocks }) => blocks) 61 .find(({ type }) => type === "media"); 62 63 return sourcemap; 64} 65--- 66 67{ 68 Object.entries( 69 groupBy( 70 Astro.props.works.filter( 71 (w) => 72 !w.metadata.private && 73 !EXCLUDED_WORKS.includes(w.id) && 74 findThumbnailBlock(w.content, w.metadata.thumbnail ?? ""), 75 ), 76 ({ metadata }) => 77 preferredDate(metadata)?.getFullYear().toString() ?? "Unknown", 78 ), 79 ) 80 .sort(([a], [b]) => b - a) 81 .map(([year, works]) => ( 82 <> 83 <p class="year">{year}</p> 84 <div class="works-grid"> 85 {works 86 .sort(({ metadata: a }, { metadata: b }) => 87 compareDesc( 88 preferredDate(a) ?? new Date(), 89 preferredDate(b) ?? new Date(), 90 ), 91 ) 92 .map( 93 ({ 94 id, 95 content, 96 metadata: { 97 wip, 98 colors, 99 started, 100 finished, 101 thumbnail, 102 additionalMetadata, 103 }, 104 }) => { 105 const tagline = additionalMetadata?.tagline; 106 const date = preferredDate({ started, finished }); 107 const thumb = findThumbnailBlock(content, thumbnail ?? "")!; 108 const attrs = imageAttrs(thumb); 109 return ( 110 thumb && ( 111 <a href={`/${id}`}> 112 <article style={setCssColors(colors)}> 113 { 114 <img 115 {...attrs} 116 style={{ 117 width: "100%", 118 height: "auto", 119 objectFit: "cover", 120 }} 121 /> 122 } 123 <section class="text"> 124 <h2> 125 <Translated 126 fr={ 127 content.fr?.title || 128 content.default?.title || 129 id 130 } 131 en={ 132 content.en?.title || 133 content.default?.title || 134 id 135 } 136 /> 137 {wip && <WIPIndicator />} 138 </h2> 139 {tagline && ( 140 <span class="tagline" i18n> 141 {tagline} 142 </span> 143 )} 144 </section> 145 </article> 146 </a> 147 ) 148 ); 149 }, 150 )} 151 </div> 152 </> 153 )) 154} 155 156<style> 157 .works-grid { 158 --size: 300px; 159 display: grid; 160 gap: 1em; 161 grid-template-rows: masonry; 162 grid-template-columns: repeat(auto-fit, var(--size)); 163 } 164 165 .works-grid a:focus-visible img, 166 .works-grid a:hover img { 167 scale: 1.11; 168 } 169 170 article img { 171 transition: scale 0.125s; 172 } 173 174 .year { 175 font-size: 1.2em; 176 font-weight: bold; 177 margin: 1em 0; 178 font-family: 179 Victor Mono, 180 monospace; 181 } 182 183 article { 184 color: var(--primary, black); 185 background: var(--secondary, white); 186 padding: 1rem; 187 max-width: var(--size); 188 display: flex; 189 flex-direction: column; 190 gap: 1.5rem; 191 } 192 193 section.text { 194 display: flex; 195 flex-direction: column; 196 gap: 0.25em; 197 } 198 a { 199 text-decoration: none; 200 } 201</style>