+
{toasts.map((t) => (
{t.message}
-
))}
@@ -51,6 +37,4 @@ export function ToastProvider({ children }: { children: ReactNode }) {
);
}
-export function useToast() {
- return useContext(ToastContext);
-}
+export function useToast() { return useContext(ToastContext); }
diff --git a/client/src/index.css b/client/src/index.css
index 2be58bb..fe29df0 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -1,134 +1,117 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
-@custom-variant dark (&:is(.dark *));
-
:root {
- --background: #faf7f3;
- --foreground: #1c1613;
+ --background: #f5f5f5;
+ --foreground: #000000;
--card: #ffffff;
- --card-foreground: #1c1613;
+ --card-foreground: #000000;
--popover: #ffffff;
- --popover-foreground: #1c1613;
- --primary: #e85d3a;
+ --popover-foreground: #000000;
+ --primary: #2aabee;
--primary-foreground: #ffffff;
- --secondary: #fff0eb;
- --secondary-foreground: #e85d3a;
- --muted: #f5f0ec;
- --muted-foreground: #8a7f75;
- --accent: #fceae3;
- --accent-foreground: #1c1613;
- --destructive: #e03e2f;
+ --secondary: #eff2f5;
+ --secondary-foreground: #000000;
+ --muted: #f0f0f0;
+ --muted-foreground: #8e8e93;
+ --accent: #e8f5ff;
+ --accent-foreground: #2aabee;
+ --destructive: #ff3b30;
--destructive-foreground: #ffffff;
- --border: #e8e0d8;
- --input: #e8e0d8;
- --ring: #e85d3a;
- --radius: 0.75rem;
+ --border: #e5e5ea;
+ --input: #e5e5ea;
+ --ring: #2aabee;
+ --radius: 0;
- --shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.04);
- --shadow-md: 0 4px 12px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.04);
- --shadow-lg: 0 12px 32px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.04);
+ --tg-blue: #2aabee;
+ --tg-blue-light: #e8f5ff;
+ --tg-separator: #e5e5ea;
+ --tg-bg: #f5f5f5;
+ --tg-bubble: #ffffff;
+ --tg-muted: #8e8e93;
}
-* {
- border-color: var(--border);
-}
+* { border-color: var(--tg-separator); }
body {
- background-color: var(--background);
+ background-color: var(--tg-bg);
color: var(--foreground);
- font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
}
-::selection {
- background: #e85d3a;
- color: white;
-}
+::selection { background: var(--tg-blue); color: white; }
/* Heart animation */
@keyframes heart-pop {
0% { transform: scale(1); }
- 25% { transform: scale(1.3); }
- 50% { transform: scale(0.9); }
- 75% { transform: scale(1.1); }
+ 40% { transform: scale(1.2); }
100% { transform: scale(1); }
}
-
-.heart-animate {
- animation: heart-pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
-}
-
-/* Double tap heart */
-@keyframes heart-burst {
- 0% { transform: scale(0) rotate(-12deg); opacity: 1; }
- 55% { transform: scale(1.2) rotate(3deg); opacity: 0.8; }
- 100% { transform: scale(1.6) rotate(0deg); opacity: 0; }
-}
-
-.heart-burst {
- animation: heart-burst 0.5s ease-out forwards;
-}
+.heart-animate { animation: heart-pop 0.3s ease-out; }
/* Fade in */
@keyframes fade-in {
- from { opacity: 0; transform: translateY(8px); }
+ from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
-
-.animate-fade-in {
- animation: fade-in 0.35s ease-out;
-}
+.animate-fade-in { animation: fade-in 0.25s ease-out; }
/* Scale in */
@keyframes scale-in {
- from { opacity: 0; transform: scale(0.95); }
+ from { opacity: 0; transform: scale(0.97); }
to { opacity: 1; transform: scale(1); }
}
+.animate-scale-in { animation: scale-in 0.15s ease-out; }
-.animate-scale-in {
- animation: scale-in 0.2s ease-out;
-}
-
-/* Slide up */
+/* Slide up for toasts */
@keyframes slide-up {
- from { opacity: 0; transform: translateY(10px); }
+ from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
-
-.animate-slide-up {
- animation: slide-up 0.25s ease-out;
-}
+.animate-slide-up { animation: slide-up 0.2s ease-out; }
/* Shimmer */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
-
.skeleton-shimmer {
- background: linear-gradient(90deg, #f0e8e2 25%, #faf7f3 50%, #f0e8e2 75%);
+ background: linear-gradient(90deg, #e8e8e8 25%, #f5f5f5 50%, #e8e8e8 75%);
background-size: 200% 100%;
animation: shimmer 1.2s ease-in-out infinite;
}
/* Page transition */
-.page-enter {
- opacity: 0;
-}
-.page-enter-active {
- opacity: 1;
- transition: opacity 0.18s ease-out;
+.page-enter { opacity: 0; }
+.page-enter-active { opacity: 1; transition: opacity 0.12s ease-out; }
+
+/* Like button */
+.like-btn { transition: transform 0.08s; cursor: pointer; }
+.like-btn:active { transform: scale(0.82); }
+
+/* Telegram-style avatar ring */
+.tg-avatar {
+ border-radius: 50%;
+ background: var(--tg-blue);
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
}
-/* Like button press */
-.like-btn {
- transition: transform 0.1s;
- cursor: pointer;
+/* Telegram-style bubble */
+.tg-bubble {
+ background: white;
+ border-radius: 12px;
+ overflow: hidden;
}
-.like-btn:active {
- transform: scale(0.8);
+
+/* Telegram-style separator */
+.tg-divider {
+ height: 1px;
+ background: var(--tg-separator);
}
/* Scrollbar hide */
diff --git a/client/src/pages/CatPage.tsx b/client/src/pages/CatPage.tsx
index 0ac5f7f..d11dc75 100644
--- a/client/src/pages/CatPage.tsx
+++ b/client/src/pages/CatPage.tsx
@@ -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 { Heart, ArrowLeft, Trash2, MessageCircle } from 'lucide-react';
+import { ArrowLeft } from 'lucide-react';
export default function CatPage() {
const { id } = useParams<{ id: string }>();
@@ -23,30 +23,24 @@ export default function CatPage() {
const deleteMutation = useDeleteCat();
const [liked, setLiked] = useState(false);
- const [animating, setAnimating] = useState(false);
-
const cat = data?.cat;
const isOwner = cat && user && cat.user_id === user.id;
const isLiked = likeData?.liked ?? false;
useEffect(() => { setLiked(isLiked); }, [isLiked]);
- const handleLike = async () => {
+ const toggleLike = async () => {
if (isOwner || !user || !cat) return;
- setAnimating(true);
- setTimeout(() => setAnimating(false), 450);
-
if (isLiked) {
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('Нравится', 'like');
try { await likeMutation.mutateAsync(); refetchLike(); }
catch { setLiked(false); }
}
@@ -56,19 +50,19 @@ export default function CatPage() {
if (!isOwner || !cat) return;
try {
await deleteMutation.mutateAsync(cat.id);
- toast('Удалено', 'success');
+ toast('Удалено');
navigate('/feed');
} catch { toast('Ошибка удаления', 'error'); }
};
if (isLoading) {
return (
-
-
+
);
@@ -77,82 +71,76 @@ export default function CatPage() {
if (isError || !cat) {
return (
-
Кот не найден
-
+
Кот не найден
+
);
}
return (
-
+
-
-
+
+
-
+
{/* Header */}
-
-
+
+
{cat.username.charAt(0).toUpperCase()}
-
@{cat.username}
-
+ @{cat.username}
+
{new Date(cat.created_at).toLocaleDateString('ru-RU', {
- year: 'numeric', month: 'long', day: 'numeric',
- hour: '2-digit', minute: '2-digit',
+ year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit',
})}
{isOwner && (
-
- {/* Actions */}
-
-
-
-
-
-
-
-
-
-
{cat.likes_count.toLocaleString('ru-RU')} отметок «Нравится»
-
+ {/* Caption */}
{cat.caption && (
-
- @{cat.username}
+
+ @{cat.username}
{cat.caption}
)}
-
🏆 {cat.user_points} баллов
+ {/* Actions */}
+
+
+
+ {isLiked ? '❤️' : '🤍'}
+
+ {cat.likes_count > 0 && {cat.likes_count}}
+
+ 🏆 {cat.user_points} баллов
+
diff --git a/client/src/pages/FeedPage.tsx b/client/src/pages/FeedPage.tsx
index d09c51d..b378c60 100644
--- a/client/src/pages/FeedPage.tsx
+++ b/client/src/pages/FeedPage.tsx
@@ -3,25 +3,24 @@ import { useCats } from '@/hooks/useCats';
import { useAuth } from '@/hooks/useAuth';
import CatCard from '@/components/CatCard';
import FeedSidebar from '@/components/FeedSidebar';
-import StoryCircle from '@/components/StoryCircle';
import CatModal from '@/components/CatModal';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
-import { ChevronLeft, ChevronRight, Cat } from 'lucide-react';
+import { ChevronLeft, ChevronRight } from 'lucide-react';
function FeedSkeleton() {
return (
-
- {Array.from({ length: 2 }).map((_, i) => (
-
-
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+
-
+
-
-
))}
@@ -33,45 +32,28 @@ export default function FeedPage() {
const [page, setPage] = useState(1);
const [modalCatId, setModalCatId] = useState
(null);
const { data, isLoading, isError } = useCats(page);
- const { user } = useAuth();
return (
-
-
- {/* Main feed */}
-
- {/* Stories */}
-
-
- {user && }
- {data?.cats
- .filter((c, i, arr) => arr.findIndex((x) => x.user_id === c.user_id) === i)
- .slice(0, 7)
- .map((c) => (
-
- ))}
-
-
-
+
+
+ {/* Feed */}
+
{isLoading &&
}
{isError && (
-
-
Не удалось загрузить котов
-
setPage(1)}>Попробовать снова
+
Не удалось загрузить котов
+
setPage(1)} className="rounded-full text-[13px]">Попробовать снова
)}
{data && data.cats.length === 0 && (
-
-
-
Пока нет котов
-
Будьте первым, кто загрузит фото кота
-
window.location.href = '/upload'}>Загрузить кота
+
+
Пока нет котов
+
Будьте первым, кто загрузит фото кота
+
window.location.href = '/upload'} className="rounded-full text-[13px] bg-[#2aabee] hover:bg-[#1d9bd9]">
+ Загрузить кота
+
)}
@@ -82,15 +64,15 @@ export default function FeedPage() {
))}
{data.totalPages > 1 && (
-
-
setPage((p) => Math.max(1, p - 1))} disabled={page === 1} className="rounded-xl">
-
+
+ setPage((p) => Math.max(1, p - 1))} disabled={page === 1} className="rounded-full text-[13px] h-8">
+
Назад
- {data.page} из {data.totalPages}
- setPage((p) => p + 1)} disabled={page >= data.totalPages} className="rounded-xl">
+ {data.page} из {data.totalPages}
+ setPage((p) => p + 1)} disabled={page >= data.totalPages} className="rounded-full text-[13px] h-8">
Вперёд
-
+
)}
@@ -98,9 +80,9 @@ export default function FeedPage() {
)}
- {/* Sidebar */}
-
-
+ {/* Sidebar — hidden on mobile */}
+
diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx
index 583db65..ce06c7c 100644
--- a/client/src/pages/LoginPage.tsx
+++ b/client/src/pages/LoginPage.tsx
@@ -22,63 +22,53 @@ export default function LoginPage() {
await login(username, password);
} catch (err: any) {
setError(err.response?.data?.error || 'Ошибка входа');
- } finally {
- setLoading(false);
- }
+ } finally { setLoading(false); }
};
return (
-
+
-
-
+
+
-
- Catstagram
-
-
Войдите, чтобы смотреть котов
+
Catstagram
+
Войдите, чтобы смотреть котов
-
-
+
-
-
- Нет аккаунта?{' '}
-
- Зарегистрироваться
-
-
-
+
+ Нет аккаунта?{' '}
+
+ Зарегистрироваться
+
+
);
diff --git a/client/src/pages/ProfilePage.tsx b/client/src/pages/ProfilePage.tsx
index 99db2c7..e43d1c5 100644
--- a/client/src/pages/ProfilePage.tsx
+++ b/client/src/pages/ProfilePage.tsx
@@ -6,7 +6,7 @@ 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 { Heart, Plus, ArrowLeft, Grid3X3, Bookmark } from 'lucide-react';
+import { ArrowLeft, Plus, Heart } from 'lucide-react';
export default function ProfilePage() {
const { id: paramId } = useParams<{ id?: string }>();
@@ -27,16 +27,16 @@ export default function ProfilePage() {
if (isLoading) {
return (
-
-
+
+
- {Array.from({ length: 6 }).map((_, i) => ())}
+ {Array.from({ length: 6 }).map((_, i) => ())}
);
@@ -45,8 +45,8 @@ export default function ProfilePage() {
if (!profileUser && !isMe) {
return (
-
Пользователь не найден
-
navigate('/feed')}>Перейти в ленту
+
Пользователь не найден
+
navigate('/feed')} className="rounded-full text-[13px]">Перейти в ленту
);
}
@@ -55,89 +55,68 @@ export default function ProfilePage() {
const initials = profileUser?.username?.charAt(0).toUpperCase() || '?';
const totalLikes = cats.reduce((sum, c) => sum + c.likes_count, 0);
- const declension = (n: number, forms: [string, string, string]) => {
- n = Math.abs(n) % 100;
- const n1 = n % 10;
- if (n > 10 && n < 20) return forms[2];
- if (n1 > 1 && n1 < 5) return forms[1];
- if (n1 === 1) return forms[0];
- return forms[2];
- };
-
return (
-
+
{!isMe && (
-
navigate(-1)} className="mb-6 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors">
-
+ navigate(-1)} className="mb-5 flex items-center gap-1.5 text-[14px] text-[#8e8e93] hover:text-black transition-colors">
+
Назад
)}
{/* Profile header */}
-
-
-
+
+
+
{initials}
-
-
@{profileUser?.username}
+
+
@{profileUser?.username}
{isMe && (
-
- Редактировать профиль
+
+ Редактировать
)}
-
-
+
- {cats.length}{' '}
- {declension(cats.length, ['публикация', 'публикации', 'публикаций'])}
+ {cats.length}{' '}
+ публикаций
- {totalLikes}{' '}
- {declension(totalLikes, ['лайк', 'лайка', 'лайков'])}
+ {totalLikes}{' '}
+ лайков
- 🏆 {profileUser?.points ?? 0}{' '}
- {declension(profileUser?.points ?? 0, ['балл', 'балла', 'баллов'])}
+ 🏆{profileUser?.points ?? 0}{' '}
+ баллов
-
{isMe && (
-
-
-
- Новая публикация
-
-
+
+
+
+
+ Новая публикация
+
+
+
)}
- {/* Tabs */}
-
-
-
- Публикации
-
-
-
- Сохранённое
-
-
-
{/* Grid */}
{cats.length === 0 ? (
-
-
Пока нет котов
-
+
+
Пока нет котов
+
{isMe ? 'Поделитесь первым фото кота' : 'У пользователя пока нет котов'}
{isMe && (
-
Загрузить кота
+
Загрузить кота
)}
@@ -147,11 +126,11 @@ export default function ProfilePage() {
setModalCatId(cat.id)}
- className="group relative aspect-square overflow-hidden rounded-xl bg-muted"
+ className="group relative aspect-square overflow-hidden bg-[#f0f0f0]"
>
-
-
+
+
{cat.likes_count}
diff --git a/client/src/pages/RegisterPage.tsx b/client/src/pages/RegisterPage.tsx
index d986b9d..e59a471 100644
--- a/client/src/pages/RegisterPage.tsx
+++ b/client/src/pages/RegisterPage.tsx
@@ -29,73 +29,63 @@ export default function RegisterPage() {
await register(username, password);
} catch (err: any) {
setError(err.response?.data?.error || 'Ошибка регистрации');
- } finally {
- setLoading(false);
- }
+ } finally { setLoading(false); }
};
return (
-
+
-
-
+
+
-
- Catstagram
-
-
Присоединяйтесь к сообществу котоводов
+
Catstagram
+
Присоединяйтесь к сообществу
-
-
+
-
-
- Уже есть аккаунт?{' '}
-
- Войти
-
-
-
+
+ Уже есть аккаунт?{' '}
+
+ Войти
+
+
);
diff --git a/client/src/pages/UploadPage.tsx b/client/src/pages/UploadPage.tsx
index 79002b1..cb3748b 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, ImageIcon } from 'lucide-react';
+import { ArrowLeft, Upload, X } from 'lucide-react';
export default function UploadPage() {
const navigate = useNavigate();
@@ -42,11 +42,9 @@ export default function UploadPage() {
try {
const result = await uploadMutation.mutateAsync(fd);
if (user) updateUser({ ...user, points: (user.points || 0) + 10 });
- toast('+10 баллов! 🎉', 'success');
+ toast('+10 баллов');
navigate(`/cat/${result.cat.id}`);
- } catch {
- toast('Ошибка загрузки', 'error');
- }
+ } catch { toast('Ошибка загрузки', 'error'); }
};
const clearFile = () => {
@@ -56,100 +54,83 @@ export default function UploadPage() {
};
return (
-
+
navigate(-1)}
- className="mb-6 flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
+ className="mb-4 flex items-center gap-1.5 text-[14px] text-[#8e8e93] hover:text-black transition-colors"
>
-
+
Назад
-
-
-
Новый пост
+
Новая публикация
+
+ {!preview ? (
+
+
+
+
+
+
+ {isDragActive ? 'Отпустите фото' : 'Перетащите фото сюда'}
+
+
или нажмите, чтобы выбрать
+
Выбрать файл
+
JPG, PNG, GIF, WebP · до 10 МБ
+ ) : (
+
+
+

+
+
+
+
- {!preview ? (
-
-
-
-
+
+
+
+ {user?.username?.charAt(0).toUpperCase() || '?'}
+
+
+
-
- {isDragActive ? 'Отпустите фото' : 'Перетащите фото сюда'}
-
-
или нажмите, чтобы выбрать
-
- Выбрать файл
+
+
+
+
+10 баллов
+
+ {uploadMutation.isPending ? 'Публикация...' : 'Опубликовать'}
-
JPG, PNG, GIF, WebP · до 10 МБ
- ) : (
-
-
-

-
-
-
-
-
-
-
-
- {user?.username?.charAt(0).toUpperCase() || '?'}
-
-
-
-
-
-
- +10 баллов за загрузку
-
- {uploadMutation.isPending ? 'Публикация...' : 'Поделиться'}
-
-
-
- )}
-
-
- {preview && (
-
-
-
- Выбрать другое фото
-
)}
{uploadMutation.isError && (
-
Не удалось загрузить. Попробуйте снова.
+
Не удалось загрузить. Попробуйте снова.
)}
);