Creating Post Page in MERN Stack Social Media Platform

Last Updated : 23 Sep, 2024

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

JavaScript
// 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); });
JavaScript
// 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);
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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.js

Frontend Example

JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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 dev


Output:

Comment

Explore