Spaces:
Sleeping
Sleeping
Upload 23 files
Browse files- utils/__pycache__/challenge_manager.cpython-311.pyc +0 -0
- utils/__pycache__/challenge_manager.cpython-312.pyc +0 -0
- utils/__pycache__/mobile_responsive.cpython-311.pyc +0 -0
- utils/__pycache__/mobile_responsive.cpython-312.pyc +0 -0
- utils/__pycache__/mobile_responsive.cpython-313.pyc +0 -0
- utils/__pycache__/score_tracker.cpython-311.pyc +0 -0
- utils/__pycache__/score_tracker.cpython-312.pyc +0 -0
- utils/__pycache__/score_tracker.cpython-313.pyc +0 -0
- utils/__pycache__/translator.cpython-311.pyc +0 -0
- utils/__pycache__/translator.cpython-312.pyc +0 -0
- utils/__pycache__/ui_components.cpython-311.pyc +0 -0
- utils/__pycache__/ui_components.cpython-312.pyc +0 -0
- utils/__pycache__/ui_components.cpython-313.pyc +0 -0
- utils/__pycache__/user_auth.cpython-311.pyc +0 -0
- utils/__pycache__/user_auth.cpython-312.pyc +0 -0
- utils/__pycache__/user_auth.cpython-313.pyc +0 -0
- utils/challenge_manager.py +78 -0
- utils/init.py +21 -0
- utils/mobile_responsive.py +67 -0
- utils/score_tracker.py +101 -0
- utils/translator.py +36 -0
- utils/ui_components.py +112 -0
- utils/user_auth.py +128 -0
utils/__pycache__/challenge_manager.cpython-311.pyc
ADDED
|
Binary file (6.18 kB). View file
|
|
|
utils/__pycache__/challenge_manager.cpython-312.pyc
ADDED
|
Binary file (5.17 kB). View file
|
|
|
utils/__pycache__/mobile_responsive.cpython-311.pyc
ADDED
|
Binary file (1.61 kB). View file
|
|
|
utils/__pycache__/mobile_responsive.cpython-312.pyc
ADDED
|
Binary file (1.54 kB). View file
|
|
|
utils/__pycache__/mobile_responsive.cpython-313.pyc
ADDED
|
Binary file (1.54 kB). View file
|
|
|
utils/__pycache__/score_tracker.cpython-311.pyc
ADDED
|
Binary file (5.76 kB). View file
|
|
|
utils/__pycache__/score_tracker.cpython-312.pyc
ADDED
|
Binary file (5.13 kB). View file
|
|
|
utils/__pycache__/score_tracker.cpython-313.pyc
ADDED
|
Binary file (5.27 kB). View file
|
|
|
utils/__pycache__/translator.cpython-311.pyc
ADDED
|
Binary file (2.02 kB). View file
|
|
|
utils/__pycache__/translator.cpython-312.pyc
ADDED
|
Binary file (1.72 kB). View file
|
|
|
utils/__pycache__/ui_components.cpython-311.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
utils/__pycache__/ui_components.cpython-312.pyc
ADDED
|
Binary file (8.44 kB). View file
|
|
|
utils/__pycache__/ui_components.cpython-313.pyc
ADDED
|
Binary file (7.16 kB). View file
|
|
|
utils/__pycache__/user_auth.cpython-311.pyc
ADDED
|
Binary file (7.02 kB). View file
|
|
|
utils/__pycache__/user_auth.cpython-312.pyc
ADDED
|
Binary file (6.21 kB). View file
|
|
|
utils/__pycache__/user_auth.cpython-313.pyc
ADDED
|
Binary file (6.37 kB). View file
|
|
|
utils/challenge_manager.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/challenge_manager.py
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import uuid
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from typing import Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
CHALLENGES_FILE = "data/challenges.json"
|
| 9 |
+
|
| 10 |
+
def _load_challenges() -> Dict:
|
| 11 |
+
"""Loads all challenges from the JSON file."""
|
| 12 |
+
if os.path.exists(CHALLENGES_FILE):
|
| 13 |
+
with open(CHALLENGES_FILE, "r") as f:
|
| 14 |
+
try:
|
| 15 |
+
return json.load(f)
|
| 16 |
+
except json.JSONDecodeError:
|
| 17 |
+
return {}
|
| 18 |
+
return {}
|
| 19 |
+
|
| 20 |
+
def _save_challenges(challenges: Dict):
|
| 21 |
+
"""Saves all challenges to the JSON file."""
|
| 22 |
+
os.makedirs(os.path.dirname(CHALLENGES_FILE), exist_ok=True)
|
| 23 |
+
with open(CHALLENGES_FILE, "w") as f:
|
| 24 |
+
json.dump(challenges, f, indent=2)
|
| 25 |
+
|
| 26 |
+
class ChallengeManager:
|
| 27 |
+
def __init__(self):
|
| 28 |
+
self.challenges = _load_challenges()
|
| 29 |
+
|
| 30 |
+
def create_challenge(self, creator_id: str, topic: str, difficulty: str, num_questions: int) -> str:
|
| 31 |
+
"""Creates a new challenge and returns its ID."""
|
| 32 |
+
challenge_id = str(uuid.uuid4())[:8] # Short UUID for easy sharing
|
| 33 |
+
challenge_data = {
|
| 34 |
+
"challenge_id": challenge_id,
|
| 35 |
+
"creator_id": creator_id,
|
| 36 |
+
"created_at": datetime.now().isoformat(),
|
| 37 |
+
"topic": topic,
|
| 38 |
+
"difficulty": difficulty,
|
| 39 |
+
"num_questions": num_questions,
|
| 40 |
+
"participants": {}, # {user_id: {score: int, quiz_results: List}}
|
| 41 |
+
"status": "active" # active, completed
|
| 42 |
+
}
|
| 43 |
+
self.challenges[challenge_id] = challenge_data
|
| 44 |
+
_save_challenges(self.challenges)
|
| 45 |
+
return challenge_id
|
| 46 |
+
|
| 47 |
+
def get_challenge(self, challenge_id: str) -> Optional[Dict]:
|
| 48 |
+
"""Retrieves a challenge by its ID."""
|
| 49 |
+
return self.challenges.get(challenge_id)
|
| 50 |
+
|
| 51 |
+
def update_challenge_score(self, challenge_id: str, user_id: str, score: int, quiz_results: List[Dict]):
|
| 52 |
+
"""Updates a participant's score and quiz results for a challenge."""
|
| 53 |
+
challenge = self.challenges.get(challenge_id)
|
| 54 |
+
if challenge:
|
| 55 |
+
challenge["participants"][user_id] = {
|
| 56 |
+
"score": score,
|
| 57 |
+
"quiz_results": quiz_results,
|
| 58 |
+
"completed_at": datetime.now().isoformat()
|
| 59 |
+
}
|
| 60 |
+
_save_challenges(self.challenges)
|
| 61 |
+
else:
|
| 62 |
+
raise ValueError(f"Challenge with ID {challenge_id} not found.")
|
| 63 |
+
|
| 64 |
+
def get_all_active_challenges(self) -> List[Dict]:
|
| 65 |
+
"""Returns a list of all active challenges."""
|
| 66 |
+
return [c for c in self.challenges.values() if c.get("status") == "active"]
|
| 67 |
+
|
| 68 |
+
def get_challenges_by_creator(self, creator_id: str) -> List[Dict]:
|
| 69 |
+
"""Returns a list of challenges created by a specific user."""
|
| 70 |
+
return [c for c in self.challenges.values() if c.get("creator_id") == creator_id]
|
| 71 |
+
|
| 72 |
+
def set_challenge_status(self, challenge_id: str, status: str):
|
| 73 |
+
"""Sets the status of a challenge."""
|
| 74 |
+
if challenge_id in self.challenges:
|
| 75 |
+
self.challenges[challenge_id]["status"] = status
|
| 76 |
+
_save_challenges(self.challenges)
|
| 77 |
+
else:
|
| 78 |
+
raise ValueError(f"Challenge with ID {challenge_id} not found.")
|
utils/init.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/__init__.py
|
| 2 |
+
"""Utility modules for TriviaVerse application"""
|
| 3 |
+
|
| 4 |
+
from .score_tracker import ScoreTracker
|
| 5 |
+
from .user_auth import init_authentication, authenticate_user
|
| 6 |
+
from .ui_components import *
|
| 7 |
+
from .mobile_responsive import *
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"ScoreTracker",
|
| 11 |
+
"init_authentication",
|
| 12 |
+
"authenticate_user",
|
| 13 |
+
"apply_custom_theme",
|
| 14 |
+
"display_user_stats",
|
| 15 |
+
"display_badges",
|
| 16 |
+
"create_progress_chart",
|
| 17 |
+
"animated_success",
|
| 18 |
+
"create_quiz_card",
|
| 19 |
+
"responsive_columns",
|
| 20 |
+
"responsive_css",
|
| 21 |
+
]
|
utils/mobile_responsive.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/mobile_responsive.py
|
| 2 |
+
"""Utilities for mobile responsiveness."""
|
| 3 |
+
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def responsive_columns(sizes: list, mobile_sizes: list = None):
|
| 8 |
+
"""
|
| 9 |
+
Creates responsive columns for Streamlit.
|
| 10 |
+
On desktop, uses 'sizes'. On mobile, stacks columns or uses 'mobile_sizes'.
|
| 11 |
+
"""
|
| 12 |
+
if mobile_sizes is None:
|
| 13 |
+
mobile_sizes = [1] * len(sizes) # Default to stacking if not specified
|
| 14 |
+
|
| 15 |
+
# Check for mobile client (a bit hacky, but works for most cases)
|
| 16 |
+
# Streamlit doesn't provide a direct way to detect mobile, so we rely on screen width
|
| 17 |
+
# This might not be perfectly accurate but is generally effective.
|
| 18 |
+
# We'll use JavaScript to pass screen width to Streamlit later if needed for more robust detection.
|
| 19 |
+
|
| 20 |
+
# For now, let's assume if the app is run on a narrow screen, it's mobile.
|
| 21 |
+
# This is often handled by CSS, but for column layout, Python-side adjustment can be useful.
|
| 22 |
+
# A more robust solution would involve custom component for screen width detection.
|
| 23 |
+
|
| 24 |
+
# Placeholder for actual mobile detection logic if implemented via JS/custom component
|
| 25 |
+
# For now, we'll let CSS handle most of the responsiveness, and this function
|
| 26 |
+
# will primarily provide a way to define column ratios.
|
| 27 |
+
|
| 28 |
+
# For demonstration, we'll just return st.columns with the given sizes.
|
| 29 |
+
# The actual mobile stacking will be handled by CSS in style.css.
|
| 30 |
+
return st.columns(sizes)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def responsive_css():
|
| 34 |
+
"""Injects CSS for mobile responsiveness."""
|
| 35 |
+
# This CSS is also defined in assets/style.css, but including it here
|
| 36 |
+
# ensures it's applied, especially for column stacking.
|
| 37 |
+
# In a real app, you'd load this from the assets file.
|
| 38 |
+
css = """
|
| 39 |
+
<style>
|
| 40 |
+
/* Mobile-first responsive design */
|
| 41 |
+
@media (max-width: 768px) {
|
| 42 |
+
/* Stack columns on mobile */
|
| 43 |
+
.row-widget.stHorizontal {
|
| 44 |
+
flex-direction: column !important;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.row-widget.stHorizontal > div {
|
| 48 |
+
width: 100% !important;
|
| 49 |
+
margin-bottom: 1rem;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* Full-width buttons on mobile */
|
| 53 |
+
.stButton > button {
|
| 54 |
+
width: 100% !important;
|
| 55 |
+
min-height: 50px;
|
| 56 |
+
font-size: 16px !important;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* Responsive cards */
|
| 60 |
+
.card {
|
| 61 |
+
padding: 15px !important;
|
| 62 |
+
margin: 10px 0 !important;
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
</style>
|
| 66 |
+
"""
|
| 67 |
+
return css
|
utils/score_tracker.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/score_tracker.py
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from config.settings import BASE_POINTS, DIFFICULTY_LEVELS, BADGES, ACHIEVEMENTS
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ScoreTracker:
|
| 9 |
+
def __init__(self, user_id: str):
|
| 10 |
+
self.user_id = user_id
|
| 11 |
+
self.scores_file = f"data/scores_{user_id}.json"
|
| 12 |
+
self.load_scores()
|
| 13 |
+
|
| 14 |
+
def load_scores(self):
|
| 15 |
+
"""Load scores from a JSON file."""
|
| 16 |
+
if os.path.exists(self.scores_file):
|
| 17 |
+
with open(self.scores_file, "r") as f:
|
| 18 |
+
self.scores = json.load(f)
|
| 19 |
+
else:
|
| 20 |
+
self.scores = {
|
| 21 |
+
"total_score": 0,
|
| 22 |
+
"quizzes_completed": 0,
|
| 23 |
+
"quiz_history": [],
|
| 24 |
+
"badges": [],
|
| 25 |
+
"achievements": [],
|
| 26 |
+
"current_streak": 0,
|
| 27 |
+
"best_streak": 0,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def save_scores(self):
|
| 31 |
+
"""Save scores to a JSON file."""
|
| 32 |
+
os.makedirs("data", exist_ok=True)
|
| 33 |
+
with open(self.scores_file, "w") as f:
|
| 34 |
+
json.dump(self.scores, f, indent=4)
|
| 35 |
+
|
| 36 |
+
def add_quiz_result(
|
| 37 |
+
self, mode: str, score: int, total: int, difficulty: str
|
| 38 |
+
) -> int:
|
| 39 |
+
"""Adds a quiz result and updates the total score."""
|
| 40 |
+
self.scores["quizzes_completed"] += 1
|
| 41 |
+
|
| 42 |
+
multiplier = DIFFICULTY_LEVELS.get(difficulty, {}).get("multiplier", 1)
|
| 43 |
+
points_earned = int(score * BASE_POINTS * multiplier)
|
| 44 |
+
self.scores["total_score"] += points_earned
|
| 45 |
+
|
| 46 |
+
self.scores["quiz_history"].append(
|
| 47 |
+
{
|
| 48 |
+
"timestamp": datetime.now().isoformat(),
|
| 49 |
+
"mode": mode,
|
| 50 |
+
"score": score,
|
| 51 |
+
"total": total,
|
| 52 |
+
"difficulty": difficulty,
|
| 53 |
+
"percentage": (score / total) * 100 if total > 0 else 0,
|
| 54 |
+
"points_earned": points_earned,
|
| 55 |
+
}
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
self.update_streak(score > 0)
|
| 59 |
+
self.check_for_badges()
|
| 60 |
+
self.check_for_achievements()
|
| 61 |
+
self.save_scores()
|
| 62 |
+
return points_earned
|
| 63 |
+
|
| 64 |
+
def update_streak(self, successful: bool):
|
| 65 |
+
"""Updates the user's streak."""
|
| 66 |
+
if successful:
|
| 67 |
+
self.scores["current_streak"] += 1
|
| 68 |
+
else:
|
| 69 |
+
self.scores["current_streak"] = 0
|
| 70 |
+
|
| 71 |
+
if self.scores["current_streak"] > self.scores["best_streak"]:
|
| 72 |
+
self.scores["best_streak"] = self.scores["current_streak"]
|
| 73 |
+
|
| 74 |
+
def check_for_badges(self):
|
| 75 |
+
"""Checks if the user has earned any new badges."""
|
| 76 |
+
for badge_id, badge_info in BADGES.items():
|
| 77 |
+
if (
|
| 78 |
+
self.scores["total_score"] >= badge_info["threshold"]
|
| 79 |
+
and badge_id not in self.scores["badges"]
|
| 80 |
+
):
|
| 81 |
+
self.scores["badges"].append(badge_id)
|
| 82 |
+
|
| 83 |
+
def check_for_achievements(self):
|
| 84 |
+
"""Checks for new achievements."""
|
| 85 |
+
# Example: First quiz achievement
|
| 86 |
+
if (
|
| 87 |
+
"first_quiz" not in self.scores["achievements"]
|
| 88 |
+
and self.scores["quizzes_completed"] > 0
|
| 89 |
+
):
|
| 90 |
+
self.scores["achievements"].append("first_quiz")
|
| 91 |
+
|
| 92 |
+
# Example: Streak achievement
|
| 93 |
+
if (
|
| 94 |
+
"streak_5" not in self.scores["achievements"]
|
| 95 |
+
and self.scores["current_streak"] >= 5
|
| 96 |
+
):
|
| 97 |
+
self.scores["achievements"].append("streak_5")
|
| 98 |
+
|
| 99 |
+
def get_stats(self) -> dict:
|
| 100 |
+
"""Returns all user statistics."""
|
| 101 |
+
return self.scores
|
utils/translator.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/translator.py
|
| 2 |
+
"""Translation utility for TriviaVerse"""
|
| 3 |
+
|
| 4 |
+
import streamlit as st
|
| 5 |
+
from googletrans import Translator
|
| 6 |
+
from config.languages import SUPPORTED_LANGUAGES
|
| 7 |
+
from config.localization import UI_TEXT
|
| 8 |
+
|
| 9 |
+
@st.cache_data(ttl=3600)
|
| 10 |
+
def get_translated_texts(dest_lang="en"):
|
| 11 |
+
"""
|
| 12 |
+
Translates all UI texts to the destination language in a single batch
|
| 13 |
+
and caches the result.
|
| 14 |
+
"""
|
| 15 |
+
if dest_lang == "en":
|
| 16 |
+
return UI_TEXT
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
translator = Translator()
|
| 20 |
+
# Get all the values from the UI_TEXT dictionary
|
| 21 |
+
original_texts = list(UI_TEXT.values())
|
| 22 |
+
|
| 23 |
+
# Translate them in a single batch
|
| 24 |
+
translated_texts = translator.translate(original_texts, dest=dest_lang)
|
| 25 |
+
|
| 26 |
+
# Create a new dictionary with the same keys but translated values
|
| 27 |
+
translated_dict = dict(zip(UI_TEXT.keys(), [t.text for t in translated_texts]))
|
| 28 |
+
|
| 29 |
+
return translated_dict
|
| 30 |
+
except Exception as e:
|
| 31 |
+
st.error(f"Translation service failed: {e}. Falling back to English.")
|
| 32 |
+
return UI_TEXT
|
| 33 |
+
|
| 34 |
+
def get_supported_languages():
|
| 35 |
+
"""Returns the list of supported languages."""
|
| 36 |
+
return SUPPORTED_LANGUAGES
|
utils/ui_components.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/ui_components.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
from config.settings import BADGES
|
| 5 |
+
from config.themes import THEMES
|
| 6 |
+
|
| 7 |
+
def apply_custom_theme(theme_name: str):
|
| 8 |
+
"""Applies a custom theme using CSS variables."""
|
| 9 |
+
theme = THEMES.get(theme_name, THEMES["default"])
|
| 10 |
+
|
| 11 |
+
css_vars = {
|
| 12 |
+
"--primary-color": theme["primary_color"],
|
| 13 |
+
"--background-color": theme["background_color"],
|
| 14 |
+
"--secondary-background-color": theme["secondary_background_color"],
|
| 15 |
+
"--text-color": theme["text_color"],
|
| 16 |
+
"--font": theme["font"],
|
| 17 |
+
"--card-background": theme.get("card_background", "#FFFFFF"),
|
| 18 |
+
"--shadow-light": theme.get("shadow_light", "0 4px 15px rgba(0, 0, 0, 0.05)"),
|
| 19 |
+
"--shadow-medium": theme.get("shadow_medium", "0 8px 30px rgba(0, 0, 0, 0.1)"),
|
| 20 |
+
"--highlight-color": theme.get("highlight_color", theme["primary_color"]),
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
css_vars_str = "\n".join([f"{key}: {value};" for key, value in css_vars.items()])
|
| 24 |
+
|
| 25 |
+
css = f"""
|
| 26 |
+
<style>
|
| 27 |
+
:root {{ {css_vars_str} }}
|
| 28 |
+
.stApp {{ background-color: var(--background-color); color: var(--text-color); }}
|
| 29 |
+
.stSidebar {{ background-color: var(--secondary-background-color); }}
|
| 30 |
+
.stButton>button {{ background-color: var(--primary-color); color: white; box-shadow: var(--shadow-light); }}
|
| 31 |
+
.stButton>button:hover {{ background-color: var(--secondary-color); box-shadow: var(--shadow-medium); }}
|
| 32 |
+
/* Add other theme styles here */
|
| 33 |
+
</style>
|
| 34 |
+
"""
|
| 35 |
+
st.markdown(css, unsafe_allow_html=True)
|
| 36 |
+
|
| 37 |
+
def display_user_stats(stats: dict, ui: dict):
|
| 38 |
+
"""Displays user statistics in a dashboard format."""
|
| 39 |
+
cols = st.columns(4)
|
| 40 |
+
with cols[0]:
|
| 41 |
+
st.metric(ui.get("total_score", "Total Score"), f"{stats.get('total_score', 0):,}")
|
| 42 |
+
with cols[1]:
|
| 43 |
+
st.metric(ui.get("quizzes", "Quizzes Played"), stats.get("quizzes_completed", 0))
|
| 44 |
+
with cols[2]:
|
| 45 |
+
st.metric(ui.get("current_streak", "Current Streak"), f"🔥 {stats.get('current_streak', 0)}")
|
| 46 |
+
with cols[3]:
|
| 47 |
+
st.metric(ui.get("best_streak", "Best Streak"), f"⭐ {stats.get('best_streak', 0)}")
|
| 48 |
+
|
| 49 |
+
def display_badges(earned_badges: list, ui: dict):
|
| 50 |
+
"""Displays earned badges."""
|
| 51 |
+
if not earned_badges:
|
| 52 |
+
return
|
| 53 |
+
|
| 54 |
+
st.markdown(f"### 🏆 {ui.get('your_badges', 'Your Badges')}")
|
| 55 |
+
cols = st.columns(len(earned_badges))
|
| 56 |
+
for i, badge_id in enumerate(earned_badges):
|
| 57 |
+
badge = BADGES.get(badge_id)
|
| 58 |
+
if badge:
|
| 59 |
+
with cols[i]:
|
| 60 |
+
st.markdown(f"<div style='text-align: center;'><span style='font-size: 50px;'>{badge['icon']}</span><br><b>{ui.get(badge['name'].lower(), badge['name'])}</b></div>", unsafe_allow_html=True)
|
| 61 |
+
|
| 62 |
+
def create_progress_chart(stats: dict, ui: dict) -> go.Figure:
|
| 63 |
+
"""Creates a progress chart for the user."""
|
| 64 |
+
history = stats.get("quiz_history", [])
|
| 65 |
+
if not history:
|
| 66 |
+
return go.Figure()
|
| 67 |
+
|
| 68 |
+
dates = [item["timestamp"] for item in history]
|
| 69 |
+
scores = [item["percentage"] for item in history]
|
| 70 |
+
|
| 71 |
+
fig = go.Figure()
|
| 72 |
+
fig.add_trace(go.Scatter(x=dates, y=scores, mode="lines+markers", name=ui.get("score", "Score") + " %"))
|
| 73 |
+
fig.update_layout(
|
| 74 |
+
title=ui.get("quiz_performance_over_time", "Quiz Performance Over Time"),
|
| 75 |
+
xaxis_title=ui.get("date", "Date"),
|
| 76 |
+
yaxis_title=f"{ui.get('score', 'Score')} (%)",
|
| 77 |
+
yaxis_range=[0, 100],
|
| 78 |
+
)
|
| 79 |
+
return fig
|
| 80 |
+
|
| 81 |
+
def animated_success(message: str):
|
| 82 |
+
"""Displays an animated success message."""
|
| 83 |
+
st.markdown(f"<div class='pop-in'>{st.success(message)}</div>", unsafe_allow_html=True)
|
| 84 |
+
|
| 85 |
+
def create_quiz_card(question: str, options: list, key: str):
|
| 86 |
+
"""Creates a card for a multiple-choice question."""
|
| 87 |
+
st.markdown(f"<div style='background-color: var(--card-background); padding: 20px; border-radius: 10px; box-shadow: var(--shadow-light);'>", unsafe_allow_html=True)
|
| 88 |
+
st.subheader(question)
|
| 89 |
+
st.session_state[key] = st.radio(
|
| 90 |
+
"Select your answer:",
|
| 91 |
+
options,
|
| 92 |
+
index=None,
|
| 93 |
+
key=f"{key}_radio",
|
| 94 |
+
label_visibility="collapsed",
|
| 95 |
+
)
|
| 96 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 97 |
+
|
| 98 |
+
def render_flashcard(front_content: str, back_content: str, is_flipped: bool):
|
| 99 |
+
"""Renders a flippable flashcard."""
|
| 100 |
+
card_style = "width: 100%; height: 300px; perspective: 1000px;"
|
| 101 |
+
flipper_style = f'position: relative; width: 100%; height: 100%; transition: transform 0.6s; transform-style: preserve-3d; transform: {"rotateY(180deg)" if is_flipped else "none"};'
|
| 102 |
+
face_style = "position: absolute; width: 100%; height: 100%; -webkit-backface-visibility: hidden; backface-visibility: hidden; display: flex; justify-content: center; align-items: center; padding: 20px; border-radius: 10px; box-shadow: var(--shadow-light);"
|
| 103 |
+
front_style = f"{face_style} background-color: var(--primary-color); color: white;"
|
| 104 |
+
back_style = f"{face_style} background-color: var(--card-background); color: var(--text-color); transform: rotateY(180deg);"
|
| 105 |
+
|
| 106 |
+
st.markdown(
|
| 107 |
+
f"<div style='{card_style}'><div style='{flipper_style}'>"
|
| 108 |
+
f"<div style='{front_style}'><div>{front_content}</div></div>"
|
| 109 |
+
f"<div style='{back_style}'><div>{back_content}</div></div>"
|
| 110 |
+
f"</div></div>",
|
| 111 |
+
unsafe_allow_html=True,
|
| 112 |
+
)
|
utils/user_auth.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/user_auth.py
|
| 2 |
+
"""User authentication module"""
|
| 3 |
+
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import hashlib
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
# Simple authentication system for demo
|
| 11 |
+
# In production, use proper authentication libraries
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def hash_password(password: str) -> str:
|
| 15 |
+
"""Hash password using SHA256"""
|
| 16 |
+
return hashlib.sha256(password.encode()).hexdigest()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def load_users():
|
| 20 |
+
"""Load users from file"""
|
| 21 |
+
users_file = "data/users.json"
|
| 22 |
+
if os.path.exists(users_file):
|
| 23 |
+
with open(users_file, "r") as f:
|
| 24 |
+
return json.load(f)
|
| 25 |
+
return {}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def save_users(users):
|
| 29 |
+
"""Save users to file"""
|
| 30 |
+
os.makedirs("data", exist_ok=True)
|
| 31 |
+
with open("data/users.json", "w") as f:
|
| 32 |
+
json.dump(users, f, indent=2)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def init_authentication():
|
| 36 |
+
"""Initialize authentication in session state"""
|
| 37 |
+
if "authenticated" not in st.session_state:
|
| 38 |
+
st.session_state.authenticated = False
|
| 39 |
+
if "username" not in st.session_state:
|
| 40 |
+
st.session_state.username = None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def authenticate_user():
|
| 44 |
+
"""Show authentication UI and handle login/signup"""
|
| 45 |
+
if st.session_state.authenticated:
|
| 46 |
+
return True
|
| 47 |
+
|
| 48 |
+
st.title("🎯 Welcome to TriviaVerse")
|
| 49 |
+
st.markdown("### Please login or create an account to continue")
|
| 50 |
+
|
| 51 |
+
tab1, tab2 = st.tabs(["Login", "Sign Up"])
|
| 52 |
+
|
| 53 |
+
with tab1:
|
| 54 |
+
with st.form("login_form"):
|
| 55 |
+
username = st.text_input("Username")
|
| 56 |
+
password = st.text_input("Password", type="password")
|
| 57 |
+
submitted = st.form_submit_button("Login", type="primary")
|
| 58 |
+
|
| 59 |
+
if submitted:
|
| 60 |
+
users = load_users()
|
| 61 |
+
|
| 62 |
+
if username in users:
|
| 63 |
+
if users[username]["password"] == hash_password(password):
|
| 64 |
+
st.session_state.authenticated = True
|
| 65 |
+
st.session_state.username = username
|
| 66 |
+
st.session_state.user_id = username
|
| 67 |
+
|
| 68 |
+
# Update last login
|
| 69 |
+
users[username]["last_login"] = datetime.now().isoformat()
|
| 70 |
+
save_users(users)
|
| 71 |
+
|
| 72 |
+
st.success("Login successful!")
|
| 73 |
+
st.rerun()
|
| 74 |
+
else:
|
| 75 |
+
st.error("Invalid password")
|
| 76 |
+
else:
|
| 77 |
+
st.error("Username not found")
|
| 78 |
+
|
| 79 |
+
with tab2:
|
| 80 |
+
with st.form("signup_form"):
|
| 81 |
+
new_username = st.text_input("Choose a username")
|
| 82 |
+
new_password = st.text_input("Choose a password", type="password")
|
| 83 |
+
confirm_password = st.text_input("Confirm password", type="password")
|
| 84 |
+
email = st.text_input("Email (optional)")
|
| 85 |
+
|
| 86 |
+
terms = st.checkbox("I agree to the terms and conditions")
|
| 87 |
+
submitted = st.form_submit_button("Sign Up", type="primary")
|
| 88 |
+
|
| 89 |
+
if submitted:
|
| 90 |
+
if not new_username or not new_password:
|
| 91 |
+
st.error("Username and password are required")
|
| 92 |
+
elif new_password != confirm_password:
|
| 93 |
+
st.error("Passwords do not match")
|
| 94 |
+
elif not terms:
|
| 95 |
+
st.error("Please agree to the terms and conditions")
|
| 96 |
+
else:
|
| 97 |
+
users = load_users()
|
| 98 |
+
|
| 99 |
+
if new_username in users:
|
| 100 |
+
st.error("Username already exists")
|
| 101 |
+
else:
|
| 102 |
+
# Create new user
|
| 103 |
+
users[new_username] = {
|
| 104 |
+
"password": hash_password(new_password),
|
| 105 |
+
"email": email,
|
| 106 |
+
"created": datetime.now().isoformat(),
|
| 107 |
+
"last_login": datetime.now().isoformat(),
|
| 108 |
+
}
|
| 109 |
+
save_users(users)
|
| 110 |
+
|
| 111 |
+
# Auto login
|
| 112 |
+
st.session_state.authenticated = True
|
| 113 |
+
st.session_state.username = new_username
|
| 114 |
+
st.session_state.user_id = new_username
|
| 115 |
+
|
| 116 |
+
st.success("Account created successfully!")
|
| 117 |
+
st.balloons()
|
| 118 |
+
st.rerun()
|
| 119 |
+
|
| 120 |
+
# Guest mode option
|
| 121 |
+
st.divider()
|
| 122 |
+
if st.button("Continue as Guest", type="secondary"):
|
| 123 |
+
st.session_state.authenticated = True
|
| 124 |
+
st.session_state.username = "Guest"
|
| 125 |
+
st.session_state.user_id = f"guest_{int(datetime.now().timestamp())}"
|
| 126 |
+
st.rerun()
|
| 127 |
+
|
| 128 |
+
return False
|