your personal website on atproto - mirror blento.app

oauth fixes? cleanup add photo gallery (hidden for now) add robots.txt

Florian dbe2f418 3440db05

+573 -260
+1
docs/Autofill.md
··· 9 9 - com.germnetwork.declaration 10 10 - pub.leaflet.document 11 11 - blue.flashes.actor.portfolio 12 + - social.grain.gallery 12 13 - add bluesky profile card
+6 -3
docs/CardIdeas.md
··· 21 21 - bluesky account card (showing follow button, follower count, avatar, name, cover image) 22 22 - youtube channel card (showing channel name, latest videos, follow button?) 23 23 - bluesky posts workcloud 24 + - steam game 24 25 25 26 ## bluesky 26 27 27 28 - bluesky feed 28 - - bluesky post (fixed or latest) 29 + - bluesky post (pinned, latest or fixed) 29 30 - social accounts card (multiple) 30 31 31 32 ## social ··· 36 37 37 38 - leaflet 38 39 - skywatched 39 - - teal.fm 40 + - teal.fm 41 + - [x] last played songs 40 42 - tangled.sh 41 43 - popfeed.social 42 44 - reading goal 43 - - latest ratings 45 + - [x] latest ratings 44 46 - lists 45 47 - smokesignal.events (https://pdsls.dev/at://did:plc:xbtmt2zjwlrfegqvch7fboei/events.smokesignal.calendar.event/3ltn2qrxf3626) 46 48 - statusphere.xyz ··· 48 50 - flashes.blue (https://pdsls.dev/at://did:plc:aytgljyikzbtgrnac2u4ccft/blue.flashes.actor.portfolio, https://app.flashes.blue/profile/j4ck.xyz) 49 51 - room: flo-bit.dev/room 50 52 - plyr.fm 53 + - grain.social 51 54 52 55 ## programming 53 56
+1
package.json
··· 49 49 "@foxui/core": "^0.4.7", 50 50 "@foxui/social": "^0.4.7", 51 51 "@foxui/time": "^0.4.7", 52 + "@foxui/visual": "^0.4.7", 52 53 "@tailwindcss/typography": "^0.5.16", 53 54 "@tiptap/core": "^2.12.0", 54 55 "@tiptap/extension-document": "^2.12.0",
+82
pnpm-lock.yaml
··· 38 38 '@foxui/time': 39 39 specifier: ^0.4.7 40 40 version: 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 41 + '@foxui/visual': 42 + specifier: ^0.4.7 43 + version: 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 41 44 '@tailwindcss/typography': 42 45 specifier: ^0.5.16 43 46 version: 0.5.16(tailwindcss@4.1.5) ··· 668 671 669 672 '@foxui/time@0.4.7': 670 673 resolution: {integrity: sha512-N4jN1QfUi7IY53MQETZp4MDj6DwwONoRi4yrN96SjpB71w7cvhli1jQCSG4QqCtyvISaizlg4T5gzORg7PYWrA==, tarball: https://registry.npmjs.org/@foxui/time/-/time-0.4.7.tgz} 674 + peerDependencies: 675 + svelte: '>=5' 676 + tailwindcss: '>=3' 677 + 678 + '@foxui/visual@0.4.7': 679 + resolution: {integrity: sha512-POcVBvmeHD5Z3UFBANIawn6mBZLB5XEy/jqkFp1NV1OnHSdE4vNjWYSva3GU1C/OVVPxjfNFB0SisZlHWmk5FA==, tarball: https://registry.npmjs.org/@foxui/visual/-/visual-0.4.7.tgz} 671 680 peerDependencies: 672 681 svelte: '>=5' 673 682 tailwindcss: '>=3' ··· 1502 1511 camelize@1.0.1: 1503 1512 resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==, tarball: https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz} 1504 1513 1514 + canvas-confetti@1.9.4: 1515 + resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==, tarball: https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz} 1516 + 1505 1517 chalk@4.1.2: 1506 1518 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, tarball: https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz} 1507 1519 engines: {node: '>=10'} ··· 1512 1524 cheerio@1.0.0-rc.11: 1513 1525 resolution: {integrity: sha512-bQwNaDIBKID5ts/DsdhxrjqFXYfLw4ste+wMKqWA8DyKcS4qwsPP4Bk8ZNaTJjvpiX/qW3BT4sU7d6Bh5i+dag==, tarball: https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.11.tgz} 1514 1526 engines: {node: '>= 6'} 1527 + 1528 + cheerio@1.1.2: 1529 + resolution: {integrity: sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==, tarball: https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz} 1530 + engines: {node: '>=20.18.1'} 1515 1531 1516 1532 chokidar@4.0.3: 1517 1533 resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==, tarball: https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz} ··· 1673 1689 encodeurl@2.0.0: 1674 1690 resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, tarball: https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz} 1675 1691 engines: {node: '>= 0.8'} 1692 + 1693 + encoding-sniffer@0.2.1: 1694 + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==, tarball: https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz} 1676 1695 1677 1696 enhanced-resolve@5.18.1: 1678 1697 resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==, tarball: https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz} ··· 1950 1969 hls.js@1.6.15: 1951 1970 resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==, tarball: https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz} 1952 1971 1972 + htmlparser2@10.0.0: 1973 + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==, tarball: https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz} 1974 + 1953 1975 htmlparser2@8.0.2: 1954 1976 resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==, tarball: https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz} 1955 1977 ··· 2303 2325 2304 2326 parse5-htmlparser2-tree-adapter@7.1.0: 2305 2327 resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==, tarball: https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz} 2328 + 2329 + parse5-parser-stream@7.1.2: 2330 + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==, tarball: https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz} 2306 2331 2307 2332 parse5@7.3.0: 2308 2333 resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==, tarball: https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz} ··· 2939 2964 w3c-keyname@2.2.8: 2940 2965 resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==, tarball: https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz} 2941 2966 2967 + whatwg-encoding@3.1.1: 2968 + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==, tarball: https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz} 2969 + engines: {node: '>=18'} 2970 + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation 2971 + 2972 + whatwg-mimetype@4.0.0: 2973 + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==, tarball: https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz} 2974 + engines: {node: '>=18'} 2975 + 2942 2976 which@2.0.2: 2943 2977 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmjs.org/which/-/which-2.0.2.tgz} 2944 2978 engines: {node: '>= 8'} ··· 3424 3458 svelte: 5.46.4 3425 3459 tailwindcss: 4.1.5 3426 3460 3461 + '@foxui/visual@0.4.7(svelte@5.46.4)(tailwindcss@4.1.5)': 3462 + dependencies: 3463 + '@foxui/colors': 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 3464 + '@foxui/core': 0.4.7(svelte@5.46.4)(tailwindcss@4.1.5) 3465 + bits-ui: 1.8.0(svelte@5.46.4) 3466 + canvas-confetti: 1.9.4 3467 + cheerio: 1.1.2 3468 + svelte: 5.46.4 3469 + tailwindcss: 4.1.5 3470 + 3427 3471 '@humanfs/core@0.19.1': {} 3428 3472 3429 3473 '@humanfs/node@0.16.6': ··· 4211 4255 4212 4256 camelize@1.0.1: {} 4213 4257 4258 + canvas-confetti@1.9.4: {} 4259 + 4214 4260 chalk@4.1.2: 4215 4261 dependencies: 4216 4262 ansi-styles: 4.3.0 ··· 4235 4281 parse5: 7.3.0 4236 4282 parse5-htmlparser2-tree-adapter: 7.1.0 4237 4283 tslib: 2.8.1 4284 + 4285 + cheerio@1.1.2: 4286 + dependencies: 4287 + cheerio-select: 2.1.0 4288 + dom-serializer: 2.0.0 4289 + domhandler: 5.0.3 4290 + domutils: 3.2.2 4291 + encoding-sniffer: 0.2.1 4292 + htmlparser2: 10.0.0 4293 + parse5: 7.3.0 4294 + parse5-htmlparser2-tree-adapter: 7.1.0 4295 + parse5-parser-stream: 7.1.2 4296 + undici: 7.14.0 4297 + whatwg-mimetype: 4.0.0 4238 4298 4239 4299 chokidar@4.0.3: 4240 4300 dependencies: ··· 4369 4429 4370 4430 encodeurl@2.0.0: {} 4371 4431 4432 + encoding-sniffer@0.2.1: 4433 + dependencies: 4434 + iconv-lite: 0.6.3 4435 + whatwg-encoding: 3.1.1 4436 + 4372 4437 enhanced-resolve@5.18.1: 4373 4438 dependencies: 4374 4439 graceful-fs: 4.2.11 ··· 4728 4793 hex-rgb@4.3.0: {} 4729 4794 4730 4795 hls.js@1.6.15: {} 4796 + 4797 + htmlparser2@10.0.0: 4798 + dependencies: 4799 + domelementtype: 2.3.0 4800 + domhandler: 5.0.3 4801 + domutils: 3.2.2 4802 + entities: 6.0.1 4731 4803 4732 4804 htmlparser2@8.0.2: 4733 4805 dependencies: ··· 5044 5116 domhandler: 5.0.3 5045 5117 parse5: 7.3.0 5046 5118 5119 + parse5-parser-stream@7.1.2: 5120 + dependencies: 5121 + parse5: 7.3.0 5122 + 5047 5123 parse5@7.3.0: 5048 5124 dependencies: 5049 5125 entities: 6.0.1 ··· 5686 5762 vite: 6.3.5(jiti@2.4.2)(lightningcss@1.29.2) 5687 5763 5688 5764 w3c-keyname@2.2.8: {} 5765 + 5766 + whatwg-encoding@3.1.1: 5767 + dependencies: 5768 + iconv-lite: 0.6.3 5769 + 5770 + whatwg-mimetype@4.0.0: {} 5689 5771 5690 5772 which@2.0.2: 5691 5773 dependencies:
+67
src/lib/cards/PhotoGalleryCard/PhotoGalleryCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { 5 + getAdditionalUserData, 6 + getDidContext, 7 + getHandleContext, 8 + getIsMobile 9 + } from '$lib/website/context'; 10 + import { CardDefinitionsByType } from '..'; 11 + import { getImageBlobUrl, parseUri } from '$lib/oauth/utils'; 12 + 13 + import { ImageMasonry } from '@foxui/visual'; 14 + 15 + let { item }: { item: Item } = $props(); 16 + 17 + const data = getAdditionalUserData(); 18 + // svelte-ignore state_referenced_locally 19 + let feed = $state((data[item.cardType] as any)?.[item.cardData.galleryUri]); 20 + 21 + let did = getDidContext(); 22 + let handle = getHandleContext(); 23 + 24 + onMount(async () => { 25 + console.log(feed); 26 + if (!feed) { 27 + feed = ( 28 + (await CardDefinitionsByType[item.cardType]?.loadData?.([item], { 29 + did, 30 + handle 31 + })) as any 32 + )?.[item.cardData.galleryUri]; 33 + 34 + console.log(feed); 35 + 36 + data[item.cardType] = feed; 37 + } 38 + }); 39 + 40 + let images = $derived( 41 + feed 42 + ?.toSorted((a, b) => { 43 + return (a.value.position ?? 0) - (b.value.position ?? 0); 44 + }) 45 + .map((i) => { 46 + const { did } = parseUri(i.uri); 47 + return { 48 + src: getImageBlobUrl({ did, link: i.value.photo?.ref?.$link }), 49 + width: i.value.aspectRatio.width, 50 + height: i.value.aspectRatio.height, 51 + position: i.value.position ?? 0 52 + }; 53 + }) 54 + ); 55 + $inspect(images); 56 + let isMobile = getIsMobile(); 57 + </script> 58 + 59 + <div class="z-10 flex h-full w-full flex-col gap-4 overflow-y-scroll p-4"> 60 + {#each (feed ?? []).slice(0, 20) as photo}{/each} 61 + 62 + <ImageMasonry 63 + images={images ?? []} 64 + showNames={false} 65 + maxColumns={!isMobile() && item.w > 4 ? 3 : 2} 66 + /> 67 + </div>
+58
src/lib/cards/PhotoGalleryCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import { getRecord, listRecords } from '$lib/oauth/atproto'; 3 + import PhotoGalleryCard from './PhotoGalleryCard.svelte'; 4 + import { parseUri } from '$lib/oauth/utils'; 5 + import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/atproto/repo/listRecords'; 6 + 7 + export const PhotoGalleryCardDefinition = { 8 + type: 'photoGallery', 9 + contentComponent: PhotoGalleryCard, 10 + createNew: (card) => { 11 + // random grain.social url for testing 12 + card.cardData.galleryUri = 13 + 'at://did:plc:tas6hj2xjrqben5653v5kohk/social.grain.gallery/3mclhsljs6h2w'; 14 + 15 + card.w = 4; 16 + card.mobileW = 8; 17 + card.h = 3; 18 + card.mobileH = 6; 19 + }, 20 + loadData: async (items) => { 21 + const itemsData: Record<string, ListRecord[]> = {}; 22 + 23 + const galleryItems: Record<string, ListRecord[] | undefined> = { 24 + 'social.grain.gallery.item': undefined 25 + }; 26 + 27 + for (const item of items) { 28 + if (!item.cardData.galleryUri) continue; 29 + 30 + const { did, collection } = parseUri(item.cardData.galleryUri); 31 + 32 + if (collection === 'social.grain.gallery') { 33 + const itemCollection = 'social.grain.gallery.item'; 34 + 35 + if (!galleryItems[itemCollection]) { 36 + galleryItems[itemCollection] = await listRecords({ 37 + did, 38 + collection: itemCollection 39 + }); 40 + } 41 + 42 + const images = galleryItems['social.grain.gallery.item'] 43 + ?.filter((i) => i.value.gallery === item.cardData.galleryUri) 44 + .map(async (i) => { 45 + const itemData = parseUri(i.value.item as string); 46 + const record = await getRecord(itemData); 47 + return { ...record, value: { ...record.value, ...i.value } }; 48 + }); 49 + 50 + itemsData[item.cardData.galleryUri] = await Promise.all(images); 51 + } 52 + } 53 + 54 + return itemsData; 55 + }, 56 + minW: 4 57 + //sidebarButtonText: 'Photo Gallery' 58 + } as CardDefinition & { type: 'photoGallery' };
+3 -1
src/lib/cards/index.ts
··· 20 20 import { GithubProfileCardDefitition } from './GitHubProfileCard'; 21 21 import { PopfeedReviewsCardDefinition } from './PopfeedReviews'; 22 22 import { TealFMPlaysCardDefinition } from './TealFMPlaysCard'; 23 + import { PhotoGalleryCardDefinition } from './PhotoGalleryCard'; 23 24 24 25 export const AllCardDefinitions = [ 25 26 ImageCardDefinition, ··· 42 43 GithubProfileCardDefitition, 43 44 TetrisCardDefinition, 44 45 PopfeedReviewsCardDefinition, 45 - TealFMPlaysCardDefinition 46 + TealFMPlaysCardDefinition, 47 + PhotoGalleryCardDefinition 46 48 ] as const; 47 49 48 50 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+8 -1
src/lib/oauth/auth.svelte.ts
··· 54 54 if (params.size > 0) { 55 55 await finalizeLogin(params, did); 56 56 } else if (did) { 57 + console.log('resuming session'); 57 58 await resumeSession(did); 58 59 } 59 60 ··· 118 119 119 120 async function resumeSession(did: string) { 120 121 try { 121 - const session = await getSession(did as `did:${string}`, { allowStale: true }); 122 + const session = await getSession(did as `did:${string}:${string}`, { allowStale: true }); 123 + console.log('got session', session); 124 + 125 + if (session.token.expires_at && session.token.expires_at < Date.now()) { 126 + throw Error('session expired'); 127 + } 122 128 client.session = session; 123 129 124 130 setAgentAndXRPC(session); ··· 128 134 client.isLoggedIn = true; 129 135 } catch (error) { 130 136 console.error('error resuming session', error); 137 + logout(); 131 138 } 132 139 } 133 140
+39
src/lib/website/Account.svelte
··· 1 + <script lang="ts"> 2 + import { client, login, logout } from '$lib/oauth'; 3 + import type { WebsiteData } from '$lib/types'; 4 + import { Button, Popover } from '@foxui/core'; 5 + 6 + let { 7 + data 8 + }: { 9 + data: WebsiteData; 10 + } = $props(); 11 + 12 + let settingsPopoverOpen = $state(false); 13 + </script> 14 + 15 + {#if client.isLoggedIn && client.profile} 16 + <div class="fixed top-4 right-4 z-20"> 17 + <Popover sideOffset={8} bind:open={settingsPopoverOpen} class="bg-base-100 dark:bg-base-900"> 18 + {#snippet child({ props })} 19 + <button {...props}> 20 + <img src={client.profile?.avatar} alt="" class="size-15 rounded-full" /> 21 + </button> 22 + {/snippet} 23 + 24 + <Button variant="ghost" onclick={logout}>Logout</Button> 25 + </Popover> 26 + </div> 27 + {:else} 28 + <div 29 + class="dark:bg-base-950 border-base-200 dark:border-base-900 fixed top-4 right-4 z-20 flex flex-col gap-4 rounded-2xl border bg-white p-4 shadow-lg" 30 + > 31 + <span class="text-sm font-semibold">Login to edit your page</span> 32 + 33 + <Button 34 + onclick={async () => { 35 + await login(data.handle); 36 + }}>Login</Button 37 + > 38 + </div> 39 + {/if}
+278
src/lib/website/EditBar.svelte
··· 1 + <script lang="ts"> 2 + import { dev } from '$app/environment'; 3 + import { client } from '$lib/oauth'; 4 + import type { WebsiteData } from '$lib/types'; 5 + import { Button, Input, Navbar, Popover, Toggle } from '@foxui/core'; 6 + 7 + let { 8 + data, 9 + linkValue = $bindable(), 10 + newCard, 11 + addLink, 12 + showSettings = $bindable(), 13 + 14 + showingMobileView = $bindable(), 15 + isSaving = $bindable(), 16 + 17 + save, 18 + 19 + handleImageInputChange, 20 + handleVideoInputChange 21 + }: { 22 + data: WebsiteData; 23 + linkValue: string; 24 + newCard: (type: string) => void; 25 + addLink: (url: string) => void; 26 + 27 + showSettings: boolean; 28 + 29 + showingMobileView: boolean; 30 + 31 + isSaving: boolean; 32 + 33 + save: () => Promise<void>; 34 + 35 + handleImageInputChange: (evt: Event) => void; 36 + handleVideoInputChange: (evt: Event) => void; 37 + } = $props(); 38 + 39 + let linkPopoverOpen = $state(false); 40 + 41 + let imageInputRef: HTMLInputElement | undefined = $state(); 42 + let videoInputRef: HTMLInputElement | undefined = $state(); 43 + </script> 44 + 45 + <input 46 + type="file" 47 + accept="image/*" 48 + onchange={handleImageInputChange} 49 + class="hidden" 50 + multiple 51 + bind:this={imageInputRef} 52 + /> 53 + 54 + <input 55 + type="file" 56 + accept="video/*" 57 + onchange={handleVideoInputChange} 58 + class="hidden" 59 + multiple 60 + bind:this={videoInputRef} 61 + /> 62 + 63 + {#if client.isLoggedIn && client.profile?.did === data.did} 64 + <Navbar 65 + class={[ 66 + 'dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 md:mx-auto lg:inline-flex', 67 + !dev ? 'hidden' : '' 68 + ]} 69 + > 70 + <div class="flex items-center gap-2"> 71 + <Button 72 + size="iconLg" 73 + variant="ghost" 74 + class="backdrop-blur-none" 75 + onclick={() => { 76 + newCard('section'); 77 + }} 78 + > 79 + <svg 80 + xmlns="http://www.w3.org/2000/svg" 81 + viewBox="0 0 24 24" 82 + fill="none" 83 + stroke="currentColor" 84 + stroke-width="2" 85 + stroke-linecap="round" 86 + stroke-linejoin="round" 87 + ><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></svg 88 + > 89 + </Button> 90 + 91 + <Button 92 + size="iconLg" 93 + variant="ghost" 94 + class="backdrop-blur-none" 95 + onclick={() => { 96 + newCard('text'); 97 + }} 98 + > 99 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" 100 + ><path 101 + fill="none" 102 + stroke="currentColor" 103 + stroke-linecap="round" 104 + stroke-linejoin="round" 105 + stroke-width="2" 106 + d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 107 + /></svg 108 + > 109 + </Button> 110 + 111 + <Popover sideOffset={16} bind:open={linkPopoverOpen} class="bg-base-100 dark:bg-base-900"> 112 + {#snippet child({ props })} 113 + <Button 114 + size="iconLg" 115 + variant="ghost" 116 + class="backdrop-blur-none" 117 + onclick={() => { 118 + newCard('link'); 119 + }} 120 + {...props} 121 + > 122 + <svg 123 + xmlns="http://www.w3.org/2000/svg" 124 + fill="none" 125 + viewBox="-2 -2 28 28" 126 + stroke-width="2" 127 + stroke="currentColor" 128 + > 129 + <path 130 + stroke-linecap="round" 131 + stroke-linejoin="round" 132 + d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 133 + /> 134 + </svg> 135 + </Button> 136 + {/snippet} 137 + <Input 138 + spellcheck={false} 139 + type="url" 140 + bind:value={linkValue} 141 + onkeydown={(event) => { 142 + if (event.code === 'Enter') { 143 + addLink(linkValue); 144 + event.preventDefault(); 145 + } 146 + }} 147 + placeholder="Enter link" 148 + /> 149 + <Button onclick={() => addLink(linkValue)} size="icon" 150 + ><svg 151 + xmlns="http://www.w3.org/2000/svg" 152 + fill="none" 153 + viewBox="0 0 24 24" 154 + stroke-width="2" 155 + stroke="currentColor" 156 + class="size-6" 157 + > 158 + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 159 + </svg> 160 + </Button> 161 + </Popover> 162 + 163 + <Button 164 + size="iconLg" 165 + variant="ghost" 166 + class="backdrop-blur-none" 167 + onclick={() => { 168 + imageInputRef?.click(); 169 + }} 170 + > 171 + <svg 172 + xmlns="http://www.w3.org/2000/svg" 173 + fill="none" 174 + viewBox="0 0 24 24" 175 + stroke-width="2" 176 + stroke="currentColor" 177 + > 178 + <path 179 + stroke-linecap="round" 180 + stroke-linejoin="round" 181 + d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 182 + /> 183 + </svg> 184 + </Button> 185 + 186 + {#if dev} 187 + <Button 188 + size="iconLg" 189 + variant="ghost" 190 + class="backdrop-blur-none" 191 + onclick={() => { 192 + videoInputRef?.click(); 193 + }} 194 + > 195 + <svg 196 + xmlns="http://www.w3.org/2000/svg" 197 + fill="none" 198 + viewBox="0 0 24 24" 199 + stroke-width="1.5" 200 + stroke="currentColor" 201 + > 202 + <path 203 + stroke-linecap="round" 204 + stroke-linejoin="round" 205 + d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" 206 + /> 207 + </svg> 208 + </Button> 209 + {/if} 210 + 211 + <Button size="iconLg" variant="ghost" class="backdrop-blur-none" popovertarget="mobile-menu"> 212 + <svg 213 + xmlns="http://www.w3.org/2000/svg" 214 + fill="none" 215 + viewBox="0 0 24 24" 216 + stroke-width="1.5" 217 + stroke="currentColor" 218 + > 219 + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 220 + </svg> 221 + </Button> 222 + </div> 223 + <div class="flex items-center gap-2"> 224 + <Button 225 + size="iconLg" 226 + variant="ghost" 227 + class="backdrop-blur-none" 228 + onclick={() => { 229 + showSettings = true; 230 + }} 231 + > 232 + <svg 233 + xmlns="http://www.w3.org/2000/svg" 234 + fill="none" 235 + viewBox="0 0 24 24" 236 + stroke-width="1.5" 237 + stroke="currentColor" 238 + > 239 + <path 240 + stroke-linecap="round" 241 + stroke-linejoin="round" 242 + d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" 243 + /> 244 + <path 245 + stroke-linecap="round" 246 + stroke-linejoin="round" 247 + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 248 + /> 249 + </svg> 250 + </Button> 251 + <Toggle 252 + class="hidden bg-transparent backdrop-blur-none lg:block dark:bg-transparent" 253 + bind:pressed={showingMobileView} 254 + > 255 + <svg 256 + xmlns="http://www.w3.org/2000/svg" 257 + fill="none" 258 + viewBox="0 0 24 24" 259 + stroke-width="1.5" 260 + stroke="currentColor" 261 + class="size-6" 262 + > 263 + <path 264 + stroke-linecap="round" 265 + stroke-linejoin="round" 266 + d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" 267 + /> 268 + </svg> 269 + </Toggle> 270 + <Button 271 + disabled={isSaving} 272 + onclick={async () => { 273 + save(); 274 + }}>{isSaving ? 'Saving...' : 'Save'}</Button 275 + > 276 + </div> 277 + </Navbar> 278 + {/if}
+24 -253
src/lib/website/EditableWebsite.svelte
··· 32 32 import Settings from './Settings.svelte'; 33 33 import Head from './Head.svelte'; 34 34 import { compressImage } from '../helper'; 35 + import Account from './Account.svelte'; 36 + import EditBar from './EditBar.svelte'; 35 37 36 38 let { 37 39 data ··· 39 41 data: WebsiteData; 40 42 } = $props(); 41 43 42 - let imageInputRef: HTMLInputElement | undefined = $state(); 43 - let videoInputRef: HTMLInputElement | undefined = $state(); 44 44 let imageDragOver = $state(false); 45 45 let imageDragPosition: { x: number; y: number } | null = $state(null); 46 46 ··· 136 136 async function save() { 137 137 isSaving = true; 138 138 139 - await savePage(data, items, publication); 139 + try { 140 + await savePage(data, items, publication); 140 141 141 - publication = JSON.stringify(data.publication); 142 + publication = JSON.stringify(data.publication); 143 + } catch (error) { 144 + toast.error('Error saving page!'); 145 + } finally { 146 + isSaving = false; 147 + } 142 148 } 143 149 144 150 const sidebarItems = AllCardDefinitions.filter( ··· 528 534 529 535 <Settings bind:open={showSettings} bind:data /> 530 536 537 + <Account {data} /> 538 + 531 539 <Context {data}> 532 - <input 533 - type="file" 534 - accept="image/*" 535 - onchange={handleImageInputChange} 536 - class="hidden" 537 - multiple 538 - bind:this={imageInputRef} 539 - /> 540 - <input 541 - type="file" 542 - accept="video/*" 543 - onchange={handleVideoInputChange} 544 - class="hidden" 545 - multiple 546 - bind:this={videoInputRef} 547 - /> 548 - 549 540 {#if !dev} 550 541 <div 551 542 class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden" ··· 750 741 </div> 751 742 </div> 752 743 753 - <!-- <Settings bind:open={showSettings} /> --> 754 - 755 744 <Sidebar mobileOnly mobileClasses="lg:block p-4 gap-4"> 756 745 <div class="flex flex-col gap-2"> 757 746 {#each sidebarItems as cardDef} ··· 766 755 </div> 767 756 </Sidebar> 768 757 769 - {#if dev || (!client.isLoggedIn && !client.isInitializing) || client.profile?.did === data.did} 770 - <Navbar 771 - class={[ 772 - 'dark:bg-base-900 bg-base-100 top-auto bottom-2 mx-4 mt-3 max-w-3xl rounded-full px-4 md:mx-auto lg:inline-flex', 773 - !dev ? 'hidden' : '' 774 - ]} 775 - > 776 - <div class="flex items-center gap-2"> 777 - <Button 778 - size="iconLg" 779 - variant="ghost" 780 - class="backdrop-blur-none" 781 - onclick={() => { 782 - newCard('section'); 783 - }} 784 - > 785 - <svg 786 - xmlns="http://www.w3.org/2000/svg" 787 - viewBox="0 0 24 24" 788 - fill="none" 789 - stroke="currentColor" 790 - stroke-width="2" 791 - stroke-linecap="round" 792 - stroke-linejoin="round" 793 - ><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></svg 794 - > 795 - </Button> 796 - 797 - <Button 798 - size="iconLg" 799 - variant="ghost" 800 - class="backdrop-blur-none" 801 - onclick={() => { 802 - newCard('text'); 803 - }} 804 - > 805 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" 806 - ><path 807 - fill="none" 808 - stroke="currentColor" 809 - stroke-linecap="round" 810 - stroke-linejoin="round" 811 - stroke-width="2" 812 - d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 813 - /></svg 814 - > 815 - </Button> 816 - 817 - <Popover sideOffset={16} bind:open={linkPopoverOpen} class="bg-base-100 dark:bg-base-900"> 818 - {#snippet child({ props })} 819 - <Button 820 - size="iconLg" 821 - variant="ghost" 822 - class="backdrop-blur-none" 823 - onclick={() => { 824 - newCard('link'); 825 - }} 826 - {...props} 827 - > 828 - <svg 829 - xmlns="http://www.w3.org/2000/svg" 830 - fill="none" 831 - viewBox="-2 -2 28 28" 832 - stroke-width="2" 833 - stroke="currentColor" 834 - > 835 - <path 836 - stroke-linecap="round" 837 - stroke-linejoin="round" 838 - d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 839 - /> 840 - </svg> 841 - </Button> 842 - {/snippet} 843 - <Input 844 - spellcheck={false} 845 - type="url" 846 - bind:value={linkValue} 847 - onkeydown={(event) => { 848 - if (event.code === 'Enter') { 849 - addLink(linkValue); 850 - event.preventDefault(); 851 - } 852 - }} 853 - placeholder="Enter link" 854 - /> 855 - <Button onclick={() => addLink(linkValue)} size="icon" 856 - ><svg 857 - xmlns="http://www.w3.org/2000/svg" 858 - fill="none" 859 - viewBox="0 0 24 24" 860 - stroke-width="2" 861 - stroke="currentColor" 862 - class="size-6" 863 - > 864 - <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 865 - </svg> 866 - </Button> 867 - </Popover> 868 - 869 - <Button 870 - size="iconLg" 871 - variant="ghost" 872 - class="backdrop-blur-none" 873 - onclick={() => { 874 - imageInputRef?.click(); 875 - }} 876 - > 877 - <svg 878 - xmlns="http://www.w3.org/2000/svg" 879 - fill="none" 880 - viewBox="0 0 24 24" 881 - stroke-width="2" 882 - stroke="currentColor" 883 - > 884 - <path 885 - stroke-linecap="round" 886 - stroke-linejoin="round" 887 - d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 888 - /> 889 - </svg> 890 - </Button> 891 - 892 - {#if dev} 893 - <Button 894 - size="iconLg" 895 - variant="ghost" 896 - class="backdrop-blur-none" 897 - onclick={() => { 898 - videoInputRef?.click(); 899 - }} 900 - > 901 - <svg 902 - xmlns="http://www.w3.org/2000/svg" 903 - fill="none" 904 - viewBox="0 0 24 24" 905 - stroke-width="1.5" 906 - stroke="currentColor" 907 - > 908 - <path 909 - stroke-linecap="round" 910 - stroke-linejoin="round" 911 - d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" 912 - /> 913 - </svg> 914 - </Button> 915 - {/if} 916 - 917 - <Button 918 - size="iconLg" 919 - variant="ghost" 920 - class="backdrop-blur-none" 921 - popovertarget="mobile-menu" 922 - > 923 - <svg 924 - xmlns="http://www.w3.org/2000/svg" 925 - fill="none" 926 - viewBox="0 0 24 24" 927 - stroke-width="1.5" 928 - stroke="currentColor" 929 - > 930 - <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> 931 - </svg> 932 - </Button> 933 - </div> 934 - <div class="flex items-center gap-2"> 935 - <Button 936 - size="iconLg" 937 - variant="ghost" 938 - class="backdrop-blur-none" 939 - onclick={() => { 940 - showSettings = true; 941 - }} 942 - > 943 - <svg 944 - xmlns="http://www.w3.org/2000/svg" 945 - fill="none" 946 - viewBox="0 0 24 24" 947 - stroke-width="1.5" 948 - stroke="currentColor" 949 - > 950 - <path 951 - stroke-linecap="round" 952 - stroke-linejoin="round" 953 - d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" 954 - /> 955 - <path 956 - stroke-linecap="round" 957 - stroke-linejoin="round" 958 - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 959 - /> 960 - </svg> 961 - </Button> 962 - <Toggle 963 - class="hidden bg-transparent backdrop-blur-none lg:block dark:bg-transparent" 964 - bind:pressed={showingMobileView} 965 - > 966 - <svg 967 - xmlns="http://www.w3.org/2000/svg" 968 - fill="none" 969 - viewBox="0 0 24 24" 970 - stroke-width="1.5" 971 - stroke="currentColor" 972 - class="size-6" 973 - > 974 - <path 975 - stroke-linecap="round" 976 - stroke-linejoin="round" 977 - d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" 978 - /> 979 - </svg> 980 - </Toggle> 981 - {#if client.isLoggedIn} 982 - <Button 983 - disabled={isSaving} 984 - onclick={async () => { 985 - save(); 986 - }}>{isSaving ? 'Saving...' : 'Save'}</Button 987 - > 988 - {:else} 989 - <BlueskyLogin 990 - login={async (handle) => { 991 - await login(handle); 992 - return true; 993 - }} 994 - /> 995 - {/if} 996 - </div> 997 - </Navbar> 998 - {/if} 758 + <EditBar 759 + {data} 760 + bind:linkValue 761 + bind:isSaving 762 + bind:showingMobileView 763 + bind:showSettings 764 + {newCard} 765 + {addLink} 766 + {save} 767 + {handleImageInputChange} 768 + {handleVideoInputChange} 769 + /> 999 770 1000 771 <Toaster /> 1001 772 </Context>
+2 -1
src/lib/website/Profile.svelte
··· 6 6 import { env } from '$env/dynamic/public'; 7 7 import type { WebsiteData } from '$lib/types'; 8 8 import { getDescription, getName } from '$lib/helper'; 9 + import { page } from '$app/state'; 9 10 10 11 let { 11 12 data, ··· 51 52 52 53 {#if showEditButton && client.isLoggedIn && client.profile?.did === data.did} 53 54 <div> 54 - <Button href="{env.PUBLIC_IS_SELFHOSTED ? '' : client.profile?.handle}/edit" class="mt-2"> 55 + <Button href="{page.url}/edit" class="mt-2"> 55 56 <svg 56 57 xmlns="http://www.w3.org/2000/svg" 57 58 fill="none"
+1 -1
src/lib/website/Settings.svelte
··· 64 64 for="hide-profile" 65 65 class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 66 66 > 67 - Hide Profile 67 + Hide Profile Section 68 68 </Label> 69 69 </div> 70 70
+3
static/robots.txt
··· 1 + # allow crawling everything by default 2 + User-agent: * 3 + Disallow: