Files
cats/client/src/components/CatModal.tsx

162 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { useCat, useDeleteCat } from '@/hooks/useCats';
import { useLikeStatus, useLikeCat, useUnlikeCat } from '@/hooks/useLikes';
import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/components/Toast';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { X, Heart, Trash2 } from 'lucide-react';
interface CatModalProps {
catId: number;
onClose: () => void;
}
export default function CatModal({ catId, onClose }: CatModalProps) {
const { user, updateUser } = useAuth();
const { data, isLoading } = useCat(catId);
const { data: likeData, refetch: refetchLike } = useLikeStatus(catId);
const likeMutation = useLikeCat(catId);
const unlikeMutation = useUnlikeCat(catId);
const deleteMutation = useDeleteCat();
const { toast } = useToast();
const [liked, setLiked] = useState(false);
const [likeCount, setLikeCount] = useState(0);
const [imageLoaded, setImageLoaded] = useState(false);
useEffect(() => { if (likeData) setLiked(likeData.liked); }, [likeData]);
useEffect(() => { if (data?.cat) setLikeCount(data.cat.likes_count); }, [data]);
useEffect(() => { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; }, []);
useEffect(() => {
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [onClose]);
const cat = data?.cat;
const isOwner = cat && user && cat.user_id === user.id;
const toggleLike = async () => {
if (isOwner || !user || !cat) return;
if (liked) {
setLiked(false); setLikeCount((c) => c - 1);
try {
const res = await unlikeMutation.mutateAsync();
if (res.ownerPoints !== undefined && cat.user_id === user.id) updateUser({ ...user, points: res.ownerPoints });
refetchLike();
} catch { setLiked(true); setLikeCount((c) => c + 1); }
} else {
setLiked(true); setLikeCount((c) => c + 1);
toast('Нравится ❤️');
try { await likeMutation.mutateAsync(); refetchLike(); }
catch { setLiked(false); setLikeCount((c) => c - 1); }
}
};
const handleDelete = async () => {
if (!isOwner || !cat) return;
try {
await deleteMutation.mutateAsync(cat.id);
toast('Удалено');
onClose();
} catch { toast('Ошибка удаления', 'error'); }
};
if (isLoading || !cat) {
return (
<div className="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center" onClick={onClose}>
<div className="flex items-center gap-3 px-6 py-4 bg-white rounded-full shadow-lg animate-scale-in">
<div className="h-5 w-5 rounded-full border-2 border-[var(--primary)]/30 border-t-[var(--primary)] animate-spin" />
<span className="text-[14px] font-medium text-[var(--fg-secondary)]">Загрузка...</span>
</div>
</div>
);
}
const dateStr = new Date(cat.created_at).toLocaleDateString('ru-RU', {
year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit',
});
return (
<div className="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-end sm:items-center justify-center" onClick={onClose}>
<div
className="w-full sm:max-w-xl bg-white sm:rounded-2xl sm:mx-4 max-h-[92vh] flex flex-col overflow-hidden shadow-xl animate-scale-in"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between px-5 py-4 border-b shrink-0">
<div className="flex items-center gap-3">
<Avatar className="h-9 w-9">
<AvatarFallback className="text-sm avatar-initials">{cat.username.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<div>
<span className="text-[15px] font-semibold">@{cat.username}</span>
<p className="text-[11px] text-[var(--fg-tertiary)]">{dateStr}</p>
</div>
</div>
<div className="flex items-center gap-1">
{isOwner && (
<button onClick={handleDelete} className="btn-icon h-9 w-9 text-[var(--fg-secondary)] hover:text-[var(--danger)] hover:bg-[var(--danger-light)]">
<Trash2 className="h-4 w-4" strokeWidth={1.5} />
</button>
)}
<button onClick={onClose} className="btn-icon h-9 w-9 text-[var(--fg-secondary)] hover:text-[var(--fg)] hover:bg-[var(--secondary)]">
<X className="h-5 w-5" strokeWidth={1.5} />
</button>
</div>
</div>
<div className="bg-[var(--secondary)] flex items-center justify-center shrink-0 relative">
{!imageLoaded && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="h-8 w-8 rounded-full border-2 border-[var(--fg-tertiary)]/30 border-t-[var(--primary)] animate-spin" />
</div>
)}
<img
src={cat.image_url}
alt={cat.caption || 'Фото кота'}
className={`w-full max-h-[55vh] object-contain transition-opacity duration-300 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setImageLoaded(true)}
/>
</div>
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-3">
<div className="flex items-start gap-3">
<Avatar className="h-8 w-8 shrink-0 mt-0.5">
<AvatarFallback className="text-[10px] avatar-initials">{cat.username.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<div>
<p className="text-[15px] leading-[1.4]">
<span className="font-semibold mr-1.5">@{cat.username}</span>
{cat.caption || 'Без подписи'}
</p>
</div>
</div>
</div>
<div className="border-t px-5 py-4 shrink-0">
<div className="flex items-center justify-between">
<button
onClick={toggleLike}
disabled={isOwner || !user}
className={`btn-icon flex items-center gap-1.5 text-[15px] font-medium ${
isOwner
? 'text-[var(--fg-tertiary)] cursor-not-allowed'
: liked
? 'text-[var(--danger)]'
: 'text-[var(--fg-secondary)] hover:text-[var(--fg)]'
}`}
>
<Heart className={`h-6 w-6 transition-all ${liked ? 'fill-[var(--danger)] stroke-[var(--danger)] animate-heart' : 'stroke-[1.5]'}`} />
{likeCount > 0 && <span className="tabular-nums">{likeCount}</span>}
</button>
<span className="flex items-center gap-1.5 text-[13px] text-[var(--fg-secondary)]">
<span>🏆</span>
<span className="font-semibold">{cat.user_points}</span>
<span className="text-[var(--fg-tertiary)]">баллов</span>
</span>
</div>
</div>
</div>
</div>
);
}