Saved your changes before starting work

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 5e584ab0-c340-4432-97ef-1972582b60e9
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6d4dbe7c-69e4-4510-bd62-638ff9c78d5c
Replit-Restored-To: befc3160be
This commit is contained in:
abhiramtx
2025-11-08 22:13:45 +00:00
71 changed files with 2699 additions and 1452 deletions

34
.replit
View File

@@ -1,4 +1,4 @@
modules = ["web", "python-3.12"] modules = ["web", "python-3.12", "postgresql-16"]
run = "python3 app.py" run = "python3 app.py"
[nix] [nix]
@@ -10,5 +10,35 @@ deploymentTarget = "cloudrun"
[[ports]] [[ports]]
localPort = 5000 localPort = 5000
externalPort = 80
[[ports]]
localPort = 44245
externalPort = 3000 externalPort = 3000
exposeLocalhost = true
[agent]
expertMode = true
[workflows]
runButton = "Project"
[[workflows.workflow]]
name = "Project"
mode = "parallel"
author = "agent"
[[workflows.workflow.tasks]]
task = "workflow.run"
args = "Flask Server"
[[workflows.workflow]]
name = "Flask Server"
author = "agent"
[[workflows.workflow.tasks]]
task = "shell.exec"
args = "python app.py"
waitForPort = 5000
[workflows.workflow.metadata]
outputType = "webview"

335
app.py
View File

@@ -1,20 +1,43 @@
# TODO import os
# Fix all text sizes across all pages except competition log import psycopg2
# Fix all top margins (do 20ish vh instead) from psycopg2.extras import RealDictCursor
from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify
from werkzeug.utils import secure_filename
from flask import Flask, render_template from datetime import datetime
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'techturb-secret-key-change-in-production')
app.config['UPLOAD_FOLDER'] = 'static/images'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
ADMIN_PASSWORD = 'techturb123'
def get_db_connection():
conn = psycopg2.connect(os.environ['DATABASE_URL'])
return conn
@app.route('/') @app.route('/')
@app.route('/home') @app.route('/home')
def home(): def home():
return render_template('home.html') conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM stats ORDER BY key')
stats = cur.fetchall()
cur.close()
conn.close()
return render_template('home.html', stats=stats)
@app.route('/contributors') @app.route('/contributors')
def contributors(): def contributors():
return render_template('contributors.html') conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM members ORDER BY display_order')
members = cur.fetchall()
cur.execute('SELECT * FROM mentors ORDER BY display_order')
mentors = cur.fetchall()
cur.close()
conn.close()
return render_template('contributors.html', members=members, mentors=mentors)
@app.route('/robot') @app.route('/robot')
def robot(): def robot():
@@ -22,11 +45,21 @@ def robot():
@app.route('/competitions') @app.route('/competitions')
def competitions(): def competitions():
return render_template('competitions.html') conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM competitions ORDER BY display_order')
competitions = cur.fetchall()
cur.close()
conn.close()
# @app.route('/awards') competitions_by_season = {}
# def awards(): for comp in competitions:
# return render_template('awards.html') season = comp['season']
if season not in competitions_by_season:
competitions_by_season[season] = []
competitions_by_season[season].append(comp)
return render_template('competitions.html', competitions_by_season=competitions_by_season)
@app.route('/contact') @app.route('/contact')
def contact(): def contact():
@@ -34,7 +67,281 @@ def contact():
@app.route('/sponsors') @app.route('/sponsors')
def sponsors(): def sponsors():
return render_template('sponsors.html') conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM sponsors ORDER BY display_order')
sponsors = cur.fetchall()
cur.close()
conn.close()
return render_template('sponsors.html', sponsors=sponsors)
@app.route('/robots')
@app.route('/robots/<type>')
def robots(type=None):
if type is None:
return render_template('robots.html')
else:
return render_template(f'robots-{type}.html')
@app.route('/admin/login', methods=['GET', 'POST'])
def admin_login():
if request.method == 'POST':
password = request.form.get('password')
if password == ADMIN_PASSWORD:
session['admin_logged_in'] = True
return redirect(url_for('admin_stats'))
else:
flash('Incorrect password', 'error')
return render_template('admin/login.html')
@app.route('/admin/logout')
def admin_logout():
session.pop('admin_logged_in', None)
return redirect(url_for('home'))
def admin_required(f):
def decorated_function(*args, **kwargs):
if not session.get('admin_logged_in'):
return redirect(url_for('admin_login'))
return f(*args, **kwargs)
decorated_function.__name__ = f.__name__
return decorated_function
@app.route('/admin/stats')
@admin_required
def admin_stats():
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM stats ORDER BY key')
stats = cur.fetchall()
cur.close()
conn.close()
return render_template('admin/stats.html', stats=stats)
@app.route('/admin/stats/update', methods=['POST'])
@admin_required
def update_stat():
stat_id = request.form.get('id')
value = request.form.get('value')
label = request.form.get('label')
conn = get_db_connection()
cur = conn.cursor()
cur.execute('UPDATE stats SET value = %s, label = %s WHERE id = %s', (value, label, stat_id))
conn.commit()
cur.close()
conn.close()
flash('Stat updated successfully', 'success')
return redirect(url_for('admin_stats'))
@app.route('/admin/members')
@admin_required
def admin_members():
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM members ORDER BY display_order')
members = cur.fetchall()
cur.execute('SELECT * FROM mentors ORDER BY display_order')
mentors = cur.fetchall()
cur.close()
conn.close()
return render_template('admin/members.html', members=members, mentors=mentors)
@app.route('/admin/member/add', methods=['POST'])
@admin_required
def add_member():
name = request.form.get('name')
role = request.form.get('role')
member_type = request.form.get('type')
image_path = 'images/default.jpg'
if 'image' in request.files:
file = request.files['image']
if file and file.filename:
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
image_path = f'images/{filename}'
conn = get_db_connection()
cur = conn.cursor()
if member_type == 'mentor':
cur.execute('INSERT INTO mentors (name, role, image_path) VALUES (%s, %s, %s)', (name, role, image_path))
else:
cur.execute('INSERT INTO members (name, role, image_path) VALUES (%s, %s, %s)', (name, role, image_path))
conn.commit()
cur.close()
conn.close()
flash(f'{"Mentor" if member_type == "mentor" else "Member"} added successfully', 'success')
return redirect(url_for('admin_members'))
@app.route('/admin/member/update', methods=['POST'])
@admin_required
def update_member():
member_id = request.form.get('id')
name = request.form.get('name')
role = request.form.get('role')
member_type = request.form.get('type')
conn = get_db_connection()
cur = conn.cursor()
image_path = None
if 'image' in request.files:
file = request.files['image']
if file and file.filename:
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
image_path = f'images/{filename}'
if member_type == 'mentor':
if image_path:
cur.execute('UPDATE mentors SET name = %s, role = %s, image_path = %s WHERE id = %s', (name, role, image_path, member_id))
else:
cur.execute('UPDATE mentors SET name = %s, role = %s WHERE id = %s', (name, role, member_id))
else:
if image_path:
cur.execute('UPDATE members SET name = %s, role = %s, image_path = %s WHERE id = %s', (name, role, image_path, member_id))
else:
cur.execute('UPDATE members SET name = %s, role = %s WHERE id = %s', (name, role, member_id))
conn.commit()
cur.close()
conn.close()
flash(f'{"Mentor" if member_type == "mentor" else "Member"} updated successfully', 'success')
return redirect(url_for('admin_members'))
@app.route('/admin/member/delete', methods=['POST'])
@admin_required
def delete_member():
member_id = request.form.get('id')
member_type = request.form.get('type')
conn = get_db_connection()
cur = conn.cursor()
if member_type == 'mentor':
cur.execute('DELETE FROM mentors WHERE id = %s', (member_id,))
else:
cur.execute('DELETE FROM members WHERE id = %s', (member_id,))
conn.commit()
cur.close()
conn.close()
flash(f'{"Mentor" if member_type == "mentor" else "Member"} deleted successfully', 'success')
return redirect(url_for('admin_members'))
@app.route('/admin/competitions')
@admin_required
def admin_competitions():
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM competitions ORDER BY display_order')
competitions = cur.fetchall()
cur.execute('SELECT DISTINCT season FROM competitions ORDER BY season DESC')
seasons = [row['season'] for row in cur.fetchall()]
cur.close()
conn.close()
return render_template('admin/competitions.html', competitions=competitions, seasons=seasons)
@app.route('/admin/competition/add', methods=['POST'])
@admin_required
def add_competition():
season = request.form.get('season')
event_name = request.form.get('event_name')
date = request.form.get('date')
description = request.form.get('description')
awards_raw = request.form.get('awards')
awards = '|'.join([line.strip() for line in awards_raw.split('\n') if line.strip()])
image_path = None
if 'image' in request.files:
file = request.files['image']
if file and file.filename:
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
image_path = f'images/{filename}'
conn = get_db_connection()
cur = conn.cursor()
cur.execute('INSERT INTO competitions (season, event_name, date, description, awards, image_path) VALUES (%s, %s, %s, %s, %s, %s)',
(season, event_name, date, description, awards, image_path))
conn.commit()
cur.close()
conn.close()
flash('Competition added successfully', 'success')
return redirect(url_for('admin_competitions'))
@app.route('/admin/competition/delete', methods=['POST'])
@admin_required
def delete_competition():
competition_id = request.form.get('id')
conn = get_db_connection()
cur = conn.cursor()
cur.execute('DELETE FROM competitions WHERE id = %s', (competition_id,))
conn.commit()
cur.close()
conn.close()
flash('Competition deleted successfully', 'success')
return redirect(url_for('admin_competitions'))
@app.route('/admin/sponsors')
@admin_required
def admin_sponsors():
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute('SELECT * FROM sponsors ORDER BY display_order')
sponsors = cur.fetchall()
cur.close()
conn.close()
return render_template('admin/sponsors.html', sponsors=sponsors)
@app.route('/admin/sponsor/add', methods=['POST'])
@admin_required
def add_sponsor():
name = request.form.get('name')
website_url = request.form.get('website_url')
logo_path = None
if 'logo' in request.files:
file = request.files['logo']
if file and file.filename:
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
logo_path = f'images/{filename}'
conn = get_db_connection()
cur = conn.cursor()
cur.execute('INSERT INTO sponsors (name, logo_path, website_url) VALUES (%s, %s, %s)', (name, logo_path, website_url))
conn.commit()
cur.close()
conn.close()
flash('Sponsor added successfully', 'success')
return redirect(url_for('admin_sponsors'))
@app.route('/admin/sponsor/delete', methods=['POST'])
@admin_required
def delete_sponsor():
sponsor_id = request.form.get('id')
conn = get_db_connection()
cur = conn.cursor()
cur.execute('DELETE FROM sponsors WHERE id = %s', (sponsor_id,))
conn.commit()
cur.close()
conn.close()
flash('Sponsor deleted successfully', 'success')
return redirect(url_for('admin_sponsors'))
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True, host='0.0.0.0', port=5000)

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

178
poetry.lock generated
View File

@@ -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"

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

BIN
static/images/abyss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
static/images/bggg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

BIN
static/images/frctees.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
static/images/fw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/images/gene.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
static/images/geneh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/images/images_3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
static/images/michiana.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
static/images/sam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
static/images/sam2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
static/images/teeaam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
static/images/teeaam2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

BIN
static/images/tt1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
static/images/tt2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
static/images/tt3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
static/images/tt4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
static/images/tt5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
static/images/tt6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
static/images/tt7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -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
View 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>

View 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
View 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>

View 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()">&times;</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 %}

View 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 %}

View 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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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">
@@ -30,75 +29,41 @@
</div> </div>
</div> </div>
<button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View our log</button> <button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View
</div> our log</button>
</div> </div> -->
<!-- </div> -->
<br> <br>
<h1 class="heading"><span class="emoji sponsors" id="team">Our mentor and coach</span></h1> <h1 class="heading"><span class="emoji sponsors" id="team">Our mentors and coaches</span></h1>
<hr> <hr>
<p class="info">❤️ meet our amazing coach and mentor! ❤️</p> <p class="info">❤️ meet our amazing coach and mentors! ❤️</p>
<div class="members-container"> <div class="members-container">
{% for mentor in mentors %}
<div class="member-card"> <div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}"> {% if mentor.image_path %}
<h2 class="member-name">Mr. Kruger</h2> <img class="member-image" src="{{ url_for('static', filename=mentor.image_path) }}">
<p class="member-role">COACH / MENTOR</p> {% endif %}
<h2 class="member-name">{{ mentor.name }}</h2>
<p class="member-role">{{ mentor.role }}</p>
</div> </div>
{% endfor %}
</div> </div>
<h1 class="heading"><span class="emoji team" id="team">Our team</span></h1> <h1 class="heading"><span class="emoji team" id="team">Our team</span></h1>
<hr> <hr>
<p class="info">⭐ meet our amazing team! ⭐</p> <p class="info">⭐ meet our amazing team! ⭐</p>
<div class="members-container"> <div class="members-container">
{% for member in members %}
<div class="member-card"> <div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}"> {% if member.image_path %}
<h2 class="member-name">Samuel</h2> <img class="member-image" src="{{ url_for('static', filename=member.image_path) }}">
<p class="member-role">HARDWARE</p> {% endif %}
<h2 class="member-name">{{ member.name }}</h2>
<p class="member-role">{{ member.role }}</p>
</div> </div>
<div class="member-card"> {% endfor %}
<img class="member-image" src="{{ url_for('static', filename='images/anish.png') }}">
<h2 class="member-name">Anish</h2>
<p class="member-role">HARDWARE</p>
</div>
<div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
<h2 class="member-name">Daniel</h2>
<p class="member-role">HARDWARE</p>
</div>
<div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
<h2 class="member-name">Stephen</h2>
<p class="member-role">HARDWARE</p>
</div>
<div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/new-keshav.png') }}">
<h2 class="member-name">Keshav</h2>
<p class="member-role">SOFTWARE</p>
</div>
<div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/sujay.png') }}">
<h2 class="member-name">Sujay</h2>
<p class="member-role">SOFTWARE</p>
</div>
<div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/abhi.png') }}">
<h2 class="member-name">Abhiram</h2>
<p class="member-role">SOFTWARE</p>
</div>
<div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
<h2 class="member-name">Caitlin</h2>
<p class="member-role">ALUMNI; HARDWARE</p>
</div>
<div class="member-card">
<img class="member-image" src="{{ url_for('static', filename='images/default.jpg') }}">
<h2 class="member-name">Krith</h2>
<p class="member-role">OUTREACH</p>
</div> </div>
</div> </div>
</div>
</body>
{% endblock %} {% endblock %}

View File

@@ -4,48 +4,55 @@
{% block content %} {% block content %}
<body> <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<div class="home"> <div class="home">
<div class="bg"> <div class="bg">
<img src="{{ url_for('static', filename='images/techturb.gif') }}"> <div class="overlay-container">
<img class="base" src="{{ url_for('static', filename='images/michiana.png') }}">
<img class="overlay" src="{{ url_for('static', filename='images/techturb.gif') }}">
</div>
<div class="footer-text">FTC #23344 <br><button class="home-but" onclick="window.open('https://www.instagram.com/technicalturbulence23344/', '_blank');"><i data-feather="instagram"></i>
</button> <button class="home-but" onclick="window.open('mailto:technicalturbulence@gmail.com', '_blank');">
<i data-feather="mail"></i>
</button> <button class="home-but" onclick="window.open('https://youtube.com/@TechnicalTurbulenceFTC', '_blank');">
<i data-feather="youtube"></i>
</button></div>
<div class="border-triangle"></div> <div class="border-triangle"></div>
</div> </div>
<h1 class="heading"><span class="emoji about">About</span></h1> <!-- <h1 class="heading"><span class="emoji about">About</span></h1>
<hr> <hr> -->
<div class="home-info"> <div class="home-info">
<h1 class="sub-header" id="cool-font">We are Technical Turbulence.</h1> <div class="hero-image-section">
<div class="about-section-imgp"> <img src="{{ url_for('static', filename='images/teeaam.png') }}" class="hero-team-image">
<div class="about-sec-img"> <div class="hero-overlay"></div>
<img src="{{ url_for('static', filename='images/main1.png') }}"> <div class="hero-text-container">
<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 our log</button> <button class="stats-button" onclick="window.location.href = '{{ url_for('competitions') }}' ; ">View
our log</button>
</div> </div>
</div> </div>
@@ -54,11 +61,15 @@
<div class="home-info"> <div class="home-info">
<h1 class="sub-header">✊ One team, one goal.</h1> <h1 class="sub-header">✊ One team, one goal.</h1>
<p class="sub-content">We aim to show individuals that with teamwork, creativity, and persistence, anyone can design, build, and code robots. Our mission is part of an outreach initiative to guide young minds toward STEM and skill-building opportunities in robotics and technology. Through this unifying goal, we want to empower young students to learn, grow, and join FTC in the future.</p> <p class="sub-content">We aim to show individuals that with teamwork, creativity, and persistence, anyone
can design, build, and code robots. Our mission is part of an outreach initiative to guide young minds
toward STEM and skill-building opportunities in robotics and technology. Through this unifying goal, we
want to empower young students to learn, grow, and join FTC in the future.</p>
<div class="meet-our-team-container"> <div class="meet-our-team-container">
<img id="meet-our-team" src="{{ url_for('static', filename='images/team.png') }}"> <img id="meet-our-team" src="{{ url_for('static', filename='images/team.png') }}">
<button class="bottom-center-button" onclick="window.location.href = '{{ url_for('contributors') }}' ; ">Meet Our Team</button> <button class="bottom-center-button"
onclick="window.location.href = '{{ url_for('contributors') }}' ; ">Meet Our Team</button>
</div> </div>
</div> </div>
@@ -68,26 +79,33 @@
<hr> <hr>
<div class="home-info"> <div class="home-info">
<p class="sub-content">We, FTC team Technical Turbulence, want to inspire the next generation by sharing our journey in robotics and the exciting world of FTC. By demonstrating our robot and showcasing the engineering behind it, we hope to spark curiosity in young minds about STEM fields. <h1 class="sub-header">❤️ We couldn't have done it without...</h1>
</p>
<h1 class="sub-header">❤️ And we couldn't have done it without...</h1> <p class="sub-content">We, FTC team Technical Turbulence, want to inspire the next generation by sharing our
journey in robotics and the exciting world of FTC. By showcasing the
engineering behind our robot, and with the help of our sponsors, we hope to spark curiosity in young minds about STEM fields.
</p>
<div class="sponsors-container"> <div class="sponsors-container" id="sponnnnn">
<img class="sponsors-card" src="{{ url_for('static', filename='images/ray.png') }}"> <img class="sponsors-card" src="{{ url_for('static', filename='images/ray.png') }}">
<img class="sponsors-card" src="{{ url_for('static', filename='images/cen3.png') }}"> <img class="sponsors-card" src="{{ url_for('static', filename='images/cen3.png') }}">
<img class="sponsors-card" src="{{ url_for('static', filename='images/ti.png') }}"> <img class="sponsors-card" src="{{ url_for('static', filename='images/ti.png') }}">
<img class="sponsors-card" src="{{ url_for('static', filename='images/twc.png') }}"> <img class="sponsors-card" src="{{ url_for('static', filename='images/twc.png') }}">
<img class="sponsors-card" src="{{ url_for('static', filename='images/fw.png') }}">
</div> </div>
<div class="sponsors-more-container"> <div class="sponsors-more-container">
<button type="button" id="sponsors-more-button" onclick="window.location.href = '{{ url_for('sponsors') }}' ; ">See more</button> <button type="button" id="sponsors-more-button"
onclick="window.location.href = '{{ url_for('sponsors') }}' ; ">See more</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</body>
<script>
feather.replace({ width: "40", height: "40" });
</script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Technical Turbulence - Home{% endblock %}
{% block content %}
<body>
<div class="robot-specific">
<h1 class="heading2" id="spon"><span class="emoji robots">Storm</span></h1>
<hr>
</div>
</body>
{% endblock %}

41
templates/robots.html Normal file
View File

@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}Technical Turbulence - Home{% endblock %}
{% block content %}
<body>
<div class="rob">
<h1 class="heading" id="spon"><span class="emoji robots">Robots</span></h1>
<hr>
<p class="info">🤖 we've built and designed several robots. 🤖</p
</div>
<div class="robots-container">
<div class="robots-inner">
<a class="robo-card" href="https://www.youtube.com/watch?v=dQw4w9WgXc">
<div class="robo-card-img">
<img src="{{ url_for('static', filename='images/abyss.png') }}">
</div>
<div class="robo-card-txt">
<p class="robo-card-name">Abyss</p>
<p class="robo-card-desc">2024 · INTO THE DEEP</p>
</div>
</a>
<a class="robo-card" href="/robots/storm">
<div class="robo-card-img">
<img src="{{ url_for('static', filename='images/storm222222.png') }}">
</div>
<div class="robo-card-txt">
<p class="robo-card-name">Storm</p>
<p class="robo-card-desc">2023 · CENTERSTAGE</p>
</div>
</a>
</div>
</div>
</body>
{% endblock %}

View File

@@ -4,38 +4,24 @@
{% block content %} {% block content %}
<body> <div class="sponsors">
<div class="sponsors"> <br>
<h1 class="heading" id="spon"><span class="emoji sponsors">Sponsors</span></h1> <h1 class="heading" id="spon"><span class="emoji sponsors">Sponsors</span></h1>
<hr> <hr>
<p class="info">❤️ companies of various sizes sponsor our initiatives. ❤️</p> <p class="info">❤️ companies of various sizes sponsor our initiatives. ❤️</p>
<div class="sponsors-container"> <div class="sponsors-container">
<div class="card-container"> <div class="card-container">
<a href="https://www.rtx.com/" target="_blank" class="card-sponsors"> {% for sponsor in sponsors %}
<a href="{{ sponsor.website_url }}" target="_blank" class="card-sponsors">
<div class="card-content-sponsors"> <div class="card-content-sponsors">
<img src="{{ url_for('static', filename='images/ray.png') }}" alt="Image 1"> <img src="{{ url_for('static', filename=sponsor.logo_path) }}" alt="{{ sponsor.name }}">
</div>
</a>
<a href="https://www.smilesinfrisco.com/" target="_blank" class="card-sponsors">
<div class="card-content-sponsors">
<img src="{{ url_for('static', filename='images/cen3.png') }}" alt="Image 2">
</div>
</a>
<a href="https://www.ti.com/" target="_blank" class="card-sponsors">
<div class="card-content-sponsors">
<img src="{{ url_for('static', filename='images/ti.png') }}" alt="Image 3">
</div>
</a>
<a href="https://www.twc.texas.gov/" target="_blank" class="card-sponsors">
<div class="card-content-sponsors">
<img src="{{ url_for('static', filename='images/twc.png') }}" alt="Image 3">
</div> </div>
</a> </a>
{% endfor %}
</div> </div>
</div> </div>
</div> </div>
</body>
{% endblock %} {% endblock %}