Add Express backend server with API routes
This commit is contained in:
@@ -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}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user