From 1837590a791ca1286c723de287a13504b4a74f68 Mon Sep 17 00:00:00 2001 From: efigueroa Date: Sat, 6 Sep 2025 23:20:09 -0700 Subject: [PATCH] Add loading states and performance optimizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Performance Improvements: - Added loading template with skeleton UI and progress animations - Split routes: show loading state immediately, then fetch data - Added memory cache for API responses (5-minute TTL) - Cached expensive calls: user lookup, NFL state, user leagues - Meta refresh redirect instead of JavaScript for no-JS requirement User Experience: - Immediate page load with branded loading screen - Skeleton UI shows expected layout structure - Progress bar and spinner animations during load - Users see something instantly instead of blank page Technical Details: - / and // now show loading.html first - ///data route handles actual API calls - Simple memory cache with fallback to stale data on errors - CSS animations for loading states and skeleton placeholders 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app.py | 66 +++++++++++++---- static/style.css | 156 +++++++++++++++++++++++++++++++++++++++++ templates/loading.html | 95 +++++++++++++++++++++++++ 3 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 templates/loading.html diff --git a/app.py b/app.py index 34e131c..cb36a58 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ from flask import Flask, render_template, jsonify, request, session, redirect, url_for -from datetime import datetime +from datetime import datetime, timedelta import os import sys import traceback @@ -20,6 +20,34 @@ app.config.from_object(Config) sleeper_api = SleeperAPI() espn_api = ESPNAPI() +# Simple memory cache for API responses +api_cache = {} +CACHE_DURATION = 300 # 5 minutes in seconds + +def get_cached_or_fetch(cache_key, fetch_function, *args): + """Get data from cache or fetch it and cache the result""" + now = datetime.now() + + if cache_key in api_cache: + cached_data, cached_time = api_cache[cache_key] + if (now - cached_time).total_seconds() < CACHE_DURATION: + print(f"DEBUG: Using cached data for {cache_key}", flush=True) + return cached_data + + print(f"DEBUG: Fetching fresh data for {cache_key}", flush=True) + try: + data = fetch_function(*args) + api_cache[cache_key] = (data, now) + return data + except Exception as e: + print(f"ERROR: Failed to fetch {cache_key}: {str(e)}", flush=True) + # Return cached data if available, even if stale + if cache_key in api_cache: + cached_data, _ = api_cache[cache_key] + print(f"DEBUG: Using stale cached data for {cache_key}", flush=True) + return cached_data + raise + def get_league_color(league_index): """Assign colors to leagues in order""" colors = app.config['LEAGUE_COLORS'] @@ -76,13 +104,14 @@ def set_timezone(): @app.route('/') def dashboard_current(username): - """Dashboard for current NFL week""" + """Dashboard for current NFL week - shows loading state first""" print(f"DEBUG: Dashboard current route - username: '{username}'", flush=True) try: nfl_state = sleeper_api.get_nfl_state() current_week = nfl_state.get('display_week', 1) print(f"DEBUG: Current week: {current_week}", flush=True) - return dashboard(username, current_week) + # Show loading state first for better UX + return render_template('loading.html', username=username, week=current_week) except Exception as e: print(f"ERROR: dashboard_current exception - {str(e)}", flush=True) print(f"ERROR: Full traceback: {traceback.format_exc()}", flush=True) @@ -91,23 +120,30 @@ def dashboard_current(username): @app.route('//') def dashboard_week(username, week): - """Dashboard for specific week""" + """Dashboard for specific week - shows loading state first""" print(f"DEBUG: Dashboard week - username: '{username}', week: {week}", flush=True) + # Show loading state first for better UX + return render_template('loading.html', username=username, week=week) + +@app.route('///data') +def dashboard_data(username, week): + """Dashboard data route - actual API calls and data loading""" + print(f"DEBUG: Dashboard data route - username: '{username}', week: {week}", flush=True) return dashboard(username, week) @app.route('///refresh', methods=['POST']) def refresh_scores(username, week): - """Server-side refresh scores - redirect back to dashboard""" + """Server-side refresh scores - redirect back to dashboard data""" print(f"DEBUG: Refresh scores POST - username: '{username}', week: {week}", flush=True) - return redirect(url_for('dashboard_week', username=username, week=week)) + return redirect(url_for('dashboard_data', username=username, week=week)) def dashboard(username, week): """Main dashboard logic - fetch and display user's fantasy data""" print(f"DEBUG: Dashboard function START - username: '{username}', week: {week}", flush=True) try: - # Get user info from Sleeper - print(f"DEBUG: Calling get_user() with username: '{username}'", flush=True) - user = sleeper_api.get_user(username) + # Get user info from Sleeper (cached) + user_cache_key = f"user_{username}" + user = get_cached_or_fetch(user_cache_key, sleeper_api.get_user, username) print(f"DEBUG: get_user() returned: {user}", flush=True) if not user: @@ -117,16 +153,16 @@ def dashboard(username, week): print(f"DEBUG: User found - ID: {user.get('user_id')}, Display: {user.get('display_name', 'Unknown')}", flush=True) - # Get current NFL season - print("DEBUG: Calling get_nfl_state()", flush=True) - nfl_state = sleeper_api.get_nfl_state() + # Get current NFL season (cached) + nfl_cache_key = "nfl_state" + nfl_state = get_cached_or_fetch(nfl_cache_key, sleeper_api.get_nfl_state) season = nfl_state.get('season', str(datetime.now().year)) print(f"DEBUG: Using season: {season}", flush=True) - # Get user's fantasy leagues + # Get user's fantasy leagues (cached) user_id = user['user_id'] - print(f"DEBUG: Calling get_user_leagues() with user_id: '{user_id}', season: '{season}'", flush=True) - leagues = sleeper_api.get_user_leagues(user_id, season) + leagues_cache_key = f"leagues_{user_id}_{season}" + leagues = get_cached_or_fetch(leagues_cache_key, sleeper_api.get_user_leagues, user_id, season) print(f"DEBUG: Found {len(leagues) if leagues else 0} leagues", flush=True) if leagues: diff --git a/static/style.css b/static/style.css index 4efe67c..878a0f6 100644 --- a/static/style.css +++ b/static/style.css @@ -1137,4 +1137,160 @@ body { letter-spacing: 0.5px; } +/* Loading States and Animations */ +.loading-section { + text-align: center; + background: var(--bg-primary); + border-radius: 12px; + padding: 40px; + margin: 20px 0; + box-shadow: var(--shadow-sm); +} + +.loading-container { + max-width: 400px; + margin: 0 auto; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 4px solid var(--border-light); + border-top: 4px solid var(--accent); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-container h2 { + color: var(--text-primary); + margin-bottom: 10px; + font-size: 1.3rem; +} + +.loading-container p { + color: var(--text-secondary); + margin-bottom: 20px; +} + +.loading-progress { + width: 100%; + max-width: 300px; + margin: 0 auto; +} + +.progress-bar { + width: 100%; + height: 6px; + background: var(--border-light); + border-radius: 3px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(45deg, var(--accent), var(--accent-hover)); + animation: progress 2s ease-in-out infinite; + border-radius: 3px; +} + +@keyframes progress { + 0% { width: 0%; } + 50% { width: 70%; } + 100% { width: 100%; } +} + +.loading-indicator { + opacity: 0.7; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 0.3; } +} + +/* Skeleton Loading Styles */ +.skeleton-score-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid var(--border-light); +} + +.skeleton-league-info { + display: flex; + align-items: center; + gap: 8px; + min-width: 140px; +} + +.skeleton-dot { + width: 10px; + height: 10px; + background: var(--border-secondary); + border-radius: 50%; + animation: skeleton 1.5s ease-in-out infinite; +} + +.skeleton-text { + height: 16px; + background: var(--border-secondary); + border-radius: 4px; + animation: skeleton 1.5s ease-in-out infinite; +} + +.skeleton-league-name { + width: 120px; +} + +.skeleton-score { + width: 200px; +} + +.skeleton-timezone { + width: 150px; + height: 12px; + margin-top: 5px; + display: inline-block; +} + +.skeleton-day-row { + background: var(--bg-primary); + border-radius: 16px; + margin-bottom: 20px; + box-shadow: var(--shadow-md); + overflow: hidden; +} + +.skeleton-day-header { + background: var(--border-secondary); + padding: 15px 20px; + animation: skeleton 1.5s ease-in-out infinite; +} + +.skeleton-day-name { + width: 150px; + height: 20px; +} + +@keyframes skeleton { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Disabled button states */ +.week-btn.disabled, +.refresh-btn.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + diff --git a/templates/loading.html b/templates/loading.html new file mode 100644 index 0000000..bbdf0db --- /dev/null +++ b/templates/loading.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} + +{% block title %}Loading {{ username }} - Week {{ week }}{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} + + +
+

{{ username }}

+
+ ← Week {{ week - 1 }} + Week {{ week }} + Week {{ week + 1 }} → +
+ +
+ +
+
+ + +
+
+
+

Loading your fantasy data...

+

Fetching leagues and player information

+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+

Week {{ week }} Games

+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +{% endblock %} \ No newline at end of file