Saved your changes before starting work
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 5e584ab0-c340-4432-97ef-1972582b60e9
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6d4dbe7c-69e4-4510-bd62-638ff9c78d5c
Replit-Restored-To: befc3160be
34
.replit
@@ -1,4 +1,4 @@
|
|||||||
modules = ["web", "python-3.12"]
|
modules = ["web", "python-3.12", "postgresql-16"]
|
||||||
run = "python3 app.py"
|
run = "python3 app.py"
|
||||||
|
|
||||||
[nix]
|
[nix]
|
||||||
@@ -10,5 +10,35 @@ deploymentTarget = "cloudrun"
|
|||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 5000
|
localPort = 5000
|
||||||
|
externalPort = 80
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 44245
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
exposeLocalhost = true
|
|
||||||
|
[agent]
|
||||||
|
expertMode = true
|
||||||
|
|
||||||
|
[workflows]
|
||||||
|
runButton = "Project"
|
||||||
|
|
||||||
|
[[workflows.workflow]]
|
||||||
|
name = "Project"
|
||||||
|
mode = "parallel"
|
||||||
|
author = "agent"
|
||||||
|
|
||||||
|
[[workflows.workflow.tasks]]
|
||||||
|
task = "workflow.run"
|
||||||
|
args = "Flask Server"
|
||||||
|
|
||||||
|
[[workflows.workflow]]
|
||||||
|
name = "Flask Server"
|
||||||
|
author = "agent"
|
||||||
|
|
||||||
|
[[workflows.workflow.tasks]]
|
||||||
|
task = "shell.exec"
|
||||||
|
args = "python app.py"
|
||||||
|
waitForPort = 5000
|
||||||
|
|
||||||
|
[workflows.workflow.metadata]
|
||||||
|
outputType = "webview"
|
||||||
|
|||||||
335
app.py
@@ -1,20 +1,43 @@
|
|||||||
# TODO
|
import os
|
||||||
# Fix all text sizes across all pages except competition log
|
import psycopg2
|
||||||
# Fix all top margins (do 20ish vh instead)
|
from psycopg2.extras import RealDictCursor
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
from flask import Flask, render_template
|
from datetime import datetime
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.environ.get('SECRET_KEY', 'techturb-secret-key-change-in-production')
|
||||||
|
app.config['UPLOAD_FOLDER'] = 'static/images'
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
||||||
|
|
||||||
|
ADMIN_PASSWORD = 'techturb123'
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
conn = psycopg2.connect(os.environ['DATABASE_URL'])
|
||||||
|
return conn
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@app.route('/home')
|
@app.route('/home')
|
||||||
def home():
|
def home():
|
||||||
return render_template('home.html')
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
cur.execute('SELECT * FROM stats ORDER BY key')
|
||||||
|
stats = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('home.html', stats=stats)
|
||||||
|
|
||||||
@app.route('/contributors')
|
@app.route('/contributors')
|
||||||
def contributors():
|
def contributors():
|
||||||
return render_template('contributors.html')
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
cur.execute('SELECT * FROM members ORDER BY display_order')
|
||||||
|
members = cur.fetchall()
|
||||||
|
cur.execute('SELECT * FROM mentors ORDER BY display_order')
|
||||||
|
mentors = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('contributors.html', members=members, mentors=mentors)
|
||||||
|
|
||||||
@app.route('/robot')
|
@app.route('/robot')
|
||||||
def robot():
|
def robot():
|
||||||
@@ -22,11 +45,21 @@ def robot():
|
|||||||
|
|
||||||
@app.route('/competitions')
|
@app.route('/competitions')
|
||||||
def competitions():
|
def competitions():
|
||||||
return render_template('competitions.html')
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
cur.execute('SELECT * FROM competitions ORDER BY display_order')
|
||||||
|
competitions = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
# @app.route('/awards')
|
competitions_by_season = {}
|
||||||
# def awards():
|
for comp in competitions:
|
||||||
# return render_template('awards.html')
|
season = comp['season']
|
||||||
|
if season not in competitions_by_season:
|
||||||
|
competitions_by_season[season] = []
|
||||||
|
competitions_by_season[season].append(comp)
|
||||||
|
|
||||||
|
return render_template('competitions.html', competitions_by_season=competitions_by_season)
|
||||||
|
|
||||||
@app.route('/contact')
|
@app.route('/contact')
|
||||||
def contact():
|
def contact():
|
||||||
@@ -34,7 +67,281 @@ def contact():
|
|||||||
|
|
||||||
@app.route('/sponsors')
|
@app.route('/sponsors')
|
||||||
def sponsors():
|
def sponsors():
|
||||||
return render_template('sponsors.html')
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
cur.execute('SELECT * FROM sponsors ORDER BY display_order')
|
||||||
|
sponsors = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('sponsors.html', sponsors=sponsors)
|
||||||
|
|
||||||
|
@app.route('/robots')
|
||||||
|
@app.route('/robots/<type>')
|
||||||
|
def robots(type=None):
|
||||||
|
if type is None:
|
||||||
|
return render_template('robots.html')
|
||||||
|
else:
|
||||||
|
return render_template(f'robots-{type}.html')
|
||||||
|
|
||||||
|
@app.route('/admin/login', methods=['GET', 'POST'])
|
||||||
|
def admin_login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
password = request.form.get('password')
|
||||||
|
if password == ADMIN_PASSWORD:
|
||||||
|
session['admin_logged_in'] = True
|
||||||
|
return redirect(url_for('admin_stats'))
|
||||||
|
else:
|
||||||
|
flash('Incorrect password', 'error')
|
||||||
|
return render_template('admin/login.html')
|
||||||
|
|
||||||
|
@app.route('/admin/logout')
|
||||||
|
def admin_logout():
|
||||||
|
session.pop('admin_logged_in', None)
|
||||||
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
|
def admin_required(f):
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if not session.get('admin_logged_in'):
|
||||||
|
return redirect(url_for('admin_login'))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
decorated_function.__name__ = f.__name__
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
@app.route('/admin/stats')
|
||||||
|
@admin_required
|
||||||
|
def admin_stats():
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
cur.execute('SELECT * FROM stats ORDER BY key')
|
||||||
|
stats = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('admin/stats.html', stats=stats)
|
||||||
|
|
||||||
|
@app.route('/admin/stats/update', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def update_stat():
|
||||||
|
stat_id = request.form.get('id')
|
||||||
|
value = request.form.get('value')
|
||||||
|
label = request.form.get('label')
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('UPDATE stats SET value = %s, label = %s WHERE id = %s', (value, label, stat_id))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
flash('Stat updated successfully', 'success')
|
||||||
|
return redirect(url_for('admin_stats'))
|
||||||
|
|
||||||
|
@app.route('/admin/members')
|
||||||
|
@admin_required
|
||||||
|
def admin_members():
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
cur.execute('SELECT * FROM members ORDER BY display_order')
|
||||||
|
members = cur.fetchall()
|
||||||
|
cur.execute('SELECT * FROM mentors ORDER BY display_order')
|
||||||
|
mentors = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('admin/members.html', members=members, mentors=mentors)
|
||||||
|
|
||||||
|
@app.route('/admin/member/add', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def add_member():
|
||||||
|
name = request.form.get('name')
|
||||||
|
role = request.form.get('role')
|
||||||
|
member_type = request.form.get('type')
|
||||||
|
|
||||||
|
image_path = 'images/default.jpg'
|
||||||
|
if 'image' in request.files:
|
||||||
|
file = request.files['image']
|
||||||
|
if file and file.filename:
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||||
|
image_path = f'images/{filename}'
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
if member_type == 'mentor':
|
||||||
|
cur.execute('INSERT INTO mentors (name, role, image_path) VALUES (%s, %s, %s)', (name, role, image_path))
|
||||||
|
else:
|
||||||
|
cur.execute('INSERT INTO members (name, role, image_path) VALUES (%s, %s, %s)', (name, role, image_path))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
flash(f'{"Mentor" if member_type == "mentor" else "Member"} added successfully', 'success')
|
||||||
|
return redirect(url_for('admin_members'))
|
||||||
|
|
||||||
|
@app.route('/admin/member/update', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def update_member():
|
||||||
|
member_id = request.form.get('id')
|
||||||
|
name = request.form.get('name')
|
||||||
|
role = request.form.get('role')
|
||||||
|
member_type = request.form.get('type')
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
image_path = None
|
||||||
|
if 'image' in request.files:
|
||||||
|
file = request.files['image']
|
||||||
|
if file and file.filename:
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||||
|
image_path = f'images/{filename}'
|
||||||
|
|
||||||
|
if member_type == 'mentor':
|
||||||
|
if image_path:
|
||||||
|
cur.execute('UPDATE mentors SET name = %s, role = %s, image_path = %s WHERE id = %s', (name, role, image_path, member_id))
|
||||||
|
else:
|
||||||
|
cur.execute('UPDATE mentors SET name = %s, role = %s WHERE id = %s', (name, role, member_id))
|
||||||
|
else:
|
||||||
|
if image_path:
|
||||||
|
cur.execute('UPDATE members SET name = %s, role = %s, image_path = %s WHERE id = %s', (name, role, image_path, member_id))
|
||||||
|
else:
|
||||||
|
cur.execute('UPDATE members SET name = %s, role = %s WHERE id = %s', (name, role, member_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
flash(f'{"Mentor" if member_type == "mentor" else "Member"} updated successfully', 'success')
|
||||||
|
return redirect(url_for('admin_members'))
|
||||||
|
|
||||||
|
@app.route('/admin/member/delete', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def delete_member():
|
||||||
|
member_id = request.form.get('id')
|
||||||
|
member_type = request.form.get('type')
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
if member_type == 'mentor':
|
||||||
|
cur.execute('DELETE FROM mentors WHERE id = %s', (member_id,))
|
||||||
|
else:
|
||||||
|
cur.execute('DELETE FROM members WHERE id = %s', (member_id,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
flash(f'{"Mentor" if member_type == "mentor" else "Member"} deleted successfully', 'success')
|
||||||
|
return redirect(url_for('admin_members'))
|
||||||
|
|
||||||
|
@app.route('/admin/competitions')
|
||||||
|
@admin_required
|
||||||
|
def admin_competitions():
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
cur.execute('SELECT * FROM competitions ORDER BY display_order')
|
||||||
|
competitions = cur.fetchall()
|
||||||
|
cur.execute('SELECT DISTINCT season FROM competitions ORDER BY season DESC')
|
||||||
|
seasons = [row['season'] for row in cur.fetchall()]
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('admin/competitions.html', competitions=competitions, seasons=seasons)
|
||||||
|
|
||||||
|
@app.route('/admin/competition/add', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def add_competition():
|
||||||
|
season = request.form.get('season')
|
||||||
|
event_name = request.form.get('event_name')
|
||||||
|
date = request.form.get('date')
|
||||||
|
description = request.form.get('description')
|
||||||
|
awards_raw = request.form.get('awards')
|
||||||
|
awards = '|'.join([line.strip() for line in awards_raw.split('\n') if line.strip()])
|
||||||
|
|
||||||
|
image_path = None
|
||||||
|
if 'image' in request.files:
|
||||||
|
file = request.files['image']
|
||||||
|
if file and file.filename:
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||||
|
image_path = f'images/{filename}'
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('INSERT INTO competitions (season, event_name, date, description, awards, image_path) VALUES (%s, %s, %s, %s, %s, %s)',
|
||||||
|
(season, event_name, date, description, awards, image_path))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
flash('Competition added successfully', 'success')
|
||||||
|
return redirect(url_for('admin_competitions'))
|
||||||
|
|
||||||
|
@app.route('/admin/competition/delete', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def delete_competition():
|
||||||
|
competition_id = request.form.get('id')
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('DELETE FROM competitions WHERE id = %s', (competition_id,))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
flash('Competition deleted successfully', 'success')
|
||||||
|
return redirect(url_for('admin_competitions'))
|
||||||
|
|
||||||
|
@app.route('/admin/sponsors')
|
||||||
|
@admin_required
|
||||||
|
def admin_sponsors():
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||||
|
cur.execute('SELECT * FROM sponsors ORDER BY display_order')
|
||||||
|
sponsors = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('admin/sponsors.html', sponsors=sponsors)
|
||||||
|
|
||||||
|
@app.route('/admin/sponsor/add', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def add_sponsor():
|
||||||
|
name = request.form.get('name')
|
||||||
|
website_url = request.form.get('website_url')
|
||||||
|
|
||||||
|
logo_path = None
|
||||||
|
if 'logo' in request.files:
|
||||||
|
file = request.files['logo']
|
||||||
|
if file and file.filename:
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
||||||
|
logo_path = f'images/{filename}'
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('INSERT INTO sponsors (name, logo_path, website_url) VALUES (%s, %s, %s)', (name, logo_path, website_url))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
flash('Sponsor added successfully', 'success')
|
||||||
|
return redirect(url_for('admin_sponsors'))
|
||||||
|
|
||||||
|
@app.route('/admin/sponsor/delete', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def delete_sponsor():
|
||||||
|
sponsor_id = request.form.get('id')
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('DELETE FROM sponsors WHERE id = %s', (sponsor_id,))
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
flash('Sponsor deleted successfully', 'success')
|
||||||
|
return redirect(url_for('admin_sponsors'))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
|
|||||||
BIN
attached_assets/image_1757182125664.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
attached_assets/image_1759456988487.png
Normal file
|
After Width: | Height: | Size: 745 KiB |
BIN
attached_assets/image_1759457109169.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
attached_assets/image_1759465591069.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
attached_assets/image_1759465825189.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
attached_assets/image_1759465917104.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
attached_assets/image_1759467028622.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
attached_assets/image_1759467055284.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
attached_assets/image_1759467229465.png
Normal file
|
After Width: | Height: | Size: 715 KiB |
BIN
attached_assets/image_1759467381587.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
attached_assets/image_1759467488103.png
Normal file
|
After Width: | Height: | Size: 919 KiB |
BIN
attached_assets/image_1759467714189.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
attached_assets/image_1759467825268.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
attached_assets/image_1759468904010.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
attached_assets/image_1759468987127.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
attached_assets/image_1759469180396.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
attached_assets/image_1759501420568.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
attached_assets/image_1759536015888.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
attached_assets/image_1759536296815.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
attached_assets/image_1759536393787.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
attached_assets/image_1759604797997.png
Normal file
|
After Width: | Height: | Size: 513 KiB |
BIN
attached_assets/image_1759604912005.png
Normal file
|
After Width: | Height: | Size: 704 KiB |
BIN
attached_assets/image_1759605102403.png
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
attached_assets/image_1759605344760.png
Normal file
|
After Width: | Height: | Size: 348 KiB |
BIN
attached_assets/image_1759605424150.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
attached_assets/image_1759605526872.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
attached_assets/image_1759605583069.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
attached_assets/image_1759605759890.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
attached_assets/image_1759605767045.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
178
poetry.lock
generated
@@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.5.4 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.5.6 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blinker"
|
name = "blinker"
|
||||||
@@ -11,6 +11,17 @@ files = [
|
|||||||
{file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"},
|
{file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachelib"
|
||||||
|
version = "0.13.0"
|
||||||
|
description = "A collection of cache libraries in the same API interface."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "cachelib-0.13.0-py3-none-any.whl", hash = "sha256:8c8019e53b6302967d4e8329a504acf75e7bc46130291d30188a6e4e58162516"},
|
||||||
|
{file = "cachelib-0.13.0.tar.gz", hash = "sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.7"
|
version = "8.1.7"
|
||||||
@@ -58,6 +69,30 @@ Werkzeug = ">=3.0.0"
|
|||||||
async = ["asgiref (>=3.2)"]
|
async = ["asgiref (>=3.2)"]
|
||||||
dotenv = ["python-dotenv"]
|
dotenv = ["python-dotenv"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask-session"
|
||||||
|
version = "0.8.0"
|
||||||
|
description = "Server-side session support for Flask"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "flask_session-0.8.0-py3-none-any.whl", hash = "sha256:5dae6e9ddab334f8dc4dea4305af37851f4e7dc0f484caf3351184001195e3b7"},
|
||||||
|
{file = "flask_session-0.8.0.tar.gz", hash = "sha256:20e045eb01103694e70be4a49f3a80dbb1b57296a22dc6f44bbf3f83ef0742ff"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cachelib = "*"
|
||||||
|
flask = ">=2.2"
|
||||||
|
msgspec = ">=0.18.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
all = ["Flask-Session[cachelib,memcached,mongodb,redis,sqlalchemy]"]
|
||||||
|
cachelib = ["cachelib (>=0.10.2)"]
|
||||||
|
memcached = ["pymemcache"]
|
||||||
|
mongodb = ["pymongo (>=4.6.2)"]
|
||||||
|
redis = ["redis (>=5.0.3)"]
|
||||||
|
sqlalchemy = ["flask-sqlalchemy (>=3.0.5)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itsdangerous"
|
name = "itsdangerous"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@@ -156,14 +191,143 @@ files = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "werkzeug"
|
name = "msgspec"
|
||||||
version = "3.0.3"
|
version = "0.19.0"
|
||||||
description = "The comprehensive WSGI web application library."
|
description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259"},
|
||||||
|
{file = "msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36"},
|
||||||
|
{file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947"},
|
||||||
|
{file = "msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909"},
|
||||||
|
{file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a"},
|
||||||
|
{file = "msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633"},
|
||||||
|
{file = "msgspec-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90"},
|
||||||
|
{file = "msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e"},
|
||||||
|
{file = "msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551"},
|
||||||
|
{file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7"},
|
||||||
|
{file = "msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011"},
|
||||||
|
{file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063"},
|
||||||
|
{file = "msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716"},
|
||||||
|
{file = "msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c"},
|
||||||
|
{file = "msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f"},
|
||||||
|
{file = "msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2"},
|
||||||
|
{file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12"},
|
||||||
|
{file = "msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc"},
|
||||||
|
{file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c"},
|
||||||
|
{file = "msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537"},
|
||||||
|
{file = "msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0"},
|
||||||
|
{file = "msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86"},
|
||||||
|
{file = "msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314"},
|
||||||
|
{file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e"},
|
||||||
|
{file = "msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5"},
|
||||||
|
{file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9"},
|
||||||
|
{file = "msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327"},
|
||||||
|
{file = "msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f"},
|
||||||
|
{file = "msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044"},
|
||||||
|
{file = "msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229"},
|
||||||
|
{file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12"},
|
||||||
|
{file = "msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446"},
|
||||||
|
{file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19"},
|
||||||
|
{file = "msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db"},
|
||||||
|
{file = "msgspec-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe"},
|
||||||
|
{file = "msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["attrs", "coverage", "eval-type-backport", "furo", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli_w"]
|
||||||
|
doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"]
|
||||||
|
test = ["attrs", "eval-type-backport", "msgpack", "pytest", "pyyaml", "tomli", "tomli_w"]
|
||||||
|
toml = ["tomli", "tomli_w"]
|
||||||
|
yaml = ["pyyaml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2-binary"
|
||||||
|
version = "2.9.10"
|
||||||
|
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"},
|
{file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"},
|
||||||
{file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"},
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "3.1.3"
|
||||||
|
description = "The comprehensive WSGI web application library."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"},
|
||||||
|
{file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -175,4 +339,4 @@ watchdog = ["watchdog (>=2.3)"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "f8a7cfa653e10af88cc9a8e65f4048a61de85d9041112d08ec18adef5458fc26"
|
content-hash = "466c14d77e1b8cc59742d5929922403a3b91b09caf2287b1e00e03b3973ab5bf"
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ packages = [{include = "repl_nix_ftc23344"}]
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.12"
|
python = "^3.12"
|
||||||
flask = "^3.0.3"
|
flask = "^3.0.3"
|
||||||
|
werkzeug = "^3.1.3"
|
||||||
|
psycopg2-binary = "^2.9.10"
|
||||||
|
flask-session = "^0.8.0"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
95
replit.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# FTC Team 23344 "Technical Turbulence" Website
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Flask-based website for FTC Team 23344 with a modern dark theme (#000000 pure black background), comprehensive content management system, and PostgreSQL database integration.
|
||||||
|
|
||||||
|
## Recent Changes (October 3, 2025)
|
||||||
|
|
||||||
|
### Admin Panel System
|
||||||
|
Created a complete admin panel with password-protected access for managing all website content:
|
||||||
|
|
||||||
|
**Access:** `/admin/login` (Password: `techturb123`)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- 📊 **Stats Management**: Edit homepage statistics (seasons, members, awards, competitions)
|
||||||
|
- 👥 **Members/Mentors Management**: Add, edit, remove team members and mentors with profile picture uploads
|
||||||
|
- 🏆 **Competitions Management**: Add competitions by season with optional images, awards (bullet points), descriptions, event names, and dates
|
||||||
|
- 💼 **Sponsors Management**: Add/remove sponsors with logo uploads and website URLs
|
||||||
|
|
||||||
|
### Database Integration
|
||||||
|
- Migrated all content from hardcoded templates to PostgreSQL database
|
||||||
|
- All public pages now pull data dynamically from database
|
||||||
|
- Data persists across server restarts
|
||||||
|
- Initial data seeded from existing website content
|
||||||
|
|
||||||
|
**Database Schema:**
|
||||||
|
- `stats` - Homepage statistics
|
||||||
|
- `members` - Team members with images, roles, names
|
||||||
|
- `mentors` - Mentors/coaches with images, roles, names
|
||||||
|
- `competitions` - Competition entries organized by season
|
||||||
|
- `sponsors` - Sponsor cards with logos and URLs
|
||||||
|
|
||||||
|
### Design Improvements
|
||||||
|
- Pure black (#000000) background throughout
|
||||||
|
- Cinematic hero section (700px tall) with darkened team image and text overlay
|
||||||
|
- Light gray text (gray-300) for paragraph content
|
||||||
|
- Reduced top margins on headers (40px instead of 64px)
|
||||||
|
- Mobile-responsive with optimized padding (20px/16px)
|
||||||
|
- Modern cards with 20px border radius and blue accent hover effects
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
├── app.py # Flask routes, database connections, admin system
|
||||||
|
├── templates/
|
||||||
|
│ ├── admin/ # Admin panel templates
|
||||||
|
│ │ ├── login.html
|
||||||
|
│ │ ├── base.html # Admin layout with side navigation
|
||||||
|
│ │ ├── stats.html
|
||||||
|
│ │ ├── members.html
|
||||||
|
│ │ ├── competitions.html
|
||||||
|
│ │ └── sponsors.html
|
||||||
|
│ ├── base.html # Public site layout
|
||||||
|
│ ├── home.html # Homepage with stats from DB
|
||||||
|
│ ├── contributors.html # Team members from DB
|
||||||
|
│ ├── competitions.html # Competitions from DB
|
||||||
|
│ └── sponsors.html # Sponsors from DB
|
||||||
|
├── static/
|
||||||
|
│ ├── css/styles.css # All styling
|
||||||
|
│ ├── js/scripts.js
|
||||||
|
│ └── images/ # Team photos, logos, uploads
|
||||||
|
└── replit.md # This file
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
- **Backend**: Flask (Python)
|
||||||
|
- **Database**: PostgreSQL (Neon-backed via Replit)
|
||||||
|
- **Session Management**: Flask-Session
|
||||||
|
- **File Uploads**: Werkzeug secure_filename
|
||||||
|
- **Frontend**: HTML5, CSS3, JavaScript
|
||||||
|
|
||||||
|
## User Preferences
|
||||||
|
- Pure black (#000000) background theme
|
||||||
|
- Gray-300 text color for paragraphs (light grayish, not pure white)
|
||||||
|
- Preserve hero section layered image design (michiana.png + techturb.gif)
|
||||||
|
- Minimal navbar with blur effect
|
||||||
|
- Dramatic spacing reduction throughout site
|
||||||
|
- Mobile-first responsive design
|
||||||
|
|
||||||
|
## Database Connection
|
||||||
|
Environment variables automatically configured:
|
||||||
|
- `DATABASE_URL`
|
||||||
|
- `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
- Admin password currently hardcoded (techturb123) - consider moving to environment variable for production
|
||||||
|
- Session secret key uses environment variable with fallback
|
||||||
|
- File uploads use secure_filename to prevent path traversal
|
||||||
|
- Admin routes protected with session-based authentication
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
- Add admin password management
|
||||||
|
- Implement image optimization for uploads
|
||||||
|
- Add bulk operations for members/competitions
|
||||||
|
- Consider adding revision history for content changes
|
||||||
|
- Add export/backup functionality for database content
|
||||||
BIN
static/images/abyss.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
static/images/bggg.png
Normal file
|
After Width: | Height: | Size: 318 KiB |
BIN
static/images/frctees.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
static/images/fw.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/images/gene.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
static/images/geneh.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
static/images/images_3.jpeg
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/images/michiana.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
static/images/sam.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
static/images/sam2.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
static/images/storm22222.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
static/images/storm222222.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
static/images/teeaam.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
static/images/teeaam2.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
static/images/tt1.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
static/images/tt2.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
static/images/tt3.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
static/images/tt4.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
static/images/tt5.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
static/images/tt6.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
static/images/tt7.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
|
const navLinks = document.querySelectorAll('.nav-item-center, .nav-item-left');
|
||||||
|
navLinks.forEach(navItem => {
|
||||||
|
const link = navItem.querySelector('a');
|
||||||
|
if (link) {
|
||||||
|
const linkPath = new URL(link.href).pathname;
|
||||||
|
if (linkPath === currentPath || (currentPath === '/' && linkPath === '/')) {
|
||||||
|
navItem.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sidebarLinks = document.querySelectorAll('.sidebar a');
|
||||||
|
const hash = window.location.hash;
|
||||||
|
if (hash) {
|
||||||
|
sidebarLinks.forEach(link => {
|
||||||
|
if (link.getAttribute('href') === hash) {
|
||||||
|
link.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (sidebarLinks.length > 0) {
|
||||||
|
sidebarLinks[0].classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebarLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function() {
|
||||||
|
sidebarLinks.forEach(l => l.classList.remove('active'));
|
||||||
|
this.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
168
templates/admin/base.html
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Admin Panel{% endblock %} - Technical Turbulence</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
width: 250px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-right: 1px solid #333;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-logo {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 24px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav a {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #999;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav a:hover {
|
||||||
|
background: #252525;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav a.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-logout {
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-logout a {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
color: #ef4444;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-logout a:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-content {
|
||||||
|
margin-left: 250px;
|
||||||
|
flex: 1;
|
||||||
|
padding: 40px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.success {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-message.error {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-sidebar {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.admin-content {
|
||||||
|
margin-left: 200px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block extra_styles %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="admin-container">
|
||||||
|
<div class="admin-sidebar">
|
||||||
|
<div class="admin-logo">Admin Panel</div>
|
||||||
|
<nav class="admin-nav">
|
||||||
|
<a href="{{ url_for('admin_stats') }}" class="{% if request.endpoint == 'admin_stats' %}active{% endif %}">Stats</a>
|
||||||
|
<a href="{{ url_for('admin_members') }}" class="{% if request.endpoint == 'admin_members' %}active{% endif %}">Members/Mentors</a>
|
||||||
|
<a href="{{ url_for('admin_competitions') }}" class="{% if request.endpoint == 'admin_competitions' %}active{% endif %}">Competitions</a>
|
||||||
|
<a href="{{ url_for('admin_sponsors') }}" class="{% if request.endpoint == 'admin_sponsors' %}active{% endif %}">Sponsors</a>
|
||||||
|
</nav>
|
||||||
|
<div class="admin-logout">
|
||||||
|
<a href="{{ url_for('admin_logout') }}">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="admin-content">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash-message {{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
195
templates/admin/competitions.html
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Competitions Management{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_styles %}
|
||||||
|
<style>
|
||||||
|
.section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--white);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.add-form {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
color: var(--gray-300);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-input, .form-select, .form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--blue);
|
||||||
|
}
|
||||||
|
.competitions-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.comp-card {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.comp-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.comp-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 22px;
|
||||||
|
color: var(--white);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.comp-meta {
|
||||||
|
color: var(--gray-400);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.comp-description {
|
||||||
|
color: var(--gray-300);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.comp-awards {
|
||||||
|
color: var(--gray-400);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--blue);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--blue-light);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
.info-text {
|
||||||
|
color: var(--gray-400);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="admin-header">Competitions Management</h1>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Add New Competition</h2>
|
||||||
|
<div class="add-form">
|
||||||
|
<form method="POST" action="{{ url_for('add_competition') }}" enctype="multipart/form-data">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Season</label>
|
||||||
|
<input type="text" name="season" class="form-input" placeholder="e.g., Into The Deep" list="seasons" required>
|
||||||
|
<datalist id="seasons">
|
||||||
|
{% for season in seasons %}
|
||||||
|
<option value="{{ season }}">
|
||||||
|
{% endfor %}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Event Name</label>
|
||||||
|
<input type="text" name="event_name" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Date</label>
|
||||||
|
<input type="text" name="date" class="form-input" placeholder="e.g., 06/19/2025 or 11/11/23 - 01/06/24" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea name="description" class="form-textarea" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Awards (optional)</label>
|
||||||
|
<textarea name="awards" class="form-textarea" placeholder="One award per line"></textarea>
|
||||||
|
<p class="info-text">Enter each award on a new line</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Competition Image (optional)</label>
|
||||||
|
<input type="file" name="image" class="form-input" accept="image/*">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add Competition</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Existing Competitions</h2>
|
||||||
|
<div class="competitions-list">
|
||||||
|
{% for comp in competitions %}
|
||||||
|
<div class="comp-card">
|
||||||
|
<div class="comp-header">
|
||||||
|
<div>
|
||||||
|
<h3 class="comp-title">{{ comp.event_name }}</h3>
|
||||||
|
<p class="comp-meta">{{ comp.season }} · {{ comp.date }}</p>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ url_for('delete_competition') }}">
|
||||||
|
<input type="hidden" name="id" value="{{ comp.id }}">
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<p class="comp-description">{{ comp.description }}</p>
|
||||||
|
{% if comp.awards %}
|
||||||
|
<p class="comp-awards">🏆 {{ comp.awards|replace('|', ' · ') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
110
templates/admin/login.html
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Login - Technical Turbulence</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 48px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.login-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
.login-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.login-button:hover {
|
||||||
|
background: #60a5fa;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
color: #ef4444;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<h1 class="login-title">Admin Login</h1>
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="error-message">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input type="password" name="password" class="form-input" required autofocus>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="login-button">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
259
templates/admin/members.html
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Members & Mentors Management{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_styles %}
|
||||||
|
<style>
|
||||||
|
.section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--white);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.add-form {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
color: var(--gray-300);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-input, .form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-input:focus, .form-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
.members-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.member-card-admin {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.member-image-admin {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.member-name-admin {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--white);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.member-role-admin {
|
||||||
|
color: var(--gray-400);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--blue);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--blue-light);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
.btn-edit {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
.btn-edit:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: 1000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
.modal-close {
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--gray-400);
|
||||||
|
}
|
||||||
|
.modal-close:hover {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="admin-header">Members & Mentors Management</h1>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Add New Member/Mentor</h2>
|
||||||
|
<div class="add-form">
|
||||||
|
<form method="POST" action="{{ url_for('add_member') }}" enctype="multipart/form-data">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input type="text" name="name" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Role</label>
|
||||||
|
<input type="text" name="role" class="form-input" placeholder="e.g., SOFTWARE, HARDWARE" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Type</label>
|
||||||
|
<select name="type" class="form-select" required>
|
||||||
|
<option value="member">Team Member</option>
|
||||||
|
<option value="mentor">Mentor</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Profile Image</label>
|
||||||
|
<input type="file" name="image" class="form-input" accept="image/*">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Mentors</h2>
|
||||||
|
<div class="members-grid">
|
||||||
|
{% for mentor in mentors %}
|
||||||
|
<div class="member-card-admin">
|
||||||
|
<img src="{{ url_for('static', filename=mentor.image_path) }}" class="member-image-admin" alt="{{ mentor.name }}">
|
||||||
|
<h3 class="member-name-admin">{{ mentor.name }}</h3>
|
||||||
|
<p class="member-role-admin">{{ mentor.role }}</p>
|
||||||
|
<button onclick="openEditModal('mentor', {{ mentor.id }}, '{{ mentor.name }}', '{{ mentor.role }}')" class="btn btn-edit">Edit</button>
|
||||||
|
<form method="POST" action="{{ url_for('delete_member') }}" style="display: inline;">
|
||||||
|
<input type="hidden" name="id" value="{{ mentor.id }}">
|
||||||
|
<input type="hidden" name="type" value="mentor">
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Team Members</h2>
|
||||||
|
<div class="members-grid">
|
||||||
|
{% for member in members %}
|
||||||
|
<div class="member-card-admin">
|
||||||
|
<img src="{{ url_for('static', filename=member.image_path) }}" class="member-image-admin" alt="{{ member.name }}">
|
||||||
|
<h3 class="member-name-admin">{{ member.name }}</h3>
|
||||||
|
<p class="member-role-admin">{{ member.role }}</p>
|
||||||
|
<button onclick="openEditModal('member', {{ member.id }}, '{{ member.name }}', '{{ member.role }}')" class="btn btn-edit">Edit</button>
|
||||||
|
<form method="POST" action="{{ url_for('delete_member') }}" style="display: inline;">
|
||||||
|
<input type="hidden" name="id" value="{{ member.id }}">
|
||||||
|
<input type="hidden" name="type" value="member">
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="modal-close" onclick="closeEditModal()">×</span>
|
||||||
|
<h2 class="section-title">Edit Member</h2>
|
||||||
|
<form method="POST" action="{{ url_for('update_member') }}" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="id" id="edit-id">
|
||||||
|
<input type="hidden" name="type" id="edit-type">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input type="text" name="name" id="edit-name" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Role</label>
|
||||||
|
<input type="text" name="role" id="edit-role" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Profile Image (optional)</label>
|
||||||
|
<input type="file" name="image" class="form-input" accept="image/*">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Update</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openEditModal(type, id, name, role) {
|
||||||
|
document.getElementById('edit-id').value = id;
|
||||||
|
document.getElementById('edit-type').value = type;
|
||||||
|
document.getElementById('edit-name').value = name;
|
||||||
|
document.getElementById('edit-role').value = role;
|
||||||
|
document.getElementById('editModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('editModal').classList.remove('active');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
163
templates/admin/sponsors.html
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Sponsors Management{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_styles %}
|
||||||
|
<style>
|
||||||
|
.section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--white);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.add-form {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
color: var(--gray-300);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
.sponsors-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.sponsor-card-admin {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.sponsor-logo-admin {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.sponsor-name-admin {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--white);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.sponsor-url-admin {
|
||||||
|
color: var(--blue);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
word-break: break-all;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.sponsor-url-admin:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--blue);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--blue-light);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #ef4444;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="admin-header">Sponsors Management</h1>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Add New Sponsor</h2>
|
||||||
|
<div class="add-form">
|
||||||
|
<form method="POST" action="{{ url_for('add_sponsor') }}" enctype="multipart/form-data">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Sponsor Name</label>
|
||||||
|
<input type="text" name="name" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Website URL</label>
|
||||||
|
<input type="url" name="website_url" class="form-input" placeholder="https://example.com" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Logo Image</label>
|
||||||
|
<input type="file" name="logo" class="form-input" accept="image/*" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add Sponsor</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">Existing Sponsors</h2>
|
||||||
|
<div class="sponsors-grid">
|
||||||
|
{% for sponsor in sponsors %}
|
||||||
|
<div class="sponsor-card-admin">
|
||||||
|
{% if sponsor.logo_path %}
|
||||||
|
<img src="{{ url_for('static', filename=sponsor.logo_path) }}" class="sponsor-logo-admin" alt="{{ sponsor.name }}">
|
||||||
|
{% endif %}
|
||||||
|
<h3 class="sponsor-name-admin">{{ sponsor.name }}</h3>
|
||||||
|
{% if sponsor.website_url %}
|
||||||
|
<a href="{{ sponsor.website_url }}" class="sponsor-url-admin" target="_blank">{{ sponsor.website_url }}</a>
|
||||||
|
{% endif %}
|
||||||
|
<form method="POST" action="{{ url_for('delete_sponsor') }}">
|
||||||
|
<input type="hidden" name="id" value="{{ sponsor.id }}">
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
89
templates/admin/stats.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
{% extends "admin/base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Stats Management{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_styles %}
|
||||||
|
<style>
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.stat-card-header {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--white);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
color: var(--gray-300);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--blue);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--blue-light);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="admin-header">Stats Management</h1>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
{% for stat in stats %}
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3 class="stat-card-header">{{ stat.key }}</h3>
|
||||||
|
<form method="POST" action="{{ url_for('update_stat') }}">
|
||||||
|
<input type="hidden" name="id" value="{{ stat.id }}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Value</label>
|
||||||
|
<input type="text" name="value" class="form-input" value="{{ stat.value }}" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Label</label>
|
||||||
|
<input type="text" name="label" class="form-input" value="{{ stat.label }}" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Update</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -4,156 +4,57 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<body>
|
<div class="competitions">
|
||||||
<div class="competitions">
|
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<a href="#2023">2023</a>
|
{% for season in competitions_by_season.keys() %}
|
||||||
<a href="#2024">2024</a>
|
<a href="#{{ season.replace(' ', '') }}">{{ season }}</a>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="competitions-container">
|
<div class="competitions-container">
|
||||||
<div class="competitions-inner">
|
<div class="competitions-inner">
|
||||||
<h1 class="heading" id="comp"><span class="emoji competitions">Competition log</span></h1>
|
<h1 class="heading" id="comp">Competition log</h1>
|
||||||
<hr>
|
<hr id="comp-hr">
|
||||||
|
|
||||||
<h1 id="2024" class="competition-year">2024</h1>
|
{% for season, comps in competitions_by_season.items() %}
|
||||||
|
<h1 id="{{ season.replace(' ', '') }}" class="competition-year">{{ season }}</h1>
|
||||||
|
|
||||||
|
{% for comp in comps %}
|
||||||
<div class="competition-card">
|
<div class="competition-card">
|
||||||
<!-- <div class="competition-card-img">
|
{% if comp.image_path %}
|
||||||
<img src="{{ url_for('static', filename='images/earlybird.png') }}">
|
<div class="competition-card-img">
|
||||||
</div> -->
|
<img src="{{ url_for('static', filename=comp.image_path) }}">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="competition-header">
|
<div class="competition-header">
|
||||||
<p class="competition-name">FiT-North Early Bird Scrimmage</p>
|
<p class="competition-name">{{ comp.event_name }}</p>
|
||||||
<p class="middle-dot">·</p>
|
<p class="middle-dot">·</p>
|
||||||
<p class="competition-date">10/6/2024</p>
|
<p class="competition-date">{{ comp.date }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="competition-subtitle">Description</p>
|
<p class="competition-subtitle">Description</p>
|
||||||
|
|
||||||
<p class="competition-description">We participated in the FiT-North Early Bird Scrimmage where we
|
<p class="competition-description">{{ comp.description }}</p>
|
||||||
won two recognitions.</p>
|
|
||||||
|
|
||||||
|
{% if comp.awards %}
|
||||||
<p class="competition-subtitle">Awards</p>
|
<p class="competition-subtitle">Awards</p>
|
||||||
|
|
||||||
<ul class="competition-awards">
|
<ul class="competition-awards">
|
||||||
<li>Innovate Award sponsored by RTX</li>
|
{% for award in comp.awards.split('|') %}
|
||||||
<li>Design Award 2nd Place</li>
|
<li>{{ award }}</li>
|
||||||
</ul>
|
{% endfor %}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h1 id="2023" class="competition-year">2023</h1>
|
|
||||||
|
|
||||||
<div class="competition-card">
|
|
||||||
<div class="competition-header">
|
|
||||||
<p class="competition-name">FiT-North F-League Meets (3)</p>
|
|
||||||
<p class="middle-dot">·</p>
|
|
||||||
<p class="competition-date">11/11/23 - 01/06/24</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Description</p>
|
|
||||||
|
|
||||||
<p class="competition-description">Consisted of 3 qualification meets: FiT-North F-League Meet 1,
|
|
||||||
FiT-North F-League Meet 2, and FiT-North F-League Meet 3.</p>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Awards</p>
|
|
||||||
|
|
||||||
<ul class="competition-awards">
|
|
||||||
<li>Accumulated 10 wins</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="competition-card">
|
|
||||||
<div class="competition-header">
|
|
||||||
<p class="competition-name">FiT-North E&F Tournament</p>
|
|
||||||
<p class="middle-dot">·</p>
|
|
||||||
<p class="competition-date">01/20/24</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Description</p>
|
|
||||||
|
|
||||||
<p class="competition-description">We got 4 wins, and got Rank 3 out of 24 going into the playoffs.
|
|
||||||
We were the 1st Team Selected by Rank 2 Team 13072. We had 2 wins in the Semi-Finals and 2 wins
|
|
||||||
in the Finals. We won 3 awards and advanced straight to North Area Championship for Texas FiT
|
|
||||||
Region.</p>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Awards</p>
|
|
||||||
|
|
||||||
<ul class="competition-awards">
|
|
||||||
<li>Design Award 3rd Place</li>
|
|
||||||
<li>Innovate Award sponsored by RTX 2nd Place</li>
|
|
||||||
<li>Winning Alliance - 1st Team Selected</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="competition-card">
|
|
||||||
<div class="competition-header">
|
|
||||||
<p class="competition-name">FiT-North Area Championship</p>
|
|
||||||
<p class="middle-dot">·</p>
|
|
||||||
<p class="competition-date">02/24/24</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Description</p>
|
|
||||||
|
|
||||||
<p class="competition-description">We won our first match [But Expansion and Control hubs were
|
|
||||||
tweaking the whole time :( ]. We ended at Rank 37 of 41, and did not get picked for playoffs. We
|
|
||||||
won 1 award and advanced to the Texas State Championship.</p>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Awards</p>
|
|
||||||
|
|
||||||
<ul class="competition-awards">
|
|
||||||
<li>Innovate Award sponsored by RTX 2nd Place</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="competition-card">
|
|
||||||
<div class="competition-header">
|
|
||||||
<p class="competition-name">Texas FTC State Championship - Johnson Division</p>
|
|
||||||
<p class="middle-dot">·</p>
|
|
||||||
<p class="competition-date">03/21/24 - 03/23/24</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Description</p>
|
|
||||||
|
|
||||||
<p class="competition-description">We got 3 wins and got Rank 12 of 36 going into playoffs. We were
|
|
||||||
the 1st Team Selected by Rank 4 Team 16226. We got 2 wins in the Semi-Finals and 1 win in the
|
|
||||||
Finals. We won 1 award, but did not advance to the State Finals nor the World Championship.</p>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Awards</p>
|
|
||||||
|
|
||||||
<ul class="competition-awards">
|
|
||||||
<li>Johnson Division Finalist Alliance - 1st Team Selected</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="competition-card">
|
|
||||||
<div class="competition-header">
|
|
||||||
<p class="competition-name">Buc Days 2024 Robotics Rodeo (Off-season Tournament)</p>
|
|
||||||
<p class="middle-dot">·</p>
|
|
||||||
<p class="competition-date">05/04/24</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Description</p>
|
|
||||||
|
|
||||||
<p class="competition-description">We got 4 wins and were Rank 8 of 41 going into playoffs. We were
|
|
||||||
the st Team Selected by Rank 3 Team 16458. We got 2 wins in the Semi-Finals and 1 win in the
|
|
||||||
Finals. We won 1 award.</p>
|
|
||||||
|
|
||||||
<p class="competition-subtitle">Awards</p>
|
|
||||||
|
|
||||||
<ul class="competition-awards">
|
|
||||||
<li>Finalist Alliance - 1st Team Selected</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Communication{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="contact">
|
|
||||||
<div class="header-container">
|
|
||||||
<div class="line"></div>
|
|
||||||
<h1 class="header">CONTACT US</h1>
|
|
||||||
<div class="line"></div>
|
|
||||||
</div>
|
|
||||||
<p class="info">you can find us on several platforms.</p>
|
|
||||||
<div class="contact-container">
|
|
||||||
<div class="contact-card">
|
|
||||||
<img class="card-img" src="{{ url_for('static', filename='images/insta4.png') }}">
|
|
||||||
<div class="card-txt-container">
|
|
||||||
<p class="card-txt"><a class="link" target="_blank" href="https://www.instagram.com/technicalturbulence23344/"> Follow Our Instagram </a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="contact-card">
|
|
||||||
<img class="card-img" src="{{ url_for('static', filename='images/yt2.png') }}">
|
|
||||||
<div class="card-txt-container">
|
|
||||||
<p class="card-txt"><a class="link" target="_blank" href="https://youtube.com/@TechnicalTurbulenceFTC"> Find Us On YT </a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="contact-card">
|
|
||||||
<img class="card-img" src="{{ url_for('static', filename='images/g3.jpg') }}">
|
|
||||||
<div class="card-txt-container">
|
|
||||||
<p class="card-txt"><a class="link" target="_blank" href="mailto:technicalturbulence@gmail.com"> Email Us </a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<body>
|
<div class="contact">
|
||||||
<div class="contact">
|
<br>
|
||||||
<h1 class="heading" id="spon"><span class="emoji contact">Contact</span></h1>
|
<h1 class="heading" id="spon"><span class="emoji contact">Contact</span></h1>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="info">🌐 you can find us on several platforms! 🌐</p>
|
<p class="info">🌐 you can find us on several platforms! 🌐</p>
|
||||||
@@ -29,6 +29,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,101 +4,66 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<body>
|
<div class="contributors">
|
||||||
<div class="contributors">
|
<!-- <div class="team-info"> -->
|
||||||
<div class="team-info">
|
<!-- <div class="stats-container" id="stats2">
|
||||||
<div class="stats-container" id="stats2">
|
<h1 class="heading"><span class="emoji stats">Our stats</span></h1>
|
||||||
<h1 class="heading"><span class="emoji stats">Our stats</span></h1>
|
<hr>
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="stats-cards">
|
<div class="stats-cards">
|
||||||
<div class="stats-card">
|
<div class="stats-card">
|
||||||
<h1 class="stats-card-header">2</h1>
|
<h1 class="stats-card-header">2</h1>
|
||||||
<p class="stats-card-info">year of robotics</p>
|
<p class="stats-card-info">seasons of robotics</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card">
|
||||||
|
<h1 class="stats-card-header">6</h1>
|
||||||
|
<p class="stats-card-info">awards this season</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card">
|
||||||
|
<h1 class="stats-card-header">11</h1>
|
||||||
|
<p class="stats-card-info">total awards won</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card">
|
||||||
|
<h1 class="stats-card-header">9</h1>
|
||||||
|
<p class="stats-card-info">team members</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-card">
|
|
||||||
<h1 class="stats-card-header">2</h1>
|
|
||||||
<p class="stats-card-info">awards this season</p>
|
|
||||||
</div>
|
|
||||||
<div class="stats-card">
|
|
||||||
<h1 class="stats-card-header">7</h1>
|
|
||||||
<p class="stats-card-info">total awards won</p>
|
|
||||||
</div>
|
|
||||||
<div class="stats-card">
|
|
||||||
<h1 class="stats-card-header">9</h1>
|
|
||||||
<p class="stats-card-info">team members</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View our log</button>
|
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View
|
||||||
</div>
|
our log</button>
|
||||||
</div>
|
</div> -->
|
||||||
|
<!-- </div> -->
|
||||||
<br>
|
<br>
|
||||||
<h1 class="heading"><span class="emoji sponsors" id="team">Our mentor and coach</span></h1>
|
<h1 class="heading"><span class="emoji sponsors" id="team">Our mentors and coaches</span></h1>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="info">❤️ meet our amazing coach and mentor! ❤️</p>
|
<p class="info">❤️ meet our amazing coach and mentors! ❤️</p>
|
||||||
<div class="members-container">
|
<div class="members-container">
|
||||||
|
{% for mentor in mentors %}
|
||||||
<div class="member-card">
|
<div class="member-card">
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
|
{% if mentor.image_path %}
|
||||||
<h2 class="member-name">Mr. Kruger</h2>
|
<img class="member-image" src="{{ url_for('static', filename=mentor.image_path) }}">
|
||||||
<p class="member-role">COACH / MENTOR</p>
|
{% endif %}
|
||||||
|
<h2 class="member-name">{{ mentor.name }}</h2>
|
||||||
|
<p class="member-role">{{ mentor.role }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<h1 class="heading"><span class="emoji team" id="team">Our team</span></h1>
|
<h1 class="heading"><span class="emoji team" id="team">Our team</span></h1>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="info">⭐ meet our amazing team! ⭐</p>
|
<p class="info">⭐ meet our amazing team! ⭐</p>
|
||||||
|
|
||||||
<div class="members-container">
|
<div class="members-container">
|
||||||
|
{% for member in members %}
|
||||||
<div class="member-card">
|
<div class="member-card">
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
|
{% if member.image_path %}
|
||||||
<h2 class="member-name">Samuel</h2>
|
<img class="member-image" src="{{ url_for('static', filename=member.image_path) }}">
|
||||||
<p class="member-role">HARDWARE</p>
|
{% endif %}
|
||||||
|
<h2 class="member-name">{{ member.name }}</h2>
|
||||||
|
<p class="member-role">{{ member.role }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-card">
|
{% endfor %}
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/anish.png') }}">
|
|
||||||
<h2 class="member-name">Anish</h2>
|
|
||||||
<p class="member-role">HARDWARE</p>
|
|
||||||
</div>
|
|
||||||
<div class="member-card">
|
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
|
|
||||||
<h2 class="member-name">Daniel</h2>
|
|
||||||
<p class="member-role">HARDWARE</p>
|
|
||||||
</div>
|
|
||||||
<div class="member-card">
|
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
|
|
||||||
<h2 class="member-name">Stephen</h2>
|
|
||||||
<p class="member-role">HARDWARE</p>
|
|
||||||
</div>
|
|
||||||
<div class="member-card">
|
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/new-keshav.png') }}">
|
|
||||||
<h2 class="member-name">Keshav</h2>
|
|
||||||
<p class="member-role">SOFTWARE</p>
|
|
||||||
</div>
|
|
||||||
<div class="member-card">
|
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/sujay.png') }}">
|
|
||||||
<h2 class="member-name">Sujay</h2>
|
|
||||||
<p class="member-role">SOFTWARE</p>
|
|
||||||
</div>
|
|
||||||
<div class="member-card">
|
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/abhi.png') }}">
|
|
||||||
<h2 class="member-name">Abhiram</h2>
|
|
||||||
<p class="member-role">SOFTWARE</p>
|
|
||||||
</div>
|
|
||||||
<div class="member-card">
|
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
|
|
||||||
<h2 class="member-name">Caitlin</h2>
|
|
||||||
<p class="member-role">ALUMNI; HARDWARE</p>
|
|
||||||
</div>
|
|
||||||
<div class="member-card">
|
|
||||||
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
|
|
||||||
<h2 class="member-name">Krith</h2>
|
|
||||||
<p class="member-role">OUTREACH</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,90 +4,108 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<body>
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
||||||
<div class="home">
|
<div class="home">
|
||||||
<div class="bg">
|
<div class="bg">
|
||||||
<img src="{{ url_for('static', filename='images/techturb.gif') }}">
|
<div class="overlay-container">
|
||||||
<div class="border-triangle"></div>
|
<img class="base" src="{{ url_for('static', filename='images/michiana.png') }}">
|
||||||
|
<img class="overlay" src="{{ url_for('static', filename='images/techturb.gif') }}">
|
||||||
|
</div>
|
||||||
|
<div class="footer-text">FTC #23344 <br><button class="home-but" onclick="window.open('https://www.instagram.com/technicalturbulence23344/', '_blank');"><i data-feather="instagram"></i>
|
||||||
|
</button> <button class="home-but" onclick="window.open('mailto:technicalturbulence@gmail.com', '_blank');">
|
||||||
|
<i data-feather="mail"></i>
|
||||||
|
</button> <button class="home-but" onclick="window.open('https://youtube.com/@TechnicalTurbulenceFTC', '_blank');">
|
||||||
|
<i data-feather="youtube"></i>
|
||||||
|
</button></div>
|
||||||
|
<div class="border-triangle"></div>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="heading"><span class="emoji about">About</span></h1>
|
<!-- <h1 class="heading"><span class="emoji about">About</span></h1>
|
||||||
<hr>
|
<hr> -->
|
||||||
<div class="home-info">
|
<div class="home-info">
|
||||||
<h1 class="sub-header" id="cool-font">We are Technical Turbulence.</h1>
|
<div class="hero-image-section">
|
||||||
<div class="about-section-imgp">
|
<img src="{{ url_for('static', filename='images/teeaam.png') }}" class="hero-team-image">
|
||||||
<div class="about-sec-img">
|
<div class="hero-overlay"></div>
|
||||||
<img src="{{ url_for('static', filename='images/main1.png') }}">
|
<div class="hero-text-container">
|
||||||
</div>
|
<h1 class="hero-title">👋 We are Technical Turbulence.</h1>
|
||||||
<p class="sub-content">Team 23344, Technical Turbulence, is a community-based returning team situated around Plano and Frisco Texas, comprising nine members primarily consisting of sophomores from different high schools. Our rookie year was 2023 and we hope to learn much more in the years to come. We want to grow our numbers and increase our outreach towards different communities so we can be the best that we can.</p>
|
<p class="hero-description">Team 23344, Technical Turbulence, is a community-based returning team situated
|
||||||
</div>
|
around Plano and Frisco Texas, comprising nine members primarily consisting of sophomores from
|
||||||
<div class="about-section-imgp">
|
different high schools. Our rookie year was 2023 and we hope to learn much more in the years to
|
||||||
<!-- <div class="about-sec-img">
|
come. We want to grow our numbers and increase our outreach towards different communities so we can
|
||||||
<img src="{{ url_for('static', filename='images/robo.png') }}">
|
be the best that we can.</p>
|
||||||
</div> -->
|
<p class="hero-description">Since we are designated as veterans this year, our members bring substantial
|
||||||
<p class="sub-content">Since we are designated as veterans this year, our members bring substantial experience from their involvement with FTC 12900 Quantum Claw and 22201 The Edge Robotics. Operating from our dedicated garage workshop, we uphold the fundamental values of FIRST, integrating Gracious Professionalism into our daily endeavors. </p>
|
experience from their involvement with FTC 12900 Quantum Claw and 22201 The Edge Robotics. Operating
|
||||||
</div>
|
from our dedicated garage workshop, we uphold the fundamental values of FIRST, integrating Gracious
|
||||||
<div class="stats-container">
|
Professionalism into our daily endeavors.</p>
|
||||||
<h1 class="heading"><span class="emoji stats">Our stats</span></h1>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="stats-cards">
|
|
||||||
<div class="stats-card">
|
|
||||||
<h1 class="stats-card-header">2</h1>
|
|
||||||
<p class="stats-card-info">year of robotics</p>
|
|
||||||
</div>
|
|
||||||
<div class="stats-card">
|
|
||||||
<h1 class="stats-card-header">2</h1>
|
|
||||||
<p class="stats-card-info">awards this season</p>
|
|
||||||
</div>
|
|
||||||
<div class="stats-card">
|
|
||||||
<h1 class="stats-card-header">7</h1>
|
|
||||||
<p class="stats-card-info">total awards won</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stats-container">
|
||||||
|
<h1 class="heading"><span class="emoji stats">Our stats</span></h1>
|
||||||
|
<hr>
|
||||||
|
|
||||||
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View our log</button>
|
<div class="stats-cards">
|
||||||
</div>
|
{% for stat in stats %}
|
||||||
|
<div class="stats-card">
|
||||||
|
<h1 class="stats-card-header">{{ stat.value }}</h1>
|
||||||
|
<p class="stats-card-info">{{ stat.label }}</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View
|
||||||
|
our log</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="heading"><span class="emoji gallary">Our mission</span></h1>
|
<h1 class="heading"><span class="emoji gallary">Our mission</span></h1>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="home-info">
|
<div class="home-info">
|
||||||
|
|
||||||
<h1 class="sub-header">✊ One team, one goal.</h1>
|
<h1 class="sub-header">✊ One team, one goal.</h1>
|
||||||
<p class="sub-content">We aim to show individuals that with teamwork, creativity, and persistence, anyone can design, build, and code robots. Our mission is part of an outreach initiative to guide young minds toward STEM and skill-building opportunities in robotics and technology. Through this unifying goal, we want to empower young students to learn, grow, and join FTC in the future.</p>
|
<p class="sub-content">We aim to show individuals that with teamwork, creativity, and persistence, anyone
|
||||||
|
can design, build, and code robots. Our mission is part of an outreach initiative to guide young minds
|
||||||
|
toward STEM and skill-building opportunities in robotics and technology. Through this unifying goal, we
|
||||||
|
want to empower young students to learn, grow, and join FTC in the future.</p>
|
||||||
|
|
||||||
<div class="meet-our-team-container">
|
<div class="meet-our-team-container">
|
||||||
<img id="meet-our-team" src="{{ url_for('static', filename='images/team.png') }}">
|
<img id="meet-our-team" src="{{ url_for('static', filename='images/team.png') }}">
|
||||||
<button class="bottom-center-button" onclick="window.location.href = '{{ url_for('contributors') }}' ; ">Meet Our Team</button>
|
<button class="bottom-center-button"
|
||||||
|
onclick="window.location.href = '{{ url_for('contributors') }}' ; ">Meet Our Team</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<h1 class="heading"><span class="emoji sponsors">Sponsors</span></h1>
|
<h1 class="heading"><span class="emoji sponsors">Sponsors</span></h1>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="home-info">
|
<div class="home-info">
|
||||||
<p class="sub-content">We, FTC team Technical Turbulence, want to inspire the next generation by sharing our journey in robotics and the exciting world of FTC. By demonstrating our robot and showcasing the engineering behind it, we hope to spark curiosity in young minds about STEM fields.
|
<h1 class="sub-header">❤️ We couldn't have done it without...</h1>
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 class="sub-header">❤️ And we couldn't have done it without...</h1>
|
<p class="sub-content">We, FTC team Technical Turbulence, want to inspire the next generation by sharing our
|
||||||
|
journey in robotics and the exciting world of FTC. By showcasing the
|
||||||
|
engineering behind our robot, and with the help of our sponsors, we hope to spark curiosity in young minds about STEM fields.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="sponsors-container">
|
<div class="sponsors-container" id="sponnnnn">
|
||||||
<img class="sponsors-card" src="{{ url_for('static', filename='images/ray.png') }}">
|
<img class="sponsors-card" src="{{ url_for('static', filename='images/ray.png') }}">
|
||||||
<img class="sponsors-card" src="{{ url_for('static', filename='images/cen3.png') }}">
|
<img class="sponsors-card" src="{{ url_for('static', filename='images/cen3.png') }}">
|
||||||
<img class="sponsors-card" src="{{ url_for('static', filename='images/ti.png') }}">
|
<img class="sponsors-card" src="{{ url_for('static', filename='images/ti.png') }}">
|
||||||
<img class="sponsors-card" src="{{ url_for('static', filename='images/twc.png') }}">
|
<img class="sponsors-card" src="{{ url_for('static', filename='images/twc.png') }}">
|
||||||
</div>
|
<img class="sponsors-card" src="{{ url_for('static', filename='images/fw.png') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sponsors-more-container">
|
<div class="sponsors-more-container">
|
||||||
<button type="button" id="sponsors-more-button" onclick="window.location.href = '{{ url_for('sponsors') }}' ; ">See more</button>
|
<button type="button" id="sponsors-more-button"
|
||||||
</div>
|
onclick="window.location.href = '{{ url_for('sponsors') }}' ; ">See more</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
feather.replace({ width: "40", height: "40" });
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
14
templates/robots-storm.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Technical Turbulence - Home{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="robot-specific">
|
||||||
|
<h1 class="heading2" id="spon"><span class="emoji robots">Storm</span></h1>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
41
templates/robots.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Technical Turbulence - Home{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="rob">
|
||||||
|
<h1 class="heading" id="spon"><span class="emoji robots">Robots</span></h1>
|
||||||
|
<hr>
|
||||||
|
<p class="info">🤖 we've built and designed several robots. 🤖</p
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="robots-container">
|
||||||
|
<div class="robots-inner">
|
||||||
|
|
||||||
|
<a class="robo-card" href="https://www.youtube.com/watch?v=dQw4w9WgXc">
|
||||||
|
<div class="robo-card-img">
|
||||||
|
<img src="{{ url_for('static', filename='images/abyss.png') }}">
|
||||||
|
</div>
|
||||||
|
<div class="robo-card-txt">
|
||||||
|
<p class="robo-card-name">Abyss</p>
|
||||||
|
<p class="robo-card-desc">2024 · INTO THE DEEP</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a class="robo-card" href="/robots/storm">
|
||||||
|
<div class="robo-card-img">
|
||||||
|
<img src="{{ url_for('static', filename='images/storm222222.png') }}">
|
||||||
|
</div>
|
||||||
|
<div class="robo-card-txt">
|
||||||
|
<p class="robo-card-name">Storm</p>
|
||||||
|
<p class="robo-card-desc">2023 · CENTERSTAGE</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -4,38 +4,24 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<body>
|
<div class="sponsors">
|
||||||
<div class="sponsors">
|
<br>
|
||||||
<h1 class="heading" id="spon"><span class="emoji sponsors">Sponsors</span></h1>
|
<h1 class="heading" id="spon"><span class="emoji sponsors">Sponsors</span></h1>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="info">❤️ companies of various sizes sponsor our initiatives. ❤️</p>
|
<p class="info">❤️ companies of various sizes sponsor our initiatives. ❤️</p>
|
||||||
|
|
||||||
<div class="sponsors-container">
|
<div class="sponsors-container">
|
||||||
<div class="card-container">
|
<div class="card-container">
|
||||||
<a href="https://www.rtx.com/" target="_blank" class="card-sponsors">
|
{% for sponsor in sponsors %}
|
||||||
|
<a href="{{ sponsor.website_url }}" target="_blank" class="card-sponsors">
|
||||||
<div class="card-content-sponsors">
|
<div class="card-content-sponsors">
|
||||||
<img src="{{ url_for('static', filename='images/ray.png') }}" alt="Image 1">
|
<img src="{{ url_for('static', filename=sponsor.logo_path) }}" alt="{{ sponsor.name }}">
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.smilesinfrisco.com/" target="_blank" class="card-sponsors">
|
|
||||||
<div class="card-content-sponsors">
|
|
||||||
<img src="{{ url_for('static', filename='images/cen3.png') }}" alt="Image 2">
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.ti.com/" target="_blank" class="card-sponsors">
|
|
||||||
<div class="card-content-sponsors">
|
|
||||||
<img src="{{ url_for('static', filename='images/ti.png') }}" alt="Image 3">
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.twc.texas.gov/" target="_blank" class="card-sponsors">
|
|
||||||
<div class="card-content-sponsors">
|
|
||||||
<img src="{{ url_for('static', filename='images/twc.png') }}" alt="Image 3">
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||