+
{toasts.map((t) => (
- {t.message}
+ {t.message}
))}
@@ -37,4 +41,4 @@ export function ToastProvider({ children }: { children: ReactNode }) {
);
}
-export function useToast() { return useContext(ToastContext); }
+export function useToast() { return useContext(ToastContext); }
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
index fe29df0..fa682a3 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -2,118 +2,218 @@
@plugin "tailwindcss-animate";
:root {
- --background: #f5f5f5;
- --foreground: #000000;
- --card: #ffffff;
- --card-foreground: #000000;
- --popover: #ffffff;
- --popover-foreground: #000000;
- --primary: #2aabee;
+ --background: #f5f5f7;
+ --surface: #ffffff;
+ --surface-hover: #f8f8fa;
+ --foreground: #1c1c1e;
+ --foreground-secondary: #8a8a8e;
+ --foreground-tertiary: #aeaeb2;
+ --primary: #007aff;
+ --primary-hover: #0066d6;
+ --primary-light: #e8f2ff;
--primary-foreground: #ffffff;
- --secondary: #eff2f5;
- --secondary-foreground: #000000;
- --muted: #f0f0f0;
- --muted-foreground: #8e8e93;
- --accent: #e8f5ff;
- --accent-foreground: #2aabee;
- --destructive: #ff3b30;
- --destructive-foreground: #ffffff;
+ --secondary: #f2f2f5;
+ --secondary-foreground: #1c1c1e;
--border: #e5e5ea;
- --input: #e5e5ea;
- --ring: #2aabee;
- --radius: 0;
-
- --tg-blue: #2aabee;
- --tg-blue-light: #e8f5ff;
- --tg-separator: #e5e5ea;
- --tg-bg: #f5f5f5;
- --tg-bubble: #ffffff;
- --tg-muted: #8e8e93;
+ --border-light: #f0f0f3;
+ --danger: #ff3b30;
+ --danger-hover: #d6342b;
+ --danger-light: #ffe8e6;
+ --success: #34c759;
+ --success-light: #e8f8ed;
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03);
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.03);
+ --shadow-lg: 0 12px 32px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.04);
+ --radius-sm: 8px;
+ --radius-md: 14px;
+ --radius-lg: 20px;
+ --radius-full: 9999px;
}
-* { border-color: var(--tg-separator); }
+* { border-color: var(--border); }
body {
- background-color: var(--tg-bg);
+ background: var(--background);
color: var(--foreground);
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text",
+ "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ line-height: 1.5;
+ letter-spacing: -0.01em;
}
-::selection { background: var(--tg-blue); color: white; }
+::selection { background: var(--primary); color: white; }
-/* Heart animation */
-@keyframes heart-pop {
- 0% { transform: scale(1); }
- 40% { transform: scale(1.2); }
- 100% { transform: scale(1); }
-}
-.heart-animate { animation: heart-pop 0.3s ease-out; }
+/* ── Animations ── */
-/* Fade in */
@keyframes fade-in {
- from { opacity: 0; transform: translateY(4px); }
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+@keyframes fade-in-up {
+ from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
-.animate-fade-in { animation: fade-in 0.25s ease-out; }
-
-/* Scale in */
@keyframes scale-in {
- from { opacity: 0; transform: scale(0.97); }
+ from { opacity: 0; transform: scale(0.96); }
to { opacity: 1; transform: scale(1); }
}
-.animate-scale-in { animation: scale-in 0.15s ease-out; }
-
-/* Slide up for toasts */
@keyframes slide-up {
- from { opacity: 0; transform: translateY(8px); }
+ from { opacity: 0; transform: translateY(100%); }
to { opacity: 1; transform: translateY(0); }
}
-.animate-slide-up { animation: slide-up 0.2s ease-out; }
-
-/* Shimmer */
+@keyframes heart-bounce {
+ 0% { transform: scale(1); }
+ 25% { transform: scale(1.3); }
+ 50% { transform: scale(0.95); }
+ 75% { transform: scale(1.1); }
+ 100% { transform: scale(1); }
+}
+@keyframes spin-slow {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
-.skeleton-shimmer {
- background: linear-gradient(90deg, #e8e8e8 25%, #f5f5f5 50%, #e8e8e8 75%);
- background-size: 200% 100%;
- animation: shimmer 1.2s ease-in-out infinite;
+@keyframes toast-in {
+ from { opacity: 0; transform: translateY(12px) scale(0.96); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
+}
+@keyframes toast-out {
+ from { opacity: 1; transform: translateY(0) scale(1); }
+ to { opacity: 0; transform: translateY(-8px) scale(0.96); }
}
-/* Page transition */
-.page-enter { opacity: 0; }
-.page-enter-active { opacity: 1; transition: opacity 0.12s ease-out; }
+.animate-fade-in { animation: fade-in 0.15s ease-out; }
+.animate-fade-in-up { animation: fade-in-up 0.25s cubic-bezier(0.16, 1, 0.3, 1); }
+.animate-scale-in { animation: scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1); }
+.animate-slide-up { animation: slide-up 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
+.animate-heart { animation: heart-bounce 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
+.animate-spin-slow { animation: spin-slow 1s linear infinite; }
+.animate-toast-in { animation: toast-in 0.25s cubic-bezier(0.16, 1, 0.3, 1); }
-/* Like button */
-.like-btn { transition: transform 0.08s; cursor: pointer; }
-.like-btn:active { transform: scale(0.82); }
+/* ── Shared UI ── */
-/* Telegram-style avatar ring */
-.tg-avatar {
+.card {
+ background: var(--surface);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--border);
+ transition: box-shadow 0.2s ease;
+}
+.card:hover {
+ box-shadow: var(--shadow-md);
+}
+
+.avatar-initials {
border-radius: 50%;
- background: var(--tg-blue);
- color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
+ color: white;
+ background: var(--primary);
+ user-select: none;
}
-/* Telegram-style bubble */
-.tg-bubble {
- background: white;
- border-radius: 12px;
- overflow: hidden;
+.btn-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-full);
+ transition: background 0.15s ease, transform 0.1s ease;
+ cursor: pointer;
+ border: none;
+}
+.btn-icon:active {
+ transform: scale(0.92);
}
-/* Telegram-style separator */
-.tg-divider {
- height: 1px;
- background: var(--tg-separator);
+.link-blue {
+ color: var(--primary);
+ font-weight: 600;
+ transition: opacity 0.15s;
+}
+.link-blue:hover {
+ opacity: 0.8;
+ text-decoration: underline;
}
-/* Scrollbar hide */
+.text-secondary { color: var(--foreground-secondary); }
+.text-tertiary { color: var(--foreground-tertiary); }
+
+/* ── Utility ── */
+
+.skeleton-shimmer {
+ background: linear-gradient(90deg, #e8e8ec 25%, #f2f2f5 50%, #e8e8ec 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.2s ease-in-out infinite;
+}
+
+/* ── Modal backdrop ── */
+
+.backdrop-blur {
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+}
+
+/* ── Button base ── */
+
+.btn-primary {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ border-radius: var(--radius-full);
+ background: var(--primary);
+ color: white;
+ transition: background 0.15s, transform 0.1s;
+ cursor: pointer;
+ border: none;
+}
+.btn-primary:hover { background: var(--primary-hover); }
+.btn-primary:active { transform: scale(0.97); }
+.btn-primary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.btn-outline {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 500;
+ border-radius: var(--radius-full);
+ background: transparent;
+ color: var(--foreground);
+ border: 1px solid var(--border);
+ transition: background 0.15s, border-color 0.15s;
+ cursor: pointer;
+}
+.btn-outline:hover { background: var(--secondary); border-color: var(--foreground-tertiary); }
+.btn-outline:active { transform: scale(0.97); }
+
+/* ── Scrollbar ── */
+
.scrollbar-none::-webkit-scrollbar { display: none; }
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
+
+/* ── Image loading ── */
+
+.img-loading {
+ background: var(--secondary);
+ position: relative;
+ overflow: hidden;
+}
+.img-loading::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
+ animation: shimmer 1.5s infinite;
+ background-size: 200% 100%;
+}
\ No newline at end of file
diff --git a/client/src/pages/CatPage.tsx b/client/src/pages/CatPage.tsx
index d11dc75..caac395 100644
--- a/client/src/pages/CatPage.tsx
+++ b/client/src/pages/CatPage.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
-import { useParams, useNavigate } from 'react-router-dom';
+import { useParams, useNavigate, Link } from 'react-router-dom';
import { useCat, useDeleteCat } from '@/hooks/useCats';
import { useLikeStatus, useLikeCat, useUnlikeCat } from '@/hooks/useLikes';
import { useAuth } from '@/hooks/useAuth';
@@ -7,7 +7,7 @@ import { useToast } from '@/components/Toast';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton';
-import { ArrowLeft } from 'lucide-react';
+import { ArrowLeft, Heart, Trash2 } from 'lucide-react';
export default function CatPage() {
const { id } = useParams<{ id: string }>();
@@ -23,6 +23,7 @@ export default function CatPage() {
const deleteMutation = useDeleteCat();
const [liked, setLiked] = useState(false);
+ const [imageLoaded, setImageLoaded] = useState(false);
const cat = data?.cat;
const isOwner = cat && user && cat.user_id === user.id;
const isLiked = likeData?.liked ?? false;
@@ -35,12 +36,13 @@ export default function CatPage() {
setLiked(false);
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); }
} else {
setLiked(true);
- toast('Нравится', 'like');
+ toast('Нравится ❤️');
try { await likeMutation.mutateAsync(); refetchLike(); }
catch { setLiked(false); }
}
@@ -60,9 +62,10 @@ export default function CatPage() {
-
);
@@ -70,79 +73,132 @@ export default function CatPage() {
if (isError || !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 (
-
-
-

+
+ {/* Image */}
+
+ {!imageLoaded && (
+
+ )}
+

setImageLoaded(true)}
+ />
-
+
{/* Header */}
-
-
-
-
+
+
+
+
{cat.username.charAt(0).toUpperCase()}
-
@{cat.username}
-
- {new Date(cat.created_at).toLocaleDateString('ru-RU', {
- year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit',
- })}
-
+
+ @{cat.username}
+
+
{dateStr}
-
+
{isOwner && (
-
{/* Caption */}
{cat.caption && (
-
+
@{cat.username}
{cat.caption}
)}
+ {/* Divider */}
+
+
{/* Actions */}
-
+
-
- {isLiked ? '❤️' : '🤍'}
-
- {cat.likes_count > 0 && {cat.likes_count}}
+
+ {cat.likes_count > 0 && {cat.likes_count}}
- 🏆 {cat.user_points} баллов
+
+
+ 🏆
+ {cat.user_points}
+ баллов
+
+
+
+ {/* Back to feed button */}
+
+ navigate('/feed')}
+ className="rounded-full text-[12px] h-8"
+ >
+ ← В ленту
+
);
-}
+}
\ No newline at end of file
diff --git a/client/src/pages/FeedPage.tsx b/client/src/pages/FeedPage.tsx
index b378c60..86ebc6b 100644
--- a/client/src/pages/FeedPage.tsx
+++ b/client/src/pages/FeedPage.tsx
@@ -1,26 +1,28 @@
import { useState } from 'react';
import { useCats } from '@/hooks/useCats';
-import { useAuth } from '@/hooks/useAuth';
import CatCard from '@/components/CatCard';
import FeedSidebar from '@/components/FeedSidebar';
import CatModal from '@/components/CatModal';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
-import { ChevronLeft, ChevronRight } from 'lucide-react';
+import { ChevronLeft, ChevronRight, RefreshCw } from 'lucide-react';
function FeedSkeleton() {
return (
-
+
{Array.from({ length: 3 }).map((_, i) => (
-
-
-
-
+
+
-
))}
@@ -31,27 +33,59 @@ function FeedSkeleton() {
export default function FeedPage() {
const [page, setPage] = useState(1);
const [modalCatId, setModalCatId] = useState
(null);
- const { data, isLoading, isError } = useCats(page);
+ const { data, isLoading, isError, refetch } = useCats(page);
return (
- {/* Feed */}
-
+
+ {/* Page indicator */}
+ {data && data.totalPages > 0 && (
+
+
+ Лента
+
+
+ {data.total} публикаций
+
+
+ )}
+
{isLoading && }
{isError && (
-
-
Не удалось загрузить котов
-
setPage(1)} className="rounded-full text-[13px]">Попробовать снова
+
+
+ 😿
+
+
Не удалось загрузить
+
+ Проверьте подключение к интернету и попробуйте снова
+
+
refetch()}
+ className="rounded-full text-[13px] gap-1.5"
+ >
+
+ Попробовать снова
+
)}
{data && data.cats.length === 0 && (
-
-
Пока нет котов
-
Будьте первым, кто загрузит фото кота
-
window.location.href = '/upload'} className="rounded-full text-[13px] bg-[#2aabee] hover:bg-[#1d9bd9]">
+
+
+ 🐱
+
+
Пока нет котов
+
+ Будьте первым, кто загрузит фото кота
+
+
window.location.href = '/upload'}
+ className="rounded-full text-[13px] bg-[var(--primary)] hover:bg-[var(--primary-hover)]"
+ >
Загрузить кота
@@ -59,36 +93,63 @@ export default function FeedPage() {
{data && data.cats.length > 0 && (
<>
- {data.cats.map((cat) => (
+ {data.cats.map((cat, index) => (
))}
{data.totalPages > 1 && (
-
-
setPage((p) => Math.max(1, p - 1))} disabled={page === 1} className="rounded-full text-[13px] h-8">
-
+
+
)}
>
)}
-
+
- {/* 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}
}
+
+
+
-
+
Нет аккаунта?{' '}
-
+
Зарегистрироваться
);
-}
+}
\ 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 (
-
-
Пользователь не найден
-
navigate('/feed')} className="rounded-full text-[13px]">Перейти в ленту
+
+
+ 🔍
+
+
Пользователь не найден
+
navigate('/feed')} className="rounded-full text-[13px] mt-2">
+ Вернуться в ленту
+
);
}
@@ -56,91 +64,146 @@ export default function ProfilePage() {
const totalLikes = cats.reduce((sum, c) => sum + c.likes_count, 0);
return (
-
+
{!isMe && (
-
navigate(-1)} className="mb-5 flex items-center gap-1.5 text-[14px] text-[#8e8e93] hover:text-black transition-colors">
+ navigate(-1)}
+ className="mb-4 flex items-center gap-1.5 text-[14px] text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors"
+ >
Назад
)}
{/* Profile header */}
-
-
-
+
+
+
{initials}
-
-
@{profileUser?.username}
+
+
@{profileUser?.username}
{isMe && (
-
- Редактировать
-
+
+
+
)}
-
-
-
{cats.length}{' '}
-
публикаций
+
+
+
+
{cats.length}
+
публикаций
-
-
{totalLikes}{' '}
-
лайков
+
-
-
🏆{profileUser?.points ?? 0}{' '}
-
баллов
+
+
🏆{profileUser?.points ?? 0}
+
баллов
+
{isMe && (
-
-
-
-
- Новая публикация
-
-
-
+
+
+
+ Новая публикация
+
+
)}
- {/* Grid */}
- {cats.length === 0 ? (
-
-
Пока нет котов
-
- {isMe ? 'Поделитесь первым фото кота' : 'У пользователя пока нет котов'}
-
- {isMe && (
-
-
Загрузить кота
-
- )}
-
- ) : (
-
- {cats.map((cat) => (
-
setModalCatId(cat.id)}
- className="group relative aspect-square overflow-hidden bg-[#f0f0f0]"
- >
-
-
-
-
-
{cat.likes_count}
+ {/* Tabs */}
+
+ setTab('photos')}
+ className={`px-5 py-3 text-[14px] font-medium border-b-2 transition-all ${
+ tab === 'photos'
+ ? 'border-[var(--primary)] text-[var(--primary)]'
+ : 'border-transparent text-[var(--foreground-secondary)] hover:text-[var(--foreground)]'
+ }`}
+ >
+ Фото
+
+ setTab('likes')}
+ className={`px-5 py-3 text-[14px] font-medium border-b-2 transition-all ${
+ tab === 'likes'
+ ? 'border-[var(--primary)] text-[var(--primary)]'
+ : 'border-transparent text-[var(--foreground-secondary)] hover:text-[var(--foreground)]'
+ }`}
+ >
+ Избранное
+
+
+
+ {/* Content */}
+ {tab === 'photos' && (
+ cats.length === 0 ? (
+
+
+ 📸
+
+
Пока нет котов
+
+ {isMe ? 'Поделитесь первым фото кота' : 'У пользователя пока нет котов'}
+
+ {isMe && (
+
+
+ Загрузить кота
+
+
+ )}
+
+ ) : (
+
+ {cats.map((cat) => (
+
setModalCatId(cat.id)}
+ className="group relative aspect-square overflow-hidden bg-[var(--secondary)] rounded-xl"
+ >
+
+
+
+
+ {cat.likes_count}
+
-
-
- ))}
+
+ ))}
+
+ )
+ )}
+
+ {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}
}
+
+
+
-
+
Уже есть аккаунт?{' '}
-
+
Войти
);
-}
+}
\ 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() {
navigate(-1)}
- className="mb-4 flex items-center gap-1.5 text-[14px] text-[#8e8e93] hover:text-black transition-colors"
+ className="mb-5 flex items-center gap-1.5 text-[14px] text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors group"
>
-
+
Назад
-
Новая публикация
+
Новая публикация
{!preview ? (
-
-
+
+
+ {isDragActive ? (
+
+ ) : (
+
+ )}
+
-
+
{isDragActive ? 'Отпустите фото' : 'Перетащите фото сюда'}
-
или нажмите, чтобы выбрать
-
Выбрать файл
-
JPG, PNG, GIF, WebP · до 10 МБ
+
+ или нажмите, чтобы выбрать
+
+
+ Выбрать файл
+
+
+ JPG, PNG, GIF, WebP · до 10 МБ
+
) : (
-
-
-

+
+ {/* Preview */}
+
+
-
+
+ {/* Caption input */}
-
-
+
+
{user?.username?.charAt(0).toUpperCase() || '?'}
-
{user?.username}
+
{user?.username}
-
-
+10 баллов
+ {/* Submit */}
+
+
+ 🏆
+ +10 баллов
+ за публикацию
+
- {uploadMutation.isPending ? 'Публикация...' : 'Опубликовать'}
+ {uploadMutation.isPending ? (
+
+
+ Публикация...
+
+ ) : 'Опубликовать'}
-
- )}
- {uploadMutation.isError && (
-
Не удалось загрузить. Попробуйте снова.
+ {uploadMutation.isError && (
+
+ ⚠
+ Не удалось загрузить. Попробуйте снова.
+
+ )}
+
)}
);
-}
+}
\ No newline at end of file