Шифрование, GIF, хранилище, админка

This commit is contained in:
Халимов Рустам
2026-03-16 14:49:31 +03:00
parent 336f9ea559
commit 6d018e41ea
44 changed files with 1876 additions and 162 deletions
+58 -27
View File
@@ -8,14 +8,11 @@ import { useAuthStore } from '../stores/authStore';
interface KlipyGif {
id: string;
images: {
original: { url: string };
fixed_height_small: { url: string };
};
file: {
sd?: { gif?: { url: string }; webp?: { url: string } };
hd?: { gif?: { url: string }; webp?: { url: string } };
};
files?: any;
file?: any;
media_formats?: any;
media?: any;
images?: any;
title?: string;
}
@@ -25,14 +22,18 @@ interface EmojiPickerProps {
onClose: () => void;
}
const getKlipyKey = () => import.meta.env.VITE_KLIPY_API_KEY || '';
const getCustomerId = () => {
const user = useAuthStore.getState().user;
return user?.id || 'anonymous';
const getKlipyKey = (config: any) => {
return config?.klipyApiKey || import.meta.env.VITE_KLIPY_API_KEY || '';
};
const getCustomerId = (config: any, user: any) => {
return config?.klipyCustomerId || user?.id || 'anonymous';
};
export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPickerProps) {
const { lang, t } = useLang();
const { config, user } = useAuthStore();
const klipyApiKey = getKlipyKey(config);
const klipyCustomerId = getCustomerId(config, user);
const [tab, setTab] = useState<'emoji' | 'gif'>('emoji');
const [gifQuery, setGifQuery] = useState('');
const [gifs, setGifs] = useState<KlipyGif[]>([]);
@@ -40,10 +41,12 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
const [trendingGifs, setTrendingGifs] = useState<KlipyGif[]>([]);
const gifSearchRef = useRef<HTMLInputElement>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const initialFetchDone = useRef(false);
// Helper to safely extract GIF array from various possible Klipy API responses
const extractGifs = (d: any): KlipyGif[] => {
if (!d) return [];
if (d.data && Array.isArray(d.data.data)) return d.data.data;
if (Array.isArray(d)) return d;
if (Array.isArray(d.data)) return d.data;
if (Array.isArray(d.result)) return d.result;
@@ -54,10 +57,17 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
// Load trending GIFs (Klipy)
useEffect(() => {
if (tab === 'gif' && getKlipyKey() && trendingGifs.length === 0) {
if (tab === 'gif' && klipyApiKey && !initialFetchDone.current) {
initialFetchDone.current = true;
setGifLoading(true);
fetch(`https://api.klipy.com/api/v1/${getKlipyKey()}/gifs/trending?customer_id=${getCustomerId()}&per_page=30`)
.then(r => r.json())
fetch(`https://api.klipy.com/api/v1/${klipyApiKey}/gifs/trending?page=1&per_page=30&customer_id=${klipyCustomerId}`)
.then(async (r) => {
if (!r.ok) {
const text = await r.text();
throw new Error(`Klipy error: ${r.status} ${text.substring(0, 100)}`);
}
return r.json();
})
.then(d => {
setTrendingGifs(extractGifs(d));
setGifLoading(false);
@@ -68,13 +78,19 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
setGifLoading(false);
});
}
}, [tab, trendingGifs.length]);
}, [tab, klipyApiKey, klipyCustomerId]);
const searchGifs = useCallback((q: string) => {
if (!getKlipyKey() || !q.trim()) { setGifs([]); return; }
if (!klipyApiKey || !q.trim()) { setGifs([]); return; }
setGifLoading(true);
fetch(`https://api.klipy.com/api/v1/${getKlipyKey()}/gifs/search?customer_id=${getCustomerId()}&q=${encodeURIComponent(q)}&per_page=30`)
.then(r => r.json())
fetch(`https://api.klipy.com/api/v1/${klipyApiKey}/gifs/search?page=1&per_page=30&q=${encodeURIComponent(q)}&customer_id=${klipyCustomerId}`)
.then(async (r) => {
if (!r.ok) {
const text = await r.text();
throw new Error(`Klipy error: ${r.status} ${text.substring(0, 100)}`);
}
return r.json();
})
.then(d => {
setGifs(extractGifs(d));
setGifLoading(false);
@@ -84,7 +100,7 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
setGifs([]);
setGifLoading(false);
});
}, []);
}, [klipyApiKey, klipyCustomerId]);
const handleGifSearch = (q: string) => {
setGifQuery(q);
@@ -92,9 +108,23 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
debounceRef.current = setTimeout(() => searchGifs(q), 400);
};
const getGifUrl = (gif: any): string => {
return gif.files?.hd?.gif?.url || gif.files?.sd?.gif?.url
|| gif.file?.hd?.gif?.url || gif.file?.sd?.gif?.url
|| gif.media_formats?.gif?.url || gif.media?.[0]?.gif?.url
|| gif.images?.original?.url || '';
};
const getGifPreview = (gif: any, fullUrl: string): string => {
return gif.files?.sd?.webp?.url || gif.files?.sd?.gif?.url
|| gif.file?.sd?.webp?.url || gif.file?.sd?.gif?.url
|| gif.media_formats?.tinygif?.url || gif.media?.[0]?.tinygif?.url
|| gif.images?.fixed_height_small?.url || fullUrl;
};
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;
const url = getGifUrl(gif);
const preview = getGifPreview(gif, url);
if (onSelectGif && url) {
onSelectGif(url, preview);
}
@@ -132,6 +162,7 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
className="fixed z-[9991] rounded-2xl shadow-2xl border border-white/10"
style={{
width: pickerWidth,
height: tab === 'gif' ? 435 : undefined,
bottom: pos ? `${window.innerHeight - pos.top}px` : undefined,
left: pos ? pos.left : undefined,
background: 'rgb(17, 17, 19)',
@@ -146,7 +177,7 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
>
EMOJI
</button>
{(getKlipyKey() || onSelectGif) && (
{config?.enableKlipy && (klipyApiKey || 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'}`}
@@ -176,9 +207,9 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
)}
{/* GIF tab */}
{tab === 'gif' && (
{config?.enableKlipy && tab === 'gif' && (
<div className="flex flex-col h-[calc(100%-41px)]">
{!getKlipyKey() ? (
{!klipyApiKey ? (
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
<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>
@@ -214,7 +245,7 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic
) : displayGifs.length === 0 ? (
<p className="text-center text-xs text-zinc-500 py-10">{gifQuery ? t('nothingFound') : ''}</p>
) : (
<div className="columns-2 gap-1.5">
<div className="columns-4 gap-1.5">
{displayGifs.map((gif) => (
<button
key={gif.id}
@@ -222,7 +253,7 @@ 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.file?.sd?.webp?.url || gif.file?.sd?.gif?.url || gif.file?.hd?.gif?.url}
src={getGifPreview(gif, getGifUrl(gif))}
alt={gif.title || 'GIF'}
className="w-full h-auto rounded-lg"
loading="lazy"