your personal website on atproto - mirror
0
fork

Configure Feed

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

add at proto collection card

Florian ef2c0d4c b86567fd

+275 -115
+3 -1
src/lib/EditableWebsite.svelte
··· 23 23 import { tick, type Component } from 'svelte'; 24 24 import type { CreationModalComponentProps } from './cards/types'; 25 25 import { dev } from '$app/environment'; 26 - import { setDidContext } from './website/context'; 26 + import { setDidContext, setHandleContext } from './website/context'; 27 27 import BaseEditingCard from './cards/BaseCard/BaseEditingCard.svelte'; 28 28 29 29 let { ··· 67 67 68 68 // svelte-ignore state_referenced_locally 69 69 setDidContext(did); 70 + // svelte-ignore state_referenced_locally 71 + setHandleContext(handle); 70 72 71 73 const getX = (item: Item) => (isMobile ? (item.mobileX ?? item.x) : item.x); 72 74 const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y);
+10 -1
src/lib/Website.svelte
··· 4 4 import { setIsMobile, sortItems } from './helper'; 5 5 import type { Item } from './types'; 6 6 import { innerWidth } from 'svelte/reactivity/window'; 7 - import { setDidContext } from './website/context'; 7 + import { setDidContext, setHandleContext } from './website/context'; 8 8 import BaseCard from './cards/BaseCard/BaseCard.svelte'; 9 + import { onMount } from 'svelte'; 10 + import { describeRepo } from './oauth/atproto'; 9 11 10 12 let { handle, did, items, data }: { handle: string; did: string; items: Item[]; data: any } = 11 13 $props(); ··· 16 18 17 19 // svelte-ignore state_referenced_locally 18 20 setDidContext(did); 21 + // svelte-ignore state_referenced_locally 22 + setHandleContext(handle); 23 + 19 24 20 25 let maxHeight = $derived( 21 26 items.reduce( ··· 25 30 ); 26 31 27 32 let container: HTMLDivElement | undefined = $state(); 33 + 34 + onMount(() => { 35 + describeRepo({did}); 36 + }); 28 37 </script> 29 38 30 39 <div class="@container/wrapper relative w-screen">
+48
src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte
··· 1 + <script lang="ts"> 2 + import { getAdditionalUserData } from '$lib/helper'; 3 + import { onMount } from 'svelte'; 4 + import type { ContentComponentProps } from '../types'; 5 + import { CardDefinitionsByType } from '..'; 6 + import { getDidContext, getHandleContext } from '$lib/website/context'; 7 + import { Badge, Button } from '@foxui/core'; 8 + 9 + let { item }: ContentComponentProps = $props(); 10 + 11 + const data = getAdditionalUserData(); 12 + // svelte-ignore state_referenced_locally 13 + let collections = $state(data[item.cardType] as string[]); 14 + 15 + let did = getDidContext(); 16 + let handle = getHandleContext(); 17 + 18 + onMount(async () => { 19 + if (!collections) { 20 + collections = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 21 + did, 22 + handle 23 + })) as string[]; 24 + 25 + data[item.cardType] = collections; 26 + } 27 + }); 28 + 29 + function getLink(collection: string) { 30 + const split = collection.split('.'); 31 + return `https://pdsls.dev/at://${did}#collections:${split[1]}.${split[0]}`; 32 + } 33 + </script> 34 + 35 + <div class="h-full overflow-y-scroll p-4"> 36 + <div class="mb-4 inline-flex w-full items-center justify-between font-semibold"> 37 + <span>My AT Protocol Collections</span> 38 + 39 + {#if collections} 40 + <Badge size="md">{collections.length}</Badge> 41 + {/if} 42 + </div> 43 + <div class="flex flex-wrap gap-2 overflow-y-scroll"> 44 + {#each collections ?? [] as collection} 45 + <Button target="_blank" href={getLink(collection)} size="sm">{collection}</Button> 46 + {/each} 47 + </div> 48 + </div>
+29
src/lib/cards/ATProtoCollectionsCard/SidebarItemATProtoCollectionsCard.svelte
··· 1 + <script lang="ts"> 2 + import { Button } from '@foxui/core'; 3 + 4 + let { onclick }: { onclick: () => void } = $props(); 5 + </script> 6 + 7 + <Button {onclick} variant="ghost" class="w-full justify-start"> 8 + <svg 9 + xmlns="http://www.w3.org/2000/svg" 10 + viewBox="0 0 24 24" 11 + fill="currentColor" 12 + class="text-accent-600 dark:text-accent-400" 13 + > 14 + <path 15 + d="M21 6.375c0 2.692-4.03 4.875-9 4.875S3 9.067 3 6.375 7.03 1.5 12 1.5s9 2.183 9 4.875Z" 16 + /> 17 + <path 18 + d="M12 12.75c2.685 0 5.19-.586 7.078-1.609a8.283 8.283 0 0 0 1.897-1.384c.016.121.025.244.025.368C21 12.817 16.97 15 12 15s-9-2.183-9-4.875c0-.124.009-.247.025-.368a8.285 8.285 0 0 0 1.897 1.384C6.809 12.164 9.315 12.75 12 12.75Z" 19 + /> 20 + <path 21 + d="M12 16.5c2.685 0 5.19-.586 7.078-1.609a8.282 8.282 0 0 0 1.897-1.384c.016.121.025.244.025.368 0 2.692-4.03 4.875-9 4.875s-9-2.183-9-4.875c0-.124.009-.247.025-.368a8.284 8.284 0 0 0 1.897 1.384C6.809 15.914 9.315 16.5 12 16.5Z" 22 + /> 23 + <path 24 + d="M12 20.25c2.685 0 5.19-.586 7.078-1.609a8.282 8.282 0 0 0 1.897-1.384c.016.121.025.244.025.368 0 2.692-4.03 4.875-9 4.875s-9-2.183-9-4.875c0-.124.009-.247.025-.368a8.284 8.284 0 0 0 1.897 1.384C6.809 19.664 9.315 20.25 12 20.25Z" 25 + /> 26 + </svg> 27 + 28 + AT Proto Collections 29 + </Button>
+25
src/lib/cards/ATProtoCollectionsCard/index.ts
··· 1 + import { describeRepo } from '$lib/oauth/atproto'; 2 + import type { CardDefinition } from '../types'; 3 + import ATProtoCollectionsCard from './ATProtoCollectionsCard.svelte'; 4 + import SidebarItemATProtoCollectionsCard from './SidebarItemATProtoCollectionsCard.svelte'; 5 + 6 + export const ATProtoCollectionsCardDefinition = { 7 + type: 'atprotocollections', 8 + contentComponent: ATProtoCollectionsCard, 9 + loadData: async (items, { did }) => { 10 + const data = (await describeRepo({ did })).data; 11 + console.log(data); 12 + const collections = new Set<string>(); 13 + for (const collection of data.collections) { 14 + const split = collection.split('.'); 15 + if (split.length > 1) collections.add(split[1] + '.' + split[0]); 16 + } 17 + 18 + return Array.from(collections); 19 + }, 20 + createNew: (item) => { 21 + item.w = 2; 22 + item.mobileW = 4; 23 + }, 24 + sidebarComponent: SidebarItemATProtoCollectionsCard 25 + } as CardDefinition & { type: 'atprotocollections' };
+7 -2
src/lib/cards/BlueskyPostCard/BlueskyPostCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { getAdditionalUserData } from '$lib/helper'; 3 + import type { Item } from '$lib/types'; 3 4 import { BlueskyPost } from '../../components/bluesky-post'; 4 5 5 - const feed = getAdditionalUserData().recentPosts?.feed; 6 + let { item }: { item: Item } = $props(); 7 + 8 + const data = getAdditionalUserData(); 9 + // svelte-ignore state_referenced_locally 10 + const feed = (data[item.cardType] as any).feed; 6 11 7 12 $inspect(feed); 8 13 </script> 9 14 10 15 <div class="flex h-full flex-col justify-center-safe overflow-y-scroll p-4"> 11 16 {#if feed?.[0].post} 12 - <BlueskyPost showLogo showBookmark={false} feedViewPost={feed?.[0].post}></BlueskyPost> 17 + <BlueskyPost showLogo feedViewPost={feed?.[0].post}></BlueskyPost> 13 18 <div class="h-4 w-full"></div> 14 19 {:else} 15 20 Your latest bluesky post will appear here.
+11 -1
src/lib/cards/BlueskyPostCard/index.ts
··· 1 + import { AtpBaseClient } from '@atproto/api'; 1 2 import type { CardDefinition } from '../types'; 2 3 import BlueskyPostCard from './BlueskyPostCard.svelte'; 3 4 import SidebarItemBlueskyPostCard from './SidebarItemBlueskyPostCard.svelte'; ··· 12 13 card.h = 2; 13 14 card.mobileH = 4; 14 15 }, 15 - sidebarComponent: SidebarItemBlueskyPostCard 16 + sidebarComponent: SidebarItemBlueskyPostCard, 17 + loadData: async (items, { did }) => { 18 + const agent = new AtpBaseClient({ service: 'https://api.bsky.app' }); 19 + const authorFeed = await agent.app.bsky.feed.getAuthorFeed({ 20 + actor: did, 21 + filter: 'posts_no_replies', 22 + limit: 2 23 + }); 24 + return JSON.parse(JSON.stringify(authorFeed.data)); 25 + } 16 26 } as CardDefinition & { type: 'latestPost' };
+34 -48
src/lib/cards/LivestreamCard/LivestreamCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from 'svelte'; 3 3 import Icon from './Icon.svelte'; 4 - import { getDidContext } from '$lib/website/context'; 4 + import { getDidContext, getHandleContext } from '$lib/website/context'; 5 5 import { listRecords } from '$lib/oauth/atproto'; 6 - import { getIsMobile } from '$lib/helper'; 6 + import { getAdditionalUserData, getIsMobile } from '$lib/helper'; 7 7 import type { ContentComponentProps } from '../types'; 8 8 import { getImageBlobUrl } from '$lib/website/utils'; 9 9 import { RelativeTime } from '@foxui/time'; 10 10 import { online } from 'svelte/reactivity/window'; 11 11 import { Badge } from '@foxui/core'; 12 + import { CardDefinitionsByType } from '..'; 12 13 13 14 let { item = $bindable() }: ContentComponentProps = $props(); 14 15 15 - let did = getDidContext(); 16 - 17 16 let isMobile = getIsMobile(); 18 17 19 18 let isLoaded = $state(false); 20 19 21 - let latestLivestream: 22 - | { 23 - createdAt: string; 24 - title: string; 25 - thumb?: string; 26 - href: string; 27 - online?: boolean; 28 - } 29 - | undefined = $state(); 30 - 31 - onMount(async () => { 32 - const records = await listRecords({ did, collection: 'place.stream.livestream', limit: 3 }); 33 - console.log(records); 34 - 35 - const values = Object.values(records); 36 - if (values?.length > 0) { 37 - const latest = JSON.parse(JSON.stringify(values[0])); 38 - console.log(latest); 39 - 40 - latestLivestream = { 41 - createdAt: latest.value.createdAt, 42 - title: latest.value.title as string, 43 - thumb: getImageBlobUrl({ link: latest.value.thumb?.ref.$link, did }), 44 - href: latest.value.canonicalUrl || latest.value.url, 45 - online: undefined 46 - }; 47 - } 20 + const data = getAdditionalUserData(); 21 + // svelte-ignore state_referenced_locally 22 + let latestLivestream = $state( 23 + data[item.cardType] as 24 + | { 25 + createdAt: string; 26 + title: string; 27 + thumb?: string; 28 + href: string; 29 + online?: boolean; 30 + } 31 + | undefined 32 + ); 48 33 49 - if (latestLivestream) { 50 - try { 51 - const segmentsResponse = await fetch( 52 - 'https://stream.place/xrpc/place.stream.live.getSegments?userDID=' + 53 - encodeURIComponent(did) 54 - ); 55 - const segments = await segmentsResponse.json(); 34 + let did = getDidContext(); 35 + let handle = getHandleContext(); 56 36 57 - const lastSegment = segments.segments[0]; 58 - const startTime = new Date(lastSegment.record.startTime).getTime(); 37 + onMount(async () => { 38 + if (!latestLivestream) { 39 + latestLivestream = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 40 + did, 41 + handle 42 + })) as 43 + | { 44 + createdAt: string; 45 + title: string; 46 + thumb?: string; 47 + href: string; 48 + online?: boolean; 49 + } 50 + | undefined; 59 51 60 - const FIVE_MINUTES = 5 * 60 * 1000; 61 - const now = Date.now(); 52 + data[item.cardType] = latestLivestream; 62 53 63 - latestLivestream.online = now - startTime <= FIVE_MINUTES; 64 - } catch (error) { 65 - console.error(error); 66 - } 54 + isLoaded = true; 67 55 } 68 - 69 - isLoaded = true; 70 56 }); 71 57 </script> 72 58
+51
src/lib/cards/LivestreamCard/index.ts
··· 1 1 import { client } from '$lib/oauth'; 2 + import { listRecords } from '$lib/oauth/atproto'; 3 + import { getImageBlobUrl } from '$lib/website/utils'; 2 4 import EmbedCard from '../EmbedCard/EmbedCard.svelte'; 3 5 import type { CardDefinition } from '../types'; 4 6 import LivestreamCard from './LivestreamCard.svelte'; ··· 14 16 card.h = 1; 15 17 card.mobileH = 2; 16 18 card.mobileW = 4; 19 + }, 20 + loadData: async (items, { did }) => { 21 + const records = await listRecords({ did, collection: 'place.stream.livestream', limit: 3 }); 22 + console.log(records); 23 + 24 + let latestLivestream: 25 + | { 26 + createdAt: string; 27 + title: string; 28 + thumb?: string; 29 + href: string; 30 + online?: boolean; 31 + } 32 + | undefined; 33 + const values = Object.values(records); 34 + if (values?.length > 0) { 35 + const latest = JSON.parse(JSON.stringify(values[0])); 36 + console.log(latest); 37 + 38 + latestLivestream = { 39 + createdAt: latest.value.createdAt, 40 + title: latest.value.title as string, 41 + thumb: getImageBlobUrl({ link: latest.value.thumb?.ref.$link, did }), 42 + href: latest.value.canonicalUrl || latest.value.url, 43 + online: undefined 44 + }; 45 + } 46 + 47 + if (latestLivestream) { 48 + try { 49 + const segmentsResponse = await fetch( 50 + 'https://stream.place/xrpc/place.stream.live.getSegments?userDID=' + 51 + encodeURIComponent(did) 52 + ); 53 + const segments = await segmentsResponse.json(); 54 + 55 + const lastSegment = segments.segments[0]; 56 + const startTime = new Date(lastSegment.record.startTime).getTime(); 57 + 58 + const FIVE_MINUTES = 5 * 60 * 1000; 59 + const now = Date.now(); 60 + 61 + latestLivestream.online = now - startTime <= FIVE_MINUTES; 62 + } catch (error) { 63 + console.error(error); 64 + } 65 + } 66 + 67 + return latestLivestream; 17 68 } 18 69 } as CardDefinition & { type: 'latestLivestream' }; 19 70
+4 -1
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 7 7 8 8 let { item }: ContentComponentProps = $props(); 9 9 10 - const recentRecords = getAdditionalUserData().recentRecords; 10 + 11 + const data = getAdditionalUserData(); 12 + // svelte-ignore state_referenced_locally 13 + const recentRecords = (data[item.cardType] as any); 11 14 12 15 let profiles: ProfileViewDetailed[] = $state([]); 13 16
+7 -1
src/lib/cards/SpecialCards/UpdatedBlentos/index.ts
··· 3 3 4 4 export const UpdatedBlentosCardDefitition = { 5 5 type: 'updatedBlentos', 6 - contentComponent: UpdatedBlentosCard 6 + contentComponent: UpdatedBlentosCard, 7 + loadData: async () => { 8 + const response = await fetch( 9 + 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' 10 + ); 11 + return await response.json(); 12 + } 7 13 } as CardDefinition & { type: 'updatedBlentos' };
+3 -1
src/lib/cards/index.ts
··· 1 + import { ATProtoCollectionsCardDefinition } from './ATProtoCollectionsCard'; 1 2 import { BlueskyPostCardDefinition } from './BlueskyPostCard'; 2 3 import { EmbedCardDefinition } from './EmbedCard'; 3 4 import { ImageCardDefinition } from './ImageCard'; ··· 19 20 LivestreamCardDefitition, 20 21 LivestreamEmbedCardDefitition, 21 22 EmbedCardDefinition, 22 - MapCardDefinition 23 + MapCardDefinition, 24 + ATProtoCollectionsCardDefinition 23 25 ] as const; 24 26 25 27 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+3
src/lib/cards/types.ts
··· 36 36 37 37 sidebarComponent?: Component<SidebarComponentProps>; 38 38 sidebarButtonText?: string; 39 + 40 + loadData?: (items: Item[], { did, handle }: { did: string; handle: string }) => Promise<unknown>; 41 + dataKey?: string; 39 42 };
+1 -1
src/lib/components/bluesky-post/BlueskyPost.svelte
··· 30 30 {/snippet} 31 31 32 32 {#if postData} 33 - <Post data={postData} logo={showLogo ? logo : undefined} {...restProps}> 33 + <Post data={postData} showBookmark={false} logo={showLogo ? logo : undefined} {...restProps}> 34 34 {@render children?.()} 35 35 </Post> 36 36 {/if}
+2 -1
src/lib/helper.ts
··· 148 148 149 149 export const [getCanEdit, setCanEdit] = createContext<() => boolean>(); 150 150 151 - export const [getAdditionalUserData, setAdditionalUserData] = createContext(); 151 + export const [getAdditionalUserData, setAdditionalUserData] = 152 + createContext<Record<string, unknown>>();
+12
src/lib/oauth/atproto.ts
··· 194 194 await putRecord({ collection, record: { [key]: blobInfo }, rkey }); 195 195 196 196 return blobInfo; 197 + } 198 + 199 + export async function describeRepo({ did }: { did: string }) { 200 + const pds = await getPDS(did); 201 + 202 + const agent = new AtpBaseClient({ service: pds }); 203 + 204 + const repo = await agent.com.atproto.repo.describeRepo({ 205 + repo: did 206 + }); 207 + 208 + return repo; 197 209 }
+1
src/lib/website/context.ts
··· 11 11 createContext<UpdateRecordFunction[]>(); 12 12 13 13 export const [getDidContext, setDidContext] = createContext<string>(); 14 + export const [getHandleContext, setHandleContext] = createContext<string>(); 14 15 15 16 export const [getDataContext, setDataContext] = createContext<DownloadedData>(); 16 17
+24 -57
src/lib/website/load.ts
··· 9 9 import { data } from './data'; 10 10 import { AtpBaseClient } from '@atproto/api'; 11 11 import { env } from '$env/dynamic/private'; 12 + import { CardDefinitionsByType } from '$lib/cards'; 13 + import type { Item } from '$lib/types'; 12 14 13 15 export async function loadData(handle: string) { 14 16 const did = await resolveHandle({ handle }); ··· 62 64 } 63 65 64 66 const cardTypes = new Set( 65 - Object.values(downloadedData['app.blento.card']).map((v) => v.value.cardType) 67 + Object.values(downloadedData['app.blento.card']).map((v) => v.value.cardType) as string[] 66 68 ); 67 69 68 - let recentRecords; 69 - if (cardTypes.has('updatedBlentos')) { 70 - try { 71 - // https://ufos-api.microcosm.blue/records?collection=app.blento.card 72 - const response = await fetch( 73 - 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' 70 + const cardTypesArray = Array.from(cardTypes); 71 + 72 + const additionDataPromises: Record<string, Promise<unknown>> = {}; 73 + 74 + const handleAndDid = { did, handle }; 75 + 76 + for (const cardType of cardTypesArray) { 77 + const cardDef = CardDefinitionsByType[cardType]; 78 + 79 + if (cardDef.loadData) { 80 + additionDataPromises[cardType] = cardDef.loadData( 81 + Object.values(downloadedData['app.blento.card']) 82 + .filter((v) => cardType == v.value.cardType) 83 + .map((v) => v.value) as Item[], 84 + handleAndDid 74 85 ); 75 - recentRecords = await response.json(); 76 - } catch (error) { 77 - console.error('failed to fetch recent records', error); 78 86 } 79 87 } 80 88 81 - let recentPosts; 89 + await Promise.all(Object.values(additionDataPromises)); 82 90 83 - if (cardTypes.has('latestPost')) { 91 + const additionalData: Record<string, unknown> = {}; 92 + for (const [key, value] of Object.entries(additionDataPromises)) { 84 93 try { 85 - const agent = new AtpBaseClient({ service: 'https://api.bsky.app' }); 86 - const authorFeed = await agent.app.bsky.feed.getAuthorFeed({ 87 - actor: did, 88 - filter: 'posts_no_replies', 89 - limit: 2 90 - }); 91 - console.log(authorFeed.data); 92 - recentPosts = JSON.parse(JSON.stringify(authorFeed.data)); 94 + additionalData[key] = await value; 93 95 } catch (error) { 94 - console.error('failed to fetch recent posts', error); 96 + console.log('error loading', key, error); 95 97 } 96 98 } 97 99 98 - let metrics; 99 - // try { 100 - // const endAt = Date.now(); 101 - 102 - // const startAt = new Date(); 103 - // startAt.setFullYear(startAt.getFullYear() - 1); 104 - 105 - // const params = new URLSearchParams({ 106 - // startAt: startAt.getTime().toString(), 107 - // endAt: endAt.toString(), 108 - // unit: 'year', 109 - // timezone: 'America/Los_Angeles', 110 - // path: '/' + handle 111 - // }); 112 - 113 - // const url = `https://umami-wispy-dream-8048.fly.dev/api/websites/${env.ANALYTICS_WEBSITE_ID}/stats?${params}`; 114 - 115 - // console.log(url); 116 - // const metricsResponse = await fetch(url, { 117 - // method: 'GET', 118 - // headers: { 119 - // Authorization: 'Bearer ' + env.ANALYTICS_TOKEN 120 - // } 121 - // }); 122 - 123 - // metrics = await metricsResponse.json(); 124 - // console.log(metrics); 125 - // } catch (error) { 126 - // console.error(error); 127 - // } 128 - 129 100 return { 130 101 did, 131 102 data: JSON.parse(JSON.stringify(downloadedData)) as DownloadedData, 132 - additionalData: { 133 - recentRecords, 134 - recentPosts, 135 - metrics 136 - } 103 + additionalData 137 104 }; 138 105 }