your personal website on atproto - mirror blento.app

Merge pull request #25 from flo-bit/card-label

add card label, add google maps link, small fixes

authored by Florian and committed by GitHub 725395dc a9b6899b

+157 -99
+1 -1
docs/Selfhosting.md
··· 31 31 6. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker 32 32 33 33 - github profile: GITHUB_TOKEN 34 - - map: PUBLIC_MAPBOX_TOKEN 34 + - map: PUBLIC_MAPBOX_TOKEN
+8
src/lib/cards/BaseCard/BaseCard.svelte
··· 68 68 ]} 69 69 > 70 70 {@render children?.()} 71 + 72 + {#if !isEditing && item.cardData.label} 73 + <div 74 + class="text-base-900 dark:text-base-50 bg-base-200/50 dark:bg-base-900/50 absolute top-2 left-2 z-30 max-w-[calc(100%-1rem)] rounded-xl p-1 px-2 text-base font-semibold backdrop-blur-md" 75 + > 76 + {item.cardData.label} 77 + </div> 78 + {/if} 71 79 </div> 72 80 {@render controls?.()} 73 81 </div>
+19 -4
src/lib/cards/BaseCard/BaseEditingCard.svelte
··· 3 3 import type { HTMLAttributes } from 'svelte/elements'; 4 4 import BaseCard from './BaseCard.svelte'; 5 5 import type { Item } from '$lib/types'; 6 - import { Button, Label, Popover } from '@foxui/core'; 6 + import { Button, cn, Label, Popover } from '@foxui/core'; 7 7 import { ColorSelect } from '@foxui/colors'; 8 8 import { AllCardDefinitions, CardDefinitionsByType, getColor } from '..'; 9 9 import { COLUMNS } from '$lib'; 10 10 import { getCanEdit, getIsMobile } from '$lib/website/context'; 11 + import PlainTextEditor from '../utils/PlainTextEditor.svelte'; 11 12 12 13 let colorsChoices = [ 13 14 { class: 'text-base-500', label: 'base' }, ··· 151 152 let settingsPopoverOpen = $state(false); 152 153 let changePopoverOpen = $state(false); 153 154 154 - const changeOptions = $derived( 155 - AllCardDefinitions.filter((def) => def.canChange?.(item)) 156 - ); 155 + const changeOptions = $derived(AllCardDefinitions.filter((def) => def.canChange?.(item))); 157 156 158 157 function applyChange(def: (typeof AllCardDefinitions)[number]) { 159 158 const updated = def.change ? def.change(item) : item; ··· 179 178 > 180 179 <div class="absolute inset-0 cursor-grab"></div> 181 180 {@render children?.()} 181 + 182 + {#if cardDef.canHaveLabel} 183 + <div 184 + class={cn( 185 + 'bg-base-200/30 dark:bg-base-900/30 absolute top-2 left-2 z-100 w-fit max-w-[calc(100%-1rem)] rounded-xl p-1 px-2 backdrop-blur-md', 186 + !item.cardData.label && 'hidden group-hover/card:block' 187 + )} 188 + > 189 + <PlainTextEditor 190 + class="text-base-900 dark:text-base-50 w-fit text-base font-semibold" 191 + key="label" 192 + bind:item 193 + placeholder="Label" 194 + /> 195 + </div> 196 + {/if} 182 197 183 198 {#snippet controls()} 184 199 <!-- class="bg-base-100 border-base-200 dark:bg-base-800 dark:border-base-700 absolute -top-3 -left-3 hidden cursor-pointer items-center justify-center rounded-full border p-2 shadow-lg group-focus-within:inline-flex group-hover/card:inline-flex" -->
+10 -6
src/lib/cards/BigSocialCard/BigSocialCard.svelte
··· 2 2 import { platformsData } from '.'; 3 3 import type { ContentComponentProps } from '../types'; 4 4 5 - let { item }: ContentComponentProps = $props(); 5 + let { item, isEditing }: ContentComponentProps = $props(); 6 6 7 7 const platform = $derived(item.cardData.platform as string); 8 8 </script> 9 9 10 - <a 11 - href={item.cardData.href} 12 - target="_blank" 13 - rel="noopener noreferrer" 10 + <div 14 11 class="flex h-full w-full items-center justify-center p-10" 15 12 style={`background-color: #${item.cardData.color}`} 16 13 > ··· 19 16 > 20 17 {@html platformsData[platform].svg} 21 18 </div> 22 - </a> 19 + </div> 20 + 21 + {#if !isEditing} 22 + <a href={item.cardData.href} target="_blank" rel="noopener noreferrer"> 23 + <div class="absolute inset-0 z-50"></div> 24 + <span class="sr-only">open {platformsData[platform].title}</span> 25 + </a> 26 + {/if}
+2 -1
src/lib/cards/BigSocialCard/index.ts
··· 50 50 51 51 return item; 52 52 }, 53 - urlHandlerPriority: 1 53 + urlHandlerPriority: 1, 54 + canHaveLabel: true 54 55 } as CardDefinition & { type: 'bigsocial' }; 55 56 56 57 import {
+2 -1
src/lib/cards/BlueskyMediaCard/index.ts
··· 9 9 createNew: () => {}, 10 10 creationModalComponent: CreateBlueskyMediaCardModal, 11 11 sidebarButtonText: 'Bluesky Media', 12 - sidebarComponent: SidebarItemBlueskyMediaCard 12 + sidebarComponent: SidebarItemBlueskyMediaCard, 13 + canHaveLabel: true 13 14 } as CardDefinition & { type: 'blueskyMedia' };
+1 -1
src/lib/cards/Card/Card.svelte
··· 7 7 8 8 {#if CardDefinitionsByType[item.cardType]} 9 9 {@const cardDef = CardDefinitionsByType[item.cardType]} 10 - <cardDef.contentComponent {item} {...rest} /> 10 + <cardDef.contentComponent isEditing={false} {item} {...rest} /> 11 11 {:else} 12 12 <div class="m-4">Unsupported card type: {item.cardType}</div> 13 13 {/if}
+2 -2
src/lib/cards/Card/EditingCard.svelte
··· 8 8 {#if CardDefinitionsByType[item.cardType]} 9 9 {@const cardDef = CardDefinitionsByType[item.cardType]} 10 10 {#if cardDef.editingContentComponent} 11 - <cardDef.editingContentComponent bind:item /> 11 + <cardDef.editingContentComponent bind:item isEditing /> 12 12 {:else} 13 - <cardDef.contentComponent bind:item /> 13 + <cardDef.contentComponent bind:item isEditing /> 14 14 {/if} 15 15 {:else} 16 16 <div class="m-4">Unsupported card type: {item.cardType}</div>
+4 -4
src/lib/cards/EmbedCard/index.ts
··· 14 14 card.mobileW = 8; 15 15 }, 16 16 17 - canChange: (item) => Boolean(item.cardData.href && !item.cardData.href.startsWith('mailto:')), 17 + // canChange: (item) => Boolean(item.cardData.href), 18 18 19 - change: (item) => { 20 - return item; 21 - }, 19 + // change: (item) => { 20 + // return item; 21 + // }, 22 22 name: 'Embed Card' 23 23 } as CardDefinition & { type: 'embed' };
+18 -21
src/lib/cards/GIFCard/GifCardSettings.svelte
··· 19 19 } 20 20 </script> 21 21 22 - <div class="flex flex-col gap-3"> 23 - <div> 24 - <Label class="mb-1 text-xs">Change GIF</Label> 25 - <Button variant="secondary" class="w-full justify-start" onclick={() => (isSearchOpen = true)}> 26 - <svg 27 - xmlns="http://www.w3.org/2000/svg" 28 - fill="none" 29 - viewBox="0 0 24 24" 30 - stroke-width="1.5" 31 - stroke="currentColor" 32 - class="mr-2 size-4" 33 - > 34 - <path 35 - stroke-linecap="round" 36 - stroke-linejoin="round" 37 - d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" 38 - /> 39 - </svg> 40 - Search GIPHY 41 - </Button> 42 - </div> 22 + <div class="flex flex-col gap-2"> 23 + <Button variant="secondary" class="w-full justify-start" onclick={() => (isSearchOpen = true)}> 24 + <svg 25 + xmlns="http://www.w3.org/2000/svg" 26 + fill="none" 27 + viewBox="0 0 24 24" 28 + stroke-width="1.5" 29 + stroke="currentColor" 30 + class="mr-2 size-4" 31 + > 32 + <path 33 + stroke-linecap="round" 34 + stroke-linejoin="round" 35 + d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" 36 + /> 37 + </svg> 38 + Change GIF 39 + </Button> 43 40 </div> 44 41 45 42 <GiphySearchModal
+1
src/lib/cards/GIFCard/index.ts
··· 26 26 allowSetColor: false, 27 27 minW: 1, 28 28 minH: 1, 29 + canHaveLabel: true, 29 30 onUrlHandler: (url, item) => { 30 31 // Match Giphy page URLs: https://giphy.com/gifs/name-ID or https://giphy.com/gifs/ID 31 32 const pageMatch = url.match(/giphy\.com\/gifs\/(?:.*-)?([a-zA-Z0-9]+)(?:\?|$)/);
+4 -4
src/lib/cards/ImageCard/ImageCard.svelte
··· 3 3 import { getImageBlobUrl } from '$lib/atproto'; 4 4 import type { ContentComponentProps } from '../types'; 5 5 6 - let { item = $bindable() }: ContentComponentProps = $props(); 6 + let { item = $bindable(), isEditing }: ContentComponentProps = $props(); 7 7 8 8 const did = getDidContext(); 9 9 ··· 21 21 <img 22 22 class={[ 23 23 'absolute inset-0 h-full w-full object-cover opacity-100 transition-transform duration-300 ease-in-out', 24 - item.cardData.href ? 'group-hover:scale-102' : '' 24 + item.cardData.href ? 'group-hover/card:scale-101' : '' 25 25 ]} 26 26 src={getSrc()} 27 27 alt="" 28 28 /> 29 29 {/key} 30 - {#if item.cardData.href} 30 + {#if item.cardData.href && !isEditing} 31 31 <a 32 32 href={item.cardData.href} 33 - class="absolute inset-0 h-full w-full" 33 + class="absolute inset-0 z-50 h-full w-full" 34 34 target="_blank" 35 35 rel="noopener noreferrer" 36 36 >
+3 -1
src/lib/cards/ImageCard/index.ts
··· 36 36 change: (item) => { 37 37 return item; 38 38 }, 39 - name: 'Image Card' 39 + name: 'Image Card', 40 + 41 + canHaveLabel: true 40 42 } as CardDefinition & { type: 'image' };
+2 -2
src/lib/cards/MapCard/CreateMapCardModal.svelte
··· 28 28 item.cardData.lon = data.lon; 29 29 item.cardData.name = data.display_name?.split(',')[0] || search; 30 30 item.cardData.type = data.class || 'city'; 31 - item.cardData.zoom = Math.max(getZoomLevel(data.class), getZoomLevel(data.type)); 31 + item.cardData.zoom = Math.max(getZoomLevel(data.class), getZoomLevel(data.type)); 32 32 } else { 33 33 throw new Error('response not ok'); 34 34 } ··· 56 56 <Alert type="error" title="Failed to create map card"><span>{errorMessage}</span></Alert> 57 57 {/if} 58 58 59 - <p class="text-xs mt-2"> 59 + <p class="mt-2 text-xs"> 60 60 Geocoding by <a 61 61 href="https://nominatim.openstreetmap.org/" 62 62 class="text-accent-800 dark:text-accent-300"
+3 -10
src/lib/cards/MapCard/Map.svelte
··· 13 13 let mapContainer: HTMLElement | undefined = $state(); 14 14 let map: mapboxgl.Map | undefined = $state(); 15 15 16 - // Update light preset when changed in settings 17 - $effect(() => { 18 - const preset = item.cardData.lightPreset; 19 - if (map && preset) { 20 - map.setConfigProperty('basemap', 'lightPreset', preset); 21 - } 22 - }); 23 - 24 16 onMount(() => { 25 17 if (!mapContainer || !env.PUBLIC_MAPBOX_TOKEN) { 26 18 console.log('no map container or no mapbox token'); 19 + return; 27 20 } 28 21 29 22 try { ··· 151 144 map.setCenter([lon, lat]); 152 145 } 153 146 }); 154 - resizeObserver.observe(mapContainer); 147 + if (mapContainer) resizeObserver.observe(mapContainer); 155 148 156 149 return () => { 157 150 resizeObserver.disconnect(); ··· 165 158 }); 166 159 </script> 167 160 168 - <div bind:this={mapContainer} class="absolute inset-0 isolate z-50 h-full w-full"></div> 161 + <div bind:this={mapContainer} class="absolute inset-0 isolate h-full w-full"></div>
+14 -2
src/lib/cards/MapCard/MapCard.svelte
··· 1 1 <script lang="ts"> 2 - import type { Item } from '$lib/types'; 2 + import type { ContentComponentProps } from '../types'; 3 3 import Map from './Map.svelte'; 4 4 5 - let { item = $bindable() }: { item: Item } = $props(); 5 + let { item = $bindable(), isEditing }: ContentComponentProps = $props(); 6 6 </script> 7 7 8 8 <Map bind:item /> 9 + 10 + {#if item.cardData.linkToGoogleMaps && !isEditing} 11 + <a 12 + target="_blank" 13 + rel="noopener noreferrer" 14 + href={'http://maps.google.com/maps?q=' + 15 + encodeURIComponent(item.cardData.lat + ',' + item.cardData.lon)} 16 + > 17 + <div class="absolute inset-0 z-100"></div> 18 + <span class="sr-only">open map</span> 19 + </a> 20 + {/if}
+24
src/lib/cards/MapCard/MapCardSettings.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { Checkbox, Label } from '@foxui/core'; 4 + 5 + let { item }: { item: Item; onclose: () => void } = $props(); 6 + </script> 7 + 8 + <div class="flex items-center space-x-2"> 9 + <Checkbox 10 + bind:checked={ 11 + () => Boolean(item.cardData.linkToGoogleMaps), (val) => (item.cardData.linkToGoogleMaps = val) 12 + } 13 + id="show-inline" 14 + aria-labelledby="show-inline-label" 15 + variant="secondary" 16 + /> 17 + <Label 18 + id="show-inline-label" 19 + for="show-inline" 20 + class="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 21 + > 22 + Link to google maps 23 + </Label> 24 + </div>
+4 -1
src/lib/cards/MapCard/index.ts
··· 1 1 import type { CardDefinition } from '../types'; 2 2 import CreateMapCardModal from './CreateMapCardModal.svelte'; 3 3 import MapCard from './MapCard.svelte'; 4 + import MapCardSettings from './MapCardSettings.svelte'; 4 5 import SidebarItemMapCard from './SidebarItemMapCard.svelte'; 5 6 6 7 export const MapCardDefinition = { ··· 16 17 17 18 sidebarComponent: SidebarItemMapCard, 18 19 creationModalComponent: CreateMapCardModal, 19 - allowSetColor: false 20 + allowSetColor: false, 21 + canHaveLabel: true, 22 + settingsComponent: MapCardSettings 20 23 } as CardDefinition & { type: 'mapLocation' }; 21 24 22 25 export function getZoomLevel(type: string | undefined): number {
+2 -1
src/lib/cards/PopfeedReviews/index.ts
··· 17 17 return data; 18 18 }, 19 19 minH: 3, 20 - sidebarButtonText: 'Popfeed Reviews' 20 + sidebarButtonText: 'Popfeed Reviews', 21 + canHaveLabel: true 21 22 } as CardDefinition & { type: 'recentPopfeedReviews' };
+2 -1
src/lib/cards/SectionCard/EditingSectionCard.svelte
··· 8 8 </script> 9 9 10 10 <div 11 - class={["line-clamp-1 inline-flex h-full w-full rounded-md p-1 px-2 font-semibold", 11 + class={[ 12 + 'line-clamp-1 inline-flex h-full w-full rounded-md p-1 px-2 font-semibold', 12 13 textAlignClasses[item.cardData.textAlign as string], 13 14 verticalAlignClasses[item.cardData.verticalAlign ?? ('center' as string)], 14 15 textSizeClasses[(item.cardData.textSize ?? 1) as number]
+1 -3
src/lib/cards/SectionCard/index.ts
··· 29 29 settingsComponent: SectionCardSettings 30 30 } as CardDefinition & { type: 'section' }; 31 31 32 - 33 - 34 32 export const textAlignClasses: Record<string, string> = { 35 33 left: '', 36 34 center: 'text-center justify-center', ··· 43 41 bottom: 'items-end-safe' 44 42 }; 45 43 46 - export const textSizeClasses = ['text-lg', 'text-2xl', 'text-4xl', 'text-6xl']; 44 + export const textSizeClasses = ['text-lg', 'text-2xl', 'text-4xl', 'text-5xl'];
-16
src/lib/cards/StatusphereCard/EditStatusphereCard.svelte
··· 5 5 import { CardDefinitionsByType } from '..'; 6 6 import { PopoverEmojiPicker } from '@foxui/social'; 7 7 import { emojiToNotoAnimatedWebp } from '.'; 8 - import PlainTextEditor from '../utils/PlainTextEditor.svelte'; 9 - import { cn } from '@foxui/core'; 10 8 11 9 let { item }: { item: Item } = $props(); 12 10 ··· 67 65 </button> 68 66 {/snippet} 69 67 </PopoverEmojiPicker> 70 - 71 - <div 72 - class={cn( 73 - 'bg-base-200/30 dark:bg-base-900/30 absolute top-2 right-2 left-2 z-30 rounded-lg p-1 backdrop-blur-md', 74 - !item.cardData.title && 'hidden group-hover/card:block' 75 - )} 76 - > 77 - <PlainTextEditor 78 - class="text-base-900 dark:text-base-50 text-md line-clamp-1 font-bold" 79 - key="title" 80 - bind:item 81 - placeholder="I'm feeling..." 82 - /> 83 - </div> 84 68 </div>
-8
src/lib/cards/StatusphereCard/StatusphereCard.svelte
··· 22 22 {:else} 23 23 No status yet 24 24 {/if} 25 - 26 - {#if item.cardData.title} 27 - <div 28 - class="text-base-900 dark:text-base-50 text-md bg-base-200/30 dark:bg-base-900/30 absolute top-2 right-2 left-2 z-30 line-clamp-1 rounded-lg p-1 font-bold backdrop-blur-md" 29 - > 30 - {item.cardData.title} 31 - </div> 32 - {/if} 33 25 </div>
+8 -1
src/lib/cards/StatusphereCard/index.ts
··· 40 40 } 41 41 42 42 return item; 43 - } 43 + }, 44 + 45 + migrate: (item) => { 46 + if (item.cardData.title && !item.cardData.label) { 47 + item.cardData.label = item.cardData.title; 48 + } 49 + }, 50 + canHaveLabel: true 44 51 } as CardDefinition & { type: 'statusphere' }; 45 52 46 53 export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined {
+2 -1
src/lib/cards/TealFMPlaysCard/index.ts
··· 21 21 return data; 22 22 }, 23 23 minW: 4, 24 - sidebarButtonText: 'teal.fm Plays' 24 + sidebarButtonText: 'teal.fm Plays', 25 + canHaveLabel: true 25 26 } as CardDefinition & { type: 'recentTealFMPlays' };
+1 -1
src/lib/cards/helper.ts
··· 15 15 } 16 16 17 17 export function getHexOfCardColor(item: Item) { 18 - let color = 18 + const color = 19 19 !item.color || item.color === 'transparent' || item.color === 'base' ? 'accent' : item.color; 20 20 21 21 return convertCSSToHex(getCSSVar(`--color-${color}-500`));
+5
src/lib/cards/types.ts
··· 19 19 20 20 export type ContentComponentProps = { 21 21 item: Item; 22 + isEditing?: boolean; 22 23 }; 23 24 24 25 export type CardDefinition = { ··· 69 70 change?: (item: Item) => Item; 70 71 71 72 name?: string; 73 + 74 + canHaveLabel?: boolean; 75 + 76 + migrate?: (item: Item) => void; 72 77 };
+2 -5
src/lib/cards/utils/PlainTextEditor.svelte
··· 74 74 75 75 <style> 76 76 :global(.tiptap p.is-editor-empty:first-child::before) { 77 - color: var(--color-base-800); 77 + color: var(--color-base-500); 78 78 content: attr(data-placeholder); 79 - opacity: 50%; 79 + opacity: 100%; 80 80 float: left; 81 81 height: 0; 82 82 pointer-events: none; 83 - } 84 - :global(.dark .tiptap p.is-editor-empty:first-child::before) { 85 - color: var(--color-base-200); 86 83 } 87 84 </style>
+12 -1
src/lib/website/load.ts
··· 163 163 return data; 164 164 } 165 165 166 + function migrateCards(data: WebsiteData): WebsiteData { 167 + for (const card of data.cards) { 168 + const cardDef = CardDefinitionsByType[card.cardType]; 169 + 170 + if (!cardDef?.migrate) continue; 171 + 172 + cardDef.migrate(card); 173 + } 174 + return data; 175 + } 176 + 166 177 function checkData(data: WebsiteData): WebsiteData { 167 178 data = migrateData(data); 168 179 ··· 182 193 } 183 194 184 195 function migrateData(data: WebsiteData): WebsiteData { 185 - return migrateFromV1ToV2(migrateFromV0ToV1(data)); 196 + return migrateCards(migrateFromV1ToV2(migrateFromV0ToV1(data))); 186 197 }