your personal website on atproto - mirror blento.app

update gif stuff

Florian 43796b4c 9a75add1

+266 -302
+26
src/lib/cards/GIFCard/CreateGifCardModal.svelte
··· 1 + <script lang="ts"> 2 + import type { CreationModalComponentProps } from '../types'; 3 + import GiphySearchModal from './GiphySearchModal.svelte'; 4 + 5 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 6 + 7 + let isOpen = $state(true); 8 + 9 + function handleGifSelect(gif: { 10 + id: string; 11 + title: string; 12 + images: { original: { mp4: string } }; 13 + }) { 14 + item.cardData.url = gif.images.original.mp4; 15 + item.cardData.alt = gif.title; 16 + isOpen = false; 17 + oncreate(); 18 + } 19 + 20 + function handleCancel() { 21 + isOpen = false; 22 + oncancel(); 23 + } 24 + </script> 25 + 26 + <GiphySearchModal bind:open={isOpen} onselect={handleGifSelect} oncancel={handleCancel} />
+25 -144
src/lib/cards/GIFCard/EditingGifCard.svelte
··· 1 1 <script lang="ts"> 2 - import { getDidContext } from '$lib/website/context'; 3 - import { getImageBlobUrl } from '$lib/oauth/utils'; 4 2 import type { ContentComponentProps } from '../types'; 3 + import GiphySearchModal from './GiphySearchModal.svelte'; 5 4 6 5 let { item = $bindable() }: ContentComponentProps = $props(); 7 6 8 - const did = getDidContext(); 9 - 10 7 let hasError = $state(false); 11 8 let isSearchOpen = $state(false); 12 - let searchQuery = $state(''); 13 - let searchResults = $state< 14 - Array<{ 15 - id: string; 16 - title: string; 17 - images: { 18 - fixed_height: { url: string; width: string; height: string }; 19 - original: { url: string }; 20 - }; 21 - }> 22 - >([]); 23 - let isLoading = $state(false); 24 - let searchTimeout: ReturnType<typeof setTimeout> | null = null; 25 9 26 - function getSrc() { 27 - if (item.cardData.url) return item.cardData.url; 28 - 29 - if (item.cardData.image && typeof item.cardData.image === 'object') { 30 - return getImageBlobUrl({ did, link: item.cardData.image?.ref?.$link }); 31 - } 32 - 33 - return item.cardData.image; 34 - } 35 - 36 - async function searchGiphy(query: string) { 37 - if (!query.trim()) { 38 - searchResults = []; 39 - return; 40 - } 41 - 42 - isLoading = true; 43 - try { 44 - const response = await fetch(`/api/giphy?q=${encodeURIComponent(query)}`); 45 - if (response.ok) { 46 - const data = await response.json(); 47 - searchResults = data.data || []; 48 - } 49 - } catch (error) { 50 - console.error('Failed to search Giphy:', error); 51 - } finally { 52 - isLoading = false; 53 - } 54 - } 55 - 56 - function handleSearchInput() { 57 - if (searchTimeout) clearTimeout(searchTimeout); 58 - searchTimeout = setTimeout(() => { 59 - searchGiphy(searchQuery); 60 - }, 300); 61 - } 62 - 63 - function selectGif(gif: (typeof searchResults)[0]) { 64 - item.cardData.url = gif.images.original.url; 10 + function handleGifSelect(gif: { 11 + id: string; 12 + title: string; 13 + images: { original: { mp4: string } }; 14 + }) { 15 + item.cardData.url = gif.images.original.mp4; 65 16 item.cardData.alt = gif.title; 66 17 hasError = false; 67 18 isSearchOpen = false; 68 - searchQuery = ''; 69 - searchResults = []; 70 19 } 71 20 72 21 function openSearch() { 73 22 isSearchOpen = true; 74 23 } 75 - 76 - function closeSearch() { 77 - isSearchOpen = false; 78 - searchQuery = ''; 79 - searchResults = []; 80 - } 81 24 </script> 82 25 83 26 <!-- svelte-ignore a11y_no_static_element_interactions --> 84 27 <!-- svelte-ignore a11y_click_events_have_key_events --> 85 - <div class="relative h-full w-full overflow-hidden" onclick={openSearch}> 86 - {#if getSrc() && !hasError} 87 - <img 28 + <div class="group relative h-full w-full cursor-pointer overflow-hidden" onclick={openSearch}> 29 + {#if item.cardData.url && !hasError} 30 + <video 88 31 class="absolute inset-0 h-full w-full object-cover" 89 - src={getSrc()} 90 - alt={item.cardData.alt || 'GIF'} 32 + src={item.cardData.url} 33 + autoplay 34 + loop 35 + muted 36 + playsinline 91 37 onerror={() => (hasError = true)} 92 - /> 38 + ></video> 39 + <!-- Click to change overlay --> 40 + <div 41 + class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100" 42 + > 43 + <span class="text-sm font-medium text-white">Click to change</span> 44 + </div> 93 45 {:else} 94 46 <!-- Empty state --> 95 47 <div 96 - class="bg-base-100 dark:bg-base-900 flex h-full w-full cursor-pointer flex-col items-center justify-center gap-3 p-4" 48 + class="bg-base-100 dark:bg-base-900 flex h-full w-full flex-col items-center justify-center gap-3 p-4" 97 49 > 98 50 <div 99 51 class="border-base-300 dark:border-base-700 flex size-12 items-center justify-center rounded-xl border-2 border-dashed" ··· 118 70 </div> 119 71 </div> 120 72 {/if} 121 - 122 - <!-- Giphy search modal --> 123 - {#if isSearchOpen} 124 - <!-- svelte-ignore a11y_click_events_have_key_events --> 125 - <div 126 - class="absolute inset-0 z-50 flex flex-col bg-black/80 backdrop-blur-sm" 127 - onclick={(e) => e.stopPropagation()} 128 - > 129 - <!-- Header --> 130 - <div class="flex items-center gap-2 p-3"> 131 - <input 132 - bind:value={searchQuery} 133 - oninput={handleSearchInput} 134 - class="flex-1 rounded-lg border border-white/20 bg-white/10 px-4 py-2 text-sm text-white placeholder-white/50 outline-none focus:border-white/40 focus:bg-white/20" 135 - placeholder="Search GIPHY..." 136 - /> 137 - <button 138 - onclick={closeSearch} 139 - aria-label="Close search" 140 - class="rounded-lg p-2 text-white/70 transition-colors hover:bg-white/10 hover:text-white" 141 - > 142 - <svg 143 - xmlns="http://www.w3.org/2000/svg" 144 - fill="none" 145 - viewBox="0 0 24 24" 146 - stroke-width="1.5" 147 - stroke="currentColor" 148 - class="size-5" 149 - > 150 - <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 151 - </svg> 152 - </button> 153 - </div> 154 - 155 - <!-- Results grid --> 156 - <div class="flex-1 overflow-y-auto p-2"> 157 - {#if isLoading} 158 - <div class="flex h-full items-center justify-center"> 159 - <p class="text-white/60">Searching...</p> 160 - </div> 161 - {:else if searchResults.length > 0} 162 - <div class="grid grid-cols-2 gap-2"> 163 - {#each searchResults as gif} 164 - <button 165 - onclick={() => selectGif(gif)} 166 - class="overflow-hidden rounded-lg transition-transform hover:scale-[1.02]" 167 - > 168 - <img 169 - src={gif.images.fixed_height.url} 170 - alt={gif.title} 171 - class="h-auto w-full" 172 - loading="lazy" 173 - /> 174 - </button> 175 - {/each} 176 - </div> 177 - {:else if searchQuery} 178 - <div class="flex h-full items-center justify-center"> 179 - <p class="text-white/60">No results found</p> 180 - </div> 181 - {:else} 182 - <div class="flex h-full items-center justify-center"> 183 - <p class="text-white/60">Type to search for GIFs</p> 184 - </div> 185 - {/if} 186 - </div> 73 + </div> 187 74 188 - <!-- Giphy attribution --> 189 - <div class="p-2 text-center"> 190 - <span class="text-xs text-white/40">Powered by GIPHY</span> 191 - </div> 192 - </div> 193 - {/if} 194 - </div> 75 + <GiphySearchModal bind:open={isSearchOpen} onselect={handleGifSelect} oncancel={() => (isSearchOpen = false)} />
+10 -20
src/lib/cards/GIFCard/GifCard.svelte
··· 1 1 <script lang="ts"> 2 - import { getDidContext } from '$lib/website/context'; 3 - import { getImageBlobUrl } from '$lib/oauth/utils'; 4 2 import type { ContentComponentProps } from '../types'; 5 3 6 4 let { item }: ContentComponentProps = $props(); 7 5 8 - const did = getDidContext(); 9 - 10 - function getSrc() { 11 - if (item.cardData.objectUrl) return item.cardData.objectUrl; 12 - 13 - if (item.cardData.image && typeof item.cardData.image === 'object') { 14 - return getImageBlobUrl({ did, link: item.cardData.image?.ref?.$link }); 15 - } 16 - 17 - return item.cardData.url || item.cardData.image; 18 - } 19 - 20 6 let hasError = $state(false); 21 7 </script> 22 8 23 9 <div class="relative h-full w-full overflow-hidden"> 24 - {#key item.cardData.url || item.cardData.image || item.cardData.objectUrl} 25 - {#if getSrc() && !hasError} 26 - <img 10 + {#key item.cardData.url} 11 + {#if item.cardData.url && !hasError} 12 + <video 27 13 class="absolute inset-0 h-full w-full object-cover" 28 - src={getSrc()} 29 - alt={item.cardData.alt || 'GIF'} 14 + src={item.cardData.url} 15 + autoplay 16 + loop 17 + muted 18 + playsinline 30 19 onerror={() => (hasError = true)} 31 - /> 20 + ></video> 21 + 32 22 {:else} 33 23 <div 34 24 class="flex h-full w-full items-center justify-center bg-base-100 dark:bg-base-900"
+13 -87
src/lib/cards/GIFCard/GifCardSettings.svelte
··· 1 1 <script lang="ts"> 2 2 import type { Item } from '$lib/types'; 3 3 import type { SettingsComponentProps } from '../types'; 4 - import { Button, Input, Label } from '@foxui/core'; 4 + import { Button, Label } from '@foxui/core'; 5 + import GiphySearchModal from './GiphySearchModal.svelte'; 5 6 6 7 let { item = $bindable<Item>() }: SettingsComponentProps = $props(); 7 8 8 9 let isSearchOpen = $state(false); 9 - let searchQuery = $state(''); 10 - let searchResults = $state< 11 - Array<{ 12 - id: string; 13 - title: string; 14 - images: { 15 - fixed_height: { url: string; width: string; height: string }; 16 - original: { url: string }; 17 - }; 18 - }> 19 - >([]); 20 - let isLoading = $state(false); 21 - let searchTimeout: ReturnType<typeof setTimeout> | null = null; 22 10 23 - async function searchGiphy(query: string) { 24 - if (!query.trim()) { 25 - searchResults = []; 26 - return; 27 - } 28 - 29 - isLoading = true; 30 - try { 31 - const response = await fetch(`/api/giphy?q=${encodeURIComponent(query)}`); 32 - if (response.ok) { 33 - const data = await response.json(); 34 - searchResults = data.data || []; 35 - } 36 - } catch (error) { 37 - console.error('Failed to search Giphy:', error); 38 - } finally { 39 - isLoading = false; 40 - } 41 - } 42 - 43 - function handleSearchInput() { 44 - if (searchTimeout) clearTimeout(searchTimeout); 45 - searchTimeout = setTimeout(() => { 46 - searchGiphy(searchQuery); 47 - }, 300); 48 - } 49 - 50 - function selectGif(gif: (typeof searchResults)[0]) { 51 - item.cardData.url = gif.images.original.url; 11 + function handleGifSelect(gif: { 12 + id: string; 13 + title: string; 14 + images: { original: { mp4: string } }; 15 + }) { 16 + item.cardData.url = gif.images.original.mp4; 52 17 item.cardData.alt = gif.title; 53 18 isSearchOpen = false; 54 - searchQuery = ''; 55 - searchResults = []; 56 19 } 57 20 </script> 58 21 59 22 <div class="flex flex-col gap-3"> 60 23 <div> 61 - <Label class="mb-1 text-xs">Search GIPHY</Label> 24 + <Label class="mb-1 text-xs">Change GIF</Label> 62 25 <Button 63 26 variant="secondary" 64 27 class="w-full justify-start" 65 - onclick={() => (isSearchOpen = !isSearchOpen)} 28 + onclick={() => (isSearchOpen = true)} 66 29 > 67 30 <svg 68 31 xmlns="http://www.w3.org/2000/svg" ··· 78 41 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" 79 42 /> 80 43 </svg> 81 - Search for GIFs 44 + Search GIPHY 82 45 </Button> 83 46 </div> 84 - 85 - {#if isSearchOpen} 86 - <div class="flex flex-col gap-2"> 87 - <Input 88 - bind:value={searchQuery} 89 - oninput={handleSearchInput} 90 - placeholder="Type to search..." 91 - class="w-full" 92 - autofocus 93 - /> 94 - {#if isLoading} 95 - <p class="text-base-500 text-xs">Searching...</p> 96 - {:else if searchResults.length > 0} 97 - <div class="grid max-h-48 grid-cols-2 gap-1 overflow-y-auto"> 98 - {#each searchResults as gif} 99 - <button 100 - onclick={() => selectGif(gif)} 101 - class="overflow-hidden rounded transition-transform hover:scale-[1.02]" 102 - > 103 - <img 104 - src={gif.images.fixed_height.url} 105 - alt={gif.title} 106 - class="h-auto w-full" 107 - loading="lazy" 108 - /> 109 - </button> 110 - {/each} 111 - </div> 112 - <p class="text-base-400 text-center text-xs">Powered by GIPHY</p> 113 - {:else if searchQuery} 114 - <p class="text-base-500 text-xs">No results found</p> 115 - {/if} 116 - </div> 117 - {/if} 47 + </div> 118 48 119 - <div> 120 - <Label class="mb-1 text-xs">Alt text</Label> 121 - <Input bind:value={item.cardData.alt} placeholder="Description" class="w-full" /> 122 - </div> 123 - </div> 49 + <GiphySearchModal bind:open={isSearchOpen} onselect={handleGifSelect} oncancel={() => (isSearchOpen = false)} />
+171
src/lib/cards/GIFCard/GiphySearchModal.svelte
··· 1 + <script lang="ts"> 2 + import { Button, Input, Modal, Subheading } from '@foxui/core'; 3 + import { PUBLIC_GIPHY_API_TOKEN } from '$env/static/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', 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', 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 bind:open onOpenChange={(isOpen) => !isOpen && handleCancel()} closeButton={true} class="flex h-[80dvh] max-w-4xl flex-col"> 122 + <Subheading>{searchQuery.trim() ? 'Search GIPHY' : 'Trending GIFs'}</Subheading> 123 + 124 + <Input 125 + bind:value={searchQuery} 126 + oninput={handleSearchInput} 127 + placeholder="Search for GIFs..." 128 + class="w-full" 129 + /> 130 + 131 + <div class="mt-4 flex-1 overflow-y-auto"> 132 + {#if isLoading && displayResults.length === 0} 133 + <div class="flex h-[300px] items-center justify-center"> 134 + <p class="text-base-500">Loading...</p> 135 + </div> 136 + {:else if displayResults.length > 0} 137 + <div class="flex items-start gap-3"> 138 + {#each columns() as column} 139 + <div class="flex w-1/4 flex-col gap-3"> 140 + {#each column as gif} 141 + <button 142 + onclick={() => selectGif(gif)} 143 + aria-label={gif.title} 144 + class="block shrink-0 overflow-hidden rounded-xl transition-transform hover:scale-[1.02] focus:ring-2 focus:ring-accent-500 focus:outline-none" 145 + > 146 + <video 147 + src={gif.images.fixed_height.mp4} 148 + autoplay 149 + loop 150 + muted 151 + playsinline 152 + class="block w-full bg-base-200 dark:bg-base-800" 153 + style="aspect-ratio: {gif.images.fixed_height.width} / {gif.images.fixed_height.height}" 154 + ></video> 155 + </button> 156 + {/each} 157 + </div> 158 + {/each} 159 + </div> 160 + {:else if searchQuery} 161 + <div class="flex h-[300px] items-center justify-center"> 162 + <p class="text-base-500">No results found</p> 163 + </div> 164 + {/if} 165 + </div> 166 + 167 + <div class="mt-4 flex items-center justify-between"> 168 + <img src={PoweredByGiphy} alt="Powered by GIPHY" class="h-7 rounded-md" /> 169 + <Button onclick={handleCancel} variant="ghost">Cancel</Button> 170 + </div> 171 + </Modal>
src/lib/cards/GIFCard/PoweredByGiphy.gif

This is a binary file and will not be displayed.

+15 -10
src/lib/cards/GIFCard/index.ts
··· 1 1 import type { CardDefinition } from '../types'; 2 + import CreateGifCardModal from './CreateGifCardModal.svelte'; 2 3 import EditingGifCard from './EditingGifCard.svelte'; 3 4 import GifCard from './GifCard.svelte'; 4 5 import GifCardSettings from './GifCardSettings.svelte'; ··· 7 8 type: 'gif', 8 9 contentComponent: GifCard, 9 10 editingContentComponent: EditingGifCard, 11 + creationModalComponent: CreateGifCardModal, 10 12 createNew: (card) => { 11 13 card.cardType = 'gif'; 12 14 card.cardData = { ··· 25 27 minW: 1, 26 28 minH: 1, 27 29 onUrlHandler: (url, item) => { 28 - const gifUrlPatterns = [/\.gif(\?.*)?$/i, /giphy\.com\/gifs\//i, /media\.giphy\.com/i]; 30 + // Match Giphy page URLs: https://giphy.com/gifs/name-ID or https://giphy.com/gifs/ID 31 + const pageMatch = url.match(/giphy\.com\/gifs\/(?:.*-)?([a-zA-Z0-9]+)(?:\?|$)/); 32 + if (pageMatch) { 33 + item.cardData.url = `https://media.giphy.com/media/${pageMatch[1]}/giphy.mp4`; 34 + return item; 35 + } 29 36 30 - if (gifUrlPatterns.some((pattern) => pattern.test(url))) { 31 - // Convert Giphy page URLs to direct media URLs 32 - const giphyMatch = url.match(/giphy\.com\/gifs\/(?:.*-)?([a-zA-Z0-9]+)(?:\?|$)/); 33 - if (giphyMatch) { 34 - item.cardData.url = `https://media.giphy.com/media/${giphyMatch[1]}/giphy.gif`; 35 - } else { 36 - item.cardData.url = url; 37 - } 37 + // Match Giphy media URLs: https://media.giphy.com/media/ID/giphy.gif or .mp4 38 + const mediaMatch = url.match(/media\.giphy\.com\/media\/([a-zA-Z0-9]+)\//); 39 + if (mediaMatch) { 40 + item.cardData.url = `https://media.giphy.com/media/${mediaMatch[1]}/giphy.mp4`; 38 41 return item; 39 42 } 43 + 40 44 return null; 41 45 }, 42 - urlHandlerPriority: 5 46 + urlHandlerPriority: 5, 47 + name: 'GIF' 43 48 } as CardDefinition & { type: 'gif' };
+6 -4
src/lib/cards/LivestreamCard/index.ts
··· 32 32 | undefined; 33 33 const values = Object.values(records); 34 34 if (values?.length > 0) { 35 - const latest = JSON.parse(JSON.stringify(values[0])); 35 + const latest = JSON.parse(JSON.stringify(values?.[0])); 36 36 37 37 latestLivestream = { 38 38 createdAt: latest.value.createdAt, 39 - title: latest.value.title as string, 40 - thumb: getImageBlobUrl({ link: latest.value.thumb?.ref.$link, did }), 41 - href: latest.value.canonicalUrl || latest.value.url, 39 + title: latest.value?.title as string, 40 + thumb: latest.value?.thumb?.ref?.$link 41 + ? getImageBlobUrl({ link: latest.value.thumb.ref.$link, did }) 42 + : undefined, 43 + href: latest.value?.canonicalUrl || latest.value.url, 42 44 online: undefined 43 45 }; 44 46 }
-37
src/routes/api/giphy/+server.ts
··· 1 - import { env } from '$env/dynamic/private'; 2 - import { json } from '@sveltejs/kit'; 3 - 4 - export async function GET({ url }) { 5 - const q = url.searchParams.get('q'); 6 - const offset = url.searchParams.get('offset') || '0'; 7 - const limit = url.searchParams.get('limit') || '24'; 8 - 9 - if (!q) { 10 - return json({ error: 'No search query provided' }, { status: 400 }); 11 - } 12 - 13 - const apiKey = env.GIPHY_API_TOKEN; 14 - if (!apiKey) { 15 - return json({ error: 'Giphy API not configured' }, { status: 500 }); 16 - } 17 - 18 - const giphyUrl = new URL('https://api.giphy.com/v1/gifs/search'); 19 - giphyUrl.searchParams.set('api_key', apiKey); 20 - giphyUrl.searchParams.set('q', q); 21 - giphyUrl.searchParams.set('limit', limit); 22 - giphyUrl.searchParams.set('offset', offset); 23 - giphyUrl.searchParams.set('rating', 'g'); 24 - giphyUrl.searchParams.set('lang', 'en'); 25 - 26 - try { 27 - const response = await fetch(giphyUrl.toString()); 28 - if (!response.ok) { 29 - throw new Error(`Giphy API error: ${response.status}`); 30 - } 31 - const data = await response.json(); 32 - return json(data); 33 - } catch (error) { 34 - console.error('Error fetching from Giphy:', error); 35 - return json({ error: 'Failed to fetch from Giphy' }, { status: 500 }); 36 - } 37 - }