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
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"
|
||||||
|
|||||||
329
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)
|
||||||
# @app.route('/awards')
|
cur.execute('SELECT * FROM competitions ORDER BY display_order')
|
||||||
# def awards():
|
competitions = cur.fetchall()
|
||||||
# return render_template('awards.html')
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
competitions_by_season = {}
|
||||||
|
for comp in competitions:
|
||||||
|
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,13 @@ 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')
|
||||||
@app.route('/robots/<type>')
|
@app.route('/robots/<type>')
|
||||||
@@ -44,5 +83,265 @@ def robots(type=None):
|
|||||||
else:
|
else:
|
||||||
return render_template(f'robots-{type}.html')
|
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/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/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/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 %}
|
||||||
@@ -30,12 +30,6 @@
|
|||||||
<li class="nav-item-center">
|
<li class="nav-item-center">
|
||||||
<a href="/sponsors">Sponsors</a>
|
<a href="/sponsors">Sponsors</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- <li class="nav-item-center">
|
|
||||||
<a href="/robots">Robots</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item-center">
|
|
||||||
<a href="/contact">Outreach</a>
|
|
||||||
</li> -->
|
|
||||||
<li class="nav-item-center">
|
<li class="nav-item-center">
|
||||||
<a href="/contact">Contact</a>
|
<a href="/contact">Contact</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -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,24 +4,23 @@
|
|||||||
|
|
||||||
{% 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>
|
||||||
<div class="stats-card">
|
<div class="stats-card">
|
||||||
<h1 class="stats-card-header">2</h1>
|
<h1 class="stats-card-header">6</h1>
|
||||||
<p class="stats-card-info">awards this season</p>
|
<p class="stats-card-info">awards this season</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-card">
|
<div class="stats-card">
|
||||||
<h1 class="stats-card-header">7</h1>
|
<h1 class="stats-card-header">11</h1>
|
||||||
<p class="stats-card-info">total awards won</p>
|
<p class="stats-card-info">total awards won</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-card">
|
<div class="stats-card">
|
||||||
@@ -32,124 +31,39 @@
|
|||||||
|
|
||||||
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View
|
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View
|
||||||
our log</button>
|
our log</button>
|
||||||
</div>
|
</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" id="hardware">
|
<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" id="hardware">
|
{% 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" id="hardware">
|
|
||||||
<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" id="hardware">
|
|
||||||
<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" id="software">
|
|
||||||
<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" id="software">
|
|
||||||
<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" id="software">
|
|
||||||
<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" id="hardware">
|
|
||||||
<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" id="software">
|
|
||||||
<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>
|
||||||
|
|
||||||
<script>
|
|
||||||
function createConfetti(emoji, originX, originY) {
|
|
||||||
const confettiCount = 40;
|
|
||||||
for (let i = 0; i < confettiCount; i++) {
|
|
||||||
let confetti = document.createElement("div");
|
|
||||||
confetti.innerHTML = emoji;
|
|
||||||
confetti.style.position = "fixed";
|
|
||||||
confetti.style.left = `${originX}px`;
|
|
||||||
confetti.style.top = `${originY}px`;
|
|
||||||
confetti.style.fontSize = `${Math.random() * 2 + 1.5}rem`;
|
|
||||||
confetti.style.pointerEvents = "none";
|
|
||||||
confetti.style.opacity = "0";
|
|
||||||
|
|
||||||
document.body.appendChild(confetti);
|
|
||||||
|
|
||||||
let angle = Math.random() * Math.PI * 2;
|
|
||||||
let velocityX = Math.cos(angle) * (Math.random() * window.innerWidth * 0.5);
|
|
||||||
let velocityY = -Math.abs(Math.sin(angle) * (Math.random() * window.innerHeight * 0.5));
|
|
||||||
|
|
||||||
let fallX = velocityX * 1.2;
|
|
||||||
let fallY = window.innerHeight;
|
|
||||||
|
|
||||||
|
|
||||||
confetti.animate([
|
|
||||||
{ transform: `translate(0, 0) scale(1)`, opacity: 0 },
|
|
||||||
{ transform: `translate(${velocityX}px, ${velocityY}px) scale(1.2)`, opacity: 1, offset: 0.2 },
|
|
||||||
{ transform: `translate(${fallX}px, ${fallY}px) scale(0.8)`, opacity: 0, offset: 1 }
|
|
||||||
], {
|
|
||||||
duration: 6000 + Math.random() * 1000,
|
|
||||||
easing: "cubic-bezier(0.2, 0.8, 0.2, 1)"
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => confetti.remove(), 7000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll("#software, #hardware").forEach(card => {
|
|
||||||
card.addEventListener("mouseenter", (event) => {
|
|
||||||
let emoji = card.id === "software" ? "😊" : "💩";
|
|
||||||
let rect = card.getBoundingClientRect();
|
|
||||||
let originX = event.clientX;
|
|
||||||
let originY = event.clientY;
|
|
||||||
createConfetti(emoji, originX, originY);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,61 +4,51 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<body>
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
||||||
<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">
|
<div class="overlay-container">
|
||||||
<img src="{{ url_for('static', filename='images/techturb.gif') }}">
|
<img class="base" src="{{ url_for('static', filename='images/michiana.png') }}">
|
||||||
<div class="buttons">
|
<img class="overlay" src="{{ url_for('static', filename='images/techturb.gif') }}">
|
||||||
<button onclick="window.open('https://www.instagram.com/technicalturbulence23344/', '_blank');">
|
</div>
|
||||||
<i data-feather="instagram"></i>
|
<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> <button class="home-but" onclick="window.open('mailto:technicalturbulence@gmail.com', '_blank');">
|
||||||
<button onclick="window.open('mailto:technicalturbulence@gmail.com', '_blank');"> <i data-feather="mail"></i>
|
<i data-feather="mail"></i>
|
||||||
</button>
|
</button> <button class="home-but" onclick="window.open('https://youtube.com/@TechnicalTurbulenceFTC', '_blank');">
|
||||||
<button onclick="window.open('https://youtube.com/@TechnicalTurbulenceFTC', '_blank');"> <i data-feather="youtube"></i></button>
|
<i data-feather="youtube"></i>
|
||||||
</div>
|
</button></div>
|
||||||
<div class="border-triangle"></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/teeaam.png') }}">
|
<div class="hero-text-container">
|
||||||
|
<h1 class="hero-title">👋 We are Technical Turbulence.</h1>
|
||||||
|
<p class="hero-description">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">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>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div class="about-section-imgp">
|
|
||||||
<!-- <div class="about-sec-img">
|
|
||||||
<img src="{{ url_for('static', filename='images/robo.png') }}">
|
|
||||||
</div> -->
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-container">
|
<div class="stats-container">
|
||||||
<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">
|
||||||
|
{% for stat in stats %}
|
||||||
<div class="stats-card">
|
<div class="stats-card">
|
||||||
<h1 class="stats-card-header">2</h1>
|
<h1 class="stats-card-header">{{ stat.value }}</h1>
|
||||||
<p class="stats-card-info">year of robotics</p>
|
<p class="stats-card-info">{{ stat.label }}</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>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View
|
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View
|
||||||
@@ -89,19 +79,19 @@
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="home-info">
|
<div class="home-info">
|
||||||
|
<h1 class="sub-header">❤️ 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
|
<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
|
journey in robotics and the exciting world of FTC. By showcasing the
|
||||||
engineering behind it, we hope to spark curiosity in young minds about STEM fields.
|
engineering behind our robot, and with the help of our sponsors, we hope to spark curiosity in young minds about STEM fields.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 class="sub-header">❤️ And we couldn't have done it without...</h1>
|
|
||||||
|
|
||||||
<div class="sponsors-container" id="sponnnnn">
|
<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') }}">
|
||||||
<img src="{{ url_for('static', filename='images/fw.png') }}" alt="Image 3">
|
<img class="sponsors-card" src="{{ url_for('static', filename='images/fw.png') }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sponsors-more-container">
|
<div class="sponsors-more-container">
|
||||||
@@ -117,7 +107,5 @@
|
|||||||
<script>
|
<script>
|
||||||
feather.replace({ width: "40", height: "40" });
|
feather.replace({ width: "40", height: "40" });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,43 +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>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.fabworks.com/" target="_blank" class="card-sponsors">
|
|
||||||
<div class="card-content-sponsors">
|
|
||||||
<img src="{{ url_for('static', filename='images/fw.png') }}" alt="Image 3">
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||