your personal website on atproto - mirror blento.app

margin.at card

Florian 5ec0e9de 1bc6705d

+311 -1
+3 -1
src/lib/cards/index.ts
··· 47 import { LastFMTopAlbumsCardDefinition } from './media/LastFMCard/LastFMTopAlbumsCard'; 48 import { LastFMProfileCardDefinition } from './media/LastFMCard/LastFMProfileCard'; 49 import { PlyrFMCardDefinition } from './media/PlyrFMCard'; 50 // import { Model3DCardDefinition } from './visual/Model3DCard'; 51 52 export const AllCardDefinitions = [ ··· 98 LastFMTopTracksCardDefinition, 99 LastFMTopAlbumsCardDefinition, 100 LastFMProfileCardDefinition, 101 - PlyrFMCardDefinition 102 ] as const; 103 104 export const CardDefinitionsByType = AllCardDefinitions.reduce(
··· 47 import { LastFMTopAlbumsCardDefinition } from './media/LastFMCard/LastFMTopAlbumsCard'; 48 import { LastFMProfileCardDefinition } from './media/LastFMCard/LastFMProfileCard'; 49 import { PlyrFMCardDefinition } from './media/PlyrFMCard'; 50 + import { MarginCardDefinition } from './social/MarginCard'; 51 // import { Model3DCardDefinition } from './visual/Model3DCard'; 52 53 export const AllCardDefinitions = [ ··· 99 LastFMTopTracksCardDefinition, 100 LastFMTopAlbumsCardDefinition, 101 LastFMProfileCardDefinition, 102 + PlyrFMCardDefinition, 103 + MarginCardDefinition 104 ] as const; 105 106 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+183
src/lib/cards/social/MarginCard/MarginCard.svelte
···
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { 5 + getAdditionalUserData, 6 + getCanEdit, 7 + getDidContext, 8 + getHandleContext 9 + } from '$lib/website/context'; 10 + import { CardDefinitionsByType } from '../..'; 11 + import type { MarginEntry } from './index'; 12 + import { Button } from '@foxui/core'; 13 + 14 + let { item }: { item: Item } = $props(); 15 + 16 + const data = getAdditionalUserData(); 17 + // svelte-ignore state_referenced_locally 18 + let entries = $state(data[item.cardType] as MarginEntry[] | undefined); 19 + 20 + let did = getDidContext(); 21 + let handle = getHandleContext(); 22 + 23 + onMount(async () => { 24 + if (!entries) { 25 + entries = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 26 + did, 27 + handle 28 + })) as MarginEntry[]; 29 + 30 + data[item.cardType] = entries; 31 + } 32 + }); 33 + 34 + let canEdit = getCanEdit(); 35 + 36 + let filtered = $derived( 37 + entries?.filter((e) => { 38 + if (e.type === 'bookmark' && item.cardData.showBookmarks === false) return false; 39 + if (e.type === 'annotation' && item.cardData.showAnnotations === false) return false; 40 + if (e.type === 'highlight' && item.cardData.showHighlights === false) return false; 41 + return true; 42 + }) 43 + ); 44 + 45 + function getMarginUrl(entry: MarginEntry) { 46 + const rkey = entry.uri.split('/').pop(); 47 + return `https://margin.at/${handle}/${entry.type}/${rkey}`; 48 + } 49 + 50 + function getDisplayUrl(url: string) { 51 + try { 52 + const u = new URL(url); 53 + return u.hostname + (u.pathname !== '/' ? u.pathname : ''); 54 + } catch { 55 + return url; 56 + } 57 + } 58 + 59 + function truncate(text: string, max: number) { 60 + if (text.length <= max) return text; 61 + return text.slice(0, max) + '…'; 62 + } 63 + </script> 64 + 65 + <div class={['flex h-full flex-col overflow-y-auto px-5 py-4', item.cardData.label ? 'pt-12' : '']}> 66 + {#if filtered && filtered.length > 0} 67 + <div class="flex flex-col gap-3"> 68 + {#each filtered as entry (entry.uri)} 69 + {@const source = 70 + entry.type === 'bookmark' ? entry.value.source : entry.value.target?.source} 71 + <a 72 + href={getMarginUrl(entry)} 73 + target="_blank" 74 + rel="noopener noreferrer" 75 + class="bg-base-100 dark:bg-base-800 accent:bg-black/10 hover:bg-base-200 dark:hover:bg-base-700 accent:hover:bg-black/15 flex flex-col gap-1.5 rounded-xl px-5 py-3 transition-colors" 76 + > 77 + <div class="flex items-center gap-2 pb-1"> 78 + {#if entry.type === 'bookmark'} 79 + <svg 80 + xmlns="http://www.w3.org/2000/svg" 81 + fill="none" 82 + viewBox="0 0 24 24" 83 + stroke-width="2" 84 + stroke="currentColor" 85 + class="text-accent-500 accent:text-black size-3.5 shrink-0" 86 + > 87 + <path 88 + stroke-linecap="round" 89 + stroke-linejoin="round" 90 + d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" 91 + /> 92 + </svg> 93 + {:else if entry.type === 'annotation'} 94 + <svg 95 + xmlns="http://www.w3.org/2000/svg" 96 + fill="none" 97 + viewBox="0 0 24 24" 98 + stroke-width="2" 99 + stroke="currentColor" 100 + class="text-accent-500 accent:text-black size-3.5 shrink-0" 101 + > 102 + <path 103 + stroke-linecap="round" 104 + stroke-linejoin="round" 105 + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" 106 + /> 107 + </svg> 108 + {:else} 109 + <svg 110 + xmlns="http://www.w3.org/2000/svg" 111 + viewBox="0 0 24 24" 112 + fill="none" 113 + stroke="currentColor" 114 + stroke-width="2" 115 + stroke-linecap="round" 116 + stroke-linejoin="round" 117 + class="text-accent-500 accent:text-black size-3.5 shrink-0" 118 + > 119 + <path d="m9 11-6 6v3h9l3-3" /> 120 + <path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" /> 121 + </svg> 122 + {/if} 123 + <span class="text-base-500 dark:text-base-400 accent:text-black/80 text-xs capitalize" 124 + >{entry.type}</span 125 + > 126 + <span class="text-base-400 dark:text-base-500 accent:text-black/60 ml-auto text-xs"> 127 + {new Date(entry.createdAt).toLocaleDateString()} 128 + </span> 129 + </div> 130 + 131 + {#if entry.type === 'bookmark' && entry.value.title} 132 + <span 133 + class="text-base-900 dark:text-base-100 accent:text-black text-sm leading-snug font-medium" 134 + > 135 + {truncate(entry.value.title as string, 80)} 136 + </span> 137 + {/if} 138 + 139 + {#if entry.type === 'annotation' && entry.value.body?.value} 140 + <span class="text-base-900 dark:text-base-100 accent:text-black text-sm leading-snug font-medium"> 141 + {truncate(entry.value.body.value as string, 120)} 142 + </span> 143 + {/if} 144 + 145 + {#if entry.type === 'highlight' && entry.value.target?.selector} 146 + {@const selectors = Array.isArray(entry.value.target.selector) 147 + ? entry.value.target.selector 148 + : [entry.value.target.selector]} 149 + {@const quote = selectors.find((s: any) => s.exact)?.exact} 150 + {#if quote} 151 + <span 152 + class="text-base-700 dark:text-base-300 accent:text-black/80 border-accent-500 accent:border-black/60 border-l-2 pl-3 text-sm leading-snug italic" 153 + > 154 + {truncate(quote as string, 120)} 155 + </span> 156 + {/if} 157 + {/if} 158 + 159 + {#if source} 160 + <span class="text-base-400 dark:text-base-500 accent:text-black/60 truncate text-xs"> 161 + {getDisplayUrl(source as string)} 162 + </span> 163 + {/if} 164 + </a> 165 + {/each} 166 + </div> 167 + {:else if filtered} 168 + <div class="flex h-full w-full flex-col items-center justify-center gap-4 text-center text-sm"> 169 + No margin entries yet. 170 + {#if canEdit()} 171 + <Button href="https://margin.at" target="_blank" rel="noopener noreferrer"> 172 + Try Margin 173 + </Button> 174 + {/if} 175 + </div> 176 + {:else} 177 + <div 178 + class="text-base-500 dark:text-base-400 accent:text-black/60 flex h-full w-full items-center justify-center text-center text-sm" 179 + > 180 + Loading... 181 + </div> 182 + {/if} 183 + </div>
+63
src/lib/cards/social/MarginCard/MarginCardSettings.svelte
···
··· 1 + <script lang="ts"> 2 + import type { SettingsComponentProps } from '../../types'; 3 + import { Checkbox, Label } from '@foxui/core'; 4 + 5 + let { item }: SettingsComponentProps = $props(); 6 + </script> 7 + 8 + <div class="flex flex-col gap-3"> 9 + <div class="flex items-center space-x-2"> 10 + <Checkbox 11 + bind:checked={ 12 + () => item.cardData.showBookmarks !== false, (val) => (item.cardData.showBookmarks = val) 13 + } 14 + id="show-bookmarks" 15 + aria-labelledby="show-bookmarks-label" 16 + variant="secondary" 17 + /> 18 + <Label 19 + id="show-bookmarks-label" 20 + for="show-bookmarks" 21 + class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 22 + > 23 + Bookmarks 24 + </Label> 25 + </div> 26 + 27 + <div class="flex items-center space-x-2"> 28 + <Checkbox 29 + bind:checked={ 30 + () => item.cardData.showAnnotations !== false, 31 + (val) => (item.cardData.showAnnotations = val) 32 + } 33 + id="show-annotations" 34 + aria-labelledby="show-annotations-label" 35 + variant="secondary" 36 + /> 37 + <Label 38 + id="show-annotations-label" 39 + for="show-annotations" 40 + class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 41 + > 42 + Annotations 43 + </Label> 44 + </div> 45 + 46 + <div class="flex items-center space-x-2"> 47 + <Checkbox 48 + bind:checked={ 49 + () => item.cardData.showHighlights !== false, (val) => (item.cardData.showHighlights = val) 50 + } 51 + id="show-highlights" 52 + aria-labelledby="show-highlights-label" 53 + variant="secondary" 54 + /> 55 + <Label 56 + id="show-highlights-label" 57 + for="show-highlights" 58 + class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 59 + > 60 + Highlights 61 + </Label> 62 + </div> 63 + </div>
+62
src/lib/cards/social/MarginCard/index.ts
···
··· 1 + import type { CardDefinition } from '../../types'; 2 + import { listRecords } from '$lib/atproto'; 3 + import MarginCard from './MarginCard.svelte'; 4 + import MarginCardSettings from './MarginCardSettings.svelte'; 5 + 6 + export type MarginEntry = { 7 + type: 'bookmark' | 'annotation' | 'highlight'; 8 + uri: string; 9 + value: any; 10 + createdAt: string; 11 + }; 12 + 13 + export const MarginCardDefinition = { 14 + type: 'margin', 15 + contentComponent: MarginCard, 16 + settingsComponent: MarginCardSettings, 17 + createNew: (card) => { 18 + card.w = 4; 19 + card.mobileW = 8; 20 + card.h = 4; 21 + card.mobileH = 6; 22 + }, 23 + loadData: async (_items, { did }) => { 24 + const [bookmarks, annotations, highlights] = await Promise.all([ 25 + listRecords({ did, collection: 'at.margin.bookmark' }).catch(() => []), 26 + listRecords({ did, collection: 'at.margin.annotation' }).catch(() => []), 27 + listRecords({ did, collection: 'at.margin.highlight' }).catch(() => []) 28 + ]); 29 + 30 + const entries: MarginEntry[] = [ 31 + ...bookmarks.map((r: any) => ({ 32 + type: 'bookmark' as const, 33 + uri: r.uri, 34 + value: r.value, 35 + createdAt: r.value.createdAt 36 + })), 37 + ...annotations.map((r: any) => ({ 38 + type: 'annotation' as const, 39 + uri: r.uri, 40 + value: r.value, 41 + createdAt: r.value.createdAt 42 + })), 43 + ...highlights.map((r: any) => ({ 44 + type: 'highlight' as const, 45 + uri: r.uri, 46 + value: r.value, 47 + createdAt: r.value.createdAt 48 + })) 49 + ]; 50 + 51 + entries.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); 52 + 53 + return entries; 54 + }, 55 + minH: 2, 56 + canHaveLabel: true, 57 + 58 + keywords: ['margin', 'bookmarks', 'annotations', 'highlights', 'reading', 'web'], 59 + groups: ['Social'], 60 + name: 'Margin highlights, bookmarks, annotations', 61 + icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" /></svg>` 62 + } as CardDefinition & { type: 'margin' };