your personal website on atproto - mirror
blento.app
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>