Files
CRM_ENVR/src/components/chats/media-picker.tsx
T

303 lines
13 KiB
TypeScript

"use client"
import { useState, useEffect, useRef, useCallback } from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Search, Image, Sticker, X, Loader2, TrendingUp } from "lucide-react"
import data from "@emoji-mart/data"
import Picker from "@emoji-mart/react"
import { getStickerPacks, type StickerPack } from "@/data/stickers"
type Tab = "emoji" | "gif" | "sticker"
interface MediaPickerProps {
onEmojiSelect: (emoji: string) => void
onMediaSelect: (content: string) => void
onClose: () => void
theme?: string
}
const RECENT_GIFS_KEY = "recent-gifs"
const RECENT_STICKERS_KEY = "recent-stickers"
const MAX_RECENT = 12
function getRecent(key: string): string[] {
if (typeof window === "undefined") return []
try { return JSON.parse(localStorage.getItem(key) || "[]") } catch { return [] }
}
function addRecent(key: string, id: string) {
if (typeof window === "undefined") return
const items = [id, ...getRecent(key).filter((x) => x !== id)].slice(0, MAX_RECENT)
localStorage.setItem(key, JSON.stringify(items))
}
export default function MediaPicker({ onEmojiSelect, onMediaSelect, onClose, theme }: MediaPickerProps) {
const [tab, setTab] = useState<Tab>("emoji")
const [gifQuery, setGifQuery] = useState("")
const [gifResults, setGifResults] = useState<any[]>([])
const [gifLoading, setGifLoading] = useState(false)
const [gifError, setGifError] = useState("")
const [gifUnavailable, setGifUnavailable] = useState(false)
const [gifNext, setGifNext] = useState("")
const gifCache = useRef<Map<string, { results: any[]; next: string }>>(new Map())
const [recentGifs, setRecentGifs] = useState<string[]>([])
const [recentStickers, setRecentStickers] = useState<string[]>([])
const [stickerPacks] = useState<StickerPack[]>(getStickerPacks)
const [activePack, setActivePack] = useState(stickerPacks[0]?.id || "")
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const pickerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) onClose()
}
document.addEventListener("mousedown", handleClick)
return () => document.removeEventListener("mousedown", handleClick)
}, [onClose])
useEffect(() => { setRecentGifs(getRecent(RECENT_GIFS_KEY)) }, [])
useEffect(() => { setRecentStickers(getRecent(RECENT_STICKERS_KEY)) }, [])
const fetchGifs = useCallback(async (query: string, pos = "") => {
const cacheKey = `${query}::${pos}`
const cached = gifCache.current.get(cacheKey)
if (cached && !pos) {
setGifResults(cached.results)
setGifNext(cached.next)
return
}
setGifLoading(true)
setGifError("")
try {
const params = new URLSearchParams({ limit: "20", pos })
if (query) params.set("q", query)
const res = await fetch(`/api/gifs?${params}`)
const data = await res.json()
if (data.noKey) {
setGifUnavailable(true)
setGifResults([])
} else if (data.results) {
setGifResults((prev) => (pos ? [...prev, ...data.results] : data.results))
setGifNext(data.next || "")
setGifUnavailable(false)
if (!pos) gifCache.current.set(cacheKey, { results: data.results, next: data.next || "" })
} else {
setGifError("No results found")
}
} catch {
setGifError("Could not load GIFs. Check your connection.")
}
setGifLoading(false)
}, [])
useEffect(() => {
if (tab !== "gif") return
if (searchTimer.current) clearTimeout(searchTimer.current)
if (gifQuery.trim()) {
searchTimer.current = setTimeout(() => fetchGifs(gifQuery.trim()), 400)
} else {
fetchGifs("")
}
return () => { if (searchTimer.current) clearTimeout(searchTimer.current) }
}, [tab, gifQuery, fetchGifs])
const handleGifSelect = (gif: any) => {
const content = JSON.stringify({ gif: gif.url, w: gif.width, h: gif.height })
addRecent(RECENT_GIFS_KEY, gif.id)
setRecentGifs(getRecent(RECENT_GIFS_KEY))
onMediaSelect(content)
}
const handleStickerSelect = (sticker: any) => {
const content = sticker.svg
? JSON.stringify({ sticker: sticker.svg, w: 200, h: 200, id: sticker.id })
: JSON.stringify({ sticker: sticker.emoji, w: 120, h: 120, id: sticker.id })
addRecent(RECENT_STICKERS_KEY, sticker.id)
setRecentStickers(getRecent(RECENT_STICKERS_KEY))
onMediaSelect(content)
}
const activeStickers = stickerPacks.find((p) => p.id === activePack)?.stickers || []
const renderGifGrid = (items: any[], emptyMsg: string) => {
if (items.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground text-sm gap-2">
<Image className="h-8 w-8 opacity-40" />
<span>{emptyMsg}</span>
</div>
)
}
return (
<div className="grid grid-cols-2 gap-1.5">
{items.map((gif: any) => (
<button
key={gif.id}
type="button"
onClick={() => handleGifSelect(gif)}
className="rounded-lg overflow-hidden bg-muted/50 hover:ring-2 hover:ring-primary/50 transition-all aspect-video"
>
<img src={gif.previewUrl || gif.url} alt={gif.title || "GIF"} className="w-full h-full object-cover" loading="lazy" />
</button>
))}
</div>
)
}
return (
<div ref={pickerRef} className="w-[360px] rounded-xl border bg-popover shadow-xl overflow-hidden">
{/* Tabs */}
<div className="flex border-b">
<button type="button" onClick={() => setTab("emoji")} className={cn("flex-1 py-2.5 text-sm font-medium transition-colors relative", tab === "emoji" ? "text-foreground" : "text-muted-foreground hover:text-foreground")}>
Emoji
{tab === "emoji" && <div className="absolute bottom-0 left-4 right-4 h-0.5 bg-primary rounded-full" />}
</button>
<button type="button" onClick={() => setTab("gif")} className={cn("flex-1 py-2.5 text-sm font-medium transition-colors relative", tab === "gif" ? "text-foreground" : "text-muted-foreground hover:text-foreground")}>
GIFs
{tab === "gif" && <div className="absolute bottom-0 left-4 right-4 h-0.5 bg-primary rounded-full" />}
</button>
<button type="button" onClick={() => setTab("sticker")} className={cn("flex-1 py-2.5 text-sm font-medium transition-colors relative", tab === "sticker" ? "text-foreground" : "text-muted-foreground hover:text-foreground")}>
Stickers
{tab === "sticker" && <div className="absolute bottom-0 left-4 right-4 h-0.5 bg-primary rounded-full" />}
</button>
</div>
{/* Emoji Tab */}
{tab === "emoji" && (
<div className="max-h-[380px] overflow-hidden">
<Picker
data={data}
onEmojiSelect={(emoji: any) => { onEmojiSelect(emoji.native); onClose() }}
theme={theme === "dark" ? "dark" : "light"}
previewPosition="none"
skinTonePosition="none"
set="native"
emojiSize={32}
maxFrequentRows={2}
perLine={8}
/>
</div>
)}
{/* GIF Tab */}
{tab === "gif" && (
<div className="flex flex-col h-[380px]">
<div className="p-2 border-b">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
value={gifQuery}
onChange={(e) => setGifQuery(e.target.value)}
placeholder="Search GIFs..."
className="h-8 pl-8 text-sm"
/>
{gifQuery && (
<button type="button" onClick={() => setGifQuery("")} className="absolute right-2 top-1/2 -translate-y-1/2">
<X className="h-3.5 w-3.5 text-muted-foreground" />
</button>
)}
</div>
</div>
<div className="flex-1 overflow-y-auto p-2">
{gifLoading && gifResults.length === 0 ? (
<div className="flex items-center justify-center h-48">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : gifUnavailable ? (
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground text-sm gap-2">
<Image className="h-8 w-8 opacity-40" />
<span>GIFs are temporarily unavailable.</span>
</div>
) : gifError ? (
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground text-sm gap-2">
<TrendingUp className="h-8 w-8 opacity-40" />
<span>{gifError}</span>
<button type="button" onClick={() => fetchGifs(gifQuery)} className="text-xs text-primary hover:underline">Retry</button>
</div>
) : gifQuery.trim() ? (
renderGifGrid(gifResults, "No GIFs found")
) : recentGifs.length > 0 && gifResults.length === 0 ? (
<>
<div className="text-xs font-medium text-muted-foreground px-1 pb-2">Recent GIFs</div>
{renderGifGrid(
recentGifs.map((id) => gifResults.find((g: any) => g.id === id)).filter(Boolean),
"No recent GIFs"
)}
</>
) : (
<>
<div className="text-xs font-medium text-muted-foreground px-1 pb-2">Trending</div>
{renderGifGrid(gifResults, "No trending GIFs available")}
</>
)}
{gifNext && !gifLoading && (
<div className="flex justify-center pt-2 pb-1">
<Button type="button" variant="ghost" size="sm" className="text-xs h-7" onClick={() => fetchGifs(gifQuery, gifNext)}>
Load more
</Button>
</div>
)}
</div>
</div>
)}
{/* Stickers Tab */}
{tab === "sticker" && (
<div className="flex flex-col h-[380px]">
<div className="flex gap-1 p-2 border-b overflow-x-auto shrink-0">
{stickerPacks.map((pack) => (
<button
key={pack.id}
type="button"
onClick={() => setActivePack(pack.id)}
className={cn(
"px-3 py-1.5 text-xs rounded-md whitespace-nowrap transition-colors",
activePack === pack.id ? "bg-primary text-primary-foreground" : "bg-muted hover:bg-muted/80 text-muted-foreground",
)}
>
{pack.name}
</button>
))}
</div>
<div className="flex-1 overflow-y-auto p-2">
<div className="grid grid-cols-3 gap-2">
{activeStickers.map((sticker) => (
<button
key={sticker.id}
type="button"
onClick={() => handleStickerSelect(sticker)}
className="rounded-xl bg-muted/30 hover:bg-muted/60 transition-colors flex items-center justify-center p-2 aspect-square"
>
{sticker.svg ? (
<div className="w-full h-full flex items-center justify-center" dangerouslySetInnerHTML={{ __html: sticker.svg.replace(/<svg /, '<svg style="width:100%;height:100%" ') }} />
) : (
<span className="text-5xl">{sticker.emoji}</span>
)}
</button>
))}
</div>
{recentStickers.length > 0 && (
<>
<div className="text-xs font-medium text-muted-foreground pt-4 pb-2">Recently Used</div>
<div className="grid grid-cols-3 gap-2">
{recentStickers.map((id) => {
const s = stickerPacks.flatMap((p) => p.stickers).find((x) => x.id === id)
if (!s) return null
return (
<button key={s.id} type="button" onClick={() => handleStickerSelect(s)} className="rounded-xl bg-muted/30 hover:bg-muted/60 transition-colors flex items-center justify-center p-2 aspect-square opacity-70 hover:opacity-100">
{s.svg ? <div className="w-full h-full flex items-center justify-center" dangerouslySetInnerHTML={{ __html: s.svg.replace(/<svg /, '<svg style="width:100%;height:100%" ') }} /> : <span className="text-5xl">{s.emoji}</span>}
</button>
)
})}
</div>
</>
)}
</div>
</div>
)}
</div>
)
}