Flask Social Media Clone Project code in Python

← Back to Projects

Flask Social Media Clone in Python

About the project: Creating a Flask application that simulates a social media platform requires handling forms, user sessions, routing, and basic data storage.

Since we are only providing a single file, we shall embed all the HTML and CSS directly within the Python script using Flask's render_template_string. This approach uses a simple in-memory structure (Python dictionaries and lists) to simulate the database, allowing the entire application to run from one app.py file.

You will need to install Flask and WTForms:


  pip install Flask Flask-WTF Werkzeug
  


Project Level: Advance

Here is the complete runnable Flask application:


import os
import uuid
from datetime import datetime
from flask import Flask, render_template_string, redirect, url_for, request, session, flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, EqualTo, Length, ValidationError
from werkzeug.security import generate_password_hash, check_password_hash

# --- 1. Flask Application Setup ---
app = Flask(__name__)
# IMPORTANT: Set a secret key for session management and WTForms
app.config['SECRET_KEY'] = 'a_very_secret_key_' + os.urandom(24).hex()

# --- 2. In-Memory Database (Simulated Firestore/SQL) ---
# Stores {user_id: {username, password_hash}}
USERS = {}
# Stores [{id, user_id, username, content, timestamp}]
POSTS = []

# --- 3. Form Definitions using WTForms ---

class RegistrationForm(FlaskForm):
    """Form for user registration."""
    username = StringField('Username', validators=[
        DataRequired(), 
        Length(min=4, max=20)
    ])
    password = PasswordField('Password', validators=[
        DataRequired(), 
        Length(min=6)
    ])
    confirm_password = PasswordField('Confirm Password', validators=[
        DataRequired(), 
        EqualTo('password', message='Passwords must match')
    ])
    submit = SubmitField('Sign Up')

    def validate_username(self, username):
        """Custom validator to check if username already exists."""
        for user_data in USERS.values():
            if user_data['username'] == username.data:
                raise ValidationError('That username is already taken. Please choose a different one.')

class LoginForm(FlaskForm):
    """Form for user login."""
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Log In')

class PostForm(FlaskForm):
    """Form for creating a new post."""
    content = TextAreaField('What\'s on your mind?', validators=[
        DataRequired(), 
        Length(max=280)
    ])
    submit = SubmitField('Post')

# --- 4. HTML Templates (Rendered using render_template_string) ---

BASE_LAYOUT = """
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }} | Flastagram</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
        :root {
            --primary: #3b82f6; /* Blue */
            --background: #f3f4f6; /* Light Gray */
            --card: #ffffff;
            --text: #1f2937; /* Dark Gray */
            --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
        }
        * { box-sizing: border-box; }
        body {
            font-family: 'Inter', sans-serif;
            background-color: var(--background);
            color: var(--text);
            margin: 0;
            padding: 0;
            display: flex;
            flex-direction: column;
            min-height: 100vh;
        }
        .navbar {
            background-color: var(--card);
            box-shadow: var(--shadow);
            padding: 1rem 1.5rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .navbar h1 a {
            color: var(--primary);
            font-size: 1.5rem;
            font-weight: 700;
            text-decoration: none;
        }
        .nav-links a, .nav-links span {
            margin-left: 1.5rem;
            color: var(--text);
            text-decoration: none;
            font-weight: 600;
            transition: color 0.2s;
        }
        .nav-links a:hover {
            color: var(--primary);
        }
        .container {
            width: 100%;
            max-width: 768px;
            margin: 2rem auto;
            padding: 0 1rem;
            flex-grow: 1;
        }
        .card {
            background-color: var(--card);
            border-radius: 0.5rem;
            box-shadow: var(--shadow);
            padding: 1.5rem;
            margin-bottom: 1.5rem;
        }
        .form-group {
            margin-bottom: 1rem;
        }
        label {
            display: block;
            margin-bottom: 0.5rem;
            font-weight: 600;
        }
        input[type="text"], input[type="password"], textarea {
            width: 100%;
            padding: 0.75rem;
            border: 1px solid #d1d5db;
            border-radius: 0.375rem;
            font-size: 1rem;
            font-family: inherit;
        }
        textarea {
            resize: vertical;
            min-height: 100px;
        }
        .btn-primary {
            background-color: var(--primary);
            color: white;
            padding: 0.75rem 1.5rem;
            border: none;
            border-radius: 0.375rem;
            cursor: pointer;
            font-weight: 600;
            transition: background-color 0.2s;
        }
        .btn-primary:hover {
            background-color: #2563eb;
        }
        .alert {
            padding: 0.75rem 1.5rem;
            border-radius: 0.375rem;
            margin-bottom: 1rem;
            font-weight: 600;
        }
        .alert-success {
            background-color: #d1fae5;
            color: #065f46;
        }
        .alert-danger {
            background-color: #fee2e2;
            color: #991b1b;
        }
        .post-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 0.75rem;
        }
        .post-author {
            font-weight: 700;
            color: var(--primary);
            text-decoration: none;
        }
        .post-time {
            font-size: 0.8rem;
            color: #6b7280;
        }
        .post-content {
            white-space: pre-wrap;
            line-height: 1.5;
        }
        .error-list {
            color: var(--primary);
            font-size: 0.875rem;
            list-style: none;
            padding-left: 0;
            margin-top: 0.5rem;
        }
    </style>
</head>
<body>
    <div class="navbar">
        <h1><a href="{{ url_for('feed') }}">Flastagram</a></h1>
        <div class="nav-links">
            {% if session.get('user_id') %}
                <span>Hello, {{ session.get('username') }}</span>
                <a href="{{ url_for('logout') }}" class="btn-primary">Logout</a>
            {% else %}
                Login
                Register
            {% endif %}
        </div>
    </div>
    <div class="container">
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                {% for category, message in messages %}
                    
{{ message }}
{% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %} </div> </body> </html> """ REGISTER_TEMPLATE = BASE_LAYOUT.replace('{% block content %}{% endblock %}', """ {% block content %} <div class="card"> <h2>Join Flastagram</h2> <form method="POST"> {{ form.hidden_tag() }} <div class="form-group"> {{ form.username.label }} {{ form.username(class="input") }} {% for error in form.username.errors %} <ul class="error-list"><li>{{ error }}</li></ul> {% endfor %} </div> <div class="form-group"> {{ form.password.label }} {{ form.password(class="input") }} {% for error in form.password.errors %} <ul class="error-list"><li>{{ error }}</li></ul> {% endfor %} </div> <div class="form-group"> {{ form.confirm_password.label }} {{ form.confirm_password(class="input") }} {% for error in form.confirm_password.errors %} <ul class="error-list"><li>{{ error }}</li></ul> {% endfor %} </div> {{ form.submit(class="btn-primary") }} </form> </div> {% endblock %} """) LOGIN_TEMPLATE = BASE_LAYOUT.replace('{% block content %}{% endblock %}', """ {% block content %} <div class="card"> <h2>Login to Flastagram</h2> <form method="POST"> {{ form.hidden_tag() }} <div class="form-group"> {{ form.username.label }} {{ form.username(class="input") }} </div> <div class="form-group"> {{ form.password.label }} {{ form.password(class="input") }} </div> {{ form.submit(class="btn-primary") }} </form> </div> {% endblock %} """) FEED_TEMPLATE = BASE_LAYOUT.replace('{% block content %}{% endblock %}', """ {% block content %} {% if session.get('user_id') %} <div class="card"> <h3>New Post</h3> <form method="POST"> {{ post_form.hidden_tag() }} <div class="form-group"> {{ post_form.content.label }} {{ post_form.content(class="input") }} {% for error in post_form.content.errors %} <ul class="error-list"><li>{{ error }}</li></ul> {% endfor %} </div> {{ post_form.submit(class="btn-primary") }} </form> </div> {% endif %} <h2 style="margin-top: 2.5rem; color: var(--primary);">Feed</h2> {% for post in posts %} <div class="card"> <div class="post-header"> <a href="#" class="post-author">@{{ post.username }}</a> <span class="post-time">{{ post.timestamp }}</span> </div> <div class="post-content">{{ post.content }}</div> </div> {% else %} <div class="card"> <p>No posts yet! Be the first one to share.</p> </div> {% endfor %} {% endblock %} """) # --- 5. Flask Routes --- @app.route('/') def index(): """Redirects base URL to the feed.""" return redirect(url_for('feed')) @app.route('/register', methods=['GET', 'POST']) def register(): """Handles user registration.""" form = RegistrationForm() if form.validate_on_submit(): user_id = str(uuid.uuid4()) hashed_password = generate_password_hash(form.password.data) USERS[user_id] = { 'username': form.username.data, 'password_hash': hashed_password, 'id': user_id } flash('Registration successful! You can now log in.', 'message') return redirect(url_for('login')) return render_template_string(REGISTER_TEMPLATE, title='Register', form=form) @app.route('/login', methods=['GET', 'POST']) def login(): """Handles user login and session creation.""" form = LoginForm() if form.validate_on_submit(): username = form.username.data password = form.password.data user_found = False user_data = None user_id = None # Find user by username for uid, data in USERS.items(): if data['username'] == username: user_found = True user_data = data user_id = uid break if user_found and check_password_hash(user_data['password_hash'], password): # Login success: set session variables session['user_id'] = user_id session['username'] = username flash(f'Welcome back, {username}!', 'message') return redirect(url_for('feed')) else: flash('Login failed. Check your username and password.', 'error') return render_template_string(LOGIN_TEMPLATE, title='Login', form=form) @app.route('/logout') def logout(): """Handles user logout and session clearing.""" session.pop('user_id', None) session.pop('username', None) flash('You have been logged out.', 'message') return redirect(url_for('login')) @app.route('/feed', methods=['GET', 'POST']) def feed(): """Displays the social feed and handles new post submission.""" post_form = PostForm() # Handle new post submission if 'user_id' in session and post_form.validate_on_submit(): new_post = { 'id': str(uuid.uuid4()), 'user_id': session['user_id'], 'username': session['username'], 'content': post_form.content.data, 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S") } # Prepend the new post to the list for a reverse chronological feed POSTS.insert(0, new_post) flash('Post created successfully!', 'message') # Redirect to GET request to prevent form resubmission return redirect(url_for('feed')) # Display the feed (sorted most recent first) return render_template_string( FEED_TEMPLATE, title='Feed', post_form=post_form, posts=POSTS ) if __name__ == '__main__': # Add a sample user and a few sample posts for initial testing sample_user_id = str(uuid.uuid4()) USERS[sample_user_id] = { 'username': 'geek4code_user', 'password_hash': generate_password_hash('password123'), 'id': sample_user_id } POSTS.append({ 'id': str(uuid.uuid4()), 'user_id': str(uuid.uuid4()), 'username': 'AI_Bot', 'content': 'Welcome to Flastagram! This is a simple, single-file social media clone built with Flask.', 'timestamp': (datetime.now() - datetime.timedelta(minutes=5)).strftime("%Y-%m-%d %H:%M:%S") }) POSTS.append({ 'id': str(uuid.uuid4()), 'user_id': sample_user_id, 'username': 'geek4code_user', 'content': 'Just testing out the new posting feature. It looks great on mobile too!', 'timestamp': (datetime.now() - datetime.timedelta(minutes=2)).strftime("%Y-%m-%d %H:%M:%S") }) # Run the Flask app print("--- Flastagram is running ---") print("Default Test User: username='geek4code_user', password='password123'") # Note: Flask runs on a different port in this environment, but the default is 5000. app.run(debug=False)


How to Run the Project

  1. Save: Save the code above as a single file named social_clone_app.py.
  2. Run: Open your terminal and run the command:
  3. 
        python social_clone_app.py
        
  4. Access: Open your browser and navigate to the local host address shown in your console (usually `http://127.0.0.1:5000/`).

You can use the default test account provided in the if __name__ == '__main__' block:

  • Username: geek4code_user
  • Password: password123

The application handles registration, login, logout, and displays a time-ordered feed where authenticated users can submit new posts.


← Back to Projects