refactor nfg
modified: app.py new file: templates/admin_dashboard.html.j2 new file: templates/base.html.j2 new file: templates/daily_leaderboard.html.j2 new file: templates/daily_puzzle.html.j2 new file: templates/home.html.j2 new file: templates/invite_admin.html.j2 new file: templates/king_of_the_mountain.html.j2 new file: templates/leaderboard_index.html.j2 new file: templates/login.html.j2 new file: templates/manage_admins.html.j2 new file: templates/profile.html.j2 new file: templates/puzzle_menu.html.j2 new file: templates/upload.html.j2 new file: templates/verify.html.j2
This commit is contained in:
parent
cf84aa01b3
commit
e13b5ef5fa
15 changed files with 1101 additions and 903 deletions
34
templates/admin_dashboard.html.j2
Normal file
34
templates/admin_dashboard.html.j2
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>Admin Dashboard</h1>
|
||||
<p>Welcome, {{ request.user.username }}! Manage puzzles and admins here.</p>
|
||||
<h2>Puzzle Management</h2>
|
||||
<p><a href="{{ request.route_url('upload') }}">Upload/Schedule Puzzles</a></p>
|
||||
<h2>Admin Management</h2>
|
||||
<p><a href="{{ request.route_url('invite_admin') }}">Invite Admin</a></p>
|
||||
<p><a href="{{ request.route_url('manage_admins') }}">Manage Admins</a></p>
|
||||
<h2>Scheduled Puzzles</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Size</th>
|
||||
<th>Title</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{% for p in puzzles %}
|
||||
<tr>
|
||||
<td>{{ p.date }}</td>
|
||||
<td>{{ p.size }}x{{ p.size }}</td>
|
||||
<td>{{ p.title }}</td>
|
||||
<td>
|
||||
<a href="{{ request.route_url('edit_puzzle', puzzle_id=p.id) }}">Edit</a>
|
||||
{% if p.is_visible %}
|
||||
<a href="{{ request.route_url('toggle_visibility', puzzle_id=p.id) }}">Hide</a>
|
||||
{% else %}
|
||||
<a href="{{ request.route_url('toggle_visibility', puzzle_id=p.id) }}">Show</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
49
templates/base.html.j2
Normal file
49
templates/base.html.j2
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}My Puzzle Game{% endblock %}</title>
|
||||
<link href="https://unpkg.com/@picocss/pico@1.*/css/pico.min.css" rel="stylesheet">
|
||||
<style>
|
||||
/* Custom styles */
|
||||
body { margin: 0; padding: 0; }
|
||||
nav ul { list-style-type: none; padding: 0; display: flex; flex-wrap: wrap; }
|
||||
nav ul li { margin-right: 10px; }
|
||||
nav ul li a { text-decoration: none; }
|
||||
@media (max-width: 600px) {
|
||||
nav ul { flex-direction: column; }
|
||||
nav ul li { margin-bottom: 10px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<ul>
|
||||
{% if request.user and request.user.is_admin %}
|
||||
<li><a href="{{ request.route_url('admin_dashboard') }}">Admin Dashboard</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{{ request.route_url('home') }}">Home</a></li>
|
||||
<li><a href="{{ request.route_url('puzzle_menu') }}">Today's Puzzles</a></li>
|
||||
<li><a href="{{ request.route_url('leaderboard_index') }}">Leaderboards</a></li>
|
||||
<li><a href="{{ request.route_url('king_of_the_mountain') }}">King of the Mountain</a></li>
|
||||
{% if request.user %}
|
||||
<li><a href="{{ request.route_url('profile') }}">Profile</a></li>
|
||||
<li><a href="{{ request.route_url('logout') }}">Logout</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ request.route_url('login') }}">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{% if request.session.peek_flash() %}
|
||||
{% for message in request.session.pop_flash() %}
|
||||
<div class="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<main class="container">
|
||||
{% block content %}
|
||||
<!-- Default content can go here, or leave blank -->
|
||||
{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
30
templates/daily_leaderboard.html.j2
Normal file
30
templates/daily_leaderboard.html.j2
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>Leaderboard for {{ puzzle.title }} ({{ puzzle.date }})</h1>
|
||||
<p>
|
||||
<strong>Sort by:</strong>
|
||||
<a href="{{ request.route_url('daily_leaderboard', puzzle_id=puzzle.id, sort='time') }}">Top Speed</a> |
|
||||
<a href="{{ request.route_url('daily_leaderboard', puzzle_id=puzzle.id, sort='moves') }}">Least Moves</a>
|
||||
</p>
|
||||
<p>Players are ranked based on their total time and moves. Faster times and fewer moves result in better rankings.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Username/Guest</th>
|
||||
<th>Moves</th>
|
||||
<th>Rotations</th>
|
||||
<th>Total Actions</th>
|
||||
<th>Time (sec)</th>
|
||||
</tr>
|
||||
{% for row in attempts %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td><a href="{{ request.route_url('view_profile', user_id=row.user_id) }}">{{ row.user_display_name }}</a></td>
|
||||
<td>{{ row.move_count }}</td>
|
||||
<td>{{ row.rotation_count }}</td>
|
||||
<td>{{ row.move_count + row.rotation_count }}</td>
|
||||
<td>{{ "%.2f"|format((row.time_completed - row.time_started).total_seconds() if row.time_completed else 0) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
344
templates/daily_puzzle.html.j2
Normal file
344
templates/daily_puzzle.html.j2
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block content %}
|
||||
{% if puzzle %}
|
||||
<h1>{{ puzzle.title }}</h1>
|
||||
<p>Date: {{ puzzle.date }}</p>
|
||||
<p>
|
||||
Moves: <span id="moveCount">{{ move_count }}</span><br>
|
||||
Rotations: <span id="rotationCount">{{ rotation_count }}</span><br>
|
||||
</p>
|
||||
|
||||
<div id="puzzle-container"></div>
|
||||
|
||||
<button id="downloadButton" style="display:none;">Download Image</button>
|
||||
|
||||
<!-- Win Modal -->
|
||||
<div id="winModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span id="winModalClose" class="close">×</span>
|
||||
<h2>Congratulations!</h2>
|
||||
<p>You solved the puzzle!</p>
|
||||
{% if user_rank is not none %}
|
||||
<p>Your Rank: {{ user_rank }}</p>
|
||||
{% else %}
|
||||
<p>Your rank is not available.</p>
|
||||
{% endif %}
|
||||
<button id="modalDownloadButton">Download Image</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Styles -->
|
||||
<style>
|
||||
#puzzle-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 100%; /* Maintain square aspect ratio */
|
||||
border: 1px solid #ccc;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.puzzle-piece {
|
||||
position: absolute;
|
||||
background-image: url('{{ puzzle_image_url }}');
|
||||
background-size: {{ puzzle.size * 100 }}%;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.puzzle-piece.selected {
|
||||
outline: 2px solid red;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.5); /* Black with opacity */
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.confetti {
|
||||
position: fixed;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: 0;
|
||||
animation: confetti-fall 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes confetti-fall {
|
||||
0% {transform: translateY(0);}
|
||||
100% {transform: translateY(100vh);}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
let pieces = {{ attempt_state_json | safe }};
|
||||
const gridSize = {{ puzzle.size }};
|
||||
let isSolved = {{ 'true' if attempt_is_solved else 'false' }};
|
||||
let moveCount = {{ move_count }};
|
||||
let rotationCount = {{ rotation_count }};
|
||||
|
||||
const puzzleContainer = document.getElementById("puzzle-container");
|
||||
const moveCountDisplay = document.getElementById("moveCount");
|
||||
const rotationCountDisplay = document.getElementById("rotationCount");
|
||||
const downloadButton = document.getElementById("downloadButton");
|
||||
const winModal = document.getElementById("winModal");
|
||||
const winModalClose = document.getElementById("winModalClose");
|
||||
const modalDownloadButton = document.getElementById("modalDownloadButton");
|
||||
|
||||
// Map of UUID to piece data for quick lookup
|
||||
let pieceMap = {};
|
||||
pieces.forEach(piece => {
|
||||
pieceMap[piece.uuid] = piece;
|
||||
});
|
||||
|
||||
// Initialize the game board
|
||||
function initBoard() {
|
||||
puzzleContainer.innerHTML = '';
|
||||
|
||||
// Build the board
|
||||
pieces.forEach(piece => {
|
||||
const pieceElement = document.createElement("div");
|
||||
pieceElement.classList.add("puzzle-piece");
|
||||
pieceElement.dataset.pieceUuid = piece.uuid;
|
||||
pieceElement.dataset.currentIndex = piece.currentIndex;
|
||||
pieceElement.dataset.rotation = piece.rotation;
|
||||
|
||||
// Set size and position
|
||||
const pieceSizePercent = 100 / gridSize;
|
||||
pieceElement.style.width = `${pieceSizePercent}%`;
|
||||
pieceElement.style.height = `${pieceSizePercent}%`;
|
||||
|
||||
const currentRow = Math.floor(piece.currentIndex / gridSize);
|
||||
const currentCol = piece.currentIndex % gridSize;
|
||||
pieceElement.style.left = `${currentCol * pieceSizePercent}%`;
|
||||
pieceElement.style.top = `${currentRow * pieceSizePercent}%`;
|
||||
|
||||
// Set background position
|
||||
const correctRow = Math.floor(piece.correctIndex / gridSize);
|
||||
const correctCol = piece.correctIndex % gridSize;
|
||||
pieceElement.style.backgroundPosition = `${-correctCol * pieceSizePercent}% ${-correctRow * pieceSizePercent}%`;
|
||||
|
||||
// Set rotation
|
||||
pieceElement.style.transform = `rotate(${piece.rotation}deg)`;
|
||||
|
||||
if (!isSolved) {
|
||||
// Click to rotate
|
||||
pieceElement.addEventListener("click", function(event) {
|
||||
rotatePiece(piece.uuid);
|
||||
});
|
||||
|
||||
// Drag events for swapping
|
||||
pieceElement.draggable = true;
|
||||
pieceElement.addEventListener("dragstart", dragStart);
|
||||
pieceElement.addEventListener("dragover", dragOver);
|
||||
pieceElement.addEventListener("drop", drop);
|
||||
}
|
||||
|
||||
puzzleContainer.appendChild(pieceElement);
|
||||
});
|
||||
}
|
||||
|
||||
let draggedPieceUuid = null;
|
||||
|
||||
function dragStart(event) {
|
||||
if (isSolved) return;
|
||||
draggedPieceUuid = event.target.dataset.pieceUuid;
|
||||
event.dataTransfer.setData("text/plain", "");
|
||||
}
|
||||
|
||||
function dragOver(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function drop(event) {
|
||||
event.preventDefault();
|
||||
if (isSolved || !draggedPieceUuid) return;
|
||||
const dropTarget = event.target;
|
||||
const targetPieceUuid = dropTarget.dataset.pieceUuid;
|
||||
if (targetPieceUuid && targetPieceUuid !== draggedPieceUuid) {
|
||||
// Swap pieces
|
||||
swapPieces(draggedPieceUuid, targetPieceUuid);
|
||||
moveCount++;
|
||||
updateScores();
|
||||
recordAction('swap', {
|
||||
pieceUUIDA: draggedPieceUuid,
|
||||
pieceUUIDB: targetPieceUuid
|
||||
});
|
||||
}
|
||||
draggedPieceUuid = null;
|
||||
}
|
||||
|
||||
function rotatePiece(pieceUuid) {
|
||||
if (isSolved) return;
|
||||
let piece = pieceMap[pieceUuid];
|
||||
piece.rotation = (piece.rotation + 90) % 360;
|
||||
updatePieceRotationInDOM(piece);
|
||||
rotationCount++;
|
||||
updateScores();
|
||||
recordAction('rotate', { pieceUUID: pieceUuid, rotation: piece.rotation });
|
||||
}
|
||||
|
||||
function updatePieceRotationInDOM(piece) {
|
||||
const pieceElement = document.querySelector(`[data-piece-uuid="${piece.uuid}"]`);
|
||||
if (pieceElement) {
|
||||
pieceElement.style.transform = `rotate(${piece.rotation}deg)`;
|
||||
}
|
||||
}
|
||||
|
||||
function swapPieces(pieceUuidA, pieceUuidB) {
|
||||
const pieceA = pieceMap[pieceUuidA];
|
||||
const pieceB = pieceMap[pieceUuidB];
|
||||
|
||||
// Swap currentIndex
|
||||
const tempIndex = pieceA.currentIndex;
|
||||
pieceA.currentIndex = pieceB.currentIndex;
|
||||
pieceB.currentIndex = tempIndex;
|
||||
|
||||
// Update positions in DOM
|
||||
updatePiecePositionInDOM(pieceA);
|
||||
updatePiecePositionInDOM(pieceB);
|
||||
}
|
||||
|
||||
function updatePiecePositionInDOM(piece) {
|
||||
const pieceElement = document.querySelector(`[data-piece-uuid="${piece.uuid}"]`);
|
||||
if (pieceElement) {
|
||||
const pieceSizePercent = 100 / gridSize;
|
||||
const currentRow = Math.floor(piece.currentIndex / gridSize);
|
||||
const currentCol = piece.currentIndex % gridSize;
|
||||
pieceElement.style.left = `${currentCol * pieceSizePercent}%`;
|
||||
pieceElement.style.top = `${currentRow * pieceSizePercent}%`;
|
||||
pieceElement.dataset.currentIndex = piece.currentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
function updateScores() {
|
||||
moveCountDisplay.textContent = moveCount;
|
||||
rotationCountDisplay.textContent = rotationCount;
|
||||
}
|
||||
|
||||
function recordAction(action, data) {
|
||||
fetch("{{ request.route_url('record_action', size=puzzle.size) }}", {
|
||||
method: "POST",
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: action, data: data })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(response => {
|
||||
if (response.status === 'win') {
|
||||
// Handle win condition
|
||||
showWinModal(response.rank);
|
||||
lockBoard();
|
||||
} else if (response.status !== 'ok') {
|
||||
console.error('Error recording action:', response.message);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
function lockBoard() {
|
||||
isSolved = true;
|
||||
const pieces = document.querySelectorAll(".puzzle-piece");
|
||||
pieces.forEach(piece => {
|
||||
piece.draggable = false;
|
||||
piece.removeEventListener("click", rotatePiece);
|
||||
// Optionally, reset rotation and position
|
||||
piece.style.transform = 'rotate(0deg)';
|
||||
});
|
||||
downloadButton.style.display = "block";
|
||||
}
|
||||
|
||||
function showWinModal(rank) {
|
||||
if (rank !== null) {
|
||||
const rankDisplay = document.createElement('p');
|
||||
rankDisplay.textContent = 'Your Rank: ' + rank;
|
||||
const modalContent = document.querySelector('.modal-content');
|
||||
modalContent.insertBefore(rankDisplay, modalDownloadButton);
|
||||
}
|
||||
|
||||
winModal.style.display = 'block';
|
||||
|
||||
winModalClose.onclick = function() {
|
||||
winModal.style.display = 'none';
|
||||
};
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target === winModal) {
|
||||
winModal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
// Confetti effect
|
||||
for (let i = 0; i < 100; i++) {
|
||||
createConfetti();
|
||||
}
|
||||
}
|
||||
|
||||
function createConfetti() {
|
||||
const confetti = document.createElement('div');
|
||||
confetti.className = 'confetti';
|
||||
confetti.style.left = Math.random() * 100 + '%';
|
||||
confetti.style.backgroundColor = 'hsl(' + Math.random() * 360 + ', 100%, 50%)';
|
||||
confetti.style.animationDelay = Math.random() * 3 + 's';
|
||||
document.body.appendChild(confetti);
|
||||
setTimeout(() => { confetti.remove(); }, 3000);
|
||||
}
|
||||
|
||||
// Download button functionality
|
||||
function downloadImage() {
|
||||
const link = document.createElement('a');
|
||||
link.href = "{{ puzzle_image_url }}";
|
||||
link.download = "{{ puzzle_filename }}";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
downloadButton.addEventListener('click', downloadImage);
|
||||
modalDownloadButton.addEventListener('click', downloadImage);
|
||||
|
||||
initBoard();
|
||||
updateScores();
|
||||
|
||||
if (isSolved) {
|
||||
lockBoard();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<h1>No puzzle available!</h1>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
12
templates/home.html.j2
Normal file
12
templates/home.html.j2
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>Daily Puzzles - Home</h1>
|
||||
{% if request.user %}
|
||||
<p>You are logged in as <strong>{{ request.user.username }}</strong>.</p>
|
||||
{% else %}
|
||||
<p>You are not logged in.</p>
|
||||
<p>Your guest name: <strong>{{ guest_name }}</strong></p>
|
||||
{% endif %}
|
||||
<p>Welcome to the Daily Puzzles! Test your skills with our daily challenges.</p>
|
||||
<p><a href="{{ request.route_url('puzzle_menu') }}">Play Today's Puzzles</a></p>
|
||||
{% endblock %}
|
||||
18
templates/invite_admin.html.j2
Normal file
18
templates/invite_admin.html.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Invite Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Invite a New Admin</h1>
|
||||
|
||||
<form method="POST">
|
||||
<label for="email">User Email:</label>
|
||||
<input type="email" name="email" required>
|
||||
<button type="submit">Send Invitation</button>
|
||||
</form>
|
||||
|
||||
{% if message %}
|
||||
<p>{{ message }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
26
templates/king_of_the_mountain.html.j2
Normal file
26
templates/king_of_the_mountain.html.j2
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>King of the Mountain - Global Leaderboard</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Username</th>
|
||||
<th>Total Puzzles Solved</th>
|
||||
<th>Total Time (sec)</th>
|
||||
<th>Total Moves</th>
|
||||
<th>Total Rotations</th>
|
||||
<th>Total Actions</th>
|
||||
</tr>
|
||||
{% for player in global_leaderboard %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td><a href="{{ request.route_url('view_profile', user_id=player.user_id) }}">{{ player.username }}</a></td>
|
||||
<td>{{ player.total_puzzles }}</td>
|
||||
<td>{{ "%.2f"|format(player.total_time) }}</td>
|
||||
<td>{{ player.total_moves }}</td>
|
||||
<td>{{ player.total_rotations }}</td>
|
||||
<td>{{ player.total_moves + player.total_rotations }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
21
templates/leaderboard_index.html.j2
Normal file
21
templates/leaderboard_index.html.j2
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>All Leaderboards</h1>
|
||||
<p>Click a puzzle date to see its full scoreboard.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Puzzle Date</th>
|
||||
<th>Size</th>
|
||||
<th>Title</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{% for p in puzzles %}
|
||||
<tr>
|
||||
<td>{{ p.date }}</td>
|
||||
<td>{{ p.size }}x{{ p.size }}</td>
|
||||
<td>{{ p.title }}</td>
|
||||
<td><a href="{{ request.route_url('daily_leaderboard', puzzle_id=p.id) }}">View Leaderboard</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
10
templates/login.html.j2
Normal file
10
templates/login.html.j2
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>Login</h1>
|
||||
<p>Enter your email to receive a 6-digit code.</p>
|
||||
<form method="POST">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" name="email" required>
|
||||
<button type="submit">Get Code</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
40
templates/manage_admins.html.j2
Normal file
40
templates/manage_admins.html.j2
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}Manage Admins{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Manage Admins</h1>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for admin in admins %}
|
||||
<tr>
|
||||
<td>{{ admin.username }}</td>
|
||||
<td>{{ admin.email }}</td>
|
||||
<td>
|
||||
{% if admin.id != request.user.id %}
|
||||
<form method="POST" action="{{ request.route_url('remove_admin', user_id=admin.id) }}" style="display:inline;">
|
||||
<button type="submit" onclick="return confirm('Are you sure you want to revoke admin privileges from {{ admin.username }}?')">Revoke Admin</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<em>Current User</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if message %}
|
||||
<p>{{ message }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p><a href="{{ request.route_url('invite_admin') }}">Invite a New Admin</a></p>
|
||||
{% endblock %}
|
||||
47
templates/profile.html.j2
Normal file
47
templates/profile.html.j2
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>Your Profile</h1>
|
||||
<p>Username: {{ user.username or "Not set" }}</p>
|
||||
{% if user.enable_gravatar %}
|
||||
<img src="{{ gravatar_url }}" alt="Gravatar Image">
|
||||
{% else %}
|
||||
<p>Gravatar is disabled.</p>
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
<label>
|
||||
<input type="checkbox" name="enable_gravatar" {% if user.enable_gravatar %}checked{% endif %}>
|
||||
Enable Gravatar
|
||||
</label><br><br>
|
||||
|
||||
<label>
|
||||
New Username:
|
||||
<input type="text" name="new_username" placeholder="Enter new username">
|
||||
</label><br><br>
|
||||
|
||||
<button type="submit">Update Profile</button>
|
||||
</form>
|
||||
|
||||
<h2>Stats</h2>
|
||||
<p>Total Puzzles Solved: {{ total_puzzles_solved }}</p>
|
||||
<p>Current Streak: {{ current_streak }} days</p>
|
||||
<p>Max Streak: {{ max_streak }} days</p>
|
||||
<h2>Your Solves</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Size</th>
|
||||
<th>Moves</th>
|
||||
<th>Rotations</th>
|
||||
<th>Time (sec)</th>
|
||||
</tr>
|
||||
{% for attempt in solved_attempts %}
|
||||
<tr>
|
||||
<td>{{ attempt.puzzle.date }}</td>
|
||||
<td>{{ attempt.puzzle.size }}x{{ attempt.puzzle.size }}</td>
|
||||
<td>{{ attempt.move_count }}</td>
|
||||
<td>{{ attempt.rotation_count }}</td>
|
||||
<td>{{ "%.2f"|format((attempt.time_completed - attempt.time_started).total_seconds()) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
13
templates/puzzle_menu.html.j2
Normal file
13
templates/puzzle_menu.html.j2
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>Select a Puzzle Size</h1>
|
||||
<ul>
|
||||
{% for size in puzzle_sizes %}
|
||||
{% if puzzles[size] %}
|
||||
<li><a href="{{ request.route_url('daily_puzzle', size=size) }}">{{ size }}x{{ size }} Puzzle</a></li>
|
||||
{% else %}
|
||||
<li>{{ size }}x{{ size }} Puzzle - Not Available Today</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
16
templates/upload.html.j2
Normal file
16
templates/upload.html.j2
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>Upload Puzzles</h1>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<label for="puzzle_date">Puzzle Date (YYYY-MM-DD):</label>
|
||||
<input type="text" name="puzzle_date" required><br><br>
|
||||
{% for size in [3, 4, 5] %}
|
||||
<h3>{{ size }}x{{ size }} Puzzle</h3>
|
||||
<label for="title_{{ size }}">Puzzle Title:</label>
|
||||
<input type="text" name="title_{{ size }}" value="Puzzle {{ size }}x{{ size }}"><br><br>
|
||||
<label for="image_file_{{ size }}">Choose Image:</label>
|
||||
<input type="file" name="image_file_{{ size }}" accept="image/*"><br><br>
|
||||
{% endfor %}
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
9
templates/verify.html.j2
Normal file
9
templates/verify.html.j2
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{% extends 'base.html.j2' %}
|
||||
{% block content %}
|
||||
<h1>Enter 6-digit code</h1>
|
||||
<form method="POST">
|
||||
<label for="code">Code:</label>
|
||||
<input type="text" name="code" required>
|
||||
<button type="submit">Verify</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue