diff --git a/client/src/components/CatCard.tsx b/client/src/components/CatCard.tsx index 2ad97e9..ed5b3c2 100644 --- a/client/src/components/CatCard.tsx +++ b/client/src/components/CatCard.tsx @@ -4,6 +4,7 @@ import { useAuth } from '@/hooks/useAuth'; import { useLikeCat, useUnlikeCat } from '@/hooks/useLikes'; import { useToast } from '@/components/Toast'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { Heart, MessageCircle } from 'lucide-react'; interface CatCardProps { cat: Cat; @@ -33,90 +34,99 @@ export default function CatCard({ cat, onOpen }: CatCardProps) { } else { setLiked(true); setLikeCount((c) => c + 1); - toast('Нравится', 'like'); + toast('Нравится ❤️'); try { await likeMutation.mutateAsync(); } catch { setLiked(false); setLikeCount((c) => c - 1); } } }; + const timeAgo = new Date(cat.created_at).toLocaleDateString('ru-RU', { + day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', + }); + return ( -
-
+
+
{/* Header */} -
+
onOpen(cat.id)} > - - + + {cat.username.charAt(0).toUpperCase()}
- @{cat.username} - - {new Date(cat.created_at).toLocaleDateString('ru-RU', { - day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', - })} - +

+ @{cat.username} +

+
{/* Image */} -
onOpen(cat.id)} > {cat.caption -
+ {/* Caption */} {cat.caption && ( -
onOpen(cat.id)}> - +
onOpen(cat.id)}> +

@{cat.username} {cat.caption} - +

)} - {/* Actions row */} -
-
+ {/* Actions */} +
+
+
- {cat.user_points > 0 && ( - 🏆 {cat.user_points} - )} + + 🏆 + {cat.user_points} + баллов +
-
+
); -} +} \ No newline at end of file diff --git a/client/src/components/CatModal.tsx b/client/src/components/CatModal.tsx index 20fea14..4824774 100644 --- a/client/src/components/CatModal.tsx +++ b/client/src/components/CatModal.tsx @@ -4,7 +4,7 @@ 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 } from 'lucide-react'; +import { X, Heart, Trash2 } from 'lucide-react'; interface CatModalProps { catId: number; @@ -22,10 +22,14 @@ export default function CatModal({ catId, onClose }: CatModalProps) { 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(() => { + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = ''; }; + }, []); const cat = data?.cat; const isOwner = cat && user && cat.user_id === user.id; @@ -37,13 +41,14 @@ export default function CatModal({ catId, onClose }: CatModalProps) { setLikeCount((c) => c - 1); try { const res = await unlikeMutation.mutateAsync(); - if (res.ownerPoints !== undefined && cat.user_id === user.id) updateUser({ ...user, points: res.ownerPoints }); + 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('Нравится', 'like'); + toast('Нравится ❤️'); try { await likeMutation.mutateAsync(); refetchLike(); } catch { setLiked(false); setLikeCount((c) => c - 1); } } @@ -53,97 +58,135 @@ export default function CatModal({ catId, onClose }: CatModalProps) { if (!isOwner || !cat) return; try { await deleteMutation.mutateAsync(cat.id); - toast('Удалено', 'success'); + toast('Удалено'); onClose(); } catch { toast('Ошибка удаления', 'error'); } }; + // Close on Escape + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [onClose]); + if (isLoading || !cat) { return ( -
-
-
+
+
+
+ Загрузка...
); } + const dateStr = new Date(cat.created_at).toLocaleDateString('ru-RU', { + year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', + }); + return ( -
+
e.stopPropagation()} > {/* Header */} -
-
- - +
+
+ + {cat.username.charAt(0).toUpperCase()} - @{cat.username} +
+ @{cat.username} +

{dateStr}

+
-
+
{isOwner && ( )} -
{/* Image */} -
- {cat.caption +
+ {!imageLoaded && ( +
+
+
+ )} + {cat.caption setImageLoaded(true)} + />
{/* Info */} -
-
- - +
+
+ + {cat.username.charAt(0).toUpperCase()}
-

+

@{cat.username} {cat.caption || 'Без подписи'}

- -

- {new Date(cat.created_at).toLocaleDateString('ru-RU', { - year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', - })} -

{/* Actions */} -
-
- - 🏆 {cat.user_points} баллов +
+
+
+ +
+ + 🏆 + {cat.user_points} + баллов +
); -} +} \ No newline at end of file diff --git a/client/src/components/FeedSidebar.tsx b/client/src/components/FeedSidebar.tsx index d2f4725..df1d0ac 100644 --- a/client/src/components/FeedSidebar.tsx +++ b/client/src/components/FeedSidebar.tsx @@ -2,7 +2,7 @@ import { useAuth } from '@/hooks/useAuth'; import { useCats } from '@/hooks/useCats'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Link } from 'react-router-dom'; -import { Trophy } from 'lucide-react'; +import { Trophy, Sparkles } from 'lucide-react'; export default function FeedSidebar() { const { user } = useAuth(); @@ -21,53 +21,87 @@ export default function FeedSidebar() { .slice(0, 5); return ( -
+
+ {/* Current user */} {user && ( - + - + {user.username.charAt(0).toUpperCase()} -
-

{user.username}

-

🏆 {user.points} баллов

+
+

+ {user.username} +

+

+ 🏆 {user.points} баллов +

)} -
-
- -

Лидеры

+ {/* Top users */} +
+
+ +

+ Лидеры +

+ {topUsers.length === 0 ? ( -

Пока нет пользователей

+

+ Пока нет пользователей +

) : ( -
+
{topUsers.map(([id, u], i) => ( - - {i + 1} + + + {i + 1} + - + {u.username.charAt(0).toUpperCase()} - + {u.username} - {u.points} + + {u.points} + ))}
)}
-
-

Catstagram · Делись фото котов

+ {/* Info */} +
+
+ + Catstagram · Сообщество котоводов +
); -} +} \ No newline at end of file diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx index 71fd8dd..81a1c97 100644 --- a/client/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -1,43 +1,53 @@ -import { Link, useNavigate, useLocation } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { useAuth } from '@/hooks/useAuth'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { House, Plus } from 'lucide-react'; export default function Navbar() { - const { user, logout } = useAuth(); - const navigate = useNavigate(); + const { user } = useAuth(); const location = useLocation(); if (!user) return null; + const navItems = [ + { to: '/feed', label: 'Лента', icon: House }, + { to: '/upload', label: 'Добавить', icon: Plus }, + ]; + return ( - )} )} -
+ - {/* Sidebar — hidden on mobile */} -
-
+
+
{modalCatId && setModalCatId(null)} />}
); -} +} \ No newline at end of file diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index ce06c7c..c1a6ac4 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -26,50 +26,86 @@ export default function LoginPage() { }; return ( -
-
-
-
- +
+
+
+
+
-

Catstagram

-

Войдите, чтобы смотреть котов

+

Catstagram

+

+ Войдите, чтобы смотреть котов +

-
- setUsername(e.target.value)} - placeholder="Имя пользователя" - required - autoFocus - className="h-11 text-[15px] rounded-xl bg-white border-[#d1d1d6]" - /> - setPassword(e.target.value)} - placeholder="Пароль" - required - className="h-11 text-[15px] rounded-xl bg-white border-[#d1d1d6]" - /> - {error &&

{error}

} +
+
+ + setUsername(e.target.value)} + placeholder="your_cat_lover" + required + autoFocus + className="h-12 text-[15px] rounded-xl bg-white border focus:border-[var(--primary)] focus:ring-2 focus:ring-[var(--primary)]/20 transition-all" + /> +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + className="h-12 text-[15px] rounded-xl bg-white border focus:border-[var(--primary)] focus:ring-2 focus:ring-[var(--primary)]/20 transition-all" + /> +
+ + {error && ( +
+ + {error} +
+ )} + +
+ +
+
+
+
+
+ + или + +
-

+

Нет аккаунта?{' '} - + Зарегистрироваться

); -} +} \ No newline at end of file diff --git a/client/src/pages/ProfilePage.tsx b/client/src/pages/ProfilePage.tsx index e43d1c5..b8082b6 100644 --- a/client/src/pages/ProfilePage.tsx +++ b/client/src/pages/ProfilePage.tsx @@ -6,13 +6,14 @@ import CatModal from '@/components/CatModal'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { ArrowLeft, Plus, Heart } from 'lucide-react'; +import { ArrowLeft, Plus, LogOut, Heart } from 'lucide-react'; export default function ProfilePage() { const { id: paramId } = useParams<{ id?: string }>(); const navigate = useNavigate(); - const { user: me } = useAuth(); + const { user: me, logout } = useAuth(); const [modalCatId, setModalCatId] = useState(null); + const [tab, setTab] = useState<'photos' | 'likes'>('photos'); const profileUserId = paramId ? parseInt(paramId) : me?.id; const isMe = !paramId || (me && profileUserId === me.id); @@ -28,15 +29,17 @@ export default function ProfilePage() { if (isLoading) { return (
-
- -
- - +
+ +
+ +
-
- {Array.from({ length: 6 }).map((_, i) => ())} +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))}
); @@ -44,9 +47,14 @@ export default function ProfilePage() { if (!profileUser && !isMe) { return ( -
-

Пользователь не найден

- +
+
+ 🔍 +
+

Пользователь не найден

+
); } @@ -56,91 +64,146 @@ export default function ProfilePage() { const totalLikes = cats.reduce((sum, c) => sum + c.likes_count, 0); return ( -
+
{!isMe && ( - )} {/* Profile header */} -
- - +
+ + {initials}
-
-

@{profileUser?.username}

+
+

@{profileUser?.username}

{isMe && ( - + )}
-
-
- {cats.length}{' '} - публикаций + +
+
+

{cats.length}

+

публикаций

-
- {totalLikes}{' '} - лайков +
+

{totalLikes}

+

лайков

-
- 🏆{profileUser?.points ?? 0}{' '} - баллов +
+

🏆{profileUser?.points ?? 0}

+

баллов

+ {isMe && ( -
- - - -
+ + + )}
- {/* Grid */} - {cats.length === 0 ? ( -
-

Пока нет котов

-

- {isMe ? 'Поделитесь первым фото кота' : 'У пользователя пока нет котов'} -

- {isMe && ( - - - - )} -
- ) : ( -
- {cats.map((cat) => ( - + +
+ + {/* Content */} + {tab === 'photos' && ( + cats.length === 0 ? ( +
+
+ 📸 +
+

Пока нет котов

+

+ {isMe ? 'Поделитесь первым фото кота' : 'У пользователя пока нет котов'} +

+ {isMe && ( + + + + )} +
+ ) : ( +
+ {cats.map((cat) => ( +
- - ))} + + ))} +
+ ) + )} + + {tab === 'likes' && ( +
+
+ +
+

Скоро

+

+ Здесь появятся понравившиеся коты +

)} {modalCatId && setModalCatId(null)} />}
); -} +} \ No newline at end of file diff --git a/client/src/pages/RegisterPage.tsx b/client/src/pages/RegisterPage.tsx index e59a471..57087a5 100644 --- a/client/src/pages/RegisterPage.tsx +++ b/client/src/pages/RegisterPage.tsx @@ -33,60 +33,101 @@ export default function RegisterPage() { }; return ( -
-
-
-
- +
+
+
+
+
-

Catstagram

-

Присоединяйтесь к сообществу

+

Catstagram

+

+ Присоединяйтесь к сообществу +

-
- setUsername(e.target.value)} - placeholder="Имя пользователя" - minLength={3} - required - autoFocus - className="h-11 text-[15px] rounded-xl bg-white border-[#d1d1d6]" - /> - setPassword(e.target.value)} - placeholder="Пароль" - minLength={4} - required - className="h-11 text-[15px] rounded-xl bg-white border-[#d1d1d6]" - /> - setConfirm(e.target.value)} - placeholder="Подтвердите пароль" - required - className="h-11 text-[15px] rounded-xl bg-white border-[#d1d1d6]" - /> - {error &&

{error}

} +
+
+ + setUsername(e.target.value)} + placeholder="your_cat_lover" + minLength={3} + required + autoFocus + className="h-12 text-[15px] rounded-xl bg-white border focus:border-[var(--primary)] focus:ring-2 focus:ring-[var(--primary)]/20 transition-all" + /> +
+
+ + setPassword(e.target.value)} + placeholder="Минимум 4 символа" + minLength={4} + required + className="h-12 text-[15px] rounded-xl bg-white border focus:border-[var(--primary)] focus:ring-2 focus:ring-[var(--primary)]/20 transition-all" + /> +
+
+ + setConfirm(e.target.value)} + placeholder="Повторите пароль" + required + className="h-12 text-[15px] rounded-xl bg-white border focus:border-[var(--primary)] focus:ring-2 focus:ring-[var(--primary)]/20 transition-all" + /> +
+ + {error && ( +
+ + {error} +
+ )} + +
+ +
+
+
+
+
+ + или + +
-

+

Уже есть аккаунт?{' '} - + Войти

); -} +} \ No newline at end of file diff --git a/client/src/pages/UploadPage.tsx b/client/src/pages/UploadPage.tsx index cb3748b..9037b23 100644 --- a/client/src/pages/UploadPage.tsx +++ b/client/src/pages/UploadPage.tsx @@ -6,7 +6,7 @@ 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 } from 'lucide-react'; +import { ArrowLeft, Upload, X, Image } from 'lucide-react'; export default function UploadPage() { const navigate = useNavigate(); @@ -42,9 +42,11 @@ export default function UploadPage() { try { const result = await uploadMutation.mutateAsync(fd); if (user) updateUser({ ...user, points: (user.points || 0) + 10 }); - toast('+10 баллов'); + toast('+10 баллов 🎉'); navigate(`/cat/${result.cat.id}`); - } catch { toast('Ошибка загрузки', 'error'); } + } catch { + toast('Ошибка загрузки', 'error'); + } }; const clearFile = () => { @@ -57,81 +59,125 @@ export default function UploadPage() {
-

Новая публикация

+

Новая публикация

{!preview ? (
-
- +
+
+ {isDragActive ? ( + + ) : ( + + )} +
-

+

{isDragActive ? 'Отпустите фото' : 'Перетащите фото сюда'}

-

или нажмите, чтобы выбрать

- -

JPG, PNG, GIF, WebP · до 10 МБ

+

+ или нажмите, чтобы выбрать +

+ +

+ JPG, PNG, GIF, WebP · до 10 МБ +

) : ( -
-
- Preview +
+ {/* Preview */} +
+ Preview
+ {/* Caption input */}
- - + + {user?.username?.charAt(0).toUpperCase() || '?'}
-

{user?.username}

+

{user?.username}