your personal website on atproto - mirror blento.app

Merge pull request #119 from flo-bit/remove-extra-buttons

Remove extra buttons

authored by Florian and committed by GitHub fe185592 6e82c6b7

+267 -135
+106
src/lib/cards/FriendsCard/FriendsCard.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import type { ContentComponentProps } from '../types'; 4 + import { getAdditionalUserData, getCanEdit, getIsMobile } from '$lib/website/context'; 5 + import { getBlentoOrBskyProfile } from '$lib/atproto/methods'; 6 + import type { FriendsProfile } from '.'; 7 + import type { Did } from '@atcute/lexicons'; 8 + import { Avatar } from '@foxui/core'; 9 + 10 + let { item }: ContentComponentProps = $props(); 11 + 12 + const isMobile = getIsMobile(); 13 + const canEdit = getCanEdit(); 14 + const additionalData = getAdditionalUserData(); 15 + 16 + let dids: string[] = $derived(item.cardData.friends ?? []); 17 + 18 + let serverProfiles: FriendsProfile[] = $derived( 19 + (additionalData[item.cardType] as FriendsProfile[]) ?? [] 20 + ); 21 + 22 + let clientProfiles: FriendsProfile[] = $state([]); 23 + 24 + let profiles = $derived.by(() => { 25 + if (serverProfiles.length > 0) { 26 + return dids 27 + .map((did) => serverProfiles.find((p) => p.did === did)) 28 + .filter((p): p is FriendsProfile => !!p); 29 + } 30 + return dids 31 + .map((did) => clientProfiles.find((p) => p.did === did)) 32 + .filter((p): p is FriendsProfile => !!p); 33 + }); 34 + 35 + onMount(() => { 36 + if (serverProfiles.length === 0 && dids.length > 0) { 37 + loadProfiles(); 38 + } 39 + }); 40 + 41 + async function loadProfiles() { 42 + const results = await Promise.all( 43 + dids.map((did) => getBlentoOrBskyProfile({ did: did as Did }).catch(() => undefined)) 44 + ); 45 + clientProfiles = results.filter( 46 + (p): p is FriendsProfile => !!p && p.handle !== 'handle.invalid' 47 + ); 48 + } 49 + 50 + // Reload when dids change in editing mode 51 + $effect(() => { 52 + if (canEdit() && dids.length > 0) { 53 + loadProfiles(); 54 + } 55 + }); 56 + 57 + let sizeClass = $derived.by(() => { 58 + const w = isMobile() ? item.mobileW / 2 : item.w; 59 + if (w < 3) return 'sm'; 60 + if (w < 5) return 'md'; 61 + return 'lg'; 62 + }); 63 + 64 + function getLink(profile: FriendsProfile): string { 65 + if (profile.hasBlento && profile.handle && profile.handle !== 'handle.invalid') { 66 + return `/${profile.handle}`; 67 + } 68 + if (profile.handle && profile.handle !== 'handle.invalid') { 69 + return `https://bsky.app/profile/${profile.handle}`; 70 + } 71 + return `https://bsky.app/profile/${profile.did}`; 72 + } 73 + </script> 74 + 75 + <div class="flex h-full w-full items-center justify-center overflow-hidden px-2"> 76 + {#if dids.length === 0} 77 + {#if canEdit()} 78 + <span class="text-base-400 dark:text-base-500 accent:text-accent-300 text-sm"> 79 + Add friends in settings 80 + </span> 81 + {/if} 82 + {:else} 83 + {@const olX = sizeClass === 'sm' ? 12 : sizeClass === 'md' ? 20 : 24} 84 + {@const olY = sizeClass === 'sm' ? 8 : sizeClass === 'md' ? 12 : 16} 85 + <div class=""> 86 + <div 87 + class="flex flex-wrap items-center justify-center" 88 + style="padding: {olY}px 0 0 {olX}px;" 89 + > 90 + {#each profiles as profile (profile.did)} 91 + <a 92 + href={getLink(profile)} 93 + class="accent:ring-accent-500 relative block rounded-full ring-2 ring-white transition-transform hover:scale-110 dark:ring-neutral-900" 94 + style="margin: -{olY}px 0 0 -{olX}px;" 95 + > 96 + <Avatar 97 + src={profile.avatar} 98 + alt={profile.handle} 99 + class={sizeClass === 'sm' ? 'size-12' : sizeClass === 'md' ? 'size-16' : 'size-20'} 100 + /> 101 + </a> 102 + {/each} 103 + </div> 104 + </div> 105 + {/if} 106 + </div>
+104
src/lib/cards/FriendsCard/FriendsCardSettings.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import type { Item } from '$lib/types'; 4 + import type { SettingsComponentProps } from '../types'; 5 + import type { AppBskyActorDefs } from '@atcute/bluesky'; 6 + import type { Did } from '@atcute/lexicons'; 7 + import type { FriendsProfile } from '.'; 8 + import { getBlentoOrBskyProfile } from '$lib/atproto/methods'; 9 + import HandleInput from '$lib/atproto/UI/HandleInput.svelte'; 10 + import { Avatar, Button } from '@foxui/core'; 11 + 12 + let { item = $bindable<Item>() }: SettingsComponentProps = $props(); 13 + 14 + let handleValue = $state(''); 15 + let inputRef: HTMLInputElement | null = $state(null); 16 + let profiles: FriendsProfile[] = $state([]); 17 + 18 + let dids: string[] = $derived(item.cardData.friends ?? []); 19 + 20 + onMount(() => { 21 + loadProfiles(); 22 + }); 23 + 24 + async function loadProfiles() { 25 + const results = await Promise.all( 26 + dids.map((did) => getBlentoOrBskyProfile({ did: did as Did }).catch(() => undefined)) 27 + ); 28 + profiles = results.filter( 29 + (p): p is FriendsProfile => !!p && p.handle !== 'handle.invalid' 30 + ); 31 + } 32 + 33 + function addFriend(actor: AppBskyActorDefs.ProfileViewBasic) { 34 + if (!item.cardData.friends) item.cardData.friends = []; 35 + if (item.cardData.friends.includes(actor.did)) return; 36 + item.cardData.friends = [...item.cardData.friends, actor.did]; 37 + profiles = [ 38 + ...profiles, 39 + { 40 + did: actor.did, 41 + handle: actor.handle, 42 + displayName: actor.displayName || actor.handle, 43 + avatar: actor.avatar, 44 + hasBlento: false 45 + } as FriendsProfile 46 + ]; 47 + requestAnimationFrame(() => { 48 + handleValue = ''; 49 + if (inputRef) inputRef.value = ''; 50 + }); 51 + } 52 + 53 + function removeFriend(did: string) { 54 + item.cardData.friends = item.cardData.friends.filter((d: string) => d !== did); 55 + profiles = profiles.filter((p) => p.did !== did); 56 + } 57 + 58 + function getProfile(did: string): FriendsProfile | undefined { 59 + return profiles.find((p) => p.did === did); 60 + } 61 + </script> 62 + 63 + <div class="flex flex-col gap-3"> 64 + <HandleInput bind:value={handleValue} onselected={addFriend} bind:ref={inputRef} /> 65 + 66 + {#if dids.length > 0} 67 + <div class="flex flex-col gap-1.5"> 68 + {#each dids as did (did)} 69 + {@const profile = getProfile(did)} 70 + <div class="flex items-center gap-2"> 71 + <Avatar 72 + src={profile?.avatar} 73 + alt={profile?.handle ?? did} 74 + class="size-6 rounded-full" 75 + /> 76 + <span class="min-w-0 flex-1 truncate text-sm"> 77 + {profile?.handle ?? did} 78 + </span> 79 + <Button 80 + variant="ghost" 81 + size="icon" 82 + class="size-6 min-w-6" 83 + onclick={() => removeFriend(did)} 84 + > 85 + <svg 86 + xmlns="http://www.w3.org/2000/svg" 87 + fill="none" 88 + viewBox="0 0 24 24" 89 + stroke-width="2" 90 + stroke="currentColor" 91 + class="size-3.5" 92 + > 93 + <path 94 + stroke-linecap="round" 95 + stroke-linejoin="round" 96 + d="M6 18 18 6M6 6l12 12" 97 + /> 98 + </svg> 99 + </Button> 100 + </div> 101 + {/each} 102 + </div> 103 + {/if} 104 + </div>
+44
src/lib/cards/FriendsCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import type { Did } from '@atcute/lexicons'; 3 + import { getBlentoOrBskyProfile } from '$lib/atproto/methods'; 4 + import FriendsCard from './FriendsCard.svelte'; 5 + import FriendsCardSettings from './FriendsCardSettings.svelte'; 6 + 7 + export type FriendsProfile = Awaited<ReturnType<typeof getBlentoOrBskyProfile>>; 8 + 9 + export const FriendsCardDefinition = { 10 + type: 'friends', 11 + contentComponent: FriendsCard, 12 + settingsComponent: FriendsCardSettings, 13 + createNew: (card) => { 14 + card.w = 4; 15 + card.h = 2; 16 + card.mobileW = 8; 17 + card.mobileH = 4; 18 + card.cardData.friends = []; 19 + }, 20 + loadData: async (items) => { 21 + const allDids = new Set<Did>(); 22 + for (const item of items) { 23 + for (const did of item.cardData.friends ?? []) { 24 + allDids.add(did as Did); 25 + } 26 + } 27 + if (allDids.size === 0) return []; 28 + 29 + const profiles = await Promise.all( 30 + Array.from(allDids).map((did) => 31 + getBlentoOrBskyProfile({ did }).catch(() => undefined) 32 + ) 33 + ); 34 + return profiles.filter((p) => p && p.handle !== 'handle.invalid'); 35 + }, 36 + allowSetColor: true, 37 + defaultColor: 'base', 38 + minW: 2, 39 + minH: 2, 40 + name: 'Friends', 41 + groups: ['Social'], 42 + keywords: ['friends', 'avatars', 'people', 'community', 'blentos'], 43 + 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="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" /></svg>` 44 + } as CardDefinition & { type: 'friends' };
+8 -1
src/lib/cards/PhotoGalleryCard/PhotoGalleryCard.svelte
··· 49 49 }); 50 50 51 51 let images = $derived( 52 - feed 52 + (feed 53 53 ?.toSorted((a: PhotoItem, b: PhotoItem) => { 54 54 return (a.value.position ?? 0) - (b.value.position ?? 0); 55 55 }) ··· 63 63 position: i.value.position ?? 0 64 64 }; 65 65 }) 66 + .filter((i) => i.src !== undefined) || []) as { 67 + src: string; 68 + name: string; 69 + width: number; 70 + height: number; 71 + position: number; 72 + }[] 66 73 ); 67 74 68 75 let isMobile = getIsMobile();
+3 -1
src/lib/cards/index.ts
··· 34 34 import { SpotifyCardDefinition } from './SpotifyCard'; 35 35 import { ButtonCardDefinition } from './ButtonCard'; 36 36 import { GuestbookCardDefinition } from './GuestbookCard'; 37 + import { FriendsCardDefinition } from './FriendsCard'; 37 38 // import { Model3DCardDefinition } from './Model3DCard'; 38 39 39 40 export const AllCardDefinitions = [ ··· 71 72 TimerCardDefinition, 72 73 ClockCardDefinition, 73 74 CountdownCardDefinition, 74 - SpotifyCardDefinition 75 + SpotifyCardDefinition, 75 76 // Model3DCardDefinition 77 + FriendsCardDefinition 76 78 ] as const; 77 79 78 80 export const CardDefinitionsByType = AllCardDefinitions.reduce(
-116
src/lib/website/EditBar.svelte
··· 331 331 </Button> 332 332 </div> 333 333 {:else} 334 - <!-- Normal add-card controls --> 335 334 <div class="flex items-center gap-2"> 336 - <Button 337 - size="iconLg" 338 - variant="ghost" 339 - class="backdrop-blur-none" 340 - onclick={() => { 341 - newCard('section'); 342 - }} 343 - > 344 - <svg 345 - xmlns="http://www.w3.org/2000/svg" 346 - viewBox="0 0 24 24" 347 - fill="none" 348 - stroke="currentColor" 349 - stroke-width="2" 350 - stroke-linecap="round" 351 - stroke-linejoin="round" 352 - ><path d="M6 12h12" /><path d="M6 20V4" /><path d="M18 20V4" /></svg 353 - > 354 - </Button> 355 - 356 - <Button 357 - size="iconLg" 358 - variant="ghost" 359 - class="backdrop-blur-none" 360 - onclick={() => { 361 - newCard('text'); 362 - }} 363 - > 364 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" 365 - ><path 366 - fill="none" 367 - stroke="currentColor" 368 - stroke-linecap="round" 369 - stroke-linejoin="round" 370 - stroke-width="2" 371 - d="m15 16l2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16m-6.303-2h5.606M2 16l4.039-9.69a.5.5 0 0 1 .923 0L11 16m-7.696-3h6.392" 372 - /></svg 373 - > 374 - </Button> 375 - 376 - <Popover sideOffset={16} bind:open={linkPopoverOpen} class="bg-base-100 dark:bg-base-900"> 377 - {#snippet child({ props })} 378 - <Button 379 - size="iconLg" 380 - variant="ghost" 381 - class="backdrop-blur-none" 382 - onclick={() => { 383 - newCard('link'); 384 - }} 385 - {...props} 386 - > 387 - <svg 388 - xmlns="http://www.w3.org/2000/svg" 389 - fill="none" 390 - viewBox="-2 -2 28 28" 391 - stroke-width="2" 392 - stroke="currentColor" 393 - > 394 - <path 395 - stroke-linecap="round" 396 - stroke-linejoin="round" 397 - d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" 398 - /> 399 - </svg> 400 - </Button> 401 - {/snippet} 402 - <Input 403 - spellcheck={false} 404 - type="url" 405 - bind:value={linkValue} 406 - onkeydown={(event) => { 407 - if (event.code === 'Enter') { 408 - addLink(linkValue); 409 - event.preventDefault(); 410 - } 411 - }} 412 - placeholder="Enter link" 413 - /> 414 - <Button onclick={() => addLink(linkValue)} size="icon" 415 - ><svg 416 - xmlns="http://www.w3.org/2000/svg" 417 - fill="none" 418 - viewBox="0 0 24 24" 419 - stroke-width="2" 420 - stroke="currentColor" 421 - class="size-6" 422 - > 423 - <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> 424 - </svg> 425 - </Button> 426 - </Popover> 427 - 428 - <Button 429 - size="iconLg" 430 - variant="ghost" 431 - class="backdrop-blur-none" 432 - onclick={() => { 433 - imageInputRef?.click(); 434 - }} 435 - > 436 - <svg 437 - xmlns="http://www.w3.org/2000/svg" 438 - fill="none" 439 - viewBox="0 0 24 24" 440 - stroke-width="2" 441 - stroke="currentColor" 442 - > 443 - <path 444 - stroke-linecap="round" 445 - stroke-linejoin="round" 446 - d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" 447 - /> 448 - </svg> 449 - </Button> 450 - 451 335 <Button size="iconLg" variant="ghost" class="backdrop-blur-none" onclick={showCardCommand}> 452 336 <svg 453 337 xmlns="http://www.w3.org/2000/svg"
-9
src/lib/website/EditableWebsite.svelte
··· 858 858 <Account {data} /> 859 859 860 860 <Context {data}> 861 - {#if !dev} 862 - <div 863 - class="bg-base-200 dark:bg-base-800 fixed inset-0 z-50 inline-flex h-full w-full items-center justify-center p-4 text-center lg:hidden" 864 - > 865 - Editing on mobile is not supported yet. Please use a desktop browser. 866 - </div> 867 - {/if} 868 - 869 861 <CardCommand 870 862 bind:open={showCardCommand} 871 863 onselect={(cardDef: CardDefinition) => { ··· 939 931 > 940 932 <div class="pointer-events-none"></div> 941 933 <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events --> 942 - <!-- svelte-ignore a11y_click_events_have_key_events --> 943 934 <div 944 935 bind:this={container} 945 936 onclick={(e) => {
+2 -8
src/lib/website/layout-mirror.ts
··· 51 51 if (fromMobile) { 52 52 // Mobile → Desktop: reflow items to use the full grid width. 53 53 // Sort by mobile position so items are placed in reading order. 54 - const sorted = items.toSorted( 55 - (a, b) => a.mobileY - b.mobileY || a.mobileX - b.mobileX 56 - ); 54 + const sorted = items.toSorted((a, b) => a.mobileY - b.mobileY || a.mobileX - b.mobileX); 57 55 58 56 // Place each item into the first available spot on the desktop grid 59 57 const placed: Item[] = []; ··· 66 64 } else { 67 65 // Desktop → Mobile: proportional positions 68 66 for (const item of items) { 69 - item.mobileX = clamp( 70 - Math.floor((item.x * 2) / 2) * 2, 71 - 0, 72 - COLUMNS - item.mobileW 73 - ); 67 + item.mobileX = clamp(Math.floor((item.x * 2) / 2) * 2, 0, COLUMNS - item.mobileW); 74 68 item.mobileY = Math.max(0, Math.round(item.y * 2)); 75 69 } 76 70 fixAllCollisions(items, true);