Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- File: Index
- <?php
- // index.php – multi-shop landing page
- session_start();
- if (empty($_SESSION['register_captcha'])) {
- $a = random_int(1, 9);
- $b = random_int(1, 9);
- $_SESSION['register_captcha'] = [
- 'question' => "$a + $b",
- 'answer' => (string)($a + $b),
- ];
- }
- require __DIR__ . '/includes/config/db.php';
- require __DIR__ . '/includes/query_counter.php';
- require __DIR__ . '/includes/panel_auth.php';
- ini_set('display_errors', 1);
- error_reporting(E_ALL);
- function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
- // ---------------- CACHING + ERROR HANDLING ----------------
- $cacheFile = __DIR__ . '/cache/shops_cache.json';
- $cacheTime = 10; // seconds
- $cacheDir = dirname($cacheFile);
- if (!is_dir($cacheDir)) {
- @mkdir($cacheDir, 0777, true);
- }
- $shops = [];
- $dbError = null;
- // Try load from cache
- if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
- $json = file_get_contents($cacheFile);
- $data = json_decode($json, true);
- if (is_array($data)) {
- $shops = $data;
- }
- }
- // If cache empty → query DB
- if (!$shops) {
- try {
- qc_increment(); // 1 query
- $stmt = $pdo->query("
- SELECT id, slug, name, logo_path,
- enable_blueprintboard, enable_bundleboard, enable_traitboard, enable_stockboard,
- is_featured, show_on_index
- FROM shops
- WHERE is_active = 1
- AND show_on_index = 1
- ORDER BY is_featured DESC, name ASC
- ");
- $shops = $stmt->fetchAll(PDO::FETCH_ASSOC);
- // Save new cache
- @file_put_contents($cacheFile, json_encode($shops));
- } catch (PDOException $e) {
- if (str_contains($e->getMessage(), 'max_questions')) {
- echo '
- <style>
- body {
- margin: 0;
- padding: 0;
- background: url("assets/images/bg.jpg") no-repeat center center fixed;
- background-size: cover;
- font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
- color: #e5ecff;
- }
- .error-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.65);
- backdrop-filter: blur(3px);
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- padding: 20px;
- }
- .error-box {
- background: rgba(10, 16, 25, 0.85);
- border: 1px solid #2a3546;
- border-radius: 14px;
- padding: 25px 35px;
- max-width: 420px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.5);
- }
- .error-title {
- font-size: 20px;
- font-weight: 600;
- margin-bottom: 10px;
- }
- .error-msg {
- font-size: 14px;
- color: #cbd5e1;
- margin-bottom: 15px;
- line-height: 1.5;
- }
- .error-btn a {
- display: inline-block;
- padding: 10px 18px;
- background: #0f66d6;
- border-radius: 999px;
- color: white;
- font-weight: 600;
- font-size: 14px;
- text-decoration: none;
- }
- .error-btn a:hover {
- background: #1877f2;
- }
- </style>
- <div class="error-overlay">
- <div class="error-box">
- <div class="error-title">⚠️ Too Many Database Requests</div>
- <div class="error-msg">
- The database query limit has been reached temporarily.<br>
- Please try again in a few minutes.
- </div>
- <div class="error-btn">
- <a href="index.php">↺ Retry</a>
- </div>
- </div>
- </div>
- ';
- exit;
- }
- die("Database error: " . htmlspecialchars($e->getMessage()));
- }
- }
- // Split into featured + normal
- $featuredShops = [];
- $normalShops = [];
- foreach ($shops as $shop) {
- if (!empty($shop['is_featured'])) $featuredShops[] = $shop;
- else $normalShops[] = $shop;
- }
- // ---------- MARKETPLACE STATS ----------
- $marketStats = [
- 'shops' => count($shops),
- 'wtb' => 0,
- 'traits' => 0,
- 'creatures' => 0,
- 'blueprints' => 0,
- ];
- try {
- qc_increment();
- $statsStmt = $pdo->query("
- SELECT
- (SELECT COUNT() FROM mp_stockboard ms
- JOIN shops s1 ON s1.id = ms.shop_id
- WHERE s1.is_active = 1
- AND s1.show_on_index = 1) AS creatures_count,
- (SELECT COUNT() FROM mp_traits mt
- JOIN shops s2 ON s2.id = mt.shop_id
- WHERE mt.is_active = 1
- AND s2.is_active = 1
- AND s2.show_on_index = 1) AS traits_count,
- (SELECT COUNT() FROM mp_blueprints mb
- JOIN shops s3 ON s3.id = mb.shop_id
- WHERE s3.is_active = 1
- AND s3.show_on_index = 1) AS blueprints_count,
- (SELECT COUNT() FROM mp_wtb mw
- JOIN shops s4 ON s4.id = mw.shop_id
- WHERE mw.is_open = 1
- AND s4.is_active = 1
- AND s4.show_on_index = 1) AS wtb_count
- ");
- $statsRow = $statsStmt->fetch(PDO::FETCH_ASSOC);
- if ($statsRow) {
- $marketStats['wtb'] = (int)($statsRow['wtb_count'] ?? 0);
- $marketStats['traits'] = (int)($statsRow['traits_count'] ?? 0);
- $marketStats['creatures'] = (int)($statsRow['creatures_count'] ?? 0);
- $marketStats['blueprints'] = (int)($statsRow['blueprints_count'] ?? 0);
- }
- } catch (Throwable $e) {
- // silent fallback
- }
- // ---------- FEATURED SPOTLIGHT ----------
- $spotlightShop = null;
- if (!empty($featuredShops)) {
- $spotlightShop = $featuredShops[array_rand($featuredShops)];
- }
- // ---------- PUBLIC WTB BOARD ----------
- $wtbRows = [];
- try {
- qc_increment();
- $stmt = $pdo->query("
- SELECT
- w.*,
- s.name AS shop_name,
- s.slug AS shop_slug,
- s.discord_user_id
- FROM mp_wtb w
- JOIN shops s ON s.id = w.shop_id
- WHERE w.is_open = 1
- AND s.is_active = 1
- ORDER BY w.created_at DESC
- LIMIT 50
- ");
- $wtbRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- } catch (PDOException $e) {
- $wtbRows = [];
- }
- // ---------- RECENT MARKETPLACE ACTIVITY ----------
- $recentActivity = [];
- try {
- qc_increment();
- $activityStmt = $pdo->query("
- SELECT * FROM (
- SELECT
- 'creature' AS activity_type,
- ms.species AS title,
- s.name AS shop_name,
- s.slug AS shop_slug,
- ms.created_at AS created_at
- FROM mp_stockboard ms
- JOIN shops s ON s.id = ms.shop_id
- WHERE s.is_active = 1
- AND s.show_on_index = 1
- UNION ALL
- SELECT
- 'trait' AS activity_type,
- CONCAT(mt.species, ' • ', mt.trait_name) AS title,
- s.name AS shop_name,
- s.slug AS shop_slug,
- mt.created_at AS created_at
- FROM mp_traits mt
- JOIN shops s ON s.id = mt.shop_id
- WHERE mt.is_active = 1
- AND s.is_active = 1
- AND s.show_on_index = 1
- UNION ALL
- SELECT
- 'blueprint' AS activity_type,
- mb.name AS title,
- s.name AS shop_name,
- s.slug AS shop_slug,
- mb.created_at AS created_at
- FROM mp_blueprints mb
- JOIN shops s ON s.id = mb.shop_id
- WHERE s.is_active = 1
- AND s.show_on_index = 1
- UNION ALL
- SELECT
- 'wtb' AS activity_type,
- mw.title AS title,
- s.name AS shop_name,
- s.slug AS shop_slug,
- mw.created_at AS created_at
- FROM mp_wtb mw
- JOIN shops s ON s.id = mw.shop_id
- WHERE mw.is_open = 1
- AND s.is_active = 1
- AND s.show_on_index = 1
- ) AS activity_feed
- ORDER BY created_at DESC
- LIMIT 10
- ");
- $recentActivity = $activityStmt->fetchAll(PDO::FETCH_ASSOC);
- } catch (Throwable $e) {
- $recentActivity = [];
- }
- // ---------- SELECTED SHOP HANDLING ----------
- $selectedSlug = $_GET['shop'] ?? null;
- $selectedShop = null;
- if ($selectedSlug && $shops) {
- foreach ($shops as $shop) {
- if ($shop['slug'] === $selectedSlug) {
- $selectedShop = $shop;
- break;
- }
- }
- }
- $registerOld = $_SESSION['register_old'] ?? [];
- $loginPrefill = $_SESSION['login_prefill'] ?? '';
- $loggedInUser = panel_current_user($pdo);
- $isLoggedIn = !!$loggedInUser;
- $canAccessShopPanel = false;
- $shopPanelUrl = '/panel.php';
- $userHasShopMembership = false;
- $shopRequestEnabled = true; // admins can later change this in DB/config if you want
- $canRequestShop = false;
- $hasPendingShopRequest = false;
- if ($isLoggedIn) {
- if (panel_is_global_admin($loggedInUser) || (($loggedInUser['role'] ?? '') === 'owner')) {
- $canAccessShopPanel = true;
- $userHasShopMembership = true;
- } else {
- $st = $pdo->prepare('SELECT 1 FROM shop_members WHERE user_id = ? LIMIT 1');
- $st->execute([(int)$loggedInUser['id']]);
- $userHasShopMembership = (bool)$st->fetchColumn();
- $canAccessShopPanel = $userHasShopMembership;
- }
- if (!$userHasShopMembership) {
- $st = $pdo->prepare("
- SELECT 1
- FROM shop_requests
- WHERE user_id = ?
- AND status = 'pending'
- LIMIT 1
- ");
- $st->execute([(int)$loggedInUser['id']]);
- $hasPendingShopRequest = (bool)$st->fetchColumn();
- }
- $canRequestShop = !$userHasShopMembership && !$hasPendingShopRequest && $shopRequestEnabled;
- }
- ?>
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>Ghost Division • Marketplace</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- html, body{
- height: 100%;
- margin: 0;
- padding: 0;
- background: url('assets/images/bg.jpg') no-repeat center center fixed;
- background-size: cover;
- background-color: #0b1016;
- font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
- }
- /* background video */
- #bgVideo{
- position: fixed;
- top: 0; left: 0;
- width: 100%;
- height: 100%;
- object-fit: cover;
- z-index: 0;
- background: #000;
- }
- /* dark overlay */
- .background-overlay{
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.55);
- backdrop-filter: blur(2px);
- z-index: 1;
- }
- /* content */
- .page{
- position: relative;
- z-index: 2;
- min-height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
- padding-top: 60px;
- color: #e8f1ff;
- text-align: center;
- }
- /* global logo */
- .logo-wrapper{ margin-bottom: 28px; }
- .logo{
- width: 250px;
- max-width: 80vw;
- filter: drop-shadow(0 0 18px rgba(0,0,0,0.6));
- }
- /* shop name / subtitle */
- .shop-title{
- font-size: 22px;
- font-weight: 600;
- margin-bottom: 18px;
- }
- .shop-subtitle{
- font-size: 13px;
- color: #b4c2dd;
- margin-bottom: 20px;
- }
- /* main menu (buttons stacked) */
- .menu-wrapper{
- display: flex;
- flex-direction: column;
- gap: 10px;
- width: 320px;
- max-width: 90vw;
- text-align: center;
- }
- /* generic pill button */
- .btn{
- display: block;
- padding: 14px 22px;
- background: #131c25;
- border: 1px solid #1f2a33;
- border-radius: 12px;
- font-size: 15px;
- font-weight: 600;
- letter-spacing: 0.2px;
- color: #e8f1ff;
- text-decoration: none;
- transition: 0.18s ease;
- }
- .btn span.icon{ margin-right: 8px; }
- .btn:hover{
- background: #1b2733;
- border-color: #3b4d60;
- color: #fff;
- box-shadow: 0 0 12px rgba(0,180,255,0.25);
- }
- .btn-outline{ background: rgba(10,15,25,0.85); }
- /* glossy / glass buttons */
- .btn{
- position: relative;
- overflow: hidden;
- background:
- radial-gradient(120% 140% at 10% 0%, rgba(255,255,255,0.16) 0%, rgba(255,255,255,0.06) 45%, rgba(255,255,255,0.03) 100%),
- linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
- border: 1px solid rgba(255,255,255,0.14);
- box-shadow:
- 0 18px 55px rgba(0,0,0,0.55),
- inset 0 1px 0 rgba(255,255,255,0.14),
- inset 0 -1px 0 rgba(0,0,0,0.35);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- }
- .btn::before{
- content:"";
- position:absolute;
- inset:-60% -35% auto -35%;
- height: 160%;
- background: linear-gradient(
- 120deg,
- rgba(255,255,255,0.26),
- rgba(255,255,255,0.06) 55%,
- transparent 75%
- );
- transform: rotate(-8deg);
- opacity: .65;
- pointer-events:none;
- }
- .btn::after{
- content:"";
- position:absolute;
- inset:0;
- background:
- radial-gradient(circle at 20% 15%, rgba(255,255,255,0.10), transparent 35%),
- radial-gradient(circle at 85% 35%, rgba(59,130,246,0.10), transparent 40%);
- opacity: .55;
- pointer-events:none;
- }
- .btn:hover{
- background:
- radial-gradient(120% 140% at 10% 0%, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0.07) 45%, rgba(255,255,255,0.03) 100%),
- linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
- border-color: rgba(96,165,250,0.45);
- box-shadow:
- 0 22px 66px rgba(0,0,0,0.60),
- 0 0 18px rgba(0,180,255,0.16),
- inset 0 1px 0 rgba(255,255,255,0.16),
- inset 0 -1px 0 rgba(0,0,0,0.35);
- }
- .btn-outline{
- background:
- radial-gradient(120% 140% at 10% 0%, rgba(255,255,255,0.14) 0%, rgba(255,255,255,0.05) 45%, rgba(255,255,255,0.02) 100%),
- linear-gradient(180deg, rgba(0,0,0,0.22), rgba(0,0,0,0.10));
- border-color: rgba(255,255,255,0.12);
- }
- @media (prefers-reduced-motion: reduce){
- .btn{ transition: none; }
- }
- /* small "back" link */
- .back-link{
- margin-top: 18px;
- font-size: 13px;
- }
- .back-link a{
- color: #9fb7ff;
- text-decoration: none;
- }
- .back-link a:hover{ text-decoration: underline; }
- /* error box for DB issues */
- .db-error{
- max-width: 420px;
- margin: 0 auto 18px;
- padding: 10px 14px;
- border-radius: 10px;
- background: rgba(127,29,29,0.9);
- border: 1px solid #fecaca;
- color: #fee2e2;
- font-size: 13px;
- }
- /* ===== NEW LAYOUT ===== */
- .home-layout{
- position:relative;
- width:100%;
- max-width:1400px;
- min-height:720px;
- }
- .home-left{
- width:460px;
- margin:0 auto;
- display:flex;
- flex-direction:column;
- gap:16px;
- }
- .home-left{
- width:460px;
- margin:0 auto;
- display:flex;
- flex-direction:column;
- gap:16px;
- align-items:center;
- }
- .home-right{
- position:absolute;
- top:0;
- left:calc(50% + 270px);
- width:460px;
- display:flex;
- align-items:flex-start;
- }
- /* marketplace stats */
- .market-stats{
- display:flex;
- flex-direction:column;
- gap:12px;
- align-items:center;
- width:100%;
- max-width:100%;
- margin:0 auto;
- }
- .market-stats-row{
- display:grid;
- gap:12px;
- justify-content:center;
- width:fit-content;
- max-width:100%;
- }
- .market-stats-top{
- grid-template-columns:repeat(2, minmax(0, 210px));
- }
- .market-stats-bottom{
- grid-template-columns:repeat(3, minmax(0, 160px));
- }
- .stat-card{
- box-sizing:border-box;
- width:100%;
- min-height:88px;
- border-radius:16px;
- padding:14px;
- text-align:center;
- display:flex;
- flex-direction:column;
- align-items:center;
- justify-content:center;
- background: linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
- border:1px solid rgba(255,255,255,0.10);
- box-shadow: 0 18px 55px rgba(0,0,0,0.45);
- }
- .stat-label{
- font-size:11px;
- color:#b4c2dd;
- margin-bottom:4px;
- text-transform:uppercase;
- letter-spacing:.08em;
- }
- .stat-value{
- font-size:22px;
- font-weight:800;
- color:#f8fbff;
- }
- /* spotlight */
- .spotlight-card{
- width:460px;
- max-width:92vw;
- margin:0;
- padding:14px 16px;
- border-radius:18px;
- text-align:center;
- background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
- border:1px solid rgba(250,204,21,.22);
- box-shadow:
- 0 24px 80px rgba(0,0,0,0.55),
- 0 0 18px rgba(250,204,21,.14);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- display:flex;
- flex-direction:column;
- align-items:center;
- }
- .spotlight-kicker{
- font-size:11px;
- color:#fde68a;
- text-transform:uppercase;
- letter-spacing:.08em;
- font-weight:800;
- margin-bottom:6px;
- }
- .spotlight-title{
- font-size:20px;
- font-weight:800;
- margin-bottom:6px;
- color:#fff8dc;
- }
- .spotlight-sub{
- font-size:13px;
- color:#d7e3f8;
- margin-bottom:10px;
- }
- /* activity */
- .activity-panel{
- width:460px;
- max-width:92vw;
- box-sizing:border-box;
- margin:0;
- padding:14px 16px;
- border-radius:18px;
- text-align:left;
- background:linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
- border:1px solid rgba(255,255,255,.10);
- box-shadow:0 24px 80px rgba(0,0,0,0.55);
- backdrop-filter:blur(12px);
- -webkit-backdrop-filter:blur(12px);
- display:flex;
- flex-direction:column;
- height:auto;
- min-height:0;
- }
- .activity-title{
- font-size:15px;
- font-weight:800;
- margin-bottom:10px;
- }
- .activity-list{
- display:flex;
- flex-direction:column;
- gap:8px;
- flex:1 1 auto;
- position:relative;
- }
- .activity-row{
- padding:10px 12px;
- border-radius:12px;
- background:rgba(255,255,255,.04);
- border:1px solid rgba(255,255,255,.06);
- transition:
- transform .38s ease,
- opacity .30s ease,
- background-color .25s ease,
- border-color .25s ease;
- will-change: transform, opacity;
- }
- .activity-row.is-entering{
- animation: activitySlideDown .46s ease;
- }
- .activity-row.is-highlight{
- background:rgba(59,130,246,.10);
- border-color:rgba(96,165,250,.22);
- }
- .activity-row.is-leaving{
- animation: activitySlideUpFade .34s ease forwards;
- }
- @keyframes activitySlideDown{
- from{
- opacity:0;
- transform:translateY(-18px);
- }
- to{
- opacity:1;
- transform:translateY(0);
- }
- }
- @keyframes activitySlideUpFade{
- from{
- opacity:1;
- transform:translateY(0);
- }
- to{
- opacity:0;
- transform:translateY(-16px);
- }
- }
- .activity-main{
- font-size:13px;
- color:#f3f7ff;
- }
- .activity-main a{
- color:#93c5fd;
- text-decoration:none;
- font-weight:700;
- }
- .activity-main a:hover{
- text-decoration:underline;
- }
- .activity-meta{
- margin-top:4px;
- font-size:11px;
- color:#94a3b8;
- }
- .activity-empty{
- font-size:13px;
- color:#94a3b8;
- padding:6px 0 2px;
- }
- /* ===== SHOP PANEL ===== */
- .shop-panel{
- width: 460px;
- max-width: 92vw;
- padding: 16px;
- border-radius: 20px;
- background: linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
- border: 1px solid rgba(255,255,255,0.10);
- box-shadow: 0 32px 110px rgba(0,0,0,0.65);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- display: flex;
- flex-direction: column;
- max-height: 72vh;
- overflow: hidden;
- }
- .shop-panel .wtb-inline{ margin: 0 0 10px; }
- .shop-search-wrap{
- margin: 0 0 12px;
- }
- .shop-search{
- width:100%;
- box-sizing:border-box;
- padding:12px 14px;
- border-radius:14px;
- border:1px solid rgba(255,255,255,.12);
- background:
- linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.04)),
- linear-gradient(180deg, rgba(12,18,30,.88), rgba(5,10,18,.92));
- color:#e8f1ff;
- font-size:14px;
- outline:none;
- box-shadow:
- inset 0 1px 0 rgba(255,255,255,.12),
- inset 0 -1px 0 rgba(0,0,0,.35),
- 0 8px 22px rgba(0,0,0,.28);
- }
- .shop-search::placeholder{
- color:#9fb1cc;
- }
- .shop-search:focus{
- border-color:rgba(96,165,250,.75);
- box-shadow:
- inset 0 1px 0 rgba(255,255,255,.14),
- inset 0 -1px 0 rgba(0,0,0,.35),
- 0 0 0 1px rgba(96,165,250,.35),
- 0 10px 28px rgba(0,0,0,.35);
- }
- .shop-scroll{
- margin-top: 12px;
- flex: 1 1 auto;
- overflow: auto;
- padding-bottom: 8px;
- -webkit-overflow-scrolling: touch;
- }
- .shop-scroll::-webkit-scrollbar{ width: 10px; }
- .shop-scroll::-webkit-scrollbar-track{
- background: rgba(255,255,255,0.04);
- border-radius: 10px;
- }
- .shop-scroll::-webkit-scrollbar-thumb{
- background: rgba(255,255,255,0.14);
- border-radius: 10px;
- }
- .shop-scroll::-webkit-scrollbar-thumb:hover{ background: rgba(255,255,255,0.22); }
- .shop-scroll{
- scrollbar-color: rgba(255,255,255,0.18) rgba(255,255,255,0.05);
- scrollbar-width: thin;
- }
- .shop-empty{
- padding: 14px;
- color: #b4c2dd;
- font-size: 13px;
- text-align: center;
- }
- .shop-grid{
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 8px;
- grid-auto-flow: row;
- }
- .shop-item{
- height: 52px;
- border-radius: 14px;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 12px;
- text-decoration: none;
- color: #e8f1ff;
- background: rgba(0,0,0,0.14);
- border: 1px solid rgba(255,255,255,0.06);
- transition: 0.18s ease;
- position: relative;
- overflow: hidden;
- }
- .shop-item:hover{
- background: rgba(255,255,255,0.05);
- border-color: rgba(96,165,250,0.28);
- box-shadow: 0 0 16px rgba(0,180,255,0.10);
- transform: translateY(-2px);
- }
- .shop-name{
- font-size: 14px;
- font-weight: 650;
- text-align: center;
- width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .shop-hidden{
- display:none !important;
- }
- /* featured */
- .shop-item.is-featured{
- border-color: rgba(250,204,21,0.35);
- box-shadow: 0 0 18px rgba(250,204,21,0.18);
- font-weight:700;
- }
- .shop-item.is-featured::before{
- content:"";
- position:absolute;
- inset:-40%;
- background:
- radial-gradient(circle at 20% 30%, rgba(250,204,21,0.20), transparent 45%),
- radial-gradient(circle at 80% 60%, rgba(255,255,255,0.10), transparent 40%),
- radial-gradient(circle at 50% 90%, rgba(250,204,21,0.14), transparent 50%);
- filter: blur(2px);
- animation: sparkleFloat 5.5s ease-in-out infinite;
- pointer-events:none;
- }
- @keyframes sparkleFloat{
- 0% { transform: translate3d(-1.5%, -1%, 0) scale(1); opacity: .75; }
- 50% { transform: translate3d( 1.5%, 1%, 0) scale(1.02); opacity: 1; }
- 100% { transform: translate3d(-1.5%, -1%, 0) scale(1); opacity: .75; }
- }
- @media (prefers-reduced-motion: reduce){
- .shop-item.is-featured::before{ animation:none; }
- }
- /* .corner-ribbon{
- position: absolute;
- top: 12px;
- left: -38px;
- transform: rotate(-35deg);
- font-size: 10px;
- font-weight: 800;
- text-transform: uppercase;
- letter-spacing: .6px;
- padding: 4px 44px;
- background: linear-gradient(180deg, rgba(250,204,21,0.35), rgba(250,204,21,0.18));
- border: 1px solid rgba(250,204,21,0.45);
- color: #fde68a;
- box-shadow: 0 0 10px rgba(250,204,21,0.25), inset 0 0 6px rgba(255,255,255,0.15);
- pointer-events: none;
- } */
- .corner-ribbon{
- display:none;
- }
- .featured-grid{
- display:grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 10px;
- margin: 10px 0 14px;
- }
- .featured-grid .shop-item{ height: 52px; }
- /* LOGIN BUTTON */
- .top-login{
- position:absolute;
- top:20px;
- right:20px;
- z-index:5;
- }
- .login-btn{
- padding:7px 14px;
- border-radius:999px;
- border:1px solid rgba(255,255,255,.16);
- background:rgba(255,255,255,.05);
- color:#e8f1ff;
- font-weight:700;
- font-size:13px;
- line-height:1.1;
- cursor:pointer;
- backdrop-filter:blur(10px);
- -webkit-backdrop-filter:blur(10px);
- transition:.18s ease;
- }
- .login-btn:hover{
- background:#0f66d6;
- border-color:#0f66d6;
- }
- /* modal backdrop */
- .login-modal{
- position:fixed;
- inset:0;
- background:rgba(0,0,0,.68);
- backdrop-filter:blur(8px);
- -webkit-backdrop-filter:blur(8px);
- display:none;
- align-items:center;
- justify-content:center;
- padding:18px;
- z-index:1000;
- }
- .login-modal.open{
- display:flex;
- }
- /* modal box */
- .login-box{
- width:min(380px, 94vw);
- border-radius:22px;
- padding:22px;
- background:
- linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
- border:1px solid rgba(255,255,255,.12);
- backdrop-filter:blur(14px);
- -webkit-backdrop-filter:blur(14px);
- box-shadow:
- 0 28px 80px rgba(0,0,0,.65),
- inset 0 1px 0 rgba(255,255,255,.08);
- position:relative;
- overflow:hidden;
- }
- .login-box::before{
- content:"";
- position:absolute;
- inset:0;
- border-radius:inherit;
- background:linear-gradient(
- 120deg,
- rgba(255,255,255,.12),
- rgba(255,255,255,0) 40%
- );
- opacity:.22;
- pointer-events:none;
- }
- .login-header{
- display:flex;
- justify-content:space-between;
- align-items:center;
- gap:12px;
- margin-bottom:18px;
- position:relative;
- z-index:1;
- }
- .login-title{
- font-weight:900;
- font-size:18px;
- color:#f3f7ff;
- letter-spacing:.2px;
- }
- .login-close{
- width:36px;
- height:36px;
- border-radius:999px;
- border:1px solid rgba(255,255,255,.14);
- background:rgba(255,255,255,.04);
- color:#f3f7ff;
- font-size:18px;
- line-height:1;
- cursor:pointer;
- transition:.15s ease;
- }
- .login-close:hover{
- background:rgba(255,255,255,.08);
- border-color:rgba(255,255,255,.22);
- }
- /* form */
- .login-form{
- display:flex;
- flex-direction:column;
- gap:12px;
- position:relative;
- z-index:1;
- }
- .login-form input{
- width:100%;
- box-sizing:border-box;
- padding:12px 14px;
- border-radius:12px;
- border:1px solid rgba(255,255,255,.14);
- background:
- linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.04)),
- linear-gradient(180deg, rgba(12,18,30,.88), rgba(5,10,18,.92));
- color:#e8f1ff;
- font-size:14px;
- outline:none;
- backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
- box-shadow:
- inset 0 1px 0 rgba(255,255,255,.18),
- inset 0 -1px 0 rgba(0,0,0,.35),
- 0 6px 16px rgba(0,0,0,.25);
- transition:.18s ease;
- }
- .login-form input:hover{
- border-color:rgba(255,255,255,.22);
- box-shadow:
- inset 0 1px 0 rgba(255,255,255,.20),
- inset 0 -1px 0 rgba(0,0,0,.35),
- 0 8px 20px rgba(0,0,0,.30);
- }
- .login-form input::placeholder{
- color:#9fb1cc;
- }
- .login-form input:focus{
- border-color:rgba(96,165,250,.75);
- background:
- linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.05)),
- linear-gradient(180deg, rgba(12,18,30,.94), rgba(5,10,18,.96));
- box-shadow:
- inset 0 1px 0 rgba(255,255,255,.22),
- inset 0 -1px 0 rgba(0,0,0,.35),
- 0 0 0 1px rgba(96,165,250,.35),
- 0 10px 28px rgba(0,0,0,.35);
- }
- .login-form input:-webkit-autofill,
- .login-form input:-webkit-autofill:hover,
- .login-form input:-webkit-autofill:focus,
- .login-form input:-webkit-autofill:active{
- -webkit-text-fill-color:#e8f1ff;
- caret-color:#e8f1ff;
- border:1px solid rgba(255,255,255,.14);
- -webkit-box-shadow:
- 0 0 0 1000px rgba(18,24,36,.92) inset,
- inset 0 1px 0 rgba(255,255,255,.18),
- inset 0 -1px 0 rgba(0,0,0,.35),
- 0 6px 16px rgba(0,0,0,.25);
- box-shadow:
- 0 0 0 1000px rgba(18,24,36,.92) inset,
- inset 0 1px 0 rgba(255,255,255,.18),
- inset 0 -1px 0 rgba(0,0,0,.35),
- 0 6px 16px rgba(0,0,0,.25);
- -webkit-transition: background-color 9999s ease-out 0s;
- transition: background-color 9999s ease-out 0s;
- }
- .login-submit{
- margin-top:4px;
- width:100%;
- padding:12px 14px;
- border-radius:999px;
- border:none;
- background:#0f66d6;
- color:#fff;
- font-weight:800;
- font-size:14px;
- cursor:pointer;
- transition:.15s ease;
- }
- .login-submit:hover{
- background:#1877f2;
- box-shadow:0 12px 28px rgba(24,119,242,.35);
- }
- .login-footer{
- margin-top:16px;
- font-size:13px;
- color:#b8c6dd;
- text-align:center;
- position:relative;
- z-index:1;
- }
- .login-footer a{
- color:#93c5fd;
- text-decoration:none;
- font-weight:700;
- }
- .login-footer a:hover{
- text-decoration:underline;
- }
- .login-link-btn{
- background:none;
- border:none;
- padding:0;
- margin:0;
- color:#93c5fd;
- font-weight:700;
- font-size:13px;
- cursor:pointer;
- text-decoration:none;
- }
- .login-link-btn:hover{
- text-decoration:underline;
- }
- .login-success{
- margin-bottom:12px;
- padding:10px 12px;
- border-radius:12px;
- background:rgba(22,163,74,.18);
- border:1px solid rgba(34,197,94,.35);
- color:#bbf7d0;
- font-size:13px;
- position:relative;
- z-index:1;
- }
- .top-userbar{
- display:flex;
- align-items:center;
- gap:10px;
- flex-wrap:wrap;
- justify-content:flex-end;
- }
- .top-usertext{
- font-size:13px;
- color:#dbe7ff;
- padding:8px 12px;
- border-radius:999px;
- border:1px solid rgba(255,255,255,.12);
- background:rgba(255,255,255,.04);
- backdrop-filter:blur(10px);
- -webkit-backdrop-filter:blur(10px);
- }
- .panel-btn{
- text-decoration:none;
- display:inline-block;
- }
- .logout-btn{
- padding:7px 12px;
- text-decoration:none;
- display:inline-block;
- border-color:rgba(220,38,38,.45);
- color:#fecaca;
- }
- .logout-btn:hover{
- background:#dc2626;
- border-color:#dc2626;
- color:#fff;
- }
- .top-user-trigger{
- text-align:left;
- }
- .modal-backdrop{
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,.62);
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
- display: none;
- align-items: center;
- justify-content: center;
- padding: 18px;
- z-index: 1100;
- }
- .modal-backdrop.open{
- display:flex;
- }
- .modal-panel{
- width:min(520px, 94vw);
- max-height:88vh;
- overflow:hidden;
- border-radius:18px;
- border:1px solid rgba(255,255,255,.12);
- background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
- box-shadow:0 30px 90px rgba(0,0,0,.65);
- display:flex;
- flex-direction:column;
- }
- .modal-head{
- display:flex;
- align-items:center;
- justify-content:space-between;
- gap:12px;
- padding:14px 16px;
- border-bottom:1px solid rgba(255,255,255,.08);
- background:rgba(0,0,0,.18);
- }
- .modal-title{
- font-size:16px;
- font-weight:900;
- }
- .modal-sub{
- font-size:12px;
- opacity:.75;
- margin-top:2px;
- }
- .modal-close{
- width:36px;
- height:36px;
- border-radius:999px;
- border:1px solid rgba(255,255,255,.14);
- background:rgba(255,255,255,.04);
- color:#e5ecff;
- cursor:pointer;
- font-size:16px;
- font-weight:900;
- }
- .modal-close:hover{
- background:rgba(255,255,255,.08);
- }
- .modal-body{
- padding:16px;
- overflow:auto;
- }
- .setting-group{
- margin-bottom:14px;
- }
- .setting-label{
- font-size:12px;
- font-weight:700;
- opacity:.85;
- margin-bottom:6px;
- text-align:left;
- }
- .setting-input{
- width:100%;
- box-sizing:border-box;
- background:#050a12;
- border:1px solid #283343;
- color:#e5ecff;
- border-radius:10px;
- padding:10px 12px;
- font-size:13px;
- outline:none;
- }
- .setting-input:focus{
- border-color:#3b82f6;
- box-shadow:0 0 0 1px rgba(59,130,246,0.4);
- }
- .modal-actions{
- display:flex;
- gap:10px;
- flex-wrap:wrap;
- margin-top:16px;
- align-items:center;
- justify-content:flex-end;
- }
- .modal-link-btn{
- display:inline-block;
- padding:8px 12px;
- border-radius:999px;
- text-decoration:none;
- font-size:13px;
- font-weight:800;
- color:#fff;
- background:#0f66d6;
- border:1px solid #0f66d6;
- cursor:pointer;
- font-family:inherit;
- }
- .modal-link-btn:hover{
- background:#1877f2;
- border-color:#1877f2;
- }
- .modal-status{
- font-size:13px;
- margin-top:10px;
- opacity:.9;
- text-align:left;
- }
- .modal-status.ok{
- color:#86efac;
- }
- .modal-status.err{
- color:#fca5a5;
- }
- #accountModal .modal-title{
- color:#f3f7ff;
- }
- #accountModal .modal-sub{
- color:#b8c6dd;
- opacity:1;
- }
- #accountModal .setting-label{
- color:#dbe7ff;
- opacity:1;
- }
- #accountModal .setting-input{
- color:#e8f1ff;
- }
- #accountModal .setting-input::placeholder{
- color:#93a6c4;
- }
- #accountModal .modal-body{
- color:#e8f1ff;
- }
- #createShopModal .modal-title{
- color:#f3f7ff;
- }
- #createShopModal .modal-sub{
- color:#b8c6dd;
- opacity:1;
- }
- #createShopModal .setting-label{
- color:#dbe7ff;
- opacity:1;
- }
- #createShopModal .setting-input{
- color:#e8f1ff;
- }
- #createShopModal .setting-input::placeholder{
- color:#93a6c4;
- }
- #createShopModal .modal-body{
- color:#e8f1ff;
- }
- #createShopModal textarea.setting-input{
- min-height:110px;
- resize:vertical;
- }
- #accountModal .setting-input:-webkit-autofill,
- #accountModal .setting-input:-webkit-autofill:hover,
- #accountModal .setting-input:-webkit-autofill:focus,
- #accountModal .setting-input:-webkit-autofill:active{
- -webkit-text-fill-color:#e8f1ff;
- caret-color:#e8f1ff;
- -webkit-box-shadow: 0 0 0 1000px #050a12 inset;
- box-shadow: 0 0 0 1000px #050a12 inset;
- transition: background-color 9999s ease-out 0s;
- }
- /* ===== Responsive ===== */
- @media (max-width: 1250px){
- .home-layout{
- max-width:960px;
- min-height:unset;
- }
- .home-right{
- position:static;
- transform:none;
- width:460px;
- margin:16px auto 0;
- }
- }
- @media (max-width: 900px){
- .shop-grid{
- grid-auto-columns: minmax(160px, 1fr);
- grid-template-rows: repeat(5, 52px);
- }
- }
- @media (max-width: 600px){
- .shop-panel{ max-height: 78vh; }
- .featured-grid{ grid-template-columns: 1fr; }
- .shop-grid{
- grid-auto-flow: row;
- grid-template-columns: 1fr;
- grid-template-rows: none;
- }
- @media (max-width: 700px){
- .market-stats{
- width:100%;
- }
- .market-stats-top,
- .market-stats-bottom{
- grid-template-columns:1fr;
- width:100%;
- }
- .stat-card{
- width:100%;
- }
- .home-left,
- .home-right,
- .activity-panel{
- width:100%;
- max-width:92vw;
- }
- }
- .home-left,
- .home-right,
- .activity-panel{
- width:100%;
- max-width:92vw;
- }
- }
- .pending-btn{
- background: linear-gradient(180deg, rgba(245,158,11,.28), rgba(217,119,6,.20));
- border-color: rgba(251,191,36,.42);
- color: #fde68a;
- cursor: not-allowed;
- box-shadow:
- 0 0 18px rgba(245,158,11,.18),
- inset 0 1px 0 rgba(255,255,255,.08);
- }
- .pending-btn:hover{
- background: linear-gradient(180deg, rgba(245,158,11,.28), rgba(217,119,6,.20));
- border-color: rgba(251,191,36,.42);
- color: #fde68a;
- transform: none;
- box-shadow:
- 0 0 18px rgba(245,158,11,.18),
- inset 0 1px 0 rgba(255,255,255,.08);
- }
- </style>
- </head>
- <body>
- <!-- Video background -->
- <video autoplay muted loop playsinline id="bgVideo">
- <source src="assets/video/bg.mp4" type="video/mp4">
- </video>
- <div class="background-overlay"></div>
- <div class="page">
- <div class="logo-wrapper">
- <img src="assets/images/header.png"
- alt="The Disturbed Cavemen Marketplace"
- class="logo"
- onerror="this.style.display='none'">
- <div class="top-login">
- <?php if (!$isLoggedIn): ?>
- <button class="login-btn" id="openLogin" type="button">Login</button>
- <?php else: ?>
- <div class="top-userbar">
- <button
- type="button"
- class="login-btn top-user-trigger js-open-account"
- data-username="<?php echo h($loggedInUser['username']); ?>"
- data-role="<?php echo h(ucfirst($loggedInUser['role'] ?? 'user')); ?>"
- data-discord-id="<?php echo h($loggedInUser['discord_user_id'] ?? ''); ?>"
- >
- Logged in as <b><?php echo h($loggedInUser['username']); ?></b>
- </button>
- <?php if ($canAccessShopPanel): ?>
- <a class="login-btn panel-btn" href="<?php echo h($shopPanelUrl); ?>">Shop Panel</a>
- <?php endif; ?>
- <?php if ($canRequestShop): ?>
- <button type="button" class="login-btn" id="openShopRequest">Create Shop</button>
- <?php elseif ($hasPendingShopRequest): ?>
- <button type="button" class="login-btn pending-btn" disabled>Request Pending</button>
- <?php endif; ?>
- <a class="login-btn logout-btn" href="/panel_logout.php">Logout</a>
- </div>
- <?php endif; ?>
- </div>
- </div>
- <?php if (!$selectedShop): ?>
- <!-- NO shop selected yet: show list of shops -->
- <div class="shop-subtitle">
- The Unofficial Ghost Division Marketplace.
- </div>
- <div class="home-layout">
- <div class="home-left">
- <div class="market-stats">
- <div class="market-stats-row market-stats-top">
- <div class="stat-card">
- <div class="stat-label">Active Shops</div>
- <div class="stat-value"><?php echo (int)$marketStats['shops']; ?></div>
- </div>
- <div class="stat-card">
- <div class="stat-label">Open WTB Posts</div>
- <div class="stat-value"><?php echo (int)$marketStats['wtb']; ?></div>
- </div>
- </div>
- <div class="market-stats-row market-stats-bottom">
- <div class="stat-card">
- <div class="stat-label">Trait Listings</div>
- <div class="stat-value"><?php echo (int)$marketStats['traits']; ?></div>
- </div>
- <div class="stat-card">
- <div class="stat-label">Creature Listings</div>
- <div class="stat-value"><?php echo (int)$marketStats['creatures']; ?></div>
- </div>
- <div class="stat-card">
- <div class="stat-label">Blueprint Listings</div>
- <div class="stat-value"><?php echo (int)$marketStats['blueprints']; ?></div>
- </div>
- </div>
- </div>
- <?php if ($spotlightShop): ?>
- <div class="spotlight-card">
- <div class="spotlight-kicker">Featured Spotlight</div>
- <div class="spotlight-title"><?php echo h($spotlightShop['name']); ?></div>
- <div class="spotlight-sub">One of the featured shops currently highlighted on the marketplace.</div>
- <a class="btn btn-outline" href="?shop=<?php echo h($spotlightShop['slug']); ?>">
- <span class="icon">⭐</span> Visit featured shop
- </a>
- </div>
- <?php endif; ?>
- <div class="shop-panel" id="shopPanel">
- <div class="wtb-inline">
- <a class="btn btn-outline" href="wtbboard.php">
- <span class="icon">📜</span> Public WTB Board
- </a>
- </div>
- <!-- <div class="shop-search-wrap">
- <input
- type="text"
- id="shopSearch"
- class="shop-search"
- placeholder="Search shops... (press / to focus)"
- autocomplete="off"
- >
- </div> -->
- <div class="shop-scroll">
- <?php if (!empty($featuredShops)): ?>
- <div class="featured-grid" id="featuredGrid">
- <?php foreach ($featuredShops as $shop): ?>
- <?php $name = (string)($shop['name'] ?? ''); ?>
- <a
- class="shop-item is-featured shop-search-item"
- href="?shop=<?php echo h($shop['slug']); ?>"
- data-shop-name="<?php echo h(mb_strtolower($name)); ?>"
- >
- <span class="corner-ribbon">Featured</span>
- <span class="shop-name"><?php echo h($name); ?></span>
- </a>
- <?php endforeach; ?>
- </div>
- <?php endif; ?>
- <div class="shop-grid" id="shopGrid">
- <?php if (!empty($normalShops)): ?>
- <?php foreach ($normalShops as $shop): ?>
- <?php $name = (string)($shop['name'] ?? ''); ?>
- <a
- class="shop-item shop-search-item"
- href="?shop=<?php echo h($shop['slug']); ?>"
- data-shop-name="<?php echo h(mb_strtolower($name)); ?>"
- >
- <span class="shop-name"><?php echo h($name); ?></span>
- </a>
- <?php endforeach; ?>
- <?php elseif (empty($shops)): ?>
- <div class="shop-empty">No shops configured yet.</div>
- <?php endif; ?>
- </div>
- <div class="shop-empty" id="shopSearchEmpty" style="display:none;">
- No shops matched your search.
- </div>
- </div>
- </div>
- </div>
- <div class="home-right">
- <div class="activity-panel">
- <div class="activity-title">Latest Marketplace Activity</div>
- <?php if (!empty($recentActivity)): ?>
- <div class="activity-list" id="activityList">
- <?php foreach ($recentActivity as $a): ?>
- <div class="activity-row">
- <div class="activity-main">
- <?php if (($a['activity_type'] ?? '') === 'creature'): ?>
- 🐲 New creature listing:
- <?php elseif (($a['activity_type'] ?? '') === 'blueprint'): ?>
- 📘 New blueprint listing:
- <?php elseif (($a['activity_type'] ?? '') === 'trait'): ?>
- 🧬 New trait listing:
- <?php else: ?>
- 📜 New WTB listing:
- <?php endif; ?>
- <strong><?php echo h($a['title'] ?? 'Untitled'); ?></strong>
- by
- <a href="?shop=<?php echo h($a['shop_slug'] ?? ''); ?>">
- <?php echo h($a['shop_name'] ?? 'Unknown Shop'); ?>
- </a>
- </div>
- <div class="activity-meta js-local-time"
- data-time="<?php echo h(gmdate('Y-m-d\TH:i:s\Z', strtotime((string)($a['created_at'] ?? '')))); ?>">
- <?php echo h(date('d-m-Y H:i', strtotime((string)($a['created_at'] ?? '')))); ?>
- </div>
- </div>
- <?php endforeach; ?>
- </div>
- <?php else: ?>
- <div class="activity-empty" id="activityEmpty">No recent marketplace activity yet.</div>
- <?php endif; ?>
- </div>
- </div>
- </div>
- <?php else: ?>
- <!-- Shop selected: show its boards -->
- <div class="shop-title">
- <?php echo h($selectedShop['name']); ?>
- </div>
- <div class="shop-subtitle">
- Select what you want to view for this shop.
- </div>
- <div class="menu-wrapper">
- <?php if (!empty($selectedShop['enable_traitboard'])): ?>
- <a class="btn" href="traitsboard.php?shop=<?php echo h($selectedShop['slug']); ?>">
- <span class="icon">🧬</span> Trait Board
- </a>
- <?php endif; ?>
- <?php if (!empty($selectedShop['enable_stockboard'])): ?>
- <a class="btn" href="stockboard.php?shop=<?php echo h($selectedShop['slug']); ?>">
- <span class="icon">🐲</span> Creature Board
- </a>
- <?php endif; ?>
- <?php if (!empty($selectedShop['enable_blueprintboard'])): ?>
- <a class="btn" href="bpboard.php?shop=<?php echo h($selectedShop['slug']); ?>">
- <span class="icon">📘</span> Blueprint Board
- </a>
- <?php endif; ?>
- <?php if (!empty($selectedShop['enable_bundleboard'])): ?>
- <a class="btn" href="bundleboard.php?shop=<?php echo h($selectedShop['slug']); ?>">
- <span class="icon">📦</span> Bundle Board
- </a>
- <?php endif; ?>
- </div>
- <div class="back-link">
- <a href="index.php">← Back to shop list</a>
- </div>
- <?php endif; ?>
- </div>
- <div class="login-modal" id="loginModal">
- <div class="login-box">
- <div class="login-header">
- <div class="login-title">Login</div>
- <button class="login-close" id="closeLogin" type="button">✕</button>
- </div>
- <?php if (!empty($_SESSION['login_success'])): ?>
- <div class="login-success">
- <?php
- echo h($_SESSION['login_success']);
- unset($_SESSION['login_success']);
- ?>
- </div>
- <?php endif; ?>
- <form method="post" action="/panel_login.php" class="login-form">
- <input type="hidden" name="login_from" value="index">
- <input type="text" name="username" placeholder="Username" value="<?php echo h($loginPrefill); ?>" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit" class="login-submit">Login</button>
- </form>
- <div class="login-footer">
- Don’t have an account?
- <button type="button" class="login-link-btn" id="openRegisterFromLogin">Create account</button>
- </div>
- </div>
- </div>
- <div class="login-modal" id="registerModal">
- <div class="login-box">
- <div class="login-header">
- <div class="login-title">Create Account</div>
- <button class="login-close" id="closeRegister" type="button">✕</button>
- </div>
- <?php if (!empty($_SESSION['register_error'])): ?>
- <div class="db-error" style="margin-bottom:12px;">
- <?php
- echo h($_SESSION['register_error']);
- unset($_SESSION['register_error']);
- ?>
- </div>
- <?php endif; ?>
- <form method="post" action="/create_user.php" class="login-form">
- <input type="text" name="username" placeholder="Type Your Survivor Name" value="<?php echo h($registerOld['username'] ?? ''); ?>" required>
- <input type="password" name="password" placeholder="Password" required>
- <input type="password" name="password_confirm" placeholder="Confirm password" required>
- <input
- type="text"
- name="website"
- placeholder="Leave this empty"
- tabindex="-1"
- autocomplete="off"
- style="display:none;"
- >
- <input
- type="text"
- name="captcha"
- placeholder="What is <?php echo h($_SESSION['register_captcha']['question']); ?>?"
- required
- >
- <button type="submit" class="login-submit">Create Account</button>
- </form>
- <div class="login-footer">
- Already have an account?
- <button type="button" class="login-link-btn" id="openLoginFromRegister">Login</button>
- </div>
- </div>
- </div>
- <div class="modal-backdrop" id="accountModal">
- <div class="modal-panel" role="dialog">
- <div class="modal-head">
- <div>
- <div class="modal-title">Account Settings</div>
- <div class="modal-sub" id="accountModalSub"></div>
- </div>
- <button class="modal-close" id="accountModalClose">✕</button>
- </div>
- <div class="modal-body">
- <form id="accountForm">
- <div class="setting-group">
- <div class="setting-label">Discord User ID</div>
- <input
- class="setting-input"
- name="discord_user_id"
- id="accountDiscordId"
- placeholder="Example: 12345678910548458"
- >
- </div>
- <div class="setting-group">
- <div class="setting-label">Current Password</div>
- <input
- class="setting-input"
- type="password"
- name="current_password"
- autocomplete="new-password"
- autocorrect="off"
- autocapitalize="off"
- spellcheck="false"
- >
- </div>
- <div class="setting-group">
- <div class="setting-label">New Password</div>
- <input class="setting-input" type="password" name="new_password">
- </div>
- <div class="setting-group">
- <div class="setting-label">Confirm Password</div>
- <input class="setting-input" type="password" name="confirm_password">
- </div>
- <div class="modal-actions">
- <button type="submit" class="modal-link-btn">Save</button>
- </div>
- <div class="modal-status" id="accountStatus" style="display:none;"></div>
- </form>
- </div>
- </div>
- </div>
- <div class="modal-backdrop" id="createShopModal">
- <div class="modal-panel" role="dialog">
- <div class="modal-head">
- <div>
- <div class="modal-title">Create Shop Request</div>
- <div class="modal-sub">Send a request for a new shop</div>
- </div>
- <button class="modal-close" id="createShopModalClose" type="button">✕</button>
- </div>
- <div class="modal-body">
- <form id="createShopForm">
- <div class="setting-group">
- <div class="setting-label">Shop name</div>
- <input
- class="setting-input"
- type="text"
- name="shop_name"
- id="createShopName"
- placeholder="Example: The Disturbed Cavemen"
- required
- >
- </div>
- <div class="setting-group">
- <div class="setting-label">Preferred slug</div>
- <input
- class="setting-input"
- type="text"
- name="shop_slug"
- id="createShopSlug"
- placeholder="Example: rdy = Ready Or Not | pur = Purple People"
- required
- >
- </div>
- <div class="setting-group">
- <div class="setting-label">Discord Username / Contact</div>
- <input
- class="setting-input"
- type="text"
- name="contact_info"
- id="createShopContact"
- placeholder="Example: Cherie"
- >
- </div>
- <div class="setting-group">
- <div class="setting-label">Request details</div>
- <textarea
- class="setting-input"
- name="request_message"
- id="createShopMessage"
- placeholder="Why do u want a shop here..."
- ></textarea>
- </div>
- <div class="modal-actions">
- <button type="submit" class="modal-link-btn">Send Request</button>
- </div>
- <div class="modal-status" id="createShopStatus" style="display:none;"></div>
- </form>
- </div>
- </div>
- </div>
- <script>
- const vid = document.getElementById('bgVideo');
- if (vid) {
- vid.addEventListener('error', () => {
- vid.style.display = 'none';
- });
- }
- const loginModal = document.getElementById("loginModal");
- const registerModal = document.getElementById("registerModal");
- const openLogin = document.getElementById("openLogin");
- const closeLogin = document.getElementById("closeLogin");
- const closeRegister = document.getElementById("closeRegister");
- const openRegisterFromLogin = document.getElementById("openRegisterFromLogin");
- const openLoginFromRegister = document.getElementById("openLoginFromRegister");
- function openModal(modal){
- modal.classList.add("open");
- }
- function closeModal(modal){
- modal.classList.remove("open");
- }
- function switchModal(fromModal, toModal){
- closeModal(fromModal);
- openModal(toModal);
- }
- if (openLogin) {
- openLogin.onclick = () => openModal(loginModal);
- }
- if (closeLogin) {
- closeLogin.onclick = () => closeModal(loginModal);
- }
- if (closeRegister) {
- closeRegister.onclick = () => closeModal(registerModal);
- }
- if (openRegisterFromLogin) {
- openRegisterFromLogin.onclick = () => switchModal(loginModal, registerModal);
- }
- if (openLoginFromRegister) {
- openLoginFromRegister.onclick = () => switchModal(registerModal, loginModal);
- }
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape") {
- closeModal(loginModal);
- closeModal(registerModal);
- }
- });
- <?php if (!empty($_GET['register'])): ?>
- openModal(registerModal);
- <?php endif; ?>
- <?php if (!empty($_GET['login'])): ?>
- openModal(loginModal);
- <?php endif; ?>
- // shop search
- (() => {
- const searchInput = document.getElementById('shopSearch');
- const items = Array.from(document.querySelectorAll('.shop-search-item'));
- const emptyState = document.getElementById('shopSearchEmpty');
- if (!searchInput || !items.length) return;
- function runSearch() {
- const q = (searchInput.value || '').trim().toLowerCase();
- let visible = 0;
- items.forEach(item => {
- const name = (item.dataset.shopName || '').toLowerCase();
- const show = !q || name.includes(q);
- item.classList.toggle('shop-hidden', !show);
- if (show) visible++;
- });
- if (emptyState) {
- emptyState.style.display = visible ? 'none' : 'block';
- }
- }
- searchInput.addEventListener('input', runSearch);
- document.addEventListener('keydown', (e) => {
- const activeTag = document.activeElement ? document.activeElement.tagName : '';
- if (e.key === '/' && activeTag !== 'INPUT' && activeTag !== 'TEXTAREA') {
- e.preventDefault();
- searchInput.focus();
- searchInput.select();
- }
- });
- })();
- (() => {
- const modal = document.getElementById('accountModal');
- const closeBtn = document.getElementById('accountModalClose');
- const subEl = document.getElementById('accountModalSub');
- const discordInput = document.getElementById('accountDiscordId');
- const form = document.getElementById('accountForm');
- const statusEl = document.getElementById('accountStatus');
- if (!modal || !form) return;
- function openAccountModal(btn) {
- const discordId = btn.dataset.discordId || '';
- subEl.textContent =
- (btn.dataset.username || '') + ' • ' + (btn.dataset.role || 'User');
- discordInput.value = discordId;
- const currentDiscordEl = document.getElementById('accountDiscordCurrent');
- if (currentDiscordEl) {
- if (discordId) {
- currentDiscordEl.textContent = 'Current Discord ID: ' + discordId;
- } else {
- currentDiscordEl.textContent = 'No Discord ID saved yet.';
- }
- }
- statusEl.style.display = 'none';
- statusEl.textContent = '';
- statusEl.className = 'modal-status';
- modal.classList.add('open');
- document.body.style.overflow = 'hidden';
- }
- function closeAccountModal() {
- modal.classList.remove('open');
- document.body.style.overflow = '';
- }
- document.querySelectorAll('.js-open-account').forEach(btn => {
- btn.addEventListener('click', () => openAccountModal(btn));
- });
- if (closeBtn) {
- closeBtn.addEventListener('click', closeAccountModal);
- }
- window.addEventListener('keydown', e => {
- if (e.key === 'Escape' && modal.classList.contains('open')) {
- closeAccountModal();
- }
- });
- form.addEventListener('submit', async (e) => {
- e.preventDefault();
- const formData = new FormData(form);
- const res = await fetch('/api/panel_account_api.php', {
- method: 'POST',
- body: formData
- });
- const text = await res.text();
- statusEl.style.display = 'block';
- if (!res.ok) {
- statusEl.className = 'modal-status err';
- statusEl.textContent = text || 'Could not save settings.';
- return;
- }
- statusEl.className = 'modal-status ok';
- statusEl.textContent = text || 'Account settings updated.';
- });
- })();
- (() => {
- const activityList = document.getElementById('activityList');
- const activityEmpty = document.getElementById('activityEmpty');
- if (!activityList) return;
- let knownKeys = [];
- let firstLoadDone = false;
- let isRendering = false;
- function iconFor(type) {
- if (type === 'creature') return '🐲';
- if (type === 'blueprint') return '📘';
- if (type === 'trait') return '🧬';
- return '📜';
- }
- function labelFor(type) {
- if (type === 'creature') return 'New creature listing:';
- if (type === 'blueprint') return 'New blueprint listing:';
- if (type === 'trait') return 'New trait listing:';
- return 'New WTB listing:';
- }
- function escapeHtml(str) {
- return String(str ?? '')
- .replaceAll('&', '&')
- .replaceAll('<', '<')
- .replaceAll('>', '>')
- .replaceAll('"', '"')
- .replaceAll("'", ''');
- }
- function rowKey(item) {
- return [
- item.activity_type || '',
- item.title || '',
- item.shop_slug || '',
- item.created_at || ''
- ].join('|');
- }
- function formatLocalDate(isoString) {
- if (!isoString) return '';
- const d = new Date(isoString);
- if (isNaN(d.getTime())) return isoString;
- const day = String(d.getDate()).padStart(2, '0');
- const month = String(d.getMonth() + 1).padStart(2, '0');
- const year = d.getFullYear();
- const hours = String(d.getHours()).padStart(2, '0');
- const minutes = String(d.getMinutes()).padStart(2, '0');
- return `${day}-${month}-${year} ${hours}:${minutes}`;
- }
- function applyLocalTimes(scope = document) {
- scope.querySelectorAll('.js-local-time').forEach(el => {
- const raw = el.dataset.time || '';
- el.textContent = formatLocalDate(raw);
- });
- }
- function buildRowHtml(item, extraClass = '') {
- return `
- <div class="activity-row ${extraClass}" data-key="${escapeHtml(rowKey(item))}">
- <div class="activity-main">
- ${iconFor(item.activity_type)} ${labelFor(item.activity_type)}
- <strong>${escapeHtml(item.title || 'Untitled')}</strong>
- by
- <a href="?shop=${encodeURIComponent(item.shop_slug || '')}">
- ${escapeHtml(item.shop_name || 'Unknown Shop')}
- </a>
- </div>
- <div class="activity-meta js-local-time" data-time="${escapeHtml(item.created_at || '')}">
- ${escapeHtml(formatLocalDate(item.created_at || ''))}
- </div>
- </div>
- `;
- }
- function renderInitial(items) {
- if (!Array.isArray(items) || !items.length) {
- activityList.innerHTML = '';
- if (activityEmpty) activityEmpty.style.display = 'block';
- knownKeys = [];
- return;
- }
- if (activityEmpty) activityEmpty.style.display = 'none';
- activityList.innerHTML = items.map(item => buildRowHtml(item)).join('');
- knownKeys = items.map(rowKey);
- applyLocalTimes(activityList);
- }
- function animateUpdate(items) {
- if (isRendering) return;
- isRendering = true;
- if (!Array.isArray(items) || !items.length) {
- activityList.innerHTML = '';
- if (activityEmpty) activityEmpty.style.display = 'block';
- knownKeys = [];
- isRendering = false;
- return;
- }
- if (activityEmpty) activityEmpty.style.display = 'none';
- const newKeys = items.map(rowKey);
- const insertedKeys = newKeys.filter(k => !knownKeys.includes(k));
- const removedKeys = knownKeys.filter(k => !newKeys.includes(k));
- const currentRows = Array.from(activityList.querySelectorAll('.activity-row'));
- const currentTopMap = new Map();
- currentRows.forEach(row => {
- const key = row.dataset.key || '';
- currentTopMap.set(key, row.getBoundingClientRect().top);
- });
- if (removedKeys.length) {
- currentRows.forEach(row => {
- const key = row.dataset.key || '';
- if (removedKeys.includes(key)) {
- row.classList.add('is-leaving');
- }
- });
- }
- setTimeout(() => {
- activityList.innerHTML = items.map(item => {
- const key = rowKey(item);
- const isInserted = insertedKeys.includes(key);
- return buildRowHtml(item, isInserted ? 'is-entering is-highlight' : '');
- }).join('');
- const nextRows = Array.from(activityList.querySelectorAll('.activity-row'));
- nextRows.forEach((row) => {
- const key = row.dataset.key || '';
- const oldTop = currentTopMap.get(key);
- const newTop = row.getBoundingClientRect().top;
- if (oldTop != null) {
- const delta = oldTop - newTop;
- if (delta !== 0) {
- row.style.transform = `translateY(${delta}px)`;
- }
- }
- });
- requestAnimationFrame(() => {
- nextRows.forEach(row => {
- row.style.transform = '';
- });
- });
- nextRows.forEach(row => {
- if (row.classList.contains('is-highlight')) {
- setTimeout(() => row.classList.remove('is-highlight'), 1400);
- }
- if (row.classList.contains('is-entering')) {
- row.addEventListener('animationend', () => {
- row.classList.remove('is-entering');
- }, { once: true });
- }
- });
- applyLocalTimes(activityList);
- knownKeys = newKeys;
- setTimeout(() => {
- isRendering = false;
- }, 500);
- }, removedKeys.length ? 180 : 0);
- }
- async function fetchActivity() {
- try {
- const res = await fetch('/api/latest_activity.php', {
- cache: 'no-store',
- headers: { 'X-Requested-With': 'XMLHttpRequest' }
- });
- if (!res.ok) return;
- const data = await res.json();
- if (!data || !data.ok || !Array.isArray(data.items)) return;
- if (!firstLoadDone) {
- renderInitial(data.items);
- firstLoadDone = true;
- } else {
- animateUpdate(data.items);
- }
- } catch (err) {
- // silent fail
- }
- }
- applyLocalTimes(document);
- fetchActivity();
- setInterval(fetchActivity, 10000);
- })();
- (() => {
- const modal = document.getElementById('createShopModal');
- const openBtn = document.getElementById('openShopRequest');
- const closeBtn = document.getElementById('createShopModalClose');
- const form = document.getElementById('createShopForm');
- const statusEl = document.getElementById('createShopStatus');
- const slugEl = document.getElementById('createShopSlug');
- const nameEl = document.getElementById('createShopName');
- if (!modal || !form || !openBtn) return;
- function slugify(text) {
- return String(text || '')
- .toLowerCase()
- .trim()
- .replace(/[^a-z0-9\s-]/g, '')
- .replace(/\s+/g, '-')
- .replace(/-+/g, '-');
- }
- function openCreateShopModal() {
- statusEl.style.display = 'none';
- statusEl.textContent = '';
- statusEl.className = 'modal-status';
- modal.classList.add('open');
- document.body.style.overflow = 'hidden';
- }
- function closeCreateShopModal() {
- modal.classList.remove('open');
- document.body.style.overflow = '';
- }
- openBtn.addEventListener('click', openCreateShopModal);
- if (closeBtn) {
- closeBtn.addEventListener('click', closeCreateShopModal);
- }
- modal.addEventListener('click', (e) => {
- if (e.target === modal) closeCreateShopModal();
- });
- window.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && modal.classList.contains('open')) {
- closeCreateShopModal();
- }
- });
- if (nameEl && slugEl) {
- nameEl.addEventListener('input', () => {
- if (!slugEl.dataset.userEdited) {
- slugEl.value = slugify(nameEl.value);
- }
- });
- slugEl.addEventListener('input', () => {
- slugEl.dataset.userEdited = '1';
- slugEl.value = slugify(slugEl.value);
- });
- }
- form.addEventListener('submit', async (e) => {
- e.preventDefault();
- statusEl.style.display = 'block';
- statusEl.className = 'modal-status';
- statusEl.textContent = 'Sending request...';
- const formData = new FormData(form);
- try {
- const res = await fetch('/api/create_shop_request.php', {
- method: 'POST',
- body: formData
- });
- const text = await res.text();
- if (!res.ok) {
- statusEl.className = 'modal-status err';
- statusEl.textContent = text || 'Could not send request.';
- return;
- }
- statusEl.className = 'modal-status ok';
- statusEl.textContent = text || 'Shop request sent successfully.';
- form.reset();
- if (slugEl) {
- delete slugEl.dataset.userEdited;
- }
- setTimeout(() => {
- closeCreateShopModal();
- window.location.reload();
- }, 1200);
- } catch (err) {
- statusEl.className = 'modal-status err';
- statusEl.textContent = 'Could not send request.';
- }
- });
- })();
- </script>
- <?php
- // Show debug overlay only when ?debug=1 is in the URL
- if (!empty($_GET['debug'])) {
- require __DIR__ . '/query_debug_overlay.php';
- }
- ?>
- <?php
- unset($_SESSION['register_old'], $_SESSION['login_prefill']);
- ?>
- </body>
- </html>
- File: Latest Activity
- <?php
- declare(strict_types=1);
- require __DIR__ . '/../includes/config/db.php';
- header('Content-Type: application/json; charset=utf-8');
- try {
- $stmt = $pdo->query("
- SELECT * FROM (
- SELECT
- 'creature' AS activity_type,
- ms.species AS title,
- s.name AS shop_name,
- s.slug AS shop_slug,
- ms.created_at AS created_at
- FROM mp_stockboard ms
- JOIN shops s ON s.id = ms.shop_id
- WHERE s.is_active = 1
- AND s.show_on_index = 1
- UNION ALL
- SELECT
- 'trait' AS activity_type,
- CONCAT(mt.species, ' • ', mt.trait_name) AS title,
- s.name AS shop_name,
- s.slug AS shop_slug,
- mt.created_at AS created_at
- FROM mp_traits mt
- JOIN shops s ON s.id = mt.shop_id
- WHERE mt.is_active = 1
- AND s.is_active = 1
- AND s.show_on_index = 1
- UNION ALL
- SELECT
- 'blueprint' AS activity_type,
- mb.name AS title,
- s.name AS shop_name,
- s.slug AS shop_slug,
- mb.created_at AS created_at
- FROM mp_blueprints mb
- JOIN shops s ON s.id = mb.shop_id
- WHERE s.is_active = 1
- AND s.show_on_index = 1
- UNION ALL
- SELECT
- 'wtb' AS activity_type,
- mw.title AS title,
- s.name AS shop_name,
- s.slug AS shop_slug,
- mw.created_at AS created_at
- FROM mp_wtb mw
- JOIN shops s ON s.id = mw.shop_id
- WHERE mw.is_open = 1
- AND s.is_active = 1
- AND s.show_on_index = 1
- ) AS activity_feed
- ORDER BY created_at DESC
- LIMIT 10
- ");
- $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
- foreach ($rows as &$row) {
- $raw = (string)($row['created_at'] ?? '');
- $row['created_at_raw'] = $raw;
- $row['created_at'] = $raw
- ? gmdate('Y-m-d\TH:i:s\Z', strtotime($raw))
- : '';
- }
- unset($row);
- echo json_encode([
- 'ok' => true,
- 'items' => $rows,
- 'server_time' => gmdate('Y-m-d\TH:i:s\Z'),
- ], JSON_UNESCAPED_UNICODE);
- } catch (Throwable $e) {
- http_response_code(500);
- echo json_encode([
- 'ok' => false,
- 'error' => 'Could not load activity.'
- ]);
- }
Advertisement
Add Comment
Please, Sign In to add comment