Post creation is one of the most essential features of social media platforms, enabling users to share content and interact with others. This process involves several stages, from designing a user-friendly interface to distributing the content across the platform. Here is a detailed look at each step:
Key Features
The UI is the first point of interaction for the user when creating a post. The design should be simple and intuitive, allowing users to focus on their content. Common elements in a post-creation UI include:
- Text Input Field: Allows users to type a message or caption.
- Media Upload Options: Users can attach images, videos, GIFs, or links. This often involves a drag-and-drop feature or file selector.
- Formatting Options: Some platforms allow for text formatting, like bold, italics, hashtags, or even markdown in certain cases.
- Tagging & Mentions: Users can tag other profiles or use @mentions to include people or entities in the post.
- Privacy & Sharing Settings: Users can set the visibility of their posts (e.g., public, friends only, or private groups).
- The goal of the UI is to make content creation as seamless and engaging as possible while offering users all the tools they need for rich interaction.
- Feeds & Timelines: The post will appear in the user’s feed and the feeds of others based on algorithms that factor in relevance, user interest, engagement rates, and recency.
- Notification Systems: Followers or friends of the user may receive notifications about the new post, especially if they are tagged.
Approach to Implement Social Media Platforms: Post Creation
Backend
- Use Express.js to handle API routes, with Mongoose for database interaction (MongoDB).
- Define CORS and cookieParser middleware for cross-origin requests and cookie handling.
- Implement a Socket.io server for real-time communication.
- Set up Multer for image uploads with file type validation and size limits.
- Use an auth middleware for route protection, ensuring only authenticated users can create posts.
- Create a Post model to store content, images, likes, comments, and user information.
- Handle CRUD operations for posts (create, read, update, delete) in the post controller.
- Implement additional features like likes, comments, post discovery, and saving posts for better user engagement.
Frontend
- State management: Use useState to handle form fields (content, images) and user authentication (user, token).
- Form handling: Use FormData in CreatePost for multipart form submission via Axios, with proper error handling and input validation.
- UI design: Use Material UI components (AppBar, Button, TextField, etc.) for responsive layout and functionality, ensuring the design is mobile-friendly.
- Authentication: Use localStorage for token storage, checking if the user is logged in (Navbar).
- Routing: Use react-router-dom to handle navigation and link management within the app.
Note: All Steps are previously defined.
Backend Example
// index.js
require("dotenv").config()
const express = require("express");
const mongoose = require("mongoose");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const SocketServer = require("./socketServer");
const app = express();
app.use(express.json())
app.use(cors({
origin: [, "http://localhost:5173"],
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
}));
app.use(cookieParser())
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
next();
})
//#region // !Socket
const http = require("http").createServer(app);
const io = require("socket.io")(http);
io.on("connection", socket => { SocketServer(socket); })
//#endregion
app.get("/",
(req, res) => {
res.send(
"Hi Welcome to Social Media App API.....")
})
//#region // !Routes
app.use("/api", require("./routes/authRouter"));
app.use("/api", require("./routes/userRouter"));
app.use("/api", require("./routes/postRouter"));
//#endregion
const URI = process.env.MONGO_URI
mongoose.connect(URI, {
useCreateIndex: true,
useFindAndModify: false,
useNewUrlParser: true,
useUnifiedTopology: true
},
err => {
if (err)
throw err;
console.log("Database Connected!!")
})
const port = process.env.PORT || 3001;
http.listen(port,
() => { console.log("Listening on ", port); });
// models/userModel.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const userSchema = new Schema(
{
fullname: {
type: String,
required: true,
trim: true,
maxlength: 25,
},
username: {
type: String,
required: true,
trim: true,
maxlength: 25,
unique: true,
},
email: {
type: String,
required: true,
trim: true,
unique: true,
},
password: {
type: String,
required: true,
},
avatar: {
type: String,
default:
"https://media.geeksforgeeks.org/wp-content/uploads/20240923184628/blank-profile-picture-973460__340.webp",
},
role: {
type: String,
default: "user",
},
gender: {
type: String,
default: "male",
},
mobile: {
type: String,
default: "",
},
address: {
type: String,
default: "",
},
saved: [
{
type: mongoose.Types.ObjectId,
ref: 'post'
}
],
story: {
type: String,
default: "",
maxlength: 200,
},
website: {
type: String,
default: "",
},
followers: [
{
type: mongoose.Types.ObjectId,
ref: "user",
},
],
following: [
{
type: mongoose.Types.ObjectId,
ref: "user",
},
],
},
{
timestamps: true,
}
);
module.exports = mongoose.model('user', userSchema);
// controllers/postCtrl.js
const Posts = require("../models/postModel");
const Comments = require("../models/commentModel");
const Users = require("../models/userModel");
class APIfeatures {
constructor(query, queryString) {
this.query = query;
this.queryString = queryString;
}
paginating() {
const page = this.queryString.page * 1 || 1;
const limit = this.queryString.limit * 1 || 9;
const skip = (page - 1) * limit;
this.query = this.query.skip(skip).limit(limit);
return this;
}
}
const postCtrl = {
createPost: async (req, res) => {
try {
const { content } = req.body;
// If no files (images) were uploaded
if (!req.files || req.files.length === 0) {
return res.status(400).json(
{ msg: "Please add photo(s)" });
}
// Extract image paths from uploaded files
const imagePaths
= req.files.map(file => file.path);
// Create a new post with the content and image
// paths
const newPost = new Posts({
content,
images: imagePaths,
user: req.user._id, // Assuming req.user is
// populated from auth
// middleware
});
// Save the new post to the database
await newPost.save();
// Return a response with the created post data
res.json({
msg: "Post created successfully.",
newPost: {
...newPost._doc,
user: req.user, // Include user data in
// the response
},
});
}
catch (err) {
return res.status(500).json(
{ msg: err.message });
}
},
module.exports = postCtrl;
// routes/postRoutes.js
const router = require("express").Router();
const auth = require("../middleware/auth");
const postCtrl = require("../controllers/postCtrl");
const upload = require("../middleware/upload");
router.route("/posts")
.post(auth,upload.array('images', 5), postCtrl.createPost)
.get(auth, postCtrl.getPosts);
module.exports = router;
// middleware/upload.js
const multer = require('multer');
const path = require('path');
// Define Storage for Multer
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // Upload destination folder
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
},
});
// File filter to only allow image types
const fileFilter = (req, file, cb) => {
const filetypes = /jpeg|jpg|png|gif/;
const mimetype = filetypes.test(file.mimetype);
const extname = filetypes.
test(path.extname(file.originalname).toLowerCase());
if (mimetype && extname) {
return cb(null, true);
}
cb(new Error('Only images (jpeg, jpg, png, gif) are allowed'));
};
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB limit
fileFilter: fileFilter,
});
// Exporting the Multer upload function
module.exports = upload;
Start Your application using the below command:
node index.jsFrontend Example
// App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import UpdateProfile from './components/UpdateProfile.jsx';
import Register from './components/Register';
import Login from './components/Login';
import Navbar from './components/Navbar';
import HomePage from './pages/HomePage';
import CreatePost from './pages/CreatePost.jsx';
const App = () => {
return (
<Router>
<Navbar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/post" element={<CreatePost />} />
<Route path="/user/:id" element={<UserProfile />} />
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
</Routes>
</Router>
);
};
export default App;
// components/Navbar.jsx
import {
AccountCircle,
Notifications,
Search
} from "@mui/icons-material";
import {
AppBar,
Avatar,
Badge,
Button,
IconButton,
InputBase,
Menu,
MenuItem,
Toolbar,
Typography
} from "@mui/material";
import { alpha, styled } from "@mui/material/styles";
import React, { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
const SearchBar = styled("div")(
({ theme }) => ({
position: "relative",
borderRadius: theme.shape.borderRadius,
backgroundColor:
alpha(theme.palette.common.white, 0.15),
"&:hover": {
backgroundColor:
alpha(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing(2),
marginLeft: 0,
width: "100%",
[theme.breakpoints.up("sm")]: {
marginLeft: theme.spacing(3),
width: "auto",
},
}));
const SearchIconWrapper
= styled("div")(({ theme }) => ({
padding: theme.spacing(0, 2),
height: "100%",
position: "absolute",
pointerEvents: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
}));
const StyledInputBase = styled(InputBase)(
({ theme }) => ({
color: "inherit",
"& .MuiInputBase-input": {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create("width"),
width: "100%",
[theme.breakpoints.up("md")]: {
width: "20ch",
},
},
}));
const Navbar = () => {
const [anchorEl, setAnchorEl] = useState(null);
const [user, setUser] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const loggedUser = localStorage.getItem("user");
if (loggedUser) {
setUser(JSON.parse(loggedUser));
}
}, []);
const handleProfileMenuOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleLogout = () => {
localStorage.removeItem("user"); // Remove user from localStorage
localStorage.removeItem("token"); // Remove token from localStorage
setUser(null); // Set user state to null
navigate("/login"); // Redirect to login page
};
const isMenuOpen = Boolean(anchorEl);
const menuId = "primary-search-account-menu";
return (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Social Media App
</Typography>
{/* Search Bar */}
<SearchBar>
<SearchIconWrapper>
<Search />
</SearchIconWrapper>
<StyledInputBase
placeholder="Search…"
inputProps={{ 'aria-label': 'search' }}
/>
</SearchBar>
{/* Navigation Links */}
<Button color="inherit" component={Link} to="/">Home</Button>
<Button color="inherit" component={Link} to="/post">Create Post</Button>
<Button color="inherit" component={Link} to="/user-posts">My Posts</Button>
<Button color="inherit" component={Link} to="/saved-posts">Saved Posts</Button>
{/* Notifications */}
<IconButton color="inherit">
<Badge badgeContent={4} color="error">
<Notifications />
</Badge>
</IconButton>
{user?.fullname ? (
<>
{/* User Profile Dropdown */}
<IconButton
edge="end"
aria-label="account of current user"
aria-controls={menuId}
aria-haspopup="true"
onClick={handleProfileMenuOpen}
color="inherit"
>
<AccountCircle />
</IconButton>
{/* Dropdown Menu */}
<Menu
anchorEl={anchorEl}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
id={menuId}
keepMounted
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
open={isMenuOpen}
onClose={handleMenuClose}
>
<MenuItem component={Link} to={`/user/${user._id}`}>Profile</MenuItem>
<MenuItem component={Link} to="/settings">Settings</MenuItem>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu>
</>
) : (
<Button color="inherit" component={Link} to="/login">Login</Button>
)}
</Toolbar>
</AppBar>
);
};
export default Navbar;
// src/pages/CreatePost.jsx
import { AddAPhoto } from "@mui/icons-material";
import {
Avatar,
Box,
Button,
Grid,
IconButton,
Paper,
TextField,
Typography
} from "@mui/material";
import axios from "axios";
import React, { useState } from "react";
const CreatePost = () => {
const [content, setContent] = useState("");
const [images, setImages] = useState([]);
const token = localStorage.getItem("token");
const handleImageChange = (e) => {
const files = Array.from(e.target.files);
setImages([...images, ...files]);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!images || images.length === 0) {
alert("Please add photo(s)");
return;
}
const formData = new FormData();
formData.append("content", content);
images.forEach((image) => {
formData.append("images", image);
});
try {
const res = await axios.post("http://localhost:3001/api/posts", formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
});
alert(res.data.msg);
setContent("");
setImages([]);
} catch (err) {
console.error(err.response.data.msg || "An error occurred.");
}
};
return (
<Box sx={{ my: 4, display: "flex", justifyContent: "center" }}>
<Paper
component="form"
onSubmit={handleSubmit}
elevation={3}
sx={{
width: "100%",
maxWidth: 600,
p: 3,
borderRadius: 3,
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
}}
>
<Grid container spacing={2}>
<Grid item>
<Avatar alt="User Profile" src="/profile-pic.jpg"
sx={{ width: 56, height: 56 }} />
</Grid>
<Grid item xs>
<Typography variant="h6" gutterBottom>
Create a New Post
</Typography>
<TextField
label="What's on your mind?"
variant="outlined"
fullWidth
multiline
rows={4}
value={content}
onChange={(e) => setContent(e.target.value)}
sx={{ mb: 2, backgroundColor: '#f9f9f9', borderRadius: 2 }}
/>
<Button
variant="outlined"
component="label"
fullWidth
sx={{
color: "#1976d2",
borderColor: "#1976d2",
textTransform: "none",
mb: 2,
"&:hover": {
borderColor: "#005bb5",
backgroundColor: "rgba(25, 118, 210, 0.04)",
},
}}
startIcon={<AddAPhoto />}
>
Add Photo
<input type="file" multiple accept="image/*"
hidden onChange={handleImageChange} />
</Button>
{images.length > 0 && (
<Box
sx={{
display: "flex",
gap: 1,
flexWrap: "wrap",
mt: 2,
mb: 2,
}}
>
{images.map((image, index) => (
<img
key={index}
src={URL.createObjectURL(image)}
alt="Preview"
style={{
width: "80px",
height: "80px",
objectFit: "cover",
borderRadius: "10px",
border: "1px solid #ddd",
}}
/>
))}
</Box>
)}
<Button
variant="contained"
color="primary"
type="submit"
fullWidth
sx={{
borderRadius: 2,
p: 1.5,
fontSize: "1rem",
textTransform: "none",
boxShadow: "0 3px 8px rgba(25, 118, 210, 0.3)",
"&:hover": {
backgroundColor: "#005bb5",
},
}}
>
Post
</Button>
</Grid>
</Grid>
</Paper>
</Box>
);
};
export default CreatePost;
Start your frontend using the below command:
npm run devOutput: