aTweet (janky, messy, barely working code)
This commit is contained in:
commit
728d481393
1
aTweet
Submodule
1
aTweet
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit e9f513d4c94737c524698308346915e1564c8573
|
956
app.py
Normal file
956
app.py
Normal file
@ -0,0 +1,956 @@
|
|||||||
|
from flask import Flask, render_template, request, redirect, url_for, session, g, flash
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import current_app
|
||||||
|
from flask import jsonify
|
||||||
|
import markdown
|
||||||
|
import re
|
||||||
|
import bleach
|
||||||
|
|
||||||
|
def render_markdown(text):
|
||||||
|
html = markdown.markdown(text, extensions=['nl2br'])
|
||||||
|
allowed_tags = ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li']
|
||||||
|
allowed_attributes = {'a': ['href', 'title']}
|
||||||
|
return bleach.clean(html, tags=allowed_tags, attributes=allowed_attributes, strip=True)
|
||||||
|
|
||||||
|
|
||||||
|
def process_tweet_content(content):
|
||||||
|
content = re.sub(r'#(\w+)', r'<a href="/hashtag/\1" class="hashtag">#\1</a>', content)
|
||||||
|
content = re.sub(r'@(\w+)', r'<a href="/profile/\1" class="mention">@\1</a>', content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
def get_user_by_id(user_id):
|
||||||
|
db = get_db()
|
||||||
|
user = db.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone()
|
||||||
|
return dict(user) if user else None
|
||||||
|
|
||||||
|
def init_jinja_env(app):
|
||||||
|
app.jinja_env.globals.update(get_user_by_id=get_user_by_id)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = 'verysecretok'
|
||||||
|
init_jinja_env(app)
|
||||||
|
|
||||||
|
@app.template_filter('replace_mentions')
|
||||||
|
def replace_mentions(content):
|
||||||
|
return bleach.linkify(re.sub(r'@(\w+)', r'<a href="/profile/\1">@\1</a>', content))
|
||||||
|
|
||||||
|
DATABASE = 'db.sqlite3'
|
||||||
|
UPLOAD_FOLDER = 'static/uploads/'
|
||||||
|
app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'uploads')
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
|
||||||
|
def add_rendered_content_column():
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
db.execute('ALTER TABLE tweets ADD COLUMN rendered_content TEXT')
|
||||||
|
db.commit()
|
||||||
|
print("Added rendered_content column to tweets table")
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
if "duplicate column name" in str(e):
|
||||||
|
print("rendered_content column already exists")
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = getattr(g, '_database', None)
|
||||||
|
if db is None:
|
||||||
|
db = g._database = sqlite3.connect(DATABASE)
|
||||||
|
db.row_factory = sqlite3.Row
|
||||||
|
return db
|
||||||
|
|
||||||
|
|
||||||
|
def create_tables():
|
||||||
|
db = get_db()
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
pfp TEXT,
|
||||||
|
banner TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)''')
|
||||||
|
|
||||||
|
|
||||||
|
cursor = db.execute("PRAGMA table_info(users)")
|
||||||
|
columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
|
||||||
|
if 'created_at' not in columns:
|
||||||
|
|
||||||
|
db.execute('ALTER TABLE users RENAME TO users_old')
|
||||||
|
|
||||||
|
|
||||||
|
db.execute('''CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
pfp TEXT,
|
||||||
|
banner TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)''')
|
||||||
|
|
||||||
|
|
||||||
|
cursor = db.execute("PRAGMA table_info(users_old)")
|
||||||
|
old_columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
insert_columns = ['id', 'username', 'password']
|
||||||
|
select_columns = ['id', 'username', 'password']
|
||||||
|
|
||||||
|
if 'email' in old_columns:
|
||||||
|
insert_columns.append('email')
|
||||||
|
select_columns.append('email')
|
||||||
|
else:
|
||||||
|
insert_columns.append('email')
|
||||||
|
select_columns.append("'example@email.com' AS email")
|
||||||
|
|
||||||
|
if 'pfp' in old_columns:
|
||||||
|
insert_columns.append('pfp')
|
||||||
|
select_columns.append('pfp')
|
||||||
|
|
||||||
|
if 'banner' in old_columns:
|
||||||
|
insert_columns.append('banner')
|
||||||
|
select_columns.append('banner')
|
||||||
|
|
||||||
|
|
||||||
|
db.execute(f'''
|
||||||
|
INSERT INTO users({', '.join(insert_columns)})
|
||||||
|
SELECT {', '.join(select_columns)} FROM users_old
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
|
db.execute('DROP TABLE users_old')
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS tweets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
likes INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
|
)''')
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
tweet_id INTEGER,
|
||||||
|
user_id INTEGER,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY(tweet_id) REFERENCES tweets(id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
|
)''')
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS likes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER,
|
||||||
|
tweet_id INTEGER,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY(tweet_id) REFERENCES tweets(id),
|
||||||
|
UNIQUE(user_id, tweet_id)
|
||||||
|
)''')
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS group_members (
|
||||||
|
group_id INTEGER,
|
||||||
|
user_id INTEGER,
|
||||||
|
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (group_id) REFERENCES groups (id),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id),
|
||||||
|
PRIMARY KEY (group_id, user_id)
|
||||||
|
)''')
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
sender_id INTEGER,
|
||||||
|
receiver_id INTEGER,
|
||||||
|
content TEXT,
|
||||||
|
image TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (sender_id) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (receiver_id) REFERENCES users (id)
|
||||||
|
)''')
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS posts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER,
|
||||||
|
group_id INTEGER,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (group_id) REFERENCES groups (id)
|
||||||
|
)''')
|
||||||
|
db.execute('''CREATE TABLE IF NOT EXISTS groups (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
avatar TEXT,
|
||||||
|
vanity_url TEXT
|
||||||
|
)''')
|
||||||
|
|
||||||
|
|
||||||
|
cursor = db.execute("PRAGMA table_info(groups)")
|
||||||
|
columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
|
||||||
|
if 'vanity_url' not in columns:
|
||||||
|
|
||||||
|
db.execute('''CREATE TABLE groups_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
avatar TEXT,
|
||||||
|
vanity_url TEXT UNIQUE
|
||||||
|
)''')
|
||||||
|
|
||||||
|
|
||||||
|
db.execute('INSERT INTO groups_new SELECT id, name, description, created_at, avatar, NULL FROM groups')
|
||||||
|
|
||||||
|
|
||||||
|
db.execute('DROP TABLE groups')
|
||||||
|
|
||||||
|
|
||||||
|
db.execute('ALTER TABLE groups_new RENAME TO groups')
|
||||||
|
|
||||||
|
|
||||||
|
db.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_vanity_url ON groups (vanity_url)')
|
||||||
|
|
||||||
|
|
||||||
|
cursor = db.execute("PRAGMA table_info(users)")
|
||||||
|
columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
if 'created_at' not in columns:
|
||||||
|
add_rendered_content_column()
|
||||||
|
update_existing_tweets()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_connection(exception):
|
||||||
|
db = getattr(g, '_database', None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def add_columns():
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
db.execute('ALTER TABLE users ADD COLUMN pfp TEXT')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.execute('ALTER TABLE tweets ADD COLUMN likes INTEGER DEFAULT 0')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.execute('ALTER TABLE users ADD COLUMN banner TEXT')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.execute('ALTER TABLE messages ADD COLUMN image TEXT')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS followers (
|
||||||
|
follower_id INTEGER,
|
||||||
|
followed_id INTEGER,
|
||||||
|
FOREIGN KEY (follower_id) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (followed_id) REFERENCES users (id),
|
||||||
|
PRIMARY KEY (follower_id, followed_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/signup', methods=['GET', 'POST'])
|
||||||
|
def signup():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form['username']
|
||||||
|
password = generate_password_hash(request.form['password'])
|
||||||
|
email = request.form['email']
|
||||||
|
|
||||||
|
restricted_usernames = ['avery', 'cgcristi', 'cg', 'ceegee']
|
||||||
|
if username.lower() in restricted_usernames:
|
||||||
|
flash('This username is not allowed. Please choose a different one.', 'error')
|
||||||
|
return render_template('signup.html')
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
db.execute('INSERT INTO users (username, password, email) VALUES (?, ?, ?)', (username, password, email))
|
||||||
|
db.commit()
|
||||||
|
flash('Account created successfully. Please log in.', 'success')
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
flash('Username already exists. Please choose a different one.', 'error')
|
||||||
|
|
||||||
|
return render_template('signup.html')
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form['username']
|
||||||
|
password = request.form['password']
|
||||||
|
db = get_db()
|
||||||
|
user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
|
||||||
|
|
||||||
|
if user and check_password_hash(user['password'], password):
|
||||||
|
session['user_id'] = user['id']
|
||||||
|
session['username'] = user['username']
|
||||||
|
if not user['pfp']:
|
||||||
|
return redirect(url_for('profile', username=user['username']))
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.pop('user_id', None)
|
||||||
|
session.pop('username', None)
|
||||||
|
session.pop('pfp', None)
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
tweets = db.execute('''
|
||||||
|
SELECT t.*, u.username, u.pfp as user_pfp,
|
||||||
|
COALESCE(t.rendered_content, t.content) as displayed_content
|
||||||
|
FROM tweets t
|
||||||
|
JOIN users u ON t.user_id = u.id
|
||||||
|
ORDER BY t.created_at DESC
|
||||||
|
''').fetchall()
|
||||||
|
|
||||||
|
tweets = [dict(tweet) for tweet in tweets]
|
||||||
|
for tweet in tweets:
|
||||||
|
tweet['created_at'] = datetime.strptime(tweet['created_at'], '%Y-%m-%d %H:%M:%S')
|
||||||
|
tweet['displayed_content'] = bleach.clean(tweet['displayed_content'], strip=True)
|
||||||
|
|
||||||
|
liked_tweet_ids = {like['tweet_id'] for like in db.execute('SELECT tweet_id FROM likes WHERE user_id = ?', (session['user_id'],)).fetchall()}
|
||||||
|
user = db.execute('SELECT * FROM users WHERE id = ?', (session['user_id'],)).fetchone()
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
flash('User not found. Please log in again.', 'error')
|
||||||
|
return redirect(url_for('logout'))
|
||||||
|
|
||||||
|
user = dict(user)
|
||||||
|
user['created_at'] = datetime.strptime(user['created_at'], '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
return render_template('index.html', tweets=tweets, liked_tweet_ids=liked_tweet_ids, user=user, get_user_by_id=get_user_by_id)
|
||||||
|
|
||||||
|
|
||||||
|
def update_existing_tweets():
|
||||||
|
db = get_db()
|
||||||
|
tweets = db.execute('SELECT id, content FROM tweets WHERE rendered_content IS NULL').fetchall()
|
||||||
|
for tweet in tweets:
|
||||||
|
rendered_content = render_markdown(tweet['content'])
|
||||||
|
db.execute('UPDATE tweets SET rendered_content = ? WHERE id = ?', (rendered_content, tweet['id']))
|
||||||
|
db.commit()
|
||||||
|
print(f"Updated {len(tweets)} existing tweets with rendered content")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tweet', methods=['POST'])
|
||||||
|
def tweet():
|
||||||
|
if 'user_id' in session:
|
||||||
|
content = request.form['content']
|
||||||
|
if len(content) > 280:
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
processed_content = process_tweet_content(content)
|
||||||
|
rendered_content = render_markdown(processed_content)
|
||||||
|
image = request.files.get('image')
|
||||||
|
db = get_db()
|
||||||
|
if image and allowed_file(image.filename):
|
||||||
|
filename = secure_filename(image.filename)
|
||||||
|
image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||||
|
db.execute('INSERT INTO tweets (content, rendered_content, user_id, image) VALUES (?, ?, ?, ?)',
|
||||||
|
(content, rendered_content, session['user_id'], filename))
|
||||||
|
else:
|
||||||
|
db.execute('INSERT INTO tweets (content, rendered_content, user_id) VALUES (?, ?, ?)',
|
||||||
|
(content, rendered_content, session['user_id']))
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/retweet/<int:tweet_id>', methods=['POST'])
|
||||||
|
def retweet(tweet_id):
|
||||||
|
if 'user_id' in session:
|
||||||
|
db = get_db()
|
||||||
|
original_tweet = db.execute('SELECT * FROM tweets WHERE id = ?', (tweet_id,)).fetchone()
|
||||||
|
if original_tweet:
|
||||||
|
db.execute('INSERT INTO tweets (content, rendered_content, user_id, original_tweet_id) VALUES (?, ?, ?, ?)',
|
||||||
|
(original_tweet['content'], original_tweet['rendered_content'], session['user_id'], tweet_id))
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/search', methods=['GET'])
|
||||||
|
def search():
|
||||||
|
query = request.args.get('q', '')
|
||||||
|
db = get_db()
|
||||||
|
users = db.execute('SELECT * FROM users WHERE username LIKE ? LIMIT 10', ('%' + query + '%',)).fetchall()
|
||||||
|
return render_template('search_results.html', users=users, query=query)
|
||||||
|
|
||||||
|
@app.route('/hashtag/<hashtag>')
|
||||||
|
def hashtag(hashtag):
|
||||||
|
db = get_db()
|
||||||
|
tweets = db.execute('''
|
||||||
|
SELECT t.*, u.username, u.pfp as user_pfp
|
||||||
|
FROM tweets t
|
||||||
|
JOIN users u ON t.user_id = u.id
|
||||||
|
WHERE t.content LIKE ?
|
||||||
|
ORDER BY t.created_at DESC
|
||||||
|
''', ('%#' + hashtag + '%',)).fetchall()
|
||||||
|
return render_template('hashtag.html', tweets=tweets, hashtag=hashtag)
|
||||||
|
|
||||||
|
@app.route('/tweet/<int:tweet_id>')
|
||||||
|
def tweet_detail(tweet_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
db = get_db()
|
||||||
|
tweet = db.execute('SELECT t.id, t.content, u.username, u.pfp, t.likes FROM tweets t JOIN users u ON t.user_id = u.id WHERE t.id = ?', (tweet_id,)).fetchone()
|
||||||
|
comments = db.execute('SELECT c.content, u.username FROM comments c JOIN users u ON c.user_id = u.id WHERE c.tweet_id = ?', (tweet_id,)).fetchall()
|
||||||
|
liked_tweet_ids = {like['tweet_id'] for like in db.execute('SELECT tweet_id FROM likes WHERE user_id = ?', (session['user_id'],)).fetchall()}
|
||||||
|
return render_template('tweet_detail.html', tweet=tweet, comments=comments, liked_tweet_ids=liked_tweet_ids)
|
||||||
|
|
||||||
|
@app.route('/comment/<int:tweet_id>', methods=['POST'])
|
||||||
|
def comment(tweet_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'message': 'User not logged in'})
|
||||||
|
|
||||||
|
content = request.form['content']
|
||||||
|
db = get_db()
|
||||||
|
db.execute('INSERT INTO comments (content, tweet_id, user_id) VALUES (?, ?, ?)', (content, tweet_id, session['user_id']))
|
||||||
|
db.commit()
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/like/<int:tweet_id>', methods=['POST'])
|
||||||
|
def like(tweet_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'message': 'User not logged in'}), 401
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
user_id = session['user_id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing_like = db.execute('SELECT * FROM likes WHERE user_id = ? AND tweet_id = ?', (user_id, tweet_id)).fetchone()
|
||||||
|
|
||||||
|
if existing_like:
|
||||||
|
db.execute('DELETE FROM likes WHERE user_id = ? AND tweet_id = ?', (user_id, tweet_id))
|
||||||
|
db.execute('UPDATE tweets SET likes = likes - 1 WHERE id = ?', (tweet_id,))
|
||||||
|
else:
|
||||||
|
db.execute('INSERT INTO likes (user_id, tweet_id) VALUES (?, ?)', (user_id, tweet_id))
|
||||||
|
db.execute('UPDATE tweets SET likes = likes + 1 WHERE id = ?', (tweet_id,))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
updated_likes = db.execute('SELECT likes FROM tweets WHERE id = ?', (tweet_id,)).fetchone()['likes']
|
||||||
|
return jsonify({'success': True, 'likes': updated_likes})
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
print(f"Error in like route: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': 'An error occurred while processing your request'}), 500
|
||||||
|
|
||||||
|
@app.route('/profile/<username>')
|
||||||
|
def profile(username):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
|
||||||
|
if not user:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
user = dict(user)
|
||||||
|
|
||||||
|
if 'created_at' not in user or not user['created_at']:
|
||||||
|
first_tweet = db.execute('SELECT MIN(created_at) as first_tweet_date FROM tweets WHERE user_id = ?', (user['id'],)).fetchone()
|
||||||
|
if first_tweet and first_tweet['first_tweet_date']:
|
||||||
|
user['created_at'] = datetime.strptime(first_tweet['first_tweet_date'], '%Y-%m-%d %H:%M:%S')
|
||||||
|
else:
|
||||||
|
user['created_at'] = datetime.now()
|
||||||
|
else:
|
||||||
|
user['created_at'] = datetime.strptime(user['created_at'], '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
tweets = db.execute('SELECT * FROM tweets WHERE user_id = ? ORDER BY created_at DESC', (user['id'],)).fetchall()
|
||||||
|
tweets = [dict(tweet) for tweet in tweets]
|
||||||
|
for tweet in tweets:
|
||||||
|
tweet['created_at'] = datetime.strptime(tweet['created_at'], '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
return render_template('profile.html', user=user, tweets=tweets, get_user_by_id=get_user_by_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/change_profile_picture', methods=['POST'])
|
||||||
|
def change_profile_picture():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
user_id = session['user_id']
|
||||||
|
|
||||||
|
if 'profile_picture' in request.files:
|
||||||
|
file = request.files['profile_picture']
|
||||||
|
if file and allowed_file(file.filename):
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute("UPDATE users SET pfp = ? WHERE id = ?", (filename, user_id))
|
||||||
|
db.commit()
|
||||||
|
flash("Profile picture updated successfully.")
|
||||||
|
else:
|
||||||
|
flash("Invalid file type. Please upload a PNG, JPG, JPEG, or GIF.")
|
||||||
|
else:
|
||||||
|
flash("No file uploaded.")
|
||||||
|
|
||||||
|
return redirect(url_for('profile'))
|
||||||
|
|
||||||
|
@app.route('/change_banner', methods=['POST'])
|
||||||
|
def change_banner():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
user_id = session['user_id']
|
||||||
|
if 'banner' in request.files:
|
||||||
|
file = request.files['banner']
|
||||||
|
if file and allowed_file(file.filename):
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute("UPDATE users SET banner = ? WHERE id = ?", (filename, user_id))
|
||||||
|
db.commit()
|
||||||
|
flash("Banner updated successfully.")
|
||||||
|
else:
|
||||||
|
flash("Invalid file type. Please upload a PNG, JPG, JPEG, or GIF.")
|
||||||
|
else:
|
||||||
|
flash("No file uploaded.")
|
||||||
|
|
||||||
|
return redirect(url_for('profile'))
|
||||||
|
|
||||||
|
@app.route('/edit_tweet/<int:tweet_id>', methods=['GET', 'POST'])
|
||||||
|
def edit_tweet(tweet_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
tweet = db.execute('SELECT * FROM tweets WHERE id = ? AND user_id = ?', (tweet_id, session['user_id'])).fetchone()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
new_content = request.form['content']
|
||||||
|
db.execute('UPDATE tweets SET content = ? WHERE id = ?', (new_content, tweet_id))
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
return render_template('edit_tweet.html', tweet=tweet)
|
||||||
|
|
||||||
|
@app.route('/delete_tweet/<int:tweet_id>', methods=['POST'])
|
||||||
|
def delete_tweet(tweet_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM tweets WHERE id = ? AND user_id = ?', (tweet_id, session['user_id']))
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/groups')
|
||||||
|
def groups():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
db = get_db()
|
||||||
|
search_query = request.args.get('search', '')
|
||||||
|
if search_query:
|
||||||
|
groups = db.execute('SELECT * FROM groups WHERE name LIKE ? ORDER BY name', ('%' + search_query + '%',)).fetchall()
|
||||||
|
else:
|
||||||
|
groups = db.execute('SELECT * FROM groups ORDER BY name').fetchall()
|
||||||
|
return render_template('g.html', groups=groups, get_user_by_id=get_user_by_id, search_query=search_query)
|
||||||
|
|
||||||
|
@app.route('/new_group', methods=['POST'])
|
||||||
|
def new_group():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
name = request.form['group_name']
|
||||||
|
vanity_url = request.form['vanity_url']
|
||||||
|
description = request.form['description']
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
db.execute('INSERT INTO groups (name, vanity_url, description) VALUES (?, ?, ?)', (name, vanity_url, description))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
group_id = db.execute('SELECT last_insert_rowid()').fetchone()[0]
|
||||||
|
db.execute('INSERT INTO group_members (group_id, user_id) VALUES (?, ?)', (group_id, session['user_id']))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
if 'group_picture' in request.files:
|
||||||
|
file = request.files['group_picture']
|
||||||
|
if file and allowed_file(file.filename):
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
file.save(file_path)
|
||||||
|
db.execute('UPDATE groups SET avatar = ? WHERE id = ?', (filename, group_id))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
flash('Group created successfully!', 'success')
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
flash('Group name or vanity URL already exists. Please choose a different one.', 'error')
|
||||||
|
|
||||||
|
return redirect(url_for('groups'))
|
||||||
|
|
||||||
|
@app.route('/group/<vanity_url>')
|
||||||
|
def group_detail(vanity_url):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
group = db.execute('SELECT * FROM groups WHERE vanity_url = ?', (vanity_url,)).fetchone()
|
||||||
|
if not group:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
posts = db.execute('''
|
||||||
|
SELECT p.*, u.username, u.pfp
|
||||||
|
FROM posts p
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
WHERE p.group_id = ?
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
''', (group['id'],)).fetchall()
|
||||||
|
|
||||||
|
is_member = db.execute('SELECT * FROM group_members WHERE group_id = ? AND user_id = ?',
|
||||||
|
(group['id'], session['user_id'])).fetchone() is not None
|
||||||
|
|
||||||
|
return render_template('group_detail.html', group=group, posts=posts, is_member=is_member, get_user_by_id=get_user_by_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/join_group/<int:group_id>', methods=['POST'])
|
||||||
|
def join_group(group_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute('INSERT INTO group_members (group_id, user_id) VALUES (?, ?)', (group_id, session['user_id']))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
group = db.execute('SELECT vanity_url FROM groups WHERE id = ?', (group_id,)).fetchone()
|
||||||
|
flash('You have joined the group!', 'success')
|
||||||
|
return redirect(url_for('group_detail', vanity_url=group['vanity_url']))
|
||||||
|
|
||||||
|
@app.route('/leave_group/<int:group_id>', methods=['POST'])
|
||||||
|
def leave_group(group_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM group_members WHERE user_id = ? AND group_id = ?', (session['user_id'], group_id))
|
||||||
|
db.commit()
|
||||||
|
group = db.execute('SELECT * FROM groups WHERE id = ?', (group_id,)).fetchone()
|
||||||
|
return redirect(url_for('group_detail', vanity_url=group['vanity_url']))
|
||||||
|
|
||||||
|
@app.route('/post_in_group/<int:group_id>', methods=['POST'])
|
||||||
|
def post_in_group(group_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
is_member = db.execute('SELECT * FROM group_members WHERE group_id = ? AND user_id = ?',
|
||||||
|
(group_id, session['user_id'])).fetchone() is not None
|
||||||
|
|
||||||
|
if not is_member:
|
||||||
|
flash('You must be a member of the group to post.', 'error')
|
||||||
|
else:
|
||||||
|
content = request.form['content']
|
||||||
|
db.execute('INSERT INTO posts (user_id, group_id, content) VALUES (?, ?, ?)',
|
||||||
|
(session['user_id'], group_id, content))
|
||||||
|
db.commit()
|
||||||
|
flash('Your post has been added to the group!', 'success')
|
||||||
|
|
||||||
|
group = db.execute('SELECT vanity_url FROM groups WHERE id = ?', (group_id,)).fetchone()
|
||||||
|
return redirect(url_for('group_detail', vanity_url=group['vanity_url']))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/dms')
|
||||||
|
def dms():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = 10
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
|
||||||
|
conversations = db.execute('''
|
||||||
|
SELECT DISTINCT
|
||||||
|
CASE
|
||||||
|
WHEN sender_id = ? THEN receiver_id
|
||||||
|
ELSE sender_id
|
||||||
|
END AS other_user_id,
|
||||||
|
MAX(created_at) as last_message_time
|
||||||
|
FROM messages
|
||||||
|
WHERE sender_id = ? OR receiver_id = ?
|
||||||
|
GROUP BY other_user_id
|
||||||
|
ORDER BY last_message_time DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
''', (session['user_id'], session['user_id'], session['user_id'], per_page, offset)).fetchall()
|
||||||
|
|
||||||
|
dms = []
|
||||||
|
for conv in conversations:
|
||||||
|
other_user = get_user_by_id(conv['other_user_id'])
|
||||||
|
dms.append({
|
||||||
|
'username': other_user['username'],
|
||||||
|
'pfp': other_user['pfp']
|
||||||
|
})
|
||||||
|
|
||||||
|
has_more = len(dms) == per_page
|
||||||
|
return render_template('dm.html', dms=dms, page=page, has_more=has_more)
|
||||||
|
|
||||||
|
@app.route('/dm/<username>')
|
||||||
|
def dm_conversation(username):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
other_user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
|
||||||
|
if not other_user:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
messages = db.execute('''
|
||||||
|
SELECT m.*, u.username, u.pfp
|
||||||
|
FROM messages m
|
||||||
|
JOIN users u ON m.sender_id = u.id
|
||||||
|
WHERE (m.sender_id = ? AND m.receiver_id = ?) OR (m.sender_id = ? AND m.receiver_id = ?)
|
||||||
|
ORDER BY m.created_at ASC
|
||||||
|
''', (session['user_id'], other_user['id'], other_user['id'], session['user_id'])).fetchall()
|
||||||
|
|
||||||
|
return render_template('dm_conversation.html', other_user=other_user, messages=messages)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/edit_group/<int:group_id>', methods=['GET', 'POST'])
|
||||||
|
def edit_group(group_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
group = db.execute('SELECT * FROM groups WHERE id = ?', (group_id,)).fetchone()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.form['name']
|
||||||
|
description = request.form['description']
|
||||||
|
|
||||||
|
db.execute('UPDATE groups SET name = ?, description = ? WHERE id = ?', (name, description, group_id))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
flash('Group updated successfully!', 'success')
|
||||||
|
return redirect(url_for('group_detail', vanity_url=group['vanity_url']))
|
||||||
|
|
||||||
|
return render_template('edit_group.html', group=group)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/dm/<username>/send', methods=['POST'])
|
||||||
|
def send_message(username):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
content = request.form['content']
|
||||||
|
image = request.files.get('image')
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
receiver = db.execute('SELECT id FROM users WHERE username = ?', (username,)).fetchone()
|
||||||
|
|
||||||
|
if receiver:
|
||||||
|
image_filename = None
|
||||||
|
if image and allowed_file(image.filename):
|
||||||
|
image_filename = secure_filename(image.filename)
|
||||||
|
image.save(os.path.join(app.config['UPLOAD_FOLDER'], image_filename))
|
||||||
|
|
||||||
|
db.execute('INSERT INTO messages (sender_id, receiver_id, content, image, created_at) VALUES (?, ?, ?, ?, ?)',
|
||||||
|
(session['user_id'], receiver['id'], content, image_filename, datetime.now()))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return redirect(url_for('dm_conversation', username=username))
|
||||||
|
|
||||||
|
@app.route('/check_new_messages/<username>', methods=['GET'])
|
||||||
|
def check_new_messages(username):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'message': 'Unauthorized'})
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
other_user = db.execute('SELECT id FROM users WHERE username = ?', (username,)).fetchone()
|
||||||
|
|
||||||
|
if not other_user:
|
||||||
|
return jsonify({'success': False, 'message': 'User not found'})
|
||||||
|
|
||||||
|
last_message_id = request.args.get('last_message_id', 0, type=int)
|
||||||
|
|
||||||
|
new_messages = db.execute('''
|
||||||
|
SELECT m.*, u.username, u.pfp
|
||||||
|
FROM messages m
|
||||||
|
JOIN users u ON m.sender_id = u.id
|
||||||
|
WHERE ((m.sender_id = ? AND m.receiver_id = ?) OR (m.sender_id = ? AND m.receiver_id = ?))
|
||||||
|
AND m.id > ?
|
||||||
|
ORDER BY m.created_at ASC
|
||||||
|
''', (session['user_id'], other_user['id'], other_user['id'], session['user_id'], last_message_id)).fetchall()
|
||||||
|
|
||||||
|
if new_messages:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'messages': [{
|
||||||
|
'id': message['id'],
|
||||||
|
'content': message['content'],
|
||||||
|
'sender_id': message['sender_id'],
|
||||||
|
'username': message['username'],
|
||||||
|
'pfp': message['pfp'],
|
||||||
|
'created_at': message['created_at'],
|
||||||
|
'image': message['image']
|
||||||
|
} for message in new_messages]
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({'success': False, 'message': 'No new messages'})
|
||||||
|
|
||||||
|
@app.route('/delete_group/<int:group_id>', methods=['POST'])
|
||||||
|
def delete_group(group_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM groups WHERE id = ?', (group_id,))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return redirect(url_for('groups'))
|
||||||
|
|
||||||
|
@app.route('/start_dm', methods=['POST'])
|
||||||
|
def start_dm():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
username = request.form['username']
|
||||||
|
return redirect(url_for('dm_conversation', username=username))
|
||||||
|
|
||||||
|
@app.route('/edit_message/<int:message_id>', methods=['POST'])
|
||||||
|
def edit_message(message_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
message = db.execute('SELECT * FROM messages WHERE id = ? AND sender_id = ?', (message_id, session['user_id'])).fetchone()
|
||||||
|
if not message:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
new_content = request.form['content']
|
||||||
|
db.execute('UPDATE messages SET content = ? WHERE id = ?', (new_content, message_id))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return redirect(url_for('dm_conversation', username=request.form['username']))
|
||||||
|
|
||||||
|
@app.route('/delete_message/<int:message_id>', methods=['POST'])
|
||||||
|
def delete_message(message_id):
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM messages WHERE id = ? AND sender_id = ?', (message_id, session['user_id']))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return redirect(url_for('dm_conversation', username=request.form['username']))
|
||||||
|
|
||||||
|
@app.route('/admin')
|
||||||
|
def admin_panel():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
user = get_user_by_id(session['user_id'])
|
||||||
|
if not user or user['username'] != '123':
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
users = db.execute('SELECT * FROM users ORDER BY created_at DESC').fetchall()
|
||||||
|
tweets = db.execute('''
|
||||||
|
SELECT t.*, u.username
|
||||||
|
FROM tweets t
|
||||||
|
JOIN users u ON t.user_id = u.id
|
||||||
|
ORDER BY t.created_at DESC
|
||||||
|
''').fetchall()
|
||||||
|
groups = db.execute('''
|
||||||
|
SELECT g.*, COUNT(gm.user_id) as member_count
|
||||||
|
FROM groups g
|
||||||
|
LEFT JOIN group_members gm ON g.id = gm.group_id
|
||||||
|
GROUP BY g.id
|
||||||
|
ORDER BY g.created_at DESC
|
||||||
|
''').fetchall()
|
||||||
|
|
||||||
|
user_count = db.execute('SELECT COUNT(*) as count FROM users').fetchone()['count']
|
||||||
|
tweet_count = db.execute('SELECT COUNT(*) as count FROM tweets').fetchone()['count']
|
||||||
|
group_count = db.execute('SELECT COUNT(*) as count FROM groups').fetchone()['count']
|
||||||
|
|
||||||
|
return render_template('admin_panel.html',
|
||||||
|
users=users,
|
||||||
|
tweets=tweets,
|
||||||
|
groups=groups,
|
||||||
|
user_count=user_count,
|
||||||
|
tweet_count=tweet_count,
|
||||||
|
group_count=group_count)
|
||||||
|
|
||||||
|
def get_all_users():
|
||||||
|
db = get_db()
|
||||||
|
users = db.execute('SELECT * FROM users').fetchall()
|
||||||
|
return [dict(user) for user in users]
|
||||||
|
|
||||||
|
def get_all_tweets():
|
||||||
|
db = get_db()
|
||||||
|
tweets = db.execute('SELECT t.*, u.username FROM tweets t JOIN users u ON t.user_id = u.id ORDER BY t.created_at DESC').fetchall()
|
||||||
|
return [dict(tweet) for tweet in tweets]
|
||||||
|
|
||||||
|
def get_all_groups():
|
||||||
|
db = get_db()
|
||||||
|
groups = db.execute('SELECT * FROM groups ORDER BY created_at DESC').fetchall()
|
||||||
|
return [dict(group) for group in groups]
|
||||||
|
|
||||||
|
@app.route('/admin/delete_user/<int:user_id>', methods=['POST'])
|
||||||
|
def delete_user(user_id):
|
||||||
|
if 'user_id' not in session or get_user_by_id(session['user_id']).username != 'avery':
|
||||||
|
return jsonify({'success': False, 'message': 'Unauthorized'})
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM users WHERE id = ?', (user_id,))
|
||||||
|
db.commit()
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
@app.route('/admin/delete_tweet/<int:tweet_id>', methods=['POST'])
|
||||||
|
def delete_tweet_admin(tweet_id):
|
||||||
|
if 'user_id' not in session or get_user_by_id(session['user_id']).username != 'avery':
|
||||||
|
return jsonify({'success': False, 'message': 'Unauthorized'})
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM tweets WHERE id = ?', (tweet_id,))
|
||||||
|
db.commit()
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
@app.route('/admin/delete_group/<int:group_id>', methods=['POST'])
|
||||||
|
def delete_group_admin(group_id):
|
||||||
|
if 'user_id' not in session or get_user_by_id(session['user_id'])['username'] != 'avery':
|
||||||
|
return jsonify({'success': False, 'message': 'Unauthorized'})
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
db.execute('DELETE FROM groups WHERE id = ?', (group_id,))
|
||||||
|
db.execute('DELETE FROM group_members WHERE group_id = ?', (group_id,))
|
||||||
|
db.execute('DELETE FROM posts WHERE group_id = ?', (group_id,))
|
||||||
|
db.commit()
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, port=4771)
|
51
clean_db.py
Normal file
51
clean_db.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Connect to the database
|
||||||
|
conn = sqlite3.connect('db.sqlite3')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Keep users 'avery' and 'asd'
|
||||||
|
cursor.execute("SELECT id FROM users WHERE username IN ('avery', 'asd')")
|
||||||
|
keep_user_ids = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Delete all users except 'avery' and 'asd'
|
||||||
|
cursor.execute("DELETE FROM users WHERE id NOT IN ({})".format(','.join('?' * len(keep_user_ids))), keep_user_ids)
|
||||||
|
|
||||||
|
# Get avery's user id
|
||||||
|
cursor.execute("SELECT id FROM users WHERE username = 'avery'")
|
||||||
|
avery_id = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# Keep only the first two tweets from 'avery' and delete all other tweets
|
||||||
|
cursor.execute("""
|
||||||
|
DELETE FROM tweets
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT id FROM tweets
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
LIMIT 2
|
||||||
|
)
|
||||||
|
""", (avery_id,))
|
||||||
|
|
||||||
|
# Delete all likes
|
||||||
|
cursor.execute("DELETE FROM likes")
|
||||||
|
|
||||||
|
# Delete all comments
|
||||||
|
cursor.execute("DELETE FROM comments")
|
||||||
|
|
||||||
|
# Delete all messages
|
||||||
|
cursor.execute("DELETE FROM messages")
|
||||||
|
|
||||||
|
# Delete all groups
|
||||||
|
cursor.execute("DELETE FROM groups")
|
||||||
|
|
||||||
|
# Delete all group members
|
||||||
|
cursor.execute("DELETE FROM group_members")
|
||||||
|
|
||||||
|
# Delete all posts
|
||||||
|
cursor.execute("DELETE FROM posts")
|
||||||
|
|
||||||
|
# Commit the changes and close the connection
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("Database cleaned successfully.")
|
BIN
db.sqlite3
Normal file
BIN
db.sqlite3
Normal file
Binary file not shown.
1167
static/styles.css
Normal file
1167
static/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/uploads/Screenshot_2024-09-21_123748.png
Normal file
BIN
static/uploads/Screenshot_2024-09-21_123748.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
static/uploads/f7d2d93d41931d2e41fd1ca49da166b4.png
Normal file
BIN
static/uploads/f7d2d93d41931d2e41fd1ca49da166b4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
352
templates/admin_panel.html
Normal file
352
templates/admin_panel.html
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Panel - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
.admin-panel { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||||
|
.admin-stats { display: flex; justify-content: space-between; margin-bottom: 30px; }
|
||||||
|
.stat-card { background-color: var(--primary-color); color: white; border-radius: 10px; padding: 20px; text-align: center; flex: 1; margin: 0 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
|
||||||
|
.admin-section { background-color: var(--background-color); border-radius: 10px; padding: 20px; margin-bottom: 40px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
|
||||||
|
.admin-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.admin-table th, .admin-table td { padding: 12px; text-align: left; border-bottom: 1px solid var(--border-color); }
|
||||||
|
.admin-table th { background-color: var(--primary-color); color: white; }
|
||||||
|
.admin-table tr:hover { background-color: var(--hover-color); }
|
||||||
|
.admin-actions { display: flex; gap: 10px; }
|
||||||
|
.admin-button { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; color: white; }
|
||||||
|
.admin-button-view { background-color: #4CAF50; }
|
||||||
|
.admin-button-edit { background-color: #2196F3; }
|
||||||
|
.admin-button-delete { background-color: #f44336; }
|
||||||
|
.search-bar { margin-bottom: 20px; }
|
||||||
|
.search-bar input { width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; }
|
||||||
|
.pagination { display: flex; justify-content: center; margin-top: 20px; }
|
||||||
|
.pagination button { margin: 0 5px; padding: 5px 10px; background-color: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
.dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
||||||
|
.dashboard-card { background-color: var(--background-color); border-radius: 10px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
|
||||||
|
.dashboard-card h3 { margin-top: 0; color: var(--primary-color); }
|
||||||
|
.quick-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 30px; }
|
||||||
|
.quick-action-button { padding: 10px 20px; background-color: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container admin-panel">
|
||||||
|
<h1>Admin Panel</h1>
|
||||||
|
|
||||||
|
<div class="admin-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ user_count }}</h3>
|
||||||
|
<p>Total Users</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ tweet_count }}</h3>
|
||||||
|
<p>Total Tweets</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ group_count }}</h3>
|
||||||
|
<p>Total Groups</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3>Recent Activity</h3>
|
||||||
|
<ul id="recentActivity"></ul>
|
||||||
|
<li>Not implemented yet (lazyass)</li>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3>System Health</h3>
|
||||||
|
<p>Server Status: <span id="serverStatus">Operational</span></p>
|
||||||
|
<p>Database Size: <span id="databaseSize">16MB</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quick-actions">
|
||||||
|
<button class="quick-action-button" onclick="showBackupModal()">Backup Database</button>
|
||||||
|
<button class="quick-action-button" onclick="showMaintenanceModal()">Maintenance Mode</button>
|
||||||
|
<button class="quick-action-button" onclick="showBroadcastModal()">Broadcast Message</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Users</h2>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="userSearch" placeholder="Search users...">
|
||||||
|
</div>
|
||||||
|
<table class="admin-table" id="userTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Joined</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>{{ user.created_at }}</td>
|
||||||
|
<td class="admin-actions">
|
||||||
|
<button class="admin-button admin-button-view" onclick="viewUser({{ user.id }})">View</button>
|
||||||
|
<button class="admin-button admin-button-edit" onclick="editUser({{ user.id }})">Edit</button>
|
||||||
|
<button class="admin-button admin-button-delete" onclick="deleteUser({{ user.id }})">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="pagination">
|
||||||
|
<button onclick="changePage(-1, 'userTable')">Previous</button>
|
||||||
|
<span id="userCurrentPage">1</span>
|
||||||
|
<button onclick="changePage(1, 'userTable')">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Tweets</h2>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="tweetSearch" placeholder="Search tweets...">
|
||||||
|
</div>
|
||||||
|
<table class="admin-table" id="tweetTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Content</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for tweet in tweets %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ tweet.content }}</td>
|
||||||
|
<td>{{ tweet.username }}</td>
|
||||||
|
<td>{{ tweet.created_at }}</td>
|
||||||
|
<td class="admin-actions">
|
||||||
|
<button class="admin-button admin-button-view" onclick="viewTweet({{ tweet.id }})">View</button>
|
||||||
|
<button class="admin-button admin-button-delete" onclick="deleteTweet({{ tweet.id }})">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="pagination">
|
||||||
|
<button onclick="changePage(-1, 'tweetTable')">Previous</button>
|
||||||
|
<span id="tweetCurrentPage">1</span>
|
||||||
|
<button onclick="changePage(1, 'tweetTable')">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Groups</h2>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="groupSearch" placeholder="Search groups...">
|
||||||
|
</div>
|
||||||
|
<table class="admin-table" id="groupTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Members</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for group in groups %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ group.name }}</td>
|
||||||
|
<td>{{ group.description }}</td>
|
||||||
|
<td>{{ group.member_count }}</td>
|
||||||
|
<td>{{ group.created_at }}</td>
|
||||||
|
<td class="admin-actions">
|
||||||
|
<button class="admin-button admin-button-view" onclick="viewGroup({{ group.id }})">View</button>
|
||||||
|
<button class="admin-button admin-button-edit" onclick="editGroup({{ group.id }})">Edit</button>
|
||||||
|
<button class="admin-button admin-button-delete" onclick="deleteGroup({{ group.id }})">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="pagination">
|
||||||
|
<button onclick="changePage(-1, 'groupTable')">Previous</button>
|
||||||
|
<span id="groupCurrentPage">1</span>
|
||||||
|
<button onclick="changePage(1, 'groupTable')">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function viewUser(userId) {
|
||||||
|
window.location.href = `/profile/${userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editUser(userId) {
|
||||||
|
const newUsername = prompt("Enter new username:");
|
||||||
|
const newEmail = prompt("Enter new email:");
|
||||||
|
if (newUsername && newEmail) {
|
||||||
|
fetch(`/admin/edit_user/${userId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username: newUsername, email: newEmail }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to edit user');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUser(userId) {
|
||||||
|
if (confirm('Are you sure you want to delete this user?')) {
|
||||||
|
fetch(`/admin/delete_user/${userId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete user');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewTweet(tweetId) {
|
||||||
|
window.location.href = `/tweet/${tweetId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTweet(tweetId) {
|
||||||
|
if (confirm('Are you sure you want to delete this tweet?')) {
|
||||||
|
fetch(`/admin/delete_tweet/${tweetId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete tweet');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewGroup(groupId) {
|
||||||
|
window.location.href = `/group/${groupId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editGroup(groupId) {
|
||||||
|
const newName = prompt("Enter new group name:");
|
||||||
|
const newDescription = prompt("Enter new group description:");
|
||||||
|
if (newName && newDescription) {
|
||||||
|
fetch(`/admin/edit_group/${groupId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: newName, description: newDescription }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to edit group');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteGroup(groupId) {
|
||||||
|
if (confirm('Are you sure you want to delete this group?')) {
|
||||||
|
fetch(`/admin/delete_group/${groupId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete group');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchTable(inputId, tableId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const filter = input.value.toUpperCase();
|
||||||
|
const table = document.getElementById(tableId);
|
||||||
|
const tr = table.getElementsByTagName("tr");
|
||||||
|
|
||||||
|
for (let i = 1; i < tr.length; i++) {
|
||||||
|
let txtValue = tr[i].textContent || tr[i].innerText;
|
||||||
|
if (txtValue.toUpperCase().indexOf(filter) > -1) {
|
||||||
|
tr[i].style.display = "";
|
||||||
|
} else {
|
||||||
|
tr[i].style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("userSearch").addEventListener("keyup", () => searchTable("userSearch", "userTable"));
|
||||||
|
document.getElementById("tweetSearch").addEventListener("keyup", () => searchTable("tweetSearch", "tweetTable"));
|
||||||
|
document.getElementById("groupSearch").addEventListener("keyup", () => searchTable("groupSearch", "groupTable"));
|
||||||
|
|
||||||
|
const itemsPerPage = 10;
|
||||||
|
const currentPages = {
|
||||||
|
userTable: 1,
|
||||||
|
tweetTable: 1,
|
||||||
|
groupTable: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
function changePage(direction, tableId) {
|
||||||
|
currentPages[tableId] += direction;
|
||||||
|
if (currentPages[tableId] < 1) currentPages[tableId] = 1;
|
||||||
|
document.getElementById(`${tableId.replace('Table', 'CurrentPage')}`).textContent = currentPages[tableId];
|
||||||
|
updateTableDisplay(tableId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTableDisplay(tableId) {
|
||||||
|
const table = document.getElementById(tableId);
|
||||||
|
const tr = table.getElementsByTagName("tr");
|
||||||
|
const start = (currentPages[tableId] - 1) * itemsPerPage + 1;
|
||||||
|
const end = start + itemsPerPage;
|
||||||
|
|
||||||
|
for (let i = 1; i < tr.length; i++) {
|
||||||
|
if (i >= start && i < end) {
|
||||||
|
tr[i].style.display = "";
|
||||||
|
} else {
|
||||||
|
tr[i].style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBackupModal() {
|
||||||
|
alert("Database backup functionality not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMaintenanceModal() {
|
||||||
|
alert("Maintenance mode functionality not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBroadcastModal() {
|
||||||
|
alert("Broadcast message functionality not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRecentActivity() {
|
||||||
|
const recentActivity = document.getElementById("recentActivity");
|
||||||
|
recentActivity.innerHTML = "<li>not even implemented</li>";
|
||||||
|
// Initial setup
|
||||||
|
updateTableDisplay('userTable');
|
||||||
|
updateTableDisplay('tweetTable');
|
||||||
|
updateTableDisplay('groupTable');
|
||||||
|
updateRecentActivity();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
359
templates/admin_panel.html.backup
Normal file
359
templates/admin_panel.html.backup
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Panel - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
.admin-panel {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.admin-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.admin-section {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.admin-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.admin-table th, .admin-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.admin-table th {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.admin-table tr:hover {
|
||||||
|
background-color: var(--hover-color);
|
||||||
|
}
|
||||||
|
.admin-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.admin-button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.admin-button-view { background-color: #4CAF50; }
|
||||||
|
.admin-button-edit { background-color: #2196F3; }
|
||||||
|
.admin-button-delete { background-color: #f44336; }
|
||||||
|
.search-bar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.search-bar input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.pagination button {
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container admin-panel">
|
||||||
|
<h1>Admin Panel</h1>
|
||||||
|
|
||||||
|
<div class="admin-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ user_count }}</h3>
|
||||||
|
<p>Total Users</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ tweet_count }}</h3>
|
||||||
|
<p>Total Tweets</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ group_count }}</h3>
|
||||||
|
<p>Total Groups</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Users</h2>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="userSearch" placeholder="Search users...">
|
||||||
|
</div>
|
||||||
|
<table class="admin-table" id="userTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Joined</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
<td>{{ user.created_at }}</td>
|
||||||
|
<td class="admin-actions">
|
||||||
|
<button class="admin-button admin-button-view" onclick="viewUser({{ user.id }})">View</button>
|
||||||
|
<button class="admin-button admin-button-edit" onclick="editUser({{ user.id }})">Edit</button>
|
||||||
|
<button class="admin-button admin-button-delete" onclick="deleteUser({{ user.id }})">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="pagination">
|
||||||
|
<button onclick="changePage(-1)">Previous</button>
|
||||||
|
<span id="currentPage">1</span>
|
||||||
|
<button onclick="changePage(1)">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Tweets</h2>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="tweetSearch" placeholder="Search tweets...">
|
||||||
|
</div>
|
||||||
|
<table class="admin-table" id="tweetTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Content</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for tweet in tweets %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ tweet.content }}</td>
|
||||||
|
<td>{{ tweet.username }}</td>
|
||||||
|
<td>{{ tweet.created_at }}</td>
|
||||||
|
<td class="admin-actions">
|
||||||
|
<button class="admin-button admin-button-view" onclick="viewTweet({{ tweet.id }})">View</button>
|
||||||
|
<button class="admin-button admin-button-delete" onclick="deleteTweet({{ tweet.id }})">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Groups</h2>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="groupSearch" placeholder="Search groups...">
|
||||||
|
</div>
|
||||||
|
<table class="admin-table" id="groupTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Members</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for group in groups %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ group.name }}</td>
|
||||||
|
<td>{{ group.description }}</td>
|
||||||
|
<td>{{ group.member_count }}</td>
|
||||||
|
<td>{{ group.created_at }}</td>
|
||||||
|
<td class="admin-actions">
|
||||||
|
<button class="admin-button admin-button-view" onclick="viewGroup({{ group.id }})">View</button>
|
||||||
|
<button class="admin-button admin-button-edit" onclick="editGroup({{ group.id }})">Edit</button>
|
||||||
|
<button class="admin-button admin-button-delete" onclick="deleteGroup({{ group.id }})">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function viewUser(userId) {
|
||||||
|
window.location.href = `/profile/${userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editUser(userId) {
|
||||||
|
const newUsername = prompt("Enter new username:");
|
||||||
|
const newEmail = prompt("Enter new email:");
|
||||||
|
if (newUsername && newEmail) {
|
||||||
|
fetch(`/admin/edit_user/${userId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username: newUsername, email: newEmail }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to edit user');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteUser(userId) {
|
||||||
|
if (confirm('Are you sure you want to delete this user?')) {
|
||||||
|
fetch(`/admin/delete_user/${userId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete user');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewTweet(tweetId) {
|
||||||
|
window.location.href = `/tweet/${tweetId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTweet(tweetId) {
|
||||||
|
if (confirm('Are you sure you want to delete this tweet?')) {
|
||||||
|
fetch(`/admin/delete_tweet/${tweetId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete tweet');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewGroup(groupId) {
|
||||||
|
window.location.href = `/group/${groupId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editGroup(groupId) {
|
||||||
|
const newName = prompt("Enter new group name:");
|
||||||
|
const newDescription = prompt("Enter new group description:");
|
||||||
|
if (newName && newDescription) {
|
||||||
|
fetch(`/admin/edit_group/${groupId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: newName, description: newDescription }),
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to edit group');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteGroup(groupId) {
|
||||||
|
if (confirm('Are you sure you want to delete this group?')) {
|
||||||
|
fetch(`/admin/delete_group/${groupId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete group');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchTable(inputId, tableId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const filter = input.value.toUpperCase();
|
||||||
|
const table = document.getElementById(tableId);
|
||||||
|
const tr = table.getElementsByTagName("tr");
|
||||||
|
|
||||||
|
for (let i = 1; i < tr.length; i++) {
|
||||||
|
let txtValue = tr[i].textContent || tr[i].innerText;
|
||||||
|
if (txtValue.toUpperCase().indexOf(filter) > -1) {
|
||||||
|
tr[i].style.display = "";
|
||||||
|
} else {
|
||||||
|
tr[i].style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("userSearch").addEventListener("keyup", () => searchTable("userSearch", "userTable"));
|
||||||
|
document.getElementById("tweetSearch").addEventListener("keyup", () => searchTable("tweetSearch", "tweetTable"));
|
||||||
|
document.getElementById("groupSearch").addEventListener("keyup", () => searchTable("groupSearch", "groupTable"));
|
||||||
|
|
||||||
|
let currentPage = 1;
|
||||||
|
const itemsPerPage = 10;
|
||||||
|
|
||||||
|
function changePage(direction) {
|
||||||
|
currentPage += direction;
|
||||||
|
if (currentPage < 1) currentPage = 1;
|
||||||
|
document.getElementById("currentPage").textContent = currentPage;
|
||||||
|
updateTableDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTableDisplay() {
|
||||||
|
const table = document.getElementById("userTable");
|
||||||
|
const tr = table.getElementsByTagName("tr");
|
||||||
|
const start = (currentPage - 1) * itemsPerPage + 1;
|
||||||
|
const end = start + itemsPerPage;
|
||||||
|
|
||||||
|
for (let i = 1; i < tr.length; i++) {
|
||||||
|
if (i >= start && i < end) {
|
||||||
|
tr[i].style.display = "";
|
||||||
|
} else {
|
||||||
|
tr[i].style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTableDisplay();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
50
templates/base.html
Normal file
50
templates/base.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<title>{% block title %}aTweet{% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>aTweet</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<a href="{{ url_for('index') }}" class="footer-button">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
{% if session.username %}
|
||||||
|
<a href="{{ url_for('profile', username=session.username) }}" class="footer-button">
|
||||||
|
<i class="fas fa-user"></i> Profile
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('groups') }}" class="footer-button">
|
||||||
|
<i class="fas fa-users"></i> Groups
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dms') }}" class="footer-button">
|
||||||
|
<i class="fas fa-envelope"></i> DMs
|
||||||
|
</a>
|
||||||
|
{% if session.user_id and get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('admin_panel') }}" class="footer-button">
|
||||||
|
<i class="fas fa-cog"></i> Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="footer-button">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
function toggleDarkMode() {
|
||||||
|
document.body.classList.toggle('dark-mode');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
77
templates/dm.html
Normal file
77
templates/dm.html
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Direct Messages - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
.messages-container {
|
||||||
|
height: 300px; /* Reduced by 25% from 400px */
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #38444d;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Direct Messages</h1>
|
||||||
|
<form action="{{ url_for('start_dm') }}" method="POST" class="dm-form">
|
||||||
|
<input type="text" name="username" placeholder="Enter username" required>
|
||||||
|
<button type="submit" class="btn btn-primary">Start Conversation</button>
|
||||||
|
</form>
|
||||||
|
<div class="messages-container" id="messagesContainer">
|
||||||
|
{% for dm in dms %}
|
||||||
|
<a href="{{ url_for('dm_conversation', username=dm.username) }}" class="dm-item">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (dm.pfp if dm.pfp else 'default_pfp.png')) }}" alt="{{ dm.username }}" class="dm-avatar">
|
||||||
|
<div class="dm-info">
|
||||||
|
<span class="dm-name">{{ dm.username }}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if has_more %}
|
||||||
|
<a href="{{ url_for('dms', page=page+1) }}" class="btn btn-primary load-more">Load More</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<a href="{{ url_for('index') }}" class="footer-button">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('profile', username=session.username) }}" class="footer-button">
|
||||||
|
<i class="fas fa-user"></i> Profile
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('groups') }}" class="footer-button">
|
||||||
|
<i class="fas fa-users"></i> Groups
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dms') }}" class="footer-button">
|
||||||
|
<i class="fas fa-envelope"></i> DMs
|
||||||
|
</a>
|
||||||
|
{% if session.user_id and get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('admin_panel') }}" class="footer-button">
|
||||||
|
<i class="fas fa-cog"></i> Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="footer-button">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
function scrollToBottom() {
|
||||||
|
const messagesContainer = document.getElementById('messagesContainer');
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this function when the page loads and after sending a new message
|
||||||
|
scrollToBottom();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
108
templates/dm_conversation.html
Normal file
108
templates/dm_conversation.html
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Conversation with {{ other_user.username }} - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
.messages-container {
|
||||||
|
height: 325px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #38444d;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Conversation with {{ other_user.username }}</h1>
|
||||||
|
<div class="messages-container" id="messageList">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="message-container {% if message.sender_id == session['user_id'] %}sent{% else %}received{% endif %}">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (message.pfp if message.pfp else 'default_pfp.png')) }}" alt="{{ message.username }}" class="message-avatar">
|
||||||
|
<div class="message-content">
|
||||||
|
<span class="message-username">{{ message.username }}</span>
|
||||||
|
<p>{{ message.content }}</p>
|
||||||
|
{% if message.image %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + message.image) }}" alt="Message image" class="message-image">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<form id="messageForm" action="{{ url_for('send_message', username=other_user.username) }}" method="POST" enctype="multipart/form-data" class="message-form">
|
||||||
|
<textarea name="content" placeholder="Type your message..." required></textarea>
|
||||||
|
<input type="file" name="image" accept="image/*">
|
||||||
|
<button type="submit" class="btn btn-primary">Send</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<a href="{{ url_for('index') }}" class="footer-button">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('profile', username=session.username) }}" class="footer-button">
|
||||||
|
<i class="fas fa-user"></i> Profile
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('groups') }}" class="footer-button">
|
||||||
|
<i class="fas fa-users"></i> Groups
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dms') }}" class="footer-button">
|
||||||
|
<i class="fas fa-envelope"></i> DMs
|
||||||
|
</a>
|
||||||
|
{% if session.user_id and get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('admin_panel') }}" class="footer-button">
|
||||||
|
<i class="fas fa-cog"></i> Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="footer-button">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let lastMessageId = 0;
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
const messageList = document.getElementById('messageList');
|
||||||
|
messageList.scrollTop = messageList.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNewMessages() {
|
||||||
|
$.get("{{ url_for('check_new_messages', username=other_user.username) }}?last_message_id=" + lastMessageId, function(data) {
|
||||||
|
if (data.success && data.messages && data.messages.length > 0) {
|
||||||
|
const messageList = document.getElementById('messageList');
|
||||||
|
data.messages.forEach(message => {
|
||||||
|
if (message.id > lastMessageId) {
|
||||||
|
const newMessage = `
|
||||||
|
<div class="message-container ${message.sender_id == {{ session['user_id'] }} ? 'sent' : 'received'}">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/') }}${message.pfp || 'default_pfp.png'}" alt="${message.username}" class="message-avatar">
|
||||||
|
<div class="message-content">
|
||||||
|
<span class="message-username">${message.username}</span>
|
||||||
|
<p>${message.content}</p>
|
||||||
|
<span class="message-timestamp">${message.created_at}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
messageList.insertAdjacentHTML('beforeend', newMessage);
|
||||||
|
lastMessageId = message.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom();
|
||||||
|
lastMessageId = {{ messages[-1].id if messages else 0 }};
|
||||||
|
|
||||||
|
setInterval(checkNewMessages, 200);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
49
templates/edit_group.html
Normal file
49
templates/edit_group.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Edit {{ group.name }} - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Edit Group</h1>
|
||||||
|
<form action="{{ url_for('edit_group', group_id=group.id) }}" method="POST" enctype="multipart/form-data" class="edit-group-form">
|
||||||
|
<input type="text" name="name" value="{{ group.name }}" required>
|
||||||
|
<textarea name="description">{{ group.description }}</textarea>
|
||||||
|
<input type="file" name="group_picture" accept="image/*">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</form>
|
||||||
|
<form action="{{ url_for('delete_group', group_id=group.id) }}" method="POST" class="delete-group-form">
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this group?')">Delete Group</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<a href="{{ url_for('index') }}" class="footer-button">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('profile', username=session.username) }}" class="footer-button">
|
||||||
|
<i class="fas fa-user"></i> Profile
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('groups') }}" class="footer-button">
|
||||||
|
<i class="fas fa-users"></i> Groups
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dms') }}" class="footer-button">
|
||||||
|
<i class="fas fa-envelope"></i> DMs
|
||||||
|
</a>
|
||||||
|
{% if session.user_id and get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('admin_panel') }}" class="footer-button">
|
||||||
|
<i class="fas fa-cog"></i> Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="footer-button">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
36
templates/edit_tweet.html
Normal file
36
templates/edit_tweet.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Edit Tweet - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Edit Tweet</h1>
|
||||||
|
<form action="{{ url_for('edit_tweet', tweet_id=tweet.id) }}" method="POST" class="tweet-form">
|
||||||
|
<textarea name="content" required maxlength="280">{{ tweet.content }}</textarea>
|
||||||
|
<div class="form-footer">
|
||||||
|
<span class="char-count">{{ 280 - tweet.content|length }}</span>
|
||||||
|
<button type="submit">Update Tweet</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const textarea = document.querySelector('textarea');
|
||||||
|
const charCount = document.querySelector('.char-count');
|
||||||
|
|
||||||
|
textarea.addEventListener('input', function() {
|
||||||
|
const remaining = 280 - this.value.length;
|
||||||
|
charCount.textContent = remaining;
|
||||||
|
if (remaining < 0) {
|
||||||
|
charCount.style.color = 'red';
|
||||||
|
} else {
|
||||||
|
charCount.style.color = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
80
templates/g.html
Normal file
80
templates/g.html
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Groups - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Groups</h1>
|
||||||
|
<button class="btn btn-primary" onclick="toggleNewGroupForm()">Create a new Group</button>
|
||||||
|
<div id="new-group-form" class="hidden overlay">
|
||||||
|
<div class="overlay-content">
|
||||||
|
<h2>Create a new Group</h2>
|
||||||
|
<form action="{{ url_for('new_group') }}" method="POST" enctype="multipart/form-data" onsubmit="return confirmGroupCreation()">
|
||||||
|
<input type="text" name="group_name" placeholder="Enter group name" required>
|
||||||
|
<input type="text" name="vanity_url" placeholder="Vanity URL (e.g., tech)" required>
|
||||||
|
<textarea name="description" placeholder="Group description"></textarea>
|
||||||
|
<input type="file" name="group_picture" accept="image/*">
|
||||||
|
<button type="submit" class="btn btn-success">Create Group</button>
|
||||||
|
<form action="{{ url_for('groups') }}" method="GET" class="search-form">
|
||||||
|
<input type="text" name="search" placeholder="Search groups" value="{{ search_query }}">
|
||||||
|
<button type="submit" class="btn btn-primary">Search</button>
|
||||||
|
</form>
|
||||||
|
</form>
|
||||||
|
<button class="btn btn-secondary" onclick="toggleNewGroupForm()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="group-list">
|
||||||
|
{% for group in groups %}
|
||||||
|
<a href="{{ url_for('group_detail', vanity_url=group.vanity_url) }}" class="group-item">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (group.avatar if group.avatar else 'default_pfp.png')) }}" alt="{{ group.name }}" class="group-avatar">
|
||||||
|
<div class="group-info">
|
||||||
|
<span class="group-name">{{ group.name | safe }}</span>
|
||||||
|
<span class="group-description">{{ group.description | safe }}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<a href="{{ url_for('index') }}" class="footer-button">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('profile', username=session.username) }}" class="footer-button">
|
||||||
|
<i class="fas fa-user"></i> Profile
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('groups') }}" class="footer-button">
|
||||||
|
<i class="fas fa-users"></i> Groups
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dms') }}" class="footer-button">
|
||||||
|
<i class="fas fa-envelope"></i> DMs
|
||||||
|
</a>
|
||||||
|
{% if session.user_id and get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('admin_panel') }}" class="footer-button">
|
||||||
|
<i class="fas fa-cog"></i> Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="footer-button">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
function toggleNewGroupForm() {
|
||||||
|
const form = document.getElementById('new-group-form');
|
||||||
|
form.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmGroupCreation() {
|
||||||
|
return confirm('Are you sure you want to create this group?');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
90
templates/group_detail.html
Normal file
90
templates/group_detail.html
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ group.name }} - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="{{ url_for('groups') }}" class="btn btn-secondary back-button"><i class="fas fa-arrow-left"></i> Back to Groups</a>
|
||||||
|
|
||||||
|
<div class="group-header">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (group.avatar if group.avatar else 'default_pfp.png')) }}" alt="{{ group.name }}" class="group-avatar">
|
||||||
|
<div class="group-info">
|
||||||
|
<h1>{{ group.name }}</h1>
|
||||||
|
<p>{{ group.description }}</p>
|
||||||
|
{% if session.user_id == group.owner_id or get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('edit_group', group_id=group.id) }}" class="btn btn-primary"><i class="fas fa-edit"></i> Edit Group</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if not is_member %}
|
||||||
|
<form action="{{ url_for('join_group', group_id=group.id) }}" method="POST">
|
||||||
|
<button type="submit" class="btn btn-success"><i class="fas fa-user-plus"></i> Join Group</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form action="{{ url_for('leave_group', group_id=group.id) }}" method="POST">
|
||||||
|
<button type="submit" class="btn btn-danger"><i class="fas fa-user-minus"></i> Leave Group</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group-content">
|
||||||
|
<h2>Posts</h2>
|
||||||
|
{% if is_member %}
|
||||||
|
<form action="{{ url_for('post_in_group', group_id=group.id) }}" method="POST" class="tweet-form">
|
||||||
|
<textarea name="content" placeholder="Write your post here..." required></textarea>
|
||||||
|
<div class="form-footer">
|
||||||
|
<span class="char-count">280</span>
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> Post</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="profile-info-container">
|
||||||
|
<p class="info-message">Join this group to post.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="post-list">
|
||||||
|
{% for post in posts %}
|
||||||
|
<div class="post">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (post.pfp if post.pfp else 'default_pfp.png')) }}" alt="{{ post.username }}" class="user-avatar">
|
||||||
|
<div class="post-content">
|
||||||
|
<span class="username">{{ post.username }}</span>
|
||||||
|
<p>{{ post.content }}</p>
|
||||||
|
<span class="timestamp">{{ post.created_at }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<a href="{{ url_for('index') }}" class="footer-button">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('profile', username=session.username) }}" class="footer-button">
|
||||||
|
<i class="fas fa-user"></i> Profile
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('groups') }}" class="footer-button">
|
||||||
|
<i class="fas fa-users"></i> Groups
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dms') }}" class="footer-button">
|
||||||
|
<i class="fas fa-envelope"></i> DMs
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('logout') }}" class="footer-button">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
{% if session.user_id and get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('admin_panel') }}" class="footer-button">
|
||||||
|
<i class="fas fa-cog"></i> Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
34
templates/hashtag.html
Normal file
34
templates/hashtag.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>#{{ hashtag }} - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>#{{ hashtag }}</h1>
|
||||||
|
<div class="tweets-list">
|
||||||
|
{% for tweet in tweets %}
|
||||||
|
<div class="tweet">
|
||||||
|
<div class="tweet-header">
|
||||||
|
<div class="tweet-user-info">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (tweet.user_pfp if tweet.user_pfp else 'default_pfp.png')) }}" alt="{{ tweet.username }}" class="tweet-pfp">
|
||||||
|
<div class="tweet-info">
|
||||||
|
<a href="{{ url_for('profile', username=tweet.username) }}" class="tweet-username">{{ tweet.username }}</a>
|
||||||
|
<span class="tweet-timestamp">{{ tweet.created_at }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="tweet-content tweet-text-cull">{{ tweet.content|safe }}</p>
|
||||||
|
{% if tweet.image %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + tweet.image) }}" alt="Tweet image" class="tweet-image">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('index') }}">Back to Home</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
198
templates/index.html
Normal file
198
templates/index.html
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>aTweet - Home</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<style>
|
||||||
|
.tweet-content a.hashtag {
|
||||||
|
color: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-content a.mention {
|
||||||
|
color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-content a.hashtag:hover {
|
||||||
|
color: #25a25a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-content a.mention:hover {
|
||||||
|
color: #1e8449;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>aTweet</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<form action="{{ url_for('search') }}" method="GET" class="search-form">
|
||||||
|
<input type="text" name="q" placeholder="Search users...">
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="{{ url_for('tweet') }}" method="POST" class="tweet-form" enctype="multipart/form-data">
|
||||||
|
<textarea name="content" required placeholder="What's happening?" maxlength="280"></textarea>
|
||||||
|
<div class="form-footer">
|
||||||
|
<span class="char-count">280</span>
|
||||||
|
<button type="submit">Tweet</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="tweets-list">
|
||||||
|
{% for tweet in tweets %}
|
||||||
|
<div class="tweet">
|
||||||
|
<div class="tweet-header">
|
||||||
|
<div class="tweet-user-info">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (tweet.user_pfp if tweet.user_pfp else 'default_pfp.png')) }}" alt="{{ tweet.username }}" class="tweet-pfp">
|
||||||
|
<div class="tweet-info">
|
||||||
|
<a href="{{ url_for('profile', username=tweet.username) }}" class="tweet-username">{{ tweet.username }}</a>
|
||||||
|
<span class="tweet-timestamp">{{ tweet.created_at.strftime('%b %d, %Y, %I:%M %p') }} (UTC +2)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if tweet.user_id == session.user_id %}
|
||||||
|
<div class="tweet-options">
|
||||||
|
<button class="options-button" onclick="toggleMenu({{ tweet.id }})">
|
||||||
|
<i class="fas fa-ellipsis-h"></i>
|
||||||
|
</button>
|
||||||
|
<div id="menu-{{ tweet.id }}" class="tweet-menu">
|
||||||
|
<button onclick="editTweet({{ tweet.id }})">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteTweet({{ tweet.id }})">
|
||||||
|
<i class="fas fa-trash-alt"></i> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="tweet-content tweet-text-cull">{{ tweet.displayed_content|safe }}</p>
|
||||||
|
{% if tweet.image %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + tweet.image) }}" alt="Tweet image" class="tweet-image">
|
||||||
|
{% endif %}
|
||||||
|
<div class="tweet-actions">
|
||||||
|
<button class="like-button {% if tweet.id in liked_tweet_ids %}liked{% endif %}" data-tweet-id="{{ tweet.id }}">
|
||||||
|
<i class="fas fa-heart"></i> {{ tweet.likes }}
|
||||||
|
</button>
|
||||||
|
<button class="comment-button" data-tweet-id="{{ tweet.id }}">
|
||||||
|
<i class="fas fa-comment"></i> Comment
|
||||||
|
</button>
|
||||||
|
<!-- Add retweet button -->
|
||||||
|
<form action="{{ url_for('retweet', tweet_id=tweet.id) }}" method="POST" style="display: inline;">
|
||||||
|
<button type="submit" class="retweet-button">
|
||||||
|
<i class="fas fa-retweet"></i> Retweet
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% if tweet.original_tweet_id %}
|
||||||
|
<div class="retweet-info">
|
||||||
|
Retweeted from <a href="{{ url_for('tweet_detail', tweet_id=tweet.original_tweet_id) }}">original tweet</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<a href="{{ url_for('index') }}" class="footer-button">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('profile', username=session.username) }}" class="footer-button">
|
||||||
|
<i class="fas fa-user"></i> Profile
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('groups') }}" class="footer-button">
|
||||||
|
<i class="fas fa-users"></i> Groups
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dms') }}" class="footer-button">
|
||||||
|
<i class="fas fa-envelope"></i> DMs
|
||||||
|
</a>
|
||||||
|
{% if session.user_id and get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('admin_panel') }}" class="footer-button">
|
||||||
|
<i class="fas fa-cog"></i> Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="footer-button">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
function toggleMenu(tweetId) {
|
||||||
|
const menu = document.getElementById(`menu-${tweetId}`);
|
||||||
|
menu.classList.toggle('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function editTweet(tweetId) {
|
||||||
|
window.location.href = `/edit_tweet/${tweetId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTweet(tweetId) {
|
||||||
|
if (confirm('Are you sure you want to delete this tweet?')) {
|
||||||
|
fetch(`/delete_tweet/${tweetId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const tweetElement = document.querySelector(`.tweet[data-tweet-id="${tweetId}"]`);
|
||||||
|
if (tweetElement) {
|
||||||
|
tweetElement.remove();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete tweet');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commentOnTweet(tweetId) {
|
||||||
|
window.location.href = `/tweet/${tweetId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function likeTweet(tweetId) {
|
||||||
|
fetch(`/like/${tweetId}`, { method: 'POST' })
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const likeButton = document.querySelector(`.like-button[data-tweet-id="${tweetId}"]`);
|
||||||
|
likeButton.classList.toggle('liked');
|
||||||
|
const likeCount = likeButton.querySelector('i').nextSibling;
|
||||||
|
likeCount.textContent = ` ${data.likes}`;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Failed to like tweet');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Failed to like tweet: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const likeButtons = document.querySelectorAll('.like-button');
|
||||||
|
const commentButtons = document.querySelectorAll('.comment-button');
|
||||||
|
|
||||||
|
likeButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const tweetId = this.getAttribute('data-tweet-id');
|
||||||
|
likeTweet(tweetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
commentButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const tweetId = this.getAttribute('data-tweet-id');
|
||||||
|
commentOnTweet(tweetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
15
templates/login.html
Normal file
15
templates/login.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Login - aTweet{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>Login to aTweet</h1>
|
||||||
|
<form action="{{ url_for('login') }}" method="POST" class="auth-form">
|
||||||
|
<input type="text" name="username" placeholder="Username" required>
|
||||||
|
<input type="password" name="password" placeholder="Password" required>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<p>Don't have an account? <a href="{{ url_for('signup') }}">Sign up</a></p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
203
templates/profile.html
Normal file
203
templates/profile.html
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ user.username }}'s Profile - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="profile-info-container">
|
||||||
|
<div class="banner-container">
|
||||||
|
<img class="profile-banner" src="{{ url_for('static', filename='uploads/' + user.banner) if user.banner else url_for('static', filename='default_banner.jpg') }}" alt="Profile Banner">
|
||||||
|
<div class="profile-pfp-container">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (user.pfp if user.pfp else 'default_pfp.png')) }}" alt="Profile Picture" class="profile-pfp">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="profile-info">
|
||||||
|
<div class="profile-header">
|
||||||
|
<h2 class="profile-username">
|
||||||
|
{{ user.username }}
|
||||||
|
{% if user.username == 'avery' %}
|
||||||
|
<span class="crown-icon" title="Owner">👑</span>
|
||||||
|
<span class="badge" title="Mod">🛡️</span>
|
||||||
|
<span class="badge" title="Dev">🛠️</span>
|
||||||
|
<span class="badge" title="Verified">⭐</span>
|
||||||
|
<span class="badge" title="Bug Finder">🪳</span>
|
||||||
|
{% elif user.username == 'asd' %}
|
||||||
|
<span class="badge" title="Test User">🧪</span>
|
||||||
|
<span class="badge" title="Verified">⭐</span>
|
||||||
|
<span class="badge" title="Bug Finder">🪳</span>
|
||||||
|
{% elif user.username == 'sqtt' %}
|
||||||
|
<span class="badge" title="Verified">⭐</span>
|
||||||
|
<span class="badge" title="Bug Finder">🪳</span>
|
||||||
|
{% elif user.username == 'Andrecon' %}
|
||||||
|
<span class="badge" title="Verified">⭐</span>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p class="join-date">Joined: {{ user.created_at.strftime('%B %d, %Y') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if session.user_id == user.id %}
|
||||||
|
<div class="profile-edit-forms">
|
||||||
|
<form action="{{ url_for('change_profile_picture') }}" method="POST" enctype="multipart/form-data" class="edit-form">
|
||||||
|
<input type="file" name="profile_picture" accept="image/*" required>
|
||||||
|
<button type="submit" class="btn btn-primary">Change Profile Picture</button>
|
||||||
|
</form>
|
||||||
|
<form action="{{ url_for('change_banner') }}" method="POST" enctype="multipart/form-data" class="edit-form">
|
||||||
|
<input type="file" name="banner" accept="image/*" required>
|
||||||
|
<button type="submit" class="btn btn-primary">Change Banner</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>Tweets</h2>
|
||||||
|
<div class="tweets-list">
|
||||||
|
{% for tweet in tweets %}
|
||||||
|
<div class="tweet">
|
||||||
|
<div class="tweet-header">
|
||||||
|
<div class="tweet-user-info">
|
||||||
|
<img class="tweet-pfp" src="{{ url_for('static', filename='uploads/' + (user.pfp if user.pfp else 'default_pfp.png')) }}" alt="Profile Picture">
|
||||||
|
<div class="tweet-info">
|
||||||
|
<h3 class="tweet-username">{{ user.username }}</h3>
|
||||||
|
<span class="tweet-date">{{ tweet.created_at.strftime('%B %d, %Y at %I:%M %p') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if tweet.user_id == session.user_id %}
|
||||||
|
<div class="tweet-options">
|
||||||
|
<button class="options-button" onclick="toggleMenu({{ tweet.id }})">
|
||||||
|
<i class="fas fa-ellipsis-h"></i>
|
||||||
|
</button>
|
||||||
|
<div id="menu-{{ tweet.id }}" class="tweet-menu">
|
||||||
|
<button onclick="editTweet({{ tweet.id }})">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteTweet({{ tweet.id }})">
|
||||||
|
<i class="fas fa-trash-alt"></i> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="tweet-content">{{ tweet.content }}</p>
|
||||||
|
<div class="tweet-actions">
|
||||||
|
<button class="like-button {% if tweet.id in liked_tweet_ids %}liked{% endif %}" data-tweet-id="{{ tweet.id }}">
|
||||||
|
<i class="fas fa-heart"></i> {{ tweet.likes }}
|
||||||
|
</button>
|
||||||
|
<button class="comment-button" data-tweet-id="{{ tweet.id }}">
|
||||||
|
<i class="fas fa-comment"></i> Comment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="footer-container">
|
||||||
|
<a href="{{ url_for('index') }}" class="footer-button">
|
||||||
|
<i class="fas fa-home"></i> Home
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('profile', username=session.username) }}" class="footer-button">
|
||||||
|
<i class="fas fa-user"></i> Profile
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('groups') }}" class="footer-button">
|
||||||
|
<i class="fas fa-users"></i> Groups
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('dms') }}" class="footer-button">
|
||||||
|
<i class="fas fa-envelope"></i> DMs
|
||||||
|
</a>
|
||||||
|
{% if session.user_id and get_user_by_id(session.user_id)['username'] == 'avery' %}
|
||||||
|
<a href="{{ url_for('admin_panel') }}" class="footer-button">
|
||||||
|
<i class="fas fa-cog"></i> Admin Panel
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('logout') }}" class="footer-button">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
function toggleMenu(tweetId) {
|
||||||
|
const menu = document.getElementById(`menu-${tweetId}`);
|
||||||
|
menu.classList.toggle('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function editTweet(tweetId) {
|
||||||
|
window.location.href = `/edit_tweet/${tweetId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTweet(tweetId) {
|
||||||
|
if (confirm('Are you sure you want to delete this tweet?')) {
|
||||||
|
fetch(`/delete_tweet/${tweetId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete tweet');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function likeTweet(tweetId) {
|
||||||
|
fetch(`/like/${tweetId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const likeButton = document.querySelector(`.like-button[data-tweet-id="${tweetId}"]`);
|
||||||
|
likeButton.classList.toggle('liked');
|
||||||
|
const likeCount = likeButton.querySelector('i').nextSibling;
|
||||||
|
likeCount.textContent = ` ${data.likes}`;
|
||||||
|
} else {
|
||||||
|
alert('Failed to like tweet');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const likeButtons = document.querySelectorAll('.like-button');
|
||||||
|
const commentButtons = document.querySelectorAll('.comment-button');
|
||||||
|
|
||||||
|
likeButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const tweetId = this.getAttribute('data-tweet-id');
|
||||||
|
likeTweet(tweetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
commentButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const tweetId = this.getAttribute('data-tweet-id');
|
||||||
|
commentOnTweet(tweetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners to like and comment buttons
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const likeButtons = document.querySelectorAll('.like-button');
|
||||||
|
const commentButtons = document.querySelectorAll('.comment-button');
|
||||||
|
|
||||||
|
likeButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const tweetId = this.getAttribute('data-tweet-id');
|
||||||
|
likeTweet(tweetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
commentButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const tweetId = this.getAttribute('data-tweet-id');
|
||||||
|
commentOnTweet(tweetId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
23
templates/search_results.html
Normal file
23
templates/search_results.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Search Results - aTweet</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Search Results for "{{ query }}"</h1>
|
||||||
|
<div class="user-list">
|
||||||
|
{% for user in users %}
|
||||||
|
<div class="user-item">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' + (user.pfp if user.pfp else 'default_pfp.png')) }}" alt="{{ user.username }}" class="user-pfp">
|
||||||
|
<a href="{{ url_for('profile', username=user.username) }}">{{ user.username }}</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('index') }}">Back to Home</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
templates/signup.html
Normal file
16
templates/signup.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Sign Up - aTweet{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>Sign Up for aTweet</h1>
|
||||||
|
<form action="{{ url_for('signup') }}" method="POST" class="auth-form">
|
||||||
|
<input type="text" name="username" placeholder="Username" required>
|
||||||
|
<input type="email" name="email" placeholder="Email" required>
|
||||||
|
<input type="password" name="password" placeholder="Password" required>
|
||||||
|
<button type="submit">Sign Up</button>
|
||||||
|
</form>
|
||||||
|
<p>Already have an account? <a href="{{ url_for('login') }}">Login</a></p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
68
templates/tweet_detail.html
Normal file
68
templates/tweet_detail.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Tweet Detail - aTweet{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="tweet-detail">
|
||||||
|
<h3>{{ tweet.username }}</h3>
|
||||||
|
<p>{{ tweet.content }}</p>
|
||||||
|
<button class="like-button {% if tweet.id in liked_tweet_ids %}liked{% endif %}" data-tweet-id="{{ tweet.id }}">
|
||||||
|
<i class="fas fa-heart"></i> <span class="like-count">{{ tweet.likes }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h4>Comments</h4>
|
||||||
|
<form id="comment-form">
|
||||||
|
<textarea name="content" rows="2" placeholder="Add a comment" required></textarea>
|
||||||
|
<button type="submit">Comment</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="comments-container">
|
||||||
|
{% for comment in comments %}
|
||||||
|
<p>{{ comment.username }}: {{ comment.content }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function likeTweet(tweetId) {
|
||||||
|
fetch(`/like/${tweetId}`, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const likeButton = document.querySelector('.like-button');
|
||||||
|
likeButton.classList.toggle('liked');
|
||||||
|
document.querySelector('.like-count').textContent = data.likes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('.like-button').addEventListener('click', function() {
|
||||||
|
const tweetId = this.getAttribute('data-tweet-id');
|
||||||
|
likeTweet(tweetId);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('comment-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const content = this.content.value;
|
||||||
|
const tweetId = {{ tweet.id }};
|
||||||
|
|
||||||
|
fetch(`/comment/${tweetId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `content=${encodeURIComponent(content)}`
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const commentsContainer = document.getElementById('comments-container');
|
||||||
|
commentsContainer.innerHTML += `<p>{{ tweet.username }}: ${content}</p>`;
|
||||||
|
this.content.value = '';
|
||||||
|
} else {
|
||||||
|
alert('Failed to add comment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user