this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

✨ Add back time spent on projects

+173 -22
+38 -1
src/content.config.ts
··· 23 23 export const collections = { 24 24 frenchMessages: gettextPoMessages("i18n/fr.po"), 25 25 englishMessages: gettextPoMessages("i18n/en.po"), 26 - wakatime: await wakatimeCollection(".wakatime-cache.json"), 26 + wakatimeLanguages: await wakatimeCollection( 27 + ".wakatime-cache.json", 28 + "languages", 29 + ), 30 + wakatimeProjects: await wakatimeCollection( 31 + ".wakatime-cache.json", 32 + "projects", 33 + ), 27 34 blogEntries: defineCollection({ 28 35 loader: glob({ pattern: "*.md", base: "./blog" }), 29 36 schema: z.object({ ··· 84 91 created: nullableDate, 85 92 title_style: z.string().optional(), 86 93 made_with: z.array(z.string()).optional(), 94 + wakatime: z 95 + .union([ 96 + z.string(), 97 + z.record(z.string(), z.string().nullable()), 98 + z.array( 99 + z.union([ 100 + z.string(), 101 + z.record(z.string(), z.string().nullable()), 102 + ]), 103 + ), 104 + ]) 105 + .optional() 106 + .transform((names) => 107 + names 108 + ? typeof names === "string" 109 + ? { [names]: null } 110 + : Array.isArray(names) 111 + ? Object.fromEntries( 112 + names.map((n) => { 113 + if (typeof n === "string") { 114 + return [n, null]; 115 + } 116 + 117 + const [[key, value]] = Object.entries(n); 118 + return [key, value]; 119 + }), 120 + ) 121 + : names 122 + : undefined, 123 + ), 87 124 tagline: z 88 125 .string() 89 126 .optional()
+1 -1
src/pages/resume/index.astro
··· 4 4 import Tech from "./_components/Tech.astro"; 5 5 import Reference from "./_components/Reference.astro"; 6 6 7 - const wakatime = await getCollection("wakatime").then((entries) => 7 + const wakatime = await getCollection("wakatimeLanguages").then((entries) => 8 8 Object.fromEntries( 9 9 entries.map((entry) => [ 10 10 entry.id,
+81 -5
src/pages/works/[work].astro
··· 48 48 const { 49 49 id, 50 50 metadata, 51 - metadata: { tags, wip, colors, started, finished, additionalMetadata: misc }, 51 + metadata: { 52 + tags, 53 + wip, 54 + colors, 55 + started, 56 + finished, 57 + aliases, 58 + additionalMetadata: misc, 59 + }, 52 60 } = entry.data; 61 + 62 + const timeSpent = await Promise.all( 63 + Object.entries( 64 + misc?.wakatime ?? 65 + Object.fromEntries([id, ...(aliases ?? [])].map((name) => [name, null])), 66 + ).map(async ([name, shareableUrl]) => 67 + getEntry("wakatimeProjects", name)?.then((entry) => 68 + entry 69 + ? { id: entry.id, seconds: entry.data.total_seconds, url: shareableUrl } 70 + : undefined, 71 + ), 72 + ) ?? [], 73 + ).then((entries) => 74 + entries 75 + .filter((x): x is NonNullable<typeof x> => x !== undefined) 76 + .filter(({ seconds }) => seconds >= 2 * 3600) 77 + .sort((a, b) => b.seconds - a.seconds), 78 + ); 79 + 80 + const hoursSpent = Math.round( 81 + timeSpent.map(({ seconds }) => seconds).reduce((a, b) => a + b, 0) / 3600, 82 + ); 53 83 54 84 const startYear = started?.getFullYear(); 55 85 const endYear = finished?.getFullYear(); ··· 152 182 </main> 153 183 154 184 { 185 + hoursSpent > 1 ? ( 186 + <section class="big"> 187 + <h2 i18n>time spent*</h2> 188 + <p> 189 + {timeSpent.length === 1 && timeSpent.at(0)?.url ? ( 190 + <a 191 + href={timeSpent.at(0)!.url!} 192 + target="_blank" 193 + rel="noopener noreferrer" 194 + > 195 + {hoursSpent} hours coding 196 + </a> 197 + ) : ( 198 + <span>{hoursSpent} hours coding</span> 199 + )} 200 + </p> 201 + {timeSpent.length > 1 ? ( 202 + <ul> 203 + {timeSpent.map(({ id, seconds, url }) => ( 204 + <li> 205 + {id}: 206 + {url ? ( 207 + <a href={url} target="_blank" rel="noopener noreferrer"> 208 + {Math.round((seconds ?? 0) / 3600)} hours 209 + </a> 210 + ) : ( 211 + <span>{Math.round((seconds ?? 0) / 3600)} hours </span> 212 + )} 213 + </li> 214 + ))} 215 + </ul> 216 + ) : null} 217 + <small> 218 + <br> 219 + <i18n>*tracked by</i18n> 220 + <a href="https://wakatime.com/" target="_blank"> 221 + WakaTime 222 + </a> 223 + </small> 224 + </section> 225 + ) : null 226 + } 227 + 228 + { 155 229 madeWith && madeWith.length > 0 && ( 156 - <section class="made-with"> 230 + <section class="big"> 157 231 <h2 i18n>made with</h2> 158 232 <ul class="technologies"> 159 233 {madeWith.map(({ id, data: { name } }) => ( ··· 259 333 } 260 334 } 261 335 262 - section.made-with h2 { 336 + section.big h2 { 263 337 font-size: 2.8em; 338 + line-height: 0.8; 264 339 } 265 - section.made-with { 340 + 341 + section.big { 266 342 font-size: 1.7rem; 267 343 } 268 - section.made-with ul { 344 + section.big ul { 269 345 margin-top: 1em; 270 346 display: flex; 271 347 flex-direction: column;
+9 -3
src/wakatime.ts
··· 4 4 import { getSecret } from "astro:env/server"; 5 5 import { defineCollection } from "astro:content"; 6 6 import { z } from "astro/zod"; 7 + import { differenceInHours } from "date-fns"; 7 8 8 - export async function wakatimeCollection(cachepath: string) { 9 + export async function wakatimeCollection( 10 + cachepath: string, 11 + subkey: "languages" | "projects", 12 + ) { 9 13 await refreshWakatimeCache(cachepath); 10 14 11 15 return defineCollection({ ··· 15 19 }), 16 20 loader: file(cachepath, { 17 21 parser: (text) => 18 - JSON.parse(text).data.languages.map((l) => ({ 22 + JSON.parse(text).data[subkey].map((l) => ({ 19 23 id: l.name.toLowerCase(), 20 24 ...l, 21 25 })), ··· 27 31 try { 28 32 const cachedResponse = JSON.parse((await readFile(cachepath)).toString()); 29 33 // cache is fresh enough 30 - if (Date.now() - new Date(cachedResponse.writtenAt).valueOf() < 12 * 3600) { 34 + if ( 35 + differenceInHours(new Date(), new Date(cachedResponse.writtenAt)) < 24 36 + ) { 31 37 return cachedResponse; 32 38 } 33 39 } catch {}
+5
technologies.yaml
··· 669 669 name: Drizzle ORM 670 670 description: A modern TypeScript ORM for SQL databases, designed for type safety and developer productivity. 671 671 learn more at: https://orm.drizzle.team/ 672 + 673 + - slug: capacitor 674 + name: Capacitor 675 + description: A cross-platform native runtime for building web apps that run natively on iOS, Android, and the web 676 + learn more at: https://capacitorjs.com/
+39 -12
works.json
··· 10607 10607 }, 10608 10608 "churros": { 10609 10609 "Partial": false, 10610 - "builtAt": "2025-12-03T12:41:08.1047888+01:00", 10610 + "builtAt": "2025-12-03T13:22:30.0276329+01:00", 10611 10611 "content": { 10612 10612 "default": { 10613 10613 "abbreviations": {}, ··· 10908 10908 "title": "Churros" 10909 10909 } 10910 10910 }, 10911 - "descriptionHash": "be/u8JCBZV/HA/vjyDJeaA==", 10911 + "descriptionHash": "VNWXrs0Ts/YsWpMBRstcDw==", 10912 10912 "id": "churros", 10913 10913 "metadata": { 10914 10914 "additionalMetadata": { ··· 10940 10940 "kubernetes", 10941 10941 "fluxcd" 10942 10942 ], 10943 - "tagline": "Complete student life platform service, includes events, tickets, clubs, student directory and study material sharing" 10943 + "tagline": "Complete student life platform service, includes events, tickets, clubs, student directory and study material sharing", 10944 + "wakatime": [ 10945 + { 10946 + "churros": "https://wakatime.com/@gwennlbh/projects/yvzdoqwsci" 10947 + }, 10948 + "centraverse", 10949 + { 10950 + "notella": "https://wakatime.com/@gwennlbh/projects/waxaqoyplq" 10951 + }, 10952 + "churros-notella", 10953 + "churros-wiki", 10954 + "churros-mobile" 10955 + ] 10944 10956 }, 10945 10957 "aliases": null, 10946 10958 "colors": { ··· 10965 10977 }, 10966 10978 "cigale": { 10967 10979 "Partial": false, 10968 - "builtAt": "2025-12-03T12:41:08.1377699+01:00", 10980 + "builtAt": "2025-12-03T13:24:45.4817685+01:00", 10969 10981 "content": { 10970 10982 "en": { 10971 10983 "abbreviations": {}, ··· 12352 12364 "title": "CIGALE" 12353 12365 } 12354 12366 }, 12355 - "descriptionHash": "HobE8N//zH30xzBPoR1UjA==", 12367 + "descriptionHash": "EnAVJ8avQTh2wuGkmV+Yyg==", 12356 12368 "id": "cigale", 12357 12369 "metadata": { 12358 12370 "additionalMetadata": { ··· 12381 12393 "electron", 12382 12394 "drizzle" 12383 12395 ], 12384 - "tagline": "Offline-ready web app that helps with insect photo cropping and annotation for entomological research" 12396 + "tagline": "Offline-ready web app that helps with insect photo cropping and annotation for entomological research", 12397 + "wakatime": { 12398 + "beamup": "https://wakatime.com/@gwennlbh/projects/yzyjfynqob", 12399 + "cigale": "https://wakatime.com/@gwennlbh/projects/fhymtteyys" 12400 + } 12385 12401 }, 12386 12402 "aliases": null, 12387 12403 "colors": { ··· 19304 19320 }, 19305 19321 "hyprls": { 19306 19322 "Partial": false, 19307 - "builtAt": "2025-12-03T12:13:05.7072046+01:00", 19323 + "builtAt": "2025-12-03T13:22:51.7024557+01:00", 19308 19324 "content": { 19309 19325 "en": { 19310 19326 "abbreviations": {}, ··· 19983 19999 "title": "HyprLS" 19984 20000 } 19985 20001 }, 19986 - "descriptionHash": "CGQifsaaL/2V+w6f5xXzrw==", 20002 + "descriptionHash": "8wMEtisCUF61i6M2YWqgAg==", 19987 20003 "id": "hyprls", 19988 20004 "metadata": { 19989 20005 "additionalMetadata": { ··· 19992 20008 ["m2", "l1"], 19993 20009 ["m3", "m4", "m5"] 19994 20010 ], 19995 - "made_with": ["go", "hyprland"] 20011 + "made_with": ["go", "hyprland"], 20012 + "wakatime": { 20013 + "hyprls": "https://wakatime.com/@gwennlbh/projects/nxyydzsmzb" 20014 + } 19996 20015 }, 19997 20016 "aliases": ["hyprlang-lsp"], 19998 20017 "colors": { ··· 30149 30168 }, 30150 30169 "portfolio": { 30151 30170 "Partial": false, 30152 - "builtAt": "2025-10-06T22:45:12.6773573+02:00", 30171 + "builtAt": "2025-12-03T13:32:27.4461714+01:00", 30153 30172 "content": { 30154 30173 "en": { 30155 30174 "abbreviations": {}, ··· 30752 30771 "title": "gwen.works" 30753 30772 } 30754 30773 }, 30755 - "descriptionHash": "fZhQv7DhqLWyiPDyLBWF4Q==", 30774 + "descriptionHash": "Igbi8kswRbats2wAkY0R5g==", 30756 30775 "id": "portfolio", 30757 30776 "metadata": { 30758 30777 "additionalMetadata": { ··· 30765 30784 ["p4", "p4", "m1"] 30766 30785 ], 30767 30786 "made_with": ["figma", "ortfo", "astro"], 30768 - "tagline": "This website!" 30787 + "tagline": "This website!", 30788 + "wakatime": [ 30789 + { 30790 + "portfolio": "https://wakatime.com/@gwennlbh/projects/earvnaxtcl" 30791 + }, 30792 + "mx3creations", 30793 + "portfolio-next", 30794 + "www" 30795 + ] 30769 30796 }, 30770 30797 "aliases": ["gwen.works"], 30771 30798 "colors": {