this repo has no description
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>