From 9f5e57d8e654a66b64e4633127919288864fec1b Mon Sep 17 00:00:00 2001 From: krisf Date: Sat, 1 Aug 2026 11:48:55 -0400 Subject: [PATCH] Add Express backend server with API routes --- server.js | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 server.js diff --git a/server.js b/server.js new file mode 100644 index 0000000..c37b1e4 --- /dev/null +++ b/server.js @@ -0,0 +1,202 @@ +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}`); +});