your personal website on atproto - mirror blento.app
26
fork

Configure Feed

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

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: