<?php declare(strict_types=1); require __DIR__ . '/../inc/admin_boot.php'; $pdo = Database::pdo(); $err = ''; $msg = ''; /* detect optional is_featured column */ $hasFeatured = false; try { $cols = $pdo->query("SHOW COLUMNS FROM games")->fetchAll(PDO::FETCH_COLUMN); $cols = array_map('strtolower', $cols); $hasFeatured = in_array('is_featured', $cols, true); } catch (Throwable $e) {} /* ensure column hint */ $featuredHint = ''; if (!$hasFeatured) { $featuredHint = "Heads up: your games table has no is_featured column. Run: ALTER TABLE `games` ADD COLUMN `is_featured` TINYINT(1) NOT NULL DEFAULT 0 AFTER `is_published`;"; } /* ------------------------------------------------- EXPORT ALL GAMES (ignores filters & pagination) ------------------------------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['export_all']) && $_POST['export_all'] === '1') { csrf_check(); try { // Determine game columns (excluding id + category_id) $colStmt = $pdo->query("SHOW COLUMNS FROM games"); $gameCols = []; while ($c = $colStmt->fetch(PDO::FETCH_ASSOC)) { $name = (string)$c['Field']; if ($name === 'id' || $name === 'category_id') { continue; } $gameCols[] = $name; } // Fetch ALL games with category name $st = $pdo->query(" SELECT g.*, c.name AS category_name FROM games g LEFT JOIN categories c ON c.id = g.category_id ORDER BY g.id ASC "); $games = []; while ($row = $st->fetch(PDO::FETCH_ASSOC)) { $fields = []; foreach ($gameCols as $col) { if (array_key_exists($col, $row)) { $fields[$col] = $row[$col]; } } $games[] = [ 'fields' => $fields, 'category_name' => $row['category_name'] ?? '', ]; } $payload = [ 'export_type' => 'games', 'scope' => 'all', 'exported_at' => gmdate('c'), 'source_host' => $_SERVER['HTTP_HOST'] ?? '', 'games_count' => count($games), 'games' => $games, ]; header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename="games-export-ALL-' . date('Ymd-His') . '.json"'); echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } catch (Throwable $e) { $err = 'Export ALL failed: ' . $e->getMessage(); } } /* ------------------------------------------------- BULK ACTIONS (selected rows) ------------------------------------------------- */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bulk_action'])) { csrf_check(); $ids = array_filter(array_map('intval', $_POST['ids'] ?? [])); $act = $_POST['bulk_action'] ?? ''; if ($ids) { $ph = implode(',', array_fill(0, count($ids), '?')); /* Export selected games as JSON for import on another installation */ if ($act === 'export_json') { try { // Determine game columns (excluding id + category_id) $colStmt = $pdo->query("SHOW COLUMNS FROM games"); $gameCols = []; while ($c = $colStmt->fetch(PDO::FETCH_ASSOC)) { $name = (string)$c['Field']; if ($name === 'id' || $name === 'category_id') { continue; } $gameCols[] = $name; } // Fetch selected games with category name $st = $pdo->prepare(" SELECT g.*, c.name AS category_name FROM games g LEFT JOIN categories c ON c.id = g.category_id WHERE g.id IN ($ph) "); $st->execute($ids); $games = []; while ($row = $st->fetch(PDO::FETCH_ASSOC)) { $fields = []; foreach ($gameCols as $col) { if (array_key_exists($col, $row)) { $fields[$col] = $row[$col]; } } $games[] = [ 'fields' => $fields, 'category_name' => $row['category_name'] ?? '', ]; } $payload = [ 'export_type' => 'games', 'scope' => 'selected', 'exported_at' => gmdate('c'), 'source_host' => $_SERVER['HTTP_HOST'] ?? '', 'games_count' => count($games), 'games' => $games, ]; header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename="games-export-' . date('Ymd-His') . '.json"'); echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } catch (Throwable $e) { $err = 'Export failed: ' . $e->getMessage(); } } // Normal bulk actions try { if ($act === 'publish') { $st = $pdo->prepare("UPDATE games SET is_published=1,updated_at=NOW() WHERE id IN ($ph)"); $st->execute($ids); $msg = "Published " . $st->rowCount() . " game(s)."; } elseif ($act === 'unpublish') { $st = $pdo->prepare("UPDATE games SET is_published=0,updated_at=NOW() WHERE id IN ($ph)"); $st->execute($ids); $msg = "Unpublished " . $st->rowCount() . " game(s)."; } elseif ($act === 'delete') { $st = $pdo->prepare("DELETE FROM games WHERE id IN ($ph)"); $st->execute($ids); $msg = "Deleted " . $st->rowCount() . " game(s)."; } elseif ($act === 'feature_on' && $hasFeatured) { $st = $pdo->prepare("UPDATE games SET is_featured=1,updated_at=NOW() WHERE id IN ($ph)"); $st->execute($ids); $msg = "Set Featured on " . $st->rowCount() . " game(s)."; } elseif ($act === 'feature_off' && $hasFeatured) { $st = $pdo->prepare("UPDATE games SET is_featured=0,updated_at=NOW() WHERE id IN ($ph)"); $st->execute($ids); $msg = "Removed Featured on " . $st->rowCount() . " game(s)."; } else { if ($act !== 'export_json') { $err = 'Unknown action or missing is_featured column.'; } } } catch (Throwable $e) { $err = $e->getMessage(); } } else { $err = 'No rows selected.'; } } /* single-row feature toggle (legacy POST: feature_id + feature_set) Kept for backward-compatibility (non-JS). The UI below now uses AJAX. */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['feature_id']) && $hasFeatured) { csrf_check(); $gid = (int)($_POST['feature_id'] ?? 0); $set = (int)($_POST['feature_set'] ?? 0); if ($gid > 0) { $st = $pdo->prepare("UPDATE games SET is_featured=:s,updated_at=NOW() WHERE id=:id"); $st->execute([':s' => $set, ':id' => $gid]); $msg = $set ? 'Marked as featured.' : 'Removed from featured.'; } } /* FILTERS */ $q = trim((string)($_GET['q'] ?? '')); $cat = (int)($_GET['cat'] ?? 0); $prov = trim((string)($_GET['provider'] ?? '')); $status = $_GET['status'] ?? 'any'; // any|1|0 $feat = $_GET['feat'] ?? 'any'; // any|1|0 $sort = $_GET['sort'] ?? 'new'; // new|name|popular|featured $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = min(100, max(10, (int)($_GET['per'] ?? 30))); $where = ['1=1']; $args = []; /* SAFE SEARCH (unique placeholders per column to avoid PDO duplicate-name issue) */ if ($q !== '') { $like = "%{$q}%"; $searchCols = ['g.name', 'g.slug', 'g.description', 'g.external_id']; $ors = []; $i = 0; foreach ($searchCols as $col) { $i++; $ph = ":q{$i}"; $ors[] = "$col LIKE $ph"; $args[$ph] = $like; } $where[] = '(' . implode(' OR ', $ors) . ')'; } if ($cat > 0) { $where[] = 'g.category_id=:cat'; $args[':cat'] = $cat; } if ($prov !== '') { $where[] = 'g.provider=:prov'; $args[':prov'] = $prov; } if ($status === '1' || $status === '0') { $where[] = 'g.is_published=:st'; $args[':st'] = (int)$status; } if ($hasFeatured && ($feat === '1' || $feat === '0')) { $where[] = 'g.is_featured=:ft'; $args[':ft'] = (int)$feat; } $orderSql = 'g.created_at DESC'; switch ($sort) { case 'name': $orderSql = 'g.name ASC'; break; case 'popular': $orderSql = 'g.plays DESC, g.created_at DESC'; break; case 'featured': $orderSql = $hasFeatured ? 'g.is_featured DESC, g.created_at DESC' : 'g.created_at DESC'; break; default: $orderSql = 'g.created_at DESC'; } $whereSql = 'WHERE ' . implode(' AND ', $where); /* COUNT */ $st = $pdo->prepare("SELECT COUNT(*) FROM games g $whereSql"); $st->execute($args); $total = (int)$st->fetchColumn(); $pages = max(1, (int)ceil($total / $perPage)); $offset = ($page - 1) * $perPage; /* FETCH */ $selectFeatured = $hasFeatured ? ', g.is_featured' : ''; $sql = "SELECT g.id,g.name,g.slug,g.provider,g.external_id,g.is_published$selectFeatured, g.thumb_url,g.category_id,c.name AS category_name,g.created_at,g.updated_at,g.plays FROM games g LEFT JOIN categories c ON c.id=g.category_id $whereSql ORDER BY $orderSql LIMIT :lim OFFSET :off"; $st = $pdo->prepare($sql); foreach ($args as $k => $v) { $st->bindValue($k, $v); } $st->bindValue(':lim', $perPage, PDO::PARAM_INT); $st->bindValue(':off', $offset, PDO::PARAM_INT); $st->execute(); $rows = $st->fetchAll(PDO::FETCH_ASSOC); /* FILTER DATA */ $cats = $pdo->query("SELECT id,name FROM categories ORDER BY name ASC")->fetchAll(PDO::FETCH_ASSOC); $providers = $pdo->query("SELECT DISTINCT provider FROM games WHERE provider IS NOT NULL AND provider<>'' ORDER BY provider ASC")->fetchAll(PDO::FETCH_COLUMN); require __DIR__ . '/../inc/header.php'; ?> <!-- Expose CSRF for AJAX --> <meta name="csrf-token" content="<?= h($_SESSION['csrf'] ?? ($_SESSION['csrf_token'] ?? '')) ?>"> <section class="section"> <div class="container"> <div class="level"> <div class="level-left"><h1 class="title">Manage Games</h1></div> <div class="level-right" style="gap:0.5rem; align-items:center;"> <a class="button is-primary is-light" href="/admin/games/edit.php">Add Game</a> <a class="button is-link is-light" href="/admin/games/externalimport.php">External Import</a> <!-- Export ALL Games (separate POST form) --> <form method="post" style="margin-left:0.5rem;"> <?php csrf_field(); ?> <input type="hidden" name="export_all" value="1"> <button class="button is-success is-light" type="submit"> Export ALL Games </button> </form> </div> </div> <?php if ($featuredHint): ?> <article class="message is-warning"> <div class="message-body"><?= nl2br(h($featuredHint)) ?></div> </article> <?php endif; ?> <?php if ($msg): ?><div class="notification is-success"><?= h($msg) ?></div><?php endif; ?> <?php if ($err): ?><div class="notification is-danger"><?= h($err) ?></div><?php endif; ?> <!-- Filters --> <form method="get" class="box" style="border-radius:12px;"> <div class="columns is-multiline"> <div class="column is-4"> <label class="label">Search</label> <input class="input" name="q" value="<?= h($q) ?>" placeholder="Name, slug, external idβ¦"> </div> <div class="column is-3"> <label class="label">Category</label> <div class="select is-fullwidth"> <select name="cat"> <option value="0">All</option> <?php foreach ($cats as $c): ?> <option value="<?= (int)$c['id'] ?>" <?= $cat === (int)$c['id'] ? 'selected' : ''; ?>><?= h($c['name']) ?></option> <?php endforeach; ?> </select> </div> </div> <div class="column is-2"> <label class="label">Provider</label> <div class="select is-fullwidth"> <select name="provider"> <option value="">All</option> <?php foreach ($providers as $p): ?> <option value="<?= h($p) ?>" <?= $prov === $p ? 'selected' : ''; ?>><?= h($p) ?></option> <?php endforeach; ?> </select> </div> </div> <div class="column is-1"> <label class="label">Status</label> <div class="select is-fullwidth"> <select name="status"> <option value="any" <?= $status === 'any' ? 'selected' : ''; ?>>Any</option> <option value="1" <?= $status === '1' ? 'selected' : ''; ?>>Published</option> <option value="0" <?= $status === '0' ? 'selected' : ''; ?>>Hidden</option> </select> </div> </div> <div class="column is-1"> <label class="label">Featured</label> <div class="select is-fullwidth"> <select name="feat" <?= $hasFeatured ? '' : 'disabled'; ?>> <option value="any" <?= $feat === 'any' ? 'selected' : ''; ?>>Any</option> <option value="1" <?= $feat === '1' ? 'selected' : ''; ?>>Yes</option> <option value="0" <?= $feat === '0' ? 'selected' : ''; ?>>No</option> </select> </div> </div> <div class="column is-1"> <label class="label">Sort</label> <div class="select is-fullwidth"> <select name="sort"> <option value="new" <?= $sort === 'new' ? 'selected' : ''; ?>>New</option> <option value="popular" <?= $sort === 'popular' ? 'selected' : ''; ?>>Popular</option> <option value="name" <?= $sort === 'name' ? 'selected' : ''; ?>>AβZ</option> <option value="featured" <?= $sort === 'featured' ? 'selected' : ''; ?> <?= $hasFeatured ? '' : 'disabled'; ?>>Featured</option> </select> </div> </div> <div class="column is-1"> <label class="label">Per</label> <div class="select is-fullwidth"> <select name="per"> <?php foreach ([10, 20, 30, 50, 100] as $pp): ?> <option value="<?= $pp ?>" <?= $perPage === $pp ? 'selected' : ''; ?>><?= $pp ?></option> <?php endforeach; ?> </select> </div> </div> <div class="column is-12"> <button class="button is-primary">Apply</button> <a class="button is-light" href="/admin/games/manage.php">Reset</a> <span class="ml-3 has-text-grey">Found <strong><?= number_format($total) ?></strong> result(s)</span> </div> </div> </form> <!-- Bulk actions (selected rows) --> <form method="post" id="games-bulk-form"> <?php csrf_field(); ?> <div class="level"> <div class="level-left"> <div class="field has-addons"> <div class="control"> <div class="select"> <select name="bulk_action" required> <option value="">Bulk actionβ¦</option> <option value="publish">Publish</option> <option value="unpublish">Unpublish</option> <option value="delete">Delete</option> <?php if ($hasFeatured): ?> <option value="feature_on">Set Featured</option> <option value="feature_off">Remove Featured</option> <?php endif; ?> <option value="export_json">Export as JSON (selected)</option> </select> </div> </div> <div class="control"> <button class="button is-danger" type="submit">Apply</button> </div> </div> </div> <div class="level-right"> <!-- Optionally add a separate "Export Selected" button here if you want --> </div> </div> <div class="table-container"> <table class="table is-striped is-hoverable is-fullwidth"> <thead> <tr> <th><label class="checkbox"><input type="checkbox" id="check-all"></label></th> <th>Game</th> <th>Provider / Ext.ID</th> <th>Category</th> <?php if ($hasFeatured): ?><th>Featured</th><?php endif; ?> <th>Status</th> <th>Plays</th> <th>Created</th> <th style="width:160px;">Actions</th> </tr> </thead> <tbody> <?php if (!$rows): ?> <tr> <td colspan="<?= $hasFeatured ? 9 : 8 ?>" class="has-text-centered has-text-grey">No games.</td> </tr> <?php else: foreach ($rows as $r): ?> <tr> <td> <label class="checkbox"> <input type="checkbox" name="ids[]" value="<?= (int)$r['id'] ?>"> </label> </td> <td> <div style="display:flex; gap:10px; align-items:center;"> <img src="<?= h($r['thumb_url'] ?: '/assets/images/placeholder-thumb.jpg') ?>" alt="" style="width:56px;height:56px;object-fit:cover;border-radius:8px;"> <div> <strong><?= h($r['name']) ?></strong><br> <span class="is-size-7 has-text-grey"><?= h($r['slug']) ?></span> </div> </div> </td> <td> <span class="tag is-light"><?= h($r['provider'] ?: 'β') ?></span><br> <span class="is-size-7"><?= h($r['external_id'] ?: 'β') ?></span> </td> <td><?= $r['category_name'] ? h($r['category_name']) : 'β' ?></td> <?php if ($hasFeatured): ?> <td> <?php $f = (int)($r['is_featured'] ?? 0); ?> <button class="button is-small js-toggle-feature <?= $f ? 'is-warning is-light' : '' ?>" data-id="<?= (int)$r['id'] ?>" data-set="<?= $f ? 0 : 1 ?>" title="<?= $f ? 'Click to unfeature' : 'Click to feature' ?>" type="button" > <?= $f ? 'β Featured' : 'β Feature' ?> </button> </td> <?php endif; ?> <td> <?php if ((int)$r['is_published'] === 1): ?> <span class="tag is-success">Published</span> <?php else: ?> <span class="tag is-warning">Hidden</span> <?php endif; ?> </td> <td><?= (int)$r['plays'] ?></td> <td class="is-size-7"><?= h($r['created_at']) ?></td> <td> <div class="buttons are-small"> <a class="button is-link is-light" href="/pages/game.php?slug=<?= urlencode($r['slug']) ?>" target="_blank">View</a> <a class="button is-info is-light" href="/admin/games/edit.php?id=<?= (int)$r['id'] ?>">Edit</a> </div> </td> </tr> <?php endforeach; endif; ?> </tbody> </table> </div> </form> <!-- Pagination --> <?php if ($pages > 1): ?> <nav class="pagination is-centered mt-4" role="navigation" aria-label="pagination"> <?php $base = $_GET; $prev = $page > 1 ? array_merge($base, ['page' => $page - 1]) : null; $next = $page < $pages ? array_merge($base, ['page' => $page + 1]) : null; $mk = function ($p) use ($base) { $x = $base; $x['page'] = $p; return '?' . http_build_query($x); }; ?> <a class="pagination-previous" <?= $page <= 1 ? 'disabled' : ''; ?> href="<?= $prev ? ('?' . http_build_query($prev)) : '#'; ?>">Previous</a> <a class="pagination-next" <?= $page >= $pages ? 'disabled' : ''; ?> href="<?= $next ? ('?' . http_build_query($next)) : '#'; ?>">Next page</a> <ul class="pagination-list"> <?php $win = 2; $start = max(1, $page - $win); $end = min($pages, $page + $win); if ($start > 1) { echo '<li><a class="pagination-link" href="' . $mk(1) . '">1</a></li>'; if ($start > 2) { echo '<li><span class="pagination-ellipsis">…</span></li>'; } } for ($p = $start; $p <= $end; $p++) { $cur = $p === $page ? ' is-current' : ''; echo '<li><a class="pagination-link' . $cur . '" href="' . $mk($p) . '">' . $p . '</a></li>'; } if ($end < $pages) { if ($end < $pages - 1) { echo '<li><span class="pagination-ellipsis">…</span></li>'; } echo '<li><a class="pagination-link" href="' . $mk($pages) . '">' . $pages . '</a></li>'; } ?> </ul> </nav> <?php endif; ?> </div> </section> <?php require __DIR__ . '/../inc/footer.php'; ?> <script> // Master "check all" checkbox document.addEventListener('DOMContentLoaded', function () { const chkAll = document.getElementById('check-all'); if (chkAll) { chkAll.addEventListener('change', function () { const checked = chkAll.checked; document.querySelectorAll('input[name="ids[]"]').forEach(function (cb) { cb.checked = checked; }); }); } }); // AJAX feature toggle (function () { const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; function setBtnLoading(btn, yes) { btn.disabled = !!yes; if (yes) { btn.dataset._text = btn.textContent; btn.textContent = 'β¦'; } else if (btn.dataset._text) { btn.textContent = btn.dataset._text; delete btn.dataset._text; } } async function toggleFeature(btn) { const id = btn.getAttribute('data-id'); const set = btn.getAttribute('data-set'); if (!id || (set !== '0' && set !== '1')) return; setBtnLoading(btn, true); try { const res = await fetch('/admin/games/feature_toggle.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ id, set, csrf: token }) }); const json = await res.json(); if (!json.ok) throw new Error(json.error || 'Request failed'); const featured = Number(json.is_featured) === 1; btn.classList.toggle('is-warning', featured); btn.classList.add('is-light'); btn.textContent = featured ? 'β Featured' : 'β Feature'; btn.setAttribute('data-set', featured ? '0' : '1'); btn.title = featured ? 'Click to unfeature' : 'Click to feature'; } catch (err) { alert('Failed: ' + err.message); } finally { setBtnLoading(btn, false); } } document.addEventListener('click', function (e) { const btn = e.target.closest('.js-toggle-feature'); if (!btn) return; e.preventDefault(); toggleFeature(btn); }); })(); </script>