Aggiunte features ma non funziona trascinamento
90
admin.php
@ -104,8 +104,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_puzzle'])) {
|
|||||||
$upload_path = $upload_dir . $safe_filename;
|
$upload_path = $upload_dir . $safe_filename;
|
||||||
|
|
||||||
if (move_uploaded_file($image_file['tmp_name'], $upload_path)) {
|
if (move_uploaded_file($image_file['tmp_name'], $upload_path)) {
|
||||||
$stmt = $pdo->prepare('INSERT INTO puzzles (name, file_name, pieces, is_public, is_admin_upload) VALUES (?, ?, ?, ?, 1)');
|
$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, $pieces, $is_public]);
|
$stmt->execute([$puzzle_name, $safe_filename, $safe_filename, $pieces, $is_public]);
|
||||||
$puzzle_id = $pdo->lastInsertId();
|
$puzzle_id = $pdo->lastInsertId();
|
||||||
|
|
||||||
create_puzzle_pieces($upload_path, $puzzle_id, $pieces);
|
create_puzzle_pieces($upload_path, $puzzle_id, $pieces);
|
||||||
@ -154,6 +154,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_puzzle'])) {
|
|||||||
exit;
|
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);
|
$puzzles = $pdo->query('SELECT id, name, file_name, pieces, is_public, created_at FROM puzzles ORDER BY created_at DESC')->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@ -172,9 +191,17 @@ require_once 'includes/header.php';
|
|||||||
<?php if (isset($_GET['deleted'])):
|
<?php if (isset($_GET['deleted'])):
|
||||||
?><div class="alert alert-info">Puzzle eliminato con successo.</div>
|
?><div class="alert alert-info">Puzzle eliminato con successo.</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (isset($_GET['error'])):
|
<?php if (isset($_GET['user_deleted'])):
|
||||||
?><div class="alert alert-danger">Errore durante l\'upload. Controlla i dati e riprova.</div>
|
?><div class="alert alert-info">Utente eliminato con successo.</div>
|
||||||
<?php endif; ?>
|
<?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 -->
|
<!-- Upload Form -->
|
||||||
<div class="card p-4 mb-4">
|
<div class="card p-4 mb-4">
|
||||||
@ -237,7 +264,12 @@ require_once 'includes/header.php';
|
|||||||
<td><a href="puzzle.php?id=<?php echo htmlspecialchars($puzzle['id']); ?>" target="_blank"><?php echo htmlspecialchars($puzzle['name']); ?></a></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><img src="<?php echo $imageUrl; ?>" alt="Puzzle" width="100" class="rounded"></td>
|
||||||
<td><?php echo htmlspecialchars($puzzle['pieces']); ?></td>
|
<td><?php echo htmlspecialchars($puzzle['pieces']); ?></td>
|
||||||
<td><?php echo $puzzle['is_public'] ? 'Sì' : 'No'; ?></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><?php echo htmlspecialchars($puzzle['created_at']); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<form method="POST" action="admin.php" onsubmit="return confirm('Sei sicuro di voler eliminare questo puzzle? L\\'azione è irreversibile.');">
|
<form method="POST" action="admin.php" onsubmit="return confirm('Sei sicuro di voler eliminare questo puzzle? L\\'azione è irreversibile.');">
|
||||||
@ -262,11 +294,12 @@ require_once 'includes/header.php';
|
|||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Registrato il</th>
|
<th>Registrato il</th>
|
||||||
|
<th>Azioni</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($users)):
|
<?php if (empty($users)):
|
||||||
?><tr class="text-center"><td colspan="4">Nessun utente trovato.</td></tr>
|
?><tr class="text-center"><td colspan="5">Nessun utente trovato.</td></tr>
|
||||||
<?php else:
|
<?php else:
|
||||||
?><?php foreach ($users as $user):
|
?><?php foreach ($users as $user):
|
||||||
?><tr >
|
?><tr >
|
||||||
@ -274,6 +307,14 @@ require_once 'includes/header.php';
|
|||||||
<td><?php echo htmlspecialchars($user['username']); ?></td>
|
<td><?php echo htmlspecialchars($user['username']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($user['email']); ?></td>
|
<td><?php echo htmlspecialchars($user['email']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($user['created_at']); ?></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>
|
</tr>
|
||||||
<?php endforeach; ?><?php endif; ?>
|
<?php endforeach; ?><?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -283,4 +324,41 @@ require_once 'includes/header.php';
|
|||||||
|
|
||||||
</main>
|
</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'; ?>
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
|
|||||||
51
api/toggle_puzzle_status.php
Normal 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.']);
|
||||||
|
}
|
||||||
@ -26,6 +26,11 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -50,6 +55,24 @@ a:hover {
|
|||||||
transform: translateY(-2px);
|
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 {
|
.form-control {
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
border: 1px solid rgba(6, 182, 212, 0.15);
|
border: 1px solid rgba(6, 182, 212, 0.15);
|
||||||
@ -83,37 +106,52 @@ header.navbar {
|
|||||||
color: var(--accent-glow) !important;
|
color: var(--accent-glow) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MAIN CONTENT */
|
/* LAYOUT & SPACING */
|
||||||
main {
|
.main-content {
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, #131c31 100%);
|
padding: 3rem 2rem;
|
||||||
padding: 40px 24px;
|
max-width: 1400px;
|
||||||
min-height: calc(100vh - 64px);
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.puzzle-grid {
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PUZZLE CARDS */
|
/* PUZZLE CARDS */
|
||||||
.puzzle-gallery .card {
|
.puzzle-card {
|
||||||
background: rgba(30, 41, 59, 0.7);
|
background: rgba(30, 41, 59, 0.7);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
border: 1px solid rgba(6, 182, 212, 0.15);
|
border: 1px solid rgba(6, 182, 212, 0.15);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
overflow: hidden;
|
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-gallery .card:hover {
|
.puzzle-card:hover {
|
||||||
transform: translateY(-4px) scale(1.02);
|
transform: translateY(-6px) scale(1.03);
|
||||||
box-shadow: 0 12px 32px rgba(6, 182, 212, 0.25);
|
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 {
|
.card-title {
|
||||||
color: var(--accent-primary);
|
background: linear-gradient(135deg, #06B6D4, #22D3EE);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-text {
|
.card-text {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body{
|
.card-body {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,9 +164,15 @@ main {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-sidebar h3 {
|
.leaderboard-title {
|
||||||
color: var(--accent-primary);
|
font-size: 1.5rem;
|
||||||
font-family: 'Orbitron', sans-serif;
|
margin-bottom: 1rem;
|
||||||
|
color: #F8FAFC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-subtitle {
|
||||||
|
color: #94A3B8;
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-sidebar table {
|
.leaderboard-sidebar table {
|
||||||
@ -147,23 +191,40 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* PUZZLE PAGE */
|
/* PUZZLE PAGE */
|
||||||
#puzzle-container {
|
#puzzle-board {
|
||||||
border: 2px solid var(--accent-primary);
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.2);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
#piece-container {
|
#pieces-tray {
|
||||||
border: 2px dashed var(--text-secondary);
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: rgba(30, 41, 59, 0.5);
|
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 {
|
.puzzle-piece {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.puzzle-piece:active {
|
.puzzle-piece:active {
|
||||||
@ -171,10 +232,92 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.puzzle-piece:hover {
|
.puzzle-piece:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.05);
|
||||||
box-shadow: 0 0 15px var(--accent-glow);
|
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 {
|
.bg-dark {
|
||||||
background-color: var(--bg-secondary) !important;
|
background-color: var(--bg-secondary) !important;
|
||||||
}
|
}
|
||||||
@ -183,7 +326,7 @@ main {
|
|||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-dark{
|
.border-dark {
|
||||||
border-color: #06b6d4 !important;
|
border-color: #06b6d4 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,3 +334,53 @@ main {
|
|||||||
--bs-table-bg: var(--bg-secondary);
|
--bs-table-bg: var(--bg-secondary);
|
||||||
--bs-table-border-color: var(--accent-primary);
|
--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);
|
||||||
|
}
|
||||||
BIN
assets/pasted-20250928-233347-c38eba48.png
Normal file
|
After Width: | Height: | Size: 539 KiB |
19
index.php
@ -129,19 +129,23 @@ require_once 'includes/header.php';
|
|||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="flex-grow-1">
|
<main class="flex-grow-1">
|
||||||
<div class="container py-5">
|
<div class="main-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Main content (Puzzles) -->
|
<!-- Main content (Puzzles) -->
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<!-- Gallery Section -->
|
<!-- Gallery Section -->
|
||||||
<section class="puzzle-gallery mb-5" aria-labelledby="gallery-heading">
|
<section class="hero-section text-center" aria-labelledby="gallery-heading">
|
||||||
<h1 id="gallery-heading" class="text-center mb-4">Scegli un Puzzle</h1>
|
<h1 id="gallery-heading">Scegli un Puzzle dalla Galleria</h1>
|
||||||
|
<p class="text-secondary fs-5">Oppure crea il tuo e sfida la community.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="puzzle-gallery mb-5" aria-label="Galleria dei Puzzle">
|
||||||
<?php if (!empty($gallery_images)): ?>
|
<?php if (!empty($gallery_images)): ?>
|
||||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-xl-3 g-4">
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-xl-3 puzzle-grid">
|
||||||
<?php foreach ($gallery_images as $img): ?>
|
<?php foreach ($gallery_images as $img): ?>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<a href="puzzle.php?id=<?php echo $img['id']; ?>" class="text-decoration-none">
|
<a href="puzzle.php?id=<?php echo $img['id']; ?>" class="text-decoration-none">
|
||||||
<div class="card h-100">
|
<div class="puzzle-card h-100">
|
||||||
<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;">
|
<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;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title"><?php echo htmlspecialchars($img['name']); ?></h5>
|
<h5 class="card-title"><?php echo htmlspecialchars($img['name']); ?></h5>
|
||||||
@ -194,7 +198,8 @@ require_once 'includes/header.php';
|
|||||||
<!-- Leaderboard Sidebar -->
|
<!-- Leaderboard Sidebar -->
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<aside class="leaderboard-sidebar">
|
<aside class="leaderboard-sidebar">
|
||||||
<h2 class="h4 mb-3">Top 10 Giocatori</h2>
|
<h2 class="leaderboard-title">Top 10 Giocatori</h2>
|
||||||
|
<p class="leaderboard-subtitle mb-3">La classifica dei maestri del puzzle.</p>
|
||||||
<?php if (!empty($leaderboard_scores)): ?>
|
<?php if (!empty($leaderboard_scores)): ?>
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
<thead>
|
<thead>
|
||||||
@ -218,7 +223,7 @@ require_once 'includes/header.php';
|
|||||||
<p class="text-secondary">Nessun punteggio ancora. Inizia a giocare!</p>
|
<p class="text-secondary">Nessun punteggio ancora. Inizia a giocare!</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="text-center mt-3">
|
<div class="text-center mt-3">
|
||||||
<a href="leaderboard.php" class="btn btn-outline-primary btn-sm">Vedi Classifica Completa</a>
|
<a href="leaderboard.php" class="btn btn-outline-cyan">Vedi Classifica Completa</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
405
puzzle.php
@ -126,21 +126,108 @@ require_once 'includes/header.php';
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<style>
|
<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 {
|
#puzzle-board {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(<?php echo $cols; ?>, 1fr);
|
border: 2px solid #333;
|
||||||
grid-template-rows: repeat(<?php echo $rows; ?>, 1fr);
|
background-color: var(--puzzle-background);
|
||||||
aspect-ratio: <?php echo $source_width; ?> / <?php echo $source_height; ?>;
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||||
max-width: 100%;
|
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>
|
</style>
|
||||||
|
|
||||||
<div id="win-message">
|
<!-- Completion Modal -->
|
||||||
|
<div id="win-modal" class="modal-backdrop hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button id="close-modal-btn" class="close-btn">×</button>
|
||||||
<h2>Complimenti!</h2>
|
<h2>Complimenti!</h2>
|
||||||
<p>Hai risolto il puzzle!</p>
|
<p>Hai risolto il puzzle!</p>
|
||||||
<p>Punteggio: <strong id="final-score">0</strong></p>
|
<p class="score">Punteggio: <strong id="final-score">0</strong></p>
|
||||||
<a href="leaderboard.php" class="btn btn-warning">Classifica</a>
|
<div class="modal-actions">
|
||||||
<a href="index.php" class="btn btn-light">Gioca Ancora</a>
|
<a href="leaderboard.php" class="btn btn-primary">Classifica</a>
|
||||||
|
<a href="index.php" class="btn btn-secondary">Gioca Ancora</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="container mt-4">
|
<main class="container mt-4">
|
||||||
@ -176,9 +263,19 @@ require_once 'includes/header.php';
|
|||||||
<div class="card p-2">
|
<div class="card p-2">
|
||||||
<h2 class="h5 text-center">I Tuoi Pezzi</h2>
|
<h2 class="h5 text-center">I Tuoi Pezzi</h2>
|
||||||
<div id="pieces-tray" class="pieces-tray p-2">
|
<div id="pieces-tray" class="pieces-tray p-2">
|
||||||
<?php foreach ($pieces as $piece_path): ?>
|
<?php
|
||||||
<img src="<?php echo htmlspecialchars($piece_path); ?>" class="puzzle-piece" alt="Pezzo del puzzle" draggable="true">
|
foreach ($pieces as $i => $piece_path) {
|
||||||
<?php endforeach; ?>
|
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>
|
||||||
</div>
|
</div>
|
||||||
@ -188,115 +285,259 @@ require_once 'includes/header.php';
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// --- ELEMENTI DEL DOM ---
|
||||||
const board = document.getElementById('puzzle-board');
|
const board = document.getElementById('puzzle-board');
|
||||||
const piecesTray = document.getElementById('pieces-tray');
|
const piecesTray = document.getElementById('pieces-tray');
|
||||||
const pieces = document.querySelectorAll('.puzzle-piece');
|
const winModal = document.getElementById('win-modal');
|
||||||
const cols = <?php echo $cols; ?>;
|
const closeModalBtn = document.getElementById('close-modal-btn');
|
||||||
const rows = <?php echo $rows; ?>;
|
const finalScoreEl = document.getElementById('final-score');
|
||||||
const puzzleId = <?php echo $puzzle_id; ?>;
|
const timerEl = document.getElementById('timer');
|
||||||
|
const moveCounterEl = document.getElementById('move-counter');
|
||||||
|
|
||||||
let draggedPiece = null;
|
// --- 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 moves = 0;
|
||||||
let gameStarted = false;
|
let gameStarted = false;
|
||||||
let startTime = 0;
|
let startTime = 0;
|
||||||
let timerInterval = null;
|
let timerInterval = null;
|
||||||
|
let dropZones = [];
|
||||||
|
let pieces = [];
|
||||||
|
|
||||||
function startTimer() {
|
// =========================================================================
|
||||||
if (gameStarted) return;
|
// --- NUOVO SISTEMA DRAG & DROP BASATO SU POINTER EVENTS API ---
|
||||||
gameStarted = true;
|
// =========================================================================
|
||||||
startTime = Date.now();
|
const dragController = {
|
||||||
timerInterval = setInterval(updateTimer, 1000);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTimer() {
|
const correctDropZone = dropZones.find(zone => zone.dataset.position === this.pieceData.correctPos);
|
||||||
const seconds = Math.floor((Date.now() - startTime) / 1000);
|
let snapped = false;
|
||||||
document.getElementById('timer').textContent = `${seconds}s`;
|
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
function incrementMoves() {
|
// Ritorno al vassoio se non agganciato
|
||||||
moves++;
|
if (!snapped) {
|
||||||
document.getElementById('move-counter').textContent = moves;
|
piecesTray.appendChild(this.dragElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let y = 0; y < rows; y++) {
|
// Pulizia finale
|
||||||
for (let x = 0; x < cols; x++) {
|
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');
|
const dropZone = document.createElement('div');
|
||||||
dropZone.classList.add('drop-zone');
|
dropZone.classList.add('drop-zone');
|
||||||
dropZone.dataset.position = `${y}-${x}`;
|
dropZone.dataset.position = `${y}-${x}`;
|
||||||
board.appendChild(dropZone);
|
board.appendChild(dropZone);
|
||||||
|
dropZones.push(dropZone);
|
||||||
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('hovered'); });
|
|
||||||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('hovered'));
|
|
||||||
dropZone.addEventListener('drop', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('hovered');
|
|
||||||
if (draggedPiece && dropZone.children.length === 0) {
|
|
||||||
startTimer();
|
|
||||||
incrementMoves();
|
|
||||||
dropZone.appendChild(draggedPiece);
|
|
||||||
checkWin();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
piecesTray.addEventListener('dragover', e => e.preventDefault());
|
function createPieces() {
|
||||||
piecesTray.addEventListener('drop', e => {
|
const allPieceElements = document.querySelectorAll('.puzzle-piece');
|
||||||
e.preventDefault();
|
const trayPieceWidth = Math.min(120, (piecesTray.clientWidth - 40) / 4); // Adatta la larghezza nel vassoio
|
||||||
if (draggedPiece) {
|
allPieceElements.forEach(element => {
|
||||||
startTimer();
|
const piece = {
|
||||||
incrementMoves();
|
element: element,
|
||||||
piecesTray.appendChild(draggedPiece);
|
id: element.id,
|
||||||
|
correctPos: element.dataset.position,
|
||||||
|
isSnapped: false
|
||||||
|
};
|
||||||
|
piece.element.style.width = `${trayPieceWidth}px`;
|
||||||
|
piece.element.style.height = 'auto';
|
||||||
|
pieces.push(piece);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
pieces.forEach(piece => {
|
// --- LOGICA DI GIOCO (TIMER, MOSSE, VITTORIA) ---
|
||||||
const match = piece.src.match(/piece_(\d+)_(\d+)\.jpg$/);
|
const startTimer = () => {
|
||||||
if (match) { piece.dataset.position = `${match[1]}-${match[2]}`; }
|
if (gameStarted) return;
|
||||||
|
gameStarted = true;
|
||||||
piece.addEventListener('dragstart', e => {
|
startTime = Date.now();
|
||||||
draggedPiece = e.target;
|
timerInterval = setInterval(() => {
|
||||||
setTimeout(() => { e.target.style.opacity = '0.5'; }, 0);
|
timerEl.textContent = `${Math.floor((Date.now() - startTime) / 1000)}s`;
|
||||||
});
|
}, 1000);
|
||||||
|
};
|
||||||
piece.addEventListener('dragend', e => {
|
const incrementMoves = () => moveCounterEl.textContent = ++moves;
|
||||||
draggedPiece = null;
|
|
||||||
e.target.style.opacity = '1';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function checkWin() {
|
function checkWin() {
|
||||||
const dropZones = board.querySelectorAll('.drop-zone');
|
const isComplete = pieces.every(p => p.isSnapped);
|
||||||
if ([...dropZones].every(zone =>
|
if (isComplete) {
|
||||||
zone.children.length > 0 &&
|
|
||||||
zone.children[0].dataset.position === zone.dataset.position
|
|
||||||
)) {
|
|
||||||
clearInterval(timerInterval);
|
clearInterval(timerInterval);
|
||||||
const timeTaken = Math.floor((Date.now() - startTime) / 1000);
|
const timeTaken = Math.floor((Date.now() - startTime) / 1000);
|
||||||
|
saveScoreAndShowModal(timeTaken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveScoreAndShowModal(timeTaken) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', 'save_score');
|
formData.append('action', 'save_score');
|
||||||
formData.append('puzzle_id', puzzleId);
|
formData.append('puzzle_id', PUZZLE_ID);
|
||||||
formData.append('time_taken', timeTaken);
|
formData.append('time_taken', timeTaken);
|
||||||
formData.append('moves', moves);
|
formData.append('moves', moves);
|
||||||
|
|
||||||
fetch('puzzle.php', {
|
fetch('puzzle.php', { method: 'POST', body: formData })
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
finalScoreEl.textContent = data.success ? data.score : 'N/A';
|
||||||
document.getElementById('final-score').textContent = data.score;
|
winModal.classList.remove('hidden');
|
||||||
document.getElementById('win-message').style.display = 'block';
|
})
|
||||||
board.style.borderColor = 'var(--mavi-orange)';
|
.catch(error => {
|
||||||
} else {
|
console.error('Error saving score:', error);
|
||||||
console.error('Failed to save score:', data.error);
|
finalScoreEl.textContent = 'Errore';
|
||||||
alert('Si è verificato un errore durante il salvataggio del punteggio.');
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
BIN
puzzles/8/16/piece_0_0.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
puzzles/8/16/piece_0_1.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
puzzles/8/16/piece_0_2.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
puzzles/8/16/piece_0_3.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
puzzles/8/16/piece_1_0.jpg
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
puzzles/8/16/piece_1_1.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
puzzles/8/16/piece_1_2.jpg
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
puzzles/8/16/piece_1_3.jpg
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
puzzles/8/16/piece_2_0.jpg
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
puzzles/8/16/piece_2_1.jpg
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
puzzles/8/16/piece_2_2.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
puzzles/8/16/piece_2_3.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
puzzles/8/16/piece_3_0.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
puzzles/8/16/piece_3_1.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
puzzles/8/16/piece_3_2.jpg
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
puzzles/8/16/piece_3_3.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
uploads/puzzle_68d9c91c5bb56.jpg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
uploads/puzzle_68d9c9b5add70.jpg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
uploads/puzzle_68d9c9e58689b.jpg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |