Compare commits

...

4 Commits

Author SHA1 Message Date
Flatlogic Bot
88e3344f55 Aggiunte features ma non funziona trascinamento 2025-09-29 00:35:53 +00:00
Flatlogic Bot
6972ed7543 + Grafica 2025-09-28 23:26:12 +00:00
Flatlogic Bot
188419fc12 + login Google 2025-09-28 23:02:21 +00:00
Flatlogic Bot
8554409a47 Prima Funzionante 2025-09-28 22:38:17 +00:00
31552 changed files with 3172357 additions and 127 deletions

364
admin.php Normal file
View File

@ -0,0 +1,364 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
// Gatekeeper: redirect if not logged in
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
$pdo = db();
// --- IMAGE SLICING FUNCTION ---
function create_puzzle_pieces($source_image_path, $puzzle_id, $pieces_count) {
if (!function_exists('gd_info')) {
error_log("GD Library is not installed or enabled.");
return false;
}
$image_info = getimagesize($source_image_path);
$source_width = $image_info[0];
$source_height = $image_info[1];
$mime_type = $image_info['mime'];
switch ($mime_type) {
case 'image/jpeg':
$source_image = imagecreatefromjpeg($source_image_path);
break;
case 'image/png':
$source_image = imagecreatefrompng($source_image_path);
break;
case 'image/gif':
$source_image = imagecreatefromgif($source_image_path);
break;
default:
error_log("Unsupported image type: " . $mime_type);
return false;
}
if (!$source_image) {
error_log("Failed to create image from source.");
return false;
}
// Grid dimensions
$cols = 0;
$rows = 0;
if ($pieces_count == 16) { $cols = 4; $rows = 4; }
elseif ($pieces_count == 32) { $cols = 8; $rows = 4; }
elseif ($pieces_count == 64) { $cols = 8; $rows = 8; }
else {
error_log("Invalid pieces count: " . $pieces_count);
return false;
}
$piece_width = floor($source_width / $cols);
$piece_height = floor($source_height / $rows);
$output_dir = __DIR__ . '/puzzles/' . $puzzle_id . '/' . $pieces_count;
if (!is_dir($output_dir)) {
mkdir($output_dir, 0777, true);
}
for ($r = 0; $r < $rows; $r++) {
for ($c = 0; $c < $cols; $c++) {
$piece = imagecreatetruecolor($piece_width, $piece_height);
imagecopy(
$piece,
$source_image,
0, 0, // dest x, y
$c * $piece_width, $r * $piece_height, // source x, y
$piece_width, $piece_height
);
$piece_path = $output_dir . '/piece_' . $r . '_' . $c . '.jpg';
imagejpeg($piece, $piece_path, 90); // Save as JPG with 90% quality
imagedestroy($piece);
}
}
imagedestroy($source_image);
return true;
}
// Handle new puzzle upload
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_puzzle'])) {
$puzzle_name = trim($_POST['puzzle_name'] ?? '');
$pieces = (int)($_POST['pieces'] ?? 16);
$is_public = isset($_POST['is_public']) ? 1 : 0;
$image_file = $_FILES['puzzle_image'] ?? null;
if (!empty($puzzle_name) && $image_file && $image_file['error'] === UPLOAD_ERR_OK) {
$upload_dir = __DIR__ . '/uploads/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$original_filename = basename($image_file['name']);
$image_extension = pathinfo($original_filename, PATHINFO_EXTENSION);
$safe_filename = 'puzzle_' . uniqid() . '.' . $image_extension;
$upload_path = $upload_dir . $safe_filename;
if (move_uploaded_file($image_file['tmp_name'], $upload_path)) {
$stmt = $pdo->prepare('INSERT INTO puzzles (name, file_name, original_image, pieces, is_public, is_admin_upload) VALUES (?, ?, ?, ?, ?, 1)');
$stmt->execute([$puzzle_name, $safe_filename, $safe_filename, $pieces, $is_public]);
$puzzle_id = $pdo->lastInsertId();
create_puzzle_pieces($upload_path, $puzzle_id, $pieces);
header("Location: admin.php?success=1");
exit;
}
}
header("Location: admin.php?error=1");
exit;
}
// Handle puzzle deletion
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_puzzle'])) {
$puzzle_id_to_delete = $_POST['puzzle_id'];
$stmt = $pdo->prepare('SELECT file_name FROM puzzles WHERE id = ?');
$stmt->execute([$puzzle_id_to_delete]);
$puzzle = $stmt->fetch(PDO::FETCH_ASSOC);
if ($puzzle) {
$original_image_path = __DIR__ . '/uploads/' . $puzzle['file_name'];
if (file_exists($original_image_path)) {
unlink($original_image_path);
}
}
$stmt = $pdo->prepare('DELETE FROM puzzles WHERE id = ?');
$stmt->execute([$puzzle_id_to_delete]);
$puzzle_dir = __DIR__ . '/puzzles/' . $puzzle_id_to_delete;
if (is_dir($puzzle_dir)) {
function delete_directory($dir) {
if (!is_dir($dir)) return;
$files = array_diff(scandir($dir), array('.','..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? delete_directory("$dir/$file") : unlink("$dir/$file");
}
rmdir($dir);
}
delete_directory($puzzle_dir);
}
header("Location: admin.php?deleted=1");
exit;
}
// Handle user deletion
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_user'])) {
$user_id_to_delete = $_POST['user_id'];
// Prevent self-deletion
if ($user_id_to_delete == $_SESSION['user_id']) {
header("Location: admin.php?error=self_delete");
exit;
}
// Add logic here to delete user-related data if necessary (e.g., scores)
$stmt = $pdo->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$user_id_to_delete]);
header("Location: admin.php?user_deleted=1");
exit;
}
$puzzles = $pdo->query('SELECT id, name, file_name, pieces, is_public, created_at FROM puzzles ORDER BY created_at DESC')->fetchAll(PDO::FETCH_ASSOC);
// Fetch users
$users = $pdo->query('SELECT id, username, email, created_at FROM users ORDER BY created_at DESC')->fetchAll(PDO::FETCH_ASSOC);
$page_title = 'Pannello di Amministrazione';
require_once 'includes/header.php';
?>
<main class="container mt-5">
<?php if (isset($_GET['success'])):
?><div class="alert alert-success">Puzzle aggiunto con successo!</div>
<?php endif; ?>
<?php if (isset($_GET['deleted'])):
?><div class="alert alert-info">Puzzle eliminato con successo.</div>
<?php endif; ?>
<?php if (isset($_GET['user_deleted'])):
?><div class="alert alert-info">Utente eliminato con successo.</div>
<?php endif; ?>
<?php if (isset($_GET['error'])):
$error_message = 'Errore durante l\'upload. Controlla i dati e riprova.';
if ($_GET['error'] === 'self_delete') {
$error_message = 'Non puoi eliminare il tuo stesso account.';
}
?><div class="alert alert-danger"><?php echo $error_message; ?></div>
<?php endif; ?>
<!-- Upload Form -->
<div class="card p-4 mb-4">
<h2 class="mb-4">Aggiungi Nuovo Puzzle</h2>
<form action="admin.php" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="puzzle_name" class="form-label">Nome del Puzzle</label>
<input type="text" class="form-control" id="puzzle_name" name="puzzle_name" required>
</div>
<div class="mb-3">
<label for="puzzle_image" class="form-label">Immagine</label>
<input type="file" class="form-control" id="puzzle_image" name="puzzle_image" accept="image/jpeg, image/png, image/gif" required>
</div>
<div class="mb-3">
<label for="pieces" class="form-label">Numero di Pezzi</label>
<select class="form-select" id="pieces" name="pieces">
<option value="16">16 (4x4)</option>
<option value="32">32 (8x4)</option>
<option value="64">64 (8x8)</option>
</select>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="is_public" name="is_public" value="1" checked>
<label class="form-check-label" for="is_public">
Rendi Pubblico
</label>
</div>
<button type="submit" name="add_puzzle" class="btn btn-primary">Aggiungi Puzzle</button>
</form>
</div>
<h1 class="mb-4">Gestione Puzzle</h1>
<div class="table-responsive card p-3">
<table class="table table-striped table-bordered table-dark">
<thead>
<tr>
<th>ID</th>
<th>Nome</th>
<th>Immagine</th>
<th>Pezzi</th>
<th>Pubblico</th>
<th>Creato il</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($puzzles)):
?><tr class="text-center"><td colspan="7">Nessun puzzle trovato.</td></tr>
<?php else:
?><?php foreach ($puzzles as $puzzle):
$image_path = __DIR__ . '/uploads/' . $puzzle['file_name'];
if (file_exists($image_path)) {
$imageUrl = 'uploads/' . htmlspecialchars($puzzle['file_name']) . '?v=' . filemtime($image_path);
} else {
$imageUrl = 'assets/images/placeholder.png';
}
?><tr >
<td><?php echo htmlspecialchars($puzzle['id']); ?></td>
<td><a href="puzzle.php?id=<?php echo htmlspecialchars($puzzle['id']); ?>" target="_blank"><?php echo htmlspecialchars($puzzle['name']); ?></a></td>
<td><img src="<?php echo $imageUrl; ?>" alt="Puzzle" width="100" class="rounded"></td>
<td><?php echo htmlspecialchars($puzzle['pieces']); ?></td>
<td>
<label class="toggle-switch">
<input type="checkbox" class="is-public-toggle" data-puzzle-id="<?php echo $puzzle['id']; ?>" <?php echo $puzzle['is_public'] ? 'checked' : ''; ?>>
<span class="slider"></span>
</label>
</td>
<td><?php echo htmlspecialchars($puzzle['created_at']); ?></td>
<td>
<form method="POST" action="admin.php" onsubmit="return confirm('Sei sicuro di voler eliminare questo puzzle? L\\'azione è irreversibile.');">
<input type="hidden" name="puzzle_id" value="<?php echo htmlspecialchars($puzzle['id']); ?>">
<button type="submit" name="delete_puzzle" class="btn btn-danger btn-sm">Elimina</button>
</form>
</td>
</tr>
<?php endforeach; ?><?php endif; ?>
</tbody>
</table>
</div>
<!-- User Management Section -->
<div class="mt-5">
<h1 class="mb-4">Gestione Utenti</h1>
<div class="table-responsive card p-3">
<table class="table table-striped table-bordered table-dark">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Registrato il</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>
<?php if (empty($users)):
?><tr class="text-center"><td colspan="5">Nessun utente trovato.</td></tr>
<?php else:
?><?php foreach ($users as $user):
?><tr >
<td><?php echo htmlspecialchars($user['id']); ?></td>
<td><?php echo htmlspecialchars($user['username']); ?></td>
<td><?php echo htmlspecialchars($user['email']); ?></td>
<td><?php echo htmlspecialchars($user['created_at']); ?></td>
<td>
<?php if ($user['id'] != $_SESSION['user_id']): // Prevent deleting yourself ?>
<form method="POST" action="admin.php" onsubmit="return confirm('Sei sicuro di voler eliminare questo utente? L\'azione è irreversibile.');">
<input type="hidden" name="user_id" value="<?php echo htmlspecialchars($user['id']); ?>">
<button type="submit" name="delete_user" class="btn btn-danger btn-sm">Elimina</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?><?php endif; ?>
</tbody>
</table>
</div>
</div>
</main>
<script>
document.addEventListener('DOMContentLoaded', function () {
const toggles = document.querySelectorAll('.is-public-toggle');
toggles.forEach(toggle => {
toggle.addEventListener('change', function () {
const puzzleId = this.dataset.puzzleId;
const isPublic = this.checked;
fetch('api/toggle_puzzle_status.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
puzzle_id: puzzleId,
is_public: isPublic
})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error('Error updating puzzle status:', data.message);
// Optionally, revert the switch on failure
this.checked = !isPublic;
}
})
.catch(error => {
console.error('Fetch error:', error);
this.checked = !isPublic;
});
});
});
});
</script>
<?php require_once 'includes/footer.php'; ?>

View File

@ -0,0 +1,51 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
header('Content-Type: application/json');
// Basic security checks
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); // Method Not Allowed
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
exit;
}
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
http_response_code(403); // Forbidden
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
exit;
}
require_once '../db/config.php';
// Get the posted data
$data = json_decode(file_get_contents('php://input'), true);
$puzzle_id = $data['puzzle_id'] ?? null;
$is_public = $data['is_public'] ?? null;
if ($puzzle_id === null || $is_public === null) {
http_response_code(400); // Bad Request
echo json_encode(['success' => false, 'message' => 'Dati mancanti.']);
exit;
}
// Update the database
try {
$pdo = db();
$stmt = $pdo->prepare('UPDATE puzzles SET is_public = ? WHERE id = ?');
$stmt->execute([(int)$is_public, $puzzle_id]);
if ($stmt->rowCount() > 0) {
echo json_encode(['success' => true, 'message' => 'Stato del puzzle aggiornato.']);
} else {
// This can happen if the puzzle ID doesn't exist or the state was already the same
echo json_encode(['success' => true, 'message' => 'Nessuna modifica necessaria o puzzle non trovato.']);
}
} catch (PDOException $e) {
http_response_code(500); // Internal Server Error
error_log('PDOException in toggle_puzzle_status.php: ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => 'Errore del database.']);
}

386
assets/css/custom.css Normal file
View File

@ -0,0 +1,386 @@
/* IMPORT TYPOGRAPHY */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Orbitron:wght@400;600;700;900&display=swap');
/* PALETTE */
:root {
--bg-primary: #0F172A; /* slate-900 - Deep space background */
--bg-secondary: #1E293B; /* slate-800 - Card backgrounds */
--accent-primary: #06B6D4; /* cyan-500 - CTAs and highlights */
--accent-glow: #22D3EE; /* cyan-400 - Hover states */
--text-primary: #F8FAFC; /* slate-50 - Primary text */
--text-secondary: #94A3B8; /* slate-400 - Secondary text */
}
/* GENERAL STYLES */
body {
background: var(--bg-primary);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Orbitron', sans-serif;
color: var(--accent-primary);
font-weight: 700;
}
h1 {
letter-spacing: -0.025em;
line-height: 1.1;
}
a {
color: var(--accent-primary);
text-decoration: none;
transition: color 0.3s ease;
}
a:hover {
color: var(--accent-glow);
}
.btn-primary {
background-color: var(--accent-primary);
border-color: var(--accent-primary);
color: var(--bg-primary) !important;
font-weight: 600;
transition: background-color 0.3s ease, border-color 0.3s ease, transform 0.2s ease;
}
.btn-primary:hover {
background-color: var(--accent-glow);
border-color: var(--accent-glow);
transform: translateY(-2px);
}
.btn-outline-cyan {
background: transparent;
border: 2px solid rgba(6, 182, 212, 0.5);
color: #06B6D4;
padding: 0.75rem 1.5rem;
border-radius: 12px;
font-weight: 600;
transition: all 0.3s ease;
backdrop-filter: blur(4px);
}
.btn-outline-cyan:hover {
background: rgba(6, 182, 212, 0.1);
border-color: #06B6D4;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.3);
}
.form-control {
background-color: var(--bg-secondary);
border: 1px solid rgba(6, 182, 212, 0.15);
color: var(--text-primary);
}
.form-control:focus {
background-color: var(--bg-secondary);
border-color: var(--accent-primary);
color: var(--text-primary);
box-shadow: 0 0 0 0.2rem rgba(6, 182, 212, 0.25);
}
/* HEADER */
header.navbar {
background: rgba(15, 23, 42, 0.95) !important;
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(6, 182, 212, 0.1);
height: 64px;
position: sticky;
top: 0;
z-index: 1000;
}
.navbar-brand, .nav-link {
color: var(--text-primary) !important;
font-family: 'Orbitron', sans-serif;
}
.navbar-brand:hover, .nav-link:hover {
color: var(--accent-glow) !important;
}
/* LAYOUT & SPACING */
.main-content {
padding: 3rem 2rem;
max-width: 1400px;
margin: 0 auto;
}
.hero-section {
margin-bottom: 4rem;
}
.puzzle-grid {
gap: 2rem;
margin-bottom: 3rem;
}
/* PUZZLE CARDS */
.puzzle-card {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(8px);
border: 1px solid rgba(6, 182, 212, 0.15);
border-radius: 16px;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.puzzle-card:hover {
transform: translateY(-6px) scale(1.03);
border-color: rgba(6, 182, 212, 0.4);
box-shadow: 0 16px 40px rgba(6, 182, 212, 0.25), 0 8px 16px rgba(0, 0, 0, 0.3);
}
.card-title {
background: linear-gradient(135deg, #06B6D4, #22D3EE);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 600;
font-size: 1.125rem;
}
.card-text {
color: var(--text-secondary);
}
.card-body {
background: var(--bg-secondary);
}
/* LEADERBOARD */
.leaderboard-sidebar {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(8px);
border: 1px solid rgba(6, 182, 212, 0.15);
border-radius: 16px;
padding: 20px;
}
.leaderboard-title {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #F8FAFC;
}
.leaderboard-subtitle {
color: #94A3B8;
font-size: 0.875rem;
}
.leaderboard-sidebar table {
width: 100%;
color: var(--text-primary);
}
.leaderboard-sidebar th {
color: var(--accent-primary);
border-bottom: 1px solid rgba(6, 182, 212, 0.15);
}
.leaderboard-sidebar td {
padding: 8px 4px;
border-bottom: 1px solid rgba(30, 41, 59, 0.5);
}
/* PUZZLE PAGE */
#puzzle-board {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.2);
background: radial-gradient(circle at center, rgba(6, 182, 212, 0.1), transparent 60%);
border: 1px solid rgba(6, 182, 212, 0.2);
padding: 10px;
}
#pieces-tray {
border-radius: 16px;
padding: 1rem;
background: radial-gradient(circle at top, rgba(248, 250, 252, 0.05), transparent 70%), var(--bg-secondary);
border: 1px solid rgba(248, 250, 252, 0.1);
min-height: 200px;
max-height: 50vh;
overflow-y: auto;
}
.drop-zone {
border: 1px dashed rgba(248, 250, 252, 0.2);
border-radius: 8px;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.drop-zone.hovered {
background-color: rgba(6, 182, 212, 0.2);
border-color: var(--accent-glow);
}
.puzzle-piece {
cursor: grab;
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
border-radius: 6px;
}
.puzzle-piece:active {
cursor: grabbing;
}
.puzzle-piece:hover {
transform: scale(1.05);
box-shadow: 0 0 15px var(--accent-glow);
}
/* COMPLETION MODAL */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(10px);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
opacity: 1;
transition: opacity 0.3s ease;
}
.modal-backdrop.hidden {
opacity: 0;
pointer-events: none;
}
.modal-content {
background: var(--bg-secondary);
padding: 2rem 3rem;
border-radius: 16px;
border: 1px solid rgba(6, 182, 212, 0.2);
text-align: center;
position: relative;
transform: scale(1);
transition: transform 0.3s ease;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.modal-backdrop.hidden .modal-content {
transform: scale(0.9);
}
.close-btn {
position: absolute;
top: 10px;
right: 15px;
background: none;
border: none;
font-size: 2rem;
color: var(--text-secondary);
cursor: pointer;
transition: color 0.3s ease;
}
.close-btn:hover {
color: var(--text-primary);
}
.modal-content h2 {
margin-bottom: 0.5rem;
}
.modal-content p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.modal-content .score {
color: var(--text-primary);
font-size: 1.25rem;
margin-bottom: 2rem;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
/* UTILITIES & OVERRIDES */
main {
background: linear-gradient(135deg, var(--bg-primary) 0%, #131c31 100%);
padding: 40px 24px;
min-height: calc(100vh - 64px);
}
.bg-dark {
background-color: var(--bg-secondary) !important;
}
.text-white {
color: var(--text-primary) !important;
}
.border-dark {
border-color: #06b6d4 !important;
}
.table-dark {
--bs-table-bg: var(--bg-secondary);
--bs-table-border-color: var(--accent-primary);
}
/* Toggle Switch styles */
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #4f5d73; /* Darker grey for off state */
transition: .4s;
border-radius: 28px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent-primary);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--accent-primary);
}
input:checked + .slider:before {
transform: translateX(22px);
}

3
assets/js/main.js Normal file
View File

@ -0,0 +1,3 @@
// Custom JavaScript will go here in future steps.
console.log("Photo Puzzle UI loaded.");

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

6
composer.json Normal file
View File

@ -0,0 +1,6 @@
{
"require": {
"google/apiclient": "^2.0"
}
}

1283
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,15 @@ define('DB_NAME', 'app_30903');
define('DB_USER', 'app_30903'); define('DB_USER', 'app_30903');
define('DB_PASS', '7d2d5a2d-6e5e-4580-a9b7-3f8e7288a494'); define('DB_PASS', '7d2d5a2d-6e5e-4580-a9b7-3f8e7288a494');
// Admin credentials (for a real app, use hashed passwords)
define('ADMIN_USER', 'admin');
define('ADMIN_PASS_HASH', '$2y$10$WnTccK10o9fJmz3xfs6gV.0GB2PH.Smtm4NOlqlnr7SmEgMcY8uC.'); // Hash for 'MaviGames'
// Google API Credentials - ** PLEASE REPLACE WITH YOUR OWN CREDENTIALS **
define('GOOGLE_CLIENT_ID', 'YOUR_GOOGLE_CLIENT_ID');
define('GOOGLE_CLIENT_SECRET', 'YOUR_GOOGLE_CLIENT_SECRET');
define('GOOGLE_REDIRECT_URI', 'https://mavigames-puzzle.dev.flatlogic.app/google-callback.php');
function db() { function db() {
static $pdo; static $pdo;
if (!$pdo) { if (!$pdo) {

16
google-auth.php Normal file
View File

@ -0,0 +1,16 @@
<?php
session_start();
require_once 'vendor/autoload.php';
require_once 'db/config.php';
$client = new Google_Client();
$client->setClientId(GOOGLE_CLIENT_ID);
$client->setClientSecret(GOOGLE_CLIENT_SECRET);
$client->setRedirectUri(GOOGLE_REDIRECT_URI);
$client->addScope('email');
$client->addScope('profile');
$auth_url = $client->createAuthUrl();
header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
exit();

63
google-callback.php Normal file
View File

@ -0,0 +1,63 @@
<?php
session_start();
require_once 'vendor/autoload.php';
require_once 'db/config.php';
$client = new Google_Client();
$client->setClientId(GOOGLE_CLIENT_ID);
$client->setClientSecret(GOOGLE_CLIENT_SECRET);
$client->setRedirectUri(GOOGLE_REDIRECT_URI);
$client->addScope('email');
$client->addScope('profile');
if (isset($_GET['code'])) {
$token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
$client->setAccessToken($token);
$google_oauth = new Google_Service_Oauth2($client);
$google_account_info = $google_oauth->userinfo->get();
$email = $google_account_info->email;
$name = $google_account_info->name;
$google_id = $google_account_info->id;
try {
$pdo = db();
// Check if user exists
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user) {
// User exists, log them in
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
if ($user['username'] === ADMIN_USER) {
$_SESSION['is_admin'] = true;
}
} else {
// User doesn't exist, create a new one
$username = strtok($email, '@'); // Create a username from email
$password = password_hash(random_bytes(16), PASSWORD_BCRYPT); // Create a random password
$stmt = $pdo->prepare("INSERT INTO users (username, email, password, google_id) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $email, $password, $google_id]);
$user_id = $pdo->lastInsertId();
$_SESSION['user_id'] = $user_id;
$_SESSION['username'] = $username;
}
header('Location: index.php');
exit();
} catch (PDOException $e) {
die("Database error: " . $e->getMessage());
}
} else {
header('Location: login.php');
exit();
}

15
includes/footer.php Normal file
View File

@ -0,0 +1,15 @@
<!-- Footer -->
<footer class="text-center py-3 mt-auto">
<small>&copy; <?php echo date("Y"); ?> Photo Puzzle. Creato con ❤️.</small>
<?php if (isset($_SESSION['is_admin']) && $_SESSION['is_admin']): ?>
<small class="d-block"><a href="admin.php">Admin Panel</a></small>
<?php endif; ?>
</footer>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- Custom JS -->
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>

55
includes/header.php Normal file
View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo isset($page_title) ? htmlspecialchars($page_title) : 'Photo Puzzle'; ?></title>
<meta name="description" content="<?php echo isset($page_description) ? htmlspecialchars($page_description) : 'Crea e risolvi puzzle fotografici.'; ?>">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body class="d-flex flex-column min-vh-100">
<!-- Sticky Header -->
<header class="navbar navbar-expand-lg sticky-top">
<div class="container-fluid">
<a class="navbar-brand fw-bold" href="index.php">Photo Puzzle</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<?php if (isset($_SESSION['user_id'])): ?>
<li class="nav-item">
<a class="nav-link" href="index.php">Gioca</a>
</li>
<li class="nav-item">
<a class="nav-link" href="leaderboard.php">Classifica</a>
</li>
<li class="nav-item">
<a class="nav-link" href="logout.php">Logout (<?php echo htmlspecialchars($_SESSION['username']); ?>)</a>
</li>
<?php if (isset($_SESSION['is_admin']) && $_SESSION['is_admin']): ?>
<li class="nav-item">
<a class="nav-link" href="admin.php">Admin</a>
</li>
<?php endif; ?>
<?php else: ?>
<li class="nav-item">
<a class="nav-link" href="login.php">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="register.php">Registrati</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
</header>

357
index.php
View File

@ -1,131 +1,234 @@
<?php <?php
declare(strict_types=1); session_start();
@ini_set('display_errors', '1'); require_once 'db/config.php';
@error_reporting(E_ALL);
@date_default_timezone_set('UTC'); // --- DATABASE SETUP ---
function setup_database() {
try {
$pdo = db();
$pdo->exec("CREATE TABLE IF NOT EXISTS puzzles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) DEFAULT 'Senza Nome',
file_name VARCHAR(255) NOT NULL UNIQUE,
is_public BOOLEAN DEFAULT FALSE,
is_admin_upload BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// Attempt to add 'name' column, ignore error if it already exists
try {
$pdo->exec("ALTER TABLE puzzles ADD COLUMN name VARCHAR(255) DEFAULT 'Senza Nome'");
} catch (PDOException $e) {
// Ignore error, likely "Duplicate column name"
}
} catch (PDOException $e) {
// In a real app, log this error instead of displaying it
die("Errore di connessione al database: " . $e->getMessage());
}
}
setup_database();
// --- GLOBAL VARIABLES ---
$uploaded_image = null;
$error_message = null;
$gallery_images = [];
// --- DATA FETCHING ---
try {
$pdo = db();
// Fetch puzzles
$stmt = $pdo->query("SELECT id, file_name, name FROM puzzles WHERE is_public = 1 OR is_admin_upload = 1 ORDER BY created_at DESC");
$gallery_images = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch leaderboard
$leaderboard_stmt = $pdo->query("
SELECT u.username, s.score
FROM scores s
JOIN users u ON s.user_id = u.id
ORDER BY s.score DESC
LIMIT 10
");
$leaderboard_scores = $leaderboard_stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$error_message = "Impossibile caricare i dati della pagina.";
// Optionally log the error: error_log($e->getMessage());
}
// --- POST REQUEST: IMAGE UPLOAD ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['puzzle_image'])) {
$target_dir = 'uploads/';
$max_file_size = 5 * 1024 * 1024; // 5 MB
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
$file = $_FILES['puzzle_image'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$error_message = 'Si è verificato un errore durante il caricamento.';
} else {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($file['tmp_name']);
if ($file['size'] > $max_file_size) {
$error_message = 'Il file è troppo grande. La dimensione massima è 5MB.';
} elseif (!in_array($mime_type, $allowed_types)) {
$error_message = 'Tipo di file non valido. Sono ammessi solo JPG, PNG, GIF.';
} else {
$file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$safe_filename = uniqid('puzzle_', true) . '.' . strtolower($file_extension);
$target_file = $target_dir . $safe_filename;
if (move_uploaded_file($file['tmp_name'], $target_file)) {
// Save to database
try {
$is_public = isset($_POST['share_puzzle']) ? 1 : 0; // Ensure 1 or 0
$puzzle_name = !empty($_POST['puzzle_name']) ? trim($_POST['puzzle_name']) : 'Senza Nome';
$pdo = db();
$stmt = $pdo->prepare("INSERT INTO puzzles (name, file_name, is_public, is_admin_upload) VALUES (?, ?, ?, ?)");
$is_admin_upload = 0; // Always false for user uploads
$stmt->execute([$puzzle_name, $safe_filename, $is_public, $is_admin_upload]);
$new_puzzle_id = $pdo->lastInsertId();
} catch (PDOException $e) {
// Error saving to DB, delete file
unlink($target_file);
// Log the detailed error for developers: error_log($e->getMessage());
$error_message = "Si è verificato un errore nel salvataggio dei dati del puzzle: " . $e->getMessage();
}
if (!$error_message) {
// Redirect to the new puzzle page
header('Location: puzzle.php?id=' . $new_puzzle_id);
exit;
}
} else {
$error_message = 'Si è verificato un errore durante il salvataggio del file.';
}
}
}
}
// --- GET REQUESTS ---
// Handle New Game
if (isset($_GET['action']) && $_GET['action'] === 'new') {
unset($_SESSION['selected_puzzle']);
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
// --- SET CURRENT IMAGE FOR DISPLAY ---
if (isset($_SESSION['selected_puzzle'])) {
$uploaded_image = $_SESSION['selected_puzzle'];
}
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?> ?>
<!doctype html> <?php
<html lang="en"> $page_title = 'Photo Puzzle - Gioca e Crea';
<head> $page_description = 'Crea e risolvi puzzle fotografici direttamente nel tuo browser. Scegli dalla nostra galleria o carica la tua immagine.';
<meta charset="utf-8" /> require_once 'includes/header.php';
<meta name="viewport" content="width=device-width, initial-scale=1" /> ?>
<title>New Style</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <!-- Main Content -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <main class="flex-grow-1">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"> <div class="main-content">
<style> <div class="row">
:root { <!-- Main content (Puzzles) -->
--bg-color-start: #6a11cb; <div class="col-lg-8">
--bg-color-end: #2575fc; <!-- Gallery Section -->
--text-color: #ffffff; <section class="hero-section text-center" aria-labelledby="gallery-heading">
--card-bg-color: rgba(255, 255, 255, 0.01); <h1 id="gallery-heading">Scegli un Puzzle dalla Galleria</h1>
--card-border-color: rgba(255, 255, 255, 0.1); <p class="text-secondary fs-5">Oppure crea il tuo e sfida la community.</p>
} </section>
body {
margin: 0; <section class="puzzle-gallery mb-5" aria-label="Galleria dei Puzzle">
font-family: 'Inter', sans-serif; <?php if (!empty($gallery_images)): ?>
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); <div class="row row-cols-1 row-cols-sm-2 row-cols-xl-3 puzzle-grid">
color: var(--text-color); <?php foreach ($gallery_images as $img): ?>
display: flex; <div class="col">
justify-content: center; <a href="puzzle.php?id=<?php echo $img['id']; ?>" class="text-decoration-none">
align-items: center; <div class="puzzle-card h-100">
min-height: 100vh; <img src="uploads/<?php echo htmlspecialchars($img['file_name']); ?>" class="card-img-top" alt="<?php echo htmlspecialchars($img['name']); ?>" style="height: 200px; object-fit: cover;">
text-align: center; <div class="card-body">
overflow: hidden; <h5 class="card-title"><?php echo htmlspecialchars($img['name']); ?></h5>
position: relative; </div>
} </div>
body::before { </a>
content: ''; </div>
position: absolute; <?php endforeach; ?>
top: 0; </div>
left: 0; <?php else: ?>
width: 100%; <div class="text-center p-5 rounded" style="background: var(--bg-secondary);">
height: 100%; <p class="text-secondary">Nessun puzzle disponibile al momento. Caricane uno tu!</p>
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>'); </div>
animation: bg-pan 20s linear infinite; <?php endif; ?>
z-index: -1; </section>
}
@keyframes bg-pan { <?php if (isset($_SESSION['user_id'])): ?>
0% { background-position: 0% 0%; } <!-- Upload Section for Logged-in Users -->
100% { background-position: 100% 100%; } <section class="p-4 rounded" style="background: var(--bg-secondary);" aria-labelledby="upload-heading">
} <h2 id="upload-heading">Crea un nuovo Puzzle</h2>
main { <p class="text-secondary mb-4">Carica un'immagine per iniziare una nuova sfida.</p>
padding: 2rem;
} <?php if ($error_message): ?>
.card { <div class="alert alert-danger"><?php echo htmlspecialchars($error_message); ?></div>
background: var(--card-bg-color); <?php endif; ?>
border: 1px solid var(--card-border-color);
border-radius: 16px; <form action="index.php" method="post" enctype="multipart/form-data">
padding: 2rem; <div class="row g-3">
backdrop-filter: blur(20px); <div class="col-md-6">
-webkit-backdrop-filter: blur(20px); <label for="puzzle_name" class="form-label">Nome del Puzzle</label>
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1); <input type="text" class="form-control" id="puzzle_name" name="puzzle_name" placeholder="Es: Tramonto a Venezia" required>
} </div>
.loader { <div class="col-md-6">
margin: 1.25rem auto 1.25rem; <label for="puzzle_image_upload" class="form-label">Seleziona immagine (max 5MB)</label>
width: 48px; <input class="form-control" type="file" name="puzzle_image" id="puzzle_image_upload" accept="image/jpeg,image/png,image/gif" required>
height: 48px; </div>
border: 3px solid rgba(255, 255, 255, 0.25); </div>
border-top-color: #fff; <div class="form-check form-switch my-3">
border-radius: 50%; <input class="form-check-input" type="checkbox" role="switch" id="share_puzzle" name="share_puzzle" value="1" checked>
animation: spin 1s linear infinite; <label class="form-check-label" for="share_puzzle">Condividi nella galleria pubblica</label>
} </div>
@keyframes spin { <div class="text-start">
from { transform: rotate(0deg); } <button type="submit" class="btn btn-primary">Crea e Gioca</button>
to { transform: rotate(360deg); } </div>
} </form>
.hint { </section>
opacity: 0.9; <?php endif; ?>
} </div>
.sr-only {
position: absolute; <!-- Leaderboard Sidebar -->
width: 1px; height: 1px; <div class="col-lg-4">
padding: 0; margin: -1px; <aside class="leaderboard-sidebar">
overflow: hidden; <h2 class="leaderboard-title">Top 10 Giocatori</h2>
clip: rect(0, 0, 0, 0); <p class="leaderboard-subtitle mb-3">La classifica dei maestri del puzzle.</p>
white-space: nowrap; border: 0; <?php if (!empty($leaderboard_scores)): ?>
} <table class="table table-borderless">
h1 { <thead>
font-size: 3rem; <tr>
font-weight: 700; <th>#</th>
margin: 0 0 1rem; <th>Utente</th>
letter-spacing: -1px; <th>Punteggio</th>
} </tr>
p { </thead>
margin: 0.5rem 0; <tbody>
font-size: 1.1rem; <?php foreach ($leaderboard_scores as $index => $score): ?>
} <tr>
code { <td><?php echo $index + 1; ?></td>
background: rgba(0,0,0,0.2); <td><?php echo htmlspecialchars($score['username']); ?></td>
padding: 2px 6px; <td><?php echo htmlspecialchars($score['score']); ?></td>
border-radius: 4px; </tr>
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; <?php endforeach; ?>
} </tbody>
footer { </table>
position: absolute; <?php else: ?>
bottom: 1rem; <p class="text-secondary">Nessun punteggio ancora. Inizia a giocare!</p>
font-size: 0.8rem; <?php endif; ?>
opacity: 0.7; <div class="text-center mt-3">
} <a href="leaderboard.php" class="btn btn-outline-cyan">Vedi Classifica Completa</a>
</style> </div>
</head> </aside>
<body> </div>
<main> </div>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWiZZy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main> </main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <?php require_once 'includes/footer.php'; ?>
</footer>
</body>
</html>

70
leaderboard.php Normal file
View File

@ -0,0 +1,70 @@
<?php
session_start();
require_once 'db/config.php';
// Gatekeeper: redirect if not logged in
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$pdo = db();
$stmt = $pdo->query("
SELECT
s.score,
s.time_taken,
s.moves,
s.completed_at,
u.username,
p.name as puzzle_name
FROM scores s
JOIN users u ON s.user_id = u.id
JOIN puzzles p ON s.puzzle_id = p.id
ORDER BY s.score DESC
LIMIT 100
");
$scores = $stmt->fetchAll(PDO::FETCH_ASSOC);
$page_title = 'Classifica';
require_once 'includes/header.php';
?>
<main class="container mt-5">
<h1 class="mb-4 text-center">Classifica Generale</h1>
<div class="table-responsive card p-3">
<table class="table table-striped table-hover table-dark">
<thead>
<tr>
<th>#</th>
<th>Utente</th>
<th>Puzzle</th>
<th>Punteggio</th>
<th>Tempo</th>
<th>Mosse</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<?php if (empty($scores)): ?>
<tr>
<td colspan="7" class="text-center">Nessun punteggio registrato.</td>
</tr>
<?php else: ?>
<?php foreach ($scores as $index => $score): ?>
<tr>
<td><?php echo $index + 1; ?></td>
<td><?php echo htmlspecialchars($score['username']); ?></td>
<td><?php echo htmlspecialchars($score['puzzle_name']); ?></td>
<td><?php echo htmlspecialchars($score['score']); ?></td>
<td><?php echo htmlspecialchars($score['time_taken']); ?>s</td>
<td><?php echo htmlspecialchars($score['moves']); ?></td>
<td><?php echo htmlspecialchars(date("d/m/Y H:i", strtotime($score['completed_at']))); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</main>
<?php require_once 'includes/footer.php'; ?>

97
login.php Normal file
View File

@ -0,0 +1,97 @@
<?php
session_start();
require_once 'db/config.php';
// Redirect if already logged in
if (isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
$error_message = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error_message = 'Username e password sono obbligatori.';
} else {
// Admin login check
if ($username === ADMIN_USER && password_verify($password, ADMIN_PASS_HASH)) {
$_SESSION['user_id'] = 0; // Special ID for admin
$_SESSION['username'] = ADMIN_USER;
$_SESSION['is_admin'] = true;
header('Location: admin.php');
exit;
}
// Regular user login check
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['is_admin'] = false; // Ensure this is false for regular users
header('Location: index.php');
exit;
} else {
$error_message = 'Credenziali non valide. Riprova.';
}
} catch (PDOException $e) {
$error_message = "Errore del database: " . $e->getMessage();
}
}
}
$page_title = 'Login';
require_once 'includes/header.php';
?>
<main class="container my-auto">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card p-4 shadow-sm">
<h1 class="h3 text-center mb-4">Login</h1>
<?php if ($error_message): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error_message); ?></div>
<?php endif; ?>
<a href="google-auth.php" class="btn btn-danger w-100 mb-3">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-google" viewBox="0 0 16 16">
<path d="M15.545 6.558a9.42 9.42 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.689 7.689 0 0 1 5.352 2.082l-2.284 2.284A4.347 4.347 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.25C2.806 7.48 2.5 8.522 2.5 9.5s.306 2.02.812 2.834c.632 1.842 2.405 3.25 4.492 3.25 1.257 0 2.34-.47 3.162-1.224l.064-.064a3.837 3.837 0 0 0 1.15-2.487H8v-2h7.545z"/>
</svg> Accedi con Google
</a>
<div class="d-flex align-items-center my-3">
<hr class="flex-grow-1">
<span class="px-2 text-muted">oppure</span>
<hr class="flex-grow-1">
</div>
<form method="post" action="login.php">
<div class="mb-3">
<label for="username" class="form-label">Nome Utente</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Accedi</button>
</form>
<div class="text-center mt-3">
<p class="mb-0">Non hai un account? <a href="register.php">Registrati</a></p>
</div>
</div>
</div>
</div>
</main>
<?php require_once 'includes/footer.php'; ?>

6
logout.php Normal file
View File

@ -0,0 +1,6 @@
<?php
session_start();
session_unset();
session_destroy();
header('Location: login.php');
exit;

544
puzzle.php Normal file
View File

@ -0,0 +1,544 @@
<?php
session_start();
// Gatekeeper: redirect if not logged in
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once 'db/config.php';
// Handle Score Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'save_score') {
header('Content-Type: application/json');
$user_id = $_SESSION['user_id'];
$puzzle_id = (int)$_POST['puzzle_id'];
$time_taken = (int)$_POST['time_taken'];
$moves = (int)$_POST['moves'];
// Simple scoring formula
$score = max(0, 10000 - ($time_taken * 10) - ($moves * 5));
try {
$pdo = db();
$stmt = $pdo->prepare("INSERT INTO scores (user_id, puzzle_id, time_taken, moves, score) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$user_id, $puzzle_id, $time_taken, $moves, $score]);
echo json_encode(['success' => true, 'score' => $score]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit; // Important: stop script execution after AJAX response
}
// --- VARIABLES ---
$puzzle_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$difficulty = isset($_GET['difficulty']) ? (int)$_GET['difficulty'] : 16;
// Determine grid size based on difficulty
$valid_difficulties = [
16 => [4, 4], // 4x4 grid
32 => [8, 4], // 8x4 grid
64 => [8, 8] // 8x8 grid
];
if (!isset($valid_difficulties[$difficulty])) {
$difficulty = 16; // Fallback to default if invalid
}
list($cols, $rows) = $valid_difficulties[$difficulty];
$puzzle = null;
$error_message = '';
$pieces = [];
$source_width = 1;
$source_height = 1;
// --- DATA FETCHING & PUZZLE CREATION ---
if ($puzzle_id > 0) {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM puzzles WHERE id = ?");
$stmt->execute([$puzzle_id]);
$puzzle = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$puzzle) {
$error_message = "Puzzle non trovato.";
} else {
$image_path = 'uploads/' . $puzzle['file_name'];
if (!file_exists($image_path)) {
$error_message = "File immagine del puzzle non trovato.";
} else {
$image_info = getimagesize($image_path);
$source_width = $image_info[0];
$source_height = $image_info[1];
$pieces_dir = sprintf('puzzles/%d/%d', $puzzle['id'], $difficulty);
if (!is_dir($pieces_dir)) {
mkdir($pieces_dir, 0777, true);
}
$piece_files = glob($pieces_dir . '/piece_*.jpg');
if (count($piece_files) !== ($cols * $rows)) {
foreach ($piece_files as $file) { unlink($file); }
$mime_type = $image_info['mime'];
$source_image = null;
switch ($mime_type) {
case 'image/jpeg': $source_image = imagecreatefromjpeg($image_path); break;
case 'image/png': $source_image = imagecreatefrompng($image_path); break;
case 'image/gif': $source_image = imagecreatefromgif($image_path); break;
default: $error_message = "Formato immagine non supportato."; break;
}
if ($source_image) {
$piece_width = floor($source_width / $cols);
$piece_height = floor($source_height / $rows);
for ($y = 0; $y < $rows; $y++) {
for ($x = 0; $x < $cols; $x++) {
$piece = imagecreatetruecolor($piece_width, $piece_height);
imagecopy($piece, $source_image, 0, 0, $x * $piece_width, $y * $piece_height, $piece_width, $piece_height);
imagejpeg($piece, "{$pieces_dir}/piece_{$y}_{$x}.jpg", 95);
imagedestroy($piece);
}
}
imagedestroy($source_image);
}
}
$pieces = glob($pieces_dir . '/piece_*.jpg');
shuffle($pieces);
}
}
} catch (PDOException $e) {
$error_message = "Errore di sistema: " . $e->getMessage();
}
} else {
$error_message = "ID del puzzle non valido.";
}
$page_title = 'Risolvi: ' . ($puzzle ? htmlspecialchars($puzzle['name']) : 'Puzzle');
require_once 'includes/header.php';
?>
<style>
:root {
--puzzle-width: clamp(500px, 70vw, 800px);
--puzzle-background: #f0f0f0;
--piece-tray-height: 75vh;
--piece-shadow: 0 1px 3px rgba(0,0,0,0.2);
--dragging-shadow: 0 10px 25px rgba(0,0,0,0.4);
--drop-zone-border: 1px dashed #aaa;
--drop-zone-near-bg: rgba(40, 167, 69, 0.25);
--drop-zone-near-border: #28a745;
--drop-zone-near-shadow: inset 0 0 10px rgba(40, 167, 69, 0.4);
}
#puzzle-board {
display: grid;
border: 2px solid #333;
background-color: var(--puzzle-background);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
width: var(--puzzle-width);
margin: 2rem auto;
position: relative;
}
#pieces-tray {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-content: flex-start;
height: var(--piece-tray-height);
overflow-y: auto;
padding: 8px;
border: 1px solid #ddd;
background: #f9f9f9;
border-radius: 4px;
}
.puzzle-piece {
border: 1px solid #ccc;
box-shadow: var(--piece-shadow);
border-radius: 3px;
cursor: grab;
/* Using touch-action to prevent scrolling on touch devices when dragging */
touch-action: none;
position: relative;
z-index: 1;
transition: box-shadow 0.2s, transform 0.2s;
}
/* --- POINTER EVENTS DRAG & DROP SYSTEM --- */
.puzzle-piece.is-dragging {
position: fixed;
cursor: grabbing;
opacity: 0.9;
transform: scale(1.05) rotate(2deg);
box-shadow: var(--dragging-shadow);
z-index: 1000;
/* CRITICAL: Prevents the dragged element from blocking pointer events to what's underneath (the drop zones) */
pointer-events: none;
}
.drop-zone {
border: var(--drop-zone-border);
background-color: rgba(0,0,0,0.02);
transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
position: relative;
}
.drop-zone.near {
background-color: var(--drop-zone-near-bg);
border-color: var(--drop-zone-near-border);
border-style: solid;
box-shadow: var(--drop-zone-near-shadow);
}
.puzzle-piece.snapped {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: none;
border: none;
border-radius: 0;
cursor: default;
transform: none;
z-index: 0;
touch-action: auto;
}
</style>
<!-- Completion Modal -->
<div id="win-modal" class="modal-backdrop hidden">
<div class="modal-content">
<button id="close-modal-btn" class="close-btn">&times;</button>
<h2>Complimenti!</h2>
<p>Hai risolto il puzzle!</p>
<p class="score">Punteggio: <strong id="final-score">0</strong></p>
<div class="modal-actions">
<a href="leaderboard.php" class="btn btn-primary">Classifica</a>
<a href="index.php" class="btn btn-secondary">Gioca Ancora</a>
</div>
</div>
</div>
<main class="container mt-4">
<div class="text-center mb-4">
<h1><?php echo $puzzle ? htmlspecialchars($puzzle['name']) : 'Risolvi il Puzzle'; ?></h1>
<div class="d-flex justify-content-center align-items-center gap-3">
<a href="index.php" class="btn btn-secondary btn-sm">Torna alla Galleria</a>
<span class="badge bg-primary">Tempo: <span id="timer">0s</span></span>
<span class="badge bg-info">Mosse: <span id="move-counter">0</span></span>
</div>
</div>
<?php if ($error_message): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error_message); ?></div>
<?php elseif ($puzzle): ?>
<div class="text-center mb-3">
<form method="GET" action="puzzle.php" class="d-inline-flex align-items-center">
<input type="hidden" name="id" value="<?php echo $puzzle_id; ?>">
<label for="difficulty" class="form-label me-2 mb-0">Difficoltà:</label>
<select name="difficulty" id="difficulty" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="16" <?php if ($difficulty == 16) echo 'selected'; ?>>Facile (16 pezzi)</option>
<option value="32" <?php if ($difficulty == 32) echo 'selected'; ?>>Medio (32 pezzi)</option>
<option value="64" <?php if ($difficulty == 64) echo 'selected'; ?>>Difficile (64 pezzi)</option>
</select>
</form>
</div>
<div class="row">
<div class="col-lg-8 col-md-12 mb-3">
<div id="puzzle-board" class="puzzle-board shadow-lg rounded"></div>
</div>
<div class="col-lg-4 col-md-12">
<div class="card p-2">
<h2 class="h5 text-center">I Tuoi Pezzi</h2>
<div id="pieces-tray" class="pieces-tray p-2">
<?php
foreach ($pieces as $i => $piece_path) {
preg_match('/piece_(\d+)_(\d+)\.jpg$/', $piece_path, $matches);
$row = $matches[1];
$col = $matches[2];
echo sprintf(
'<img src="%s" id="piece-%d" class="puzzle-piece" alt="Pezzo del puzzle" draggable="true" data-position="%s">',
htmlspecialchars($piece_path),
$i,
"{$row}-{$col}"
);
}
?>
</div>
</div>
</div>
</div>
<?php endif; ?>
</main>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- ELEMENTI DEL DOM ---
const board = document.getElementById('puzzle-board');
const piecesTray = document.getElementById('pieces-tray');
const winModal = document.getElementById('win-modal');
const closeModalBtn = document.getElementById('close-modal-btn');
const finalScoreEl = document.getElementById('final-score');
const timerEl = document.getElementById('timer');
const moveCounterEl = document.getElementById('move-counter');
// --- COSTANTI DI GIOCO ---
const PUZZLE_ID = <?php echo $puzzle_id; ?>;
const ORIGINAL_IMAGE_PATH = 'uploads/<?php echo $puzzle['file_name']; ?>';
const COLS = <?php echo $cols; ?>;
const ROWS = <?php echo $rows; ?>;
const SNAP_RADIUS = 30; // Increased radius for better usability
// --- STATO DI GIOCO ---
let moves = 0;
let gameStarted = false;
let startTime = 0;
let timerInterval = null;
let dropZones = [];
let pieces = [];
// =========================================================================
// --- NUOVO SISTEMA DRAG & DROP BASATO SU POINTER EVENTS API ---
// =========================================================================
const dragController = {
isDragging: false,
dragElement: null, // L'elemento DOM trascinato
pieceData: null, // L'oggetto pezzo con i suoi dati
offsetX: 0,
offsetY: 0,
lastPointerX: 0,
lastPointerY: 0,
animationFrameId: null,
// Inizializza l'intero sistema
initialize() {
// Eventi universali per mouse e touch
document.addEventListener('pointerdown', this.handlePointerDown.bind(this));
document.addEventListener('pointermove', this.handlePointerMove.bind(this), { passive: false });
document.addEventListener('pointerup', this.handlePointerUp.bind(this));
// Previene comportamenti di default che interferiscono
document.addEventListener('selectstart', (e) => {
if (this.isDragging) e.preventDefault();
});
},
// --- GESTIONE EVENTI POINTER ---
handlePointerDown(e) {
const target = e.target.closest('.puzzle-piece');
if (!target || target.classList.contains('snapped') || this.isDragging) return;
if (!gameStarted) startTimer();
this.isDragging = true;
this.dragElement = target;
this.pieceData = pieces.find(p => p.element === target);
const rect = this.dragElement.getBoundingClientRect();
this.offsetX = e.clientX - rect.left;
this.offsetY = e.clientY - rect.top;
this.dragElement.classList.add('is-dragging');
// Posiziona l'elemento per il trascinamento
this.updatePosition(e.clientX, e.clientY);
},
handlePointerMove(e) {
if (!this.isDragging) return;
e.preventDefault(); // Previene lo scroll su mobile
this.lastPointerX = e.clientX;
this.lastPointerY = e.clientY;
// Usa rAF per un'animazione fluida e performante
if (!this.animationFrameId) {
this.animationFrameId = requestAnimationFrame(() => {
this.updatePosition(this.lastPointerX, this.lastPointerY);
this.checkNearDropZone();
this.animationFrameId = null;
});
}
},
handlePointerUp(e) {
if (!this.isDragging) return;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
const correctDropZone = dropZones.find(zone => zone.dataset.position === this.pieceData.correctPos);
let snapped = false;
// Logica di Snap
if (correctDropZone && correctDropZone.classList.contains('near') && !correctDropZone.hasChildNodes()) {
correctDropZone.appendChild(this.dragElement);
this.dragElement.classList.add('snapped');
this.pieceData.isSnapped = true;
snapped = true;
incrementMoves();
checkWin();
}
// Ritorno al vassoio se non agganciato
if (!snapped) {
piecesTray.appendChild(this.dragElement);
}
// Pulizia finale
this.dragElement.classList.remove('is-dragging');
this.dragElement.style.transform = ''; // Resetta la trasformazione
dropZones.forEach(zone => zone.classList.remove('near'));
// Resetta lo stato del controller
this.isDragging = false;
this.dragElement = null;
this.pieceData = null;
},
// --- FUNZIONI AUSILIARIE ---
updatePosition(x, y) {
if (!this.dragElement) return;
const newX = x - this.offsetX;
const newY = y - this.offsetY;
// Applica la trasformazione per muovere l'elemento
this.dragElement.style.transform = `translate(${newX}px, ${newY}px) scale(1.05) rotate(2deg)`;
},
checkNearDropZone() {
if (!this.dragElement) return;
const pieceRect = this.dragElement.getBoundingClientRect();
const pieceCenterX = pieceRect.left + pieceRect.width / 2;
const pieceCenterY = pieceRect.top + pieceRect.height / 2;
dropZones.forEach(zone => {
if (zone.dataset.position === this.pieceData.correctPos) {
const zoneRect = zone.getBoundingClientRect();
const zoneCenterX = zoneRect.left + zoneRect.width / 2;
const zoneCenterY = zoneRect.top + zoneRect.height / 2;
const distance = Math.hypot(pieceCenterX - zoneCenterX, pieceCenterY - zoneCenterY);
if (distance < SNAP_RADIUS) {
zone.classList.add('near');
} else {
zone.classList.remove('near');
}
} else {
zone.classList.remove('near');
}
});
}
};
// --- IMPOSTAZIONE INIZIALE DEL GIOCO ---
function initializeGame() {
const img = new Image();
img.onload = () => {
board.style.aspectRatio = `${img.naturalWidth} / ${img.naturalHeight}`;
board.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
board.style.gridTemplateRows = `repeat(${ROWS}, 1fr)`;
createDropZones();
createPieces();
dragController.initialize(); // Avvia il controller del drag & drop
};
img.src = ORIGINAL_IMAGE_PATH;
}
function createDropZones() {
board.innerHTML = '';
dropZones = [];
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
const dropZone = document.createElement('div');
dropZone.classList.add('drop-zone');
dropZone.dataset.position = `${y}-${x}`;
board.appendChild(dropZone);
dropZones.push(dropZone);
}
}
}
function createPieces() {
const allPieceElements = document.querySelectorAll('.puzzle-piece');
const trayPieceWidth = Math.min(120, (piecesTray.clientWidth - 40) / 4); // Adatta la larghezza nel vassoio
allPieceElements.forEach(element => {
const piece = {
element: element,
id: element.id,
correctPos: element.dataset.position,
isSnapped: false
};
piece.element.style.width = `${trayPieceWidth}px`;
piece.element.style.height = 'auto';
pieces.push(piece);
});
}
// --- LOGICA DI GIOCO (TIMER, MOSSE, VITTORIA) ---
const startTimer = () => {
if (gameStarted) return;
gameStarted = true;
startTime = Date.now();
timerInterval = setInterval(() => {
timerEl.textContent = `${Math.floor((Date.now() - startTime) / 1000)}s`;
}, 1000);
};
const incrementMoves = () => moveCounterEl.textContent = ++moves;
function checkWin() {
const isComplete = pieces.every(p => p.isSnapped);
if (isComplete) {
clearInterval(timerInterval);
const timeTaken = Math.floor((Date.now() - startTime) / 1000);
saveScoreAndShowModal(timeTaken);
}
}
function saveScoreAndShowModal(timeTaken) {
const formData = new FormData();
formData.append('action', 'save_score');
formData.append('puzzle_id', PUZZLE_ID);
formData.append('time_taken', timeTaken);
formData.append('moves', moves);
fetch('puzzle.php', { method: 'POST', body: formData })
.then(response => response.json())
.then(data => {
finalScoreEl.textContent = data.success ? data.score : 'N/A';
winModal.classList.remove('hidden');
})
.catch(error => {
console.error('Error saving score:', error);
finalScoreEl.textContent = 'Errore';
winModal.classList.remove('hidden');
});
}
closeModalBtn.addEventListener('click', () => winModal.classList.add('hidden'));
winModal.addEventListener('click', e => {
if (e.target === winModal) winModal.classList.add('hidden');
});
// --- AVVIO GIOCO ---
initializeGame();
});
</script>
<?php require_once 'includes/footer.php'; ?>

BIN
puzzles/6/16/piece_0_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
puzzles/6/16/piece_0_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
puzzles/6/16/piece_0_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
puzzles/6/16/piece_0_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
puzzles/6/16/piece_1_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
puzzles/6/16/piece_1_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
puzzles/6/16/piece_1_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
puzzles/6/16/piece_1_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
puzzles/6/16/piece_2_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
puzzles/6/16/piece_2_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
puzzles/6/16/piece_2_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
puzzles/6/16/piece_2_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
puzzles/6/16/piece_3_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
puzzles/6/16/piece_3_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
puzzles/6/16/piece_3_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
puzzles/6/16/piece_3_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
puzzles/6/32/piece_0_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
puzzles/6/32/piece_0_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
puzzles/6/32/piece_0_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
puzzles/6/32/piece_0_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
puzzles/6/32/piece_0_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
puzzles/6/32/piece_0_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
puzzles/6/32/piece_0_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
puzzles/6/32/piece_0_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
puzzles/6/32/piece_1_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
puzzles/6/32/piece_1_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
puzzles/6/32/piece_1_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
puzzles/6/32/piece_1_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
puzzles/6/32/piece_1_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
puzzles/6/32/piece_1_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
puzzles/6/32/piece_1_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
puzzles/6/32/piece_1_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
puzzles/6/32/piece_2_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
puzzles/6/32/piece_2_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
puzzles/6/32/piece_2_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
puzzles/6/32/piece_2_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
puzzles/6/32/piece_2_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
puzzles/6/32/piece_2_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
puzzles/6/32/piece_2_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
puzzles/6/32/piece_2_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
puzzles/6/32/piece_3_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
puzzles/6/32/piece_3_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
puzzles/6/32/piece_3_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
puzzles/6/32/piece_3_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
puzzles/6/32/piece_3_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
puzzles/6/32/piece_3_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
puzzles/6/32/piece_3_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
puzzles/6/32/piece_3_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
puzzles/6/64/piece_0_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
puzzles/6/64/piece_0_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
puzzles/6/64/piece_0_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
puzzles/6/64/piece_0_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
puzzles/6/64/piece_0_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
puzzles/6/64/piece_0_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
puzzles/6/64/piece_0_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
puzzles/6/64/piece_0_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
puzzles/6/64/piece_1_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
puzzles/6/64/piece_1_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
puzzles/6/64/piece_1_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
puzzles/6/64/piece_1_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
puzzles/6/64/piece_1_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
puzzles/6/64/piece_1_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
puzzles/6/64/piece_1_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
puzzles/6/64/piece_1_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
puzzles/6/64/piece_2_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
puzzles/6/64/piece_2_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
puzzles/6/64/piece_2_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
puzzles/6/64/piece_2_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
puzzles/6/64/piece_2_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
puzzles/6/64/piece_2_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
puzzles/6/64/piece_2_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
puzzles/6/64/piece_2_7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
puzzles/6/64/piece_3_0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
puzzles/6/64/piece_3_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
puzzles/6/64/piece_3_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
puzzles/6/64/piece_3_3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
puzzles/6/64/piece_3_4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
puzzles/6/64/piece_3_5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
puzzles/6/64/piece_3_6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show More