grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

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

dark mode, css content hash

+126 -54
+2 -1
deno.json
··· 2 2 "imports": { 3 3 "$lexicon/": "./__generated__/", 4 4 "@atproto/syntax": "npm:@atproto/syntax@^0.4.0", 5 - "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.7", 5 + "@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.8", 6 6 "@gfx/canvas": "jsr:@gfx/canvas@^0.5.8", 7 + "@std/path": "jsr:@std/path@^1.0.9", 7 8 "@tailwindcss/cli": "npm:@tailwindcss/cli@^4.1.4", 8 9 "date-fns": "npm:date-fns@^4.1.0", 9 10 "popmotion": "npm:popmotion@^11.0.5",
+5 -4
deno.lock
··· 2 2 "version": "4", 3 3 "specifiers": { 4 4 "jsr:@bigmoves/atproto-oauth-client@0.1": "0.1.0", 5 - "jsr:@bigmoves/bff@0.3.0-beta.7": "0.3.0-beta.7", 5 + "jsr:@bigmoves/bff@0.3.0-beta.8": "0.3.0-beta.8", 6 6 "jsr:@denosaurs/plug@1": "1.0.5", 7 7 "jsr:@denosaurs/plug@1.0.5": "1.0.5", 8 8 "jsr:@gfx/canvas@~0.5.8": "0.5.8", ··· 70 70 "npm:jose" 71 71 ] 72 72 }, 73 - "@bigmoves/bff@0.3.0-beta.7": { 74 - "integrity": "cf9b6469e239abaad539aa5e06c786018731c1c0ab8b59debdbb8e83eebb6e83", 73 + "@bigmoves/bff@0.3.0-beta.8": { 74 + "integrity": "9f4193d02b9ff1a16f600fbb1ad5323c75285848457429172914a2b797787055", 75 75 "dependencies": [ 76 76 "jsr:@bigmoves/atproto-oauth-client", 77 77 "jsr:@std/assert@^1.0.12", ··· 1609 1609 }, 1610 1610 "workspace": { 1611 1611 "dependencies": [ 1612 - "jsr:@bigmoves/bff@0.3.0-beta.7", 1612 + "jsr:@bigmoves/bff@0.3.0-beta.8", 1613 1613 "jsr:@gfx/canvas@~0.5.8", 1614 + "jsr:@std/path@^1.0.9", 1614 1615 "npm:@atproto/syntax@0.4", 1615 1616 "npm:@tailwindcss/cli@^4.1.4", 1616 1617 "npm:date-fns@^4.1.0",
+64 -33
main.tsx
··· 44 44 Textarea, 45 45 } from "@bigmoves/bff/components"; 46 46 import { createCanvas, Image } from "@gfx/canvas"; 47 + import { join } from "@std/path"; 47 48 import { formatDistanceStrict } from "date-fns"; 48 49 import { wrap } from "popmotion"; 49 50 import { ComponentChildren, JSX, VNode } from "preact"; 51 + 52 + let cssContentHash: string = ""; 50 53 51 54 bff({ 52 55 appName: "Grain Social", ··· 60 63 jetstreamUrl: JETSTREAM.WEST_1, 61 64 lexicons, 62 65 rootElement: Root, 66 + onListen: async () => { 67 + const cssFileContent = await Deno.readFile( 68 + join(Deno.cwd(), "static", "styles.css"), 69 + ); 70 + const hashBuffer = await crypto.subtle.digest( 71 + "SHA-256", 72 + cssFileContent, 73 + ); 74 + cssContentHash = Array.from(new Uint8Array(hashBuffer)) 75 + .map((b) => b.toString(16).padStart(2, "0")) 76 + .join(""); 77 + }, 63 78 onError: (err) => { 64 79 if (err instanceof UnauthorizedError) { 65 80 const ctx = err.ctx; ··· 91 106 <Login hx-target="#login" error={error} errorClass="text-white" /> 92 107 <div class="absolute bottom-2 right-2 text-white text-sm"> 93 108 Photo by{" "} 94 - <a href={profileLink("chadtmiller.com")}>@chadtmiller.com</a> 109 + <a 110 + href={profileLink("chadtmiller.com")} 111 + class="hover:underline font-semibold" 112 + > 113 + @chadtmiller.com 114 + </a> 95 115 </div> 96 116 </div> 97 117 ), ··· 145 165 <GalleryPage favs={favs} gallery={gallery} currentUserDid={did} />, 146 166 ); 147 167 }), 148 - 149 168 route("/upload", (_req, _params, ctx) => { 150 169 requireAuth(ctx); 151 170 const photos = getActorPhotos(ctx.currentUser.did, ctx); ··· 278 297 const formData = await req.formData(); 279 298 const title = formData.get("title") as string; 280 299 const description = formData.get("description") as string; 281 - const cids = formData.getAll("cids") as string[]; 282 300 const url = new URL(req.url); 283 301 const searchParams = new URLSearchParams(url.search); 284 302 const uri = searchParams.get("uri"); ··· 922 940 <script src="https://unpkg.com/htmx.org@1.9.10" /> 923 941 <script src="https://unpkg.com/hyperscript.org@0.9.14" /> 924 942 <style dangerouslySetInnerHTML={{ __html: CSS }} /> 925 - <link rel="stylesheet" href="/static/styles.css" /> 943 + <link rel="stylesheet" href={`/static/styles.css?${cssContentHash}`} /> 926 944 <link rel="preconnect" href="https://fonts.googleapis.com" /> 927 945 <link 928 946 rel="preconnect" ··· 932 950 <link 933 951 href="https://fonts.googleapis.com/css2?family=Jersey+20&display=swap" 934 952 rel="stylesheet" 935 - > 936 - </link> 953 + /> 937 954 <link 938 955 rel="stylesheet" 939 956 href="https://unpkg.com/@fortawesome/fontawesome-free@6.7.2/css/all.min.css" ··· 943 960 ? <script src="/static/image_dialog.js" /> 944 961 : null} 945 962 </head> 946 - <body class="h-full w-full"> 947 - <Layout id="layout"> 963 + <body class="h-full w-full dark:bg-zinc-950 dark:text-white"> 964 + <Layout id="layout" class="dark:border-zinc-800"> 948 965 <Layout.Nav 949 966 title={ 950 - <h1 class="font-['Jersey_20'] text-4xl text-gray-900"> 967 + <h1 class="font-['Jersey_20'] text-4xl text-zinc-900 dark:text-white"> 951 968 grain 952 969 <sub class="bottom-[0.75rem] text-[1rem]">beta</sub> 953 970 </h1> 954 971 } 955 972 profile={profile} 973 + class="dark:border-zinc-800" 956 974 /> 957 975 <Layout.Content>{props.children}</Layout.Content> 958 976 </Layout> ··· 1031 1049 function TimelineItem({ item }: Readonly<{ item: TimelineItem }>) { 1032 1050 return ( 1033 1051 <li class="space-y-2"> 1034 - <div class="bg-gray-100 w-fit p-2"> 1052 + <div class="bg-zinc-100 dark:bg-zinc-900 w-fit p-2"> 1035 1053 <a 1036 1054 href={profileLink(item.actor.handle)} 1037 1055 class="font-semibold hover:underline" ··· 1044 1062 item.gallery.creator.handle, 1045 1063 new AtUri(item.gallery.uri).rkey, 1046 1064 )} 1047 - class="font-semibold" 1065 + class="font-semibold hover:underline" 1048 1066 > 1049 1067 {(item.gallery.record as Gallery).title} 1050 1068 </a> ··· 1082 1100 class="w-full h-full object-cover" 1083 1101 /> 1084 1102 ) 1085 - : <div className="w-full h-full bg-gray-200" />} 1103 + : ( 1104 + <div className="w-full h-full bg-zinc-200 dark:bg-zinc-900" /> 1105 + )} 1086 1106 </div> 1087 1107 <div class="h-1/2"> 1088 1108 {item.gallery.items?.filter(isPhotoView)?.[2] ··· 1094 1114 class="w-full h-full object-cover" 1095 1115 /> 1096 1116 ) 1097 - : <div className="w-full h-full bg-gray-200" />} 1117 + : ( 1118 + <div className="w-full h-full bg-zinc-200 dark:bg-zinc-900" /> 1119 + )} 1098 1120 </div> 1099 1121 </div> 1100 1122 </div> ··· 1124 1146 <div class="flex flex-col"> 1125 1147 <AvatarButton profile={profile} /> 1126 1148 <p class="text-2xl font-bold">{profile.displayName}</p> 1127 - <p class="text-gray-600">@{profile.handle}</p> 1149 + <p class="text-zinc-600 dark:text-zinc-500">@{profile.handle}</p> 1128 1150 <p class="my-2">{profile.description}</p> 1129 1151 </div> 1130 1152 {loggedInUserDid === profile.did ··· 1168 1190 hx-swap="outerHTML" 1169 1191 class={cn( 1170 1192 "flex-1 py-2 px-4 cursor-pointer font-semibold", 1171 - !selectedTab && "bg-gray-100 font-semibold", 1193 + !selectedTab && "bg-zinc-100 dark:bg-zinc-800 font-semibold", 1172 1194 )} 1173 1195 role="tab" 1174 1196 aria-selected="true" ··· 1183 1205 hx-swap="outerHTML" 1184 1206 class={cn( 1185 1207 "flex-1 py-2 px-4 cursor-pointer font-semibold", 1186 - selectedTab === "galleries" && "bg-gray-100", 1208 + selectedTab === "galleries" && "bg-zinc-100 dark:bg-zinc-800", 1187 1209 )} 1188 1210 role="tab" 1189 1211 aria-selected="false" ··· 1217 1239 )} 1218 1240 class="cursor-pointer relative aspect-square" 1219 1241 > 1220 - <img 1221 - src={gallery.items?.filter(isPhotoView)?.[0]?.thumb} 1222 - alt={gallery.items?.filter(isPhotoView)?.[0]?.alt} 1223 - class="w-full h-full object-cover" 1224 - /> 1242 + {gallery.items?.length 1243 + ? ( 1244 + <img 1245 + src={gallery.items?.filter(isPhotoView)?.[0]?.thumb} 1246 + alt={gallery.items?.filter(isPhotoView)?.[0]?.alt} 1247 + class="w-full h-full object-cover" 1248 + /> 1249 + ) 1250 + : ( 1251 + <div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" /> 1252 + )} 1225 1253 <div class="absolute bottom-0 left-0 bg-black/80 text-white p-2"> 1226 1254 {(gallery.record as Gallery).title} 1227 1255 </div> ··· 1286 1314 }>) { 1287 1315 return ( 1288 1316 <Dialog> 1289 - <Dialog.Content> 1317 + <Dialog.Content class="dark:bg-zinc-950"> 1290 1318 <Dialog.Title>Edit my profile</Dialog.Title> 1291 1319 <div> 1292 1320 <AvatarForm src={profile.avatar} alt={profile.handle} /> ··· 1306 1334 required 1307 1335 id="displayName" 1308 1336 name="displayName" 1309 - class="input" 1337 + class="dark:bg-zinc-800 dark:text-white" 1310 1338 value={profile.displayName} 1311 1339 /> 1312 1340 </div> ··· 1316 1344 id="description" 1317 1345 name="description" 1318 1346 rows={4} 1319 - class="input" 1347 + class="dark:bg-zinc-800 dark:text-white" 1320 1348 > 1321 1349 {profile.description} 1322 1350 </Textarea> ··· 1404 1432 > 1405 1433 <span class="font-semibold">{gallery.creator.displayName}</span> 1406 1434 {" "} 1407 - <span class="text-gray-600">@{gallery.creator.handle}</span> 1435 + <span class="text-zinc-600 dark:text-zinc-500"> 1436 + @{gallery.creator.handle} 1437 + </span> 1408 1438 </a> 1409 1439 </div> 1410 1440 {(gallery.record as Gallery).description} ··· 1530 1560 }: Readonly<{ gallery?: GalleryView | null }>) { 1531 1561 return ( 1532 1562 <Dialog id="gallery-dialog" class="z-30"> 1533 - <Dialog.Content> 1563 + <Dialog.Content class="dark:bg-zinc-950"> 1534 1564 <Dialog.Title> 1535 1565 {gallery ? "Edit gallery" : "Create a new gallery"} 1536 1566 </Dialog.Title> ··· 1551 1581 type="text" 1552 1582 id="title" 1553 1583 name="title" 1554 - class="input" 1584 + class="dark:bg-zinc-800 dark:text-white" 1555 1585 required 1556 1586 value={(gallery?.record as Gallery)?.title} 1557 1587 autofocus ··· 1563 1593 id="description" 1564 1594 name="description" 1565 1595 rows={4} 1566 - class="input" 1596 + class="dark:bg-zinc-800 dark:text-white" 1567 1597 > 1568 1598 {(gallery?.record as Gallery)?.description} 1569 1599 </Textarea> ··· 1626 1656 uri?: string; 1627 1657 }>) { 1628 1658 return ( 1629 - <div class="relative aspect-square"> 1659 + <div class="relative aspect-square dark:bg-zinc-900"> 1630 1660 {uri 1631 1661 ? ( 1632 1662 <button ··· 1679 1709 prevImage?: PhotoView; 1680 1710 }>) { 1681 1711 return ( 1682 - <Dialog id="photo-dialog" class="bg-black z-30"> 1712 + <Dialog id="photo-dialog" class="bg-zinc-950 z-30"> 1683 1713 {nextImage 1684 1714 ? ( 1685 1715 <div ··· 1732 1762 }>) { 1733 1763 return ( 1734 1764 <Dialog id="photo-alt-dialog" class="z-30"> 1735 - <Dialog.Content> 1765 + <Dialog.Content class="dark:bg-zinc-950"> 1736 1766 <Dialog.Title>Add alt text</Dialog.Title> 1737 - <div class="aspect-square relative bg-gray-100"> 1767 + <div class="aspect-square relative bg-zinc-100 dark:bg-zinc-900"> 1738 1768 <img 1739 1769 src={photo.fullsize} 1740 1770 alt={photo.alt} ··· 1755 1785 rows={4} 1756 1786 defaultValue={photo.alt} 1757 1787 placeholder="Alt text" 1788 + class="dark:bg-zinc-800 dark:text-white" 1758 1789 /> 1759 1790 </div> 1760 1791 <div class="w-full flex flex-col gap-2 mt-2"> ··· 1782 1813 }>) { 1783 1814 return ( 1784 1815 <Dialog id="photo-select-dialog" class="z-30"> 1785 - <Dialog.Content class="w-full max-w-5xl"> 1816 + <Dialog.Content class="w-full max-w-5xl dark:bg-zinc-950"> 1786 1817 <Dialog.Title>Add photos</Dialog.Title> 1787 1818 <div class="grid grid-cols-2 sm:grid-cols-3 gap-4 my-4"> 1788 1819 {photos.map((photo) => (
+55 -16
static/styles.css
··· 10 10 --color-sky-500: oklch(68.5% 0.169 237.323); 11 11 --color-slate-800: oklch(27.9% 0.041 260.031); 12 12 --color-slate-900: oklch(20.8% 0.042 265.755); 13 - --color-gray-100: oklch(96.7% 0.003 264.542); 14 - --color-gray-200: oklch(92.8% 0.006 264.531); 15 - --color-gray-600: oklch(44.6% 0.03 256.802); 16 - --color-gray-900: oklch(21% 0.034 264.665); 13 + --color-zinc-100: oklch(96.7% 0.001 286.375); 14 + --color-zinc-200: oklch(92% 0.004 286.32); 15 + --color-zinc-500: oklch(55.2% 0.016 285.938); 16 + --color-zinc-600: oklch(44.2% 0.017 285.786); 17 + --color-zinc-800: oklch(27.4% 0.006 286.033); 18 + --color-zinc-900: oklch(21% 0.006 285.885); 19 + --color-zinc-950: oklch(14.1% 0.005 285.823); 17 20 --color-black: #000; 18 21 --color-white: #fff; 19 22 --spacing: 0.25rem; ··· 198 201 } 199 202 .relative { 200 203 position: relative; 204 + } 205 + .static { 206 + position: static; 201 207 } 202 208 .inset-0 { 203 209 inset: calc(var(--spacing) * 0); ··· 412 418 background-color: color-mix(in oklab, var(--color-black) 80%, transparent); 413 419 } 414 420 } 415 - .bg-gray-100 { 416 - background-color: var(--color-gray-100); 417 - } 418 - .bg-gray-200 { 419 - background-color: var(--color-gray-200); 420 - } 421 421 .bg-slate-800 { 422 422 background-color: var(--color-slate-800); 423 + } 424 + .bg-zinc-100 { 425 + background-color: var(--color-zinc-100); 426 + } 427 + .bg-zinc-200 { 428 + background-color: var(--color-zinc-200); 429 + } 430 + .bg-zinc-950 { 431 + background-color: var(--color-zinc-950); 423 432 } 424 433 .object-contain { 425 434 object-fit: contain; ··· 491 500 --tw-font-weight: var(--font-weight-semibold); 492 501 font-weight: var(--font-weight-semibold); 493 502 } 494 - .text-gray-600 { 495 - color: var(--color-gray-600); 496 - } 497 - .text-gray-900 { 498 - color: var(--color-gray-900); 499 - } 500 503 .text-sky-500 { 501 504 color: var(--color-sky-500); 502 505 } 503 506 .text-white { 504 507 color: var(--color-white); 505 508 } 509 + .text-zinc-600 { 510 + color: var(--color-zinc-600); 511 + } 512 + .text-zinc-900 { 513 + color: var(--color-zinc-900); 514 + } 506 515 .lowercase { 507 516 text-transform: lowercase; 508 517 } ··· 615 624 .sm\:px-0 { 616 625 @media (width >= 40rem) { 617 626 padding-inline: calc(var(--spacing) * 0); 627 + } 628 + } 629 + .dark\:border-zinc-800 { 630 + @media (prefers-color-scheme: dark) { 631 + border-color: var(--color-zinc-800); 632 + } 633 + } 634 + .dark\:bg-zinc-800 { 635 + @media (prefers-color-scheme: dark) { 636 + background-color: var(--color-zinc-800); 637 + } 638 + } 639 + .dark\:bg-zinc-900 { 640 + @media (prefers-color-scheme: dark) { 641 + background-color: var(--color-zinc-900); 642 + } 643 + } 644 + .dark\:bg-zinc-950 { 645 + @media (prefers-color-scheme: dark) { 646 + background-color: var(--color-zinc-950); 647 + } 648 + } 649 + .dark\:text-white { 650 + @media (prefers-color-scheme: dark) { 651 + color: var(--color-white); 652 + } 653 + } 654 + .dark\:text-zinc-500 { 655 + @media (prefers-color-scheme: dark) { 656 + color: var(--color-zinc-500); 618 657 } 619 658 } 620 659 }