diff --git a/client/src/App.tsx b/client/src/App.tsx index 0be805b..5639ec3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,6 +5,7 @@ import { ThemeProvider } from '@/hooks/useTheme'; import { ToastProvider } from '@/components/Toast'; import ProtectedRoute from '@/components/ProtectedRoute'; import Navbar from '@/components/Navbar'; +import ThemeBubble from '@/components/ThemeBubble'; import LoginPage from '@/pages/LoginPage'; import RegisterPage from '@/pages/RegisterPage'; import FeedPage from '@/pages/FeedPage'; @@ -107,6 +108,7 @@ export default function App() { +
diff --git a/client/src/components/CatCard.tsx b/client/src/components/CatCard.tsx index 139522c..418b7ba 100644 --- a/client/src/components/CatCard.tsx +++ b/client/src/components/CatCard.tsx @@ -17,15 +17,15 @@ export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) { const { user } = useAuth(); const [liked, setLiked] = useState(() => likedIds?.has(cat.id) ?? false); const [likeCount, setLikeCount] = useState(cat.likes_count); - - useEffect(() => { - if (likedIds !== undefined) setLiked(likedIds.has(cat.id)); - }, [likedIds, cat.id]); const { toast } = useToast(); const likeMutation = useLikeCat(cat.id); const unlikeMutation = useUnlikeCat(cat.id); const isOwner = user && cat.user_id === user.id; + useEffect(() => { + if (likedIds !== undefined) setLiked(likedIds.has(cat.id)); + }, [likedIds, cat.id]); + const toggleLike = async (e: React.MouseEvent) => { e.stopPropagation(); if (isOwner || !user) return; @@ -48,62 +48,85 @@ export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) { return (
onOpen(cat.id)}> -
+ {/* Шапка */} +
e.stopPropagation()}> - +
- e.stopPropagation()} className="hover:text-[var(--accent)] transition-colors"> -

@{cat.username}

+ e.stopPropagation()} + className="hover:text-[var(--accent)] transition-colors" + > +

@{cat.username}

+ + + {cat.user_points} +
+ {/* Изображение */}
{cat.caption
-
+ {/* Подпись и действия */} +
{cat.caption && ( -

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

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

)} -
-
- + /> + {likeCount > 0 && ( + {likeCount} + )} + - -
- - - - {cat.user_points} - + {/* Комментарии */} +
diff --git a/client/src/components/CatModal.tsx b/client/src/components/CatModal.tsx index ff1d57a..db31b9e 100644 --- a/client/src/components/CatModal.tsx +++ b/client/src/components/CatModal.tsx @@ -85,8 +85,8 @@ export default function CatModal({ catId, onClose }: CatModalProps) { if (isLoading || !cat) { return createPortal( -
-
+
+
Загрузка...
@@ -100,130 +100,199 @@ export default function CatModal({ catId, onClose }: CatModalProps) { }); return createPortal( -
+
+ {/* Двухколоночный макет на десктопе, вертикальный на мобильном */}
e.stopPropagation()} > -
- - -
- @{cat.username} -

{dateStr}

-
- -
- {isOwner && ( - - )} - -
-
- -
+ {/* Левая колонка — фото */} +
{!imageLoaded && (
-
+
)} {cat.caption setImageLoaded(true)} />
-
- {cat.caption && ( -
- -

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

+ {/* Правая колонка — мета + комментарии */} +
+ {/* Шапка */} +
+ + +
+ + @{cat.username} + +

{dateStr}

+
+ +
+ {isOwner && ( + + )} +
- )} - - {comments.length > 0 && ( -
- {comments.map(c => ( -
- - - -
-

- @{c.username} - {c.text} -

-

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

-
- {(user && (c.user_id === user.id || user.is_admin)) && ( - - )} -
- ))} -
-
- )} -
- -
-
- - - - {cat.user_points} -
- {user && ( -
{ e.preventDefault(); handleComment(); }} - className="flex items-center gap-2" - > - setCommentText(e.target.value)} - placeholder="Комментарий..." - maxLength={500} - className="flex-1 h-9 px-3 text-sm rounded-xl border border-[var(--border)] bg-[var(--surface)] placeholder:text-[var(--fg-tertiary)] focus-ring" - /> + {/* Комментарии и подпись */} +
+ {cat.caption && ( +
+ +

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

+
+ )} + + {comments.length > 0 && ( +
+ {comments.map(c => ( +
+ + + +
+

+ + @{c.username} + + {c.text} +

+

+ {new Date(c.created_at).toLocaleDateString('ru-RU', { + day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', + })} +

+
+ {(user && (c.user_id === user.id || user.is_admin)) && ( + + )} +
+ ))} +
+
+ )} + + {comments.length === 0 && !cat.caption && ( +

Нет комментариев

+ )} +
+ + {/* Нижняя панель */} +
+ {/* Лайк + очки */} +
- - )} + + + + {cat.user_points} + +
+ + {/* Поле комментария */} + {user && ( +
{ e.preventDefault(); handleComment(); }} + className="flex items-center gap-2" + > + + setCommentText(e.target.value)} + placeholder="Написать комментарий..." + maxLength={500} + className="flex-1 h-9 px-3.5 text-sm rounded-xl border border-[var(--border)] bg-[var(--bg)] placeholder:text-[var(--fg-tertiary)] focus-ring" + /> + + + )} +
, document.body ); -} \ No newline at end of file +} diff --git a/client/src/components/FeedSidebar.tsx b/client/src/components/FeedSidebar.tsx index d8220b8..cd8f0d0 100644 --- a/client/src/components/FeedSidebar.tsx +++ b/client/src/components/FeedSidebar.tsx @@ -2,13 +2,29 @@ import { useAuth } from '@/hooks/useAuth'; import { useCats } from '@/hooks/useCats'; import UserAvatar from '@/components/UserAvatar'; import { Link } from 'react-router-dom'; -import { Trophy, Crown, Medal } from 'lucide-react'; +import { Trophy, Crown, Medal, Star } from 'lucide-react'; function RankBadge({ rank }: { rank: number }) { - if (rank === 0) return ; - if (rank === 1) return ; - if (rank === 2) return ; - return {rank + 1}; + if (rank === 0) return ( + + + + ); + if (rank === 1) return ( + + + + ); + if (rank === 2) return ( + + + + ); + return ( + + {rank + 1} + + ); } export default function FeedSidebar() { @@ -27,41 +43,74 @@ export default function FeedSidebar() { .slice(0, 5); return ( -
+
+ {/* Карточка профиля */} {user && ( - - + +
-

{user.username}

-

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

+ @{user.username}

+

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

+
+
+
)} + {/* Таблица лидеров */}
-

- Лидеры +

+ + Лидеры недели

{topUsers.length === 0 ? (

Пока никого нет

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

Котограм · сообщество котоводов

+

+ 🐾 Котограм · сообщество котоводов +

); -} \ No newline at end of file +} diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx index e10e7f0..b0ebf15 100644 --- a/client/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -1,47 +1,53 @@ import { Link, useLocation } from 'react-router-dom'; import { useAuth } from '@/hooks/useAuth'; -import { useTheme } from '@/hooks/useTheme'; import UserAvatar from '@/components/UserAvatar'; -import { Home, PlusCircle, Shield, Sun, Moon, Monitor } from 'lucide-react'; +import { Home, PlusSquare, Shield } from 'lucide-react'; export default function Navbar() { const { user } = useAuth(); - const { theme, setTheme } = useTheme(); const location = useLocation(); - const cycleTheme = () => { - const next: Record = { light: 'dark', dark: 'system', system: 'light' }; - setTheme(next[theme]); - }; - const ThemeIcon = theme === 'dark' ? Moon : theme === 'light' ? Sun : Monitor; - if (!user) return null; + const isFeed = location.pathname === '/feed' || location.pathname === '/'; + const isUpload = location.pathname === '/upload'; + const isProfile = location.pathname.startsWith('/profile'); + return ( -