your personal website on atproto - mirror
0
fork

Configure Feed

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

add recent teal.fm plays card

Florian 3440db05 e7201b21

+171 -5
+10 -4
src/lib/cards/PopfeedReviews/PopfeedReviewsCard.svelte
··· 1 1 <script lang="ts"> 2 2 import type { Item } from '$lib/types'; 3 3 import { onMount } from 'svelte'; 4 - import { BlueskyPost } from '../../components/bluesky-post'; 5 4 import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 6 5 import { CardDefinitionsByType } from '..'; 7 6 import Rating from './Rating.svelte'; ··· 32 31 33 32 <div class="z-10 flex h-full gap-4 overflow-x-scroll p-4"> 34 33 {#each feed ?? [] as review} 35 - <a target="_blank" class="flex" href="https://popfeed.social/review/{review.uri}"> 34 + <a 35 + rel="noopener noreferrer" 36 + target="_blank" 37 + class="flex" 38 + href="https://popfeed.social/review/{review.uri}" 39 + > 36 40 <div 37 - class="relative flex aspect-[2/3] h-full flex-col items-center justify-end overflow-hidden p-1 rounded-xl" 41 + class="relative flex aspect-[2/3] h-full flex-col items-center justify-end overflow-hidden rounded-xl p-1" 38 42 > 39 43 <img src={review.value.posterUrl} alt="" class="absolute inset-0 -z-10" /> 40 - <div class="from-base-900/80 absolute bottom-0 left-0 right-0 h-1/3 bg-gradient-to-t via-transparent"></div> 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> 41 47 42 48 <Rating class="z-10 text-lg" rating={review.value.rating} /> 43 49 </div>
+8
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 34 34 {/each} 35 35 </div> 36 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' };
+3 -1
src/lib/cards/index.ts
··· 19 19 import { BlueskyProfileCardDefinition } from './BlueskyProfileCard'; 20 20 import { GithubProfileCardDefitition } from './GitHubProfileCard'; 21 21 import { PopfeedReviewsCardDefinition } from './PopfeedReviews'; 22 + import { TealFMPlaysCardDefinition } from './TealFMPlaysCard'; 22 23 23 24 export const AllCardDefinitions = [ 24 25 ImageCardDefinition, ··· 40 41 BlueskyProfileCardDefinition, 41 42 GithubProfileCardDefitition, 42 43 TetrisCardDefinition, 43 - PopfeedReviewsCardDefinition 44 + PopfeedReviewsCardDefinition, 45 + TealFMPlaysCardDefinition 44 46 ] as const; 45 47 46 48 export const CardDefinitionsByType = AllCardDefinitions.reduce(