Compare commits

..

2 Commits

7 changed files with 12 additions and 80 deletions

View File

@@ -33,11 +33,10 @@ function AnimatedRoutes() {
if (el) { if (el) {
el.classList.remove('page-enter-active'); el.classList.remove('page-enter-active');
el.classList.add('page-enter'); el.classList.add('page-enter');
const rafId = requestAnimationFrame(() => { requestAnimationFrame(() => {
el.classList.remove('page-enter'); el.classList.remove('page-enter');
el.classList.add('page-enter-active'); el.classList.add('page-enter-active');
}); });
return () => cancelAnimationFrame(rafId);
} }
}, [location.pathname]); }, [location.pathname]);

View File

@@ -11,18 +11,12 @@ interface CatCardProps {
cat: Cat; cat: Cat;
onOpen: (id: number) => void; onOpen: (id: number) => void;
likedIds?: Set<number>; likedIds?: Set<number>;
priority?: boolean;
} }
export default function CatCard({ cat, onOpen, likedIds, priority }: CatCardProps) { export default function CatCard({ cat, onOpen, likedIds }: CatCardProps) {
const { user } = useAuth(); const { user } = useAuth();
const [liked, setLiked] = useState(() => likedIds?.has(cat.id) ?? false); const [liked, setLiked] = useState(() => likedIds?.has(cat.id) ?? false);
const [likeCount, setLikeCount] = useState(cat.likes_count); const [likeCount, setLikeCount] = useState(cat.likes_count);
const [imgLoaded, setImgLoaded] = useState(false);
const thumbSrc = cat.image_url.endsWith('.gif')
? cat.image_url
: cat.image_url.replace(/\.jpg$/, '_thumb.jpg');
const { toast } = useToast(); const { toast } = useToast();
const likeMutation = useLikeCat(cat.id); const likeMutation = useLikeCat(cat.id);
const unlikeMutation = useUnlikeCat(cat.id); const unlikeMutation = useUnlikeCat(cat.id);
@@ -81,16 +75,13 @@ export default function CatCard({ cat, onOpen, likedIds, priority }: CatCardProp
</div> </div>
{/* Изображение */} {/* Изображение */}
<figure className="cat-card-image relative min-h-[200px] bg-[var(--border-light)]"> <figure className="cat-card-image">
<img <img
src={thumbSrc} src={cat.image_url}
alt={cat.caption || 'Фото кота'} alt={cat.caption || 'Фото кота'}
className={`w-full max-h-[540px] object-contain select-none mx-auto cat-card-img transition-opacity duration-300 ${imgLoaded ? 'opacity-100' : 'opacity-0'}`} className="w-full max-h-[540px] object-contain select-none mx-auto cat-card-img"
draggable={false} draggable={false}
loading={priority ? 'eager' : 'lazy'} loading="lazy"
decoding="async"
onLoad={() => setImgLoaded(true)}
onError={e => { e.currentTarget.src = cat.image_url; }}
/> />
</figure> </figure>

View File

@@ -103,8 +103,8 @@ export default function FeedPage() {
{allCats.length > 0 && ( {allCats.length > 0 && (
<> <>
<div className="space-y-5"> <div className="space-y-5">
{allCats.map((cat, index) => ( {allCats.map((cat) => (
<CatCard key={cat.id} cat={cat} onOpen={setModalCatId} likedIds={likedIds} priority={index < 3} /> <CatCard key={cat.id} cat={cat} onOpen={setModalCatId} likedIds={likedIds} />
))} ))}
</div> </div>

View File

@@ -268,6 +268,7 @@ export default function ProfilePage() {
<div className="divider mb-4" /> <div className="divider mb-4" />
<div> <div>
<input type="file" ref={fileInputRef} onChange={onFileChange} accept="image/*" className="hidden" />
<Button <Button
variant="outline" variant="outline"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}

View File

@@ -54,16 +54,9 @@ async function main() {
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
}); });
// Rate-limit only cat uploads (POST), not reads app.use('/api/cats', uploadLimiter);
app.use('/api/cats', (req, _res, next) => {
if (req.method !== 'POST') return next();
return uploadLimiter(req, _res, next);
});
app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads'), { app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads')));
maxAge: '365d',
immutable: true,
}));
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
app.use('/api/cats', catRoutes); app.use('/api/cats', catRoutes);

View File

@@ -127,17 +127,8 @@ router.post('/', authMiddleware, upload.single('image'), async (req: AuthRequest
finalFilename = req.file.filename.replace(/\.[^.]+$/, '.jpg'); finalFilename = req.file.filename.replace(/\.[^.]+$/, '.jpg');
finalPath = path.join(uploadsDir, finalFilename); finalPath = path.join(uploadsDir, finalFilename);
try { try {
await sharp(origPath) await sharp(origPath).jpeg({ quality: 85 }).toFile(finalPath);
.resize({ width: 1200, withoutEnlargement: true })
.jpeg({ quality: 85 })
.toFile(finalPath);
if (origPath !== finalPath) fs.unlinkSync(origPath); if (origPath !== finalPath) fs.unlinkSync(origPath);
const thumbFilename = finalFilename.replace(/\.jpg$/, '_thumb.jpg');
await sharp(finalPath)
.resize({ width: 600, withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(path.join(uploadsDir, thumbFilename));
} catch { } catch {
try { fs.unlinkSync(origPath); } catch {} try { fs.unlinkSync(origPath); } catch {}
res.status(400).json({ error: 'Не удалось обработать изображение. Попробуйте другой формат.' }); res.status(400).json({ error: 'Не удалось обработать изображение. Попробуйте другой формат.' });

View File

@@ -1,43 +0,0 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import sharp from 'sharp';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const uploadsDir = path.join(__dirname, '..', '..', 'uploads');
async function main() {
const files = fs.readdirSync(uploadsDir);
const candidates = files.filter(
f => f.endsWith('.jpg') && !f.endsWith('_thumb.jpg')
);
console.log(`Found ${candidates.length} images to process`);
let created = 0;
let skipped = 0;
for (const filename of candidates) {
const thumbFilename = filename.replace(/\.jpg$/, '_thumb.jpg');
const thumbPath = path.join(uploadsDir, thumbFilename);
if (fs.existsSync(thumbPath)) {
skipped++;
continue;
}
try {
await sharp(path.join(uploadsDir, filename))
.resize({ width: 600, withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(thumbPath);
created++;
console.log(`${thumbFilename}`);
} catch (err) {
console.error(`${filename}: ${err}`);
}
}
console.log(`\nDone: ${created} created, ${skipped} already existed`);
}
main().catch(console.error);