const express = require('express'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { v4: uuidv4 } = require('uuid'); const cors = require('cors'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); app.use(express.static('public')); app.use('/uploads', express.static(path.join(__dirname, 'public', 'uploads'))); // In-memory data store let posts = []; let votes = {}; let comments = []; // Multer config for file uploads const storage = multer.diskStorage({ destination: (req, file, cb) => { const uploadDir = path.join(__dirname, 'public', 'uploads'); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); cb(null, uploadDir); }, filename: (req, file, cb) => { const id = uuidv4().slice(0, 8); const ext = path.extname(file.originalname); cb(null, `${id}${ext}`); } }); const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; const upload = multer({ storage, limits: { fileSize: 20 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (ALLOWED_TYPES.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Unsupported file type. Only JPEG, PNG, GIF, and WebP are allowed.'), false); } } }); // Load seed data function loadSeedData() { const seedPath = path.join(__dirname, 'seed', 'data.js'); if (fs.existsSync(seedPath)) { const seed = require(seedPath); posts = seed.posts.map(p => ({ ...p, id: p.id, createdAt: new Date().toISOString() })); comments = seed.comments || []; } } // --- API Routes --- // GET /api/posts - List all posts with optional sort app.get('/api/posts', (req, res) => { const { sort = 'new' } = req.query; let sorted = [...posts]; switch (sort) { case 'viral': sorted.sort((a, b) => (b.upvotes || 0) - (a.upvotes || 0)); break; case 'top': const today = new Date(); today.setHours(0, 0, 0, 0); const todayPosts = posts.filter(p => new Date(p.createdAt) >= today); sorted = todayPosts.sort((a, b) => (b.upvotes || 0) - (a.upvotes || 0)); break; case 'user': sorted.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); break; default: sorted.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } sorted = sorted.map(p => ({ ...p, commentCount: comments.filter(c => c.postId === p.id).length })); res.json(sorted); }); // GET /api/posts/:id - Single post app.get('/api/posts/:id', (req, res) => { const post = posts.find(p => p.id === req.params.id); if (!post) return res.status(404).json({ error: 'Post not found' }); const commentCount = comments.filter(c => c.postId === post.id).length; res.json({ ...post, commentCount }); }); // POST /api/posts - Create new post app.post('/api/posts', upload.single('image'), (req, res) => { if (!req.file) { return res.status(400).json({ error: 'No image file provided' }); } const post = { id: uuidv4().slice(0, 8), title: req.body.title || 'Untitled', url: `/uploads/${req.file.filename}`, upvotes: 1, views: 0, tags: req.body.tags ? req.body.tags.split(',').map(t => t.trim()).filter(Boolean) : [], createdAt: new Date().toISOString(), author: req.body.author || 'Anonymous' }; posts.unshift(post); res.status(201).json(post); }); // POST /api/posts/:id/vote - Vote on a post app.post('/api/posts/:id/vote', (req, res) => { const post = posts.find(p => p.id === req.params.id); if (!post) return res.status(404).json({ error: 'Post not found' }); const { direction } = req.body; const userId = req.body.userId || 'anonymous'; if (direction === 'up') { post.upvotes = (post.upvotes || 0) + 1; } else if (direction === 'down') { post.upvotes = Math.max(0, (post.upvotes || 0) - 1); } post.views = (post.views || 0) + 1; res.json({ upvotes: post.upvotes, views: post.views }); }); // GET /api/comments/:postId - Get comments for a post app.get('/api/comments/:postId', (req, res) => { const postComments = comments.filter(c => c.postId === req.params.postId); res.json(postComments); }); // POST /api/comments - Create a comment app.post('/api/comments', (req, res) => { const { postId, text, author, parentId } = req.body; if (!postId || !text) { return res.status(400).json({ error: 'postId and text are required' }); } const comment = { id: uuidv4().slice(0, 8), postId, text, author: author || 'Anonymous', parentId: parentId || null, timestamp: new Date().toISOString() }; comments.push(comment); res.status(201).json(comment); }); // POST /api/upload - File upload endpoint app.post('/api/upload', upload.single('image'), (req, res) => { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } const post = { id: uuidv4().slice(0, 8), title: req.body.title || 'Untitled', url: `/uploads/${req.file.filename}`, upvotes: 1, views: 0, tags: req.body.tags ? req.body.tags.split(',').map(t => t.trim()).filter(Boolean) : [], createdAt: new Date().toISOString(), author: req.body.author || 'You' }; posts.unshift(post); res.status(201).json(post); }); // Health check app.get('/api/health', (req, res) => { res.json({ status: 'ok', posts: posts.length, comments: comments.length, uptime: process.uptime() }); }); // Initialize and start loadSeedData(); console.log(`🚀 Imgur Clone running on port ${PORT}`); console.log(`📦 ${posts.length} seed posts loaded`); // Listen on both IPv4 and IPv6 for health check compatibility const server = app.listen(PORT, () => { console.log(`✅ Server listening on http://0.0.0.0:${PORT}`); console.log(`✅ Server listening on http://[::]:${PORT}`); });