commit 11f14f50485fabf52b7e27219473dff39e204063 Author: hex Date: Mon May 19 20:13:39 2025 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ebc7ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Environment variables +.env + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environment +venv/ +env/ +ENV/ + +# Flask instance folder +instance/ + +# Database files +*.sqlite +*.sqlite3 +*.db + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# Logs +logs/ +*.log + +# OS specific files +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6fc0bd --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Game Twacker App ʕ•ᴥ•ʔ + +A web appwication fow twacking videogames you'we cuwwently pwaying or compweting! This app uses the IGDB (Intewnet Game Database) API to seawch fow games and save them to youw pewsonal twacking wist~ + +## Featuwes ✨ + +- Seawch fow games thwough the IGDB API +- Twack up to 5 games you'we cuwwently pwaying +- Update game pwogwess (0-100%) +- Set game status (Pwaying, Compweted, On Howd) +- Add pewsonal notes fow each game +- Beautifuw dawk mode UI with puwple accent + +## Technologies Used 🛠️ + +- Python 3.12 +- Flask web fwamework +- SQLite database +- IGDB API +- HTML, CSS, and JavaScript +- Fwask-SQLAwchemy ORM +- Responsibe design + +## Setup Instructions 🚀 + +### 1. Cweate an IGDB API Account + +You need to get API cwedentwials from IGDB: + +1. Go to [Twitch Developer Portal](https://dev.twitch.tv/console/apps/create) +2. Wegistew a new appwication +3. Get youw Cwient ID and Cwient Secwet +4. Add them to the `.env` fiwe: + +``` +IGDB_CLIENT_ID=your_client_id_here +IGDB_CLIENT_SECRET=your_client_secret_here +``` + +### 2. Instaww Dependencwies + +```bash +# Activate the viwtuaw enviwonment +source venv/bin/activate + +# Instaww all wequired packages +pip install -r requirements.txt +``` + +### 3. Wun the Appwication + +```bash +# Make suwe you'we in the pwoject woot with viwtuaw enviwonment activated +python run.py +``` + +The app wiww be avaiwable at `http://127.0.0.1:5000/` + +## Scweenshots 📷 + +(Add scweenshots when avaiwable~) + +## Futuwe Enhancements 🌟 + +- User authentication +- Supppowt fow twacking mowe than 5 games +- Game compwetion statistics +- Integwation with othew gaming pwatforms +- Achievements fow compweting games +- Timew/twacking fow time spent pwaying +- Expowt/impowt game wists + +--- + +Made with wove and Fwask! (。・ω・。) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..8cd6c09 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,39 @@ +import os +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Initialize SQLAlchemy +db = SQLAlchemy() + +def create_app(): + app = Flask(__name__, instance_relative_config=True) + + # Configure the app + app.config.from_mapping( + SECRET_KEY=os.environ.get('SECRET_KEY', 'dev'), + SQLALCHEMY_DATABASE_URI=f"sqlite:///{os.path.join(app.instance_path, 'game_tracker.sqlite')}", + SQLALCHEMY_TRACK_MODIFICATIONS=False, + ) + + # Ensure the instance folder exists + try: + os.makedirs(app.instance_path) + except OSError: + pass + + # Initialize database with app + db.init_app(app) + + # Import and register blueprints + from .routes import main_bp + app.register_blueprint(main_bp) + + # Create database tables + with app.app_context(): + db.create_all() + + return app diff --git a/app/igdb_helpers.py b/app/igdb_helpers.py new file mode 100644 index 0000000..627b8a5 --- /dev/null +++ b/app/igdb_helpers.py @@ -0,0 +1,155 @@ +import os +import requests +import time + +class IGDBHelper: + def __init__(self): + self.client_id = os.environ.get('IGDB_CLIENT_ID') + self.client_secret = os.environ.get('IGDB_CLIENT_SECRET') + self.access_token = None + self.token_expires = 0 + self.base_url = "https://api.igdb.com/v4" + + def get_access_token(self): + """Get a new access token from Twitch API for IGDB""" + if not self.client_id or not self.client_secret: + raise ValueError("IGDB API credentials not found in environment variables") + + current_time = time.time() + + # If token exists and not expired, return it + if self.access_token and current_time < self.token_expires: + return self.access_token + + # Otherwise get a new token + auth_url = "https://id.twitch.tv/oauth2/token" + auth_params = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "client_credentials" + } + + response = requests.post(auth_url, params=auth_params) + data = response.json() + + if response.status_code == 200 and "access_token" in data: + self.access_token = data["access_token"] + # Set expiry time (subtract 60 seconds to be safe) + self.token_expires = current_time + data["expires_in"] - 60 + return self.access_token + else: + raise Exception(f"Failed to get access token: {data}") + + def search_games(self, query, limit=10): + """Search for games by name""" + token = self.get_access_token() + + headers = { + 'Client-ID': self.client_id, + 'Authorization': f'Bearer {token}' + } + + # IGDB API endpoint for games + endpoint = f"{self.base_url}/games" + + # Search for games with the given query, include relevant fields + body = f""" + search "{query}"; + fields name, cover.url, summary, first_release_date, platforms.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; + limit {limit}; + """ + + response = requests.post(endpoint, headers=headers, data=body) + + if response.status_code == 200: + games = response.json() + return self._process_game_results(games) + else: + raise Exception(f"IGDB API error: {response.status_code} - {response.text}") + + def get_game_by_id(self, game_id): + """Get a game by its IGDB ID""" + token = self.get_access_token() + + headers = { + 'Client-ID': self.client_id, + 'Authorization': f'Bearer {token}' + } + + # IGDB API endpoint for games + endpoint = f"{self.base_url}/games" + + # Get game with the given ID, include relevant fields + body = f""" + where id = {game_id}; + fields name, cover.url, summary, first_release_date, platforms.name, involved_companies.company.name, involved_companies.developer, involved_companies.publisher; + limit 1; + """ + + response = requests.post(endpoint, headers=headers, data=body) + + if response.status_code == 200 and response.json(): + game = response.json()[0] + return self._process_game_results([game])[0] + else: + raise Exception(f"IGDB API error: {response.status_code} - {response.text}") + + def _process_game_results(self, games): + """Process the raw API results into a more usable format""" + processed_games = [] + + for game in games: + # Extract developer and publisher + developer = None + publisher = None + + if 'involved_companies' in game: + for company in game['involved_companies']: + if company.get('developer', False): + try: + developer = company['company']['name'] + except (KeyError, TypeError): + pass + + if company.get('publisher', False): + try: + publisher = company['company']['name'] + except (KeyError, TypeError): + pass + + # Format cover URL + cover_url = None + if 'cover' in game and 'url' in game['cover']: + # IGDB returns image URLs without https:, and in thumbnail size + # Replace to get larger images: t_thumb -> t_cover_big + cover_url = game['cover']['url'] + if cover_url.startswith('//'): + cover_url = f"https:{cover_url}" + cover_url = cover_url.replace('t_thumb', 't_cover_big') + + # Format platforms + platforms = [] + if 'platforms' in game: + platforms = [platform['name'] for platform in game['platforms']] + + # Format release date + release_date = None + if 'first_release_date' in game: + release_date = game['first_release_date'] + # Convert Unix timestamp to readable format + release_date = time.strftime('%Y-%m-%d', time.gmtime(release_date)) + + processed_game = { + 'id': game['id'], + 'name': game['name'], + 'cover_url': cover_url, + 'description': game.get('summary'), + 'release_date': release_date, + 'platforms': platforms, + 'developer': developer, + 'publisher': publisher + } + + processed_games.append(processed_game) + + return processed_games diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..c09dcaa --- /dev/null +++ b/app/models.py @@ -0,0 +1,21 @@ +from . import db +from datetime import datetime + +class Game(db.Model): + id = db.Column(db.Integer, primary_key=True) + igdb_id = db.Column(db.Integer, unique=True, nullable=False) + name = db.Column(db.String(100), nullable=False) + cover_url = db.Column(db.String(255)) + description = db.Column(db.Text) + release_date = db.Column(db.DateTime) + platform = db.Column(db.String(100)) + developer = db.Column(db.String(100)) + publisher = db.Column(db.String(100)) + status = db.Column(db.String(20), default="Playing") # Playing, Completed, On Hold + progress = db.Column(db.Integer, default=0) # 0-100% + notes = db.Column(db.Text) + date_added = db.Column(db.DateTime, default=datetime.utcnow) + last_updated = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f'' diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..e475cb1 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,114 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app +from .models import Game +from . import db +from .igdb_helpers import IGDBHelper +import os + +main_bp = Blueprint('main', __name__) +igdb = IGDBHelper() + +@main_bp.route('/') +def index(): + """Home page showing currently tracked games""" + games = Game.query.all() + return render_template('index.html', games=games) + +@main_bp.route('/search', methods=['GET', 'POST']) +def search_games(): + """Search for games using IGDB API""" + results = [] + query = '' + + if request.method == 'POST': + query = request.form.get('query', '') + + if query: + try: + results = igdb.search_games(query) + except Exception as e: + flash(f"Oopsie! Couldn't search IGDB: {str(e)}", 'error') + + return render_template('search.html', results=results, query=query) + +@main_bp.route('/add_game/', methods=['GET', 'POST']) +def add_game(igdb_id): + """Add a game to tracking list""" + # Check if we already have this game + existing_game = Game.query.filter_by(igdb_id=igdb_id).first() + + if existing_game: + flash(f"You'we alweady twacking {existing_game.name}!", 'info') + return redirect(url_for('main.index')) + + # Check if we're at the 5 game limit + if Game.query.count() >= 5: + flash("You can onwy twack up to 5 games at once! Pwease wemove a game fiwst~", 'error') + return redirect(url_for('main.index')) + + # Get game details from IGDB + try: + game_data = igdb.get_game_by_id(igdb_id) + + # Create new Game object + new_game = Game( + igdb_id=game_data['id'], + name=game_data['name'], + cover_url=game_data['cover_url'], + description=game_data['description'], + release_date=game_data['release_date'], + platform=', '.join(game_data['platforms']) if game_data['platforms'] else None, + developer=game_data['developer'], + publisher=game_data['publisher'], + status="Playing", + progress=0 + ) + + db.session.add(new_game) + db.session.commit() + + flash(f"Added {new_game.name} to youw twacking wist! ^w^", 'success') + return redirect(url_for('main.index')) + + except Exception as e: + flash(f"Faiwed to add game: {str(e)}", 'error') + return redirect(url_for('main.search_games')) + +@main_bp.route('/update_game/', methods=['POST']) +def update_game(game_id): + """Update game progress and status""" + game = Game.query.get_or_404(game_id) + + if request.method == 'POST': + # Update status if provided + if 'status' in request.form: + game.status = request.form['status'] + + # Update progress if provided + if 'progress' in request.form: + try: + progress = int(request.form['progress']) + if 0 <= progress <= 100: + game.progress = progress + except ValueError: + flash("Pwogwess must be a numbew between 0 and 100!", 'error') + + # Update notes if provided + if 'notes' in request.form: + game.notes = request.form['notes'] + + db.session.commit() + flash(f"Updated {game.name}! (。・ω・。)", 'success') + + return redirect(url_for('main.index')) + +@main_bp.route('/remove_game/', methods=['POST']) +def remove_game(game_id): + """Remove a game from tracking list""" + game = Game.query.get_or_404(game_id) + name = game.name + + db.session.delete(game) + db.session.commit() + + flash(f"Removed {name} from youw twacking wist! OwO", 'success') + return redirect(url_for('main.index')) diff --git a/app/static/css/styles.css b/app/static/css/styles.css new file mode 100644 index 0000000..33ad7a6 --- /dev/null +++ b/app/static/css/styles.css @@ -0,0 +1,649 @@ +:root { + --primary-color: #3a0570; + --primary-light: #6a35a0; + --primary-dark: #260050; + --accent-color: #9c27b0; + --text-light: #f0f0f0; + --text-dark: #121212; + --bg-dark: #121212; + --bg-card: #1e1e1e; + --bg-card-hover: #2a2a2a; + --border-color: #333333; + --danger-color: #f44336; + --success-color: #4caf50; + --info-color: #2196f3; + --warning-color: #ff9800; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Nunito', sans-serif; + background-color: var(--bg-dark); + color: var(--text-light); + line-height: 1.6; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +header { + background-color: var(--primary-dark); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + padding: 1rem 0; +} + +header .container { + display: flex; + justify-content: space-between; + align-items: center; +} + +header h1 { + font-size: 1.8rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-light); +} + +header i { + color: var(--primary-light); +} + +nav ul { + display: flex; + list-style: none; + gap: 1.5rem; +} + +nav a { + color: var(--text-light); + text-decoration: none; + font-weight: 600; + transition: all 0.2s; + padding: 0.5rem 1rem; + border-radius: 20px; +} + +nav a:hover { + background-color: var(--primary-light); +} + +main { + flex: 1; + padding: 2rem 0; +} + +h1, h2, h3 { + margin-bottom: 1rem; +} + +footer { + background-color: var(--primary-dark); + padding: 1.5rem 0; + text-align: center; + margin-top: 2rem; + color: var(--text-light); + opacity: 0.8; +} + +footer a { + color: var(--accent-color); + text-decoration: none; +} + +footer .heart { + color: var(--danger-color); + display: inline-block; + animation: heartbeat 1.5s infinite; +} + +/* Flashes */ +.flashes { + position: fixed; + top: 1rem; + right: 1rem; + max-width: 300px; + z-index: 1000; +} + +.flash { + padding: 0.8rem; + margin-bottom: 0.5rem; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + cursor: pointer; + transition: opacity 0.3s; + animation: flash-in 0.3s ease-out; +} + +.flash-success { + background-color: var(--success-color); + color: white; +} + +.flash-error { + background-color: var(--danger-color); + color: white; +} + +.flash-info { + background-color: var(--info-color); + color: white; +} + +.flash-warning { + background-color: var(--warning-color); + color: var(--text-dark); +} + +/* Page header */ +.page-header { + margin-bottom: 2rem; + text-align: center; +} + +.page-header h1 { + color: var(--accent-color); + font-size: 2.2rem; + margin-bottom: 0.2rem; +} + +.subtitle { + color: var(--text-light); + opacity: 0.7; + font-size: 1.1rem; +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 0.6rem 1.2rem; + border: none; + border-radius: 20px; + cursor: pointer; + font-weight: 600; + transition: all 0.2s; + font-size: 0.9rem; + text-decoration: none; + display: flex; + align-items: center; + gap: 0.5rem; + justify-content: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} + +.btn:active { + transform: translateY(0); +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--primary-light); +} + +.btn-secondary { + background-color: var(--border-color); + color: white; +} + +.btn-secondary:hover { + background-color: #444; +} + +.btn-danger { + background-color: var(--danger-color); + color: white; +} + +.btn-danger:hover { + background-color: #d32f2f; +} + +/* Game cards */ +.game-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.game-card { + background-color: var(--bg-card); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); + transition: all 0.3s; + position: relative; +} + +.game-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3); + background-color: var(--bg-card-hover); +} + +.game-cover { + height: 200px; + overflow: hidden; + position: relative; +} + +.game-cover img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s; +} + +.game-card:hover .game-cover img { + transform: scale(1.1); +} + +.no-cover { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 4rem; + background: linear-gradient(45deg, var(--primary-dark), var(--primary-light)); + color: rgba(255, 255, 255, 0.5); +} + +.game-info { + padding: 1.2rem; +} + +.game-info h2 { + font-size: 1.4rem; + margin-bottom: 0.5rem; + color: var(--text-light); +} + +.game-meta { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + margin-bottom: 1rem; + font-size: 0.9rem; + opacity: 0.8; +} + +.game-meta span { + display: flex; + align-items: center; + gap: 0.3rem; +} + +.game-meta i { + font-size: 0.8rem; + color: var(--accent-color); +} + +.progress-container { + margin: 1rem 0; +} + +.progress-label { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.3rem; +} + +.progress-value { + font-weight: bold; + font-size: 0.9rem; +} + +.progress-bar { + height: 8px; + background-color: var(--border-color); + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(to right, var(--primary-color), var(--accent-color)); + border-radius: 4px; + transition: width 0.3s ease; +} + +.status-badge { + display: inline-block; + padding: 0.2rem 0.6rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: bold; +} + +.status-playing { + background-color: var(--info-color); + color: white; +} + +.status-completed { + background-color: var(--success-color); + color: white; +} + +.status-on.hold { + background-color: var(--warning-color); + color: var(--text-dark); +} + +.game-details { + margin-top: 1rem; +} + +.game-details summary { + cursor: pointer; + color: var(--accent-color); + font-weight: 600; + padding: 0.5rem 0; +} + +.game-details summary:hover { + text-decoration: underline; +} + +.update-form { + padding: 1rem 0; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.3rem; + font-weight: 600; + font-size: 0.9rem; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 0.6rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.1); + color: var(--text-light); +} + +.form-group textarea { + resize: vertical; + min-height: 60px; +} + +.form-actions { + display: flex; + justify-content: space-between; + margin-top: 1rem; +} + +/* Range slider styling */ +.progress-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 8px; + background: var(--border-color); + border-radius: 4px; + outline: none; +} + +.progress-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--accent-color); + cursor: pointer; +} + +.progress-slider::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--accent-color); + cursor: pointer; + border: none; +} + +.range-value { + margin-left: 0.5rem; + font-weight: 600; + color: var(--accent-color); +} + +/* Empty state */ +.empty-state { + text-align: center; + padding: 3rem 1rem; +} + +.empty-icon { + font-size: 4rem; + color: var(--accent-color); + margin-bottom: 1rem; + opacity: 0.7; +} + +.empty-state h2 { + font-size: 1.8rem; + margin-bottom: 0.5rem; +} + +.empty-state p { + margin-bottom: 1.5rem; + opacity: 0.8; +} + +.add-more { + text-align: center; + margin: 2rem 0; +} + +/* Search page styles */ +.search-container { + margin-bottom: 2rem; +} + +.search-form { + max-width: 600px; + margin: 0 auto; +} + +.search-input { + display: flex; + gap: 0.5rem; +} + +.search-input input { + flex: 1; + padding: 0.8rem 1rem; + border: 2px solid var(--border-color); + border-radius: 25px; + background-color: rgba(255, 255, 255, 0.1); + color: var(--text-light); + font-size: 1rem; +} + +.search-input input:focus { + border-color: var(--primary-color); + outline: none; +} + +.results-info { + margin-bottom: 1.5rem; + text-align: center; +} + +.search-results { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.search-result-card { + background-color: var(--bg-card); + border-radius: 8px; + overflow: hidden; + display: flex; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); + transition: all 0.3s; +} + +.search-result-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3); + background-color: var(--bg-card-hover); +} + +.search-result-card .game-cover { + width: 120px; + min-width: 120px; + height: auto; +} + +.search-result-card .game-info { + flex: 1; + padding: 1rem; +} + +.search-result-card h3 { + font-size: 1.3rem; + margin-bottom: 0.5rem; +} + +.game-description { + margin: 0.8rem 0; + font-size: 0.95rem; + line-height: 1.5; + opacity: 0.85; +} + +.search-actions { + display: flex; + justify-content: flex-end; + margin-top: 1rem; +} + +.empty-search { + text-align: center; + padding: 3rem 1rem; +} + +.empty-search .empty-icon { + color: var(--border-color); +} + +/* Modal */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background-color: var(--bg-card); + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 500px; +} + +.modal h3 { + color: var(--danger-color); + margin-bottom: 1rem; +} + +.modal p { + margin-bottom: 1.5rem; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 1rem; +} + +/* Animations */ +@keyframes heartbeat { + 0% { transform: scale(1); } + 10% { transform: scale(1.2); } + 20% { transform: scale(1); } + 30% { transform: scale(1.2); } + 40% { transform: scale(1); } +} + +@keyframes flash-in { + from { + transform: translateX(50px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + header .container { + flex-direction: column; + gap: 1rem; + } + + .search-result-card { + flex-direction: column; + } + + .search-result-card .game-cover { + width: 100%; + height: 180px; + } + + .form-actions { + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..1225faf --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,65 @@ + + + + + + {% block title %}Game Twacker{% endblock %} + + + + + +
+
+

Game Twacker

+ +
+
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ +
+
+

Game data provided by IGDB

+

Made with UwU

+
+
+ + + + diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..42c512e --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,153 @@ +{% extends 'base.html' %} + +{% block title %}My Games ~ Game Twacker{% endblock %} + +{% block content %} + + +{% if games %} +
+ {% for game in games %} +
+
+ {% if game.cover_url %} + {{ game.name }} cover + {% else %} +
+ +
+ {% endif %} +
+
+

{{ game.name }}

+
+ {% if game.developer %} + {{ game.developer }} + {% endif %} + {% if game.platform %} + {{ game.platform }} + {% endif %} +
+
+
+ {{ game.status }} + {{ game.progress }}% +
+
+
+
+
+ +
+ Update Pwogwess +
+
+ + +
+ +
+ + + {{ game.progress }}% +
+ +
+ + +
+ +
+ + +
+
+
+
+ + + +
+ {% endfor %} +
+ +{% else %} +
+
+ +
+

No games being twacked yet~ >w<

+

Click the button bewow to seawch and add games to twack!

+ + Add Youw Fiwst Game + +
+{% endif %} + +{% if games|length < 5 %} + +{% endif %} + + +{% endblock %} diff --git a/app/templates/search.html b/app/templates/search.html new file mode 100644 index 0000000..e576c1f --- /dev/null +++ b/app/templates/search.html @@ -0,0 +1,92 @@ +{% extends 'base.html' %} + +{% block title %}Add New Game ~ Game Twacker{% endblock %} + +{% block content %} + + +
+
+
+ + +
+
+
+ +{% if results %} +
+

Seawch Wesuwts

+

Found {{ results|length }} games matching "{{ query }}"

+
+ +
+ {% for game in results %} +
+
+ {% if game.cover_url %} + {{ game.name }} cover + {% else %} +
+ +
+ {% endif %} +
+
+

{{ game.name }}

+ +
+ {% if game.release_date %} + {{ game.release_date }} + {% endif %} + + {% if game.platforms %} + + {% if game.platforms|length > 2 %} + {{ game.platforms[0] }} + {{ game.platforms|length - 1 }} more + {% else %} + {{ game.platforms|join(', ') }} + {% endif %} + + {% endif %} + + {% if game.developer %} + {{ game.developer }} + {% endif %} +
+ + {% if game.description %} +
+

{{ game.description|truncate(150) }}

+
+ {% endif %} + +
+
+ +
+
+
+
+ {% endfor %} +
+ +{% elif query %} + +{% endif %} + +{% endblock %} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a63169b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Flask==2.3.3 +Flask-SQLAlchemy==3.1.1 +Flask-WTF==1.2.1 +requests==2.31.0 +python-dotenv==1.0.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000..a3fdaf3 --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True)