admin
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@@ -14,7 +15,7 @@ def create_app():
|
|||||||
|
|
||||||
# Configure the app
|
# Configure the app
|
||||||
app.config.from_mapping(
|
app.config.from_mapping(
|
||||||
SECRET_KEY=os.environ.get('SECRET_KEY', 'dev'),
|
SECRET_KEY=os.environ.get('SECRET_KEY', secrets.token_hex(16)),
|
||||||
SQLALCHEMY_DATABASE_URI=f"sqlite:///{os.path.join(app.instance_path, 'game_tracker.sqlite')}",
|
SQLALCHEMY_DATABASE_URI=f"sqlite:///{os.path.join(app.instance_path, 'game_tracker.sqlite')}",
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
class IGDBHelper:
|
class IGDBHelper:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -135,9 +136,9 @@ class IGDBHelper:
|
|||||||
# Format release date
|
# Format release date
|
||||||
release_date = None
|
release_date = None
|
||||||
if 'first_release_date' in game:
|
if 'first_release_date' in game:
|
||||||
release_date = game['first_release_date']
|
timestamp = game['first_release_date']
|
||||||
# Convert Unix timestamp to readable format
|
# Convert Unix timestamp to datetime object
|
||||||
release_date = time.strftime('%Y-%m-%d', time.gmtime(release_date))
|
release_date = datetime.datetime.fromtimestamp(timestamp)
|
||||||
|
|
||||||
processed_game = {
|
processed_game = {
|
||||||
'id': game['id'],
|
'id': game['id'],
|
||||||
|
|||||||
@@ -1,5 +1,43 @@
|
|||||||
from . import db
|
from . import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
class AdminConfig(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
password_hash = db.Column(db.String(64), nullable=False)
|
||||||
|
last_updated = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_password(cls, password):
|
||||||
|
"""Check if the provided password matches the stored hash"""
|
||||||
|
admin = cls.query.first()
|
||||||
|
if not admin:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create hash from input password
|
||||||
|
hash_input = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
# Compare with stored hash
|
||||||
|
return hash_input == admin.password_hash
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_password(cls, password):
|
||||||
|
"""Set a new admin password"""
|
||||||
|
admin = cls.query.first()
|
||||||
|
|
||||||
|
# Create hash from the password
|
||||||
|
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
if admin:
|
||||||
|
admin.password_hash = password_hash
|
||||||
|
admin.last_updated = datetime.utcnow()
|
||||||
|
else:
|
||||||
|
admin = cls(password_hash=password_hash)
|
||||||
|
db.session.add(admin)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Game(db.Model):
|
class Game(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|||||||
157
app/routes.py
157
app/routes.py
@@ -1,12 +1,23 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app, session
|
||||||
from .models import Game
|
from .models import Game, AdminConfig
|
||||||
from . import db
|
from . import db
|
||||||
from .igdb_helpers import IGDBHelper
|
from .igdb_helpers import IGDBHelper
|
||||||
import os
|
import os
|
||||||
|
import functools
|
||||||
|
|
||||||
main_bp = Blueprint('main', __name__)
|
main_bp = Blueprint('main', __name__)
|
||||||
igdb = IGDBHelper()
|
igdb = IGDBHelper()
|
||||||
|
|
||||||
|
# Admin authentication decorator
|
||||||
|
def admin_required(view):
|
||||||
|
@functools.wraps(view)
|
||||||
|
def wrapped_view(**kwargs):
|
||||||
|
if not session.get('admin_authenticated'):
|
||||||
|
flash('Please log in to access the admin area', 'error')
|
||||||
|
return redirect(url_for('main.admin_login'))
|
||||||
|
return view(**kwargs)
|
||||||
|
return wrapped_view
|
||||||
|
|
||||||
@main_bp.route('/')
|
@main_bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""Home page showing currently tracked games"""
|
"""Home page showing currently tracked games"""
|
||||||
@@ -110,5 +121,145 @@ def remove_game(game_id):
|
|||||||
db.session.delete(game)
|
db.session.delete(game)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash(f"Removed {name} from youw twacking wist! OwO", 'success')
|
flash(f"Removed {name} from tracking list!", 'success')
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
# Admin Routes
|
||||||
|
@main_bp.route('/admin/login', methods=['GET', 'POST'])
|
||||||
|
def admin_login():
|
||||||
|
"""Admin login page"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
password = request.form.get('password')
|
||||||
|
|
||||||
|
# Check if this is initial setup
|
||||||
|
admin_exists = AdminConfig.query.first()
|
||||||
|
|
||||||
|
if not admin_exists:
|
||||||
|
# Initial setup - set password
|
||||||
|
if password and len(password) >= 6:
|
||||||
|
AdminConfig.set_password(password)
|
||||||
|
session['admin_authenticated'] = True
|
||||||
|
flash('Admin account created successfully!', 'success')
|
||||||
|
return redirect(url_for('main.admin_dashboard'))
|
||||||
|
else:
|
||||||
|
flash('Password must be at least 6 characters long', 'error')
|
||||||
|
else:
|
||||||
|
# Regular login
|
||||||
|
if AdminConfig.check_password(password):
|
||||||
|
session['admin_authenticated'] = True
|
||||||
|
flash('Logged in successfully!', 'success')
|
||||||
|
return redirect(url_for('main.admin_dashboard'))
|
||||||
|
else:
|
||||||
|
flash('Invalid password', 'error')
|
||||||
|
|
||||||
|
# Check if admin exists to show proper message
|
||||||
|
admin_exists = AdminConfig.query.first()
|
||||||
|
return render_template('admin/login.html', admin_exists=admin_exists)
|
||||||
|
|
||||||
|
@main_bp.route('/admin/dashboard')
|
||||||
|
@admin_required
|
||||||
|
def admin_dashboard():
|
||||||
|
"""Admin dashboard"""
|
||||||
|
games = Game.query.all()
|
||||||
|
return render_template('admin/dashboard.html', games=games)
|
||||||
|
|
||||||
|
@main_bp.route('/admin/search', methods=['GET', 'POST'])
|
||||||
|
@admin_required
|
||||||
|
def admin_search():
|
||||||
|
"""Admin game search"""
|
||||||
|
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"Couldn't search IGDB: {str(e)}", 'error')
|
||||||
|
|
||||||
|
return render_template('admin/search.html', results=results, query=query)
|
||||||
|
|
||||||
|
@main_bp.route('/admin/add_game/<int:igdb_id>', methods=['GET', 'POST'])
|
||||||
|
@admin_required
|
||||||
|
def admin_add_game(igdb_id):
|
||||||
|
"""Admin add game"""
|
||||||
|
existing_game = Game.query.filter_by(igdb_id=igdb_id).first()
|
||||||
|
|
||||||
|
if existing_game:
|
||||||
|
flash(f"Game {existing_game.name} is already being tracked", 'info')
|
||||||
|
return redirect(url_for('main.admin_dashboard'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
game_data = igdb.get_game_by_id(igdb_id)
|
||||||
|
|
||||||
|
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 tracking list", 'success')
|
||||||
|
return redirect(url_for('main.admin_dashboard'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Failed to add game: {str(e)}", 'error')
|
||||||
|
return redirect(url_for('main.admin_search'))
|
||||||
|
|
||||||
|
@main_bp.route('/admin/update_game/<int:game_id>', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def admin_update_game(game_id):
|
||||||
|
"""Admin update game"""
|
||||||
|
game = Game.query.get_or_404(game_id)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
if 'status' in request.form:
|
||||||
|
game.status = request.form['status']
|
||||||
|
|
||||||
|
if 'progress' in request.form:
|
||||||
|
try:
|
||||||
|
progress = int(request.form['progress'])
|
||||||
|
if 0 <= progress <= 100:
|
||||||
|
game.progress = progress
|
||||||
|
except ValueError:
|
||||||
|
flash("Progress must be a number between 0 and 100", 'error')
|
||||||
|
|
||||||
|
if 'notes' in request.form:
|
||||||
|
game.notes = request.form['notes']
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Updated {game.name}", 'success')
|
||||||
|
|
||||||
|
return redirect(url_for('main.admin_dashboard'))
|
||||||
|
|
||||||
|
@main_bp.route('/admin/remove_game/<int:game_id>', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def admin_remove_game(game_id):
|
||||||
|
"""Admin remove game"""
|
||||||
|
game = Game.query.get_or_404(game_id)
|
||||||
|
name = game.name
|
||||||
|
|
||||||
|
db.session.delete(game)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f"Removed {name} from tracking list", 'success')
|
||||||
|
return redirect(url_for('main.admin_dashboard'))
|
||||||
|
|
||||||
|
@main_bp.route('/admin/logout')
|
||||||
|
@admin_required
|
||||||
|
def admin_logout():
|
||||||
|
"""Admin logout"""
|
||||||
|
session.pop('admin_authenticated', None)
|
||||||
|
flash('Logged out successfully', 'success')
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ footer .heart {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-on.hold {
|
.status-on-hold {
|
||||||
background-color: var(--warning-color);
|
background-color: var(--warning-color);
|
||||||
color: var(--text-dark);
|
color: var(--text-dark);
|
||||||
}
|
}
|
||||||
@@ -626,6 +626,191 @@ footer .heart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Admin Styles */
|
||||||
|
.admin-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-title h1 {
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-games h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table th,
|
||||||
|
.admin-table td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table th {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-light);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table tbody tr:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cover-cell {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-cover-cell img {
|
||||||
|
width: 60px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-cover-small {
|
||||||
|
width: 60px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
background: linear-gradient(45deg, var(--primary-dark), var(--primary-light));
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-details-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-small {
|
||||||
|
height: 20px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-row {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-edit-form {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-wide {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login styles */
|
||||||
|
.login-container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
color: var(--text-light);
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
/* Responsive Adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
header .container {
|
header .container {
|
||||||
@@ -646,4 +831,18 @@ footer .heart {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
224
app/templates/admin/dashboard.html
Normal file
224
app/templates/admin/dashboard.html
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Admin Dashboard - Game Tracker{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-header">
|
||||||
|
<div class="admin-title">
|
||||||
|
<h1>Admin Dashboard</h1>
|
||||||
|
<p class="subtitle">Manage tracked games</p>
|
||||||
|
</div>
|
||||||
|
<div class="admin-actions">
|
||||||
|
<a href="{{ url_for('main.admin_search') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-search"></i> Search New Games
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('main.admin_logout') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if games %}
|
||||||
|
<div class="admin-games">
|
||||||
|
<h2>Currently Tracked Games</h2>
|
||||||
|
<table class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Cover</th>
|
||||||
|
<th>Game</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Progress</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for game in games %}
|
||||||
|
<tr>
|
||||||
|
<td class="game-cover-cell">
|
||||||
|
{% if game.cover_url %}
|
||||||
|
<img src="{{ game.cover_url }}" alt="{{ game.name }} cover">
|
||||||
|
{% else %}
|
||||||
|
<div class="no-cover-small">
|
||||||
|
<i class="fas fa-gamepad"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{ game.name }}</strong>
|
||||||
|
<div class="game-details-row">
|
||||||
|
{% if game.developer %}
|
||||||
|
<small><i class="fas fa-code"></i> {{ game.developer }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% if game.platform %}
|
||||||
|
<small><i class="fas fa-desktop"></i> {{ game.platform }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-{{ game.status|lower|replace(' ', '-') }}">{{ game.status }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="progress-bar-small">
|
||||||
|
<div class="progress-fill progress-{{ game.progress }}"></div>
|
||||||
|
<span class="progress-text">{{ game.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-small btn-secondary edit-button" data-id="{{ game.id }}">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-small btn-danger delete-button" data-id="{{ game.id }}"
|
||||||
|
data-name="{{ game.name }}">
|
||||||
|
<i class="fas fa-trash"></i> Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Edit form row -->
|
||||||
|
<tr class="edit-row" id="edit-row-{{ game.id }}">
|
||||||
|
<td colspan="5">
|
||||||
|
<form action="{{ url_for('main.admin_update_game', game_id=game.id) }}" method="POST" class="admin-edit-form">
|
||||||
|
<div class="form-group-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="status-{{ game.id }}">Status:</label>
|
||||||
|
<select name="status" id="status-{{ game.id }}">
|
||||||
|
<option value="Playing" {% if game.status == 'Playing' %}selected{% endif %}>Playing</option>
|
||||||
|
<option value="Completed" {% if game.status == 'Completed' %}selected{% endif %}>Completed</option>
|
||||||
|
<option value="On Hold" {% if game.status == 'On Hold' %}selected{% endif %}>On Hold</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="progress-{{ game.id }}">Progress:</label>
|
||||||
|
<div class="progress-input">
|
||||||
|
<input type="range" name="progress" id="progress-{{ game.id }}" min="0" max="100" value="{{ game.progress }}" class="progress-slider">
|
||||||
|
<span class="range-value">{{ game.progress }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notes-{{ game.id }}">Notes:</label>
|
||||||
|
<textarea name="notes" id="notes-{{ game.id }}" rows="2">{{ game.notes or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Update</button>
|
||||||
|
<button type="button" class="btn btn-secondary cancel-edit">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-gamepad"></i>
|
||||||
|
</div>
|
||||||
|
<h2>No games being tracked yet</h2>
|
||||||
|
<p>Use the button below to search and add games to track!</p>
|
||||||
|
<a href="{{ url_for('main.admin_search') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Add Your First Game
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Delete confirmation modal -->
|
||||||
|
<div class="modal" id="delete-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Remove Game</h3>
|
||||||
|
<p>Are you sure you want to remove <strong id="delete-game-name"></strong> from the tracking list?</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary close-modal">Cancel</button>
|
||||||
|
<form id="delete-form" method="POST">
|
||||||
|
<button type="submit" class="btn btn-danger">Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Range slider handling
|
||||||
|
const sliders = document.querySelectorAll('.progress-slider');
|
||||||
|
sliders.forEach(slider => {
|
||||||
|
const valueDisplay = slider.nextElementSibling;
|
||||||
|
|
||||||
|
slider.addEventListener('input', function() {
|
||||||
|
valueDisplay.textContent = this.value + '%';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit row handling
|
||||||
|
const editButtons = document.querySelectorAll('.edit-button');
|
||||||
|
const editRows = document.querySelectorAll('.edit-row');
|
||||||
|
const cancelButtons = document.querySelectorAll('.cancel-edit');
|
||||||
|
|
||||||
|
// Hide all edit rows initially
|
||||||
|
editRows.forEach(row => {
|
||||||
|
row.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
editButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const gameId = this.getAttribute('data-id');
|
||||||
|
const editRow = document.getElementById(`edit-row-${gameId}`);
|
||||||
|
|
||||||
|
// Hide any other open edit rows
|
||||||
|
editRows.forEach(row => {
|
||||||
|
if (row.id !== `edit-row-${gameId}`) {
|
||||||
|
row.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle the clicked row
|
||||||
|
if (editRow.style.display === 'none' || !editRow.style.display) {
|
||||||
|
editRow.style.display = 'table-row';
|
||||||
|
} else {
|
||||||
|
editRow.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const editRow = this.closest('.edit-row');
|
||||||
|
editRow.style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete modal handling
|
||||||
|
const deleteModal = document.getElementById('delete-modal');
|
||||||
|
const deleteButtons = document.querySelectorAll('.delete-button');
|
||||||
|
const closeModalBtn = document.querySelector('.close-modal');
|
||||||
|
const deleteForm = document.getElementById('delete-form');
|
||||||
|
const deleteGameName = document.getElementById('delete-game-name');
|
||||||
|
|
||||||
|
deleteButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const gameId = this.getAttribute('data-id');
|
||||||
|
const gameName = this.getAttribute('data-name');
|
||||||
|
|
||||||
|
deleteForm.action = `/admin/remove_game/${gameId}`;
|
||||||
|
deleteGameName.textContent = gameName;
|
||||||
|
deleteModal.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
closeModalBtn.addEventListener('click', function() {
|
||||||
|
deleteModal.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('click', function(e) {
|
||||||
|
if (e.target === deleteModal) {
|
||||||
|
deleteModal.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
35
app/templates/admin/login.html
Normal file
35
app/templates/admin/login.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Admin Login - Game Tracker{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Admin Login</h1>
|
||||||
|
{% if admin_exists %}
|
||||||
|
<p class="subtitle">Enter your password to access the admin area</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="subtitle">First time setup - Create an admin password</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<form class="login-form" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">
|
||||||
|
{% if admin_exists %}Password:{% else %}Create Password:{% endif %}
|
||||||
|
</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-wide">
|
||||||
|
{% if admin_exists %}Login{% else %}Create Admin Account{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
<a href="{{ url_for('main.index') }}" class="back-link">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Game Tracker
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
99
app/templates/admin/search.html
Normal file
99
app/templates/admin/search.html
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Admin Search - Game Tracker{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-header">
|
||||||
|
<div class="admin-title">
|
||||||
|
<h1>Admin Game Search</h1>
|
||||||
|
<p class="subtitle">Find games to add to the tracking list</p>
|
||||||
|
</div>
|
||||||
|
<div class="admin-actions">
|
||||||
|
<a href="{{ url_for('main.admin_dashboard') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-container">
|
||||||
|
<form action="{{ url_for('main.admin_search') }}" method="POST" class="search-form">
|
||||||
|
<div class="search-input">
|
||||||
|
<input type="text" name="query" placeholder="Enter game name..." value="{{ query }}" required>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-search"></i> Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if results %}
|
||||||
|
<div class="results-info">
|
||||||
|
<h2>Search Results</h2>
|
||||||
|
<p>Found {{ results|length }} games matching "{{ query }}"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-results">
|
||||||
|
{% for game in results %}
|
||||||
|
<div class="search-result-card">
|
||||||
|
<div class="game-cover">
|
||||||
|
{% if game.cover_url %}
|
||||||
|
<img src="{{ game.cover_url }}" alt="{{ game.name }} cover">
|
||||||
|
{% else %}
|
||||||
|
<div class="no-cover">
|
||||||
|
<i class="fas fa-gamepad"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="game-info">
|
||||||
|
<h3>{{ game.name }}</h3>
|
||||||
|
|
||||||
|
<div class="game-meta">
|
||||||
|
{% if game.release_date %}
|
||||||
|
<span><i class="fas fa-calendar-alt"></i> {{ game.release_date }}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if game.platforms %}
|
||||||
|
<span><i class="fas fa-desktop"></i>
|
||||||
|
{% if game.platforms|length > 2 %}
|
||||||
|
{{ game.platforms[0] }} + {{ game.platforms|length - 1 }} more
|
||||||
|
{% else %}
|
||||||
|
{{ game.platforms|join(', ') }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if game.developer %}
|
||||||
|
<span><i class="fas fa-code"></i> {{ game.developer }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if game.description %}
|
||||||
|
<div class="game-description">
|
||||||
|
<p>{{ game.description|truncate(150) }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="search-actions">
|
||||||
|
<form action="{{ url_for('main.admin_add_game', igdb_id=game.id) }}" method="POST">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Add to Tracking List
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% elif query %}
|
||||||
|
<div class="empty-search">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</div>
|
||||||
|
<h2>No games found</h2>
|
||||||
|
<p>We couldn't find any games matching "{{ query }}"</p>
|
||||||
|
<p>Try a different search term!</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user