Fix likes display, add avatar system (presets+upload), improve desktop layout, fix profile viewing

This commit is contained in:
2026-05-29 14:21:40 +03:00
parent 3664b60f10
commit 025e52e6b1
19 changed files with 346 additions and 94 deletions

View File

@@ -9,6 +9,7 @@ export interface Cat {
user_id: number;
username: string;
user_points: number;
user_avatar_url: string | null;
likes_count: number;
comments_count: number;
}
@@ -72,6 +73,10 @@ export function getLikeStatus(catId: number) {
return api.get<{ liked: boolean }>(`/likes/${catId}/status`);
}
export function getBatchLikeStatus(catIds: number[]) {
return api.post<{ likedIds: number[] }>('/likes/batch/status', { catIds });
}
// Admin
export function getAdminUsers() {
return api.get<{ users: User[] }>('/admin/users');
@@ -109,3 +114,19 @@ export function addComment(catId: number, text: string) {
export function deleteComment(commentId: number) {
return api.delete(`/comments/${commentId}`);
}
// Users
export function getUser(id: number) {
return api.get<{ user: User }>(`/users/${id}`);
}
// Avatar
export function uploadAvatar(formData: FormData) {
return api.put<{ user: User }>('/auth/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
export function setAvatarPreset(preset: string) {
return api.put<{ user: User }>('/auth/avatar/preset', { preset });
}

View File

@@ -3,17 +3,18 @@ import { Cat } from '@/api/endpoints';
import { useAuth } from '@/hooks/useAuth';
import { useLikeCat, useUnlikeCat } from '@/hooks/useLikes';
import { useToast } from '@/components/Toast';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import UserAvatar from '@/components/UserAvatar';
import { Heart, MessageCircle, Trophy } from 'lucide-react';
interface CatCardProps {
cat: Cat;
onOpen: (id: number) => void;
likedIds?: Set<number>;
}
export default function CatCard({ cat, onOpen }: CatCardProps) {
export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) {
const { user } = useAuth();
const [liked, setLiked] = useState(false);
const [liked, setLiked] = useState(() => likedIds?.has(cat.id) ?? false);
const [likeCount, setLikeCount] = useState(cat.likes_count);
const { toast } = useToast();
const likeMutation = useLikeCat(cat.id);
@@ -43,11 +44,7 @@ export default function CatCard({ cat, onOpen }: CatCardProps) {
<article className="animate-fade-up">
<div className="card overflow-hidden cursor-pointer card-interactive" onClick={() => onOpen(cat.id)}>
<div className="flex items-center gap-3 px-5 pt-5 pb-3">
<Avatar className="h-9 w-9">
<AvatarFallback className="text-xs avatar-colored bg-[var(--accent)]">
{cat.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={cat.user_avatar_url} username={cat.username} className="h-9 w-9" fallbackClassName="text-xs avatar-colored bg-[var(--accent)]" />
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold truncate">@{cat.username}</h3>
<time className="text-[11px] text-[var(--fg-tertiary)]">{timeAgo}</time>

View File

@@ -4,7 +4,7 @@ import { useLikeStatus, useLikeCat, useUnlikeCat } from '@/hooks/useLikes';
import { useComments, useAddComment, useDeleteComment } from '@/hooks/useComments';
import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/components/Toast';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import UserAvatar from '@/components/UserAvatar';
import { X, Heart, Trash2, Trophy, Send } from 'lucide-react';
interface CatModalProps {
@@ -116,11 +116,7 @@ export default function CatModal({ catId, onClose }: CatModalProps) {
>
<div className="flex items-center justify-between px-5 py-4 shrink-0">
<div className="flex items-center gap-3">
<Avatar className="h-9 w-9">
<AvatarFallback className="text-xs avatar-colored bg-[var(--accent)]">
{cat.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={cat.user_avatar_url} username={cat.username} className="h-9 w-9" fallbackClassName="text-xs avatar-colored bg-[var(--accent)]" />
<div>
<span className="text-sm font-semibold">@{cat.username}</span>
<p className="text-[11px] text-[var(--fg-tertiary)]">{dateStr}</p>
@@ -155,11 +151,7 @@ export default function CatModal({ catId, onClose }: CatModalProps) {
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-3 min-h-0">
{cat.caption && (
<div className="flex items-start gap-3">
<Avatar className="h-7 w-7 shrink-0 mt-0.5">
<AvatarFallback className="text-[9px] avatar-colored bg-[var(--accent)]">
{cat.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={cat.user_avatar_url} username={cat.username} className="h-7 w-7" fallbackClassName="text-[9px] avatar-colored bg-[var(--accent)]" />
<p className="text-sm leading-[1.5]">
<span className="font-semibold mr-1">@{cat.username}</span>
{cat.caption}
@@ -171,11 +163,7 @@ export default function CatModal({ catId, onClose }: CatModalProps) {
<div className="space-y-3 pt-1">
{comments.map(c => (
<div key={c.id} className="flex items-start gap-2.5 group">
<Avatar className="h-7 w-7 shrink-0 mt-0.5">
<AvatarFallback className="text-[9px] avatar-colored bg-[var(--fg-tertiary)]">
{c.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={undefined} username={c.username} className="h-7 w-7" fallbackClassName="text-[9px] avatar-colored bg-[var(--fg-tertiary)]" />
<div className="flex-1 min-w-0">
<p className="text-sm leading-[1.5]">
<span className="font-semibold mr-1">@{c.username}</span>

View File

@@ -1,6 +1,6 @@
import { useAuth } from '@/hooks/useAuth';
import { useCats } from '@/hooks/useCats';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import UserAvatar from '@/components/UserAvatar';
import { Link } from 'react-router-dom';
import { Trophy, Crown, Medal } from 'lucide-react';
@@ -15,11 +15,11 @@ export default function FeedSidebar() {
const { user } = useAuth();
const { data } = useCats(1);
const usersMap = new Map<number, { username: string; points: number }>();
const usersMap = new Map<number, { username: string; points: number; avatar_url: string | null }>();
if (data?.cats) {
for (const cat of data.cats) {
if (!usersMap.has(cat.user_id))
usersMap.set(cat.user_id, { username: cat.username, points: cat.user_points });
usersMap.set(cat.user_id, { username: cat.username, points: cat.user_points, avatar_url: cat.user_avatar_url });
}
}
@@ -31,11 +31,7 @@ export default function FeedSidebar() {
<div className="space-y-4">
{user && (
<Link to="/profile" className="card p-4 flex items-center gap-3.5 card-interactive group block">
<Avatar className="h-11 w-11">
<AvatarFallback className="text-sm font-bold avatar-colored bg-[var(--accent)]">
{user.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={user.avatar_url} username={user.username} className="h-11 w-11" fallbackClassName="text-sm font-bold avatar-colored bg-[var(--accent)]" />
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold truncate group-hover:text-[var(--accent)] transition-colors">{user.username}</p>
<p className="text-xs text-[var(--fg-tertiary)] flex items-center gap-1">
@@ -57,11 +53,7 @@ export default function FeedSidebar() {
{topUsers.map(([id, u], i) => (
<Link key={id} to={`/profile/${id}`} className="flex items-center gap-2.5 group py-0.5">
<RankBadge rank={i} />
<Avatar className="h-6 w-6">
<AvatarFallback className="text-[8px] font-bold avatar-colored bg-[var(--fg-tertiary)]">
{u.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={u.avatar_url} username={u.username} className="h-6 w-6" fallbackClassName="text-[8px] font-bold avatar-colored bg-[var(--fg-tertiary)]" />
<span className="text-sm flex-1 truncate font-medium group-hover:text-[var(--accent)] transition-colors">{u.username}</span>
<span className="stat-value text-xs text-[var(--fg-secondary)]">{u.points}</span>
</Link>

View File

@@ -1,7 +1,7 @@
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Home, PlusCircle, User, Shield } from 'lucide-react';
import UserAvatar from '@/components/UserAvatar';
import { Home, PlusCircle, Shield } from 'lucide-react';
export default function Navbar() {
const { user } = useAuth();
@@ -11,7 +11,7 @@ export default function Navbar() {
return (
<nav className="sticky top-0 z-50 nav-glass border-b border-[var(--border-light)]">
<div className="mx-auto flex h-14 max-w-[540px] items-center justify-between px-5">
<div className="mx-auto flex h-14 max-w-[900px] items-center justify-between px-5">
<Link to="/feed" className="text-[15px] font-800 tracking-tight text-[var(--fg)]">
Котограм
</Link>
@@ -24,11 +24,7 @@ export default function Navbar() {
<div className={`flex items-center justify-center h-8 w-8 rounded-full transition-all ${
location.pathname.startsWith('/profile') ? 'ring-2 ring-[var(--accent)] ring-offset-2 ring-offset-[var(--bg)]' : ''
}`}>
<Avatar className="h-8 w-8">
<AvatarFallback className="text-[11px] avatar-colored bg-[var(--accent)]">
{user.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={user.avatar_url} username={user.username} className="h-8 w-8" fallbackClassName="text-[11px] avatar-colored bg-[var(--accent)]" />
</div>
{user.is_admin && (
<span className="absolute -top-0.5 -right-0.5 h-3.5 w-3.5 rounded-full bg-[var(--accent)] flex items-center justify-center ring-2 ring-[var(--bg)]">

View File

@@ -0,0 +1,39 @@
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { getPresetEmoji } from '@/lib/avatars';
interface UserAvatarProps {
avatarUrl: string | null | undefined;
username: string;
className?: string;
fallbackClassName?: string;
}
export default function UserAvatar({ avatarUrl, username, className, fallbackClassName }: UserAvatarProps) {
const presetEmoji = getPresetEmoji(avatarUrl);
const initial = username.charAt(0).toUpperCase();
if (presetEmoji) {
return (
<Avatar className={className}>
<AvatarFallback className={fallbackClassName ?? 'avatar-colored bg-[var(--accent)] text-base leading-none'}>
{presetEmoji}
</AvatarFallback>
</Avatar>
);
}
if (avatarUrl) {
return (
<Avatar className={className}>
<AvatarImage src={avatarUrl} alt={username} />
<AvatarFallback className={fallbackClassName ?? 'avatar-colored bg-[var(--accent)]'}>{initial}</AvatarFallback>
</Avatar>
);
}
return (
<Avatar className={className}>
<AvatarFallback className={fallbackClassName ?? 'avatar-colored bg-[var(--accent)]'}>{initial}</AvatarFallback>
</Avatar>
);
}

View File

@@ -6,6 +6,7 @@ export interface User {
username: string;
points: number;
is_admin?: boolean;
avatar_url?: string | null;
created_at?: string;
}

16
client/src/lib/avatars.ts Normal file
View File

@@ -0,0 +1,16 @@
const PRESETS = ['🐱', '🐶', '🦊', '🐻', '🐼', '🦁', '🐯', '🐮', '🐷', '🐸', '🦉', '🦋', '🐙', '🦄', '🐰', '🐹', '🐨', '🦎', '🐧', '🦈'] as const;
export type AvatarPreset = typeof PRESETS[number];
export { PRESETS };
export function isPresetAvatar(avatarUrl: string | null | undefined): avatarUrl is `preset:${AvatarPreset}` {
return !!avatarUrl && avatarUrl.startsWith('preset:');
}
export function getPresetEmoji(avatarUrl: string | null | undefined): string | null {
if (isPresetAvatar(avatarUrl)) return avatarUrl.slice(7);
return null;
}
export function isCustomAvatar(avatarUrl: string | null | undefined): boolean {
return !!avatarUrl && !avatarUrl.startsWith('preset:');
}

View File

@@ -6,8 +6,8 @@ import { useComments, useAddComment, useDeleteComment } from '@/hooks/useComment
import { useAuth } from '@/hooks/useAuth';
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 UserAvatar from '@/components/UserAvatar';
import { ArrowLeft, Heart, Trash2, Trophy, Image, Send, X } from 'lucide-react';
export default function CatPage() {
@@ -33,13 +33,12 @@ export default function CatPage() {
const cat = data?.cat;
const comments = commentsData?.comments ?? [];
const isOwner = cat && user && cat.user_id === user.id;
const isLiked = likeData?.liked ?? false;
useEffect(() => { setLiked(isLiked); }, [isLiked]);
useEffect(() => { if (likeData?.liked !== undefined) setLiked(likeData.liked); }, [likeData?.liked]);
const toggleLike = async () => {
if (isOwner || !user || !cat) return;
if (isLiked) {
if (liked) {
setLiked(false);
try { await unlikeMutation.mutateAsync(); refetchLike(); }
catch { setLiked(true); }
@@ -74,7 +73,7 @@ export default function CatPage() {
if (isLoading) {
return (
<div className="mx-auto max-w-[520px] px-5 py-8">
<div className="mx-auto max-w-[540px] px-5 py-8">
<Skeleton className="mb-4 h-4 w-20 rounded-lg" />
<Skeleton className="aspect-[4/3] w-full rounded-2xl" />
<div className="mt-5 space-y-3">
@@ -102,7 +101,7 @@ export default function CatPage() {
});
return (
<div className="mx-auto max-w-[520px] px-5 py-6 animate-fade-up">
<div className="mx-auto max-w-[540px] px-5 py-6 animate-fade-up">
<button onClick={() => navigate(-1)}
className="mb-5 flex items-center gap-1.5 text-xs text-[var(--fg-tertiary)] hover:text-[var(--fg)] transition-colors">
<ArrowLeft className="h-4 w-4" strokeWidth={1.5} /> Назад
@@ -123,11 +122,7 @@ export default function CatPage() {
<div className="p-5 space-y-4">
<div className="flex items-start justify-between">
<Link to={`/profile/${cat.user_id}`} className="flex items-center gap-3 group">
<Avatar className="h-10 w-10">
<AvatarFallback className="text-sm avatar-colored bg-[var(--accent)]">
{cat.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={cat.user_avatar_url} username={cat.username} className="h-10 w-10" fallbackClassName="text-sm avatar-colored bg-[var(--accent)]" />
<div>
<span className="text-sm font-semibold group-hover:text-[var(--accent)] transition-colors">@{cat.username}</span>
<p className="text-[11px] text-[var(--fg-tertiary)]">{dateStr}</p>
@@ -170,11 +165,7 @@ export default function CatPage() {
</p>
{comments.map(c => (
<div key={c.id} className="flex items-start gap-2.5 group">
<Avatar className="h-7 w-7 shrink-0 mt-0.5">
<AvatarFallback className="text-[9px] avatar-colored bg-[var(--fg-tertiary)]">
{c.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<UserAvatar avatarUrl={undefined} username={c.username} className="h-7 w-7" fallbackClassName="text-[9px] avatar-colored bg-[var(--fg-tertiary)]" />
<div className="flex-1 min-w-0">
<p className="text-sm leading-[1.5]">
<span className="font-semibold mr-1">@{c.username}</span>

View File

@@ -1,9 +1,11 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useCats } from '@/hooks/useCats';
import CatCard from '@/components/CatCard';
import FeedSidebar from '@/components/FeedSidebar';
import CatModal from '@/components/CatModal';
import { Button } from '@/components/ui/button';
import { getBatchLikeStatus } from '@/api/endpoints';
import { useAuth } from '@/hooks/useAuth';
import { ChevronLeft, ChevronRight, RefreshCw, Cat } from 'lucide-react';
function FeedSkeleton() {
@@ -32,12 +34,29 @@ function FeedSkeleton() {
export default function FeedPage() {
const [page, setPage] = useState(1);
const [modalCatId, setModalCatId] = useState<number | null>(null);
const [likedIds, setLikedIds] = useState<Set<number>>(new Set());
const { data, isLoading, isError, refetch } = useCats(page);
const { user } = useAuth();
useEffect(() => {
if (!data?.cats?.length || !user) return;
const ids = data.cats.map(c => c.id);
getBatchLikeStatus(ids)
.then(r => setLikedIds(new Set(r.data.likedIds)))
.catch(() => {});
}, [data?.cats, user]);
const handleUnlike = (catId: number) => {
setLikedIds(prev => { const n = new Set(prev); n.delete(catId); return n; });
};
const handleLike = (catId: number) => {
setLikedIds(prev => { const n = new Set(prev); n.add(catId); return n; });
};
return (
<div className="mx-auto max-w-[540px] px-5 py-5">
<div className="mx-auto max-w-[900px] px-5 py-5">
<div className="flex gap-8">
<main className="flex-1 min-w-0">
<main className="flex-1 min-w-0 max-w-[540px]">
{isLoading && <FeedSkeleton />}
{isError && (
@@ -71,7 +90,7 @@ export default function FeedPage() {
<>
<div className="space-y-5">
{data.cats.map(cat => (
<CatCard key={cat.id} cat={cat} onOpen={setModalCatId} />
<CatCard key={cat.id} cat={cat} onOpen={setModalCatId} likedIds={likedIds} />
))}
</div>
@@ -103,7 +122,7 @@ export default function FeedPage() {
)}
</main>
<aside className="hidden lg:block w-[220px] shrink-0">
<aside className="hidden lg:block w-[260px] shrink-0">
<div className="sticky top-20"><FeedSidebar /></div>
</aside>
</div>

View File

@@ -1,29 +1,61 @@
import { useState } from 'react';
import { useState, useRef, useCallback } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useUserCats } from '@/hooks/useCats';
import { useQuery } from '@tanstack/react-query';
import { getUser, uploadAvatar, setAvatarPreset } from '@/api/endpoints';
import CatModal from '@/components/CatModal';
import UserAvatar from '@/components/UserAvatar';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { ArrowLeft, Plus, LogOut, Shield, Heart, Image, Trophy } from 'lucide-react';
import { ArrowLeft, Plus, LogOut, Shield, Heart, Image, Trophy, Camera, X } from 'lucide-react';
import { PRESETS } from '@/lib/avatars';
import { useToast } from '@/components/Toast';
export default function ProfilePage() {
const { id: paramId } = useParams<{ id?: string }>();
const navigate = useNavigate();
const { user: me, logout } = useAuth();
const { user: me, logout, updateUser } = useAuth();
const [modalCatId, setModalCatId] = useState<number | null>(null);
const [showAvatarPicker, setShowAvatarPicker] = useState(false);
const { toast } = useToast();
const profileUserId = paramId ? parseInt(paramId) : me?.id;
const isMe = !paramId || (me && profileUserId === me.id);
const { data: userData } = useQuery({
queryKey: ['user', profileUserId],
queryFn: () => getUser(profileUserId!).then(r => r.data.user),
enabled: !isMe && !!profileUserId,
});
const { data, isLoading } = useUserCats(profileUserId ?? 0);
const profileUser = isMe
? me
: data?.cats?.[0]
? { id: data.cats[0].user_id, username: data.cats[0].username, points: data.cats[0].user_points }
: null;
const profileUser = isMe ? me : userData;
const fileInputRef = useRef<HTMLInputElement>(null);
const onFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !isMe) return;
const fd = new FormData();
fd.append('avatar', file);
try {
const res = await uploadAvatar(fd);
updateUser(res.data.user);
toast('Аватар обновлён');
} catch { toast('Ошибка', 'error'); }
e.target.value = '';
}, [isMe, updateUser, toast]);
const handlePreset = async (emoji: string) => {
if (!isMe) return;
try {
const res = await setAvatarPreset(emoji);
updateUser(res.data.user);
setShowAvatarPicker(false);
toast('Аватар обновлён');
} catch { toast('Ошибка', 'error'); }
};
if (isLoading) {
return (
@@ -55,7 +87,6 @@ export default function ProfilePage() {
}
const cats = data?.cats ?? [];
const initials = profileUser?.username?.charAt(0).toUpperCase() || '?';
const totalLikes = cats.reduce((sum, c) => sum + c.likes_count, 0);
return (
@@ -67,11 +98,23 @@ export default function ProfilePage() {
)}
<div className="flex items-center gap-5 mb-8">
<Avatar className="h-[72px] w-[72px] shrink-0">
<AvatarFallback className="text-2xl font-800 avatar-colored bg-[var(--accent)]">
{initials}
</AvatarFallback>
</Avatar>
<div className="relative group">
<UserAvatar
avatarUrl={profileUser?.avatar_url}
username={profileUser?.username || '?'}
className="h-[72px] w-[72px]"
fallbackClassName="text-2xl font-800 avatar-colored bg-[var(--accent)]"
/>
{isMe && (
<button
onClick={() => setShowAvatarPicker(true)}
className="absolute inset-0 rounded-full bg-black/0 group-hover:bg-black/30 flex items-center justify-center transition-colors"
>
<Camera className="h-6 w-6 text-white opacity-0 group-hover:opacity-100 transition-opacity" strokeWidth={1.5} />
</button>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-3">
<h1 className="text-lg font-800 truncate">@{profileUser?.username}</h1>
@@ -153,6 +196,48 @@ export default function ProfilePage() {
)}
{modalCatId && <CatModal catId={modalCatId} onClose={() => setModalCatId(null)} />}
{showAvatarPicker && (
<div className="fixed inset-0 z-[70] bg-black/30 flex items-center justify-center animate-fade-in" onClick={() => setShowAvatarPicker(false)}>
<div className="bg-white rounded-2xl w-full max-w-sm p-5 shadow-xl animate-scale-in mx-4" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-5">
<h2 className="text-base font-800">Выбрать аватар</h2>
<button onClick={() => setShowAvatarPicker(false)} className="btn-ghost h-8 w-8 text-[var(--fg-tertiary)]">
<X className="h-5 w-5" strokeWidth={1.5} />
</button>
</div>
<div className="grid grid-cols-5 gap-2 mb-5">
{PRESETS.map(emoji => (
<button
key={emoji}
onClick={() => handlePreset(emoji)}
className={`h-12 w-12 rounded-xl flex items-center justify-center text-2xl transition-all hover:bg-[var(--accent-light)] hover:scale-110 ${
me?.avatar_url === `preset:${emoji}` ? 'ring-2 ring-[var(--accent)] bg-[var(--accent-light)]' : 'bg-[var(--border-light)]'
}`}
>
{emoji}
</button>
))}
</div>
<div className="divider mb-4" />
<div>
<input type="file" ref={fileInputRef} onChange={onFileChange} accept="image/*" className="hidden" />
<Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
className="w-full rounded-xl h-10 gap-2"
>
<Camera className="h-4 w-4" strokeWidth={1.5} /> Загрузить фото
</Button>
</div>
</div>
</div>
)}
<input type="file" ref={fileInputRef} onChange={onFileChange} accept="image/*" className="hidden" />
</div>
);
}

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/api/endpoints.ts","./src/components/catcard.tsx","./src/components/catmodal.tsx","./src/components/feedsidebar.tsx","./src/components/navbar.tsx","./src/components/protectedroute.tsx","./src/components/toast.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/skeleton.tsx","./src/hooks/useauth.tsx","./src/hooks/usecats.ts","./src/hooks/usecomments.ts","./src/hooks/uselikes.ts","./src/lib/auth.ts","./src/lib/utils.ts","./src/pages/adminpage.tsx","./src/pages/catpage.tsx","./src/pages/feedpage.tsx","./src/pages/loginpage.tsx","./src/pages/profilepage.tsx","./src/pages/registerpage.tsx","./src/pages/uploadpage.tsx"],"version":"5.9.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/api/client.ts","./src/api/endpoints.ts","./src/components/catcard.tsx","./src/components/catmodal.tsx","./src/components/feedsidebar.tsx","./src/components/navbar.tsx","./src/components/protectedroute.tsx","./src/components/toast.tsx","./src/components/useravatar.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/skeleton.tsx","./src/hooks/useauth.tsx","./src/hooks/usecats.ts","./src/hooks/usecomments.ts","./src/hooks/uselikes.ts","./src/lib/auth.ts","./src/lib/avatars.ts","./src/lib/utils.ts","./src/pages/adminpage.tsx","./src/pages/catpage.tsx","./src/pages/feedpage.tsx","./src/pages/loginpage.tsx","./src/pages/profilepage.tsx","./src/pages/registerpage.tsx","./src/pages/uploadpage.tsx"],"version":"5.9.3"}