your personal website on atproto - mirror
0
fork

Configure Feed

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

Merge branch 'main' of https://github.com/unbedenklich/blento

+334 -6
+12
docs/Autofill.md
··· 1 + # autofilling a profile 2 + 3 + - find all links in bluesky profile 4 + - check for lexicons: 5 + - place.stream.livestream 6 + - social.popfeed.challenge.participation 7 + - social.popfeed.feed.review 8 + - site.standard.document 9 + - com.germnetwork.declaration 10 + - pub.leaflet.document 11 + - blue.flashes.actor.portfolio 12 + - add bluesky profile card
+5 -1
docs/CardIdeas.md
··· 39 39 - teal.fm 40 40 - tangled.sh 41 41 - popfeed.social 42 + - reading goal 43 + - latest ratings 44 + - lists 42 45 - smokesignal.events (https://pdsls.dev/at://did:plc:xbtmt2zjwlrfegqvch7fboei/events.smokesignal.calendar.event/3ltn2qrxf3626) 43 46 - statusphere.xyz 44 47 - goals.garden 45 48 - flashes.blue (https://pdsls.dev/at://did:plc:aytgljyikzbtgrnac2u4ccft/blue.flashes.actor.portfolio, https://app.flashes.blue/profile/j4ck.xyz) 46 49 - room: flo-bit.dev/room 50 + - plyr.fm 47 51 48 52 ## programming 49 53 ··· 58 62 - minesweeper 59 63 - memory 60 64 - pinball esque 61 - - 2048 65 + - 2048
+1 -1
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 159 159 160 160 function applyChange(def: (typeof AllCardDefinitions)[number]) { 161 161 const updated = def.change ? def.change(item) : item; 162 - if (updated !== item) { 162 + if (updated && updated !== item) { 163 163 item = updated; 164 164 } 165 165 item.cardType = def.type;
+26 -1
src/lib/cards/BigSocialCard/index.ts
··· 143 143 line: /(?:line\.me)/i, 144 144 skype: /(?:skype\.com)/i, 145 145 146 - ao3: /(?:archiveofourown\.org)/i 146 + ao3: /(?:archiveofourown\.org)/i, 147 + 148 + germ: /(?:ger\.mx)/i 147 149 }; 148 150 149 151 export const platformsData: Record<string, SimpleIcon> = { ··· 180 182 source: 'https://brand.linkedin.com', 181 183 guidelines: 'https://brand.linkedin.com/policies', 182 184 svg: `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>LinkedIn</title><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>` 185 + }, 186 + 187 + germ: { 188 + slug: 'germ', 189 + path: '', 190 + title: 'Germ Network', 191 + hex: '7EE35A', 192 + source: '', 193 + svg: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 194 + <g clip-path="url(#clip0_2_3)"> 195 + <path d="M9.97852 2.31976C10.9816 4.81045 13.9828 5.28448 15.4834 3.19552C15.6921 2.89824 15.9489 2.6572 16.0371 2.6572C16.3501 2.6572 18.2841 4.7301 18.6933 5.50945C19.2791 6.62624 19.247 6.65838 18.1476 6.32093C15.018 5.34876 12.4742 6.21648 11.1742 8.69111C10.2112 10.539 11.1822 13.4475 13.0841 14.3956C14.1674 14.9339 14.7933 14.8054 16.2378 13.7207C18.0513 12.3629 19.0303 13.1342 17.626 14.8134C15.652 17.1836 12.3378 18.2039 9.32852 17.3764C8.21309 17.0711 8.11679 16.9506 8.20506 15.9623C8.35753 14.3956 7.77173 12.7405 6.63223 11.5112C5.68532 10.4908 5.75754 10.2739 7.2421 9.7838C10.797 8.59469 10.8933 4.15162 7.38655 3.17945C6.99334 3.06696 6.6884 2.92234 6.71247 2.85807C6.79272 2.62507 9.08778 1.50024 9.56926 1.46007C9.60136 1.46007 9.78592 1.84572 9.97852 2.31976Z" fill="#4183FF"/> 196 + <path d="M18.1316 7.74304C19.7445 8.54648 19.9773 9.14104 19.4958 11.1979L19.4935 11.2075C19.255 12.2183 19.2534 12.2252 18.7415 11.8728C17.8587 11.2782 17.0563 11.463 15.5316 12.6039C14.2637 13.56 13.8625 13.568 12.9797 12.6441C10.1791 9.71149 14.2637 5.83083 18.1316 7.74304Z" fill="#22F137"/> 197 + <path d="M6.50383 4.35248C8.08469 4.64976 8.71864 5.3809 8.55012 6.71462C8.42975 7.65466 8.03654 8.05638 6.6563 8.62683C4.30507 9.599 4.04026 10.5631 5.62914 12.3789C6.45568 13.327 6.82482 14.1706 6.89704 15.2714C6.96926 16.4122 6.88902 16.4122 5.66927 15.1589C3.20569 12.6521 2.61989 9.15711 4.13655 5.96741C4.96309 4.24 5.15569 4.10341 6.50383 4.35248Z" fill="#22F137"/> 198 + <path d="M13.7581 1.58862C14.7371 1.89393 14.681 1.83769 14.4483 2.28762C13.6618 3.80614 11.1341 3.15534 11.1341 1.43596C11.1341 1.10655 12.4341 1.18689 13.7581 1.58862Z" fill="#22F137"/> 199 + <path fill-rule="evenodd" clip-rule="evenodd" d="M9.40876 0.19865C2.66804 1.65289 -0.260969 9.79987 3.97606 15.3115C8.39765 21.0723 17.6983 19.7867 20.3143 13.0378C23.1069 5.83083 16.952 -1.41628 9.40876 0.19865ZM14.4483 2.28762C14.681 1.83769 14.7371 1.89393 13.7581 1.58862C12.4341 1.18689 11.1341 1.10655 11.1341 1.43596C11.1341 3.15534 13.6618 3.80614 14.4483 2.28762ZM15.4834 3.19552C13.9828 5.28448 10.9816 4.81045 9.97852 2.31976C9.78592 1.84572 9.60136 1.46007 9.56926 1.46007C9.08778 1.50024 6.79272 2.62507 6.71247 2.85807C6.6884 2.92234 6.99334 3.06696 7.38655 3.17945C10.8933 4.15162 10.797 8.59469 7.2421 9.7838C5.75754 10.2739 5.68532 10.4908 6.63223 11.5112C7.77173 12.7405 8.35753 14.3956 8.20506 15.9623C8.11679 16.9506 8.21309 17.0711 9.32852 17.3764C12.3378 18.2039 15.652 17.1836 17.626 14.8134C19.0303 13.1342 18.0513 12.3629 16.2378 13.7207C14.7933 14.8054 14.1674 14.9339 13.0841 14.3956C11.1822 13.4475 10.2112 10.539 11.1742 8.69111C12.4742 6.21648 15.018 5.34876 18.1476 6.32093C19.247 6.65838 19.2791 6.62624 18.6933 5.50945C18.2841 4.7301 16.3501 2.6572 16.0371 2.6572C15.9489 2.6572 15.6921 2.89824 15.4834 3.19552ZM8.55012 6.71462C8.71864 5.3809 8.08469 4.64976 6.50383 4.35248C5.15569 4.10341 4.96309 4.24 4.13655 5.96741C2.61989 9.15711 3.20569 12.6521 5.66927 15.1589C6.88902 16.4122 6.96926 16.4122 6.89704 15.2714C6.82482 14.1706 6.45568 13.327 5.62914 12.3789C4.04026 10.5631 4.30507 9.599 6.6563 8.62683C8.03654 8.05638 8.42975 7.65466 8.55012 6.71462ZM19.4958 11.1979C19.9773 9.14104 19.7445 8.54648 18.1316 7.74304C14.2637 5.83083 10.1791 9.71149 12.9797 12.6441C13.8625 13.568 14.2637 13.56 15.5316 12.6039C17.0563 11.463 17.8587 11.2782 18.7415 11.8728C19.2534 12.2252 19.255 12.2183 19.4935 11.2075L19.4958 11.1979Z" fill="black"/> 200 + <path d="M11.11 21.3936C11.0939 21.4097 10.5402 21.4579 9.89025 21.4981C7.15383 21.6668 5.5489 22.5586 6.71247 23.2657C8.25321 24.2057 14.7371 24.2057 16.2779 23.2657C17.3853 22.5988 15.9248 21.707 13.4211 21.5222C11.4711 21.3776 11.1501 21.3615 11.11 21.3936Z" fill="black"/> 201 + </g> 202 + <defs> 203 + <clipPath id="clip0_2_3"> 204 + <rect width="24" height="24" fill="white"/> 205 + </clipPath> 206 + </defs> 207 + </svg>` 183 208 }, 184 209 185 210 // support / monetization
+52
src/lib/cards/PopfeedReviews/PopfeedReviewsCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 5 + import { CardDefinitionsByType } from '..'; 6 + import Rating from './Rating.svelte'; 7 + 8 + let { item }: { item: Item } = $props(); 9 + 10 + const data = getAdditionalUserData(); 11 + // svelte-ignore state_referenced_locally 12 + let feed = $state(data[item.cardType] as any); 13 + 14 + let did = getDidContext(); 15 + let handle = getHandleContext(); 16 + 17 + onMount(async () => { 18 + console.log(feed); 19 + if (!feed) { 20 + feed = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 21 + did, 22 + handle 23 + })) as any; 24 + 25 + console.log(feed); 26 + 27 + data[item.cardType] = feed; 28 + } 29 + }); 30 + </script> 31 + 32 + <div class="z-10 flex h-full gap-4 overflow-x-scroll p-4"> 33 + {#each feed ?? [] as review} 34 + <a 35 + rel="noopener noreferrer" 36 + target="_blank" 37 + class="flex" 38 + href="https://popfeed.social/review/{review.uri}" 39 + > 40 + <div 41 + class="relative flex aspect-[2/3] h-full flex-col items-center justify-end overflow-hidden rounded-xl p-1" 42 + > 43 + <img src={review.value.posterUrl} alt="" class="absolute inset-0 -z-10" /> 44 + <div 45 + class="from-base-900/80 absolute right-0 bottom-0 left-0 h-1/3 bg-gradient-to-t via-transparent" 46 + ></div> 47 + 48 + <Rating class="z-10 text-lg" rating={review.value.rating} /> 49 + </div> 50 + </a> 51 + {/each} 52 + </div>
+51
src/lib/cards/PopfeedReviews/Rating.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '@foxui/core'; 3 + 4 + let { rating, class: className }: { rating: number; class?: string } = $props(); 5 + 6 + // Convert 0-10 rating to 0-5 stars 7 + const starRating = $derived(Math.max(0, Math.min(10, rating ?? 0)) / 2); 8 + const fullStars = $derived(Math.floor(starRating)); 9 + const hasHalfStar = $derived(rating % 2 === 1); 10 + const emptyStars = $derived(Math.max(0, 5 - fullStars - (hasHalfStar ? 1 : 0))); 11 + </script> 12 + 13 + {#snippet star(className: string)} 14 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 {className}"> 15 + <path 16 + fill="currentColor" 17 + fill-rule="evenodd" 18 + d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" 19 + clip-rule="evenodd" 20 + /> 21 + </svg> 22 + {/snippet} 23 + 24 + {#snippet halfStar()} 25 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-5 fill-accent-500"> 26 + <defs> 27 + <linearGradient id="half-filled" x1="0%" y1="0%" x2="100%" y2="0%"> 28 + <stop offset="50%" stop-color="currentColor" /> 29 + <stop offset="50%" stop-color="var(--color-base-400)" /> 30 + </linearGradient> 31 + </defs> 32 + <path 33 + fill="url(#half-filled)" 34 + fill-rule="evenodd" 35 + d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" 36 + clip-rule="evenodd" 37 + /> 38 + </svg> 39 + {/snippet} 40 + 41 + <div class={cn('text-accent-500 flex', className)}> 42 + {#each Array(fullStars) as _} 43 + {@render star("text-accent-500")} 44 + {/each} 45 + {#if hasHalfStar} 46 + {@render halfStar()} 47 + {/if} 48 + {#each Array(emptyStars) as _} 49 + {@render star("text-base-400")} 50 + {/each} 51 + </div>
+21
src/lib/cards/PopfeedReviews/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import { listRecords } from '$lib/oauth/atproto'; 3 + import PopfeedReviewsCard from './PopfeedReviewsCard.svelte'; 4 + 5 + export const PopfeedReviewsCardDefinition = { 6 + type: 'recentPopfeedReviews', 7 + contentComponent: PopfeedReviewsCard, 8 + createNew: (card) => { 9 + card.w = 8; 10 + card.mobileW = 8; 11 + card.h = 3; 12 + card.mobileH = 6; 13 + }, 14 + loadData: async (items, { did }) => { 15 + const data = await listRecords({ did, collection: 'social.popfeed.feed.review' }); 16 + 17 + return data; 18 + }, 19 + minH: 3, 20 + sidebarButtonText: 'Popfeed Reviews' 21 + } as CardDefinition & { type: 'recentPopfeedReviews' };
+10 -1
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 25 25 src={profile.avatar} 26 26 class="bg-base-200 dark:bg-base-700 accent:bg-accent-300 aspect-square size-28 rounded-full" 27 27 alt="" 28 + loading="lazy" 28 29 /> 29 - <div class="text-md line-clamp-1 text-center font-bold"> 30 + <div class="text-md line-clamp-1 text-center font-bold max-w-full"> 30 31 {profile.displayName || profile.handle} 31 32 </div> 32 33 </a> 33 34 {/each} 34 35 </div> 35 36 </div> 37 + 38 + 39 + https://coverartarchive.org/release/46656572-9172-44cc-9efa-7ee9b87fe489/front-250 40 + https://coverartarchive.org/release/d8e8f57d-40b6-45e3-8c01-8816d33995d1/front-250 41 + 42 + 42bed944-e781-4447-82df-8cae4918dd94 43 + 44 + d8e8f57d-40b6-45e3-8c01-8816d33995d1
+39
src/lib/cards/TealFMPlaysCard/AlbumArt.svelte
··· 1 + <script lang="ts"> 2 + let { releaseMbId, alt }: { releaseMbId: string; alt: string } = $props(); 3 + 4 + let isLoading = $state(true); 5 + let hasError = $state(false); 6 + </script> 7 + 8 + {#if isLoading} 9 + <div class="bg-base-200 dark:bg-base-800 h-10 w-10 animate-pulse rounded-lg"></div> 10 + {/if} 11 + 12 + {#if hasError} 13 + <div 14 + class="bg-base-300 dark:bg-base-800 accent:bg-accent-700/50 flex h-10 w-10 items-center justify-center rounded-lg" 15 + > 16 + <svg 17 + class="text-base-400 dark:text-base-600 accent:text-accent-900 h-5 w-5" 18 + fill="currentColor" 19 + viewBox="0 0 20 20" 20 + > 21 + <path 22 + d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" 23 + /> 24 + </svg> 25 + </div> 26 + {:else} 27 + <img 28 + src="https://coverartarchive.org/release/{releaseMbId}/front-250" 29 + {alt} 30 + class="h-10 w-10 rounded-lg object-cover {isLoading && 'hidden'}" 31 + onload={() => { 32 + isLoading = false; 33 + }} 34 + onerror={() => { 35 + isLoading = false; 36 + hasError = true; 37 + }} 38 + /> 39 + {/if}
+86
src/lib/cards/TealFMPlaysCard/TealFMPlaysCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 5 + import { CardDefinitionsByType } from '..'; 6 + import AlbumArt from './AlbumArt.svelte'; 7 + import { RelativeTime } from '@foxui/time'; 8 + 9 + let { item }: { item: Item } = $props(); 10 + 11 + const data = getAdditionalUserData(); 12 + // svelte-ignore state_referenced_locally 13 + let feed = $state(data[item.cardType] as any); 14 + 15 + let did = getDidContext(); 16 + let handle = getHandleContext(); 17 + 18 + onMount(async () => { 19 + console.log(feed); 20 + if (!feed) { 21 + feed = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 22 + did, 23 + handle 24 + })) as any; 25 + 26 + console.log(feed); 27 + 28 + data[item.cardType] = feed; 29 + } 30 + }); 31 + 32 + function isNumeric(str: string) { 33 + if (typeof str != 'string') return false; // we only process strings! 34 + return ( 35 + !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... 36 + !isNaN(parseFloat(str)) 37 + ); // ...and ensure strings of whitespace fail 38 + } 39 + </script> 40 + 41 + {#snippet musicItem(play)} 42 + <div class="flex w-full items-center gap-3"> 43 + <div class="size-10 shrink-0"> 44 + <AlbumArt releaseMbId={play.value.releaseMbId} alt="" /> 45 + </div> 46 + <div class="min-w-0 flex-1"> 47 + <div class="inline-flex w-full max-w-full justify-between gap-2"> 48 + <div 49 + class="text-accent-500 accent:text-accent-950 min-w-0 flex-1 shrink truncate font-semibold" 50 + > 51 + {play.value.trackName} 52 + </div> 53 + 54 + {#if play.value.playedTime} 55 + <div class="shrink-0 text-xs"> 56 + <RelativeTime 57 + date={new Date( 58 + isNumeric(play.value.playedTime) 59 + ? parseInt(play.value.playedTime) * 1000 60 + : play.value.playedTime 61 + )} 62 + locale="en-US" 63 + /> ago 64 + </div> 65 + {:else} 66 + <div></div> 67 + {/if} 68 + </div> 69 + <div class="my-1 min-w-0 gap-2 truncate text-xs whitespace-nowrap"> 70 + {(play?.value?.artists ?? []).map((a) => a.artistName).join(', ')} 71 + </div> 72 + </div> 73 + </div> 74 + {/snippet} 75 + 76 + <div class="z-10 flex h-full w-full flex-col gap-4 overflow-y-scroll p-4"> 77 + {#each (feed ?? []).slice(0, 20) as play} 78 + {#if play.value.originUrl} 79 + <a href={play.value.originUrl} target="_blank" rel="noopener noreferrer" class="w-full"> 80 + {@render musicItem(play)} 81 + </a> 82 + {:else} 83 + {@render musicItem(play)} 84 + {/if} 85 + {/each} 86 + </div>
+25
src/lib/cards/TealFMPlaysCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import { listRecords } from '$lib/oauth/atproto'; 3 + import TealFMPlaysCard from './TealFMPlaysCard.svelte'; 4 + 5 + export const TealFMPlaysCardDefinition = { 6 + type: 'recentTealFMPlays', 7 + contentComponent: TealFMPlaysCard, 8 + createNew: (card) => { 9 + card.w = 4; 10 + card.mobileW = 8; 11 + card.h = 3; 12 + card.mobileH = 6; 13 + }, 14 + loadData: async (items, { did }) => { 15 + const data = await listRecords({ 16 + did, 17 + collection: 'fm.teal.alpha.feed.play', 18 + limit: 99 19 + }); 20 + 21 + return data; 22 + }, 23 + minW: 4, 24 + sidebarButtonText: 'teal.fm Plays' 25 + } as CardDefinition & { type: 'recentTealFMPlays' };
+5 -1
src/lib/cards/index.ts
··· 19 19 import { BlueskyProfileCardDefinition } from './BlueskyProfileCard'; 20 20 import { GithubProfileCardDefitition } from './GitHubProfileCard'; 21 21 import { FluidTextCardDefinition } from './FluidTextCard'; 22 + import { PopfeedReviewsCardDefinition } from './PopfeedReviews'; 23 + import { TealFMPlaysCardDefinition } from './TealFMPlaysCard'; 22 24 23 25 export const AllCardDefinitions = [ 24 26 ImageCardDefinition, ··· 40 42 BlueskyProfileCardDefinition, 41 43 GithubProfileCardDefitition, 42 44 TetrisCardDefinition, 43 - FluidTextCardDefinition 45 + FluidTextCardDefinition, 46 + PopfeedReviewsCardDefinition, 47 + TealFMPlaysCardDefinition 44 48 ] as const; 45 49 46 50 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+1 -1
src/lib/website/Settings.svelte
··· 50 50 (value) => { 51 51 data.publication ??= {}; 52 52 data.publication.preferences ??= {}; 53 - data.publication.preferences.hideProfile = value; 53 + data.publication.preferences.hideProfileSection = value; 54 54 55 55 data = { ...data }; 56 56 }