Files
cats/client/src/pages/UploadPage.tsx

157 lines
6.3 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 { useState, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDropzone } from 'react-dropzone';
import { useUploadCat } from '@/hooks/useCats';
import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/components/Toast';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { ArrowLeft, Upload, X, ImageIcon } from 'lucide-react';
export default function UploadPage() {
const navigate = useNavigate();
const { user, updateUser } = useAuth();
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [caption, setCaption] = useState('');
const uploadMutation = useUploadCat();
const { toast } = useToast();
const onDrop = useCallback((accepted: File[]) => {
const f = accepted[0];
if (f) {
setFile(f);
if (preview) URL.revokeObjectURL(preview);
setPreview(URL.createObjectURL(f));
}
}, [preview]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp'] },
maxFiles: 1,
maxSize: 10 * 1024 * 1024,
});
const handleUpload = async () => {
if (!file) return;
const fd = new FormData();
fd.append('image', file);
fd.append('caption', caption);
try {
const result = await uploadMutation.mutateAsync(fd);
if (user) updateUser({ ...user, points: (user.points || 0) + 10 });
toast('+10 баллов! 🎉', 'success');
navigate(`/cat/${result.cat.id}`);
} catch {
toast('Ошибка загрузки', 'error');
}
};
const clearFile = () => {
setFile(null);
if (preview) URL.revokeObjectURL(preview);
setPreview(null);
};
return (
<div className="mx-auto max-w-xl px-4 py-6">
<button
onClick={() => navigate(-1)}
className="mb-6 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Назад
</button>
<div className="bg-white rounded-2xl border border-border/60 shadow-sm overflow-hidden">
<div className="px-5 py-3.5 border-b">
<h1 className="text-base font-semibold text-center">Новый пост</h1>
</div>
{!preview ? (
<div
{...getRootProps()}
className={`flex cursor-pointer flex-col items-center justify-center p-16 text-center transition-all ${
isDragActive
? 'bg-gradient-to-br from-orange-50 to-rose-50 border-2 border-dashed border-orange-300 m-2 rounded-xl'
: 'hover:bg-secondary/50'
}`}
>
<input {...getInputProps()} />
<div className="mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-secondary">
<Upload className={`h-8 w-8 ${isDragActive ? 'text-orange-500' : 'text-muted-foreground'}`} strokeWidth={1.5} />
</div>
<p className="text-base font-medium mb-1">
{isDragActive ? 'Отпустите фото' : 'Перетащите фото сюда'}
</p>
<p className="text-sm text-muted-foreground mb-5">или нажмите, чтобы выбрать</p>
<Button variant="outline" size="sm" className="rounded-xl">
Выбрать файл
</Button>
<p className="mt-4 text-xs text-muted-foreground">JPG, PNG, GIF, WebP · до 10 МБ</p>
</div>
) : (
<div>
<div className="relative bg-muted flex items-center justify-center max-h-96 overflow-hidden">
<img src={preview} alt="Preview" className="max-h-96 w-full object-contain" />
<button
onClick={clearFile}
className="absolute right-3 top-3 flex h-8 w-8 items-center justify-center rounded-xl bg-black/50 text-white hover:bg-black/70 transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="flex items-start gap-3 px-4 py-3">
<Avatar className="h-8 w-8 shrink-0 ring-2 ring-offset-1 ring-orange-200">
<AvatarFallback className="text-xs bg-gradient-to-br from-orange-400 to-rose-500 text-white">
{user?.username?.charAt(0).toUpperCase() || '?'}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="text-xs font-semibold mb-1">{user?.username}</p>
<textarea
placeholder="Напишите подпись..."
value={caption}
onChange={(e) => setCaption(e.target.value)}
maxLength={200}
rows={2}
className="w-full resize-none text-sm bg-transparent outline-none placeholder:text-muted-foreground"
/>
<p className="text-right text-xs text-muted-foreground mt-1">{caption.length}/200</p>
</div>
</div>
<div className="flex items-center justify-between px-4 py-3 bg-gradient-to-r from-orange-50 to-rose-50 border-t">
<span className="text-sm font-medium text-orange-600">+10 баллов за загрузку</span>
<Button
onClick={handleUpload}
disabled={uploadMutation.isPending}
size="sm"
className="rounded-xl bg-gradient-to-r from-orange-400 to-rose-500 text-white hover:from-orange-500 hover:to-rose-600 shadow-sm"
>
{uploadMutation.isPending ? 'Публикация...' : 'Поделиться'}
</Button>
</div>
</div>
)}
</div>
{preview && (
<div className="mt-4 flex justify-center">
<Button variant="ghost" size="sm" onClick={clearFile} className="text-muted-foreground rounded-xl">
<ImageIcon className="h-4 w-4 mr-1.5" />
Выбрать другое фото
</Button>
</div>
)}
{uploadMutation.isError && (
<p className="mt-3 text-center text-sm text-destructive">Не удалось загрузить. Попробуйте снова.</p>
)}
</div>
);
}