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
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 %}
{% 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
- Save: Save the code above as a single file named social_clone_app.py.
- Run: Open your terminal and run the command:
- Access: Open your browser and navigate to the local host address shown in your console (usually `http://127.0.0.1:5000/`).
python social_clone_app.py
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
