your personal website on atproto - mirror
0
fork

Configure Feed

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

Present "TopEight" feature code

Woovie db1c2c5a cf1ddedc

+282 -33
+122
src/lib/cards/BlueskyTopEight/AddUserModal.svelte
··· 1 + <script lang="ts"> 2 + import { Button, Input, Modal, Subheading } from '@foxui/core'; 3 + import { searchActorsTypeahead } from '$lib/atproto/methods'; 4 + import type { AppBskyActorDefs } from '@atcute/bluesky'; 5 + 6 + let { 7 + open = $bindable(false), 8 + onadd, 9 + existingHandles = [] 10 + }: { 11 + open: boolean; 12 + onadd: (handle: string) => void; 13 + existingHandles: string[]; 14 + } = $props(); 15 + 16 + let searchQuery = $state(''); 17 + let searchResults: AppBskyActorDefs.ProfileViewBasic[] = $state([]); 18 + let isSearching = $state(false); 19 + let searchTimeout: ReturnType<typeof setTimeout> | undefined; 20 + let lastSearchQuery = $state(''); 21 + 22 + async function performSearch() { 23 + if (!searchQuery.trim()) { 24 + searchResults = []; 25 + return; 26 + } 27 + 28 + isSearching = true; 29 + const result = await searchActorsTypeahead(searchQuery, 10); 30 + 31 + // Only update if query matches what we searched for 32 + if (result.q === searchQuery) { 33 + // Filter out users already in the list 34 + searchResults = result.actors.filter((actor) => !existingHandles.includes(actor.handle)); 35 + lastSearchQuery = searchQuery; 36 + } 37 + isSearching = false; 38 + } 39 + 40 + function handleSearchInput() { 41 + if (searchTimeout) clearTimeout(searchTimeout); 42 + searchTimeout = setTimeout(performSearch, 300); 43 + } 44 + 45 + function handleAdd(handle: string) { 46 + onadd(handle); 47 + searchQuery = ''; 48 + searchResults = []; 49 + open = false; 50 + } 51 + 52 + function handleClose() { 53 + searchQuery = ''; 54 + searchResults = []; 55 + open = false; 56 + } 57 + </script> 58 + 59 + <Modal bind:open closeButton={true} class="flex max-h-screen flex-col"> 60 + <Subheading>Add a Bluesky user</Subheading> 61 + 62 + <Input 63 + bind:value={searchQuery} 64 + placeholder="Search for a user..." 65 + oninput={handleSearchInput} 66 + onkeydown={(e) => { 67 + if (e.key === 'Escape') handleClose(); 68 + }} 69 + /> 70 + 71 + <div class="bg-base-100 dark:bg-base-950 mt-4 max-h-[40dvh] min-h-32 overflow-y-auto rounded-xl"> 72 + {#if isSearching} 73 + <div class="text-base-500 p-4 text-center italic">Searching...</div> 74 + {:else if searchResults.length === 0 && searchQuery.trim()} 75 + <div class="text-base-500 p-4 text-center italic"> 76 + No users found{existingHandles.length > 0 ? ' (or already added)' : ''} 77 + </div> 78 + {:else if searchResults.length === 0} 79 + <div class="text-base-500 p-4 text-center italic">Type to search for Bluesky users</div> 80 + {:else} 81 + {#each searchResults as actor (actor.did)} 82 + <button 83 + onclick={() => handleAdd(actor.handle)} 84 + class="hover:bg-base-200 dark:hover:bg-base-800 flex w-full items-center gap-3 p-3 text-left transition-colors" 85 + > 86 + {#if actor.avatar} 87 + <img src={actor.avatar} alt="" class="size-10 shrink-0 rounded-full object-cover" /> 88 + {:else} 89 + <div 90 + class="bg-base-300 dark:bg-base-700 flex size-10 shrink-0 items-center justify-center rounded-full" 91 + > 92 + <svg 93 + xmlns="http://www.w3.org/2000/svg" 94 + fill="none" 95 + viewBox="0 0 24 24" 96 + stroke-width="1.5" 97 + stroke="currentColor" 98 + class="size-5" 99 + > 100 + <path 101 + stroke-linecap="round" 102 + stroke-linejoin="round" 103 + d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" 104 + /> 105 + </svg> 106 + </div> 107 + {/if} 108 + <div class="min-w-0 flex-1"> 109 + <div class="text-base-900 dark:text-base-50 truncate font-medium"> 110 + {actor.displayName || actor.handle} 111 + </div> 112 + <div class="text-base-500 truncate text-sm">@{actor.handle}</div> 113 + </div> 114 + </button> 115 + {/each} 116 + {/if} 117 + </div> 118 + 119 + <div class="mt-4 flex justify-end"> 120 + <Button onclick={handleClose} variant="ghost">Cancel</Button> 121 + </div> 122 + </Modal>
+12 -33
src/lib/cards/BlueskyTopEight/BlueskyTopEightCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { getAdditionalUserData } from '$lib/website/context'; 3 - import { AppBskyActorDefs } from '@atcute/bluesky'; 4 3 5 4 import type { ContentComponentProps } from '../types'; 5 + 6 + import ProfileContainer from './ProfileContainer.svelte'; 6 7 7 8 let { item }: ContentComponentProps = $props(); 8 9 9 10 const data = getAdditionalUserData(); 10 11 11 - // svelte-ignore state_referenced_locally 12 - const profiles = data[item.cardType][item.id] as AppBskyActorDefs.ProfileViewDetailed[]; 12 + const profileCardData = $derived({ 13 + "card": { 14 + "width": item.w, 15 + "height": item.h 16 + }, 17 + "profiles": data[item.cardType][item.id] 18 + }) 13 19 </script> 14 20 15 - <div class="flex h-full flex-col justify-center-safe overflow-y-scroll p-2"> 16 - <div class="topEightContainer"> 17 - {#each profiles as profile (profile.did)} 18 - <img 19 - class="topEightProfilePicture" 20 - src={profile.avatar} 21 - alt="Profile picture of {profile.handle}" 22 - style="--card-height: {item.h}; --card-width: {item.w}" 23 - /> 24 - {/each} 25 - </div> 26 - </div> 27 - 28 - <style scoped> 29 - .topEightContainer { 30 - flex-grow: 1; 31 - overflow: hidden; 32 - display: flex; 33 - flex-direction: row; 34 - flex-wrap: wrap; 35 - justify-content: center; 36 - } 37 - 38 - .topEightProfilePicture { 39 - margin: 0.5rem; 40 - width: calc(var(--card-width) * 1.15rem); 41 - aspect-ratio: 1 / 1; 42 - border-radius: 0.5rem; 43 - } 44 - </style> 21 + <div class="flex h-full flex-col justify-center-safe overflow-y-scroll rounded-[inherit] p-2"> 22 + <ProfileContainer cardData={profileCardData}></ProfileContainer> 23 + </div>
+65
src/lib/cards/BlueskyTopEight/EditingBlueskyTopEightCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { getAdditionalUserData } from '$lib/website/context'; 4 + import type { AppBskyActorDefs } from '@atcute/bluesky'; 5 + import type { ContentComponentProps } from '../types'; 6 + import type { Editor } from '@tiptap/core'; 7 + 8 + let { item = $bindable<Item>() }: ContentComponentProps = $props(); 9 + 10 + let data = getAdditionalUserData(); 11 + 12 + let profilesSaved = data[item.cardType][item.id] as AppBskyActorDefs.ProfileViewBasic[]; 13 + 14 + let profilesEditing = []; 15 + 16 + let editor: Editor | null = $state(null); 17 + </script> 18 + 19 + <div class="profileCenteringContainer"> 20 + <div class="profileContainer" style="grid-template-columns: repeat({item.w}, 1fr);"> 21 + {#each { length: item.h * item.w }, index} 22 + <div class="profileItem"> 23 + <img src="{profilesSaved[index].avatar}" alt="profile picture of {profilesSaved[index].handle}" /> 24 + </div> 25 + {/each} 26 + </div> 27 + </div> 28 + <style scoped> 29 + .profileCenteringContainer { 30 + border-radius: inherit; 31 + width: 100%; 32 + height: 100%; 33 + display: flex; 34 + flex-direction: column; 35 + padding: 0.5rem; 36 + } 37 + 38 + .profileContainer { 39 + display: grid; 40 + overflow: hidden; 41 + border-radius: inherit; 42 + gap: 0; 43 + place-items: center; 44 + flex-shrink: 1; 45 + } 46 + 47 + .profileItem{ 48 + max-width: 100%; 49 + max-height: 100%; 50 + padding: 0.25rem; 51 + overflow: hidden; 52 + border-radius: inherit; 53 + display: flex; 54 + flex-direction: row; 55 + justify-content: center; 56 + aspect-ratio: 1 / 1; 57 + 58 + & img { 59 + max-width: 100%; 60 + max-height: 100%; 61 + aspect-ratio: 1 / 1; 62 + border-radius: inherit; 63 + } 64 + } 65 + </style>
+27
src/lib/cards/BlueskyTopEight/ProfileContainer.svelte
··· 1 + <script lang="ts"> 2 + import ProfileItem from './ProfileItem.svelte'; 3 + 4 + let {cardData}: any = $props(); 5 + 6 + let cardWidth = $derived(cardData.card.width); 7 + let cardHeight = $derived(cardData.card.height); 8 + let cardTotal = $derived(cardWidth * cardHeight); 9 + </script> 10 + 11 + <div class="profileContainer" style="--cardWidth: {cardWidth}; --cardHeight: {cardHeight};"> 12 + {#each cardData.profiles.slice(0, cardTotal) as profile (profile.did)} 13 + <ProfileItem profile={profile}></ProfileItem> 14 + {/each} 15 + </div> 16 + 17 + <style scoped> 18 + .profileContainer { 19 + display: grid; 20 + grid-template-columns: repeat(var(--cardWidth), 1fr); 21 + width: 100%; 22 + height: 100%; 23 + overflow: hidden; 24 + border-radius: inherit; 25 + place-items: center; 26 + } 27 + </style>
+54
src/lib/cards/BlueskyTopEight/ProfileItem.svelte
··· 1 + <script lang="ts"> 2 + import { Button } from '@foxui/core'; 3 + let { profile }: any = $props(); 4 + </script> 5 + 6 + <div class="profileItem"> 7 + <Button 8 + size="icon" 9 + variant="rose" 10 + onclick={() => { 11 + ondelete(); 12 + }} 13 + class="absolute -top-3 -left-3 hidden group-focus-within:inline-flex group-hover/card:inline-flex" 14 + > 15 + <svg 16 + xmlns="http://www.w3.org/2000/svg" 17 + fill="none" 18 + viewBox="0 0 24 24" 19 + stroke-width="1.5" 20 + stroke="currentColor" 21 + > 22 + <path 23 + stroke-linecap="round" 24 + stroke-linejoin="round" 25 + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" 26 + /> 27 + </svg> 28 + 29 + <span class="sr-only">Delete card</span> 30 + </Button> 31 + <img 32 + class="profilePicture" 33 + src={profile.avatar} 34 + alt="Profile picture of {profile.handle}" 35 + /> 36 + </div> 37 + 38 + <style scoped> 39 + .profileItem { 40 + border-radius: inherit; 41 + padding: 0.25rem; 42 + align-content: center; 43 + max-height: 100%; 44 + max-width: 100%; 45 + overflow: hidden; 46 + } 47 + 48 + .profilePicture { 49 + aspect-ratio: 1 / 1; 50 + border-radius: inherit; 51 + max-width: 100%; 52 + max-height: 100%; 53 + } 54 + </style>
+2
src/lib/cards/BlueskyTopEight/index.ts
··· 1 1 import { AtpBaseClient } from '@atproto/api'; 2 2 import type { CardDefinition } from '../types'; 3 3 import BlueskyTopEightCard from './BlueskyTopEightCard.svelte'; 4 + import EditingBlueskyTopEightCard from './EditingBlueskyTopEightCard.svelte'; 4 5 import SidebarItemBlueskyTopEightCard from './SidebarItemBlueskyTopEightCard.svelte'; 5 6 6 7 export const BlueskyTopEightCardDefinition = { 7 8 type: 'topEight', 8 9 contentComponent: BlueskyTopEightCard, 10 + editingContentComponent: EditingBlueskyTopEightCard, 9 11 createNew: (card) => { 10 12 card.cardType = 'topEight'; 11 13 card.w = 8;