You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
7.9 KiB
351 lines
7.9 KiB
<script lang="ts"> |
|
import { onMount } from 'svelte'; |
|
import { fetchGifs, searchGifs, type GifMetadata } from '../../services/nostr/gif-service.js'; |
|
|
|
interface Props { |
|
open: boolean; |
|
onSelect: (gifUrl: string) => void; |
|
onClose: () => void; |
|
} |
|
|
|
let { open, onSelect, onClose }: Props = $props(); |
|
|
|
let gifs = $state<GifMetadata[]>([]); |
|
let loading = $state(false); |
|
let searchQuery = $state(''); |
|
let searchInput: HTMLInputElement | null = $state(null); |
|
let selectedGif: GifMetadata | null = $state(null); |
|
|
|
// Debounce search |
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null; |
|
|
|
async function loadGifs(query?: string) { |
|
loading = true; |
|
try { |
|
if (query && query.trim()) { |
|
gifs = await searchGifs(query.trim(), 50); |
|
} else { |
|
gifs = await fetchGifs(undefined, 50); |
|
} |
|
} catch (error) { |
|
console.error('[GifPicker] Error loading GIFs:', error); |
|
gifs = []; |
|
} finally { |
|
loading = false; |
|
} |
|
} |
|
|
|
function handleSearchInput(e: Event) { |
|
const target = e.target as HTMLInputElement; |
|
searchQuery = target.value; |
|
|
|
// Debounce search |
|
if (searchTimeout) { |
|
clearTimeout(searchTimeout); |
|
} |
|
searchTimeout = setTimeout(() => { |
|
loadGifs(searchQuery); |
|
}, 300); |
|
} |
|
|
|
function handleGifSelect(gif: GifMetadata) { |
|
selectedGif = gif; |
|
// Use fallback URL if available, otherwise use main URL |
|
const url = gif.fallbackUrl || gif.url; |
|
onSelect(url); |
|
onClose(); |
|
} |
|
|
|
// Load GIFs when panel opens |
|
$effect(() => { |
|
if (open) { |
|
loadGifs(); |
|
// Focus search input after a short delay |
|
setTimeout(() => { |
|
if (searchInput) { |
|
searchInput.focus(); |
|
} |
|
}, 100); |
|
} else { |
|
// Reset when closed |
|
searchQuery = ''; |
|
selectedGif = null; |
|
} |
|
}); |
|
|
|
// Handle keyboard navigation |
|
function handleKeyDown(e: KeyboardEvent) { |
|
if (e.key === 'Escape') { |
|
onClose(); |
|
} |
|
} |
|
</script> |
|
|
|
{#if open} |
|
<div |
|
class="drawer-backdrop" |
|
onclick={onClose} |
|
onkeydown={handleKeyDown} |
|
role="button" |
|
tabindex="0" |
|
aria-label="Close GIF picker" |
|
></div> |
|
<div class="gif-picker drawer-left" onkeydown={handleKeyDown} role="dialog" aria-label="GIF picker" tabindex="-1"> |
|
<div class="drawer-header"> |
|
<h3 class="drawer-title">Choose GIF</h3> |
|
<button |
|
onclick={onClose} |
|
class="drawer-close" |
|
aria-label="Close GIF picker" |
|
title="Close" |
|
> |
|
× |
|
</button> |
|
</div> |
|
|
|
<div class="gif-search-container"> |
|
<input |
|
bind:this={searchInput} |
|
type="text" |
|
placeholder="Search GIFs..." |
|
value={searchQuery} |
|
oninput={handleSearchInput} |
|
class="gif-search-input" |
|
aria-label="Search GIFs" |
|
/> |
|
</div> |
|
|
|
<div class="gif-picker-content"> |
|
{#if loading} |
|
<div class="gif-loading">Loading GIFs...</div> |
|
{:else if gifs.length === 0} |
|
<div class="gif-empty"> |
|
{#if searchQuery} |
|
No GIFs found for "{searchQuery}" |
|
{:else} |
|
No GIFs available |
|
{/if} |
|
</div> |
|
{:else} |
|
<div class="gif-grid"> |
|
{#each gifs as gif} |
|
<button |
|
onclick={() => handleGifSelect(gif)} |
|
class="gif-item {selectedGif?.eventId === gif.eventId ? 'selected' : ''}" |
|
title="Click to insert GIF" |
|
> |
|
<img |
|
src={gif.url} |
|
alt="GIF" |
|
loading="lazy" |
|
class="gif-thumbnail" |
|
/> |
|
</button> |
|
{/each} |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
{/if} |
|
|
|
<style> |
|
.drawer-backdrop { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: rgba(0, 0, 0, 0.5); |
|
z-index: 999; |
|
animation: fadeIn 0.3s ease-out; |
|
} |
|
|
|
@keyframes fadeIn { |
|
from { |
|
opacity: 0; |
|
} |
|
to { |
|
opacity: 1; |
|
} |
|
} |
|
|
|
.gif-picker { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
bottom: 0; |
|
width: min(500px, 85vw); |
|
max-width: 500px; |
|
background: var(--fog-post, #ffffff); |
|
border-right: 2px solid var(--fog-border, #cbd5e1); |
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2); |
|
padding: 0; |
|
z-index: 1000; |
|
display: flex; |
|
flex-direction: column; |
|
overflow: hidden; |
|
animation: slideInLeft 0.3s ease-out; |
|
transform: translateX(0); |
|
} |
|
|
|
:global(.dark) .gif-picker { |
|
background: var(--fog-dark-post, #1f2937); |
|
border-right-color: var(--fog-dark-border, #475569); |
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
.drawer-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 1rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .drawer-header { |
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.drawer-title { |
|
margin: 0; |
|
font-size: 1.125rem; |
|
font-weight: 600; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .drawer-title { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.drawer-close { |
|
background: transparent; |
|
border: none; |
|
font-size: 1.5rem; |
|
line-height: 1; |
|
cursor: pointer; |
|
color: var(--fog-text-light, #9ca3af); |
|
padding: 0.25rem 0.5rem; |
|
border-radius: 0.25rem; |
|
transition: all 0.2s; |
|
} |
|
|
|
.drawer-close:hover { |
|
background: var(--fog-highlight, #f3f4f6); |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .drawer-close { |
|
color: var(--fog-dark-text-light, #6b7280); |
|
} |
|
|
|
:global(.dark) .drawer-close:hover { |
|
background: var(--fog-dark-highlight, #374151); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
@keyframes slideInLeft { |
|
from { |
|
transform: translateX(-100%); |
|
} |
|
to { |
|
transform: translateX(0); |
|
} |
|
} |
|
|
|
.gif-search-container { |
|
padding: 1rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .gif-search-container { |
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.gif-search-input { |
|
width: 100%; |
|
padding: 0.5rem 0.75rem; |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.375rem; |
|
background: var(--fog-surface, #f8fafc); |
|
color: var(--fog-text, #1f2937); |
|
font-size: 0.875rem; |
|
transition: all 0.2s; |
|
} |
|
|
|
.gif-search-input:focus { |
|
outline: none; |
|
border-color: var(--fog-accent, #64748b); |
|
background: var(--fog-post, #ffffff); |
|
} |
|
|
|
:global(.dark) .gif-search-input { |
|
background: var(--fog-dark-surface, #1e293b); |
|
border-color: var(--fog-dark-border, #374151); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
:global(.dark) .gif-search-input:focus { |
|
background: var(--fog-dark-post, #1f2937); |
|
border-color: var(--fog-dark-accent, #64748b); |
|
} |
|
|
|
.gif-picker-content { |
|
overflow-y: auto; |
|
overflow-x: hidden; |
|
flex: 1; |
|
padding: 1rem; |
|
} |
|
|
|
.gif-loading, |
|
.gif-empty { |
|
text-align: center; |
|
padding: 2rem; |
|
color: var(--fog-text-light, #9ca3af); |
|
font-size: 0.875rem; |
|
} |
|
|
|
:global(.dark) .gif-loading, |
|
:global(.dark) .gif-empty { |
|
color: var(--fog-dark-text-light, #6b7280); |
|
} |
|
|
|
.gif-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); |
|
gap: 0.5rem; |
|
} |
|
|
|
.gif-item { |
|
position: relative; |
|
padding: 0; |
|
border: 2px solid transparent; |
|
border-radius: 0.375rem; |
|
background: transparent; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
overflow: hidden; |
|
aspect-ratio: 1; |
|
} |
|
|
|
.gif-item:hover { |
|
border-color: var(--fog-accent, #64748b); |
|
transform: scale(1.05); |
|
} |
|
|
|
.gif-item.selected { |
|
border-color: var(--fog-accent, #64748b); |
|
box-shadow: 0 0 0 2px var(--fog-accent, #64748b); |
|
} |
|
|
|
:global(.dark) .gif-item:hover, |
|
:global(.dark) .gif-item.selected { |
|
border-color: var(--fog-dark-accent, #64748b); |
|
} |
|
|
|
.gif-thumbnail { |
|
width: 100%; |
|
height: 100%; |
|
object-fit: cover; |
|
display: block; |
|
} |
|
</style>
|
|
|