your personal website on atproto - mirror blento.app
25
fork

Configure Feed

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

at mail-icon 177 lines 4.6 kB view raw
1<script lang="ts"> 2 import { Button, Input, Modal, Subheading } from '@foxui/core'; 3 import { env } from '$env/dynamic/public'; 4 import PoweredByGiphy from './PoweredByGiphy.gif'; 5 6 type GiphyGif = { 7 id: string; 8 title: string; 9 images: { 10 fixed_height: { url: string; mp4: string; width: string; height: string }; 11 original: { mp4: string }; 12 }; 13 }; 14 15 let { 16 open = $bindable(false), 17 onselect, 18 oncancel 19 }: { 20 open: boolean; 21 onselect: (gif: GiphyGif) => void; 22 oncancel: () => void; 23 } = $props(); 24 25 let searchQuery = $state(''); 26 let searchResults = $state<GiphyGif[]>([]); 27 let trendingResults = $state<GiphyGif[]>([]); 28 let isLoading = $state(false); 29 let searchTimeout: ReturnType<typeof setTimeout> | null = null; 30 31 // Split results into 4 columns for masonry layout 32 let columns = $derived(() => { 33 const results = searchQuery.trim() ? searchResults : trendingResults; 34 const cols: [GiphyGif[], GiphyGif[], GiphyGif[], GiphyGif[]] = [[], [], [], []]; 35 results.forEach((gif, i) => { 36 cols[i % 4].push(gif); 37 }); 38 return cols; 39 }); 40 41 let displayResults = $derived(searchQuery.trim() ? searchResults : trendingResults); 42 43 async function fetchTrending() { 44 if (trendingResults.length > 0) return; 45 46 isLoading = true; 47 try { 48 const url = new URL('https://api.giphy.com/v1/gifs/trending'); 49 url.searchParams.set('api_key', env.PUBLIC_GIPHY_API_TOKEN); 50 url.searchParams.set('limit', '24'); 51 url.searchParams.set('rating', 'g'); 52 53 const response = await fetch(url.toString()); 54 if (response.ok) { 55 const data = await response.json(); 56 trendingResults = data.data || []; 57 } 58 } catch (error) { 59 console.error('Failed to fetch trending:', error); 60 } finally { 61 isLoading = false; 62 } 63 } 64 65 async function searchGiphy(query: string) { 66 if (!query.trim()) { 67 searchResults = []; 68 return; 69 } 70 71 isLoading = true; 72 try { 73 const url = new URL('https://api.giphy.com/v1/gifs/search'); 74 url.searchParams.set('api_key', env.PUBLIC_GIPHY_API_TOKEN); 75 url.searchParams.set('q', query); 76 url.searchParams.set('limit', '24'); 77 url.searchParams.set('rating', 'g'); 78 url.searchParams.set('lang', 'en'); 79 80 const response = await fetch(url.toString()); 81 if (response.ok) { 82 const data = await response.json(); 83 searchResults = data.data || []; 84 } 85 } catch (error) { 86 console.error('Failed to search Giphy:', error); 87 } finally { 88 isLoading = false; 89 } 90 } 91 92 function handleSearchInput() { 93 if (searchTimeout) clearTimeout(searchTimeout); 94 searchTimeout = setTimeout(() => { 95 searchGiphy(searchQuery); 96 }, 300); 97 } 98 99 function selectGif(gif: GiphyGif) { 100 onselect(gif); 101 resetState(); 102 } 103 104 function handleCancel() { 105 oncancel(); 106 resetState(); 107 } 108 109 function resetState() { 110 searchQuery = ''; 111 searchResults = []; 112 } 113 114 $effect(() => { 115 if (open) { 116 fetchTrending(); 117 } 118 }); 119</script> 120 121<Modal 122 bind:open 123 onOpenChange={(isOpen) => !isOpen && handleCancel()} 124 closeButton={true} 125 class="flex h-[80dvh] max-w-4xl flex-col" 126> 127 <Subheading>{searchQuery.trim() ? 'Search GIPHY' : 'Trending GIFs'}</Subheading> 128 129 <Input 130 bind:value={searchQuery} 131 oninput={handleSearchInput} 132 placeholder="Search for GIFs..." 133 class="w-full" 134 /> 135 136 <div class="mt-4 flex-1 overflow-y-auto"> 137 {#if isLoading && displayResults.length === 0} 138 <div class="flex h-75 items-center justify-center"> 139 <p class="text-base-500">Loading...</p> 140 </div> 141 {:else if displayResults.length > 0} 142 <div class="flex items-start gap-3"> 143 {#each columns() as column, i (i)} 144 <div class="flex w-1/4 flex-col gap-3"> 145 {#each column as gif (gif.id)} 146 <button 147 onclick={() => selectGif(gif)} 148 aria-label={gif.title} 149 class="focus:ring-accent-500 block shrink-0 overflow-hidden rounded-xl transition-transform hover:scale-[1.02] focus:ring-2 focus:outline-none" 150 > 151 <video 152 src={gif.images.fixed_height.mp4} 153 autoplay 154 loop 155 muted 156 playsinline 157 class="bg-base-200 dark:bg-base-800 block w-full" 158 style="aspect-ratio: {gif.images.fixed_height.width} / {gif.images.fixed_height 159 .height}" 160 ></video> 161 </button> 162 {/each} 163 </div> 164 {/each} 165 </div> 166 {:else if searchQuery} 167 <div class="flex h-75 items-center justify-center"> 168 <p class="text-base-500">No results found</p> 169 </div> 170 {/if} 171 </div> 172 173 <div class="mt-4 flex items-center justify-between"> 174 <img src={PoweredByGiphy} alt="Powered by GIPHY" class="h-7 rounded-md" /> 175 <Button onclick={handleCancel} variant="ghost">Cancel</Button> 176 </div> 177</Modal>