your personal website on atproto - mirror blento.app

big refactor

add site.standard.publication
add subpages
option to hide profile
update svelte+sveltekit

Florian d3f5c384 682b03f5

+1442 -1353
+3 -5
package.json
··· 18 18 "devDependencies": { 19 19 "@eslint/compat": "^1.2.5", 20 20 "@eslint/js": "^9.18.0", 21 - "@sveltejs/adapter-auto": "^4.0.0", 22 21 "@sveltejs/adapter-cloudflare": "^7.2.4", 23 - "@sveltejs/adapter-static": "^3.0.8", 24 - "@sveltejs/kit": "^2.16.0", 22 + "@sveltejs/kit": "^2.49.5", 25 23 "@sveltejs/vite-plugin-svelte": "^5.0.0", 26 24 "@tailwindcss/forms": "^0.5.10", 27 25 "@tailwindcss/vite": "^4.0.0", ··· 33 31 "prettier": "^3.4.2", 34 32 "prettier-plugin-svelte": "^3.3.3", 35 33 "prettier-plugin-tailwindcss": "^0.6.11", 36 - "svelte": "^5.45.8", 34 + "svelte": "^5.46.4", 37 35 "svelte-check": "^4.0.0", 38 36 "tailwindcss": "^4.0.0", 39 37 "typescript": "^5.0.0", ··· 43 41 "dependencies": { 44 42 "@atcute/client": "^3.1.0", 45 43 "@atcute/oauth-browser-client": "^1.0.13", 46 - "@atproto/api": "^0.18.13", 44 + "@atproto/api": "^0.18.16", 47 45 "@atproto/common-web": "^0.4.2", 48 46 "@cloudflare/workers-types": "^4.20260109.0", 49 47 "@ethercorps/sveltekit-og": "^4.2.1",
+167 -158
pnpm-lock.yaml
··· 15 15 specifier: ^1.0.13 16 16 version: 1.0.18 17 17 '@atproto/api': 18 - specifier: ^0.18.13 19 - version: 0.18.13 18 + specifier: ^0.18.16 19 + version: 0.18.16 20 20 '@atproto/common-web': 21 21 specifier: ^0.4.2 22 22 version: 0.4.2 ··· 25 25 version: 4.20260109.0 26 26 '@ethercorps/sveltekit-og': 27 27 specifier: ^4.2.1 28 - version: 4.2.1(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))) 28 + version: 4.2.1(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))) 29 29 '@foxui/colors': 30 30 specifier: ^0.4.7 31 - version: 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 31 + version: 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 32 32 '@foxui/core': 33 33 specifier: ^0.4.7 34 - version: 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 34 + version: 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 35 35 '@foxui/social': 36 36 specifier: ^0.4.7 37 - version: 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 37 + version: 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 38 38 '@foxui/time': 39 39 specifier: ^0.4.7 40 - version: 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 40 + version: 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 41 41 '@tailwindcss/typography': 42 42 specifier: ^0.5.16 43 43 version: 0.5.16(tailwindcss@4.1.5) ··· 67 67 version: 2.12.0 68 68 bits-ui: 69 69 specifier: ^2.14.4 70 - version: 2.14.4(@internationalized/date@3.8.0)(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8) 70 + version: 2.14.4(@internationalized/date@3.8.0)(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4) 71 71 clsx: 72 72 specifier: ^2.1.1 73 73 version: 2.1.1 ··· 94 94 version: 16.5.0 95 95 svelte-sonner: 96 96 specifier: ^1.0.7 97 - version: 1.0.7(svelte@5.45.8) 97 + version: 1.0.7(svelte@5.46.4) 98 98 tailwind-merge: 99 99 specifier: ^3.4.0 100 100 version: 3.4.0 ··· 114 114 '@eslint/js': 115 115 specifier: ^9.18.0 116 116 version: 9.26.0 117 - '@sveltejs/adapter-auto': 118 - specifier: ^4.0.0 119 - version: 4.0.0(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))) 120 117 '@sveltejs/adapter-cloudflare': 121 118 specifier: ^7.2.4 122 - version: 7.2.4(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(wrangler@4.54.0(@cloudflare/workers-types@4.20260109.0)) 123 - '@sveltejs/adapter-static': 124 - specifier: ^3.0.8 125 - version: 3.0.8(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))) 119 + version: 7.2.4(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(wrangler@4.54.0(@cloudflare/workers-types@4.20260109.0)) 126 120 '@sveltejs/kit': 127 - specifier: ^2.16.0 128 - version: 2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 121 + specifier: ^2.49.5 122 + version: 2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 129 123 '@sveltejs/vite-plugin-svelte': 130 124 specifier: ^5.0.0 131 - version: 5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 125 + version: 5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 132 126 '@tailwindcss/forms': 133 127 specifier: ^0.5.10 134 128 version: 0.5.10(tailwindcss@4.1.5) ··· 146 140 version: 10.1.3(eslint@9.26.0(jiti@2.4.2)) 147 141 eslint-plugin-svelte: 148 142 specifier: ^2.46.1 149 - version: 2.46.1(eslint@9.26.0(jiti@2.4.2))(svelte@5.45.8) 143 + version: 2.46.1(eslint@9.26.0(jiti@2.4.2))(svelte@5.46.4) 150 144 globals: 151 145 specifier: ^15.14.0 152 146 version: 15.15.0 ··· 155 149 version: 3.5.3 156 150 prettier-plugin-svelte: 157 151 specifier: ^3.3.3 158 - version: 3.3.3(prettier@3.5.3)(svelte@5.45.8) 152 + version: 3.3.3(prettier@3.5.3)(svelte@5.46.4) 159 153 prettier-plugin-tailwindcss: 160 154 specifier: ^0.6.11 161 - version: 0.6.11(prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.45.8))(prettier@3.5.3) 155 + version: 0.6.11(prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.46.4))(prettier@3.5.3) 162 156 svelte: 163 - specifier: ^5.45.8 164 - version: 5.45.8 157 + specifier: ^5.46.4 158 + version: 5.46.4 165 159 svelte-check: 166 160 specifier: ^4.0.0 167 - version: 4.1.7(picomatch@4.0.2)(svelte@5.45.8)(typescript@5.8.3) 161 + version: 4.1.7(picomatch@4.0.2)(svelte@5.46.4)(typescript@5.8.3) 168 162 tailwindcss: 169 163 specifier: ^4.0.0 170 164 version: 4.1.5 ··· 195 189 '@atproto/api@0.15.27': 196 190 resolution: {integrity: sha512-ok/WGafh1nz4t8pEQGtAF/32x2E2VDWU4af6BajkO5Gky2jp2q6cv6aB2A5yuvNNcc3XkYMYipsqVHVwLPMF9g==, tarball: https://registry.npmjs.org/@atproto/api/-/api-0.15.27.tgz} 197 191 198 - '@atproto/api@0.18.13': 199 - resolution: {integrity: sha512-CULZ01pSJDltLS/Gc9MMrhFzB6OM3ezyZw7KoeLT/sBfwgA1ddA4mWdTh7DIRosPRigXtA05bnoiCutZbQDo+Q==, tarball: https://registry.npmjs.org/@atproto/api/-/api-0.18.13.tgz} 192 + '@atproto/api@0.18.16': 193 + resolution: {integrity: sha512-tRGKSWr83pP5CQpSboePU21pE+GqLDYy1XHae4HH4hjaT0pr5V8wNgu70kbKB0B02GVUumeDRpJnlHKD+eMzLg==, tarball: https://registry.npmjs.org/@atproto/api/-/api-0.18.16.tgz} 200 194 201 195 '@atproto/common-web@0.4.11': 202 196 resolution: {integrity: sha512-VHejNmSABU8/03VrQ3e36AmT5U3UIeio+qSUqCrO1oNgrJcWfGy1rpj0FVtUugWF8Un29+yzkukzWGZfXL70rQ==, tarball: https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.11.tgz} 197 + 198 + '@atproto/common-web@0.4.12': 199 + resolution: {integrity: sha512-3aCJemqM/fkHQrVPbTCHCdiVstKFI+2LkFLvUhO6XZP0EqUZa/rg/CIZBKTFUWu9I5iYiaEiXL9VwcDRpEevSw==, tarball: https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.12.tgz} 203 200 204 201 '@atproto/common-web@0.4.2': 205 202 resolution: {integrity: sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==, tarball: https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.2.tgz} ··· 207 204 '@atproto/lex-data@0.0.7': 208 205 resolution: {integrity: sha512-W/Q5o9o7n2Sv3UywckChu01X5lwQUtaiiOkGJLnRsdkQTyC6813nPgY+p2sG7NwwM+82lu+FUV9fE/Ul3VqaJw==, tarball: https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.7.tgz} 209 206 207 + '@atproto/lex-data@0.0.8': 208 + resolution: {integrity: sha512-1Y5tz7BkS7380QuLNXaE8GW8Xba+mRWugt8BKM4BUFYjjUZdmirU8lr72iM4XlEBrzRu8Cfvj+MbsbYaZv+IgA==, tarball: https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.8.tgz} 209 + 210 210 '@atproto/lex-json@0.0.7': 211 211 resolution: {integrity: sha512-bjNPD5M/MhLfjNM7tcxuls80UgXpHqxdOxDXEUouAtZQV/nIDhGjmNUvKxOmOgnDsiZRnT2g5y3onrnjH3a44g==, tarball: https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.7.tgz} 212 + 213 + '@atproto/lex-json@0.0.8': 214 + resolution: {integrity: sha512-w1Qmkae1QhmNz+i1Zm3xr3jp0UPPRENmdlpU0qIrdxWDo9W4Mzkeyc3eSoa+Zs+zN8xkRSQw7RLZte/B7Ipdwg==, tarball: https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.8.tgz} 212 215 213 216 '@atproto/lexicon@0.4.14': 214 217 resolution: {integrity: sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==, tarball: https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.14.tgz} ··· 977 980 '@speed-highlight/core@1.2.12': 978 981 resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==, tarball: https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.12.tgz} 979 982 983 + '@standard-schema/spec@1.1.0': 984 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, tarball: https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz} 985 + 980 986 '@sveltejs/acorn-typescript@1.0.5': 981 987 resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==, tarball: https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz} 982 988 peerDependencies: 983 989 acorn: ^8.9.0 984 990 985 - '@sveltejs/adapter-auto@4.0.0': 986 - resolution: {integrity: sha512-kmuYSQdD2AwThymQF0haQhM8rE5rhutQXG4LNbnbShwhMO4qQGnKaaTy+88DuNSuoQDi58+thpq8XpHc1+oEKQ==, tarball: https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-4.0.0.tgz} 987 - peerDependencies: 988 - '@sveltejs/kit': ^2.0.0 989 - 990 991 '@sveltejs/adapter-cloudflare@7.2.4': 991 992 resolution: {integrity: sha512-uD8VlOuGXGuZWL+zbBYSjtmC4WDtlonUodfqAZ/COd5uIy2Z0QptIicB/nkTrGNI9sbmzgf7z0N09CHyWYlUvQ==, tarball: https://registry.npmjs.org/@sveltejs/adapter-cloudflare/-/adapter-cloudflare-7.2.4.tgz} 992 993 peerDependencies: 993 994 '@sveltejs/kit': ^2.0.0 994 995 wrangler: ^4.0.0 995 996 996 - '@sveltejs/adapter-static@3.0.8': 997 - resolution: {integrity: sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==, tarball: https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz} 998 - peerDependencies: 999 - '@sveltejs/kit': ^2.0.0 1000 - 1001 - '@sveltejs/kit@2.20.8': 1002 - resolution: {integrity: sha512-ep9qTxL7WALhfm0kFecL3VHeuNew8IccbYGqv5TqL/KSqWRKzEgDG8blNlIu1CkLTTua/kHjI+f5T8eCmWIxKw==, tarball: https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.8.tgz} 997 + '@sveltejs/kit@2.49.5': 998 + resolution: {integrity: sha512-dCYqelr2RVnWUuxc+Dk/dB/SjV/8JBndp1UovCyCZdIQezd8TRwFLNZctYkzgHxRJtaNvseCSRsuuHPeUgIN/A==, tarball: https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.5.tgz} 1003 999 engines: {node: '>=18.13'} 1004 1000 hasBin: true 1005 1001 peerDependencies: 1006 - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 1002 + '@opentelemetry/api': ^1.0.0 1003 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 1007 1004 svelte: ^4.0.0 || ^5.0.0-next.0 1008 - vite: ^5.0.3 || ^6.0.0 1005 + typescript: ^5.3.3 1006 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 1007 + peerDependenciesMeta: 1008 + '@opentelemetry/api': 1009 + optional: true 1010 + typescript: 1011 + optional: true 1009 1012 1010 1013 '@sveltejs/vite-plugin-svelte-inspector@4.0.1': 1011 1014 resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==, tarball: https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz} ··· 1638 1641 resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==, tarball: https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz} 1639 1642 engines: {node: '>=8'} 1640 1643 1641 - devalue@5.1.1: 1642 - resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==, tarball: https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz} 1643 - 1644 - devalue@5.6.0: 1645 - resolution: {integrity: sha512-BaD1s81TFFqbD6Uknni42TrolvEWA1Ih5L+OiHWmi4OYMJVwAYPGtha61I9KxTf52OvVHozHyjPu8zljqdF3uA==, tarball: https://registry.npmjs.org/devalue/-/devalue-5.6.0.tgz} 1644 + devalue@5.6.2: 1645 + resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==, tarball: https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz} 1646 1646 1647 1647 dom-serializer@2.0.0: 1648 1648 resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==, tarball: https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz} ··· 1969 1969 resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, tarball: https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz} 1970 1970 engines: {node: '>=6'} 1971 1971 1972 - import-meta-resolve@4.1.0: 1973 - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==, tarball: https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz} 1974 - 1975 1972 imurmurhash@0.1.4: 1976 1973 resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, tarball: https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz} 1977 1974 engines: {node: '>=0.8.19'} ··· 2755 2752 peerDependencies: 2756 2753 svelte: ^5.0.0 2757 2754 2758 - svelte@5.45.8: 2759 - resolution: {integrity: sha512-1Jh7FwVh/2Uxg0T7SeE1qFKMhwYH45b2v53bcZpW7qHa6O8iU1ByEj56PF0IQ6dU4HE5gRkic6h+vx+tclHeiw==, tarball: https://registry.npmjs.org/svelte/-/svelte-5.45.8.tgz} 2755 + svelte@5.46.4: 2756 + resolution: {integrity: sha512-VJwdXrmv9L8L7ZasJeWcCjoIuMRVbhuxbss0fpVnR8yorMmjNDwcjIH08vS6wmSzzzgAG5CADQ1JuXPS2nwt9w==, tarball: https://registry.npmjs.org/svelte/-/svelte-5.46.4.tgz} 2760 2757 engines: {node: '>=18'} 2761 2758 2762 2759 tabbable@6.2.0: ··· 3043 3040 tlds: 1.258.0 3044 3041 zod: 3.24.4 3045 3042 3046 - '@atproto/api@0.18.13': 3043 + '@atproto/api@0.18.16': 3047 3044 dependencies: 3048 - '@atproto/common-web': 0.4.11 3045 + '@atproto/common-web': 0.4.12 3049 3046 '@atproto/lexicon': 0.6.0 3050 3047 '@atproto/syntax': 0.4.2 3051 3048 '@atproto/xrpc': 0.7.7 ··· 3060 3057 '@atproto/lex-json': 0.0.7 3061 3058 zod: 3.24.4 3062 3059 3060 + '@atproto/common-web@0.4.12': 3061 + dependencies: 3062 + '@atproto/lex-data': 0.0.8 3063 + '@atproto/lex-json': 0.0.8 3064 + zod: 3.24.4 3065 + 3063 3066 '@atproto/common-web@0.4.2': 3064 3067 dependencies: 3065 3068 graphemer: 1.4.0 ··· 3075 3078 uint8arrays: 3.0.0 3076 3079 unicode-segmenter: 0.14.5 3077 3080 3081 + '@atproto/lex-data@0.0.8': 3082 + dependencies: 3083 + '@atproto/syntax': 0.4.2 3084 + multiformats: 9.9.0 3085 + tslib: 2.8.1 3086 + uint8arrays: 3.0.0 3087 + unicode-segmenter: 0.14.5 3088 + 3078 3089 '@atproto/lex-json@0.0.7': 3079 3090 dependencies: 3080 3091 '@atproto/lex-data': 0.0.7 3092 + tslib: 2.8.1 3093 + 3094 + '@atproto/lex-json@0.0.8': 3095 + dependencies: 3096 + '@atproto/lex-data': 0.0.8 3081 3097 tslib: 2.8.1 3082 3098 3083 3099 '@atproto/lexicon@0.4.14': ··· 3342 3358 '@eslint/core': 0.13.0 3343 3359 levn: 0.4.1 3344 3360 3345 - '@ethercorps/sveltekit-og@4.2.1(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))': 3361 + '@ethercorps/sveltekit-og@4.2.1(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))': 3346 3362 dependencies: 3347 3363 '@resvg/resvg-wasm': 2.6.2 3348 - '@sveltejs/kit': 2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3364 + '@sveltejs/kit': 2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3349 3365 '@takumi-rs/helpers': 0.55.4 3350 3366 '@takumi-rs/image-response': 0.55.4 3351 3367 '@takumi-rs/wasm': 0.55.4 ··· 3365 3381 3366 3382 '@floating-ui/utils@0.2.10': {} 3367 3383 3368 - '@foxui/colors@0.4.7(svelte@5.45.8)(tailwindcss@4.1.5)': 3384 + '@foxui/colors@0.4.7(svelte@5.46.4)(tailwindcss@4.1.5)': 3369 3385 dependencies: 3370 - '@foxui/core': 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 3386 + '@foxui/core': 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 3371 3387 '@texel/color': 1.1.11 3372 3388 '@use-gesture/vanilla': 10.3.1 3373 - bits-ui: 1.8.0(svelte@5.45.8) 3374 - svelte: 5.45.8 3389 + bits-ui: 1.8.0(svelte@5.46.4) 3390 + svelte: 5.46.4 3375 3391 tailwindcss: 4.1.5 3376 3392 3377 - '@foxui/core@0.4.7(svelte@5.45.8)(tailwindcss@4.1.5)': 3393 + '@foxui/core@0.4.7(svelte@5.46.4)(tailwindcss@4.1.5)': 3378 3394 dependencies: 3379 - '@number-flow/svelte': 0.3.9(svelte@5.45.8) 3380 - bits-ui: 1.8.0(svelte@5.45.8) 3395 + '@number-flow/svelte': 0.3.9(svelte@5.46.4) 3396 + bits-ui: 1.8.0(svelte@5.46.4) 3381 3397 clsx: 2.1.1 3382 - mode-watcher: 1.1.0(svelte@5.45.8) 3383 - svelte: 5.45.8 3384 - svelte-sonner: 0.3.28(svelte@5.45.8) 3398 + mode-watcher: 1.1.0(svelte@5.46.4) 3399 + svelte: 5.46.4 3400 + svelte-sonner: 0.3.28(svelte@5.46.4) 3385 3401 tailwind-merge: 3.4.0 3386 3402 tailwind-variants: 1.0.0(tailwindcss@4.1.5) 3387 3403 tailwindcss: 4.1.5 3388 3404 3389 - '@foxui/social@0.4.7(svelte@5.45.8)(tailwindcss@4.1.5)': 3405 + '@foxui/social@0.4.7(svelte@5.46.4)(tailwindcss@4.1.5)': 3390 3406 dependencies: 3391 3407 '@atproto/api': 0.15.27 3392 - '@foxui/core': 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 3393 - '@foxui/time': 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 3408 + '@foxui/core': 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 3409 + '@foxui/time': 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 3394 3410 '@use-gesture/vanilla': 10.3.1 3395 - bits-ui: 1.8.0(svelte@5.45.8) 3411 + bits-ui: 1.8.0(svelte@5.46.4) 3396 3412 emoji-picker-element: 1.28.1 3397 3413 hls.js: 1.6.15 3398 3414 is-emoji-supported: 0.0.5 3399 3415 plyr: 3.8.4 3400 - svelte: 5.45.8 3416 + svelte: 5.46.4 3401 3417 tailwindcss: 4.1.5 3402 3418 3403 - '@foxui/time@0.4.7(svelte@5.45.8)(tailwindcss@4.1.5)': 3419 + '@foxui/time@0.4.7(svelte@5.46.4)(tailwindcss@4.1.5)': 3404 3420 dependencies: 3405 - '@foxui/core': 0.4.7(svelte@5.45.8)(tailwindcss@4.1.5) 3406 - '@number-flow/svelte': 0.3.9(svelte@5.45.8) 3407 - bits-ui: 1.8.0(svelte@5.45.8) 3408 - svelte: 5.45.8 3421 + '@foxui/core': 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 3422 + '@number-flow/svelte': 0.3.9(svelte@5.46.4) 3423 + bits-ui: 1.8.0(svelte@5.46.4) 3424 + svelte: 5.46.4 3409 3425 tailwindcss: 4.1.5 3410 3426 3411 3427 '@humanfs/core@0.19.1': {} ··· 3502 3518 3503 3519 '@jridgewell/gen-mapping@0.3.13': 3504 3520 dependencies: 3505 - '@jridgewell/sourcemap-codec': 1.5.0 3521 + '@jridgewell/sourcemap-codec': 1.5.5 3506 3522 '@jridgewell/trace-mapping': 0.3.31 3507 3523 3508 3524 '@jridgewell/remapping@2.3.5': ··· 3524 3540 '@jridgewell/trace-mapping@0.3.31': 3525 3541 dependencies: 3526 3542 '@jridgewell/resolve-uri': 3.1.2 3527 - '@jridgewell/sourcemap-codec': 1.5.0 3543 + '@jridgewell/sourcemap-codec': 1.5.5 3528 3544 3529 3545 '@jridgewell/trace-mapping@0.3.9': 3530 3546 dependencies: ··· 3560 3576 '@nodelib/fs.scandir': 2.1.5 3561 3577 fastq: 1.19.1 3562 3578 3563 - '@number-flow/svelte@0.3.9(svelte@5.45.8)': 3579 + '@number-flow/svelte@0.3.9(svelte@5.46.4)': 3564 3580 dependencies: 3565 3581 esm-env: 1.2.2 3566 3582 number-flow: 0.5.8 3567 - svelte: 5.45.8 3583 + svelte: 5.46.4 3568 3584 3569 3585 '@polka/url@1.0.0-next.29': {} 3570 3586 ··· 3653 3669 3654 3670 '@speed-highlight/core@1.2.12': {} 3655 3671 3656 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': 3657 - dependencies: 3658 - acorn: 8.14.1 3672 + '@standard-schema/spec@1.1.0': {} 3659 3673 3660 - '@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))': 3674 + '@sveltejs/acorn-typescript@1.0.5(acorn@8.15.0)': 3661 3675 dependencies: 3662 - '@sveltejs/kit': 2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3663 - import-meta-resolve: 4.1.0 3676 + acorn: 8.15.0 3664 3677 3665 - '@sveltejs/adapter-cloudflare@7.2.4(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(wrangler@4.54.0(@cloudflare/workers-types@4.20260109.0))': 3678 + '@sveltejs/adapter-cloudflare@7.2.4(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(wrangler@4.54.0(@cloudflare/workers-types@4.20260109.0))': 3666 3679 dependencies: 3667 3680 '@cloudflare/workers-types': 4.20260109.0 3668 - '@sveltejs/kit': 2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3681 + '@sveltejs/kit': 2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3669 3682 worktop: 0.8.0-next.18 3670 3683 wrangler: 4.54.0(@cloudflare/workers-types@4.20260109.0) 3671 3684 3672 - '@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))': 3673 - dependencies: 3674 - '@sveltejs/kit': 2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3675 - 3676 - '@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))': 3685 + '@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))': 3677 3686 dependencies: 3678 - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3687 + '@standard-schema/spec': 1.1.0 3688 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) 3689 + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3679 3690 '@types/cookie': 0.6.0 3691 + acorn: 8.15.0 3680 3692 cookie: 0.6.0 3681 - devalue: 5.1.1 3693 + devalue: 5.6.2 3682 3694 esm-env: 1.2.2 3683 - import-meta-resolve: 4.1.0 3684 3695 kleur: 4.1.5 3685 - magic-string: 0.30.17 3696 + magic-string: 0.30.21 3686 3697 mrmime: 2.0.1 3687 3698 sade: 1.8.1 3688 3699 set-cookie-parser: 2.7.1 3689 3700 sirv: 3.0.1 3690 - svelte: 5.45.8 3701 + svelte: 5.46.4 3691 3702 vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2) 3703 + optionalDependencies: 3704 + typescript: 5.8.3 3692 3705 3693 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))': 3706 + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))': 3694 3707 dependencies: 3695 - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3708 + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3696 3709 debug: 4.4.0 3697 - svelte: 5.45.8 3710 + svelte: 5.46.4 3698 3711 vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2) 3699 3712 transitivePeerDependencies: 3700 3713 - supports-color 3701 3714 3702 - '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))': 3715 + '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2))': 3703 3716 dependencies: 3704 - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3717 + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3705 3718 debug: 4.4.0 3706 3719 deepmerge: 4.3.1 3707 3720 kleur: 4.1.5 3708 3721 magic-string: 0.30.17 3709 - svelte: 5.45.8 3722 + svelte: 5.46.4 3710 3723 vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2) 3711 3724 vitefu: 1.0.6(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 3712 3725 transitivePeerDependencies: ··· 4126 4139 4127 4140 base64-js@0.0.8: {} 4128 4141 4129 - bits-ui@1.8.0(svelte@5.45.8): 4142 + bits-ui@1.8.0(svelte@5.46.4): 4130 4143 dependencies: 4131 4144 '@floating-ui/core': 1.7.3 4132 4145 '@floating-ui/dom': 1.7.4 4133 4146 '@internationalized/date': 3.8.0 4134 4147 css.escape: 1.5.1 4135 4148 esm-env: 1.2.2 4136 - runed: 0.23.4(svelte@5.45.8) 4137 - svelte: 5.45.8 4138 - svelte-toolbelt: 0.7.1(svelte@5.45.8) 4149 + runed: 0.23.4(svelte@5.46.4) 4150 + svelte: 5.46.4 4151 + svelte-toolbelt: 0.7.1(svelte@5.46.4) 4139 4152 tabbable: 6.2.0 4140 4153 4141 - bits-ui@2.14.4(@internationalized/date@3.8.0)(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8): 4154 + bits-ui@2.14.4(@internationalized/date@3.8.0)(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4): 4142 4155 dependencies: 4143 4156 '@floating-ui/core': 1.7.3 4144 4157 '@floating-ui/dom': 1.7.4 4145 4158 '@internationalized/date': 3.8.0 4146 4159 esm-env: 1.2.2 4147 - runed: 0.35.1(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8) 4148 - svelte: 5.45.8 4149 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8) 4160 + runed: 0.35.1(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4) 4161 + svelte: 5.46.4 4162 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4) 4150 4163 tabbable: 6.2.0 4151 4164 transitivePeerDependencies: 4152 4165 - '@sveltejs/kit' ··· 4322 4335 4323 4336 detect-libc@2.0.4: {} 4324 4337 4325 - devalue@5.1.1: {} 4326 - 4327 - devalue@5.6.0: {} 4338 + devalue@5.6.2: {} 4328 4339 4329 4340 dom-serializer@2.0.0: 4330 4341 dependencies: ··· 4447 4458 dependencies: 4448 4459 eslint: 9.26.0(jiti@2.4.2) 4449 4460 4450 - eslint-plugin-svelte@2.46.1(eslint@9.26.0(jiti@2.4.2))(svelte@5.45.8): 4461 + eslint-plugin-svelte@2.46.1(eslint@9.26.0(jiti@2.4.2))(svelte@5.46.4): 4451 4462 dependencies: 4452 4463 '@eslint-community/eslint-utils': 4.7.0(eslint@9.26.0(jiti@2.4.2)) 4453 4464 '@jridgewell/sourcemap-codec': 1.5.0 ··· 4460 4471 postcss-safe-parser: 6.0.0(postcss@8.5.3) 4461 4472 postcss-selector-parser: 6.1.2 4462 4473 semver: 7.7.1 4463 - svelte-eslint-parser: 0.43.0(svelte@5.45.8) 4474 + svelte-eslint-parser: 0.43.0(svelte@5.46.4) 4464 4475 optionalDependencies: 4465 - svelte: 5.45.8 4476 + svelte: 5.46.4 4466 4477 transitivePeerDependencies: 4467 4478 - ts-node 4468 4479 ··· 4544 4555 4545 4556 esrap@2.2.1: 4546 4557 dependencies: 4547 - '@jridgewell/sourcemap-codec': 1.5.0 4558 + '@jridgewell/sourcemap-codec': 1.5.5 4548 4559 4549 4560 esrecurse@4.3.0: 4550 4561 dependencies: ··· 4744 4755 parent-module: 1.0.1 4745 4756 resolve-from: 4.0.0 4746 4757 4747 - import-meta-resolve@4.1.0: {} 4748 - 4749 4758 imurmurhash@0.1.4: {} 4750 4759 4751 4760 inherits@2.0.4: {} ··· 4960 4969 pkg-types: 1.3.1 4961 4970 ufo: 1.6.2 4962 4971 4963 - mode-watcher@1.1.0(svelte@5.45.8): 4972 + mode-watcher@1.1.0(svelte@5.46.4): 4964 4973 dependencies: 4965 - runed: 0.25.0(svelte@5.45.8) 4966 - svelte: 5.45.8 4967 - svelte-toolbelt: 0.7.1(svelte@5.45.8) 4974 + runed: 0.25.0(svelte@5.46.4) 4975 + svelte: 5.46.4 4976 + svelte-toolbelt: 0.7.1(svelte@5.46.4) 4968 4977 4969 4978 mri@1.2.0: {} 4970 4979 ··· 5114 5123 5115 5124 prelude-ls@1.2.1: {} 5116 5125 5117 - prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.45.8): 5126 + prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.46.4): 5118 5127 dependencies: 5119 5128 prettier: 3.5.3 5120 - svelte: 5.45.8 5129 + svelte: 5.46.4 5121 5130 5122 - prettier-plugin-tailwindcss@0.6.11(prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.45.8))(prettier@3.5.3): 5131 + prettier-plugin-tailwindcss@0.6.11(prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.46.4))(prettier@3.5.3): 5123 5132 dependencies: 5124 5133 prettier: 3.5.3 5125 5134 optionalDependencies: 5126 - prettier-plugin-svelte: 3.3.3(prettier@3.5.3)(svelte@5.45.8) 5135 + prettier-plugin-svelte: 3.3.3(prettier@3.5.3)(svelte@5.46.4) 5127 5136 5128 5137 prettier@3.5.3: {} 5129 5138 ··· 5306 5315 dependencies: 5307 5316 queue-microtask: 1.2.3 5308 5317 5309 - runed@0.23.4(svelte@5.45.8): 5318 + runed@0.23.4(svelte@5.46.4): 5310 5319 dependencies: 5311 5320 esm-env: 1.2.2 5312 - svelte: 5.45.8 5321 + svelte: 5.46.4 5313 5322 5314 - runed@0.25.0(svelte@5.45.8): 5323 + runed@0.25.0(svelte@5.46.4): 5315 5324 dependencies: 5316 5325 esm-env: 1.2.2 5317 - svelte: 5.45.8 5326 + svelte: 5.46.4 5318 5327 5319 - runed@0.28.0(svelte@5.45.8): 5328 + runed@0.28.0(svelte@5.46.4): 5320 5329 dependencies: 5321 5330 esm-env: 1.2.2 5322 - svelte: 5.45.8 5331 + svelte: 5.46.4 5323 5332 5324 - runed@0.35.1(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8): 5333 + runed@0.35.1(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4): 5325 5334 dependencies: 5326 5335 dequal: 2.0.3 5327 5336 esm-env: 1.2.2 5328 5337 lz-string: 1.5.0 5329 - svelte: 5.45.8 5338 + svelte: 5.46.4 5330 5339 optionalDependencies: 5331 - '@sveltejs/kit': 2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 5340 + '@sveltejs/kit': 2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)) 5332 5341 5333 5342 sade@1.8.1: 5334 5343 dependencies: ··· 5480 5489 dependencies: 5481 5490 has-flag: 4.0.0 5482 5491 5483 - svelte-check@4.1.7(picomatch@4.0.2)(svelte@5.45.8)(typescript@5.8.3): 5492 + svelte-check@4.1.7(picomatch@4.0.2)(svelte@5.46.4)(typescript@5.8.3): 5484 5493 dependencies: 5485 5494 '@jridgewell/trace-mapping': 0.3.25 5486 5495 chokidar: 4.0.3 5487 5496 fdir: 6.4.4(picomatch@4.0.2) 5488 5497 picocolors: 1.1.1 5489 5498 sade: 1.8.1 5490 - svelte: 5.45.8 5499 + svelte: 5.46.4 5491 5500 typescript: 5.8.3 5492 5501 transitivePeerDependencies: 5493 5502 - picomatch 5494 5503 5495 - svelte-eslint-parser@0.43.0(svelte@5.45.8): 5504 + svelte-eslint-parser@0.43.0(svelte@5.46.4): 5496 5505 dependencies: 5497 5506 eslint-scope: 7.2.2 5498 5507 eslint-visitor-keys: 3.4.3 ··· 5500 5509 postcss: 8.5.3 5501 5510 postcss-scss: 4.0.9(postcss@8.5.3) 5502 5511 optionalDependencies: 5503 - svelte: 5.45.8 5512 + svelte: 5.46.4 5504 5513 5505 - svelte-sonner@0.3.28(svelte@5.45.8): 5514 + svelte-sonner@0.3.28(svelte@5.46.4): 5506 5515 dependencies: 5507 - svelte: 5.45.8 5516 + svelte: 5.46.4 5508 5517 5509 - svelte-sonner@1.0.7(svelte@5.45.8): 5518 + svelte-sonner@1.0.7(svelte@5.46.4): 5510 5519 dependencies: 5511 - runed: 0.28.0(svelte@5.45.8) 5512 - svelte: 5.45.8 5520 + runed: 0.28.0(svelte@5.46.4) 5521 + svelte: 5.46.4 5513 5522 5514 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8): 5523 + svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4): 5515 5524 dependencies: 5516 5525 clsx: 2.1.1 5517 - runed: 0.35.1(@sveltejs/kit@2.20.8(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.45.8) 5526 + runed: 0.35.1(@sveltejs/kit@2.49.5(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.46.4)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4)(typescript@5.8.3)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.46.4) 5518 5527 style-to-object: 1.0.8 5519 - svelte: 5.45.8 5528 + svelte: 5.46.4 5520 5529 transitivePeerDependencies: 5521 5530 - '@sveltejs/kit' 5522 5531 5523 - svelte-toolbelt@0.7.1(svelte@5.45.8): 5532 + svelte-toolbelt@0.7.1(svelte@5.46.4): 5524 5533 dependencies: 5525 5534 clsx: 2.1.1 5526 - runed: 0.23.4(svelte@5.45.8) 5535 + runed: 0.23.4(svelte@5.46.4) 5527 5536 style-to-object: 1.0.8 5528 - svelte: 5.45.8 5537 + svelte: 5.46.4 5529 5538 5530 - svelte@5.45.8: 5539 + svelte@5.46.4: 5531 5540 dependencies: 5532 5541 '@jridgewell/remapping': 2.3.5 5533 - '@jridgewell/sourcemap-codec': 1.5.0 5534 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) 5542 + '@jridgewell/sourcemap-codec': 1.5.5 5543 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) 5535 5544 '@types/estree': 1.0.7 5536 - acorn: 8.14.1 5545 + acorn: 8.15.0 5537 5546 aria-query: 5.3.2 5538 5547 axobject-query: 4.1.0 5539 5548 clsx: 2.1.1 5540 - devalue: 5.6.0 5549 + devalue: 5.6.2 5541 5550 esm-env: 1.2.2 5542 5551 esrap: 2.2.1 5543 5552 is-reference: 3.0.3 5544 5553 locate-character: 3.0.0 5545 - magic-string: 0.30.17 5554 + magic-string: 0.30.21 5546 5555 zimmerframe: 1.1.2 5547 5556 5548 5557 tabbable@6.2.0: {}
-631
src/lib/EditableWebsite.svelte
··· 1 - <script lang="ts"> 2 - import { client, login } from '$lib/oauth/auth.svelte.js'; 3 - 4 - import { Navbar, Button, toast, Toaster, Toggle, Sidebar } from '@foxui/core'; 5 - import { BlueskyLogin } from '@foxui/social'; 6 - 7 - import { COLUMNS, margin, mobileMargin } from '$lib'; 8 - import { 9 - cardsEqual, 10 - clamp, 11 - compactItems, 12 - fixCollisions, 13 - setCanEdit, 14 - setIsMobile, 15 - setPositionOfNewItem, 16 - simulateFinalPosition 17 - } from './helper'; 18 - import Profile from './Profile.svelte'; 19 - import type { Item } from './types'; 20 - import { deleteRecord, putRecord } from './oauth/atproto'; 21 - import { innerWidth } from 'svelte/reactivity/window'; 22 - import { TID } from '@atproto/common-web'; 23 - import EditingCard from './cards/Card/EditingCard.svelte'; 24 - import { AllCardDefinitions, CardDefinitionsByType } from './cards'; 25 - import { tick, type Component } from 'svelte'; 26 - import type { CreationModalComponentProps } from './cards/types'; 27 - import { dev } from '$app/environment'; 28 - import { setDidContext, setHandleContext } from './website/context'; 29 - import BaseEditingCard from './cards/BaseCard/BaseEditingCard.svelte'; 30 - import Settings from './Settings.svelte'; 31 - import ImageDropper from './components/ImageDropper.svelte'; 32 - 33 - let { 34 - handle, 35 - did, 36 - data, 37 - items: originalItems, 38 - settings 39 - }: { handle: string; did: string; data: any; items: Item[]; settings: any } = $props(); 40 - 41 - // svelte-ignore state_referenced_locally 42 - let items: Item[] = $state(originalItems); 43 - 44 - let container: HTMLDivElement | undefined = $state(); 45 - 46 - let activeDragElement: { 47 - element: HTMLDivElement | null; 48 - item: Item | null; 49 - w: number; 50 - h: number; 51 - x: number; 52 - y: number; 53 - mouseDeltaX: number; 54 - mouseDeltaY: number; 55 - } = $state({ 56 - element: null, 57 - item: null, 58 - w: 0, 59 - h: 0, 60 - x: -1, 61 - y: -1, 62 - mouseDeltaX: 0, 63 - mouseDeltaY: 0 64 - }); 65 - 66 - let showingMobileView = $state(false); 67 - let isMobile = $derived(showingMobileView || (innerWidth.current ?? 1000) < 1024); 68 - 69 - setIsMobile(() => isMobile); 70 - 71 - setCanEdit(() => dev || (client.isLoggedIn && client.profile?.did === did)); 72 - 73 - // svelte-ignore state_referenced_locally 74 - setDidContext(did); 75 - // svelte-ignore state_referenced_locally 76 - setHandleContext(handle); 77 - 78 - const getX = (item: Item) => (isMobile ? (item.mobileX ?? item.x) : item.x); 79 - const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y); 80 - const getW = (item: Item) => (isMobile ? (item.mobileW ?? item.w) : item.w); 81 - const getH = (item: Item) => (isMobile ? (item.mobileH ?? item.h) : item.h); 82 - 83 - let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); 84 - 85 - function newCard(type: string = 'link') { 86 - // close sidebar if open 87 - const popover = document.getElementById('mobile-menu'); 88 - if (popover) { 89 - popover.hidePopover(); 90 - } 91 - 92 - let item: Item = { 93 - id: TID.nextStr(), 94 - x: 0, 95 - y: 0, 96 - w: 2, 97 - h: 2, 98 - mobileH: 4, 99 - mobileW: 4, 100 - mobileX: 0, 101 - mobileY: 0, 102 - cardType: type, 103 - cardData: {} 104 - }; 105 - const cardDef = CardDefinitionsByType[type]; 106 - cardDef?.createNew?.(item); 107 - 108 - newItem.item = item; 109 - 110 - if (cardDef?.creationModalComponent) { 111 - newItem.modal = cardDef.creationModalComponent; 112 - } else { 113 - saveNewItem(); 114 - } 115 - } 116 - 117 - async function saveNewItem() { 118 - if (!newItem.item) return; 119 - const item = newItem.item; 120 - 121 - setPositionOfNewItem(item, items); 122 - 123 - items = [...items, item]; 124 - 125 - const containerRect = container?.getBoundingClientRect(); 126 - 127 - newItem = {}; 128 - 129 - await tick(); 130 - 131 - // scroll to newly created card 132 - if (!containerRect) return; 133 - const currentMargin = isMobile ? mobileMargin : margin; 134 - const currentY = isMobile ? item.mobileY : item.y; 135 - const bodyRect = document.body.getBoundingClientRect(); 136 - const offset = containerRect.top - bodyRect.top; 137 - const cellSize = (containerRect.width - currentMargin * 2) / COLUMNS; 138 - window.scrollTo({ top: offset + cellSize * (currentY - 1), behavior: 'smooth' }); 139 - } 140 - 141 - let isSaving = $state(false); 142 - 143 - let newItem: { modal?: Component<CreationModalComponentProps>; item?: Item } = $state({}); 144 - 145 - async function save() { 146 - isSaving = true; 147 - 148 - const promises = []; 149 - // find all cards that have been updated (where items differ from originalItems) 150 - for (let item of items) { 151 - const originalItem = originalItems.find((i) => cardsEqual(i, item)); 152 - 153 - if (!originalItem) { 154 - console.log('updated or new item', item); 155 - item.updatedAt = new Date().toISOString(); 156 - // run optional upload function for this card type 157 - const cardDef = CardDefinitionsByType[item.cardType]; 158 - 159 - if (cardDef?.upload) { 160 - item = await cardDef?.upload(item); 161 - } 162 - 163 - item.version = 1; 164 - 165 - promises.push( 166 - putRecord({ 167 - collection: 'app.blento.card', 168 - rkey: item.id, 169 - record: item 170 - }) 171 - ); 172 - } 173 - } 174 - 175 - // delete items that are in originalItems but not in items 176 - for (let originalItem of originalItems) { 177 - const item = items.find((i) => i.id === originalItem.id); 178 - if (!item) { 179 - console.log('deleting item', originalItem); 180 - promises.push(deleteRecord({ collection: 'app.blento.card', rkey: originalItem.id, did })); 181 - } 182 - } 183 - 184 - await Promise.all(promises); 185 - 186 - isSaving = false; 187 - 188 - fetch('/' + handle + '/api/refreshData'); 189 - console.log('refreshing data'); 190 - 191 - toast('Saved', { 192 - description: 'Your website has been saved!' 193 - }); 194 - } 195 - 196 - const sidebarItems = AllCardDefinitions.filter( 197 - (cardDef) => cardDef.sidebarComponent || cardDef.sidebarButtonText 198 - ); 199 - 200 - let showSettings = $state(false); 201 - 202 - let debugPoint = $state({ x: 0, y: 0 }); 203 - 204 - function getDragXY( 205 - e: DragEvent & { 206 - currentTarget: EventTarget & HTMLDivElement; 207 - } 208 - ) { 209 - if (!container) return; 210 - 211 - const x = e.clientX + activeDragElement.mouseDeltaX; 212 - const y = e.clientY + activeDragElement.mouseDeltaY; 213 - 214 - const rect = container.getBoundingClientRect(); 215 - 216 - debugPoint.x = x - rect.left; 217 - debugPoint.y = y - rect.top + margin; 218 - console.log(rect.top); 219 - 220 - let gridX = clamp( 221 - Math.floor(((x - rect.left) / rect.width) * 8), 222 - 0, 223 - COLUMNS - (activeDragElement.w ?? 0) 224 - ); 225 - gridX = Math.floor(gridX / 2) * 2; 226 - let gridY = Math.max( 227 - Math.round(((y - rect.top + margin) / (rect.width - margin)) * COLUMNS), 228 - 0 229 - ); 230 - if (isMobile) { 231 - gridX = Math.floor(gridX / 2) * 2; 232 - gridY = Math.floor(gridY / 2) * 2; 233 - } 234 - return { x: gridX, y: gridY }; 235 - } 236 - </script> 237 - 238 - <svelte:body 239 - onpaste={(event) => { 240 - const target = event.target; 241 - 242 - const active = document.activeElement; 243 - const isEditable = 244 - active instanceof HTMLInputElement || 245 - active instanceof HTMLTextAreaElement || 246 - active?.isContentEditable; 247 - 248 - if (isEditable) { 249 - // Let normal paste happen 250 - return; 251 - } 252 - 253 - const text = event.clipboardData?.getData('text/plain'); 254 - 255 - if (!text) return; 256 - 257 - try { 258 - const url = new URL(text); 259 - 260 - let item: Item = { 261 - id: TID.nextStr(), 262 - x: 0, 263 - y: 0, 264 - w: 2, 265 - h: 2, 266 - mobileH: 4, 267 - mobileW: 4, 268 - mobileX: 0, 269 - mobileY: 0, 270 - cardType: '', 271 - cardData: {} 272 - }; 273 - 274 - newItem.item = item; 275 - 276 - for (const cardDef of AllCardDefinitions) { 277 - if (cardDef.onUrlHandler?.(text, item)) { 278 - item.cardType = cardDef.type; 279 - saveNewItem(); 280 - } 281 - } 282 - 283 - newItem = {}; 284 - } catch (e) { 285 - return; 286 - } 287 - }} 288 - /> 289 - 290 - <!-- <ImageDropper processImageFile={(file: File) => {}} /> --> 291 - 292 - {#if !dev} 293 - <div 294 - class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden" 295 - > 296 - Editing on mobile is not supported yet. Please use a desktop browser. 297 - </div> 298 - {/if} 299 - 300 - {#if showingMobileView} 301 - <div 302 - class="bg-base-200 dark:bg-base-900 pointer-events-none fixed inset-0 -z-10 h-full w-full" 303 - ></div> 304 - {/if} 305 - 306 - {#if newItem.modal && newItem.item} 307 - <newItem.modal 308 - oncreate={() => { 309 - saveNewItem(); 310 - }} 311 - bind:item={newItem.item} 312 - oncancel={() => { 313 - newItem = {}; 314 - }} 315 - /> 316 - {/if} 317 - 318 - <div 319 - class={[ 320 - '@container/wrapper relative w-full', 321 - showingMobileView 322 - ? 'bg-base-50 dark:bg-base-950 my-4 min-h-[calc(100dhv-2em)] rounded-2xl lg:mx-auto lg:w-[400px]' 323 - : '' 324 - ]} 325 - > 326 - <Profile {handle} {did} {data} /> 327 - 328 - <div class="mx-auto max-w-lg @5xl/wrapper:grid @5xl/wrapper:max-w-7xl @5xl/wrapper:grid-cols-4"> 329 - <div></div> 330 - <!-- svelte-ignore a11y_no_static_element_interactions --> 331 - <div 332 - bind:this={container} 333 - ondragover={(e) => { 334 - e.preventDefault(); 335 - 336 - const cell = getDragXY(e); 337 - if (!cell) return; 338 - 339 - activeDragElement.x = cell.x; 340 - activeDragElement.y = cell.y; 341 - 342 - if (activeDragElement.item) { 343 - if (isMobile) { 344 - activeDragElement.item.mobileX = cell.x; 345 - activeDragElement.item.mobileY = cell.y; 346 - } else { 347 - activeDragElement.item.x = cell.x; 348 - activeDragElement.item.y = cell.y; 349 - } 350 - 351 - fixCollisions(items, activeDragElement.item, isMobile); 352 - } 353 - 354 - // Auto-scroll when dragging near top or bottom of viewport 355 - const scrollZone = 100; 356 - const scrollSpeed = 10; 357 - const viewportHeight = window.innerHeight; 358 - 359 - if (e.clientY < scrollZone) { 360 - // Near top - scroll up 361 - const intensity = 1 - e.clientY / scrollZone; 362 - window.scrollBy(0, -scrollSpeed * intensity); 363 - } else if (e.clientY > viewportHeight - scrollZone) { 364 - // Near bottom - scroll down 365 - const intensity = 1 - (viewportHeight - e.clientY) / scrollZone; 366 - window.scrollBy(0, scrollSpeed * intensity); 367 - } 368 - }} 369 - ondragend={async (e) => { 370 - e.preventDefault(); 371 - const cell = getDragXY(e); 372 - if (!cell) return; 373 - 374 - if (activeDragElement.item) { 375 - if (isMobile) { 376 - activeDragElement.item.mobileX = cell.x; 377 - activeDragElement.item.mobileY = cell.y; 378 - } else { 379 - activeDragElement.item.x = cell.x; 380 - activeDragElement.item.y = cell.y; 381 - } 382 - 383 - fixCollisions(items, activeDragElement.item, isMobile); 384 - } 385 - activeDragElement.x = -1; 386 - activeDragElement.y = -1; 387 - activeDragElement.element = null; 388 - return true; 389 - }} 390 - class="@container/grid relative col-span-3 px-2 py-8 @5xl/wrapper:px-8" 391 - > 392 - {#each items as item, i (item.id)} 393 - <!-- {#if item !== activeDragElement.item} --> 394 - <BaseEditingCard 395 - bind:item={items[i]} 396 - ondelete={() => { 397 - items = items.filter((it) => it !== item); 398 - compactItems(items, isMobile); 399 - }} 400 - onsetsize={(newW: number, newH: number) => { 401 - if (isMobile) { 402 - item.mobileW = newW; 403 - item.mobileH = newH; 404 - } else { 405 - item.w = newW; 406 - item.h = newH; 407 - } 408 - 409 - fixCollisions(items, item, isMobile); 410 - }} 411 - ondragstart={(e) => { 412 - const target = e.currentTarget as HTMLDivElement; 413 - activeDragElement.element = target; 414 - activeDragElement.w = item.w; 415 - activeDragElement.h = item.h; 416 - activeDragElement.item = item; 417 - 418 - const rect = target.getBoundingClientRect(); 419 - activeDragElement.mouseDeltaX = rect.left - e.clientX; 420 - activeDragElement.mouseDeltaY = rect.top - e.clientY; 421 - console.log(activeDragElement.mouseDeltaY); 422 - console.log(rect.width); 423 - }} 424 - > 425 - <EditingCard bind:item={items[i]} /> 426 - </BaseEditingCard> 427 - <!-- {/if} --> 428 - {/each} 429 - 430 - <div style="height: {((maxHeight + 2) / 8) * 100}cqw;"></div> 431 - </div> 432 - </div> 433 - </div> 434 - 435 - <Settings bind:open={showSettings} /> 436 - 437 - <Sidebar mobileOnly mobileClasses="lg:block p-4 gap-4"> 438 - <div class="flex flex-col gap-2"> 439 - {#each sidebarItems as cardDef} 440 - {#if cardDef.sidebarComponent} 441 - <cardDef.sidebarComponent onclick={() => newCard(cardDef.type)} /> 442 - {:else if cardDef.sidebarButtonText} 443 - <Button onclick={() => newCard(cardDef.type)} variant="ghost" class="w-full justify-start" 444 - >{cardDef.sidebarButtonText}</Button 445 - > 446 - {/if} 447 - {/each} 448 - </div> 449 - </Sidebar> 450 - 451 - {#if dev || (!client.isLoggedIn && !client.isInitializing) || client.profile?.did === did} 452 - <Navbar 453 - class={[ 454 - 'dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 md:mx-auto lg:inline-flex', 455 - !dev ? 'hidden' : '' 456 - ]} 457 - > 458 - <div class="flex items-center gap-2"> 459 - {#if dev} 460 - <Button 461 - size="iconLg" 462 - variant="ghost" 463 - class="mr-4 backdrop-blur-none" 464 - onclick={() => { 465 - showSettings = true; 466 - }} 467 - > 468 - <svg 469 - xmlns="http://www.w3.org/2000/svg" 470 - fill="none" 471 - viewBox="0 0 24 24" 472 - stroke-width="1.5" 473 - stroke="currentColor" 474 - > 475 - <path 476 - stroke-linecap="round" 477 - stroke-linejoin="round" 478 - d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" 479 - /> 480 - <path 481 - stroke-linecap="round" 482 - stroke-linejoin="round" 483 - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 484 - /> 485 - </svg> 486 - </Button> 487 - {/if} 488 - 489 - <Button 490 - size="iconLg" 491 - variant="ghost" 492 - class="backdrop-blur-none" 493 - onclick={() => { 494 - newCard('text'); 495 - }} 496 - > 497 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" 498 - ><path 499 - fill="none" 500 - stroke="currentColor" 501 - stroke-linecap="round" 502 - stroke-linejoin="round" 503 - stroke-width="1.5" 504 - d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 505 - /></svg 506 - > 507 - </Button> 508 - <Button 509 - size="iconLg" 510 - variant="ghost" 511 - class="backdrop-blur-none" 512 - onclick={() => { 513 - newCard('link'); 514 - }} 515 - > 516 - <svg 517 - xmlns="http://www.w3.org/2000/svg" 518 - fill="none" 519 - viewBox="-2 -2 28 28" 520 - stroke-width="1.5" 521 - stroke="currentColor" 522 - > 523 - <path 524 - stroke-linecap="round" 525 - stroke-linejoin="round" 526 - d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 527 - /> 528 - </svg> 529 - </Button> 530 - 531 - <Button 532 - size="iconLg" 533 - variant="ghost" 534 - class="backdrop-blur-none" 535 - onclick={() => { 536 - newCard('image'); 537 - }} 538 - > 539 - <svg 540 - xmlns="http://www.w3.org/2000/svg" 541 - fill="none" 542 - viewBox="0 0 24 24" 543 - stroke-width="1.5" 544 - stroke="currentColor" 545 - > 546 - <path 547 - stroke-linecap="round" 548 - stroke-linejoin="round" 549 - d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 550 - /> 551 - </svg> 552 - </Button> 553 - 554 - <Button size="iconLg" variant="ghost" class="backdrop-blur-none" popovertarget="mobile-menu"> 555 - <svg 556 - xmlns="http://www.w3.org/2000/svg" 557 - fill="none" 558 - viewBox="0 0 24 24" 559 - stroke-width="1.5" 560 - stroke="currentColor" 561 - > 562 - <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 563 - </svg> 564 - </Button> 565 - 566 - <!-- for special stuff --> 567 - {#if handle === 'blento.app'} 568 - <Button 569 - size="iconLg" 570 - variant="ghost" 571 - class="backdrop-blur-none" 572 - onclick={() => { 573 - newCard('updatedBlentos'); 574 - }} 575 - > 576 - <svg 577 - xmlns="http://www.w3.org/2000/svg" 578 - fill="none" 579 - viewBox="0 0 24 24" 580 - stroke-width="1.5" 581 - stroke="currentColor" 582 - > 583 - <path 584 - stroke-linecap="round" 585 - stroke-linejoin="round" 586 - d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" 587 - /> 588 - </svg> 589 - </Button> 590 - {/if} 591 - </div> 592 - <div class="flex items-center gap-2"> 593 - <Toggle 594 - class="hidden bg-transparent backdrop-blur-none lg:block dark:bg-transparent" 595 - bind:pressed={showingMobileView} 596 - > 597 - <svg 598 - xmlns="http://www.w3.org/2000/svg" 599 - fill="none" 600 - viewBox="0 0 24 24" 601 - stroke-width="1.5" 602 - stroke="currentColor" 603 - class="size-6" 604 - > 605 - <path 606 - stroke-linecap="round" 607 - stroke-linejoin="round" 608 - d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" 609 - /> 610 - </svg> 611 - </Toggle> 612 - {#if client.isLoggedIn} 613 - <Button 614 - disabled={isSaving} 615 - onclick={async () => { 616 - save(); 617 - }}>{isSaving ? 'Saving...' : 'Save'}</Button 618 - > 619 - {:else} 620 - <BlueskyLogin 621 - login={async (handle) => { 622 - await login(handle); 623 - return true; 624 - }} 625 - /> 626 - {/if} 627 - </div> 628 - </Navbar> 629 - {/if} 630 - 631 - <Toaster />
-20
src/lib/Head.svelte
··· 1 - <script lang="ts"> 2 - let { favicon, title, image }: { favicon: string | null; title: string | null; image?: string } = 3 - $props(); 4 - </script> 5 - 6 - <svelte:head> 7 - {#if favicon} 8 - <link rel="icon" href={favicon} /> 9 - {/if} 10 - 11 - {#if title} 12 - <title>{title}</title> 13 - {/if} 14 - 15 - {#if image} 16 - <meta property="og:image" content={image} /> 17 - <meta name="twitter:image" content={image} /> 18 - <meta name="twitter:card" content="summary_large_image" /> 19 - {/if} 20 - </svelte:head>
-166
src/lib/Profile.svelte
··· 1 - <script lang="ts"> 2 - import Head from './Head.svelte'; 3 - 4 - import { marked } from 'marked'; 5 - import { client, login } from './oauth'; 6 - import { Button, Subheading } from '@foxui/core'; 7 - import { BlueskyLogin } from '@foxui/social'; 8 - import { env } from '$env/dynamic/public'; 9 - let { 10 - handle, 11 - did, 12 - data, 13 - showEditButton = false 14 - }: { handle: string; did: string; data: any; showEditButton?: boolean } = $props(); 15 - 16 - // svelte-ignore state_referenced_locally 17 - const profileData = data?.data?.['app.bsky.actor.profile']?.self?.value; 18 - 19 - const renderer = new marked.Renderer(); 20 - renderer.link = ({ href, title, text }) => 21 - `<a target="_blank" href="${href}" title="${title}">${text}</a>`; 22 - </script> 23 - 24 - <Head 25 - favicon={profileData?.avatar?.ref?.$link ? 'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData.avatar.ref.$link : null} 26 - title={profileData?.displayName || handle} 27 - image={'/' + handle + '/og.png'} 28 - /> 29 - 30 - <!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 --> 31 - <div 32 - class="mx-auto flex max-w-lg flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12" 33 - > 34 - <div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24"> 35 - {#if profileData?.avatar?.ref?.$link} 36 - <img 37 - class="size-32 rounded-full @5xl/wrapper:size-44 border border-base-400 dark:border-base-800" 38 - src={'https://cdn.bsky.app/img/avatar/plain/' + did + '/' + profileData.avatar.ref.$link} 39 - alt="" 40 - /> 41 - {:else} 42 - <div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div> 43 - {/if} 44 - <div class="text-4xl font-bold wrap-anywhere"> 45 - {profileData?.displayName || handle} 46 - </div> 47 - 48 - <div class="scrollbar -mx-4 flex-grow overflow-y-scroll px-4 overflow-x-hidden"> 49 - <div 50 - class="text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline" 51 - > 52 - {@html marked.parse(profileData?.description ?? '', { renderer })} 53 - </div> 54 - </div> 55 - 56 - {#if showEditButton && client.isLoggedIn && client.profile?.did === did} 57 - <div> 58 - <Button href="{env.PUBLIC_IS_SELFHOSTED ? '' : client.profile?.handle}/edit" class="mt-2"> 59 - <svg 60 - xmlns="http://www.w3.org/2000/svg" 61 - fill="none" 62 - viewBox="0 0 24 24" 63 - stroke-width="1.5" 64 - stroke="currentColor" 65 - > 66 - <path 67 - stroke-linecap="round" 68 - stroke-linejoin="round" 69 - d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" 70 - /> 71 - </svg> 72 - 73 - Edit Your Website</Button 74 - > 75 - </div> 76 - {/if} 77 - 78 - {#if !env.PUBLIC_IS_SELFHOSTED && handle === 'blento.app' && client.profile?.handle !== handle} 79 - {#if !client.isInitializing && !client.isLoggedIn} 80 - <div> 81 - <div class="my-4 text-sm"> 82 - To create your own blento, sign in with your bluesky account 83 - </div> 84 - <BlueskyLogin 85 - login={async (handle) => { 86 - await login(handle); 87 - return true; 88 - }} 89 - /> 90 - </div> 91 - {:else if client.isLoggedIn} 92 - <div> 93 - <Button href={'/' + client.profile?.handle} class="mt-2"> 94 - <svg 95 - xmlns="http://www.w3.org/2000/svg" 96 - fill="none" 97 - viewBox="0 0 24 24" 98 - stroke-width="1.5" 99 - stroke="currentColor" 100 - > 101 - <path 102 - stroke-linecap="round" 103 - stroke-linejoin="round" 104 - d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" 105 - /> 106 - </svg> 107 - 108 - Open Your Blento</Button 109 - > 110 - </div> 111 - {/if} 112 - {/if} 113 - <div class="hidden text-xs font-light @5xl/wrapper:block"> 114 - made with <a 115 - href="https://blento.app" 116 - target="_blank" 117 - class="hover:text-accent-600 dark:hover:text-accent-400 font-medium transition-colors duration-200" 118 - >blento</a 119 - > 120 - </div> 121 - </div> 122 - </div> 123 - 124 - <style> 125 - .scrollbar::-webkit-scrollbar-track { 126 - background-color: transparent; 127 - } 128 - 129 - @supports (scrollbar-width: auto) { 130 - .scrollbar { 131 - scrollbar-color: var(--color-base-400) transparent; 132 - scrollbar-width: thin; 133 - } 134 - 135 - :global(.dark .scrollbar) { 136 - scrollbar-color: var(--color-base-800) transparent; 137 - } 138 - } 139 - 140 - @supports not (scrollbar-width: auto) { 141 - :global(.scrollbar::-webkit-scrollbar) { 142 - width: 14px; 143 - height: 14px; 144 - } 145 - } 146 - 147 - .scrollbar::-webkit-scrollbar-thumb { 148 - background-color: var(--color-base-400); 149 - border-radius: 20px; 150 - border: 4px solid transparent; 151 - background-clip: content-box; 152 - } 153 - 154 - .scrollbar::-webkit-scrollbar-thumb:hover { 155 - background-color: var(--color-base-500); 156 - } 157 - 158 - /* Dark mode rules */ 159 - :global(.dark .scrollbar::-webkit-scrollbar-thumb) { 160 - background-color: var(--color-base-800); 161 - } 162 - 163 - :global(.dark .scrollbar::-webkit-scrollbar-thumb:hover) { 164 - background-color: var(--color-base-700); 165 - } 166 - </style>
-11
src/lib/Settings.svelte
··· 1 - <script lang="ts"> 2 - import { Modal } from '@foxui/core'; 3 - 4 - export type Settings = { 5 - title: string; 6 - }; 7 - 8 - let { open = $bindable(), settings }: { open: boolean; settings: Settings } = $props(); 9 - </script> 10 - 11 - <Modal bind:open>Settings</Modal>
-65
src/lib/Website.svelte
··· 1 - <script lang="ts"> 2 - import Card from './cards/Card/Card.svelte'; 3 - import Profile from './Profile.svelte'; 4 - import { setIsMobile, sortItems } from './helper'; 5 - import type { Item } from './types'; 6 - import { innerWidth } from 'svelte/reactivity/window'; 7 - import { setDidContext, setHandleContext } from './website/context'; 8 - import BaseCard from './cards/BaseCard/BaseCard.svelte'; 9 - import { onMount } from 'svelte'; 10 - import { describeRepo } from './oauth/atproto'; 11 - 12 - let { handle, did, items, data }: { handle: string; did: string; items: Item[]; data: any } = 13 - $props(); 14 - 15 - let isMobile = $derived((innerWidth.current ?? 1000) < 1024); 16 - 17 - setIsMobile(() => isMobile); 18 - 19 - // svelte-ignore state_referenced_locally 20 - setDidContext(did); 21 - // svelte-ignore state_referenced_locally 22 - setHandleContext(handle); 23 - 24 - 25 - let maxHeight = $derived( 26 - items.reduce( 27 - (max, item) => Math.max(max, isMobile ? item.mobileY + item.mobileH : item.y + item.h), 28 - 0 29 - ) 30 - ); 31 - 32 - let container: HTMLDivElement | undefined = $state(); 33 - 34 - onMount(() => { 35 - describeRepo({did}); 36 - }); 37 - </script> 38 - 39 - <div class="@container/wrapper relative w-full"> 40 - <Profile {handle} {did} {data} showEditButton={true} /> 41 - 42 - <div class="mx-auto max-w-lg lg:grid lg:max-w-none lg:grid-cols-4"> 43 - <div></div> 44 - <div 45 - bind:this={container} 46 - class="@container/grid relative col-span-3 px-2 py-8 lg:px-8" 47 - > 48 - {#each items.toSorted(sortItems) as item} 49 - <BaseCard {item}> 50 - <Card {item} /> 51 - </BaseCard> 52 - {/each} 53 - <div style="height: {(maxHeight / 8) * 100}cqw;"></div> 54 - </div> 55 - </div> 56 - 57 - <div class="mx-auto block pb-8 text-center text-xs font-light @5xl/wrapper:hidden"> 58 - made with <a 59 - href="https://blento.app" 60 - target="_blank" 61 - class="hover:text-accent-600 dark:hover:text-accent-400 font-medium transition-colors duration-200" 62 - >blento</a 63 - > 64 - </div> 65 - </div>
+1 -2
src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte
··· 1 1 <script lang="ts"> 2 - import { getAdditionalUserData } from '$lib/helper'; 3 2 import { onMount } from 'svelte'; 4 3 import type { ContentComponentProps } from '../types'; 5 4 import { CardDefinitionsByType } from '..'; 6 - import { getDidContext, getHandleContext } from '$lib/website/context'; 5 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 7 6 import { Badge, Button } from '@foxui/core'; 8 7 9 8 let { item }: ContentComponentProps = $props();
+1 -1
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 4 4 import BaseCard from './BaseCard.svelte'; 5 5 import type { Item } from '$lib/types'; 6 6 import { Button, Popover } from '@foxui/core'; 7 - import { getCanEdit, getIsMobile } from '$lib/helper'; 8 7 import { ColorSelect } from '@foxui/colors'; 9 8 import { CardDefinitionsByType, getColor } from '..'; 10 9 import { COLUMNS } from '$lib'; 10 + import { getCanEdit, getIsMobile } from '$lib/website/context'; 11 11 12 12 let colorsChoices = [ 13 13 { class: 'text-base-500', label: 'base' },
+1 -3
src/lib/cards/BlueskyMediaCard/BlueskyMediaCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { getDidContext } from '$lib/website/context'; 3 - import { getImageBlobUrl } from '$lib/website/utils'; 3 + import { getImageBlobUrl } from '$lib/oauth/utils'; 4 4 import type { ContentComponentProps } from '../types'; 5 5 import Video from './Video.svelte'; 6 6 ··· 16 16 } 17 17 return item.cardData.image; 18 18 } 19 - 20 - $inspect(item.cardData); 21 19 </script> 22 20 23 21 {#if item.cardData.image}
-1
src/lib/cards/BlueskyMediaCard/CreateBlueskyMediaCardModal.svelte
··· 4 4 import { onMount } from 'svelte'; 5 5 import { AtpBaseClient } from '@atproto/api'; 6 6 import { getDidContext } from '$lib/website/context'; 7 - import { getImageBlobUrl } from '$lib/website/utils'; 8 7 9 8 let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 10 9
+1 -2
src/lib/cards/BlueskyPostCard/BlueskyPostCard.svelte
··· 1 1 <script lang="ts"> 2 - import { getAdditionalUserData } from '$lib/helper'; 3 2 import type { Item } from '$lib/types'; 4 3 import { onMount } from 'svelte'; 5 4 import { BlueskyPost } from '../../components/bluesky-post'; 6 - import { getDidContext, getHandleContext } from '$lib/website/context'; 5 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 7 6 import { CardDefinitionsByType } from '..'; 8 7 9 8 let { item }: { item: Item } = $props();
+3
src/lib/cards/GameCards/DinoGameCard/DinoGameCard.svelte
··· 1 1 <script lang="ts"> 2 + import { isTyping } from '$lib/helper'; 2 3 import type { ContentComponentProps } from '../../types'; 3 4 import { onMount, onDestroy } from 'svelte'; 4 5 ··· 196 197 } 197 198 198 199 function handleKeyDown(e: KeyboardEvent) { 200 + if(isTyping()) return; 201 + 199 202 if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { 200 203 e.preventDefault(); 201 204 jump();
+1 -1
src/lib/cards/GameCards/DinoGameCard/index.ts
··· 1 - import type { CardDefinition } from '../types'; 1 + import type { CardDefinition } from '$lib/cards/types'; 2 2 import DinoGameCard from './DinoGameCard.svelte'; 3 3 import SidebarItemDinoGameCard from './SidebarItemDinoGameCard.svelte'; 4 4
+1 -1
src/lib/cards/ImageCard/ImageCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { getDidContext } from '$lib/website/context'; 3 - import { getImageBlobUrl } from '$lib/website/utils'; 3 + import { getImageBlobUrl } from '$lib/oauth/utils'; 4 4 import type { ContentComponentProps } from '../types'; 5 5 6 6 let { item = $bindable(), ...rest }: ContentComponentProps = $props();
+1 -1
src/lib/cards/ImageCard/index.ts
··· 1 - import { uploadBlob } from '$lib/website/utils'; 1 + import { uploadBlob } from '$lib/oauth/utils'; 2 2 import type { CardDefinition } from '../types'; 3 3 import CreateImageCardModal from './CreateImageCardModal.svelte'; 4 4 import ImageCard from './ImageCard.svelte';
+1 -1
src/lib/cards/LinkCard/EditingLinkCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { browser } from '$app/environment'; 3 - import { getIsMobile } from '$lib/helper'; 3 + import { getIsMobile } from '$lib/website/context'; 4 4 import type { ContentComponentProps } from '../types'; 5 5 import PlainTextEditor from '../utils/PlainTextEditor.svelte'; 6 6
+1 -1
src/lib/cards/LinkCard/LinkCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { browser } from '$app/environment'; 3 - import { getIsMobile } from '$lib/helper'; 3 + import { getIsMobile } from '$lib/website/context'; 4 4 import type { ContentComponentProps } from '../types'; 5 5 6 6 let { item }: ContentComponentProps = $props();
+3 -6
src/lib/cards/LivestreamCard/LivestreamCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from 'svelte'; 3 3 import Icon from './Icon.svelte'; 4 - import { getDidContext, getHandleContext } from '$lib/website/context'; 5 - import { listRecords } from '$lib/oauth/atproto'; 6 - import { getAdditionalUserData, getIsMobile } from '$lib/helper'; 4 + import { getAdditionalUserData, getDidContext, getHandleContext, getIsMobile } from '$lib/website/context'; 7 5 import type { ContentComponentProps } from '../types'; 8 - import { getImageBlobUrl } from '$lib/website/utils'; 9 6 import { RelativeTime } from '@foxui/time'; 10 - import { online } from 'svelte/reactivity/window'; 11 7 import { Badge } from '@foxui/core'; 12 8 import { CardDefinitionsByType } from '..'; 9 + import { browser } from '$app/environment'; 13 10 14 11 let { item = $bindable() }: ContentComponentProps = $props(); 15 12 ··· 94 91 </a> 95 92 </div> 96 93 97 - {#if ((isMobile() && item.mobileH >= 4) || (!isMobile() && item.h >= 2)) && latestLivestream?.thumb} 94 + {#if browser && ((isMobile() && item.mobileH >= 7) || (!isMobile() && item.h >= 4)) && latestLivestream?.thumb} 98 95 <a href={latestLivestream?.href} target="_blank" rel="noopener noreferrer"> 99 96 <img 100 97 class="my-4 max-h-32 w-full rounded-xl object-cover"
+1 -1
src/lib/cards/LivestreamCard/index.ts
··· 1 1 import { client } from '$lib/oauth'; 2 2 import { listRecords } from '$lib/oauth/atproto'; 3 - import { getImageBlobUrl } from '$lib/website/utils'; 3 + import { getImageBlobUrl } from '$lib/oauth/utils'; 4 4 import EmbedCard from '../EmbedCard/EmbedCard.svelte'; 5 5 import type { CardDefinition } from '../types'; 6 6 import LivestreamCard from './LivestreamCard.svelte';
+1 -2
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 1 1 <script lang="ts"> 2 2 import type { ContentComponentProps } from '$lib/cards/types'; 3 - import { getAdditionalUserData } from '$lib/helper'; 4 - import { getProfile } from '$lib/oauth/atproto'; 3 + import { getAdditionalUserData } from '$lib/website/context'; 5 4 import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 6 5 import { onMount } from 'svelte'; 7 6
+30 -10
src/lib/helper.ts
··· 1 - import { createContext } from 'svelte'; 2 - import type { Item } from './types'; 1 + import type { Item, WebsiteData } from './types'; 3 2 import { COLUMNS } from '$lib'; 4 3 5 4 export function clamp(value: number, min: number, max: number): number { ··· 226 225 a.y === b.y && 227 226 a.mobileX === b.mobileX && 228 227 a.mobileY === b.mobileY && 229 - a.color === b.color 228 + a.color === b.color && 229 + a.page === b.page 230 230 ); 231 231 } 232 232 ··· 257 257 } 258 258 } 259 259 260 - export const [getIsMobile, setIsMobile] = createContext<() => boolean>(); 261 - 262 - export const [getCanEdit, setCanEdit] = createContext<() => boolean>(); 263 - 264 - export const [getAdditionalUserData, setAdditionalUserData] = 265 - createContext<Record<string, unknown>>(); 266 - 267 260 export async function refreshData(data: { updatedAt?: number; handle: string }) { 268 261 const FIVE_MINUTES = 5 * 60 * 1000; 269 262 const now = Date.now(); ··· 279 272 console.log('data still fresh, skipping refreshing', data.handle); 280 273 } 281 274 } 275 + 276 + export function getName(data: WebsiteData): string { 277 + return (data.publication?.name ?? data.profile.displayName) || data.handle; 278 + } 279 + 280 + export function getDescription(data: WebsiteData): string { 281 + return data.publication?.description ?? data.profile.description ?? ''; 282 + } 283 + 284 + export function getHideProfile(data: WebsiteData): boolean { 285 + if (data?.publication?.preferences?.hideProfile === false) return false; 286 + if (data?.publication?.preferences?.hideProfile === true) return true; 287 + 288 + return data.page !== 'blento.self'; 289 + } 290 + 291 + export function isTyping() { 292 + const active = document.activeElement; 293 + 294 + const isEditable = 295 + active instanceof HTMLInputElement || 296 + active instanceof HTMLTextAreaElement || 297 + // @ts-expect-error this fine 298 + active?.isContentEditable; 299 + 300 + return isEditable; 301 + }
+19 -22
src/lib/oauth/atproto.ts
··· 1 1 import { AtpBaseClient } from '@atproto/api'; 2 2 import { client } from './auth.svelte'; 3 + import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 4 + import type { At } from '@atcute/client/lexicons'; 3 5 4 6 export async function resolveHandle({ handle }: { handle: string }) { 5 7 const agent = new AtpBaseClient({ service: 'https://api.bsky.app' }); ··· 39 41 did, 40 42 collection, 41 43 cursor, 42 - limit = 100 44 + limit = 0 43 45 }: { 44 46 did: string; 45 47 collection: string; ··· 50 52 51 53 const agent = new AtpBaseClient({ service: pds }); 52 54 53 - const room = await agent.com.atproto.repo.listRecords({ 54 - repo: did, 55 - collection, 56 - limit, 57 - cursor 58 - }); 55 + const allRecords = []; 59 56 60 - // convert to { [rkey]: record } 61 - const records = room.data.records.reduce( 62 - (acc, record) => { 63 - acc[parseUri(record.uri).rkey] = record; 64 - return acc; 65 - }, 66 - {} as Record<string, ListRecord> 67 - ); 57 + let currentCursor = cursor; 58 + do { 59 + const response = await agent.com.atproto.repo.listRecords({ 60 + repo: did, 61 + collection, 62 + limit: limit || 100, 63 + cursor: currentCursor 64 + }); 65 + allRecords.push(...response.data.records); 66 + currentCursor = response.data.cursor; 67 + } while (currentCursor && (!limit || allRecords.length < limit)); 68 68 69 - return records; 69 + return allRecords; 70 70 } 71 - 72 - import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 73 - import { parseUri } from '$lib/website/utils'; 74 71 75 72 export async function getRecord({ 76 73 did, ··· 112 109 const response = await client.rpc.call('com.atproto.repo.putRecord', { 113 110 data: { 114 111 collection, 115 - repo: client.profile.did, 112 + repo: client.profile.did as At.Identifier, 116 113 rkey, 117 114 record: { 118 115 ...record ··· 137 134 const response = await client.rpc.call('com.atproto.repo.deleteRecord', { 138 135 data: { 139 136 collection, 140 - repo: did, 137 + repo: did as At.Identifier, 141 138 rkey 142 139 } 143 140 }); ··· 196 193 }); 197 194 198 195 return repo; 199 - } 196 + }
+34
src/lib/types.ts
··· 1 + import type { At } from '@atcute/client/lexicons'; 2 + import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 3 + 1 4 export type Item = { 2 5 id: string; 3 6 ··· 21 24 updatedAt?: string; 22 25 23 26 version?: number; 27 + 28 + page?: string; 29 + }; 30 + 31 + export type WebsiteData = { 32 + page: string; 33 + did: string; 34 + handle: string; 35 + 36 + cards: Item[]; 37 + publication: 38 + | { 39 + url?: string; 40 + name?: string; 41 + description?: string; 42 + icon?: At.Blob; 43 + preferences?: { 44 + hideProfile?: boolean; 45 + }; 46 + } 47 + | undefined; 48 + profile: ProfileViewDetailed; 49 + 50 + additionalData: Record<string, unknown>; 51 + updatedAt: number; 52 + version?: number; 53 + }; 54 + 55 + export type UserCache = { 56 + get: (key: string) => string; 57 + put: (key: string, value: string) => void; 24 58 };
+27
src/lib/website/Context.svelte
··· 1 + <script lang="ts"> 2 + import type { WebsiteData } from '$lib/types'; 3 + import type { Snippet } from 'svelte'; 4 + import { setAdditionalUserData, setCanEdit, setDidContext, setHandleContext } from './context'; 5 + import { dev } from '$app/environment'; 6 + import { client } from '$lib/oauth'; 7 + 8 + let { 9 + data, 10 + children 11 + }: { 12 + data: WebsiteData; 13 + children: Snippet<[]>; 14 + } = $props(); 15 + 16 + // svelte-ignore state_referenced_locally 17 + setAdditionalUserData(data.additionalData); 18 + 19 + setCanEdit(() => dev || (client.isLoggedIn && client.profile?.did === data.did)); 20 + 21 + // svelte-ignore state_referenced_locally 22 + setDidContext(data.did); 23 + // svelte-ignore state_referenced_locally 24 + setHandleContext(data.handle); 25 + </script> 26 + 27 + {@render children()}
+650
src/lib/website/EditableWebsite.svelte
··· 1 + <script lang="ts"> 2 + import { client, login } from '$lib/oauth/auth.svelte.js'; 3 + 4 + import { Navbar, Button, toast, Toaster, Toggle, Sidebar } from '@foxui/core'; 5 + import { BlueskyLogin } from '@foxui/social'; 6 + 7 + import { COLUMNS, margin, mobileMargin } from '$lib'; 8 + import { 9 + cardsEqual, 10 + clamp, 11 + compactItems, 12 + fixCollisions, 13 + getDescription, 14 + getHideProfile, 15 + getName, 16 + isTyping, 17 + setPositionOfNewItem 18 + } from '../helper'; 19 + import Profile from './Profile.svelte'; 20 + import type { Item, WebsiteData } from '../types'; 21 + import { deleteRecord, putRecord } from '../oauth/atproto'; 22 + import { innerWidth } from 'svelte/reactivity/window'; 23 + import { TID } from '@atproto/common-web'; 24 + import EditingCard from '../cards/Card/EditingCard.svelte'; 25 + import { AllCardDefinitions, CardDefinitionsByType } from '../cards'; 26 + import { tick, type Component } from 'svelte'; 27 + import type { CreationModalComponentProps } from '../cards/types'; 28 + import { dev } from '$app/environment'; 29 + import { setIsMobile } from './context'; 30 + import BaseEditingCard from '../cards/BaseCard/BaseEditingCard.svelte'; 31 + import Context from './Context.svelte'; 32 + import Settings from './Settings.svelte'; 33 + import Head from './Head.svelte'; 34 + 35 + let { 36 + data 37 + }: { 38 + data: WebsiteData; 39 + } = $props(); 40 + 41 + // svelte-ignore state_referenced_locally 42 + let items: Item[] = $state(data.cards); 43 + 44 + // svelte-ignore state_referenced_locally 45 + let publication = $state(JSON.stringify(data.publication)); 46 + 47 + let container: HTMLDivElement | undefined = $state(); 48 + 49 + let activeDragElement: { 50 + element: HTMLDivElement | null; 51 + item: Item | null; 52 + w: number; 53 + h: number; 54 + x: number; 55 + y: number; 56 + mouseDeltaX: number; 57 + mouseDeltaY: number; 58 + } = $state({ 59 + element: null, 60 + item: null, 61 + w: 0, 62 + h: 0, 63 + x: -1, 64 + y: -1, 65 + mouseDeltaX: 0, 66 + mouseDeltaY: 0 67 + }); 68 + 69 + let showingMobileView = $state(false); 70 + let isMobile = $derived(showingMobileView || (innerWidth.current ?? 1000) < 1024); 71 + 72 + setIsMobile(() => isMobile); 73 + 74 + const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y); 75 + const getH = (item: Item) => (isMobile ? (item.mobileH ?? item.h) : item.h); 76 + 77 + let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); 78 + 79 + function newCard(type: string = 'link') { 80 + // close sidebar if open 81 + const popover = document.getElementById('mobile-menu'); 82 + if (popover) { 83 + popover.hidePopover(); 84 + } 85 + 86 + let item: Item = { 87 + id: TID.nextStr(), 88 + x: 0, 89 + y: 0, 90 + w: 2, 91 + h: 2, 92 + mobileH: 4, 93 + mobileW: 4, 94 + mobileX: 0, 95 + mobileY: 0, 96 + cardType: type, 97 + cardData: {}, 98 + version: 2, 99 + page: data.page 100 + }; 101 + const cardDef = CardDefinitionsByType[type]; 102 + cardDef?.createNew?.(item); 103 + 104 + newItem.item = item; 105 + 106 + if (cardDef?.creationModalComponent) { 107 + newItem.modal = cardDef.creationModalComponent; 108 + } else { 109 + saveNewItem(); 110 + } 111 + } 112 + 113 + async function saveNewItem() { 114 + if (!newItem.item) return; 115 + const item = newItem.item; 116 + 117 + setPositionOfNewItem(item, items); 118 + 119 + items = [...items, item]; 120 + 121 + const containerRect = container?.getBoundingClientRect(); 122 + 123 + newItem = {}; 124 + 125 + await tick(); 126 + 127 + // scroll to newly created card 128 + if (!containerRect) return; 129 + const currentMargin = isMobile ? mobileMargin : margin; 130 + const currentY = isMobile ? item.mobileY : item.y; 131 + const bodyRect = document.body.getBoundingClientRect(); 132 + const offset = containerRect.top - bodyRect.top; 133 + const cellSize = (containerRect.width - currentMargin * 2) / COLUMNS; 134 + window.scrollTo({ top: offset + cellSize * (currentY - 1), behavior: 'smooth' }); 135 + } 136 + 137 + let isSaving = $state(false); 138 + 139 + let newItem: { modal?: Component<CreationModalComponentProps>; item?: Item } = $state({}); 140 + 141 + async function save() { 142 + isSaving = true; 143 + 144 + const promises = []; 145 + // find all cards that have been updated (where items differ from originalItems) 146 + for (let item of items) { 147 + const originalItem = data.cards.find((i) => cardsEqual(i, item)); 148 + 149 + if (!originalItem) { 150 + console.log('updated or new item', item); 151 + item.updatedAt = new Date().toISOString(); 152 + // run optional upload function for this card type 153 + const cardDef = CardDefinitionsByType[item.cardType]; 154 + 155 + if (cardDef?.upload) { 156 + item = await cardDef?.upload(item); 157 + } 158 + 159 + item.page = data.page; 160 + item.version = 2; 161 + 162 + promises.push( 163 + putRecord({ 164 + collection: 'app.blento.card', 165 + rkey: item.id, 166 + record: item 167 + }) 168 + ); 169 + } 170 + } 171 + 172 + // delete items that are in originalItems but not in items 173 + for (let originalItem of data.cards) { 174 + const item = items.find((i) => i.id === originalItem.id); 175 + if (!item) { 176 + console.log('deleting item', originalItem); 177 + promises.push( 178 + deleteRecord({ collection: 'app.blento.card', rkey: originalItem.id, did: data.did }) 179 + ); 180 + } 181 + } 182 + 183 + console.log(publication, data.publication); 184 + if (!publication || publication !== JSON.stringify(data.publication)) { 185 + data.publication ??= { 186 + name: getName(data), 187 + description: getDescription(data), 188 + preferences: { 189 + hideProfile: getHideProfile(data) 190 + } 191 + }; 192 + 193 + if (!data.publication.url) { 194 + data.publication.url = 'https://blento.app/' + data.handle; 195 + 196 + if (data.page !== 'blento.self') { 197 + data.publication.url += '/' + data.page.replace('blento.', ''); 198 + } 199 + } 200 + promises.push( 201 + putRecord({ 202 + collection: 'site.standard.publication', 203 + rkey: data.page, 204 + record: data.publication 205 + }) 206 + ); 207 + 208 + publication = JSON.stringify(data.publication); 209 + 210 + console.log('updating or adding publication', data.publication); 211 + } 212 + 213 + await Promise.all(promises); 214 + 215 + isSaving = false; 216 + 217 + fetch('/' + data.handle + '/api/refreshData').then(() => { 218 + console.log('data refreshed!'); 219 + }); 220 + console.log('refreshing data'); 221 + 222 + toast('Saved', { 223 + description: 'Your website has been saved!' 224 + }); 225 + } 226 + 227 + const sidebarItems = AllCardDefinitions.filter( 228 + (cardDef) => cardDef.sidebarComponent || cardDef.sidebarButtonText 229 + ); 230 + 231 + let showSettings = $state(false); 232 + 233 + let debugPoint = $state({ x: 0, y: 0 }); 234 + 235 + function getDragXY( 236 + e: DragEvent & { 237 + currentTarget: EventTarget & HTMLDivElement; 238 + } 239 + ) { 240 + if (!container) return; 241 + 242 + const x = e.clientX + activeDragElement.mouseDeltaX; 243 + const y = e.clientY + activeDragElement.mouseDeltaY; 244 + 245 + const rect = container.getBoundingClientRect(); 246 + 247 + debugPoint.x = x - rect.left; 248 + debugPoint.y = y - rect.top + margin; 249 + console.log(rect.top); 250 + 251 + let gridX = clamp( 252 + Math.floor(((x - rect.left) / rect.width) * 8), 253 + 0, 254 + COLUMNS - (activeDragElement.w ?? 0) 255 + ); 256 + gridX = Math.floor(gridX / 2) * 2; 257 + let gridY = Math.max( 258 + Math.round(((y - rect.top + margin) / (rect.width - margin)) * COLUMNS), 259 + 0 260 + ); 261 + if (isMobile) { 262 + gridX = Math.floor(gridX / 2) * 2; 263 + gridY = Math.floor(gridY / 2) * 2; 264 + } 265 + return { x: gridX, y: gridY }; 266 + } 267 + </script> 268 + 269 + <svelte:body 270 + onpaste={(event) => { 271 + if (isTyping()) return; 272 + 273 + const text = event.clipboardData?.getData('text/plain'); 274 + 275 + if (!text) return; 276 + 277 + try { 278 + const url = new URL(text); 279 + 280 + let item: Item = { 281 + id: TID.nextStr(), 282 + x: 0, 283 + y: 0, 284 + w: 2, 285 + h: 2, 286 + mobileH: 4, 287 + mobileW: 4, 288 + mobileX: 0, 289 + mobileY: 0, 290 + cardType: '', 291 + cardData: {} 292 + }; 293 + 294 + newItem.item = item; 295 + 296 + for (const cardDef of AllCardDefinitions) { 297 + if (cardDef.onUrlHandler?.(text, item)) { 298 + item.cardType = cardDef.type; 299 + saveNewItem(); 300 + return; 301 + } 302 + } 303 + 304 + newItem = {}; 305 + } catch (e) { 306 + return; 307 + } 308 + }} 309 + /> 310 + 311 + <Head 312 + favicon={data.profile.avatar ?? null} 313 + title={getName(data)} 314 + image={'/' + data.handle + '/og.png'} 315 + /> 316 + 317 + <Settings bind:open={showSettings} bind:data /> 318 + 319 + <Context {data}> 320 + <!-- <ImageDropper processImageFile={(file: File) => {}} /> --> 321 + 322 + {#if !dev} 323 + <div 324 + class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden" 325 + > 326 + Editing on mobile is not supported yet. Please use a desktop browser. 327 + </div> 328 + {/if} 329 + 330 + {#if showingMobileView} 331 + <div 332 + class="bg-base-200 dark:bg-base-900 pointer-events-none fixed inset-0 -z-10 h-full w-full" 333 + ></div> 334 + {/if} 335 + 336 + {#if newItem.modal && newItem.item} 337 + <newItem.modal 338 + oncreate={() => { 339 + saveNewItem(); 340 + }} 341 + bind:item={newItem.item} 342 + oncancel={() => { 343 + newItem = {}; 344 + }} 345 + /> 346 + {/if} 347 + 348 + <div 349 + class={[ 350 + '@container/wrapper relative w-full', 351 + showingMobileView 352 + ? 'bg-base-50 dark:bg-base-950 my-4 min-h-[calc(100dhv-2em)] rounded-2xl lg:mx-auto lg:w-[400px]' 353 + : '' 354 + ]} 355 + > 356 + {#if !getHideProfile(data)} 357 + <Profile {data} /> 358 + {/if} 359 + 360 + <div 361 + class={[ 362 + 'mx-auto max-w-lg', 363 + !getHideProfile(data) 364 + ? '@5xl/wrapper:grid @5xl/wrapper:max-w-7xl @5xl/wrapper:grid-cols-4' 365 + : '@5xl/wrapper:max-w-4xl' 366 + ]} 367 + > 368 + <div></div> 369 + <!-- svelte-ignore a11y_no_static_element_interactions --> 370 + <div 371 + bind:this={container} 372 + ondragover={(e) => { 373 + e.preventDefault(); 374 + 375 + const cell = getDragXY(e); 376 + if (!cell) return; 377 + 378 + activeDragElement.x = cell.x; 379 + activeDragElement.y = cell.y; 380 + 381 + if (activeDragElement.item) { 382 + if (isMobile) { 383 + activeDragElement.item.mobileX = cell.x; 384 + activeDragElement.item.mobileY = cell.y; 385 + } else { 386 + activeDragElement.item.x = cell.x; 387 + activeDragElement.item.y = cell.y; 388 + } 389 + 390 + fixCollisions(items, activeDragElement.item, isMobile); 391 + } 392 + 393 + // Auto-scroll when dragging near top or bottom of viewport 394 + const scrollZone = 100; 395 + const scrollSpeed = 10; 396 + const viewportHeight = window.innerHeight; 397 + 398 + if (e.clientY < scrollZone) { 399 + // Near top - scroll up 400 + const intensity = 1 - e.clientY / scrollZone; 401 + window.scrollBy(0, -scrollSpeed * intensity); 402 + } else if (e.clientY > viewportHeight - scrollZone) { 403 + // Near bottom - scroll down 404 + const intensity = 1 - (viewportHeight - e.clientY) / scrollZone; 405 + window.scrollBy(0, scrollSpeed * intensity); 406 + } 407 + }} 408 + ondragend={async (e) => { 409 + e.preventDefault(); 410 + const cell = getDragXY(e); 411 + if (!cell) return; 412 + 413 + if (activeDragElement.item) { 414 + if (isMobile) { 415 + activeDragElement.item.mobileX = cell.x; 416 + activeDragElement.item.mobileY = cell.y; 417 + } else { 418 + activeDragElement.item.x = cell.x; 419 + activeDragElement.item.y = cell.y; 420 + } 421 + 422 + fixCollisions(items, activeDragElement.item, isMobile); 423 + } 424 + activeDragElement.x = -1; 425 + activeDragElement.y = -1; 426 + activeDragElement.element = null; 427 + return true; 428 + }} 429 + class="@container/grid relative col-span-3 px-2 py-8 @5xl/wrapper:px-8" 430 + > 431 + {#each items as item, i (item.id)} 432 + <!-- {#if item !== activeDragElement.item} --> 433 + <BaseEditingCard 434 + bind:item={items[i]} 435 + ondelete={() => { 436 + items = items.filter((it) => it !== item); 437 + compactItems(items, isMobile); 438 + }} 439 + onsetsize={(newW: number, newH: number) => { 440 + if (isMobile) { 441 + item.mobileW = newW; 442 + item.mobileH = newH; 443 + } else { 444 + item.w = newW; 445 + item.h = newH; 446 + } 447 + 448 + fixCollisions(items, item, isMobile); 449 + }} 450 + ondragstart={(e) => { 451 + const target = e.currentTarget as HTMLDivElement; 452 + activeDragElement.element = target; 453 + activeDragElement.w = item.w; 454 + activeDragElement.h = item.h; 455 + activeDragElement.item = item; 456 + 457 + const rect = target.getBoundingClientRect(); 458 + activeDragElement.mouseDeltaX = rect.left - e.clientX; 459 + activeDragElement.mouseDeltaY = rect.top - e.clientY; 460 + console.log(activeDragElement.mouseDeltaY); 461 + console.log(rect.width); 462 + }} 463 + > 464 + <EditingCard bind:item={items[i]} /> 465 + </BaseEditingCard> 466 + <!-- {/if} --> 467 + {/each} 468 + 469 + <div style="height: {((maxHeight + 2) / 8) * 100}cqw;"></div> 470 + </div> 471 + </div> 472 + </div> 473 + 474 + <!-- <Settings bind:open={showSettings} /> --> 475 + 476 + <Sidebar mobileOnly mobileClasses="lg:block p-4 gap-4"> 477 + <div class="flex flex-col gap-2"> 478 + {#each sidebarItems as cardDef} 479 + {#if cardDef.sidebarComponent} 480 + <cardDef.sidebarComponent onclick={() => newCard(cardDef.type)} /> 481 + {:else if cardDef.sidebarButtonText} 482 + <Button onclick={() => newCard(cardDef.type)} variant="ghost" class="w-full justify-start" 483 + >{cardDef.sidebarButtonText}</Button 484 + > 485 + {/if} 486 + {/each} 487 + </div> 488 + </Sidebar> 489 + 490 + {#if dev || (!client.isLoggedIn && !client.isInitializing) || client.profile?.did === data.did} 491 + <Navbar 492 + class={[ 493 + 'dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 md:mx-auto lg:inline-flex', 494 + !dev ? 'hidden' : '' 495 + ]} 496 + > 497 + <div class="flex items-center gap-2"> 498 + {#if dev} 499 + <Button 500 + size="iconLg" 501 + variant="ghost" 502 + class="mr-4 backdrop-blur-none" 503 + onclick={() => { 504 + showSettings = true; 505 + }} 506 + > 507 + <svg 508 + xmlns="http://www.w3.org/2000/svg" 509 + fill="none" 510 + viewBox="0 0 24 24" 511 + stroke-width="1.5" 512 + stroke="currentColor" 513 + > 514 + <path 515 + stroke-linecap="round" 516 + stroke-linejoin="round" 517 + d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" 518 + /> 519 + <path 520 + stroke-linecap="round" 521 + stroke-linejoin="round" 522 + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 523 + /> 524 + </svg> 525 + </Button> 526 + {/if} 527 + 528 + <Button 529 + size="iconLg" 530 + variant="ghost" 531 + class="backdrop-blur-none" 532 + onclick={() => { 533 + newCard('text'); 534 + }} 535 + > 536 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" 537 + ><path 538 + fill="none" 539 + stroke="currentColor" 540 + stroke-linecap="round" 541 + stroke-linejoin="round" 542 + stroke-width="1.5" 543 + d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 544 + /></svg 545 + > 546 + </Button> 547 + <Button 548 + size="iconLg" 549 + variant="ghost" 550 + class="backdrop-blur-none" 551 + onclick={() => { 552 + newCard('link'); 553 + }} 554 + > 555 + <svg 556 + xmlns="http://www.w3.org/2000/svg" 557 + fill="none" 558 + viewBox="-2 -2 28 28" 559 + stroke-width="1.5" 560 + stroke="currentColor" 561 + > 562 + <path 563 + stroke-linecap="round" 564 + stroke-linejoin="round" 565 + d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 566 + /> 567 + </svg> 568 + </Button> 569 + 570 + <Button 571 + size="iconLg" 572 + variant="ghost" 573 + class="backdrop-blur-none" 574 + onclick={() => { 575 + newCard('image'); 576 + }} 577 + > 578 + <svg 579 + xmlns="http://www.w3.org/2000/svg" 580 + fill="none" 581 + viewBox="0 0 24 24" 582 + stroke-width="1.5" 583 + stroke="currentColor" 584 + > 585 + <path 586 + stroke-linecap="round" 587 + stroke-linejoin="round" 588 + d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 589 + /> 590 + </svg> 591 + </Button> 592 + 593 + <Button 594 + size="iconLg" 595 + variant="ghost" 596 + class="backdrop-blur-none" 597 + popovertarget="mobile-menu" 598 + > 599 + <svg 600 + xmlns="http://www.w3.org/2000/svg" 601 + fill="none" 602 + viewBox="0 0 24 24" 603 + stroke-width="1.5" 604 + stroke="currentColor" 605 + > 606 + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 607 + </svg> 608 + </Button> 609 + </div> 610 + <div class="flex items-center gap-2"> 611 + <Toggle 612 + class="hidden bg-transparent backdrop-blur-none lg:block dark:bg-transparent" 613 + bind:pressed={showingMobileView} 614 + > 615 + <svg 616 + xmlns="http://www.w3.org/2000/svg" 617 + fill="none" 618 + viewBox="0 0 24 24" 619 + stroke-width="1.5" 620 + stroke="currentColor" 621 + class="size-6" 622 + > 623 + <path 624 + stroke-linecap="round" 625 + stroke-linejoin="round" 626 + d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" 627 + /> 628 + </svg> 629 + </Toggle> 630 + {#if client.isLoggedIn} 631 + <Button 632 + disabled={isSaving} 633 + onclick={async () => { 634 + save(); 635 + }}>{isSaving ? 'Saving...' : 'Save'}</Button 636 + > 637 + {:else} 638 + <BlueskyLogin 639 + login={async (handle) => { 640 + await login(handle); 641 + return true; 642 + }} 643 + /> 644 + {/if} 645 + </div> 646 + </Navbar> 647 + {/if} 648 + 649 + <Toaster /> 650 + </Context>
+31
src/lib/website/Head.svelte
··· 1 + <script lang="ts"> 2 + let { 3 + favicon, 4 + title, 5 + image, 6 + description 7 + }: { favicon: string | null; title: string | null; image?: string; description?: string } = 8 + $props(); 9 + </script> 10 + 11 + <svelte:head> 12 + {#if favicon} 13 + <link rel="icon" href={favicon} /> 14 + {/if} 15 + 16 + {#if title} 17 + <title>{title}</title> 18 + {/if} 19 + 20 + {#if image} 21 + <meta property="og:image" content={image} /> 22 + <meta name="twitter:image" content={image} /> 23 + <meta name="twitter:card" content="summary_large_image" /> 24 + {/if} 25 + 26 + {#if description} 27 + <meta name="description" content={description} /> 28 + <meta property="og:description" content={description} /> 29 + <meta name="twitter:description" content={description} /> 30 + {/if} 31 + </svelte:head>
+120
src/lib/website/Profile.svelte
··· 1 + <script lang="ts"> 2 + import { marked } from 'marked'; 3 + import { client, login } from '../oauth'; 4 + import { Button } from '@foxui/core'; 5 + import { BlueskyLogin } from '@foxui/social'; 6 + import { env } from '$env/dynamic/public'; 7 + import type { WebsiteData } from '$lib/types'; 8 + import { getDescription, getName } from '$lib/helper'; 9 + 10 + let { 11 + data, 12 + showEditButton = false 13 + }: { 14 + data: WebsiteData; 15 + showEditButton?: boolean; 16 + } = $props(); 17 + 18 + const renderer = new marked.Renderer(); 19 + renderer.link = ({ href, title, text }) => 20 + `<a target="_blank" href="${href}" title="${title}">${text}</a>`; 21 + </script> 22 + 23 + <!-- lg:fixed lg:h-screen lg:w-1/4 lg:max-w-none lg:px-12 lg:pt-24 xl:w-1/3 --> 24 + <div 25 + class="mx-auto flex max-w-lg flex-col justify-between px-8 @5xl/wrapper:fixed @5xl/wrapper:h-screen @5xl/wrapper:w-1/4 @5xl/wrapper:max-w-none @5xl/wrapper:px-12" 26 + > 27 + <div class="flex flex-col gap-4 pt-16 pb-8 @5xl/wrapper:h-screen @5xl/wrapper:pt-24"> 28 + {#if data.profile.avatar} 29 + <img 30 + class="border-base-400 dark:border-base-800 size-32 rounded-full border @5xl/wrapper:size-44" 31 + src={data.profile.avatar} 32 + alt="" 33 + /> 34 + {:else} 35 + <div class="bg-base-300 dark:bg-base-700 size-32 rounded-full @5xl/wrapper:size-44"></div> 36 + {/if} 37 + 38 + <div class="text-4xl font-bold wrap-anywhere"> 39 + {getName(data)} 40 + </div> 41 + 42 + <div class="scrollbar -mx-4 flex-grow overflow-x-hidden overflow-y-scroll px-4"> 43 + <div 44 + class="text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline" 45 + > 46 + {@html marked.parse(getDescription(data), { 47 + renderer 48 + })} 49 + </div> 50 + </div> 51 + 52 + {#if showEditButton && client.isLoggedIn && client.profile?.did === data.did} 53 + <div> 54 + <Button href="{env.PUBLIC_IS_SELFHOSTED ? '' : client.profile?.handle}/edit" class="mt-2"> 55 + <svg 56 + xmlns="http://www.w3.org/2000/svg" 57 + fill="none" 58 + viewBox="0 0 24 24" 59 + stroke-width="1.5" 60 + stroke="currentColor" 61 + > 62 + <path 63 + stroke-linecap="round" 64 + stroke-linejoin="round" 65 + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" 66 + /> 67 + </svg> 68 + 69 + Edit Your Website</Button 70 + > 71 + </div> 72 + {:else} 73 + <div class="h-[42px] w-1 @5xl/wrapper:hidden"></div> 74 + {/if} 75 + 76 + {#if !env.PUBLIC_IS_SELFHOSTED && data.handle === 'blento.app' && client.profile?.handle !== data.handle} 77 + {#if !client.isInitializing && !client.isLoggedIn} 78 + <div> 79 + <div class="my-4 text-sm"> 80 + To create your own blento, sign in with your bluesky account 81 + </div> 82 + <BlueskyLogin 83 + login={async (handle) => { 84 + await login(handle); 85 + return true; 86 + }} 87 + /> 88 + </div> 89 + {:else if client.isLoggedIn} 90 + <div> 91 + <Button href={'/' + client.profile?.handle} class="mt-2"> 92 + <svg 93 + xmlns="http://www.w3.org/2000/svg" 94 + fill="none" 95 + viewBox="0 0 24 24" 96 + stroke-width="1.5" 97 + stroke="currentColor" 98 + > 99 + <path 100 + stroke-linecap="round" 101 + stroke-linejoin="round" 102 + d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" 103 + /> 104 + </svg> 105 + 106 + Open Your Blento</Button 107 + > 108 + </div> 109 + {/if} 110 + {/if} 111 + <div class="hidden text-xs font-light @5xl/wrapper:block"> 112 + made with <a 113 + href="https://blento.app" 114 + target="_blank" 115 + class="hover:text-accent-600 dark:hover:text-accent-400 font-medium transition-colors duration-200" 116 + >blento</a 117 + > 118 + </div> 119 + </div> 120 + </div>
+74
src/lib/website/Settings.svelte
··· 1 + <script lang="ts"> 2 + import { getDescription, getHideProfile, getName } from '$lib/helper'; 3 + import type { WebsiteData } from '$lib/types'; 4 + import { Button, Checkbox, Heading, Input, Label, Modal, Textarea } from '@foxui/core'; 5 + 6 + export type Settings = { 7 + title: string; 8 + }; 9 + 10 + let { open = $bindable(), data = $bindable() }: { open: boolean; data: WebsiteData } = $props(); 11 + 12 + let name = $state(getName(data)); 13 + 14 + $effect(() => { 15 + if (!open && name && name !== getName(data)) { 16 + data.publication ??= {}; 17 + data.publication.name = name; 18 + 19 + data = { ...data }; 20 + } 21 + }); 22 + </script> 23 + 24 + <Modal bind:open class="dark:bg-base-900"> 25 + <Heading>Settings</Heading> 26 + <Label>Name</Label> 27 + <Input bind:value={name} /> 28 + <Label class="mt-4">Description</Label> 29 + <Textarea 30 + rows={5} 31 + bind:value={ 32 + () => { 33 + return getDescription(data); 34 + }, 35 + (value) => { 36 + data.publication ??= {}; 37 + data.publication.description = value; 38 + 39 + data = { ...data }; 40 + } 41 + } 42 + /> 43 + 44 + <div class="flex items-center space-x-2"> 45 + <Checkbox 46 + bind:checked={ 47 + () => { 48 + return getHideProfile(data); 49 + }, 50 + (value) => { 51 + data.publication ??= {}; 52 + data.publication.preferences ??= {}; 53 + data.publication.preferences.hideProfile = value; 54 + 55 + data = { ...data }; 56 + } 57 + } 58 + id="hide-profile" 59 + aria-labelledby="hide-profile-label" 60 + variant="secondary" 61 + /> 62 + <Label 63 + id="hide-profile-label" 64 + for="hide-profile" 65 + class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 66 + > 67 + Hide Profile 68 + </Label> 69 + </div> 70 + 71 + <div class="flex w-full justify-end space-x-2"> 72 + <Button onclick={() => (open = false)}>Close</Button> 73 + </div> 74 + </Modal>
+72
src/lib/website/Website.svelte
··· 1 + <script lang="ts"> 2 + import Card from '../cards/Card/Card.svelte'; 3 + import Profile from './Profile.svelte'; 4 + import { getHideProfile, getName, sortItems } from '../helper'; 5 + import { innerWidth } from 'svelte/reactivity/window'; 6 + import { setDidContext, setHandleContext, setIsMobile } from './context'; 7 + import BaseCard from '../cards/BaseCard/BaseCard.svelte'; 8 + import type { WebsiteData } from '$lib/types'; 9 + import Context from './Context.svelte'; 10 + import Head from './Head.svelte'; 11 + 12 + let { data }: { data: WebsiteData } = $props(); 13 + 14 + let isMobile = $derived((innerWidth.current ?? 1000) < 1024); 15 + setIsMobile(() => isMobile); 16 + 17 + // svelte-ignore state_referenced_locally 18 + setDidContext(data.did); 19 + // svelte-ignore state_referenced_locally 20 + setHandleContext(data.handle); 21 + 22 + let maxHeight = $derived( 23 + data.cards.reduce( 24 + (max, item) => Math.max(max, isMobile ? item.mobileY + item.mobileH : item.y + item.h), 25 + 0 26 + ) 27 + ); 28 + 29 + let container: HTMLDivElement | undefined = $state(); 30 + </script> 31 + 32 + <Head 33 + favicon={data.profile.avatar ?? null} 34 + title={getName(data)} 35 + image={'/' + data.handle + '/og.png'} 36 + /> 37 + 38 + <Context {data}> 39 + <div class="@container/wrapper relative w-full"> 40 + {#if !getHideProfile(data)} 41 + <Profile {data} showEditButton={true} /> 42 + {/if} 43 + 44 + <div 45 + class={[ 46 + 'mx-auto max-w-lg', 47 + !getHideProfile(data) 48 + ? '@5xl/wrapper:grid @5xl/wrapper:max-w-7xl @5xl/wrapper:grid-cols-4' 49 + : '@5xl/wrapper:max-w-4xl' 50 + ]} 51 + > 52 + <div></div> 53 + <div bind:this={container} class="@container/grid relative col-span-3 px-2 py-8 lg:px-8"> 54 + {#each data.cards.toSorted(sortItems) as item} 55 + <BaseCard {item}> 56 + <Card {item} /> 57 + </BaseCard> 58 + {/each} 59 + <div style="height: {(maxHeight / 8) * 100}cqw;"></div> 60 + </div> 61 + </div> 62 + 63 + <div class="mx-auto block pb-8 text-center text-xs font-light @5xl/wrapper:hidden"> 64 + made with <a 65 + href="https://blento.app" 66 + target="_blank" 67 + class="hover:text-accent-600 dark:hover:text-accent-400 font-medium transition-colors duration-200" 68 + >blento</a 69 + > 70 + </div> 71 + </div> 72 + </Context>
+4 -14
src/lib/website/context.ts
··· 1 1 import { createContext } from 'svelte'; 2 - import type { DownloadedData } from './types'; 3 - 4 - export type UpdateFunction = () => boolean | Promise<boolean>; 5 - 6 - export type UpdateRecordFunction = () => Record<string, unknown> | Promise<Record<string, unknown>>; 7 - 8 - export const [getUpdateFunctionsContext, setUpdateFunctionsContext] = 9 - createContext<UpdateFunction[]>(); 10 - export const [getUpdateRecordFunctionsContext, setUpdateRecordFunctionsContext] = 11 - createContext<UpdateRecordFunction[]>(); 12 2 13 3 export const [getDidContext, setDidContext] = createContext<string>(); 14 4 export const [getHandleContext, setHandleContext] = createContext<string>(); 15 - 16 - export const [getDataContext, setDataContext] = createContext<DownloadedData>(); 17 - 18 - export const [isEditing, setIsEditing] = createContext<boolean>(); 5 + export const [getIsMobile, setIsMobile] = createContext<() => boolean>(); 6 + export const [getCanEdit, setCanEdit] = createContext<() => boolean>(); 7 + export const [getAdditionalUserData, setAdditionalUserData] = 8 + createContext<Record<string, unknown>>();
-9
src/lib/website/data.ts
··· 1 - export const image_collection = 'com.example.image' as const; 2 - 3 - // collections and records we want to grab 4 - export const data = { 5 - 'app.bsky.actor.profile': ['self'], 6 - 7 - 'app.blento.card': 'all', 8 - 'app.blento.settings': ['self'] 9 - } as const;
+106 -113
src/lib/website/load.ts
··· 1 - import { 2 - type Collection, 3 - type DownloadedData, 4 - type IndividualCollections, 5 - type ListCollections 6 - } from './types'; 7 - import { getRecord, listRecords, resolveHandle } from '$lib/oauth/atproto'; 1 + import { getProfile, listRecords, resolveHandle } from '$lib/oauth/atproto'; 8 2 import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 9 - import { data } from './data'; 10 3 import { CardDefinitionsByType } from '$lib/cards'; 11 - import type { Item } from '$lib/types'; 12 - import { compactItems, fixAllCollisions, fixCollisions } from '$lib/helper'; 4 + import type { Item, UserCache, WebsiteData } from '$lib/types'; 5 + import { compactItems, fixAllCollisions } from '$lib/helper'; 6 + import { parseUri } from '$lib/oauth/utils'; 7 + import { error } from '@sveltejs/kit'; 13 8 14 - type LoadedData = { 15 - did: string; 16 - data: DownloadedData; 17 - additionalData: Record<string, unknown>; 18 - updatedAt: number; 19 - }; 9 + const CURRENT_CACHE_VERSION = 1; 20 10 21 - export async function loadData( 22 - handle: string, 23 - platform?: App.Platform, 24 - forceUpdate: boolean = false 25 - ): Promise<LoadedData> { 26 - console.log(handle); 27 - if (!forceUpdate) { 28 - try { 29 - const cachedResult = await platform?.env?.USER_DATA_CACHE?.get(handle); 11 + export async function getCache(handle: string, page: string, cache?: UserCache) { 12 + try { 13 + const cachedResult = await cache?.get?.(handle); 14 + 15 + if (!cachedResult) return; 16 + const result = JSON.parse(cachedResult); 17 + const update = result.updatedAt; 18 + const timePassed = (Date.now() - update) / 1000; 30 19 31 - if (cachedResult) { 32 - const result = JSON.parse(cachedResult); 33 - const update = result.updatedAt; 34 - const timePassed = (Date.now() - update) / 1000; 35 - console.log( 36 - 'using cached result for handle', 37 - handle, 38 - 'last update', 39 - timePassed, 40 - 'seconds ago' 41 - ); 42 - return checkData(migrateData(JSON.parse(cachedResult))); 43 - } 44 - } catch (error) { 45 - console.log('getting cached result failed', error); 20 + if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 21 + console.log('skipping cache because of version mismatch'); 22 + return; 46 23 } 24 + 25 + result.page = 'blento.' + page; 26 + 27 + result.publication = (result.publications as ListRecord[]).find( 28 + (v) => parseUri(v.uri).rkey === result.page 29 + )?.value; 30 + 31 + delete result['publications']; 32 + 33 + console.log('using cached result for handle', handle, 'last update', timePassed, 'seconds ago'); 34 + return checkData(result); 35 + } catch (error) { 36 + console.log('getting cached result failed', error); 47 37 } 38 + } 48 39 49 - const did = await resolveHandle({ handle }); 40 + export async function loadData( 41 + handle: string, 42 + cache: UserCache | undefined, 43 + forceUpdate: boolean = false, 44 + page: string = 'self' 45 + ): Promise<WebsiteData> { 46 + if (!handle) throw error(404); 50 47 51 - const downloadedData = {} as DownloadedData; 48 + if (!forceUpdate) { 49 + const cachedResult = await getCache(handle, page, cache); 52 50 53 - const promises: { 54 - collection: string; 55 - rkey?: string; 56 - record: Promise<ListRecord> | Promise<Record<string, ListRecord>>; 57 - }[] = []; 51 + if (cachedResult) return cachedResult; 52 + } 58 53 59 - for (const collection of Object.keys(data) as Collection[]) { 60 - const cfg = data[collection]; 54 + if (handle === 'favicon.ico') throw error(404); 61 55 62 - try { 63 - if (Array.isArray(cfg)) { 64 - for (const rkey of cfg) { 65 - const record = getRecord({ did, collection, rkey }).catch((error) => { 66 - console.error('error getting record', rkey, 'for collection', collection); 67 - }); 68 - promises.push({ 69 - collection, 70 - rkey, 71 - record 72 - }); 73 - } 74 - } else if (cfg === 'all') { 75 - const records = listRecords({ did, collection }).catch((error) => { 76 - console.error('error getting records for collection', collection); 77 - }); 78 - promises.push({ collection, record: records }); 79 - } 80 - } catch (error) { 81 - console.error('failed getting', collection, cfg, error); 82 - } 83 - } 56 + console.log('resolving', handle); 57 + const did = await resolveHandle({ handle }); 84 58 85 - await Promise.all(promises.map((v) => v.record)); 59 + const cards = await listRecords({ did, collection: 'app.blento.card' }).catch(() => { 60 + console.error('error getting records for collection app.blento.card'); 61 + return [] as ListRecord[]; 62 + }); 86 63 87 - for (const promise of promises) { 88 - if (promise.rkey) { 89 - downloadedData[promise.collection as IndividualCollections] ??= {} as Record< 90 - string, 91 - ListRecord 92 - >; 93 - downloadedData[promise.collection as IndividualCollections][promise.rkey] = 94 - (await promise.record) as ListRecord; 95 - } else { 96 - downloadedData[promise.collection as ListCollections] ??= (await promise.record) as Record< 97 - string, 98 - ListRecord 99 - >; 64 + const publications = await listRecords({ did, collection: 'site.standard.publication' }).catch( 65 + () => { 66 + console.error('error getting records for collection site.standard.publication'); 67 + return [] as ListRecord[]; 100 68 } 101 - } 69 + ); 102 70 103 - const cardTypes = new Set( 104 - Object.values(downloadedData['app.blento.card']).map((v) => v.value.cardType) as string[] 105 - ); 71 + const profile = await getProfile({ did }); 106 72 73 + const cardTypes = new Set(cards.map((v) => v.value.cardType ?? '') as string[]); 107 74 const cardTypesArray = Array.from(cardTypes); 108 75 109 76 const additionDataPromises: Record<string, Promise<unknown>> = {}; 110 77 111 - const loadOptions = { did, handle, platform }; 78 + const loadOptions = { did, handle, cache }; 112 79 113 80 for (const cardType of cardTypesArray) { 114 81 const cardDef = CardDefinitionsByType[cardType]; 115 82 116 - if (cardDef.loadData) { 117 - additionDataPromises[cardType] = cardDef 118 - .loadData( 119 - Object.values(downloadedData['app.blento.card']) 120 - .filter((v) => cardType == v.value.cardType) 121 - .map((v) => v.value) as Item[], 122 - loadOptions 123 - ) 124 - .catch((error) => { 125 - console.error('error getting additional data for', cardType, error); 126 - }); 127 - } 83 + if (!cardDef.loadData) continue; 84 + 85 + additionDataPromises[cardType] = cardDef 86 + .loadData( 87 + cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[], 88 + loadOptions 89 + ) 90 + .catch((error: Error) => { 91 + console.error('error getting additional data for', cardType, error); 92 + }); 128 93 } 129 94 130 95 await Promise.all(Object.values(additionDataPromises)); ··· 139 104 } 140 105 141 106 const result = { 107 + page: 'blento.' + page, 108 + handle, 142 109 did, 143 - data: JSON.parse(JSON.stringify(downloadedData)) as DownloadedData, 110 + cards: (cards.map((v) => { 111 + return { ...v.value }; 112 + }) ?? []) as Item[], 113 + publications: publications, 144 114 additionalData, 145 - updatedAt: Date.now() 115 + profile, 116 + updatedAt: Date.now(), 117 + version: CURRENT_CACHE_VERSION 146 118 }; 147 119 148 - await platform?.env?.USER_DATA_CACHE?.put(handle, JSON.stringify(result)); 120 + const stringifiedResult = JSON.stringify(result); 121 + await cache?.put?.(handle, stringifiedResult); 122 + 123 + const parsedResult = JSON.parse(stringifiedResult); 124 + parsedResult.publication = (parsedResult.publications as ListRecord[]).find( 125 + (v) => parseUri(v.uri).rkey === parsedResult.page 126 + )?.value; 149 127 150 - return checkData(migrateData(result)); 128 + delete parsedResult['publications']; 129 + 130 + return checkData(parsedResult); 151 131 } 152 132 153 - function migrateFromV0ToV1(data: LoadedData): LoadedData { 154 - for (const card of Object.values(data.data['app.blento.card']).map((i) => i.value) as Item[]) { 133 + function migrateFromV0ToV1(data: WebsiteData): WebsiteData { 134 + for (const card of data.cards) { 155 135 if (card.version) continue; 156 136 card.x *= 2; 157 137 card.y *= 2; ··· 167 147 return data; 168 148 } 169 149 170 - function checkData(data: LoadedData): LoadedData { 171 - const cards = Object.values(data.data['app.blento.card']).map((i) => i.value) as Item[]; 150 + function migrateFromV1ToV2(data: WebsiteData): WebsiteData { 151 + for (const card of data.cards) { 152 + if (!card.version || card.version < 2) { 153 + card.page = 'blento.self'; 154 + card.version = 2; 155 + } 156 + } 157 + return data; 158 + } 159 + 160 + function checkData(data: WebsiteData): WebsiteData { 161 + data = migrateData(data); 162 + 163 + const cards = data.cards.filter((v) => v.page === data.page); 172 164 173 - console.log(cards); 174 165 if (cards.length > 0) { 175 166 fixAllCollisions(cards); 176 167 fixAllCollisions(cards, true); ··· 179 170 compactItems(cards, true); 180 171 } 181 172 173 + data.cards = cards; 174 + 182 175 return data; 183 176 } 184 177 185 - function migrateData(data: LoadedData): LoadedData { 186 - return migrateFromV0ToV1(data); 178 + function migrateData(data: WebsiteData): WebsiteData { 179 + return migrateFromV1ToV2(migrateFromV0ToV1(data)); 187 180 }
-16
src/lib/website/types.ts
··· 1 - import type { data } from './data'; 2 - import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 3 - 4 - export type Collection = keyof typeof data; 5 - 6 - export type IndividualCollections = { 7 - [K in Collection]: (typeof data)[K] extends readonly unknown[] ? K : never; 8 - }[Collection]; 9 - 10 - export type ListCollections = Exclude<Collection, IndividualCollections>; 11 - 12 - export type ElementType<C extends Collection> = (typeof data)[C] extends readonly (infer U)[] 13 - ? U 14 - : unknown; 15 - 16 - export type DownloadedData = { [C in Collection]: Record<string, ListRecord> };
src/lib/website/utils.ts src/lib/oauth/utils.ts
+3 -2
src/routes/+page.server.ts
··· 1 1 import { loadData } from '$lib/website/load'; 2 2 import { env } from '$env/dynamic/public'; 3 + import type { UserCache } from '$lib/types'; 3 4 4 5 export async function load({ platform, url }) { 5 6 const hostname = url.hostname; ··· 8 9 if (hostname === 'flo-bit.blento.app') { 9 10 handle = 'flo-bit.dev'; 10 11 } 12 + const cache = platform?.env?.USER_DATA_CACHE as unknown; 11 13 12 - const data = await loadData(handle, platform); 13 - return { ...data, handle }; 14 + return await loadData(handle, cache as UserCache); 14 15 }
+3 -12
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { refreshData, setAdditionalUserData } from '$lib/helper.js'; 3 - import { type Item } from '$lib/types.js'; 4 - import Website from '$lib/Website.svelte'; 2 + import { refreshData } from '$lib/helper.js'; 3 + import Website from '$lib/website/Website.svelte'; 5 4 import { onMount } from 'svelte'; 6 5 7 6 let { data } = $props(); 8 - 9 - // svelte-ignore state_referenced_locally 10 - setAdditionalUserData(data.additionalData); 11 7 12 8 onMount(() => { 13 9 refreshData(data); 14 10 }); 15 11 </script> 16 12 17 - <Website 18 - {data} 19 - handle={data.handle} 20 - did={data.did} 21 - items={Object.values(data.data['app.blento.card']).map((i) => i.value) as Item[]} 22 - /> 13 + <Website {data} />
-9
src/routes/[handle]/+layout.server.ts
··· 1 - import { loadData } from '$lib/website/load'; 2 - import { env } from '$env/dynamic/private'; 3 - import { error } from '@sveltejs/kit'; 4 - 5 - export async function load({ params, platform }) { 6 - if (env.PUBLIC_IS_SELFHOSTED) error(404); 7 - const data = await loadData(params.handle, platform); 8 - return { ...data, handle: params.handle }; 9 - }
-24
src/routes/[handle]/+page.svelte
··· 1 - <script lang="ts"> 2 - import { page } from '$app/state'; 3 - import { refreshData, setAdditionalUserData } from '$lib/helper.js'; 4 - import { type Item } from '$lib/types.js'; 5 - import Website from '$lib/Website.svelte'; 6 - import { onMount } from 'svelte'; 7 - 8 - let { data } = $props(); 9 - 10 - // svelte-ignore state_referenced_locally 11 - setAdditionalUserData(data.additionalData); 12 - 13 - onMount(() => { 14 - refreshData(data); 15 - }) 16 - </script> 17 - 18 - <Website 19 - {data} 20 - handle={page.params.handle} 21 - did={data.did} 22 - items={Object.values(data.data['app.blento.card']).map((i) => i.value) as Item[]} 23 - settings={data.data['app.blento.settings']?.['self']?.value} 24 - />
src/routes/[handle]/.well_known/site.standard.publication/+server.ts

This is a binary file and will not be displayed.

+14
src/routes/[handle]/[[page]]/+layout.server.ts
··· 1 + import { loadData } from '$lib/website/load'; 2 + import { env } from '$env/dynamic/private'; 3 + import { error } from '@sveltejs/kit'; 4 + import type { UserCache } from '$lib/types'; 5 + 6 + export async function load({ params, platform }) { 7 + if (env.PUBLIC_IS_SELFHOSTED) error(404); 8 + 9 + const cache = platform?.env?.USER_DATA_CACHE as unknown; 10 + 11 + console.log(params.page); 12 + 13 + return await loadData(params.handle, cache as UserCache, false, params.page); 14 + }
+13
src/routes/[handle]/[[page]]/+page.svelte
··· 1 + <script lang="ts"> 2 + import { refreshData } from '$lib/helper.js'; 3 + import Website from '$lib/website/Website.svelte'; 4 + import { onMount } from 'svelte'; 5 + 6 + let { data } = $props(); 7 + 8 + onMount(() => { 9 + refreshData(data); 10 + }); 11 + </script> 12 + 13 + <Website {data} />
+6
src/routes/[handle]/[[page]]/edit/+page.svelte
··· 1 + <script lang="ts"> 2 + import EditableWebsite from '$lib/website/EditableWebsite.svelte'; 3 + let { data } = $props(); 4 + </script> 5 + 6 + <EditableWebsite {data} />
+3 -1
src/routes/[handle]/api/refreshData/+server.ts
··· 1 + import type { UserCache } from '$lib/types'; 1 2 import { loadData } from '$lib/website/load.js'; 2 3 import { json } from '@sveltejs/kit'; 3 4 ··· 5 6 if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 6 7 const handle = params.handle; 7 8 8 - await loadData(handle, platform, true); 9 + const cache = platform?.env?.USER_DATA_CACHE as unknown; 10 + await loadData(handle, cache as UserCache, true); 9 11 10 12 return json('ok'); 11 13 }
-20
src/routes/[handle]/edit/+page.svelte
··· 1 - <script lang="ts"> 2 - import { page } from '$app/state'; 3 - import EditableWebsite from '$lib/EditableWebsite.svelte'; 4 - import { setAdditionalUserData } from '$lib/helper.js'; 5 - import { type Item } from '$lib/types.js'; 6 - 7 - let { data } = $props(); 8 - 9 - // svelte-ignore state_referenced_locally 10 - setAdditionalUserData(data.additionalData); 11 - </script> 12 - 13 - <EditableWebsite 14 - handle={page.params.handle} 15 - did={data.did} 16 - {data} 17 - items={Object.values(data.data['app.blento.card']).map((i) => i.value) as Item[]} 18 - 19 - settings={data.data['app.blento.settings']?.['self']?.value} 20 - />
+6 -7
src/routes/[handle]/og.png/+server.ts
··· 1 + import type { UserCache } from '$lib/types'; 1 2 import { loadData } from '$lib/website/load'; 2 3 import { ImageResponse } from '@ethercorps/sveltekit-og'; 3 4 4 5 export async function GET({ params, platform }) { 5 6 const handle = params.handle; 6 - const data = await loadData(params.handle, platform); 7 + 8 + const cache = platform?.env?.USER_DATA_CACHE as unknown; 9 + 10 + const data = await loadData(params.handle, cache as UserCache); 7 11 8 - console.log(data.data['app.bsky.actor.profile'].self); 9 - const image = 10 - 'https://cdn.bsky.app/img/avatar/plain/' + 11 - data.did + 12 - '/' + 13 - data.data['app.bsky.actor.profile'].self.value.avatar.ref.$link; 12 + const image = data.profile.avatar; 14 13 15 14 const htmlString = ` 16 15 <div class="flex flex-col p-8 w-full h-full bg-neutral-900">
+31
src/routes/api/update/+server.ts
··· 1 + import type { UserCache } from '$lib/types'; 2 + import { getCache, loadData } from '$lib/website/load'; 3 + import type { ProfileViewDetailed } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; 4 + import { json } from '@sveltejs/kit'; 5 + 6 + export async function GET({ platform }) { 7 + if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 8 + const existingUsers = await platform?.env?.USER_DATA_CACHE?.get('updatedBlentos'); 9 + 10 + const existingUsersArray: ProfileViewDetailed[] = existingUsers ? JSON.parse(existingUsers) : []; 11 + 12 + const existingUsersHandle = existingUsersArray.map((v) => v.handle); 13 + 14 + const cache = platform?.env?.USER_DATA_CACHE as unknown; 15 + 16 + for (const handle of existingUsersHandle) { 17 + if (!handle) continue; 18 + 19 + console.log('updating', handle); 20 + try { 21 + const cached = await getCache(handle, 'self', cache as UserCache); 22 + if (!cached) await loadData(handle, cache as UserCache, true); 23 + } catch (error) { 24 + console.error(error); 25 + return json('error'); 26 + } 27 + console.log('updated', handle); 28 + } 29 + 30 + return json('ok'); 31 + }
+4 -2
src/routes/edit/+page.server.ts
··· 1 1 import { loadData } from '$lib/website/load'; 2 2 import { env } from '$env/dynamic/public'; 3 + import type { UserCache } from '$lib/types'; 3 4 4 5 export async function load({ url, platform }) { 5 6 const hostname = url.hostname; ··· 9 10 handle = 'flo-bit.dev'; 10 11 } 11 12 12 - const data = await loadData(handle, platform); 13 - return { ...data, handle }; 13 + const cache = platform?.env?.USER_DATA_CACHE as unknown; 14 + 15 + return await loadData(handle, cache as UserCache); 14 16 }
+2 -12
src/routes/edit/+page.svelte
··· 1 1 <script lang="ts"> 2 - import EditableWebsite from '$lib/EditableWebsite.svelte'; 3 - import { setAdditionalUserData } from '$lib/helper.js'; 4 - import { type Item } from '$lib/types.js'; 2 + import EditableWebsite from '$lib/website/EditableWebsite.svelte'; 5 3 6 4 let { data } = $props(); 7 - 8 - // svelte-ignore state_referenced_locally 9 - setAdditionalUserData(data.additionalData); 10 5 </script> 11 6 12 - <EditableWebsite 13 - handle={data.handle} 14 - did={data.did} 15 - {data} 16 - items={Object.values(data.data['app.blento.card']).map((i) => i.value) as Item[]} 17 - /> 7 + <EditableWebsite {data} />
+3 -1
todo.md
··· 62 62 - github pages 63 63 64 64 - analytics (get page views) 65 - - custom subdomain 65 + - custom subdomain 66 + 67 + - dino game: fix fps dependency