this repo has no descr,ription vt3e.cat

feat: gallery page

vt3e.cat 1d56c286 7305632e

verified
+346 -18
+33 -4
bun.lock
··· 4 4 "": { 5 5 "name": "@sillowww/web", 6 6 "dependencies": { 7 + "@atcute/atproto": "^3.1.10", 8 + "@atcute/client": "^4.2.0", 9 + "@atcute/lexicons": "^1.2.6", 7 10 "@iconify-prerendered/vue-material-symbols": "^0.28.1755063979", 11 + "blurhash": "^2.0.5", 8 12 }, 9 13 "devDependencies": { 10 14 "@biomejs/biome": "2.1.4", ··· 50 54 "vue-router": "^4.6.4", 51 55 }, 52 56 "devDependencies": { 57 + "@sillowww/gallery": "workspace:*", 53 58 "@tsconfig/node24": "^24.0.3", 54 59 "@types/node": "^24.10.4", 55 60 "@vitejs/plugin-vue": "^6.0.3", ··· 74 79 }, 75 80 }, 76 81 "packages": { 77 - "@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="], 82 + "@atcute/atproto": ["@atcute/atproto@3.1.10", "", { "dependencies": { "@atcute/lexicons": "^1.2.6" } }, "sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ=="], 78 83 79 - "@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="], 84 + "@atcute/client": ["@atcute/client@4.2.0", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.6" } }, "sha512-vYixpXevM+dkzN4HGTfmxCJBOXNSRAMki1bfi1atFdmZGo9n1zStyClHqn0SRs8I5nOQoVG1XBkYAGSFJWqy2Q=="], 80 85 81 86 "@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="], 82 87 ··· 86 91 87 92 "@atcute/lexicon-doc": ["@atcute/lexicon-doc@1.1.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-HlQBmB4NCZPzREyVzr7lzjRxSiRHook2xfa7DgA3dk3oYZ+KnnPEtS6M1sAmAAddtUdrOrJ+0xJPQHkfElZmpQ=="], 88 93 89 - "@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="], 94 + "@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 95 + 96 + "@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 90 97 91 98 "@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="], 99 + 100 + "@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 92 101 93 102 "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], 94 103 ··· 452 461 453 462 "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], 454 463 455 - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 464 + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], 456 465 457 466 "@tsconfig/node24": ["@tsconfig/node24@24.0.3", "", {}, "sha512-vcERKtKQKHgzt/vfS3Gjasd8SUI2a0WZXpgJURdJsMySpS5+ctgbPfuLj2z/W+w4lAfTWxoN4upKfu2WzIRYnw=="], 458 467 ··· 1021 1030 "typescript-eslint": ["typescript-eslint@8.50.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/parser": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/utils": "8.50.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ=="], 1022 1031 1023 1032 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 1033 + 1034 + "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], 1024 1035 1025 1036 "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], 1026 1037 ··· 1094 1105 1095 1106 "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], 1096 1107 1108 + "@atcute/client/@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="], 1109 + 1110 + "@atcute/identity/@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="], 1111 + 1112 + "@atcute/identity-resolver/@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="], 1113 + 1097 1114 "@atcute/lex-cli/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], 1098 1115 1099 1116 "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], ··· 1114 1131 1115 1132 "@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], 1116 1133 1134 + "@sillowww/uploader/@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="], 1135 + 1136 + "@sillowww/uploader/@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="], 1137 + 1138 + "@sillowww/uploader/@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="], 1139 + 1117 1140 "@types/react/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 1118 1141 1119 1142 "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], ··· 1148 1171 1149 1172 "vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], 1150 1173 1174 + "@atcute/identity-resolver/@atcute/lexicons/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 1175 + 1176 + "@atcute/identity/@atcute/lexicons/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 1177 + 1151 1178 "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], 1152 1179 1153 1180 "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], ··· 1157 1184 "@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1158 1185 1159 1186 "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1187 + 1188 + "@sillowww/uploader/@atcute/lexicons/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 1160 1189 1161 1190 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1162 1191
+5 -1
package.json
··· 23 23 }, 24 24 "private": true, 25 25 "dependencies": { 26 - "@iconify-prerendered/vue-material-symbols": "^0.28.1755063979" 26 + "@atcute/atproto": "^3.1.10", 27 + "@atcute/client": "^4.2.0", 28 + "@atcute/lexicons": "^1.2.6", 29 + "@iconify-prerendered/vue-material-symbols": "^0.28.1755063979", 30 + "blurhash": "^2.0.5" 27 31 } 28 32 }
+1
pkgs/web/package.json
··· 25 25 "vue-router": "^4.6.4" 26 26 }, 27 27 "devDependencies": { 28 + "@sillowww/gallery": "workspace:*", 28 29 "@tsconfig/node24": "^24.0.3", 29 30 "@types/node": "^24.10.4", 30 31 "@vitejs/plugin-vue": "^6.0.3",
+225
pkgs/web/src/components/Gallery/GalleryItem.vue
··· 1 + <script setup lang="ts"> 2 + import { onMounted, computed, ref, onUnmounted } from 'vue' 3 + import { decode } from 'blurhash' 4 + import { isLegacyBlob } from '@atcute/lexicons/interfaces' 5 + import { MoeWloGalleryImage } from '@sillowww/gallery' 6 + 7 + import { DID } from '@/utils/links' 8 + 9 + const props = defineProps<{ 10 + item: MoeWloGalleryImage.Main 11 + }>() 12 + 13 + const rootEl = ref<HTMLElement | null>(null) 14 + const imageUrl = ref<string | null>(null) 15 + const isLoaded = ref(false) 16 + const error = ref(false) 17 + 18 + const aspectRatio = computed(() => { 19 + if (!props.item?.width || !props.item?.height) return 1 20 + return props.item.width / props.item.height 21 + }) 22 + 23 + const blobUrl = computed(() => { 24 + const parts = [ 25 + 'https://pds.wlo.moe/xrpc/com.atproto.sync.getBlob', 26 + `?did=${DID}`, 27 + `&cid=${isLegacyBlob(props.item.image) ? props.item.image.cid : props.item.image.ref.$link}`, 28 + ] 29 + return parts.join('') 30 + }) 31 + 32 + const blurhashData = computed(() => { 33 + if (!props.item.blurhash) return null 34 + try { 35 + const pixels = decode(props.item.blurhash, 32, 32) 36 + const canvas = document.createElement('canvas') 37 + canvas.width = 32 38 + canvas.height = 32 39 + const ctx = canvas.getContext('2d') 40 + if (!ctx) return null 41 + const imageData = ctx.createImageData(32, 32) 42 + imageData.data.set(pixels) 43 + ctx.putImageData(imageData, 0, 0) 44 + return canvas.toDataURL() 45 + } catch { 46 + return null 47 + } 48 + }) 49 + 50 + const CACHE_NAME = 'wlo-gallery-v1' 51 + 52 + const loadImage = async () => { 53 + if (imageUrl.value || error.value) return 54 + 55 + try { 56 + const url = blobUrl.value 57 + let blob: Blob | null = null 58 + 59 + const cache = await caches.open(CACHE_NAME) 60 + const cachedRes = await cache.match(url) 61 + 62 + if (cachedRes) { 63 + blob = await cachedRes.blob() 64 + } else { 65 + const res = await fetch(url) 66 + if (!res.ok) throw new Error('fetch failed') 67 + 68 + cache.put(url, res.clone()) 69 + 70 + blob = await res.blob() 71 + } 72 + 73 + const objectUrl = URL.createObjectURL(blob) 74 + 75 + const img = new Image() 76 + img.src = objectUrl 77 + await img.decode() 78 + 79 + imageUrl.value = objectUrl 80 + requestAnimationFrame(() => { 81 + isLoaded.value = true 82 + }) 83 + } catch (e) { 84 + console.error(e) 85 + error.value = true 86 + } 87 + } 88 + 89 + let observer: IntersectionObserver | null = null 90 + 91 + onMounted(() => { 92 + if (!rootEl.value) return 93 + 94 + observer = new IntersectionObserver( 95 + (entries) => { 96 + if (entries[0]?.isIntersecting) { 97 + loadImage() 98 + observer?.disconnect() 99 + } 100 + }, 101 + { rootMargin: '200px' }, 102 + ) 103 + 104 + observer.observe(rootEl.value) 105 + }) 106 + 107 + onUnmounted(() => { 108 + observer?.disconnect() 109 + if (imageUrl.value) URL.revokeObjectURL(imageUrl.value) 110 + }) 111 + </script> 112 + 113 + <template> 114 + <div 115 + ref="rootEl" 116 + class="gallery-item" 117 + :class="{ 'is-loaded': isLoaded, 'has-error': error }" 118 + :style="{ aspectRatio: aspectRatio }" 119 + > 120 + <div class="blurhash-layer"> 121 + <img v-if="blurhashData" :src="blurhashData" alt="" /> 122 + <div v-else class="fallback-gradient"></div> 123 + </div> 124 + 125 + <div class="image-layer"> 126 + <img v-if="imageUrl" :src="imageUrl" :alt="item.alt" /> 127 + </div> 128 + 129 + <div v-if="error" class="error-layer"> 130 + <span>could not load</span> 131 + </div> 132 + </div> 133 + </template> 134 + 135 + <style scoped lang="scss"> 136 + @use '@/styles/variables.scss' as *; 137 + 138 + .gallery-item { 139 + position: relative; 140 + break-inside: avoid; 141 + border-radius: 0.75rem; 142 + overflow: hidden; 143 + background-color: hsla(var(--surface0) / 0.5); 144 + 145 + transform: translateZ(0); /* try to force hardware accel */ 146 + cursor: zoom-in; 147 + border: 1px solid hsla(var(--surface2)); 148 + 149 + &:hover { 150 + filter: brightness(1.1); 151 + .image-layer { 152 + transform: scale(1.025); 153 + } 154 + } 155 + &:active .image-layer { 156 + transform: scale(1); 157 + } 158 + } 159 + 160 + .blurhash-layer, 161 + .image-layer { 162 + position: absolute; 163 + top: 0; 164 + left: 0; 165 + width: 100%; 166 + height: 100%; 167 + 168 + img { 169 + width: 100%; 170 + height: 100%; 171 + object-fit: cover; 172 + display: block; 173 + } 174 + } 175 + 176 + .blurhash-layer { 177 + z-index: 1; 178 + opacity: 1; 179 + 180 + img { 181 + transform: scale(1.2); 182 + filter: blur(20px); 183 + } 184 + } 185 + 186 + .image-layer { 187 + z-index: 2; 188 + opacity: 0; 189 + 190 + will-change: opacity, transform; 191 + 192 + transform: scale(1.1); 193 + filter: blur(8px) saturate(0.8); 194 + } 195 + 196 + .is-loaded { 197 + .blurhash-layer { 198 + opacity: 0; 199 + } 200 + 201 + .image-layer { 202 + opacity: 1; 203 + transform: scale(1.05); 204 + filter: blur(0px) saturate(1); 205 + } 206 + } 207 + 208 + .error-layer { 209 + z-index: 3; 210 + position: absolute; 211 + inset: 0; 212 + display: flex; 213 + align-items: center; 214 + justify-content: center; 215 + background: hsla(var(--surface0) / 0.8); 216 + color: hsla(var(--red)); 217 + font-size: 0.8rem; 218 + } 219 + 220 + .fallback-gradient { 221 + width: 100%; 222 + height: 100%; 223 + background: linear-gradient(45deg, hsla(var(--surface0)), hsla(var(--surface1))); 224 + } 225 + </style>
+2 -1
pkgs/web/src/router/index.ts
··· 13 13 const ProjectsView = () => import('../views/ProjectsView.vue') 14 14 const UsesView = () => import('../views/UsesView.vue') 15 15 const AboutView = () => import('../views/AboutView.vue') 16 + const GalleryView = () => import('../views/GalleryView.vue') 16 17 17 18 declare module 'vue-router' { 18 19 interface RouteMeta { ··· 131 132 { 132 133 path: '/gallery', 133 134 name: 'gallery', 134 - component: ProjectsView, 135 + component: GalleryView, 135 136 meta: { 136 137 isCard: true, 137 138 title: 'gallery',
+70 -2
pkgs/web/src/views/GalleryView.vue
··· 1 1 <script setup lang="ts"> 2 + import { Client, simpleFetchHandler } from '@atcute/client' 3 + import type { MoeWloGalleryImage } from '@sillowww/gallery' 4 + import { onMounted, ref } from 'vue' 5 + 2 6 import CardLayout from '@/components/Card/CardLayout.vue' 7 + import GalleryItem from '@/components/Gallery/GalleryItem.vue' 8 + 9 + import { DID } from '@/utils/links' 10 + 11 + const items = ref<{ uri: string; value: MoeWloGalleryImage.Main }[]>([]) 12 + const loading = ref(true) 13 + const error = ref<string | null>(null) 14 + 15 + onMounted(async () => { 16 + const manager = simpleFetchHandler({ service: 'https://pds.wlo.moe' }) 17 + const client = new Client({ handler: manager }) 18 + 19 + const { ok, data } = await client.get('com.atproto.repo.listRecords', { 20 + params: { 21 + repo: DID, 22 + collection: 'moe.wlo.gallery.image', 23 + }, 24 + }) 25 + 26 + if (!ok) { 27 + error.value = data.error || 'An unknown error occurred while fetching gallery images.' 28 + loading.value = false 29 + return 30 + } 31 + 32 + items.value = data.records.map((record) => { 33 + return { uri: record.uri, value: record.value as MoeWloGalleryImage.Main } 34 + }) 35 + loading.value = false 36 + }) 3 37 </script> 4 38 5 39 <template> 6 - <CardLayout title="placeholder!"> i love place holding :D </CardLayout> 40 + <CardLayout title="gallery"> 41 + <template #intro> 42 + <p>a collection of images that i've taken in the past</p> 43 + </template> 44 + 45 + <div v-if="loading">loading...</div> 46 + <div v-else-if="error">error: {{ error }}</div> 47 + 48 + <div class="masonry-grid" v-else> 49 + <GalleryItem v-for="item in items" :key="item.uri" :item="item.value" /> 50 + </div> 51 + </CardLayout> 7 52 </template> 8 53 9 - <style scoped></style> 54 + <style scoped lang="scss"> 55 + .masonry-grid { 56 + --gap: 0.5rem; 57 + column-count: 4; 58 + column-gap: var(--gap); 59 + margin-top: var(--gap); 60 + 61 + :deep(.gallery-item) { 62 + margin-bottom: var(--gap); 63 + } 64 + 65 + @media (max-width: 1200px) { 66 + column-count: 3; 67 + } 68 + 69 + @media (max-width: 768px) { 70 + column-count: 2; 71 + } 72 + 73 + @media (max-width: 480px) { 74 + column-count: 1; 75 + } 76 + } 77 + </style>
+10 -10
pkgs/web/tsconfig.app.json
··· 1 1 { 2 - "extends": "@vue/tsconfig/tsconfig.dom.json", 3 - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 - "exclude": ["src/**/__tests__/*"], 5 - "compilerOptions": { 6 - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 7 - 8 - "paths": { 9 - "@/*": ["./src/*"] 10 - } 11 - } 2 + "extends": "@vue/tsconfig/tsconfig.dom.json", 3 + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 + "exclude": ["src/**/__tests__/*"], 5 + "compilerOptions": { 6 + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 7 + "types": ["@atcute/atproto", "@sillowww/gallery"], 8 + "paths": { 9 + "@/*": ["./src/*"], 10 + }, 11 + }, 12 12 }