Доработки профиля

This commit is contained in:
Халимов Рустам
2026-03-14 01:59:06 +03:00
parent 010b96d362
commit 15344f8636
69 changed files with 1625 additions and 561 deletions
+61 -27
View File
@@ -4,14 +4,19 @@ import Picker from '@emoji-mart/react';
import data from '@emoji-mart/data';
import { Search, TrendingUp, Loader2 } from 'lucide-react';
import { useLang } from '../lib/i18n';
import { useAuthStore } from '../stores/authStore';
interface TenorGif {
interface KlipyGif {
id: string;
media_formats?: {
gif?: { url: string };
tinygif?: { url: string };
images: {
original: { url: string };
fixed_height_small: { url: string };
};
content_description?: string;
file: {
sd?: { gif?: { url: string }; webp?: { url: string } };
hd?: { gif?: { url: string }; webp?: { url: string } };
};
title?: string;
}
interface EmojiPickerProps {
@@ -20,36 +25,65 @@ interface EmojiPickerProps {
onClose: () => void;
}
const getTenorKey = () => localStorage.getItem('vortex_tenor_key') || '';
const getKlipyKey = () => import.meta.env.VITE_KLIPY_API_KEY || '';
const getCustomerId = () => {
const user = useAuthStore.getState().user;
return user?.id || 'anonymous';
};
export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPickerProps) {
const { lang, t } = useLang();
const [tab, setTab] = useState<'emoji' | 'gif'>('emoji');
const [gifQuery, setGifQuery] = useState('');
const [gifs, setGifs] = useState<TenorGif[]>([]);
const [gifs, setGifs] = useState<KlipyGif[]>([]);
const [gifLoading, setGifLoading] = useState(false);
const [trendingGifs, setTrendingGifs] = useState<TenorGif[]>([]);
const [trendingGifs, setTrendingGifs] = useState<KlipyGif[]>([]);
const gifSearchRef = useRef<HTMLInputElement>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
// Load trending GIFs
// Helper to safely extract GIF array from various possible Klipy API responses
const extractGifs = (d: any): KlipyGif[] => {
if (!d) return [];
if (Array.isArray(d)) return d;
if (Array.isArray(d.data)) return d.data;
if (Array.isArray(d.result)) return d.result;
if (d.result && Array.isArray(d.result.data)) return d.result.data;
if (Array.isArray(d.gifs)) return d.gifs;
return [];
};
// Load trending GIFs (Klipy)
useEffect(() => {
if (tab === 'gif' && getTenorKey() && trendingGifs.length === 0) {
if (tab === 'gif' && getKlipyKey() && trendingGifs.length === 0) {
setGifLoading(true);
fetch(`https://tenor.googleapis.com/v2/featured?key=${getTenorKey()}&limit=30&media_filter=gif,tinygif`)
fetch(`https://api.klipy.com/api/v1/${getKlipyKey()}/gifs/trending?customer_id=${getCustomerId()}&per_page=30`)
.then(r => r.json())
.then(d => { setTrendingGifs(d.results || []); setGifLoading(false); })
.catch(() => setGifLoading(false));
.then(d => {
setTrendingGifs(extractGifs(d));
setGifLoading(false);
})
.catch((e) => {
console.error('Klipy trending error:', e);
setTrendingGifs([]);
setGifLoading(false);
});
}
}, [tab]);
}, [tab, trendingGifs.length]);
const searchGifs = useCallback((q: string) => {
if (!getTenorKey() || !q.trim()) { setGifs([]); return; }
if (!getKlipyKey() || !q.trim()) { setGifs([]); return; }
setGifLoading(true);
fetch(`https://tenor.googleapis.com/v2/search?key=${getTenorKey()}&q=${encodeURIComponent(q)}&limit=30&media_filter=gif,tinygif`)
fetch(`https://api.klipy.com/api/v1/${getKlipyKey()}/gifs/search?customer_id=${getCustomerId()}&q=${encodeURIComponent(q)}&per_page=30`)
.then(r => r.json())
.then(d => { setGifs(d.results || []); setGifLoading(false); })
.catch(() => setGifLoading(false));
.then(d => {
setGifs(extractGifs(d));
setGifLoading(false);
})
.catch((e) => {
console.error('Klipy search error:', e);
setGifs([]);
setGifLoading(false);
});
}, []);
const handleGifSearch = (q: string) => {
@@ -58,9 +92,9 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
debounceRef.current = setTimeout(() => searchGifs(q), 400);
};
const pickGif = (gif: TenorGif) => {
const url = gif.media_formats?.gif?.url || gif.media_formats?.tinygif?.url || '';
const preview = gif.media_formats?.tinygif?.url || url;
const pickGif = (gif: KlipyGif) => {
const url = gif.file?.hd?.gif?.url || gif.file?.sd?.gif?.url || '';
const preview = gif.file?.sd?.webp?.url || gif.file?.sd?.gif?.url || url;
if (onSelectGif && url) {
onSelectGif(url, preview);
}
@@ -112,7 +146,7 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
>
EMOJI
</button>
{(getTenorKey() || onSelectGif) && (
{(getKlipyKey() || onSelectGif) && (
<button
onClick={() => { setTab('gif'); setTimeout(() => gifSearchRef.current?.focus(), 100); }}
className={`flex-1 py-2.5 text-xs font-semibold tracking-wide transition-colors ${tab === 'gif' ? 'text-white border-b-2 border-accent' : 'text-zinc-500 hover:text-zinc-300'}`}
@@ -144,12 +178,12 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
{/* GIF tab */}
{tab === 'gif' && (
<div className="flex flex-col h-[calc(100%-41px)]">
{!getTenorKey() ? (
{!getKlipyKey() ? (
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
<p className="text-sm text-zinc-400 mb-2">{t('tenorKeyRequired')}</p>
<p className="text-sm text-zinc-400 mb-2">Klipy API Key required</p>
<p className="text-xs text-zinc-500 mb-3">{t('openConsoleRun')}</p>
<code className="text-xs bg-black/30 px-3 py-1.5 rounded-lg text-vortex-400">
localStorage.setItem('vortex_tenor_key', 'YOUR_KEY')
VITE_KLIPY_API_KEY in .env
</code>
</div>
) : (
@@ -188,8 +222,8 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
className="w-full mb-1.5 rounded-lg overflow-hidden hover:opacity-80 transition-opacity block"
>
<img
src={gif.media_formats?.tinygif?.url || gif.media_formats?.gif?.url}
alt={gif.content_description || 'GIF'}
src={gif.file?.sd?.webp?.url || gif.file?.sd?.gif?.url || gif.file?.hd?.gif?.url}
alt={gif.title || 'GIF'}
className="w-full h-auto rounded-lg"
loading="lazy"
/>