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

<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>