Баллы +1 за загрузку, убраны из лайков. Админ-панель (admin/admin123)

This commit is contained in:
2026-05-29 12:13:42 +03:00
parent f8f220469c
commit 12701ffbc2
19 changed files with 250 additions and 62 deletions

View File

@@ -10,6 +10,7 @@ import FeedPage from '@/pages/FeedPage';
import UploadPage from '@/pages/UploadPage';
import CatPage from '@/pages/CatPage';
import ProfilePage from '@/pages/ProfilePage';
import AdminPage from '@/pages/AdminPage';
import { useEffect, useRef } from 'react';
const queryClient = new QueryClient({
@@ -82,6 +83,14 @@ function AnimatedRoutes() {
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminPage />
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/feed" replace />} />
<Route path="*" element={<Navigate to="/feed" replace />} />
</Routes>

View File

@@ -52,13 +52,26 @@ export function deleteCat(id: number) {
}
export function likeCat(catId: number) {
return api.post<{ liked: boolean; likesCount: number; ownerPoints: number }>(`/likes/${catId}`);
return api.post<{ liked: boolean; likesCount: number }>(`/likes/${catId}`);
}
export function unlikeCat(catId: number) {
return api.delete<{ liked: boolean; likesCount: number; ownerPoints: number }>(`/likes/${catId}`);
return api.delete<{ liked: boolean; likesCount: number }>(`/likes/${catId}`);
}
export function getLikeStatus(catId: number) {
return api.get<{ liked: boolean }>(`/likes/${catId}/status`);
}
// Admin
export function getAdminUsers() {
return api.get<{ users: User[] }>('/admin/users');
}
export function adminDeleteCat(id: number) {
return api.delete(`/admin/cats/${id}`);
}
export function adminDeleteUser(id: number) {
return api.delete(`/admin/users/${id}`);
}

View File

@@ -12,7 +12,7 @@ interface CatCardProps {
}
export default function CatCard({ cat, onOpen }: CatCardProps) {
const { user, updateUser } = useAuth();
const { user } = useAuth();
const [liked, setLiked] = useState(false);
const [likeCount, setLikeCount] = useState(cat.likes_count);
const { toast } = useToast();
@@ -25,11 +25,8 @@ export default function CatCard({ cat, onOpen }: CatCardProps) {
if (liked) {
setLiked(false);
setLikeCount((c) => c - 1);
try {
const res = await unlikeMutation.mutateAsync();
if (res.ownerPoints !== undefined && cat.user_id === user.id)
updateUser({ ...user, points: res.ownerPoints });
} catch { setLiked(true); setLikeCount((c) => c + 1); }
try { await unlikeMutation.mutateAsync(); }
catch { setLiked(true); setLikeCount((c) => c + 1); }
} else {
setLiked(true);
setLikeCount((c) => c + 1);

View File

@@ -12,7 +12,7 @@ interface CatModalProps {
}
export default function CatModal({ catId, onClose }: CatModalProps) {
const { user, updateUser } = useAuth();
const { user } = useAuth();
const { data, isLoading } = useCat(catId);
const { data: likeData, refetch: refetchLike } = useLikeStatus(catId);
const likeMutation = useLikeCat(catId);
@@ -40,11 +40,8 @@ export default function CatModal({ catId, onClose }: CatModalProps) {
if (isOwner || !user || !cat) return;
if (liked) {
setLiked(false); setLikeCount((c) => c - 1);
try {
const res = await unlikeMutation.mutateAsync();
if (res.ownerPoints !== undefined && cat.user_id === user.id) updateUser({ ...user, points: res.ownerPoints });
refetchLike();
} catch { setLiked(true); setLikeCount((c) => c + 1); }
try { await unlikeMutation.mutateAsync(); refetchLike(); }
catch { setLiked(true); setLikeCount((c) => c + 1); }
} else {
setLiked(true); setLikeCount((c) => c + 1);
toast('Нравится ❤️');

View File

@@ -1,6 +1,7 @@
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Shield } from 'lucide-react';
export default function Navbar() {
const { user } = useAuth();
@@ -19,12 +20,17 @@ export default function Navbar() {
<NavLink to="/feed" active={location.pathname === '/feed'} emoji="🏠" label="Лента" />
<NavLink to="/upload" active={location.pathname === '/upload'} emoji="" label="Добавить" short />
<Link to="/profile" className="ml-2">
<Link to="/profile" className="ml-2 relative">
<Avatar className="h-8 w-8 ring-1 ring-[var(--border)] hover:ring-[var(--primary)] transition-all">
<AvatarFallback className="text-[11px] avatar-initials">
{user.username.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
{user.is_admin && (
<span className="absolute -top-1 -right-1 h-3.5 w-3.5 rounded-full bg-[var(--primary)] flex items-center justify-center">
<Shield className="h-2 w-2 text-white" strokeWidth={3} />
</span>
)}
</Link>
</div>
</div>

View File

@@ -13,34 +13,26 @@ export function useLikeStatus(catId: number) {
export function useLikeCat(catId: number) {
const qc = useQueryClient();
const { updateUser, user } = useAuth();
return useMutation({
mutationFn: () => likeCat(catId).then((r) => r.data),
onSuccess: (data) => {
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['likeStatus', catId] });
qc.invalidateQueries({ queryKey: ['cat', catId] });
qc.invalidateQueries({ queryKey: ['cats'] });
if (user && data.ownerPoints !== undefined) {
updateUser({ ...user, points: data.ownerPoints });
}
},
});
}
export function useUnlikeCat(catId: number) {
const qc = useQueryClient();
const { updateUser, user } = useAuth();
return useMutation({
mutationFn: () => unlikeCat(catId).then((r) => r.data),
onSuccess: (data) => {
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['likeStatus', catId] });
qc.invalidateQueries({ queryKey: ['cat', catId] });
qc.invalidateQueries({ queryKey: ['cats'] });
if (user && data.ownerPoints !== undefined) {
updateUser({ ...user, points: data.ownerPoints });
}
},
});
}

View File

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

View File

@@ -0,0 +1,99 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getAdminUsers, adminDeleteUser, adminDeleteCat } from '@/api/endpoints';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { ArrowLeft, Trash2, Shield } from 'lucide-react';
import { useToast } from '@/components/Toast';
export default function AdminPage() {
const { user } = useAuth();
const navigate = useNavigate();
const { toast } = useToast();
const qc = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['admin', 'users'],
queryFn: () => getAdminUsers().then(r => r.data),
enabled: !!user?.is_admin,
});
const deleteUserMutation = useMutation({
mutationFn: (id: number) => adminDeleteUser(id),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin', 'users'] }); toast('Пользователь удалён'); },
onError: () => toast('Ошибка удаления', 'error'),
});
if (!user?.is_admin) {
return (
<div className="flex flex-col items-center justify-center py-24">
<div className="mb-5 flex h-16 w-16 items-center justify-center rounded-full bg-[var(--secondary)]">
<Shield className="h-8 w-8 text-[var(--fg-tertiary)]" strokeWidth={1.5} />
</div>
<p className="text-[17px] font-semibold mb-1.5">Доступ запрещён</p>
<Button variant="outline" onClick={() => navigate('/feed')} className="rounded-full text-[13px] mt-2">Вернуться в ленту</Button>
</div>
);
}
const users = data?.users ?? [];
return (
<div className="mx-auto max-w-[660px] px-4 py-6 animate-fade-in-up">
<button onClick={() => navigate(-1)}
className="mb-5 flex items-center gap-1.5 text-[14px] text-[var(--fg-secondary)] hover:text-[var(--fg)] transition-colors group">
<ArrowLeft className="h-4 w-4 stroke-[1.5] group-hover:-translate-x-0.5 transition-transform" />
Назад
</button>
<div className="flex items-center gap-3 mb-6">
<Shield className="h-6 w-6 text-[var(--primary)]" strokeWidth={1.5} />
<h1 className="text-[22px] font-bold">Админ-панель</h1>
</div>
{isLoading ? (
<div className="space-y-3">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="card p-4 flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-[var(--secondary)] animate-skeleton" />
<div className="space-y-2 flex-1">
<div className="h-4 w-32 rounded-full bg-[var(--secondary)] animate-skeleton" />
<div className="h-3 w-20 rounded-full bg-[var(--secondary)] animate-skeleton" />
</div>
</div>
))}
</div>
) : (
<div className="card divide-y">
<div className="px-4 py-2.5 text-[12px] font-bold text-[var(--fg-secondary)] uppercase tracking-widest flex items-center justify-between">
<span>Пользователи ({users.length})</span>
</div>
{users.map((u: any) => (
<div key={u.id} className="flex items-center gap-3 px-4 py-3">
<Avatar className="h-10 w-10 ring-1 ring-[var(--border)]">
<AvatarFallback className="text-sm avatar-initials">{u.username.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-[15px] font-semibold truncate">@{u.username}</span>
{u.is_admin ? (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-[var(--primary-light)] text-[var(--primary)] font-semibold">Админ</span>
) : null}
</div>
<p className="text-[12px] text-[var(--fg-secondary)]">🏆 {u.points} баллов</p>
</div>
{!u.is_admin && (
<button onClick={() => { if (confirm('Удалить пользователя и все его фото?')) deleteUserMutation.mutate(u.id); }}
className="btn-icon h-8 w-8 text-[var(--fg-tertiary)] hover:text-[var(--danger)] hover:bg-[var(--danger-light)]">
<Trash2 className="h-3.5 w-3.5" strokeWidth={1.5} />
</button>
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -13,7 +13,7 @@ export default function CatPage() {
const { id } = useParams<{ id: string }>();
const catId = parseInt(id!);
const navigate = useNavigate();
const { user, updateUser } = useAuth();
const { user } = useAuth();
const { toast } = useToast();
const { data, isLoading, isError } = useCat(catId);
@@ -34,11 +34,8 @@ export default function CatPage() {
if (isOwner || !user || !cat) return;
if (isLiked) {
setLiked(false);
try {
const res = await unlikeMutation.mutateAsync();
if (res.ownerPoints !== undefined && cat.user_id === user.id) updateUser({ ...user, points: res.ownerPoints });
refetchLike();
} catch { setLiked(true); }
try { await unlikeMutation.mutateAsync(); refetchLike(); }
catch { setLiked(true); }
} else {
setLiked(true);
toast('Нравится ❤️');

View File

@@ -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 { ArrowLeft, Plus, LogOut, Heart } from 'lucide-react';
import { ArrowLeft, Plus, LogOut, Heart, Shield } from 'lucide-react';
export default function ProfilePage() {
const { id: paramId } = useParams<{ id?: string }>();
@@ -75,11 +75,18 @@ export default function ProfilePage() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h1 className="text-[20px] font-bold truncate">@{profileUser?.username}</h1>
{isMe && (
<button onClick={logout} className="btn-icon h-8 w-8 text-[var(--fg-secondary)] hover:text-[var(--danger)] hover:bg-[var(--danger-light)] ml-auto" title="Выйти">
<LogOut className="h-4 w-4" strokeWidth={1.5} />
</button>
)}
<div className="flex items-center gap-1 ml-auto">
{isMe && me?.is_admin && (
<Link to="/admin" className="btn-icon h-8 w-8 text-[var(--primary)] hover:bg-[var(--primary-light)]" title="Админ-панель">
<Shield className="h-4 w-4" strokeWidth={1.5} />
</Link>
)}
{isMe && (
<button onClick={logout} className="btn-icon h-8 w-8 text-[var(--fg-secondary)] hover:text-[var(--danger)] hover:bg-[var(--danger-light)]" title="Выйти">
<LogOut className="h-4 w-4" strokeWidth={1.5} />
</button>
)}
</div>
</div>
<div className="flex items-center gap-6 text-[14px] mb-3">

View File

@@ -36,7 +36,7 @@ export default function UploadPage() {
try {
const result = await uploadMutation.mutateAsync(fd);
if (user) updateUser({ ...user, points: (user.points || 0) + 10 });
toast('+10 баллов 🎉');
toast('+1 балл 🎉');
navigate(`/cat/${result.cat.id}`);
} catch { toast('Ошибка загрузки', 'error'); }
};
@@ -98,7 +98,7 @@ export default function UploadPage() {
<div className="flex items-center justify-between pt-3 border-t">
<div className="flex items-center gap-2 text-[13px] text-[var(--fg-secondary)]">
<span>🏆</span>
<span className="font-medium">+10 баллов</span>
<span className="font-medium">+1 балл</span>
<span className="text-[var(--fg-tertiary)]">за публикацию</span>
</div>
<Button onClick={handleUpload} disabled={uploadMutation.isPending}

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/storycircle.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/uselikes.ts","./src/lib/auth.ts","./src/lib/utils.ts","./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/storycircle.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/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"}

View File

@@ -2,6 +2,7 @@ import initSqlJs, { Database as SqlJsDatabase } from 'sql.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import bcrypt from 'bcryptjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dbPath = path.join(__dirname, '..', 'data.db');
@@ -26,10 +27,22 @@ export async function initDb(): Promise<SqlJsDatabase> {
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
points INTEGER DEFAULT 0,
is_admin INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// Add is_admin column if missing (existing DB migration)
try { db.run('ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0'); } catch {}
// Auto-create admin if not exists
const admin = queryOne('SELECT id FROM users WHERE username = ?', ['admin']);
if (!admin) {
const hash = bcrypt.hashSync('admin123', 10);
execute('INSERT INTO users (username, password_hash, is_admin) VALUES (?, ?, 1)', ['admin', hash]);
console.log('Admin user created: admin / admin123');
}
db.run(`
CREATE TABLE IF NOT EXISTS cats (
id INTEGER PRIMARY KEY AUTOINCREMENT,

View File

@@ -7,6 +7,7 @@ import { initDb } from './db.js';
import authRoutes from './routes/auth.js';
import catRoutes from './routes/cats.js';
import likeRoutes from './routes/likes.js';
import adminRoutes from './routes/admin.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -24,6 +25,7 @@ async function main() {
app.use('/api/auth', authRoutes);
app.use('/api/cats', catRoutes);
app.use('/api/likes', likeRoutes);
app.use('/api/admin', adminRoutes);
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok' });

View File

@@ -1,15 +1,17 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { queryOne } from '../db.js';
const JWT_SECRET = process.env.JWT_SECRET || 'catstagram-dev-secret-key-change-in-production';
export interface AuthRequest extends Request {
userId?: number;
username?: string;
isAdmin?: boolean;
}
export function generateToken(userId: number, username: string): string {
return jwt.sign({ userId, username }, JWT_SECRET, { expiresIn: '7d' });
export function generateToken(userId: number, username: string, isAdmin: boolean): string {
return jwt.sign({ userId, username, isAdmin }, JWT_SECRET, { expiresIn: '7d' });
}
export function authMiddleware(req: AuthRequest, res: Response, next: NextFunction): void {
@@ -21,11 +23,20 @@ export function authMiddleware(req: AuthRequest, res: Response, next: NextFuncti
const token = header.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET) as { userId: number; username: string };
const decoded = jwt.verify(token, JWT_SECRET) as { userId: number; username: string; isAdmin: boolean };
req.userId = decoded.userId;
req.username = decoded.username;
req.isAdmin = decoded.isAdmin;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
export function adminOnly(req: AuthRequest, res: Response, next: NextFunction): void {
if (!req.isAdmin) {
res.status(403).json({ error: 'Доступ только администратору' });
return;
}
next();
}

View File

@@ -0,0 +1,48 @@
import { Router, Response } from 'express';
import { queryAll, queryOne, execute } from '../db';
import { authMiddleware, adminOnly, AuthRequest } from '../middleware/auth.js';
const router = Router();
// All routes require admin
router.use(authMiddleware, adminOnly);
// Get all users
router.get('/users', (req: AuthRequest, res: Response) => {
const users = queryAll(
'SELECT id, username, points, is_admin, created_at FROM users ORDER BY points DESC'
);
res.json({ users });
});
// Delete any cat
router.delete('/cats/:id', (req: AuthRequest, res: Response) => {
const cat = queryOne('SELECT id FROM cats WHERE id = ?', [req.params.id]);
if (!cat) {
res.status(404).json({ error: 'Cat not found' });
return;
}
execute('DELETE FROM likes WHERE cat_id = ?', [req.params.id]);
execute('DELETE FROM cats WHERE id = ?', [req.params.id]);
res.json({ ok: true });
});
// Ban user (delete their cats and likes)
router.delete('/users/:id', (req: AuthRequest, res: Response) => {
const userId = parseInt(req.params.id);
const user = queryOne('SELECT id, username, is_admin FROM users WHERE id = ?', [userId]);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
if (user.is_admin) {
res.status(400).json({ error: 'Cannot delete admin' });
return;
}
execute('DELETE FROM likes WHERE cat_id IN (SELECT id FROM cats WHERE user_id = ?)', [userId]);
execute('DELETE FROM cats WHERE user_id = ?', [userId]);
execute('DELETE FROM users WHERE id = ?', [userId]);
res.json({ ok: true });
});
export default router;

View File

@@ -27,32 +27,32 @@ router.post('/register', (req: Request, res: Response) => {
const password_hash = bcrypt.hashSync(password, 10);
execute('INSERT INTO users (username, password_hash) VALUES (?, ?)', [username, password_hash]);
const user = queryOne('SELECT id, username, points FROM users WHERE username = ?', [username]);
const user = queryOne('SELECT id, username, points, is_admin FROM users WHERE username = ?', [username]);
const token = generateToken(user.id, user.username);
res.status(201).json({ token, user });
const token = generateToken(user.id, user.username, !!user.is_admin);
res.status(201).json({ token, user: { id: user.id, username: user.username, points: user.points, is_admin: !!user.is_admin } });
});
router.post('/login', (req: Request, res: Response) => {
const { username, password } = req.body;
const user = queryOne('SELECT id, username, password_hash, points FROM users WHERE username = ?', [username]);
const user = queryOne('SELECT id, username, password_hash, points, is_admin FROM users WHERE username = ?', [username]);
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
res.status(401).json({ error: 'Неверное имя пользователя или пароль' });
return;
}
const token = generateToken(user.id, user.username);
res.json({ token, user: { id: user.id, username: user.username, points: user.points } });
const token = generateToken(user.id, user.username, !!user.is_admin);
res.json({ token, user: { id: user.id, username: user.username, points: user.points, is_admin: !!user.is_admin } });
});
router.get('/me', authMiddleware, (req: AuthRequest, res: Response) => {
const user = queryOne('SELECT id, username, points, created_at FROM users WHERE id = ?', [req.userId]);
const user = queryOne('SELECT id, username, points, is_admin, created_at FROM users WHERE id = ?', [req.userId]);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json({ user });
res.json({ user: { id: user.id, username: user.username, points: user.points, is_admin: !!user.is_admin, created_at: user.created_at } });
});
export default router;

View File

@@ -99,7 +99,7 @@ router.post('/', authMiddleware, upload.single('image'), (req: AuthRequest, res:
[req.userId, image_url, caption]
);
execute('UPDATE users SET points = points + 10 WHERE id = ?', [req.userId]);
execute('UPDATE users SET points = points + 1 WHERE id = ?', [req.userId]);
const cat = queryOne(
`SELECT c.id, c.image_url, c.caption, c.created_at, c.user_id,
@@ -120,7 +120,7 @@ router.delete('/:id', authMiddleware, (req: AuthRequest, res: Response) => {
res.status(404).json({ error: 'Cat not found' });
return;
}
if (cat.user_id !== req.userId) {
if (cat.user_id !== req.userId && !req.isAdmin) {
res.status(403).json({ error: 'Not your cat' });
return;
}

View File

@@ -26,12 +26,10 @@ router.post('/:catId', authMiddleware, (req: AuthRequest, res: Response) => {
}
execute('INSERT INTO likes (cat_id, user_id) VALUES (?, ?)', [catId, userId]);
execute('UPDATE users SET points = points + 1 WHERE id = ?', [cat.user_id]);
const countResult = queryOne('SELECT COUNT(*) as count FROM likes WHERE cat_id = ?', [catId]);
const owner = queryOne('SELECT points FROM users WHERE id = ?', [cat.user_id]);
res.json({ liked: true, likesCount: countResult?.count || 0, ownerPoints: owner?.points || 0 });
res.json({ liked: true, likesCount: countResult?.count || 0 });
});
router.delete('/:catId', authMiddleware, (req: AuthRequest, res: Response) => {
@@ -51,12 +49,10 @@ router.delete('/:catId', authMiddleware, (req: AuthRequest, res: Response) => {
}
execute('DELETE FROM likes WHERE id = ?', [existing.id]);
execute('UPDATE users SET points = MAX(0, points - 1) WHERE id = ?', [cat.user_id]);
const countResult = queryOne('SELECT COUNT(*) as count FROM likes WHERE cat_id = ?', [catId]);
const owner = queryOne('SELECT points FROM users WHERE id = ?', [cat.user_id]);
res.json({ liked: false, likesCount: countResult?.count || 0, ownerPoints: owner?.points || 0 });
res.json({ liked: false, likesCount: countResult?.count || 0 });
});
router.get('/:catId/status', authMiddleware, (req: AuthRequest, res: Response) => {