your personal website on atproto - mirror blento.app

Compare changes

Choose any two refs to compare.

+1021 -1180
+17 -3
CLAUDE.md
··· 53 **Card System (`src/lib/cards/`):** 54 55 - `CardDefinition` type in `types.ts` defines the interface for card types 56 - - Each card type exports a definition with: `type`, `contentComponent`, optional `editingContentComponent`, `creationModalComponent`, `sidebarButtonText`, `loadData`, `upload` (see more info and description in `src/lib/cards/types.ts`) 57 - Card types include Text, Link, Image, Bluesky, Embed, Map, Livestream, ATProto collections, and special cards (see `src/lib/cards`). 58 - `AllCardDefinitions` and `CardDefinitionsByType` in `index.ts` aggregate all card types 59 - See e.g. `src/lib/cards/EmbedCard/` and `src/lib/cards/LivestreamCard/` for examples of implementation. ··· 66 - Data is stored in user's PDS under collection `app.blento.card` 67 - **Important**: ATProto does not allow floating point numbers in records. All numeric values must be integers. 68 69 **Data Loading (`src/lib/website/`):** 70 71 - - `load.ts` - Fetches user data from their PDS, with optional KV caching via `UserCache` 72 - `data.ts` - Defines which collections/records to fetch 73 - `context.ts` - Svelte contexts for passing DID, handle, and data down the component tree 74 ··· 80 - `/edit` - Self-hosted edit mode 81 - `/api/links` - Link preview API 82 - `/api/geocoding` - Geocoding API for map cards 83 - - `/api/instagram`, `/api/reloadRecent`, `/api/update` - Additional data endpoints 84 85 ### Item Type 86
··· 53 **Card System (`src/lib/cards/`):** 54 55 - `CardDefinition` type in `types.ts` defines the interface for card types 56 + - Each card type exports a definition with: `type`, `contentComponent`, optional `editingContentComponent`, `creationModalComponent`, `sidebarButtonText`, `loadData`, `loadDataServer`, `upload` (see more info and description in `src/lib/cards/types.ts`) 57 + - `loadData` fetches external data on the client (via remote functions). `loadDataServer` is the server-side equivalent used during SSR to avoid self-referential HTTP requests on Cloudflare Workers. 58 + - Cards that need external data use `.remote.ts` files (SvelteKit remote functions) co-located in the card folder (e.g. `GitHubProfileCard/api.remote.ts`, `LastFMCard/api.remote.ts`). These use `query()` from `$app/server` and run server-side, with SvelteKit generating fetch wrappers for client use. 59 - Card types include Text, Link, Image, Bluesky, Embed, Map, Livestream, ATProto collections, and special cards (see `src/lib/cards`). 60 - `AllCardDefinitions` and `CardDefinitionsByType` in `index.ts` aggregate all card types 61 - See e.g. `src/lib/cards/EmbedCard/` and `src/lib/cards/LivestreamCard/` for examples of implementation. ··· 68 - Data is stored in user's PDS under collection `app.blento.card` 69 - **Important**: ATProto does not allow floating point numbers in records. All numeric values must be integers. 70 71 + **Caching (`src/lib/cache.ts`):** 72 + 73 + - `CacheService` class wraps a single Cloudflare KV namespace (`USER_DATA_CACHE`) with namespaced keys 74 + - Keys are stored as `namespace:key` (e.g. `blento:did:plc:abc`, `github:someuser`, `lastfm:method:user:period:limit`) 75 + - Per-namespace default TTLs via KV `expirationTtl`: `blento` (24h), `identity` (7d), `github` (12h), `gh-contrib` (12h), `lastfm` (1h, overridable), `npmx` (12h), `meta` (no expiry) 76 + - Blento data is keyed by DID with bidirectional handleโ†”DID identity mappings (`identity:h:{handle}` โ†’ DID, `identity:d:{did}` โ†’ handle) 77 + - `getBlento(identifier)` accepts either a handle or DID and resolves automatically 78 + - `putBlento(did, handle, data)` writes data + both identity mappings 79 + - Generic `get(namespace, key)` / `put(namespace, key, value, ttl?)` and JSON variants `getJSON` / `putJSON` for all namespaces 80 + - `createCache(platform)` factory function creates a `CacheService` from `platform.env` 81 + - `CUSTOM_DOMAINS` KV namespace is separate and accessed directly for custom domain โ†’ DID resolution 82 + 83 **Data Loading (`src/lib/website/`):** 84 85 + - `load.ts` - Fetches user data from their PDS, with optional caching via `CacheService` 86 - `data.ts` - Defines which collections/records to fetch 87 - `context.ts` - Svelte contexts for passing DID, handle, and data down the component tree 88 ··· 94 - `/edit` - Self-hosted edit mode 95 - `/api/links` - Link preview API 96 - `/api/geocoding` - Geocoding API for map cards 97 + - `/api/reloadRecent`, `/api/update` - Additional data endpoints 98 99 ### Item Type 100
+1 -1
docs/Selfhosting.md
··· 35 6. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker 36 37 - github profile: GITHUB_TOKEN (token with public_repo access) 38 - - map: PUBLIC_MAPBOX_TOKEN
··· 35 6. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker 36 37 - github profile: GITHUB_TOKEN (token with public_repo access) 38 + - map: no token needed (uses OpenFreeMap)
+2 -1
package.json
··· 33 "prettier-plugin-tailwindcss": "^0.7.2", 34 "svelte": "^5.48.0", 35 "svelte-check": "^4.3.5", 36 "tailwindcss": "^4.1.18", 37 "typescript": "^5.9.3", 38 "typescript-eslint": "^8.53.1", ··· 76 "hls.js": "^1.6.15", 77 "leaflet": "^1.9.4", 78 "link-preview-js": "^4.0.0", 79 - "mapbox-gl": "^3.18.1", 80 "marked": "^17.0.1", 81 "perfect-freehand": "^1.2.2", 82 "plyr": "^3.8.4",
··· 33 "prettier-plugin-tailwindcss": "^0.7.2", 34 "svelte": "^5.48.0", 35 "svelte-check": "^4.3.5", 36 + "svelte-maplibre-gl": "^1.0.3", 37 "tailwindcss": "^4.1.18", 38 "typescript": "^5.9.3", 39 "typescript-eslint": "^8.53.1", ··· 77 "hls.js": "^1.6.15", 78 "leaflet": "^1.9.4", 79 "link-preview-js": "^4.0.0", 80 + "maplibre-gl": "^5.17.0", 81 "marked": "^17.0.1", 82 "perfect-freehand": "^1.2.2", 83 "plyr": "^3.8.4",
+112 -93
pnpm-lock.yaml
··· 119 link-preview-js: 120 specifier: ^4.0.0 121 version: 4.0.0 122 - mapbox-gl: 123 - specifier: ^3.18.1 124 - version: 3.18.1 125 marked: 126 specifier: ^17.0.1 127 version: 17.0.1 ··· 213 svelte-check: 214 specifier: ^4.3.5 215 version: 4.3.5(picomatch@4.0.3)(svelte@5.48.0)(typescript@5.9.3) 216 tailwindcss: 217 specifier: ^4.1.18 218 version: 4.1.18 ··· 947 '@jridgewell/trace-mapping@0.3.9': 948 resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 949 950 '@mapbox/jsonlint-lines-primitives@2.0.2': 951 - resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} 952 engines: {node: '>= 0.6'} 953 954 - '@mapbox/mapbox-gl-supported@3.0.0': 955 - resolution: {integrity: sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==} 956 - 957 '@mapbox/point-geometry@1.1.0': 958 - resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} 959 960 '@mapbox/tiny-sdf@2.0.7': 961 - resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==} 962 963 '@mapbox/unitbezier@0.0.1': 964 - resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} 965 966 '@mapbox/vector-tile@2.0.4': 967 - resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==} 968 969 '@mapbox/whoots-js@3.1.0': 970 - resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} 971 engines: {node: '>=6.0.0'} 972 973 '@mixmark-io/domino@2.2.0': 974 resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} ··· 1511 '@types/estree@1.0.8': 1512 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 1513 1514 - '@types/geojson-vt@3.2.5': 1515 - resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} 1516 - 1517 '@types/geojson@7946.0.16': 1518 - resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} 1519 1520 '@types/json-schema@7.0.15': 1521 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} ··· 1523 '@types/linkify-it@5.0.0': 1524 resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} 1525 1526 - '@types/mapbox__point-geometry@0.1.4': 1527 - resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} 1528 - 1529 '@types/markdown-it@14.1.2': 1530 resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} 1531 ··· 1534 1535 '@types/node@25.0.10': 1536 resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==} 1537 - 1538 - '@types/pbf@3.0.5': 1539 - resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} 1540 1541 '@types/stats.js@0.17.4': 1542 resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} 1543 1544 '@types/supercluster@7.1.3': 1545 - resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} 1546 1547 '@types/three@0.176.0': 1548 resolution: {integrity: sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==} ··· 1713 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 1714 engines: {node: '>=10'} 1715 1716 - cheap-ruler@4.0.0: 1717 - resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==} 1718 - 1719 cheerio-select@2.1.0: 1720 resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} 1721 ··· 1792 css.escape@1.5.1: 1793 resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} 1794 1795 - csscolorparser@1.0.3: 1796 - resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} 1797 - 1798 cssesc@3.0.0: 1799 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 1800 engines: {node: '>=4'} ··· 1853 resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} 1854 1855 earcut@3.0.2: 1856 - resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} 1857 1858 emoji-picker-element@1.28.1: 1859 resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==} ··· 2014 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 2015 os: [darwin] 2016 2017 - geojson-vt@4.0.2: 2018 - resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} 2019 2020 gl-matrix@3.4.4: 2021 - resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} 2022 2023 glob-parent@6.0.2: 2024 resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} ··· 2038 2039 graceful-fs@4.2.11: 2040 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 2041 - 2042 - grid-index@1.1.0: 2043 - resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} 2044 2045 gsap@3.14.2: 2046 resolution: {integrity: sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==} ··· 2125 json-stable-stringify-without-jsonify@1.0.1: 2126 resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 2127 2128 kdbush@4.0.2: 2129 - resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} 2130 2131 keyv@4.5.4: 2132 resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} ··· 2265 magic-string@0.30.21: 2266 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 2267 2268 - mapbox-gl@3.18.1: 2269 - resolution: {integrity: sha512-Izc8dee2zkmb6Pn9hXFbVioPRLXJz1OFUcrvri69MhFACPU4bhLyVmhEsD9AyW1qOAP0Yvhzm60v63xdMIHPPw==} 2270 2271 markdown-it@14.1.0: 2272 resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} ··· 2277 engines: {node: '>= 20'} 2278 hasBin: true 2279 2280 - martinez-polygon-clipping@0.8.1: 2281 - resolution: {integrity: sha512-9PLLMzMPI6ihHox4Ns6LpVBLpRc7sbhULybZ/wyaY8sY3ECNe2+hxm1hA2/9bEEpRrdpjoeduBuZLg2aq1cSIQ==} 2282 - 2283 mdurl@2.0.0: 2284 resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} 2285 ··· 2301 minimatch@9.0.5: 2302 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 2303 engines: {node: '>=16 || 14 >=14.17'} 2304 2305 mitt@3.0.1: 2306 resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} ··· 2328 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 2329 2330 murmurhash-js@1.0.0: 2331 - resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} 2332 2333 nanoid@3.3.11: 2334 resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} ··· 2409 resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 2410 2411 pbf@4.0.1: 2412 - resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==} 2413 hasBin: true 2414 2415 perfect-freehand@1.2.2: ··· 2471 engines: {node: ^10 || ^12 || >=14} 2472 2473 potpack@2.1.0: 2474 - resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} 2475 2476 prelude-ls@1.2.1: 2477 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} ··· 2605 resolution: {integrity: sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==} 2606 2607 protocol-buffers-schema@3.6.0: 2608 - resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} 2609 2610 punycode.js@2.3.1: 2611 resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} ··· 2623 resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==} 2624 2625 quickselect@3.0.0: 2626 - resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} 2627 2628 rangetouch@2.0.1: 2629 resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} ··· 2678 engines: {node: '>=4'} 2679 2680 resolve-protobuf-schema@2.1.0: 2681 - resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} 2682 - 2683 - robust-predicates@2.0.4: 2684 - resolution: {integrity: sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==} 2685 2686 rollup@4.56.0: 2687 resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==} ··· 2715 '@sveltejs/kit': 2716 optional: true 2717 2718 sade@1.8.1: 2719 resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 2720 engines: {node: '>=6'} ··· 2764 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 2765 engines: {node: '>=0.10.0'} 2766 2767 - splaytree@0.1.4: 2768 - resolution: {integrity: sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==} 2769 - 2770 std-env@3.10.0: 2771 resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 2772 ··· 2781 resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} 2782 2783 supercluster@8.0.1: 2784 - resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} 2785 2786 supports-color@10.2.2: 2787 resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} ··· 2808 svelte: 2809 optional: true 2810 2811 svelte-sonner@0.3.28: 2812 resolution: {integrity: sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==} 2813 peerDependencies: ··· 2902 engines: {node: '>=12.0.0'} 2903 2904 tinyqueue@3.0.0: 2905 - resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} 2906 2907 tlds@1.261.0: 2908 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} ··· 3724 '@jridgewell/resolve-uri': 3.1.2 3725 '@jridgewell/sourcemap-codec': 1.5.5 3726 3727 - '@mapbox/jsonlint-lines-primitives@2.0.2': {} 3728 3729 - '@mapbox/mapbox-gl-supported@3.0.0': {} 3730 3731 '@mapbox/point-geometry@1.1.0': {} 3732 ··· 3741 pbf: 4.0.1 3742 3743 '@mapbox/whoots-js@3.1.0': {} 3744 3745 '@mixmark-io/domino@2.2.0': {} 3746 ··· 4226 4227 '@types/estree@1.0.8': {} 4228 4229 - '@types/geojson-vt@3.2.5': 4230 - dependencies: 4231 - '@types/geojson': 7946.0.16 4232 - 4233 '@types/geojson@7946.0.16': {} 4234 4235 '@types/json-schema@7.0.15': {} 4236 4237 '@types/linkify-it@5.0.0': {} 4238 - 4239 - '@types/mapbox__point-geometry@0.1.4': {} 4240 4241 '@types/markdown-it@14.1.2': 4242 dependencies: ··· 4248 '@types/node@25.0.10': 4249 dependencies: 4250 undici-types: 7.16.0 4251 - 4252 - '@types/pbf@3.0.5': {} 4253 4254 '@types/stats.js@0.17.4': {} 4255 ··· 4463 ansi-styles: 4.3.0 4464 supports-color: 7.2.0 4465 4466 - cheap-ruler@4.0.0: {} 4467 - 4468 cheerio-select@2.1.0: 4469 dependencies: 4470 boolbase: 1.0.0 ··· 4554 css-what@6.2.2: {} 4555 4556 css.escape@1.5.1: {} 4557 - 4558 - csscolorparser@1.0.3: {} 4559 4560 cssesc@3.0.0: {} 4561 ··· 4820 fsevents@2.3.3: 4821 optional: true 4822 4823 - geojson-vt@4.0.2: {} 4824 4825 gl-matrix@3.4.4: {} 4826 ··· 4836 4837 graceful-fs@4.2.11: {} 4838 4839 - grid-index@1.1.0: {} 4840 - 4841 gsap@3.14.2: {} 4842 4843 has-flag@4.0.0: {} ··· 4906 json-schema-traverse@0.4.1: {} 4907 4908 json-stable-stringify-without-jsonify@1.0.1: {} 4909 4910 kdbush@4.0.2: {} 4911 ··· 5017 dependencies: 5018 '@jridgewell/sourcemap-codec': 1.5.5 5019 5020 - mapbox-gl@3.18.1: 5021 dependencies: 5022 '@mapbox/jsonlint-lines-primitives': 2.0.2 5023 - '@mapbox/mapbox-gl-supported': 3.0.0 5024 '@mapbox/point-geometry': 1.1.0 5025 '@mapbox/tiny-sdf': 2.0.7 5026 '@mapbox/unitbezier': 0.0.1 5027 '@mapbox/vector-tile': 2.0.4 5028 '@mapbox/whoots-js': 3.1.0 5029 '@types/geojson': 7946.0.16 5030 - '@types/geojson-vt': 3.2.5 5031 - '@types/mapbox__point-geometry': 0.1.4 5032 - '@types/pbf': 3.0.5 5033 '@types/supercluster': 7.1.3 5034 - cheap-ruler: 4.0.0 5035 - csscolorparser: 1.0.3 5036 earcut: 3.0.2 5037 - geojson-vt: 4.0.2 5038 gl-matrix: 3.4.4 5039 - grid-index: 1.1.0 5040 kdbush: 4.0.2 5041 - martinez-polygon-clipping: 0.8.1 5042 murmurhash-js: 1.0.0 5043 pbf: 4.0.1 5044 potpack: 2.1.0 ··· 5057 5058 marked@17.0.1: {} 5059 5060 - martinez-polygon-clipping@0.8.1: 5061 - dependencies: 5062 - robust-predicates: 2.0.4 5063 - splaytree: 0.1.4 5064 - tinyqueue: 3.0.0 5065 - 5066 mdurl@2.0.0: {} 5067 5068 meshoptimizer@0.18.1: {} ··· 5089 minimatch@9.0.5: 5090 dependencies: 5091 brace-expansion: 2.0.2 5092 5093 mitt@3.0.1: {} 5094 ··· 5439 dependencies: 5440 protocol-buffers-schema: 3.6.0 5441 5442 - robust-predicates@2.0.4: {} 5443 - 5444 rollup@4.56.0: 5445 dependencies: 5446 '@types/estree': 1.0.8 ··· 5497 svelte: 5.48.0 5498 optionalDependencies: 5499 '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) 5500 5501 sade@1.8.1: 5502 dependencies: ··· 5573 totalist: 3.0.1 5574 5575 source-map-js@1.2.1: {} 5576 - 5577 - splaytree@0.1.4: {} 5578 5579 std-env@3.10.0: {} 5580 ··· 5617 postcss-scss: 4.0.9(postcss@8.5.6) 5618 postcss-selector-parser: 7.1.1 5619 optionalDependencies: 5620 svelte: 5.48.0 5621 5622 svelte-sonner@0.3.28(svelte@5.48.0):
··· 119 link-preview-js: 120 specifier: ^4.0.0 121 version: 4.0.0 122 + maplibre-gl: 123 + specifier: ^5.17.0 124 + version: 5.17.0 125 marked: 126 specifier: ^17.0.1 127 version: 17.0.1 ··· 213 svelte-check: 214 specifier: ^4.3.5 215 version: 4.3.5(picomatch@4.0.3)(svelte@5.48.0)(typescript@5.9.3) 216 + svelte-maplibre-gl: 217 + specifier: ^1.0.3 218 + version: 1.0.3(maplibre-gl@5.17.0)(svelte@5.48.0) 219 tailwindcss: 220 specifier: ^4.1.18 221 version: 4.1.18 ··· 950 '@jridgewell/trace-mapping@0.3.9': 951 resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 952 953 + '@mapbox/geojson-rewind@0.5.2': 954 + resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==, tarball: https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz} 955 + hasBin: true 956 + 957 '@mapbox/jsonlint-lines-primitives@2.0.2': 958 + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==, tarball: https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz} 959 engines: {node: '>= 0.6'} 960 961 '@mapbox/point-geometry@1.1.0': 962 + resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==, tarball: https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz} 963 964 '@mapbox/tiny-sdf@2.0.7': 965 + resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==, tarball: https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz} 966 967 '@mapbox/unitbezier@0.0.1': 968 + resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==, tarball: https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz} 969 970 '@mapbox/vector-tile@2.0.4': 971 + resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==, tarball: https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz} 972 973 '@mapbox/whoots-js@3.1.0': 974 + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==, tarball: https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz} 975 engines: {node: '>=6.0.0'} 976 + 977 + '@maplibre/geojson-vt@5.0.4': 978 + resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==, tarball: https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz} 979 + 980 + '@maplibre/maplibre-gl-style-spec@24.4.1': 981 + resolution: {integrity: sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==, tarball: https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.4.1.tgz} 982 + hasBin: true 983 + 984 + '@maplibre/mlt@1.1.5': 985 + resolution: {integrity: sha512-HjoAIOYAfZo2El/tHBnvWpCRlodWgWLZ/tyO4Wrw0LVY2bkQz1+pwAwDvmgSRvfkSkSzWnqJJd1JwgCrq/60HQ==, tarball: https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.5.tgz} 986 + 987 + '@maplibre/vt-pbf@4.2.1': 988 + resolution: {integrity: sha512-IxZBGq/+9cqf2qdWlFuQ+ZfoMhWpxDUGQZ/poPHOJBvwMUT1GuxLo6HgYTou+xxtsOsjfbcjI8PZaPCtmt97rA==, tarball: https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.2.1.tgz} 989 990 '@mixmark-io/domino@2.2.0': 991 resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} ··· 1528 '@types/estree@1.0.8': 1529 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 1530 1531 '@types/geojson@7946.0.16': 1532 + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==, tarball: https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz} 1533 1534 '@types/json-schema@7.0.15': 1535 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} ··· 1537 '@types/linkify-it@5.0.0': 1538 resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} 1539 1540 '@types/markdown-it@14.1.2': 1541 resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} 1542 ··· 1545 1546 '@types/node@25.0.10': 1547 resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==} 1548 1549 '@types/stats.js@0.17.4': 1550 resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} 1551 1552 '@types/supercluster@7.1.3': 1553 + resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==, tarball: https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz} 1554 1555 '@types/three@0.176.0': 1556 resolution: {integrity: sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==} ··· 1721 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 1722 engines: {node: '>=10'} 1723 1724 cheerio-select@2.1.0: 1725 resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} 1726 ··· 1797 css.escape@1.5.1: 1798 resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} 1799 1800 cssesc@3.0.0: 1801 resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 1802 engines: {node: '>=4'} ··· 1855 resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} 1856 1857 earcut@3.0.2: 1858 + resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==, tarball: https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz} 1859 1860 emoji-picker-element@1.28.1: 1861 resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==} ··· 2016 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 2017 os: [darwin] 2018 2019 + get-stream@6.0.1: 2020 + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, tarball: https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz} 2021 + engines: {node: '>=10'} 2022 2023 gl-matrix@3.4.4: 2024 + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==, tarball: https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz} 2025 2026 glob-parent@6.0.2: 2027 resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} ··· 2041 2042 graceful-fs@4.2.11: 2043 resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 2044 2045 gsap@3.14.2: 2046 resolution: {integrity: sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==} ··· 2125 json-stable-stringify-without-jsonify@1.0.1: 2126 resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 2127 2128 + json-stringify-pretty-compact@4.0.0: 2129 + resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==, tarball: https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz} 2130 + 2131 kdbush@4.0.2: 2132 + resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==, tarball: https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz} 2133 2134 keyv@4.5.4: 2135 resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} ··· 2268 magic-string@0.30.21: 2269 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 2270 2271 + maplibre-gl@5.17.0: 2272 + resolution: {integrity: sha512-gwS6NpXBfWD406dtT5YfEpl2hmpMm+wcPqf04UAez/TxY1OBjiMdK2ZoMGcNIlGHelKc4+Uet6zhDdDEnlJVHA==, tarball: https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.17.0.tgz} 2273 + engines: {node: '>=16.14.0', npm: '>=8.1.0'} 2274 2275 markdown-it@14.1.0: 2276 resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} ··· 2281 engines: {node: '>= 20'} 2282 hasBin: true 2283 2284 mdurl@2.0.0: 2285 resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} 2286 ··· 2302 minimatch@9.0.5: 2303 resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 2304 engines: {node: '>=16 || 14 >=14.17'} 2305 + 2306 + minimist@1.2.8: 2307 + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, tarball: https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz} 2308 2309 mitt@3.0.1: 2310 resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} ··· 2332 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 2333 2334 murmurhash-js@1.0.0: 2335 + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==, tarball: https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz} 2336 2337 nanoid@3.3.11: 2338 resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} ··· 2413 resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 2414 2415 pbf@4.0.1: 2416 + resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==, tarball: https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz} 2417 hasBin: true 2418 2419 perfect-freehand@1.2.2: ··· 2475 engines: {node: ^10 || ^12 || >=14} 2476 2477 potpack@2.1.0: 2478 + resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==, tarball: https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz} 2479 2480 prelude-ls@1.2.1: 2481 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} ··· 2609 resolution: {integrity: sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==} 2610 2611 protocol-buffers-schema@3.6.0: 2612 + resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==, tarball: https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz} 2613 2614 punycode.js@2.3.1: 2615 resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} ··· 2627 resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==} 2628 2629 quickselect@3.0.0: 2630 + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==, tarball: https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz} 2631 2632 rangetouch@2.0.1: 2633 resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} ··· 2682 engines: {node: '>=4'} 2683 2684 resolve-protobuf-schema@2.1.0: 2685 + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==, tarball: https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz} 2686 2687 rollup@4.56.0: 2688 resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==} ··· 2716 '@sveltejs/kit': 2717 optional: true 2718 2719 + rw@1.3.3: 2720 + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==, tarball: https://registry.npmjs.org/rw/-/rw-1.3.3.tgz} 2721 + 2722 sade@1.8.1: 2723 resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 2724 engines: {node: '>=6'} ··· 2768 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 2769 engines: {node: '>=0.10.0'} 2770 2771 std-env@3.10.0: 2772 resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 2773 ··· 2782 resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} 2783 2784 supercluster@8.0.1: 2785 + resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==, tarball: https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz} 2786 2787 supports-color@10.2.2: 2788 resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} ··· 2809 svelte: 2810 optional: true 2811 2812 + svelte-maplibre-gl@1.0.3: 2813 + resolution: {integrity: sha512-pGZMLN5EcJoe6lREyaY2lsbkvrnKjIGHJRM2ijN+TUDeJbV9oecjBxbDXQZd8b5X4JJztSG915L731dFdhhSwA==, tarball: https://registry.npmjs.org/svelte-maplibre-gl/-/svelte-maplibre-gl-1.0.3.tgz} 2814 + peerDependencies: 2815 + maplibre-gl: ^5.0.0 || ^4.0.0 2816 + svelte: '>=5.0.0' 2817 + 2818 svelte-sonner@0.3.28: 2819 resolution: {integrity: sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==} 2820 peerDependencies: ··· 2909 engines: {node: '>=12.0.0'} 2910 2911 tinyqueue@3.0.0: 2912 + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==, tarball: https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz} 2913 2914 tlds@1.261.0: 2915 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} ··· 3731 '@jridgewell/resolve-uri': 3.1.2 3732 '@jridgewell/sourcemap-codec': 1.5.5 3733 3734 + '@mapbox/geojson-rewind@0.5.2': 3735 + dependencies: 3736 + get-stream: 6.0.1 3737 + minimist: 1.2.8 3738 3739 + '@mapbox/jsonlint-lines-primitives@2.0.2': {} 3740 3741 '@mapbox/point-geometry@1.1.0': {} 3742 ··· 3751 pbf: 4.0.1 3752 3753 '@mapbox/whoots-js@3.1.0': {} 3754 + 3755 + '@maplibre/geojson-vt@5.0.4': {} 3756 + 3757 + '@maplibre/maplibre-gl-style-spec@24.4.1': 3758 + dependencies: 3759 + '@mapbox/jsonlint-lines-primitives': 2.0.2 3760 + '@mapbox/unitbezier': 0.0.1 3761 + json-stringify-pretty-compact: 4.0.0 3762 + minimist: 1.2.8 3763 + quickselect: 3.0.0 3764 + rw: 1.3.3 3765 + tinyqueue: 3.0.0 3766 + 3767 + '@maplibre/mlt@1.1.5': 3768 + dependencies: 3769 + '@mapbox/point-geometry': 1.1.0 3770 + 3771 + '@maplibre/vt-pbf@4.2.1': 3772 + dependencies: 3773 + '@mapbox/point-geometry': 1.1.0 3774 + '@mapbox/vector-tile': 2.0.4 3775 + '@maplibre/geojson-vt': 5.0.4 3776 + '@types/geojson': 7946.0.16 3777 + '@types/supercluster': 7.1.3 3778 + pbf: 4.0.1 3779 + supercluster: 8.0.1 3780 3781 '@mixmark-io/domino@2.2.0': {} 3782 ··· 4262 4263 '@types/estree@1.0.8': {} 4264 4265 '@types/geojson@7946.0.16': {} 4266 4267 '@types/json-schema@7.0.15': {} 4268 4269 '@types/linkify-it@5.0.0': {} 4270 4271 '@types/markdown-it@14.1.2': 4272 dependencies: ··· 4278 '@types/node@25.0.10': 4279 dependencies: 4280 undici-types: 7.16.0 4281 4282 '@types/stats.js@0.17.4': {} 4283 ··· 4491 ansi-styles: 4.3.0 4492 supports-color: 7.2.0 4493 4494 cheerio-select@2.1.0: 4495 dependencies: 4496 boolbase: 1.0.0 ··· 4580 css-what@6.2.2: {} 4581 4582 css.escape@1.5.1: {} 4583 4584 cssesc@3.0.0: {} 4585 ··· 4844 fsevents@2.3.3: 4845 optional: true 4846 4847 + get-stream@6.0.1: {} 4848 4849 gl-matrix@3.4.4: {} 4850 ··· 4860 4861 graceful-fs@4.2.11: {} 4862 4863 gsap@3.14.2: {} 4864 4865 has-flag@4.0.0: {} ··· 4928 json-schema-traverse@0.4.1: {} 4929 4930 json-stable-stringify-without-jsonify@1.0.1: {} 4931 + 4932 + json-stringify-pretty-compact@4.0.0: {} 4933 4934 kdbush@4.0.2: {} 4935 ··· 5041 dependencies: 5042 '@jridgewell/sourcemap-codec': 1.5.5 5043 5044 + maplibre-gl@5.17.0: 5045 dependencies: 5046 + '@mapbox/geojson-rewind': 0.5.2 5047 '@mapbox/jsonlint-lines-primitives': 2.0.2 5048 '@mapbox/point-geometry': 1.1.0 5049 '@mapbox/tiny-sdf': 2.0.7 5050 '@mapbox/unitbezier': 0.0.1 5051 '@mapbox/vector-tile': 2.0.4 5052 '@mapbox/whoots-js': 3.1.0 5053 + '@maplibre/geojson-vt': 5.0.4 5054 + '@maplibre/maplibre-gl-style-spec': 24.4.1 5055 + '@maplibre/mlt': 1.1.5 5056 + '@maplibre/vt-pbf': 4.2.1 5057 '@types/geojson': 7946.0.16 5058 '@types/supercluster': 7.1.3 5059 earcut: 3.0.2 5060 gl-matrix: 3.4.4 5061 kdbush: 4.0.2 5062 murmurhash-js: 1.0.0 5063 pbf: 4.0.1 5064 potpack: 2.1.0 ··· 5077 5078 marked@17.0.1: {} 5079 5080 mdurl@2.0.0: {} 5081 5082 meshoptimizer@0.18.1: {} ··· 5103 minimatch@9.0.5: 5104 dependencies: 5105 brace-expansion: 2.0.2 5106 + 5107 + minimist@1.2.8: {} 5108 5109 mitt@3.0.1: {} 5110 ··· 5455 dependencies: 5456 protocol-buffers-schema: 3.6.0 5457 5458 rollup@4.56.0: 5459 dependencies: 5460 '@types/estree': 1.0.8 ··· 5511 svelte: 5.48.0 5512 optionalDependencies: 5513 '@sveltejs/kit': 2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) 5514 + 5515 + rw@1.3.3: {} 5516 5517 sade@1.8.1: 5518 dependencies: ··· 5589 totalist: 3.0.1 5590 5591 source-map-js@1.2.1: {} 5592 5593 std-env@3.10.0: {} 5594 ··· 5631 postcss-scss: 4.0.9(postcss@8.5.6) 5632 postcss-selector-parser: 7.1.1 5633 optionalDependencies: 5634 + svelte: 5.48.0 5635 + 5636 + svelte-maplibre-gl@1.0.3(maplibre-gl@5.17.0)(svelte@5.48.0): 5637 + dependencies: 5638 + maplibre-gl: 5.17.0 5639 svelte: 5.48.0 5640 5641 svelte-sonner@0.3.28(svelte@5.48.0):
+98
src/lib/cache.ts
···
··· 1 + import type { ActorIdentifier, Did } from '@atcute/lexicons'; 2 + import { isDid } from '@atcute/lexicons/syntax'; 3 + import type { KVNamespace } from '@cloudflare/workers-types'; 4 + 5 + /** TTL in seconds for each cache namespace */ 6 + const NAMESPACE_TTL = { 7 + blento: 60 * 60 * 24, // 24 hours 8 + identity: 60 * 60 * 24 * 7, // 7 days 9 + github: 60 * 60 * 12, // 12 hours 10 + 'gh-contrib': 60 * 60 * 12, // 12 hours 11 + lastfm: 60 * 60, // 1 hour (default, overridable per-put) 12 + npmx: 60 * 60 * 12, // 12 hours 13 + meta: 0 // no auto-expiry 14 + } as const; 15 + 16 + export type CacheNamespace = keyof typeof NAMESPACE_TTL; 17 + 18 + export class CacheService { 19 + constructor(private kv: KVNamespace) {} 20 + 21 + // === Generic namespaced operations === 22 + 23 + async get(namespace: CacheNamespace, key: string): Promise<string | null> { 24 + return this.kv.get(`${namespace}:${key}`); 25 + } 26 + 27 + async put( 28 + namespace: CacheNamespace, 29 + key: string, 30 + value: string, 31 + ttlSeconds?: number 32 + ): Promise<void> { 33 + const ttl = ttlSeconds ?? NAMESPACE_TTL[namespace] ?? 0; 34 + await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined); 35 + } 36 + 37 + async delete(namespace: CacheNamespace, key: string): Promise<void> { 38 + await this.kv.delete(`${namespace}:${key}`); 39 + } 40 + 41 + async list(namespace: CacheNamespace): Promise<string[]> { 42 + const prefix = `${namespace}:`; 43 + const result = await this.kv.list({ prefix }); 44 + return result.keys.map((k) => k.name.slice(prefix.length)); 45 + } 46 + 47 + // === JSON convenience === 48 + 49 + async getJSON<T = unknown>(namespace: CacheNamespace, key: string): Promise<T | null> { 50 + const raw = await this.get(namespace, key); 51 + if (!raw) return null; 52 + return JSON.parse(raw) as T; 53 + } 54 + 55 + async putJSON( 56 + namespace: CacheNamespace, 57 + key: string, 58 + value: unknown, 59 + ttlSeconds?: number 60 + ): Promise<void> { 61 + await this.put(namespace, key, JSON.stringify(value), ttlSeconds); 62 + } 63 + 64 + // === blento data (keyed by DID, with handleโ†”did resolution) === 65 + async getBlento(identifier: ActorIdentifier): Promise<string | null> { 66 + const did = await this.resolveDid(identifier); 67 + if (!did) return null; 68 + return this.get('blento', did); 69 + } 70 + 71 + async putBlento(did: string, handle: string, data: string): Promise<void> { 72 + await Promise.all([ 73 + this.put('blento', did, data), 74 + this.put('identity', `h:${handle}`, did), 75 + this.put('identity', `d:${did}`, handle) 76 + ]); 77 + } 78 + 79 + async listBlentos(): Promise<string[]> { 80 + return this.list('blento'); 81 + } 82 + 83 + // === Identity resolution === 84 + async resolveDid(identifier: ActorIdentifier): Promise<string | null> { 85 + if (isDid(identifier)) return identifier; 86 + return this.get('identity', `h:${identifier}`); 87 + } 88 + 89 + async resolveHandle(did: Did): Promise<string | null> { 90 + return this.get('identity', `d:${did}`); 91 + } 92 + } 93 + 94 + export function createCache(platform?: App.Platform): CacheService | undefined { 95 + const kv = platform?.env?.USER_DATA_CACHE; 96 + if (!kv) return undefined; 97 + return new CacheService(kv); 98 + }
+29 -154
src/lib/cards/core/MapCard/Map.svelte
··· 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 - import 'mapbox-gl/dist/mapbox-gl.css'; 4 - import mapboxgl from 'mapbox-gl'; 5 - import { env } from '$env/dynamic/public'; 6 import type { Item } from '$lib/types'; 7 - import { getHexOfCardColor } from '../../helper'; 8 9 let { item = $bindable() }: { item: Item } = $props(); 10 11 - // $inspect(item); 12 - 13 - let mapContainer: HTMLElement | undefined = $state(); 14 - let map: mapboxgl.Map | undefined = $state(); 15 - 16 - onMount(() => { 17 - if (!mapContainer || !env.PUBLIC_MAPBOX_TOKEN) { 18 - console.error('no map container or no mapbox token'); 19 - return; 20 - } 21 - 22 - try { 23 - mapboxgl.accessToken = env.PUBLIC_MAPBOX_TOKEN; 24 - 25 - const lat = parseFloat(item.cardData.lat); 26 - const lon = parseFloat(item.cardData.lon); 27 - const zoom = item.cardData.zoom ? parseFloat(item.cardData.zoom) : 0; 28 - const lightPreset = item.cardData.lightPreset || 'day'; 29 - 30 - map = new mapboxgl.Map({ 31 - container: mapContainer, 32 - style: 'mapbox://styles/mapbox/standard', 33 - center: [lon, lat], 34 - config: { 35 - basemap: { 36 - lightPreset: lightPreset, 37 - showPointOfInterestLabels: false 38 - } 39 - }, 40 - zoom: zoom, 41 - attributionControl: false, 42 - dragPan: false, 43 - dragRotate: false, 44 - keyboard: false, 45 - doubleClickZoom: true, 46 - touchZoomRotate: true, 47 - scrollZoom: true, 48 - boxZoom: false, 49 - pitchWithRotate: false, 50 - touchPitch: false 51 - }); 52 53 - // Keep location centered during zoom and save zoom level 54 - map.on('zoom', () => { 55 - if (map) { 56 - map.setCenter([lon, lat]); 57 - } 58 - }); 59 - 60 - map.on('zoomend', () => { 61 - if (map) { 62 - item.cardData.zoom = map.getZoom().toString(); 63 - } 64 - }); 65 - 66 - map.on('load', () => { 67 - if (!map) return; 68 69 - map.resize(); 70 - map.setCenter([lon, lat]); 71 - 72 - const accentColor = getHexOfCardColor(item); 73 - 74 - // Add location point source 75 - map.addSource('location-point', { 76 - type: 'geojson', 77 - data: { 78 - type: 'Feature', 79 - geometry: { 80 - type: 'Point', 81 - coordinates: [lon, lat] 82 - }, 83 - properties: { 84 - name: item.cardData.name || '' 85 - } 86 - } 87 - }); 88 - 89 - // Outer glow 90 - map.addLayer({ 91 - id: 'location-glow-outer', 92 - type: 'circle', 93 - source: 'location-point', 94 - paint: { 95 - 'circle-radius': 20, 96 - 'circle-color': accentColor, 97 - 'circle-opacity': 0.15, 98 - 'circle-blur': 1 99 - } 100 - }); 101 - 102 - // Middle glow 103 - map.addLayer({ 104 - id: 'location-glow-middle', 105 - type: 'circle', 106 - source: 'location-point', 107 - paint: { 108 - 'circle-radius': 12, 109 - 'circle-color': accentColor, 110 - 'circle-opacity': 0.3, 111 - 'circle-blur': 0.5 112 - } 113 - }); 114 - 115 - // White border 116 - map.addLayer({ 117 - id: 'location-dot-border', 118 - type: 'circle', 119 - source: 'location-point', 120 - paint: { 121 - 'circle-radius': 8, 122 - 'circle-color': '#ffffff', 123 - 'circle-opacity': 1 124 - } 125 - }); 126 - 127 - // Accent color center dot 128 - map.addLayer({ 129 - id: 'location-dot', 130 - type: 'circle', 131 - source: 'location-point', 132 - paint: { 133 - 'circle-radius': 6, 134 - 'circle-color': accentColor, 135 - 'circle-opacity': 1 136 - } 137 - }); 138 - }); 139 - 140 - // Handle container resize 141 - const resizeObserver = new ResizeObserver(() => { 142 - if (map) { 143 - map.resize(); 144 - map.setCenter([lon, lat]); 145 - } 146 - }); 147 - if (mapContainer) resizeObserver.observe(mapContainer); 148 - 149 - return () => { 150 - resizeObserver.disconnect(); 151 - if (map) { 152 - map.remove(); 153 - } 154 - }; 155 - } catch (err) { 156 - console.error(`Something went wrong trying to initialize the map`, err); 157 - } 158 - }); 159 - </script> 160 - 161 - <div bind:this={mapContainer} class="absolute inset-0 isolate h-full w-full"></div>
··· 1 <script lang="ts"> 2 import type { Item } from '$lib/types'; 3 + import { MapLibre, Projection, Marker } from 'svelte-maplibre-gl'; 4 5 let { item = $bindable() }: { item: Item } = $props(); 6 7 + let center = $state({ lng: parseFloat(item.cardData.lon), lat: parseFloat(item.cardData.lat) }); 8 + </script> 9 10 + <div class="absolute inset-0 isolate h-full w-full"> 11 + <MapLibre 12 + class="h-full w-full" 13 + style="https://tiles.openfreemap.org/styles/liberty" 14 + zoom={item.cardData.zoom} 15 + {center} 16 + attributionControl={{ compact: true }} 17 + dragPan={false} 18 + dragRotate={false} 19 + keyboard={false} 20 + touchZoomRotate={true} 21 + scrollZoom={true} 22 + boxZoom={false} 23 + pitchWithRotate={false} 24 + touchPitch={false} 25 + > 26 + <Projection type={'globe'} /> 27 28 + <Marker bind:lnglat={center}> 29 + {#snippet content()} 30 + <div class="from-accent-400 size-10 rounded-full bg-radial via-transparent p-3"> 31 + <div class="bg-accent-500 size-4 rounded-full ring-2 ring-white"></div> 32 + </div> 33 + {/snippet} 34 + </Marker> 35 + </MapLibre> 36 + </div>
+1 -1
src/lib/cards/core/MapCard/index.ts
··· 14 }, 15 16 creationModalComponent: CreateMapCardModal, 17 - allowSetColor: false, 18 canHaveLabel: true, 19 settingsComponent: MapCardSettings, 20
··· 14 }, 15 16 creationModalComponent: CreateMapCardModal, 17 + allowSetColor: true, 18 canHaveLabel: true, 19 settingsComponent: MapCardSettings, 20
+1 -1
src/lib/cards/core/SectionCard/EditingSectionCard.svelte
··· 9 10 <div 11 class={[ 12 - 'line-clamp-1 inline-flex h-full w-full rounded-md p-1 px-2 font-semibold', 13 textAlignClasses[item.cardData.textAlign as string], 14 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 15 textSizeClasses[(item.cardData.textSize ?? 1) as number]
··· 9 10 <div 11 class={[ 12 + 'line-clamp-1 inline-flex h-full w-full rounded-md py-2 px-4 font-semibold', 13 textAlignClasses[item.cardData.textAlign as string], 14 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 15 textSizeClasses[(item.cardData.textSize ?? 1) as number]
+1 -1
src/lib/cards/core/SectionCard/SectionCard.svelte
··· 12 13 <div 14 class={[ 15 - 'line-clamp-1 inline-flex h-full w-full rounded-md p-1 px-2 font-semibold', 16 textAlignClasses[item.cardData.textAlign as string], 17 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 18 textSizeClasses[(item.cardData.textSize ?? 1) as number]
··· 12 13 <div 14 class={[ 15 + 'line-clamp-1 inline-flex h-full w-full rounded-md py-2 px-4 font-semibold', 16 textAlignClasses[item.cardData.textAlign as string], 17 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 18 textSizeClasses[(item.cardData.textSize ?? 1) as number]
+1 -1
src/lib/cards/core/TextCard/EditingTextCard.svelte
··· 15 <!-- svelte-ignore a11y_click_events_have_key_events --> 16 <div 17 class={cn( 18 - 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-700/5 accent:hover:bg-accent-300/20 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex h-full w-full max-w-none cursor-text overflow-y-scroll rounded-md p-2 text-lg transition-colors duration-150', 19 textAlignClasses[item.cardData.textAlign as string], 20 verticalAlignClasses[item.cardData.verticalAlign as string], 21 textSizeClasses[(item.cardData.textSize ?? 0) as number]
··· 15 <!-- svelte-ignore a11y_click_events_have_key_events --> 16 <div 17 class={cn( 18 + 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 hover:bg-base-700/5 accent:hover:bg-accent-300/20 prose-p:first:mt-0 prose-p:last:mb-0 inline-flex h-full w-full max-w-none cursor-text overflow-y-scroll rounded-md px-6 py-4 text-lg transition-colors duration-150', 19 textAlignClasses[item.cardData.textAlign as string], 20 verticalAlignClasses[item.cardData.verticalAlign as string], 21 textSizeClasses[(item.cardData.textSize ?? 0) as number]
+1 -1
src/lib/cards/core/TextCard/TextCard.svelte
··· 14 15 <div 16 class={cn( 17 - 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 prose-headings:first:mt-0 prose-headings:last:mb-0 inline-flex h-full min-h-full w-full max-w-none overflow-x-hidden overflow-y-scroll rounded-md p-3 text-lg', 18 textAlignClasses?.[item.cardData.textAlign as string], 19 verticalAlignClasses[item.cardData.verticalAlign as string], 20 textSizeClasses[(item.cardData.textSize ?? 0) as number]
··· 14 15 <div 16 class={cn( 17 + 'prose dark:prose-invert prose-neutral prose-sm prose-a:no-underline prose-a:text-accent-600 dark:prose-a:text-accent-400 accent:prose-a:text-accent-950 accent:prose-a:underline accent:prose-p:text-base-900 prose-p:first:mt-0 prose-p:last:mb-0 prose-headings:first:mt-0 prose-headings:last:mb-0 inline-flex h-full min-h-full w-full max-w-none overflow-x-hidden overflow-y-scroll rounded-md px-6 py-4 text-lg', 18 textAlignClasses?.[item.cardData.textAlign as string], 19 verticalAlignClasses[item.cardData.verticalAlign as string], 20 textSizeClasses[(item.cardData.textSize ?? 0) as number]
+6 -5
src/lib/cards/media/LastFMCard/LastFMProfileCard/LastFMProfileCard.svelte
··· 4 import { getAdditionalUserData } from '$lib/website/context'; 5 import type { ContentComponentProps } from '../../../types'; 6 import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 7 8 interface UserInfo { 9 name: string; ··· 27 if (!item.cardData.lastfmUsername) return; 28 29 try { 30 - const response = await fetch( 31 - `/api/lastfm?method=user.getInfo&user=${encodeURIComponent(item.cardData.lastfmUsername)}` 32 - ); 33 - if (response.ok) { 34 - const result = await response.json(); 35 userInfo = result?.user; 36 data[cacheKey] = userInfo; 37 }
··· 4 import { getAdditionalUserData } from '$lib/website/context'; 5 import type { ContentComponentProps } from '../../../types'; 6 import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 7 + import { fetchLastFM } from '../api.remote'; 8 9 interface UserInfo { 10 name: string; ··· 28 if (!item.cardData.lastfmUsername) return; 29 30 try { 31 + const result = await fetchLastFM({ 32 + method: 'user.getInfo', 33 + user: item.cardData.lastfmUsername 34 + }); 35 + if (result) { 36 userInfo = result?.user; 37 data[cacheKey] = userInfo; 38 }
+17 -7
src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts
··· 1 import type { CardDefinition } from '../../../types'; 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 import LastFMProfileCard from './LastFMProfileCard.svelte'; 4 5 export const LastFMProfileCardDefinition = { 6 type: 'lastfmProfile', ··· 18 const username = item.cardData.lastfmUsername; 19 if (!username) continue; 20 try { 21 - const response = await fetch( 22 - `https://blento.app/api/lastfm?method=user.getInfo&user=${encodeURIComponent(username)}` 23 - ); 24 - if (!response.ok) continue; 25 - const text = await response.text(); 26 - const result = JSON.parse(text); 27 - allData[`lastfmProfile:${username}`] = result?.user; 28 } catch (error) { 29 console.error('Failed to fetch Last.fm profile:', error); 30 }
··· 1 import type { CardDefinition } from '../../../types'; 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 import LastFMProfileCard from './LastFMProfileCard.svelte'; 4 + import { fetchLastFM } from '../api.remote'; 5 6 export const LastFMProfileCardDefinition = { 7 type: 'lastfmProfile', ··· 19 const username = item.cardData.lastfmUsername; 20 if (!username) continue; 21 try { 22 + const data = await fetchLastFM({ method: 'user.getInfo', user: username }); 23 + if (data) allData[`lastfmProfile:${username}`] = data?.user; 24 + } catch (error) { 25 + console.error('Failed to fetch Last.fm profile:', error); 26 + } 27 + } 28 + return allData; 29 + }, 30 + loadDataServer: async (items) => { 31 + const allData: Record<string, unknown> = {}; 32 + for (const item of items) { 33 + const username = item.cardData.lastfmUsername; 34 + if (!username) continue; 35 + try { 36 + const data = await fetchLastFM({ method: 'user.getInfo', user: username }); 37 + if (data) allData[`lastfmProfile:${username}`] = data?.user; 38 } catch (error) { 39 console.error('Failed to fetch Last.fm profile:', error); 40 }
+6 -5
src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/LastFMRecentTracksCard.svelte
··· 4 import type { ContentComponentProps } from '../../../types'; 5 import LastFMAlbumArt from '../LastFMAlbumArt.svelte'; 6 import { RelativeTime } from '@foxui/time'; 7 8 interface Track { 9 name: string; ··· 29 if (!item.cardData.lastfmUsername) return; 30 31 try { 32 - const response = await fetch( 33 - `/api/lastfm?method=user.getRecentTracks&user=${encodeURIComponent(item.cardData.lastfmUsername)}&limit=50` 34 - ); 35 - if (response.ok) { 36 - const result = await response.json(); 37 tracks = result?.recenttracks?.track ?? []; 38 data[cacheKey] = tracks; 39 } else {
··· 4 import type { ContentComponentProps } from '../../../types'; 5 import LastFMAlbumArt from '../LastFMAlbumArt.svelte'; 6 import { RelativeTime } from '@foxui/time'; 7 + import { fetchLastFM } from '../api.remote'; 8 9 interface Track { 10 name: string; ··· 30 if (!item.cardData.lastfmUsername) return; 31 32 try { 33 + const result = await fetchLastFM({ 34 + method: 'user.getRecentTracks', 35 + user: item.cardData.lastfmUsername 36 + }); 37 + if (result) { 38 tracks = result?.recenttracks?.track ?? []; 39 data[cacheKey] = tracks; 40 } else {
+17 -7
src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts
··· 1 import type { CardDefinition } from '../../../types'; 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 import LastFMRecentTracksCard from './LastFMRecentTracksCard.svelte'; 4 5 export const LastFMRecentTracksCardDefinition = { 6 type: 'lastfmRecentTracks', ··· 18 const username = item.cardData.lastfmUsername; 19 if (!username) continue; 20 try { 21 - const response = await fetch( 22 - `https://blento.app/api/lastfm?method=user.getRecentTracks&user=${encodeURIComponent(username)}&limit=50` 23 - ); 24 - if (!response.ok) continue; 25 - const text = await response.text(); 26 - const result = JSON.parse(text); 27 - allData[`lastfmRecentTracks:${username}`] = result?.recenttracks?.track ?? []; 28 } catch (error) { 29 console.error('Failed to fetch Last.fm recent tracks:', error); 30 }
··· 1 import type { CardDefinition } from '../../../types'; 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 import LastFMRecentTracksCard from './LastFMRecentTracksCard.svelte'; 4 + import { fetchLastFM } from '../api.remote'; 5 6 export const LastFMRecentTracksCardDefinition = { 7 type: 'lastfmRecentTracks', ··· 19 const username = item.cardData.lastfmUsername; 20 if (!username) continue; 21 try { 22 + const data = await fetchLastFM({ method: 'user.getRecentTracks', user: username }); 23 + if (data) allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; 24 + } catch (error) { 25 + console.error('Failed to fetch Last.fm recent tracks:', error); 26 + } 27 + } 28 + return allData; 29 + }, 30 + loadDataServer: async (items) => { 31 + const allData: Record<string, unknown> = {}; 32 + for (const item of items) { 33 + const username = item.cardData.lastfmUsername; 34 + if (!username) continue; 35 + try { 36 + const data = await fetchLastFM({ method: 'user.getRecentTracks', user: username }); 37 + if (data) allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; 38 } catch (error) { 39 console.error('Failed to fetch Last.fm recent tracks:', error); 40 }
+7 -5
src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/LastFMTopAlbumsCard.svelte
··· 3 import type { ContentComponentProps } from '../../../types'; 4 import { getAdditionalUserData } from '$lib/website/context'; 5 import ImageGrid from '$lib/components/ImageGrid.svelte'; 6 7 interface Album { 8 name: string; ··· 30 loading = true; 31 32 try { 33 - const response = await fetch( 34 - `/api/lastfm?method=user.getTopAlbums&user=${encodeURIComponent(item.cardData.lastfmUsername)}&period=${period}&limit=50` 35 - ); 36 - if (response.ok) { 37 - const result = await response.json(); 38 albums = result?.topalbums?.album ?? []; 39 data[cacheKey] = albums; 40 } else {
··· 3 import type { ContentComponentProps } from '../../../types'; 4 import { getAdditionalUserData } from '$lib/website/context'; 5 import ImageGrid from '$lib/components/ImageGrid.svelte'; 6 + import { fetchLastFM } from '../api.remote'; 7 8 interface Album { 9 name: string; ··· 31 loading = true; 32 33 try { 34 + const result = await fetchLastFM({ 35 + method: 'user.getTopAlbums', 36 + user: item.cardData.lastfmUsername, 37 + period 38 + }); 39 + if (result) { 40 albums = result?.topalbums?.album ?? []; 41 data[cacheKey] = albums; 42 } else {
+18 -7
src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts
··· 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 import LastFMTopAlbumsCard from './LastFMTopAlbumsCard.svelte'; 4 import LastFMTopAlbumsCardSettings from './LastFMTopAlbumsCardSettings.svelte'; 5 6 export const LastFMTopAlbumsCardDefinition = { 7 type: 'lastfmTopAlbums', ··· 22 const period = item.cardData.period ?? '7day'; 23 if (!username) continue; 24 try { 25 - const response = await fetch( 26 - `https://blento.app/api/lastfm?method=user.getTopAlbums&user=${encodeURIComponent(username)}&period=${period}&limit=50` 27 - ); 28 - if (!response.ok) continue; 29 - const text = await response.text(); 30 - const result = JSON.parse(text); 31 - allData[`lastfmTopAlbums:${username}:${period}`] = result?.topalbums?.album ?? []; 32 } catch (error) { 33 console.error('Failed to fetch Last.fm top albums:', error); 34 }
··· 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 import LastFMTopAlbumsCard from './LastFMTopAlbumsCard.svelte'; 4 import LastFMTopAlbumsCardSettings from './LastFMTopAlbumsCardSettings.svelte'; 5 + import { fetchLastFM } from '../api.remote'; 6 7 export const LastFMTopAlbumsCardDefinition = { 8 type: 'lastfmTopAlbums', ··· 23 const period = item.cardData.period ?? '7day'; 24 if (!username) continue; 25 try { 26 + const data = await fetchLastFM({ method: 'user.getTopAlbums', user: username, period }); 27 + if (data) allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; 28 + } catch (error) { 29 + console.error('Failed to fetch Last.fm top albums:', error); 30 + } 31 + } 32 + return allData; 33 + }, 34 + loadDataServer: async (items) => { 35 + const allData: Record<string, unknown> = {}; 36 + for (const item of items) { 37 + const username = item.cardData.lastfmUsername; 38 + const period = item.cardData.period ?? '7day'; 39 + if (!username) continue; 40 + try { 41 + const data = await fetchLastFM({ method: 'user.getTopAlbums', user: username, period }); 42 + if (data) allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; 43 } catch (error) { 44 console.error('Failed to fetch Last.fm top albums:', error); 45 }
+7 -5
src/lib/cards/media/LastFMCard/LastFMTopTracksCard/LastFMTopTracksCard.svelte
··· 3 import { getAdditionalUserData } from '$lib/website/context'; 4 import type { ContentComponentProps } from '../../../types'; 5 import LastFMAlbumArt from '../LastFMAlbumArt.svelte'; 6 7 interface Track { 8 name: string; ··· 29 loading = true; 30 31 try { 32 - const response = await fetch( 33 - `/api/lastfm?method=user.getTopTracks&user=${encodeURIComponent(item.cardData.lastfmUsername)}&period=${period}&limit=50` 34 - ); 35 - if (response.ok) { 36 - const result = await response.json(); 37 tracks = result?.toptracks?.track ?? []; 38 data[cacheKey] = tracks; 39 } else {
··· 3 import { getAdditionalUserData } from '$lib/website/context'; 4 import type { ContentComponentProps } from '../../../types'; 5 import LastFMAlbumArt from '../LastFMAlbumArt.svelte'; 6 + import { fetchLastFM } from '../api.remote'; 7 8 interface Track { 9 name: string; ··· 30 loading = true; 31 32 try { 33 + const result = await fetchLastFM({ 34 + method: 'user.getTopTracks', 35 + user: item.cardData.lastfmUsername, 36 + period 37 + }); 38 + if (result) { 39 tracks = result?.toptracks?.track ?? []; 40 data[cacheKey] = tracks; 41 } else {
+18 -7
src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts
··· 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 import LastFMPeriodSettings from '../LastFMPeriodSettings.svelte'; 4 import LastFMTopTracksCard from './LastFMTopTracksCard.svelte'; 5 6 export const LastFMTopTracksCardDefinition = { 7 type: 'lastfmTopTracks', ··· 22 const period = item.cardData.period ?? '7day'; 23 if (!username) continue; 24 try { 25 - const response = await fetch( 26 - `https://blento.app/api/lastfm?method=user.getTopTracks&user=${encodeURIComponent(username)}&period=${period}&limit=50` 27 - ); 28 - if (!response.ok) continue; 29 - const text = await response.text(); 30 - const result = JSON.parse(text); 31 - allData[`lastfmTopTracks:${username}:${period}`] = result?.toptracks?.track ?? []; 32 } catch (error) { 33 console.error('Failed to fetch Last.fm top tracks:', error); 34 }
··· 2 import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; 3 import LastFMPeriodSettings from '../LastFMPeriodSettings.svelte'; 4 import LastFMTopTracksCard from './LastFMTopTracksCard.svelte'; 5 + import { fetchLastFM } from '../api.remote'; 6 7 export const LastFMTopTracksCardDefinition = { 8 type: 'lastfmTopTracks', ··· 23 const period = item.cardData.period ?? '7day'; 24 if (!username) continue; 25 try { 26 + const data = await fetchLastFM({ method: 'user.getTopTracks', user: username, period }); 27 + if (data) allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; 28 + } catch (error) { 29 + console.error('Failed to fetch Last.fm top tracks:', error); 30 + } 31 + } 32 + return allData; 33 + }, 34 + loadDataServer: async (items) => { 35 + const allData: Record<string, unknown> = {}; 36 + for (const item of items) { 37 + const username = item.cardData.lastfmUsername; 38 + const period = item.cardData.period ?? '7day'; 39 + if (!username) continue; 40 + try { 41 + const data = await fetchLastFM({ method: 'user.getTopTracks', user: username, period }); 42 + if (data) allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; 43 } catch (error) { 44 console.error('Failed to fetch Last.fm top tracks:', error); 45 }
+59
src/lib/cards/media/LastFMCard/api.remote.ts
···
··· 1 + import { query, getRequestEvent } from '$app/server'; 2 + import { env } from '$env/dynamic/private'; 3 + import { createCache } from '$lib/cache'; 4 + 5 + const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; 6 + 7 + const CACHE_TTL: Record<string, number> = { 8 + 'user.getRecentTracks': 15 * 60, 9 + 'user.getTopTracks': 60 * 60, 10 + 'user.getTopAlbums': 60 * 60, 11 + 'user.getInfo': 12 * 60 * 60 12 + }; 13 + 14 + export const fetchLastFM = query( 15 + 'unchecked', 16 + async ({ 17 + method, 18 + user, 19 + period = '7day', 20 + limit = '50' 21 + }: { 22 + method: string; 23 + user: string; 24 + period?: string; 25 + limit?: string; 26 + }) => { 27 + const apiKey = env?.LASTFM_API_KEY; 28 + if (!apiKey) return undefined; 29 + 30 + const { platform } = getRequestEvent(); 31 + const cache = createCache(platform); 32 + 33 + const cacheKey = `${method}:${user}:${period}:${limit}`; 34 + const cached = await cache?.get('lastfm', cacheKey); 35 + if (cached) return JSON.parse(cached); 36 + 37 + const params = new URLSearchParams({ 38 + method, 39 + user, 40 + api_key: apiKey, 41 + format: 'json', 42 + limit 43 + }); 44 + 45 + if (method === 'user.getTopTracks' || method === 'user.getTopAlbums') { 46 + params.set('period', period); 47 + } 48 + 49 + const response = await fetch(`${LASTFM_API_URL}?${params}`); 50 + if (!response.ok) return undefined; 51 + 52 + const data = await response.json(); 53 + if (data.error) return undefined; 54 + 55 + const ttl = CACHE_TTL[method] || 60 * 60; 56 + await cache?.put('lastfm', cacheKey, JSON.stringify(data), ttl); 57 + return data; 58 + } 59 + );
+3 -7
src/lib/cards/social/GitHubContributorsCard/GitHubContributorsCard.svelte
··· 4 import { getAdditionalUserData, getCanEdit } from '$lib/website/context'; 5 import type { GitHubContributor, GitHubContributorsLoadedData } from '.'; 6 import ImageGrid from '$lib/components/ImageGrid.svelte'; 7 8 let { item }: ContentComponentProps = $props(); 9 ··· 41 async function loadContributors() { 42 if (!owner || !repo) return; 43 try { 44 - const response = await fetch( 45 - `/api/github/contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 46 - ); 47 - if (response.ok) { 48 - const data = await response.json(); 49 - clientContributors = data; 50 - } 51 } catch (error) { 52 console.error('Failed to fetch GitHub contributors:', error); 53 }
··· 4 import { getAdditionalUserData, getCanEdit } from '$lib/website/context'; 5 import type { GitHubContributor, GitHubContributorsLoadedData } from '.'; 6 import ImageGrid from '$lib/components/ImageGrid.svelte'; 7 + import { fetchGitHubContributors } from './api.remote'; 8 9 let { item }: ContentComponentProps = $props(); 10 ··· 42 async function loadContributors() { 43 if (!owner || !repo) return; 44 try { 45 + const data = await fetchGitHubContributors({ owner, repo }); 46 + if (data) clientContributors = data; 47 } catch (error) { 48 console.error('Failed to fetch GitHub contributors:', error); 49 }
+26
src/lib/cards/social/GitHubContributorsCard/api.remote.ts
···
··· 1 + import { query, getRequestEvent } from '$app/server'; 2 + import { createCache } from '$lib/cache'; 3 + 4 + const GITHUB_CONTRIBUTORS_API_URL = 5 + 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; 6 + 7 + export const fetchGitHubContributors = query( 8 + 'unchecked', 9 + async ({ owner, repo }: { owner: string; repo: string }) => { 10 + const { platform } = getRequestEvent(); 11 + const cache = createCache(platform); 12 + 13 + const key = `${owner}/${repo}`; 14 + const cached = await cache?.get('gh-contrib', key); 15 + if (cached) return JSON.parse(cached); 16 + 17 + const response = await fetch( 18 + `${GITHUB_CONTRIBUTORS_API_URL}?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 19 + ); 20 + if (!response.ok) return undefined; 21 + 22 + const data = await response.json(); 23 + await cache?.put('gh-contrib', key, JSON.stringify(data)); 24 + return data; 25 + } 26 + );
+19 -6
src/lib/cards/social/GitHubContributorsCard/index.ts
··· 2 import GitHubContributorsCard from './GitHubContributorsCard.svelte'; 3 import CreateGitHubContributorsCardModal from './CreateGitHubContributorsCardModal.svelte'; 4 import GitHubContributorsCardSettings from './GitHubContributorsCardSettings.svelte'; 5 6 export type GitHubContributor = { 7 username: string; ··· 33 const key = `${owner}/${repo}`; 34 if (contributorsData[key]) continue; 35 try { 36 - const response = await fetch( 37 - `https://blento.app/api/github/contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 38 - ); 39 - if (response.ok) { 40 - contributorsData[key] = await response.json(); 41 - } 42 } catch (error) { 43 console.error('Failed to fetch GitHub contributors:', error); 44 }
··· 2 import GitHubContributorsCard from './GitHubContributorsCard.svelte'; 3 import CreateGitHubContributorsCardModal from './CreateGitHubContributorsCardModal.svelte'; 4 import GitHubContributorsCardSettings from './GitHubContributorsCardSettings.svelte'; 5 + import { fetchGitHubContributors } from './api.remote'; 6 7 export type GitHubContributor = { 8 username: string; ··· 34 const key = `${owner}/${repo}`; 35 if (contributorsData[key]) continue; 36 try { 37 + const data = await fetchGitHubContributors({ owner, repo }); 38 + if (data) contributorsData[key] = data; 39 + } catch (error) { 40 + console.error('Failed to fetch GitHub contributors:', error); 41 + } 42 + } 43 + return contributorsData; 44 + }, 45 + loadDataServer: async (items) => { 46 + const contributorsData: GitHubContributorsLoadedData = {}; 47 + for (const item of items) { 48 + const { owner, repo } = item.cardData; 49 + if (!owner || !repo) continue; 50 + const key = `${owner}/${repo}`; 51 + if (contributorsData[key]) continue; 52 + try { 53 + const data = await fetchGitHubContributors({ owner, repo }); 54 + if (data) contributorsData[key] = data; 55 } catch (error) { 56 console.error('Failed to fetch GitHub contributors:', error); 57 }
+3 -3
src/lib/cards/social/GitHubProfileCard/GitHubProfileCard.svelte
··· 8 import { Button } from '@foxui/core'; 9 import { browser } from '$app/environment'; 10 import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 11 12 let { item, isEditing }: ContentComponentProps = $props(); 13 ··· 23 onMount(async () => { 24 if (!contributionsData && item.cardData?.user) { 25 try { 26 - const response = await fetch(`/api/github?user=${encodeURIComponent(item.cardData.user)}`); 27 - if (response.ok) { 28 - contributionsData = await response.json(); 29 data[item.cardType] ??= {}; 30 (data[item.cardType] as GithubProfileLoadedData)[item.cardData.user] = contributionsData; 31 }
··· 8 import { Button } from '@foxui/core'; 9 import { browser } from '$app/environment'; 10 import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 11 + import { fetchGitHubContributions } from './api.remote'; 12 13 let { item, isEditing }: ContentComponentProps = $props(); 14 ··· 24 onMount(async () => { 25 if (!contributionsData && item.cardData?.user) { 26 try { 27 + contributionsData = await fetchGitHubContributions(item.cardData.user); 28 + if (contributionsData) { 29 data[item.cardType] ??= {}; 30 (data[item.cardType] as GithubProfileLoadedData)[item.cardData.user] = contributionsData; 31 }
+21
src/lib/cards/social/GitHubProfileCard/api.remote.ts
···
··· 1 + import { query, getRequestEvent } from '$app/server'; 2 + import { createCache } from '$lib/cache'; 3 + 4 + const GITHUB_API_URL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; 5 + 6 + export const fetchGitHubContributions = query('unchecked', async (user: string) => { 7 + const { platform } = getRequestEvent(); 8 + const cache = createCache(platform); 9 + 10 + const cached = await cache?.get('github', user); 11 + if (cached) return JSON.parse(cached); 12 + 13 + const response = await fetch(GITHUB_API_URL + encodeURIComponent(user)); 14 + if (!response.ok) return undefined; 15 + 16 + const data = await response.json(); 17 + if (!data?.user) return undefined; 18 + 19 + await cache?.put('github', user, JSON.stringify(data.user)); 20 + return data.user; 21 + });
+20 -8
src/lib/cards/social/GitHubProfileCard/index.ts
··· 1 import type { CardDefinition } from '../../types'; 2 import CreateGitHubProfileCardModal from './CreateGitHubProfileCardModal.svelte'; 3 - import type GithubContributionsGraph from './GithubContributionsGraph.svelte'; 4 import GitHubProfileCard from './GitHubProfileCard.svelte'; 5 import type { GitHubContributionsData } from './types'; 6 7 export type GithubProfileLoadedData = Record<string, GitHubContributionsData | undefined>; 8 ··· 12 creationModalComponent: CreateGitHubProfileCardModal, 13 14 loadData: async (items) => { 15 - const githubData: Record<string, GithubContributionsGraph> = {}; 16 for (const item of items) { 17 try { 18 - const response = await fetch( 19 - `https://blento.app/api/github?user=${encodeURIComponent(item.cardData.user)}` 20 - ); 21 - if (response.ok) { 22 - githubData[item.cardData.user] = await response.json(); 23 - } 24 } catch (error) { 25 console.error('Failed to fetch GitHub contributions:', error); 26 }
··· 1 import type { CardDefinition } from '../../types'; 2 import CreateGitHubProfileCardModal from './CreateGitHubProfileCardModal.svelte'; 3 import GitHubProfileCard from './GitHubProfileCard.svelte'; 4 import type { GitHubContributionsData } from './types'; 5 + import { fetchGitHubContributions } from './api.remote'; 6 7 export type GithubProfileLoadedData = Record<string, GitHubContributionsData | undefined>; 8 ··· 12 creationModalComponent: CreateGitHubProfileCardModal, 13 14 loadData: async (items) => { 15 + const githubData: Record<string, GitHubContributionsData> = {}; 16 for (const item of items) { 17 + const user = item.cardData.user; 18 + if (!user) continue; 19 try { 20 + const data = await fetchGitHubContributions(user); 21 + if (data) githubData[user] = data; 22 + } catch (error) { 23 + console.error('Failed to fetch GitHub contributions:', error); 24 + } 25 + } 26 + return githubData; 27 + }, 28 + loadDataServer: async (items) => { 29 + const githubData: Record<string, GitHubContributionsData> = {}; 30 + for (const item of items) { 31 + const user = item.cardData.user; 32 + if (!user) continue; 33 + try { 34 + const data = await fetchGitHubContributions(user); 35 + if (data) githubData[user] = data; 36 } catch (error) { 37 console.error('Failed to fetch GitHub contributions:', error); 38 }
+20
src/lib/cards/social/NpmxLikesLeaderboardCard/api.remote.ts
···
··· 1 + import { query, getRequestEvent } from '$app/server'; 2 + import { createCache } from '$lib/cache'; 3 + 4 + const LEADERBOARD_API_URL = 5 + 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; 6 + 7 + export const fetchNpmxLeaderboard = query(async () => { 8 + const { platform } = getRequestEvent(); 9 + const cache = createCache(platform); 10 + 11 + const cached = await cache?.get('npmx', 'likes'); 12 + if (cached) return JSON.parse(cached); 13 + 14 + const response = await fetch(LEADERBOARD_API_URL); 15 + if (!response.ok) return undefined; 16 + 17 + const data = await response.json(); 18 + await cache?.put('npmx', 'likes', JSON.stringify(data)); 19 + return data; 20 + });
+5 -3
src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts
··· 1 import type { CardDefinition } from '../../types'; 2 import NpmxLikesLeaderboardCard from './NpmxLikesLeaderboardCard.svelte'; 3 4 export const NpmxLikesLeaderboardCardDefinition = { 5 type: 'npmxLikesLeaderboard', ··· 11 card.mobileH = 6; 12 }, 13 loadData: async () => { 14 - const res = await fetch('https://blento.app/api/npmx-leaderboard'); 15 - const data = await res.json(); 16 - return data; 17 }, 18 minW: 3, 19 canHaveLabel: true,
··· 1 import type { CardDefinition } from '../../types'; 2 import NpmxLikesLeaderboardCard from './NpmxLikesLeaderboardCard.svelte'; 3 + import { fetchNpmxLeaderboard } from './api.remote'; 4 5 export const NpmxLikesLeaderboardCardDefinition = { 6 type: 'npmxLikesLeaderboard', ··· 12 card.mobileH = 6; 13 }, 14 loadData: async () => { 15 + return await fetchNpmxLeaderboard(); 16 + }, 17 + loadDataServer: async () => { 18 + return await fetchNpmxLeaderboard(); 19 }, 20 minW: 3, 21 canHaveLabel: true,
+3 -4
src/lib/cards/special/UpdatedBlentos/index.ts
··· 15 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' 16 ); 17 const recentRecords = await response.json(); 18 - const existingUsers = await cache?.get('updatedBlentos'); 19 const existingUsersArray: ProfileWithBlentoFlag[] = existingUsers 20 ? JSON.parse(existingUsers) 21 : []; ··· 50 (v) => v && v.handle !== 'handle.invalid' && !v.handle.endsWith('.pds.rip') 51 ); 52 53 - if (cache) { 54 - await cache?.put('updatedBlentos', JSON.stringify(result)); 55 - } 56 return JSON.parse(JSON.stringify(result.slice(0, 20))); 57 } catch (error) { 58 console.error('error fetching updated blentos', error);
··· 15 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' 16 ); 17 const recentRecords = await response.json(); 18 + const existingUsers = await cache?.get('meta', 'updatedBlentos'); 19 const existingUsersArray: ProfileWithBlentoFlag[] = existingUsers 20 ? JSON.parse(existingUsers) 21 : []; ··· 50 (v) => v && v.handle !== 'handle.invalid' && !v.handle.endsWith('.pds.rip') 51 ); 52 53 + await cache?.put('meta', 'updatedBlentos', JSON.stringify(result)); 54 + 55 return JSON.parse(JSON.stringify(result.slice(0, 20))); 56 } catch (error) { 57 console.error('error fetching updated blentos', error);
+20 -2
src/lib/cards/types.ts
··· 1 import type { Component } from 'svelte'; 2 - import type { Item, UserCache } from '$lib/types'; 3 import type { Did } from '@atcute/lexicons'; 4 5 export type CreationModalComponentProps = { ··· 36 loadData?: ( 37 // all cards of that type 38 items: Item[], 39 - { did, handle, cache }: { did: Did; handle: string; cache?: UserCache } 40 ) => Promise<unknown>; 41 42 // show color selection popup
··· 1 import type { Component } from 'svelte'; 2 + import type { Item } from '$lib/types'; 3 + import type { CacheService } from '$lib/cache'; 4 import type { Did } from '@atcute/lexicons'; 5 6 export type CreationModalComponentProps = { ··· 37 loadData?: ( 38 // all cards of that type 39 items: Item[], 40 + { did, handle, cache }: { did: Did; handle: string; cache?: CacheService } 41 + ) => Promise<unknown>; 42 + 43 + // server-side version of loadData that calls external APIs directly 44 + // instead of going through self-referential /api/ routes (avoids 522 on Cloudflare Workers) 45 + loadDataServer?: ( 46 + items: Item[], 47 + { 48 + did, 49 + handle, 50 + cache, 51 + env 52 + }: { 53 + did: Did; 54 + handle: string; 55 + cache?: CacheService; 56 + env?: Record<string, string | undefined>; 57 + } 58 ) => Promise<unknown>; 59 60 // show color selection popup
+2 -1
src/lib/layout/EditableGrid.svelte
··· 373 ondrop={handleFileDrop} 374 /> 375 376 - <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> 377 <div 378 bind:this={container} 379 onpointerdown={handlePointerDown}
··· 373 ondrop={handleFileDrop} 374 /> 375 376 + <!-- svelte-ignore a11y_no_static_element_interactions --> 377 + <!-- svelte-ignore a11y_click_events_have_key_events --> 378 <div 379 bind:this={container} 380 onpointerdown={handlePointerDown}
-5
src/lib/types.ts
··· 66 updatedAt: number; 67 version?: number; 68 }; 69 - 70 - export type UserCache = { 71 - get: (key: string) => string; 72 - put: (key: string, value: string) => void; 73 - };
··· 66 updatedAt: number; 67 version?: number; 68 };
+23 -24
src/lib/website/load.ts
··· 1 import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2 import { CardDefinitionsByType } from '$lib/cards'; 3 - import type { Item, UserCache, WebsiteData } from '$lib/types'; 4 import { error } from '@sveltejs/kit'; 5 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 6 ··· 9 10 const CURRENT_CACHE_VERSION = 1; 11 12 - export async function getCache(handle: string, page: string, cache?: UserCache) { 13 try { 14 - const cachedResult = await cache?.get?.(handle); 15 16 if (!cachedResult) return; 17 const result = JSON.parse(cachedResult); 18 - const update = result.updatedAt; 19 - const timePassed = (Date.now() - update) / 1000; 20 - 21 - const ONE_DAY = 60 * 60 * 24; 22 23 if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 24 console.log('skipping cache because of version mismatch'); 25 return; 26 } 27 28 - if (timePassed > ONE_DAY) { 29 - console.log('skipping cache because of age'); 30 - return; 31 - } 32 - 33 result.page = 'blento.' + page; 34 35 result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find( ··· 42 43 delete result['publications']; 44 45 - console.log('using cached result for handle', handle, 'last update', timePassed, 'seconds ago'); 46 return checkData(result); 47 } catch (error) { 48 console.log('getting cached result failed', error); ··· 51 52 export async function loadData( 53 handle: ActorIdentifier, 54 - cache: UserCache | undefined, 55 forceUpdate: boolean = false, 56 - page: string = 'self' 57 ): Promise<WebsiteData> { 58 if (!handle) throw error(404); 59 if (handle === 'favicon.ico') throw error(404); ··· 74 } 75 76 const [cards, mainPublication, pages, profile] = await Promise.all([ 77 - listRecords({ did, collection: 'app.blento.card' }).catch(() => { 78 - console.error('error getting records for collection app.blento.card'); 79 return [] as Awaited<ReturnType<typeof listRecords>>; 80 }), 81 getRecord({ ··· 103 for (const cardType of cardTypesArray) { 104 const cardDef = CardDefinitionsByType[cardType]; 105 106 - if (!cardDef?.loadData) continue; 107 108 try { 109 - additionDataPromises[cardType] = cardDef.loadData( 110 - cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[], 111 - loadOptions 112 - ); 113 } catch { 114 console.error('error getting additional data for', cardType); 115 } ··· 140 version: CURRENT_CACHE_VERSION 141 }; 142 143 - const stringifiedResult = JSON.stringify(result); 144 - await cache?.put?.(handle, stringifiedResult); 145 146 const parsedResult = structuredClone(result) as any; 147
··· 1 import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; 2 import { CardDefinitionsByType } from '$lib/cards'; 3 + import type { CacheService } from '$lib/cache'; 4 + import type { Item, WebsiteData } from '$lib/types'; 5 import { error } from '@sveltejs/kit'; 6 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 7 ··· 10 11 const CURRENT_CACHE_VERSION = 1; 12 13 + export async function getCache(identifier: ActorIdentifier, page: string, cache?: CacheService) { 14 try { 15 + const cachedResult = await cache?.getBlento(identifier); 16 17 if (!cachedResult) return; 18 const result = JSON.parse(cachedResult); 19 20 if (!result.version || result.version !== CURRENT_CACHE_VERSION) { 21 console.log('skipping cache because of version mismatch'); 22 return; 23 } 24 25 result.page = 'blento.' + page; 26 27 result.publication = (result.publications as Awaited<ReturnType<typeof listRecords>>).find( ··· 34 35 delete result['publications']; 36 37 return checkData(result); 38 } catch (error) { 39 console.log('getting cached result failed', error); ··· 42 43 export async function loadData( 44 handle: ActorIdentifier, 45 + cache: CacheService | undefined, 46 forceUpdate: boolean = false, 47 + page: string = 'self', 48 + env?: Record<string, string | undefined> 49 ): Promise<WebsiteData> { 50 if (!handle) throw error(404); 51 if (handle === 'favicon.ico') throw error(404); ··· 66 } 67 68 const [cards, mainPublication, pages, profile] = await Promise.all([ 69 + listRecords({ did, collection: 'app.blento.card' }).catch((e) => { 70 + console.error('error getting records for collection app.blento.card', e); 71 return [] as Awaited<ReturnType<typeof listRecords>>; 72 }), 73 getRecord({ ··· 95 for (const cardType of cardTypesArray) { 96 const cardDef = CardDefinitionsByType[cardType]; 97 98 + const items = cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[]; 99 100 try { 101 + if (cardDef?.loadDataServer) { 102 + additionDataPromises[cardType] = cardDef.loadDataServer(items, { 103 + ...loadOptions, 104 + env 105 + }); 106 + } else if (cardDef?.loadData) { 107 + additionDataPromises[cardType] = cardDef.loadData(items, loadOptions); 108 + } 109 } catch { 110 console.error('error getting additional data for', cardType); 111 } ··· 136 version: CURRENT_CACHE_VERSION 137 }; 138 139 + // Only cache results that have cards to avoid caching PDS errors 140 + if (result.cards.length > 0) { 141 + const stringifiedResult = JSON.stringify(result); 142 + await cache?.putBlento(did, handle as string, stringifiedResult); 143 + } 144 145 const parsedResult = structuredClone(result) as any; 146
+6
src/params/actor.ts
···
··· 1 + import { isActorIdentifier } from '@atcute/lexicons/syntax'; 2 + import type { ParamMatcher } from '@sveltejs/kit'; 3 + 4 + export const match = ((param: string) => { 5 + return isActorIdentifier(param); 6 + }) satisfies ParamMatcher;
-6
src/params/handle.ts
··· 1 - import { isActorIdentifier } from '@atcute/lexicons/syntax'; 2 - import type { ParamMatcher } from '@sveltejs/kit'; 3 - 4 - export const match = ((param: string) => { 5 - return isActorIdentifier(param); 6 - }) satisfies ParamMatcher;
···
-2
src/routes/+layout.server.ts
··· 1 export async function load({ request }) { 2 const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 3 4 - console.log('domain:', customDomain); 5 - 6 return { customDomain }; 7 }
··· 1 export async function load({ request }) { 2 const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 3 4 return { customDomain }; 5 }
-1
src/routes/+layout.svelte
··· 17 }; 18 19 onMount(() => { 20 - console.log(data.customDomain); 21 initClient({ customDomain: data.customDomain }); 22 23 const error = page.url.searchParams.get('error');
··· 17 }; 18 19 onMount(() => { 20 initClient({ customDomain: data.customDomain }); 21 22 const error = page.url.searchParams.get('error');
+5 -4
src/routes/+page.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 4 import type { ActorIdentifier } from '@atcute/lexicons'; 5 6 export async function load({ platform, request }) { ··· 8 9 const kv = platform?.env?.CUSTOM_DOMAINS; 10 11 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 12 const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); 13 14 if (kv && customDomain) { 15 try { 16 const did = await kv.get(customDomain); 17 18 - if (did) return await loadData(did as ActorIdentifier, cache as UserCache); 19 } catch (error) { 20 console.error('failed to get custom domain kv', error); 21 } 22 } 23 24 - return await loadData(handle as ActorIdentifier, cache as UserCache); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 + import { createCache } from '$lib/cache'; 5 import type { ActorIdentifier } from '@atcute/lexicons'; 6 7 export async function load({ platform, request }) { ··· 9 10 const kv = platform?.env?.CUSTOM_DOMAINS; 11 12 + const cache = createCache(platform); 13 const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); 14 15 if (kv && customDomain) { 16 try { 17 const did = await kv.get(customDomain); 18 19 + if (did) return await loadData(did as ActorIdentifier, cache, false, 'self', privateEnv); 20 } catch (error) { 21 console.error('failed to get custom domain kv', error); 22 } 23 } 24 25 + return await loadData(handle as ActorIdentifier, cache, false, 'self', privateEnv); 26 }
+18
src/routes/[actor=actor]/(pages)/+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 { createCache } from '$lib/cache'; 5 + 6 + export async function load({ params, platform, request }) { 7 + if (env.PUBLIC_IS_SELFHOSTED) error(404); 8 + 9 + const cache = createCache(platform); 10 + 11 + const customDomain = request.headers.get('X-Custom-Domain'); 12 + 13 + if (customDomain) { 14 + throw error(404, 'Page not found!'); 15 + } 16 + 17 + return await loadData(params.actor, cache, false, params.page, env); 18 + }
+13
src/routes/[actor=actor]/(pages)/+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/[actor=actor]/(pages)/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} />
+13
src/routes/[actor=actor]/(pages)/p/[[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} />
+252
src/routes/[actor=actor]/(pages)/p/[[page]]/copy/+page.svelte
···
··· 1 + <script lang="ts"> 2 + import { 3 + putRecord, 4 + deleteRecord, 5 + listRecords, 6 + uploadBlob, 7 + getCDNImageBlobUrl 8 + } from '$lib/atproto/methods'; 9 + import { user } from '$lib/atproto/auth.svelte'; 10 + import { goto } from '$app/navigation'; 11 + import * as TID from '@atcute/tid'; 12 + import { Button } from '@foxui/core'; 13 + import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 14 + 15 + let { data } = $props(); 16 + 17 + let destinationPage = $state(''); 18 + let copying = $state(false); 19 + let error = $state(''); 20 + let success = $state(false); 21 + 22 + const sourceHandle = $derived(data.handle); 23 + 24 + const sourcePage = $derived( 25 + data.page === 'blento.self' ? 'main' : data.page.replace('blento.', '') 26 + ); 27 + const sourceCards = $derived(data.cards); 28 + 29 + // Re-upload blobs from source repo to current user's repo 30 + async function reuploadBlobs(obj: any, sourceDid: string): Promise<void> { 31 + if (!obj || typeof obj !== 'object') return; 32 + 33 + for (const key of Object.keys(obj)) { 34 + const value = obj[key]; 35 + 36 + if (value && typeof value === 'object') { 37 + // Check if this is a blob reference 38 + if (value.$type === 'blob' && value.ref?.$link) { 39 + try { 40 + // Get the blob URL from source repo 41 + const blobUrl = getCDNImageBlobUrl({ did: sourceDid, blob: value }); 42 + if (!blobUrl) continue; 43 + 44 + // Fetch the blob via proxy to avoid CORS 45 + const response = await fetch(`/api/image-proxy?url=${encodeURIComponent(blobUrl)}`); 46 + if (!response.ok) { 47 + console.error('Failed to fetch blob:', blobUrl); 48 + continue; 49 + } 50 + 51 + // Upload to current user's repo 52 + const blob = await response.blob(); 53 + const newBlobRef = await uploadBlob({ blob }); 54 + 55 + if (newBlobRef) { 56 + // Replace with new blob reference 57 + obj[key] = newBlobRef; 58 + } 59 + } catch (err) { 60 + console.error('Failed to re-upload blob:', err); 61 + } 62 + } else { 63 + // Recursively check nested objects 64 + await reuploadBlobs(value, sourceDid); 65 + } 66 + } 67 + } 68 + } 69 + 70 + async function copyPage() { 71 + if (!user.isLoggedIn || !user.did) { 72 + error = 'You must be logged in to copy pages'; 73 + return; 74 + } 75 + 76 + copying = true; 77 + error = ''; 78 + 79 + try { 80 + const targetPage = 81 + destinationPage.trim() === '' ? 'blento.self' : `blento.${destinationPage.trim()}`; 82 + 83 + // Fetch existing cards from destination page and delete them 84 + const existingCards = await listRecords({ 85 + did: user.did, 86 + collection: 'app.blento.card' 87 + }); 88 + 89 + const cardsToDelete = existingCards.filter( 90 + (card: { value: { page?: string } }) => card.value.page === targetPage 91 + ); 92 + 93 + // Delete existing cards from destination page 94 + const deletePromises = cardsToDelete.map((card: { uri: string }) => { 95 + const rkey = card.uri.split('/').pop()!; 96 + return deleteRecord({ 97 + collection: 'app.blento.card', 98 + rkey 99 + }); 100 + }); 101 + 102 + await Promise.all(deletePromises); 103 + 104 + // Copy each card with a new ID to the destination page 105 + // Re-upload blobs from source repo to current user's repo 106 + for (const card of sourceCards) { 107 + const newCard = { 108 + ...structuredClone(card), 109 + id: TID.now(), 110 + page: targetPage, 111 + updatedAt: new Date().toISOString(), 112 + version: 2 113 + }; 114 + 115 + // Re-upload any blobs in cardData 116 + await reuploadBlobs(newCard.cardData, data.did); 117 + 118 + await putRecord({ 119 + collection: 'app.blento.card', 120 + rkey: newCard.id, 121 + record: newCard 122 + }); 123 + } 124 + 125 + const userHandle = user.profile?.handle ?? data.handle; 126 + 127 + // Copy publication data if it exists 128 + if (data.publication) { 129 + const publicationCopy = structuredClone(data.publication) as Record<string, unknown>; 130 + 131 + // Re-upload any blobs in publication (e.g., icon) 132 + await reuploadBlobs(publicationCopy, data.did); 133 + 134 + // Update the URL to point to the user's page 135 + publicationCopy.url = `https://blento.app/${userHandle}`; 136 + if (targetPage !== 'blento.self') { 137 + publicationCopy.url += '/' + targetPage.replace('blento.', ''); 138 + } 139 + 140 + // Save to appropriate collection based on destination page type 141 + if (targetPage === 'blento.self') { 142 + await putRecord({ 143 + collection: 'site.standard.publication', 144 + rkey: targetPage, 145 + record: publicationCopy 146 + }); 147 + } else { 148 + await putRecord({ 149 + collection: 'app.blento.page', 150 + rkey: targetPage, 151 + record: publicationCopy 152 + }); 153 + } 154 + } 155 + 156 + // Refresh the logged-in user's cache 157 + await fetch(`/${userHandle}/api/refresh`); 158 + 159 + success = true; 160 + 161 + // Redirect to the logged-in user's destination page edit 162 + const destPath = destinationPage.trim() === '' ? '' : `/${destinationPage.trim()}`; 163 + setTimeout(() => { 164 + goto(`/${userHandle}${destPath}/edit`); 165 + }, 1000); 166 + } catch (e) { 167 + error = e instanceof Error ? e.message : 'Failed to copy page'; 168 + } finally { 169 + copying = false; 170 + } 171 + } 172 + </script> 173 + 174 + <div class="bg-base-50 dark:bg-base-900 flex min-h-screen items-center justify-center p-4"> 175 + <div class="bg-base-100 dark:bg-base-800 w-full max-w-md rounded-2xl p-6 shadow-lg"> 176 + {#if user.isLoggedIn} 177 + <h1 class="text-base-900 dark:text-base-50 mb-6 text-2xl font-bold">Copy Page</h1> 178 + 179 + <div class="mb-4"> 180 + <div class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium"> 181 + Source Page 182 + </div> 183 + <div 184 + class="bg-base-200 dark:bg-base-700 text-base-900 dark:text-base-100 rounded-lg px-3 py-2" 185 + > 186 + {sourceHandle}/{sourcePage || 'main'} 187 + </div> 188 + <p class="text-base-500 mt-1 text-sm">{sourceCards.length} cards</p> 189 + </div> 190 + 191 + <div class="mb-6"> 192 + <label 193 + for="destination" 194 + class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium" 195 + > 196 + Destination Page (on your profile: {user.profile?.handle}) 197 + </label> 198 + <input 199 + id="destination" 200 + type="text" 201 + bind:value={destinationPage} 202 + placeholder="Leave empty for main page" 203 + class="bg-base-50 dark:bg-base-700 border-base-300 dark:border-base-600 text-base-900 dark:text-base-100 focus:ring-accent-500 w-full rounded-lg border px-3 py-2 focus:ring-2 focus:outline-none" 204 + /> 205 + </div> 206 + 207 + {#if error} 208 + <div 209 + class="mb-4 rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900/30 dark:text-red-400" 210 + > 211 + {error} 212 + </div> 213 + {/if} 214 + 215 + {#if success} 216 + <div 217 + class="mb-4 rounded-lg bg-green-100 p-3 text-green-700 dark:bg-green-900/30 dark:text-green-400" 218 + > 219 + Page copied successfully! Redirecting... 220 + </div> 221 + {/if} 222 + 223 + <div class="flex gap-3"> 224 + <a 225 + href="/{data.handle}/{sourcePage === 'main' ? '' : sourcePage}" 226 + class="bg-base-200 hover:bg-base-300 dark:bg-base-700 dark:hover:bg-base-600 text-base-700 dark:text-base-300 flex-1 rounded-lg px-4 py-2 text-center font-medium transition-colors" 227 + > 228 + Cancel 229 + </a> 230 + <button 231 + onclick={copyPage} 232 + disabled={copying || success} 233 + class="bg-accent-500 hover:bg-accent-600 flex-1 rounded-lg px-4 py-2 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50" 234 + > 235 + {#if copying} 236 + Copying... 237 + {:else} 238 + Copy {sourceCards.length} cards 239 + {/if} 240 + </button> 241 + </div> 242 + {:else} 243 + <h1 class="text-base-900 dark:text-base-50 mb-6 text-2xl font-bold"> 244 + You must be signed in to copy a page! 245 + </h1> 246 + 247 + <div class="flex w-full justify-center"> 248 + <Button size="lg" onclick={() => loginModalState.show()}>Login</Button> 249 + </div> 250 + {/if} 251 + </div> 252 + </div>
+6
src/routes/[actor=actor]/(pages)/p/[[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} />
+16
src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts
···
··· 1 + import { loadData } from '$lib/website/load'; 2 + import { createCache } from '$lib/cache'; 3 + import { env } from '$env/dynamic/private'; 4 + 5 + import { error } from '@sveltejs/kit'; 6 + import { text } from '@sveltejs/kit'; 7 + 8 + export async function GET({ params, platform }) { 9 + const cache = createCache(platform); 10 + 11 + const data = await loadData(params.actor, cache, false, params.page, env); 12 + 13 + if (!data.publication) throw error(300); 14 + 15 + return text(data.did + '/site.standard.publication/blento.self'); 16 + }
+14
src/routes/[actor=actor]/api/refresh/+server.ts
···
··· 1 + import { createCache } from '$lib/cache'; 2 + import { loadData } from '$lib/website/load.js'; 3 + import { env } from '$env/dynamic/private'; 4 + import type { Handle } from '@atcute/lexicons'; 5 + import { json } from '@sveltejs/kit'; 6 + 7 + export async function GET({ params, platform }) { 8 + const cache = createCache(platform); 9 + if (!cache) return json('no cache'); 10 + 11 + await loadData(params.actor, cache, true, 'self', env); 12 + 13 + return json('ok'); 14 + }
+58
src/routes/[actor=actor]/og.png/+server.ts
···
··· 1 + import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; 2 + import { createCache } from '$lib/cache'; 3 + import { loadData } from '$lib/website/load'; 4 + import { env } from '$env/dynamic/private'; 5 + import type { Handle } from '@atcute/lexicons'; 6 + import { ImageResponse } from '@ethercorps/sveltekit-og'; 7 + 8 + function escapeHtml(str: string): string { 9 + return str 10 + .replace(/&/g, '&amp;') 11 + .replace(/</g, '&lt;') 12 + .replace(/>/g, '&gt;') 13 + .replace(/"/g, '&quot;') 14 + .replace(/'/g, '&#39;'); 15 + } 16 + 17 + export async function GET({ params, platform }) { 18 + const cache = createCache(platform); 19 + 20 + const data = await loadData(params.actor, cache, false, 'self', env); 21 + 22 + let image: string | undefined = data.profile.avatar; 23 + 24 + if (data.publication.icon) { 25 + image = 26 + getCDNImageBlobUrl({ did: data.did, blob: data.publication.icon }) ?? data.profile.avatar; 27 + } 28 + 29 + const name = data.publication?.name ?? data.profile.displayName ?? data.profile.handle; 30 + 31 + const htmlString = ` 32 + <div class="flex flex-col p-8 w-full h-full bg-neutral-900"> 33 + <div class="flex items-center mb-8 mt-16"> 34 + <img src="${escapeHtml(image ?? '')}" width="128" height="128" class="rounded-full" /> 35 + 36 + <h1 class="text-neutral-50 text-7xl ml-4">${escapeHtml(name)}</h1> 37 + </div> 38 + 39 + <p class="mt-8 text-4xl text-neutral-300">Check out my blento</p> 40 + 41 + <svg class="absolute w-130 h-130 top-50 right-0" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg"> 42 + <rect x="100" y="100" width="160" height="340" rx="23" fill="#EF4444"/> 43 + <rect x="640" y="280" width="160" height="340" rx="23" fill="#22C55E"/> 44 + <rect x="280" y="100" width="340" height="340" rx="23" fill="#F59E0B"/> 45 + <rect x="100" y="460" width="340" height="160" rx="23" fill="#0EA5E9"/> 46 + <rect x="640" y="100" width="160" height="160" rx="23" fill="#EAB308"/> 47 + <rect x="100" y="640" width="160" height="160" rx="23" fill="#6366F1"/> 48 + <rect x="460" y="460" width="160" height="160" rx="23" fill="#14B8A6"/> 49 + <rect x="280" y="640" width="520" height="160" rx="23" fill="#A855F7"/> 50 + </svg> 51 + </div> 52 + `; 53 + 54 + return new ImageResponse(htmlString, { 55 + width: 1200, 56 + height: 630 57 + }); 58 + }
-13
src/routes/[handle=handle]/(pages)/+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 - import type { Handle } from '@atcute/lexicons'; 6 - 7 - export async function load({ params, platform }) { 8 - if (env.PUBLIC_IS_SELFHOSTED) error(404); 9 - 10 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 11 - 12 - return await loadData(params.handle as Handle, cache as UserCache, false, params.page); 13 - }
···
-13
src/routes/[handle=handle]/(pages)/+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=handle]/(pages)/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} />
···
-13
src/routes/[handle=handle]/(pages)/p/[[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} />
···
-252
src/routes/[handle=handle]/(pages)/p/[[page]]/copy/+page.svelte
··· 1 - <script lang="ts"> 2 - import { 3 - putRecord, 4 - deleteRecord, 5 - listRecords, 6 - uploadBlob, 7 - getCDNImageBlobUrl 8 - } from '$lib/atproto/methods'; 9 - import { user } from '$lib/atproto/auth.svelte'; 10 - import { goto } from '$app/navigation'; 11 - import * as TID from '@atcute/tid'; 12 - import { Button } from '@foxui/core'; 13 - import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte'; 14 - 15 - let { data } = $props(); 16 - 17 - let destinationPage = $state(''); 18 - let copying = $state(false); 19 - let error = $state(''); 20 - let success = $state(false); 21 - 22 - const sourceHandle = $derived(data.handle); 23 - 24 - const sourcePage = $derived( 25 - data.page === 'blento.self' ? 'main' : data.page.replace('blento.', '') 26 - ); 27 - const sourceCards = $derived(data.cards); 28 - 29 - // Re-upload blobs from source repo to current user's repo 30 - async function reuploadBlobs(obj: any, sourceDid: string): Promise<void> { 31 - if (!obj || typeof obj !== 'object') return; 32 - 33 - for (const key of Object.keys(obj)) { 34 - const value = obj[key]; 35 - 36 - if (value && typeof value === 'object') { 37 - // Check if this is a blob reference 38 - if (value.$type === 'blob' && value.ref?.$link) { 39 - try { 40 - // Get the blob URL from source repo 41 - const blobUrl = getCDNImageBlobUrl({ did: sourceDid, blob: value }); 42 - if (!blobUrl) continue; 43 - 44 - // Fetch the blob via proxy to avoid CORS 45 - const response = await fetch(`/api/image-proxy?url=${encodeURIComponent(blobUrl)}`); 46 - if (!response.ok) { 47 - console.error('Failed to fetch blob:', blobUrl); 48 - continue; 49 - } 50 - 51 - // Upload to current user's repo 52 - const blob = await response.blob(); 53 - const newBlobRef = await uploadBlob({ blob }); 54 - 55 - if (newBlobRef) { 56 - // Replace with new blob reference 57 - obj[key] = newBlobRef; 58 - } 59 - } catch (err) { 60 - console.error('Failed to re-upload blob:', err); 61 - } 62 - } else { 63 - // Recursively check nested objects 64 - await reuploadBlobs(value, sourceDid); 65 - } 66 - } 67 - } 68 - } 69 - 70 - async function copyPage() { 71 - if (!user.isLoggedIn || !user.did) { 72 - error = 'You must be logged in to copy pages'; 73 - return; 74 - } 75 - 76 - copying = true; 77 - error = ''; 78 - 79 - try { 80 - const targetPage = 81 - destinationPage.trim() === '' ? 'blento.self' : `blento.${destinationPage.trim()}`; 82 - 83 - // Fetch existing cards from destination page and delete them 84 - const existingCards = await listRecords({ 85 - did: user.did, 86 - collection: 'app.blento.card' 87 - }); 88 - 89 - const cardsToDelete = existingCards.filter( 90 - (card: { value: { page?: string } }) => card.value.page === targetPage 91 - ); 92 - 93 - // Delete existing cards from destination page 94 - const deletePromises = cardsToDelete.map((card: { uri: string }) => { 95 - const rkey = card.uri.split('/').pop()!; 96 - return deleteRecord({ 97 - collection: 'app.blento.card', 98 - rkey 99 - }); 100 - }); 101 - 102 - await Promise.all(deletePromises); 103 - 104 - // Copy each card with a new ID to the destination page 105 - // Re-upload blobs from source repo to current user's repo 106 - for (const card of sourceCards) { 107 - const newCard = { 108 - ...structuredClone(card), 109 - id: TID.now(), 110 - page: targetPage, 111 - updatedAt: new Date().toISOString(), 112 - version: 2 113 - }; 114 - 115 - // Re-upload any blobs in cardData 116 - await reuploadBlobs(newCard.cardData, data.did); 117 - 118 - await putRecord({ 119 - collection: 'app.blento.card', 120 - rkey: newCard.id, 121 - record: newCard 122 - }); 123 - } 124 - 125 - const userHandle = user.profile?.handle ?? data.handle; 126 - 127 - // Copy publication data if it exists 128 - if (data.publication) { 129 - const publicationCopy = structuredClone(data.publication) as Record<string, unknown>; 130 - 131 - // Re-upload any blobs in publication (e.g., icon) 132 - await reuploadBlobs(publicationCopy, data.did); 133 - 134 - // Update the URL to point to the user's page 135 - publicationCopy.url = `https://blento.app/${userHandle}`; 136 - if (targetPage !== 'blento.self') { 137 - publicationCopy.url += '/' + targetPage.replace('blento.', ''); 138 - } 139 - 140 - // Save to appropriate collection based on destination page type 141 - if (targetPage === 'blento.self') { 142 - await putRecord({ 143 - collection: 'site.standard.publication', 144 - rkey: targetPage, 145 - record: publicationCopy 146 - }); 147 - } else { 148 - await putRecord({ 149 - collection: 'app.blento.page', 150 - rkey: targetPage, 151 - record: publicationCopy 152 - }); 153 - } 154 - } 155 - 156 - // Refresh the logged-in user's cache 157 - await fetch(`/${userHandle}/api/refresh`); 158 - 159 - success = true; 160 - 161 - // Redirect to the logged-in user's destination page edit 162 - const destPath = destinationPage.trim() === '' ? '' : `/${destinationPage.trim()}`; 163 - setTimeout(() => { 164 - goto(`/${userHandle}${destPath}/edit`); 165 - }, 1000); 166 - } catch (e) { 167 - error = e instanceof Error ? e.message : 'Failed to copy page'; 168 - } finally { 169 - copying = false; 170 - } 171 - } 172 - </script> 173 - 174 - <div class="bg-base-50 dark:bg-base-900 flex min-h-screen items-center justify-center p-4"> 175 - <div class="bg-base-100 dark:bg-base-800 w-full max-w-md rounded-2xl p-6 shadow-lg"> 176 - {#if user.isLoggedIn} 177 - <h1 class="text-base-900 dark:text-base-50 mb-6 text-2xl font-bold">Copy Page</h1> 178 - 179 - <div class="mb-4"> 180 - <div class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium"> 181 - Source Page 182 - </div> 183 - <div 184 - class="bg-base-200 dark:bg-base-700 text-base-900 dark:text-base-100 rounded-lg px-3 py-2" 185 - > 186 - {sourceHandle}/{sourcePage || 'main'} 187 - </div> 188 - <p class="text-base-500 mt-1 text-sm">{sourceCards.length} cards</p> 189 - </div> 190 - 191 - <div class="mb-6"> 192 - <label 193 - for="destination" 194 - class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium" 195 - > 196 - Destination Page (on your profile: {user.profile?.handle}) 197 - </label> 198 - <input 199 - id="destination" 200 - type="text" 201 - bind:value={destinationPage} 202 - placeholder="Leave empty for main page" 203 - class="bg-base-50 dark:bg-base-700 border-base-300 dark:border-base-600 text-base-900 dark:text-base-100 focus:ring-accent-500 w-full rounded-lg border px-3 py-2 focus:ring-2 focus:outline-none" 204 - /> 205 - </div> 206 - 207 - {#if error} 208 - <div 209 - class="mb-4 rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900/30 dark:text-red-400" 210 - > 211 - {error} 212 - </div> 213 - {/if} 214 - 215 - {#if success} 216 - <div 217 - class="mb-4 rounded-lg bg-green-100 p-3 text-green-700 dark:bg-green-900/30 dark:text-green-400" 218 - > 219 - Page copied successfully! Redirecting... 220 - </div> 221 - {/if} 222 - 223 - <div class="flex gap-3"> 224 - <a 225 - href="/{data.handle}/{sourcePage === 'main' ? '' : sourcePage}" 226 - class="bg-base-200 hover:bg-base-300 dark:bg-base-700 dark:hover:bg-base-600 text-base-700 dark:text-base-300 flex-1 rounded-lg px-4 py-2 text-center font-medium transition-colors" 227 - > 228 - Cancel 229 - </a> 230 - <button 231 - onclick={copyPage} 232 - disabled={copying || success} 233 - class="bg-accent-500 hover:bg-accent-600 flex-1 rounded-lg px-4 py-2 font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50" 234 - > 235 - {#if copying} 236 - Copying... 237 - {:else} 238 - Copy {sourceCards.length} cards 239 - {/if} 240 - </button> 241 - </div> 242 - {:else} 243 - <h1 class="text-base-900 dark:text-base-50 mb-6 text-2xl font-bold"> 244 - You must be signed in to copy a page! 245 - </h1> 246 - 247 - <div class="flex w-full justify-center"> 248 - <Button size="lg" onclick={() => loginModalState.show()}>Login</Button> 249 - </div> 250 - {/if} 251 - </div> 252 - </div>
···
-6
src/routes/[handle=handle]/(pages)/p/[[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} />
···
-14
src/routes/[handle=handle]/.well-known/site.standard.publication/+server.ts
··· 1 - import { loadData } from '$lib/website/load'; 2 - import { error } from '@sveltejs/kit'; 3 - import type { UserCache } from '$lib/types'; 4 - import { text } from '@sveltejs/kit'; 5 - 6 - export async function GET({ params, platform }) { 7 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 8 - 9 - const data = await loadData(params.handle, cache as UserCache, false, params.page); 10 - 11 - if (!data.publication) throw error(300); 12 - 13 - return text(data.did + '/site.standard.publication/blento.self'); 14 - }
···
-14
src/routes/[handle=handle]/api/refresh/+server.ts
··· 1 - import type { UserCache } from '$lib/types'; 2 - import { loadData } from '$lib/website/load.js'; 3 - import type { Handle } from '@atcute/lexicons'; 4 - import { json } from '@sveltejs/kit'; 5 - 6 - export async function GET({ params, platform }) { 7 - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 8 - const handle = params.handle; 9 - 10 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 11 - await loadData(handle as Handle, cache as UserCache, true); 12 - 13 - return json('ok'); 14 - }
···
-58
src/routes/[handle=handle]/og.png/+server.ts
··· 1 - import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; 2 - import type { UserCache } from '$lib/types'; 3 - import { loadData } from '$lib/website/load'; 4 - import type { Handle } from '@atcute/lexicons'; 5 - import { isDid } from '@atcute/lexicons/syntax'; 6 - import { ImageResponse } from '@ethercorps/sveltekit-og'; 7 - 8 - function escapeHtml(str: string): string { 9 - return str 10 - .replace(/&/g, '&amp;') 11 - .replace(/</g, '&lt;') 12 - .replace(/>/g, '&gt;') 13 - .replace(/"/g, '&quot;') 14 - .replace(/'/g, '&#39;'); 15 - } 16 - 17 - export async function GET({ params, platform }) { 18 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 19 - 20 - const data = await loadData(params.handle as Handle, cache as UserCache); 21 - 22 - let image: string | undefined = data.profile.avatar; 23 - 24 - if (data.publication.icon) { 25 - image = 26 - getCDNImageBlobUrl({ did: data.did, blob: data.publication.icon }) ?? data.profile.avatar; 27 - } 28 - 29 - const name = data.publication?.name ?? data.profile.displayName ?? data.profile.handle; 30 - 31 - const htmlString = ` 32 - <div class="flex flex-col p-8 w-full h-full bg-neutral-900"> 33 - <div class="flex items-center mb-8 mt-16"> 34 - <img src="${escapeHtml(image ?? '')}" width="128" height="128" class="rounded-full" /> 35 - 36 - <h1 class="text-neutral-50 text-7xl ml-4">${escapeHtml(name)}</h1> 37 - </div> 38 - 39 - <p class="mt-8 text-4xl text-neutral-300">Check out my blento</p> 40 - 41 - <svg class="absolute w-130 h-130 top-50 right-0" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg"> 42 - <rect x="100" y="100" width="160" height="340" rx="23" fill="#EF4444"/> 43 - <rect x="640" y="280" width="160" height="340" rx="23" fill="#22C55E"/> 44 - <rect x="280" y="100" width="340" height="340" rx="23" fill="#F59E0B"/> 45 - <rect x="100" y="460" width="340" height="160" rx="23" fill="#0EA5E9"/> 46 - <rect x="640" y="100" width="160" height="160" rx="23" fill="#EAB308"/> 47 - <rect x="100" y="640" width="160" height="160" rx="23" fill="#6366F1"/> 48 - <rect x="460" y="460" width="160" height="160" rx="23" fill="#14B8A6"/> 49 - <rect x="280" y="640" width="520" height="160" rx="23" fill="#A855F7"/> 50 - </svg> 51 - </div> 52 - `; 53 - 54 - return new ImageResponse(htmlString, { 55 - width: 1200, 56 - height: 630 57 - }); 58 - }
···
-35
src/routes/all/+page.server.ts
··· 1 - import { env } from '$env/dynamic/public'; 2 - import type { UserCache, WebsiteData } from '$lib/types.js'; 3 - import { loadData } from '$lib/website/load'; 4 - import type { Handle } from '@atcute/lexicons'; 5 - import type { AppBskyActorDefs } from '@atcute/bluesky'; 6 - 7 - export async function load({ platform }) { 8 - const cache = platform?.env?.USER_DATA_CACHE; 9 - 10 - const list = await cache?.list(); 11 - 12 - const profiles: AppBskyActorDefs.ProfileViewDetailed[] = []; 13 - for (const value of list?.keys ?? []) { 14 - // check if at least one card 15 - const result = await cache?.get(value.name); 16 - if (!result) continue; 17 - const parsed = JSON.parse(result) as WebsiteData; 18 - 19 - if (parsed.version !== 1 || !parsed.cards?.length) continue; 20 - 21 - profiles.push(parsed.profile); 22 - } 23 - 24 - profiles.sort((a, b) => a.handle.localeCompare(b.handle)); 25 - 26 - const handle = env.PUBLIC_HANDLE; 27 - 28 - const data = await loadData(handle as Handle, cache as unknown as UserCache); 29 - 30 - data.publication ??= {}; 31 - data.publication.preferences ??= {}; 32 - data.publication.preferences.hideProfileSection = true; 33 - 34 - return { ...data, profiles }; 35 - }
···
-29
src/routes/all/+page.svelte
··· 1 - <script lang="ts"> 2 - import { createEmptyCard } from '$lib/helper.js'; 3 - import Website from '$lib/website/Website.svelte'; 4 - 5 - let { data } = $props(); 6 - </script> 7 - 8 - <Website 9 - data={{ 10 - ...data, 11 - cards: data.profiles.map((v, i) => { 12 - const card = createEmptyCard(''); 13 - card.cardType = 'blueskyProfile'; 14 - card.cardData = { 15 - avatar: v.avatar, 16 - handle: v.handle, 17 - displayName: v.displayName 18 - }; 19 - 20 - card.x = (i % 4) * 2; 21 - card.y = Math.floor(i / 4) * 2; 22 - 23 - card.mobileX = (i % 2) * 4; 24 - card.mobileY = Math.floor(i / 2) * 4; 25 - 26 - return card; 27 - }) 28 - }} 29 - />
···
-54
src/routes/api/github/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { RequestHandler } from './$types'; 3 - import type { GitHubContributionsData } from '$lib/cards/social/GitHubProfileCard/types'; 4 - 5 - const GithubAPIURL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; 6 - 7 - export const GET: RequestHandler = async ({ url, platform }) => { 8 - const user = url.searchParams.get('user'); 9 - 10 - if (!user) { 11 - return json({ error: 'No user provided' }, { status: 400 }); 12 - } 13 - 14 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get('#github:' + user); 15 - 16 - if (cachedData) { 17 - const parsedCache = JSON.parse(cachedData); 18 - 19 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 20 - const now = Date.now(); 21 - 22 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 23 - return json(parsedCache); 24 - } 25 - } 26 - 27 - try { 28 - const response = await fetch(GithubAPIURL + user); 29 - 30 - if (!response.ok) { 31 - console.error('error', response.statusText); 32 - return json( 33 - { error: 'Failed to fetch GitHub data ' + response.statusText }, 34 - { status: response.status } 35 - ); 36 - } 37 - 38 - const data = await response.json(); 39 - 40 - if (!data?.user) { 41 - return json({ error: 'User not found' }, { status: 404 }); 42 - } 43 - 44 - const result = data.user as GitHubContributionsData; 45 - result.updatedAt = Date.now(); 46 - 47 - await platform?.env?.USER_DATA_CACHE?.put('#github:' + user, JSON.stringify(result)); 48 - 49 - return json(result); 50 - } catch (error) { 51 - console.error('Error fetching GitHub contributions:', error); 52 - return json({ error: 'Failed to fetch GitHub data' }, { status: 500 }); 53 - } 54 - };
···
-53
src/routes/api/github/contributors/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { RequestHandler } from './$types'; 3 - 4 - const GithubContributorsAPIURL = 5 - 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; 6 - 7 - export const GET: RequestHandler = async ({ url, platform }) => { 8 - const owner = url.searchParams.get('owner'); 9 - const repo = url.searchParams.get('repo'); 10 - 11 - if (!owner || !repo) { 12 - return json({ error: 'Missing owner or repo parameter' }, { status: 400 }); 13 - } 14 - 15 - const cacheKey = `#github-contributors:${owner}/${repo}`; 16 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 17 - 18 - if (cachedData) { 19 - const parsedCache = JSON.parse(cachedData); 20 - 21 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 22 - const now = Date.now(); 23 - 24 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 25 - return json(parsedCache.data); 26 - } 27 - } 28 - 29 - try { 30 - const response = await fetch( 31 - `${GithubContributorsAPIURL}?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 32 - ); 33 - 34 - if (!response.ok) { 35 - return json( 36 - { error: 'Failed to fetch GitHub contributors ' + response.statusText }, 37 - { status: response.status } 38 - ); 39 - } 40 - 41 - const data = await response.json(); 42 - 43 - await platform?.env?.USER_DATA_CACHE?.put( 44 - cacheKey, 45 - JSON.stringify({ data, updatedAt: Date.now() }) 46 - ); 47 - 48 - return json(data); 49 - } catch (error) { 50 - console.error('Error fetching GitHub contributors:', error); 51 - return json({ error: 'Failed to fetch GitHub contributors' }, { status: 500 }); 52 - } 53 - };
···
-89
src/routes/api/lastfm/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { RequestHandler } from './$types'; 3 - import { env } from '$env/dynamic/private'; 4 - 5 - const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; 6 - 7 - const ALLOWED_METHODS = [ 8 - 'user.getRecentTracks', 9 - 'user.getTopTracks', 10 - 'user.getTopAlbums', 11 - 'user.getInfo' 12 - ]; 13 - 14 - const CACHE_TTL: Record<string, number> = { 15 - 'user.getRecentTracks': 15 * 60 * 1000, 16 - 'user.getTopTracks': 60 * 60 * 1000, 17 - 'user.getTopAlbums': 60 * 60 * 1000, 18 - 'user.getInfo': 12 * 60 * 60 * 1000 19 - }; 20 - 21 - export const GET: RequestHandler = async ({ url, platform }) => { 22 - const method = url.searchParams.get('method'); 23 - const user = url.searchParams.get('user'); 24 - const period = url.searchParams.get('period') || '7day'; 25 - const limit = url.searchParams.get('limit') || '50'; 26 - 27 - if (!method || !user) { 28 - return json({ error: 'Missing method or user parameter' }, { status: 400 }); 29 - } 30 - 31 - if (!ALLOWED_METHODS.includes(method)) { 32 - return json({ error: 'Method not allowed' }, { status: 400 }); 33 - } 34 - 35 - const cacheKey = `#lastfm:${method}:${user}:${period}:${limit}`; 36 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 37 - 38 - if (cachedData) { 39 - const parsed = JSON.parse(cachedData); 40 - const ttl = CACHE_TTL[method] || 60 * 60 * 1000; 41 - 42 - if (Date.now() - (parsed._cachedAt || 0) < ttl) { 43 - return json(parsed); 44 - } 45 - } 46 - 47 - const apiKey = env?.LASTFM_API_KEY; 48 - if (!apiKey) { 49 - return json({ error: 'Last.fm API key not configured' }, { status: 500 }); 50 - } 51 - 52 - try { 53 - const params = new URLSearchParams({ 54 - method, 55 - user, 56 - api_key: apiKey, 57 - format: 'json', 58 - limit 59 - }); 60 - 61 - if (method === 'user.getTopTracks' || method === 'user.getTopAlbums') { 62 - params.set('period', period); 63 - } 64 - 65 - const response = await fetch(`${LASTFM_API_URL}?${params}`); 66 - 67 - if (!response.ok) { 68 - return json( 69 - { error: 'Failed to fetch Last.fm data: ' + response.statusText }, 70 - { status: response.status } 71 - ); 72 - } 73 - 74 - const data = await response.json(); 75 - 76 - if (data.error) { 77 - return json({ error: data.message || 'Last.fm API error' }, { status: 400 }); 78 - } 79 - 80 - data._cachedAt = Date.now(); 81 - 82 - await platform?.env?.USER_DATA_CACHE?.put(cacheKey, JSON.stringify(data)); 83 - 84 - return json(data); 85 - } catch (error) { 86 - console.error('Error fetching Last.fm data:', error); 87 - return json({ error: 'Failed to fetch Last.fm data' }, { status: 500 }); 88 - } 89 - };
···
-44
src/routes/api/npmx-leaderboard/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - import type { RequestHandler } from './$types'; 3 - 4 - const LEADERBOARD_API_URL = 5 - 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; 6 - 7 - export const GET: RequestHandler = async ({ platform }) => { 8 - const cacheKey = '#npmx-leaderboard:likes'; 9 - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); 10 - 11 - if (cachedData) { 12 - const parsedCache = JSON.parse(cachedData); 13 - 14 - const TWELVE_HOURS = 12 * 60 * 60 * 1000; 15 - const now = Date.now(); 16 - 17 - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { 18 - return json(parsedCache.data); 19 - } 20 - } 21 - 22 - try { 23 - const response = await fetch(LEADERBOARD_API_URL); 24 - 25 - if (!response.ok) { 26 - return json( 27 - { error: 'Failed to fetch npmx leaderboard ' + response.statusText }, 28 - { status: response.status } 29 - ); 30 - } 31 - 32 - const data = await response.json(); 33 - 34 - await platform?.env?.USER_DATA_CACHE?.put( 35 - cacheKey, 36 - JSON.stringify({ data, updatedAt: Date.now() }) 37 - ); 38 - 39 - return json(data); 40 - } catch (error) { 41 - console.error('Error fetching npmx leaderboard:', error); 42 - return json({ error: 'Failed to fetch npmx leaderboard' }, { status: 500 }); 43 - } 44 - };
···
+6 -3
src/routes/api/reloadRecent/+server.ts
··· 1 import { getDetailedProfile } from '$lib/atproto'; 2 import { json } from '@sveltejs/kit'; 3 import type { AppBskyActorDefs } from '@atcute/bluesky'; 4 5 export async function GET({ platform }) { 6 - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); 7 - const existingUsers = await platform?.env?.USER_DATA_CACHE?.get('updatedBlentos'); 8 9 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 10 ? JSON.parse(existingUsers) ··· 21 22 const newProfiles = await Promise.all(newProfilesPromises); 23 24 - await platform?.env?.USER_DATA_CACHE.put('updatedBlentos', JSON.stringify(newProfiles)); 25 26 return json('ok'); 27 }
··· 1 import { getDetailedProfile } from '$lib/atproto'; 2 + import { createCache } from '$lib/cache'; 3 import { json } from '@sveltejs/kit'; 4 import type { AppBskyActorDefs } from '@atcute/bluesky'; 5 6 export async function GET({ platform }) { 7 + const cache = createCache(platform); 8 + if (!cache) return json('no cache'); 9 + 10 + const existingUsers = await cache.get('meta', 'updatedBlentos'); 11 12 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 13 ? JSON.parse(existingUsers) ··· 24 25 const newProfiles = await Promise.all(newProfilesPromises); 26 27 + await cache.put('meta', 'updatedBlentos', JSON.stringify(newProfiles)); 28 29 return json('ok'); 30 }
+9 -8
src/routes/api/update/+server.ts
··· 1 - import type { UserCache } from '$lib/types'; 2 import { getCache, loadData } from '$lib/website/load'; 3 - import type { AppBskyActorDefs } from '@atcute/bluesky'; 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: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 11 ? JSON.parse(existingUsers) 12 : []; 13 14 const existingUsersHandle = existingUsersArray.map((v) => v.handle); 15 - 16 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 17 18 for (const handle of existingUsersHandle) { 19 if (!handle) continue; 20 21 try { 22 - const cached = await getCache(handle, 'self', cache as UserCache); 23 - if (!cached) await loadData(handle, cache as UserCache, true); 24 } catch (error) { 25 console.error(error); 26 return json('error');
··· 1 + import { createCache } from '$lib/cache'; 2 import { getCache, loadData } from '$lib/website/load'; 3 + import { env } from '$env/dynamic/private'; 4 import { json } from '@sveltejs/kit'; 5 + import type { AppBskyActorDefs } from '@atcute/bluesky'; 6 7 export async function GET({ platform }) { 8 + const cache = createCache(platform); 9 + if (!cache) return json('no cache'); 10 + 11 + const existingUsers = await cache.get('meta', 'updatedBlentos'); 12 13 const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers 14 ? JSON.parse(existingUsers) 15 : []; 16 17 const existingUsersHandle = existingUsersArray.map((v) => v.handle); 18 19 for (const handle of existingUsersHandle) { 20 if (!handle) continue; 21 22 try { 23 + const cached = await getCache(handle, 'self', cache); 24 + if (!cached) await loadData(handle, cache, true, 'self', env); 25 } catch (error) { 26 console.error(error); 27 return json('error');
+6 -5
src/routes/edit/+page.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 4 import type { ActorIdentifier } from '@atcute/lexicons'; 5 6 export async function load({ platform, request }) { ··· 8 9 const kv = platform?.env?.CUSTOM_DOMAINS; 10 11 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 12 - const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); 13 14 if (kv && customDomain) { 15 try { 16 const did = await kv.get(customDomain); 17 18 - if (did) return await loadData(did as ActorIdentifier, cache as UserCache); 19 } catch (error) { 20 console.error('failed to get custom domain kv', error); 21 } 22 } 23 24 - return await loadData(handle as ActorIdentifier, cache as UserCache); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 + import { createCache } from '$lib/cache'; 5 import type { ActorIdentifier } from '@atcute/lexicons'; 6 7 export async function load({ platform, request }) { ··· 9 10 const kv = platform?.env?.CUSTOM_DOMAINS; 11 12 + const cache = createCache(platform); 13 + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); 14 15 if (kv && customDomain) { 16 try { 17 const did = await kv.get(customDomain); 18 19 + if (did) return await loadData(did as ActorIdentifier, cache, false, 'self', privateEnv); 20 } catch (error) { 21 console.error('failed to get custom domain kv', error); 22 } 23 } 24 25 + return await loadData(handle as ActorIdentifier, cache, false, 'self', privateEnv); 26 }
+5 -4
src/routes/p/[[page]]/+layout.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 - import type { UserCache } from '$lib/types'; 4 import type { Did, Handle } from '@atcute/lexicons'; 5 6 export async function load({ params, platform, request }) { 7 - const cache = platform?.env?.USER_DATA_CACHE as unknown; 8 9 const handle = env.PUBLIC_HANDLE; 10 ··· 15 if (kv && customDomain) { 16 try { 17 const did = await kv.get(customDomain); 18 - return await loadData(did as Did, cache as UserCache, false, params.page); 19 } catch { 20 console.error('failed'); 21 } 22 } 23 24 - return await loadData(handle as Handle, cache as UserCache, false, params.page); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 + import { createCache } from '$lib/cache'; 5 import type { Did, Handle } from '@atcute/lexicons'; 6 7 export async function load({ params, platform, request }) { 8 + const cache = createCache(platform); 9 10 const handle = env.PUBLIC_HANDLE; 11 ··· 16 if (kv && customDomain) { 17 try { 18 const did = await kv.get(customDomain); 19 + return await loadData(did as Did, cache, false, params.page, privateEnv); 20 } catch { 21 console.error('failed'); 22 } 23 } 24 25 + return await loadData(handle as Handle, cache, false, params.page, privateEnv); 26 }
-30
src/routes/random/+page.server.ts
··· 1 - import type { UserCache, WebsiteData } from '$lib/types.js'; 2 - import { getCache } from '$lib/website/load.js'; 3 - import { error } from '@sveltejs/kit'; 4 - 5 - export async function load({ platform }) { 6 - const cache = platform?.env?.USER_DATA_CACHE; 7 - 8 - const list = await cache?.list(); 9 - 10 - if (!list) { 11 - throw error(404); 12 - } 13 - 14 - let foundData: WebsiteData | undefined = undefined; 15 - let i = 0; 16 - 17 - while (!foundData && i < 20) { 18 - const rando = Math.floor(Math.random() * list.keys.length); 19 - console.log(list.keys[rando].name); 20 - 21 - foundData = await getCache(list.keys[rando].name, 'self', cache as unknown as UserCache); 22 - 23 - if (!foundData?.cards.length) foundData = undefined; 24 - i++; 25 - } 26 - 27 - if (!foundData) throw error(404); 28 - 29 - return foundData; 30 - }
···
-40
src/routes/random/+page.svelte
··· 1 - <script lang="ts"> 2 - import Website from '$lib/website/Website.svelte'; 3 - import { Button } from '@foxui/core'; 4 - 5 - let { data } = $props(); 6 - </script> 7 - 8 - <svelte:body 9 - onkeydown={(e) => { 10 - if (e.key === 'ArrowRight' || e.key === 'r') { 11 - window.location.reload(); 12 - } 13 - }} 14 - /> 15 - 16 - <Website {data} /> 17 - 18 - <Button 19 - onclick={() => { 20 - window.location.reload(); 21 - }} 22 - size="lg" 23 - class="bg-accent-100 hover:bg-accent-200 dark:bg-accent-950/50 dark:hover:bg-accent-900/50 fixed right-4 bottom-4" 24 - ><svg 25 - xmlns="http://www.w3.org/2000/svg" 26 - width="24" 27 - height="24" 28 - viewBox="0 0 24 24" 29 - fill="none" 30 - stroke="currentColor" 31 - stroke-width="2" 32 - stroke-linecap="round" 33 - stroke-linejoin="round" 34 - class="lucide lucide-dices-icon lucide-dices" 35 - ><rect width="12" height="12" x="2" y="10" rx="2" ry="2" /><path 36 - d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6" 37 - /><path d="M6 18h.01" /><path d="M10 14h.01" /><path d="M15 6h.01" /><path d="M18 9h.01" /></svg 38 - >Next 39 - <span class="sr-only">Next random profile</span></Button 40 - >
···
-16
src/routes/test/domains/+server.ts
··· 1 - import { json } from '@sveltejs/kit'; 2 - 3 - export async function GET({ platform }) { 4 - const kv = platform?.env?.CUSTOM_DOMAINS; 5 - if (!kv) return json({ error: 'KV not available' }, { status: 500 }); 6 - 7 - const list = await kv.list(); 8 - const entries: Record<string, string> = {}; 9 - 10 - for (const key of list.keys) { 11 - const value = await kv.get(key.name); 12 - entries[key.name] = value ?? ''; 13 - } 14 - 15 - return json(entries); 16 - }
···
+4
svelte.config.js
··· 11 adapter: adapter(), 12 paths: { 13 base: '' 14 } 15 }, 16 compilerOptions: { 17 experimental: { 18 async: true
··· 11 adapter: adapter(), 12 paths: { 13 base: '' 14 + }, 15 + experimental: { 16 + remoteFunctions: true 17 } 18 }, 19 + 20 compilerOptions: { 21 experimental: { 22 async: true