ScaryScoope

Untitled

Mar 15th, 2026 (edited)
84
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 66.69 KB | None | 0 0
  1. File: Index
  2.  
  3. <?php
  4. // index.php – multi-shop landing page
  5. session_start();
  6.  
  7. if (empty($_SESSION['register_captcha'])) {
  8. $a = random_int(1, 9);
  9. $b = random_int(1, 9);
  10. $_SESSION['register_captcha'] = [
  11. 'question' => "$a + $b",
  12. 'answer' => (string)($a + $b),
  13. ];
  14. }
  15.  
  16. require __DIR__ . '/includes/config/db.php';
  17. require __DIR__ . '/includes/query_counter.php';
  18. require __DIR__ . '/includes/panel_auth.php';
  19.  
  20. ini_set('display_errors', 1);
  21. error_reporting(E_ALL);
  22.  
  23. function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
  24.  
  25. // ---------------- CACHING + ERROR HANDLING ----------------
  26. $cacheFile = __DIR__ . '/cache/shops_cache.json';
  27. $cacheTime = 10; // seconds
  28.  
  29. $cacheDir = dirname($cacheFile);
  30. if (!is_dir($cacheDir)) {
  31. @mkdir($cacheDir, 0777, true);
  32. }
  33.  
  34. $shops = [];
  35. $dbError = null;
  36.  
  37. // Try load from cache
  38. if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
  39. $json = file_get_contents($cacheFile);
  40. $data = json_decode($json, true);
  41. if (is_array($data)) {
  42. $shops = $data;
  43. }
  44. }
  45.  
  46. // If cache empty → query DB
  47. if (!$shops) {
  48. try {
  49. qc_increment(); // 1 query
  50. $stmt = $pdo->query("
  51. SELECT id, slug, name, logo_path,
  52. enable_blueprintboard, enable_bundleboard, enable_traitboard, enable_stockboard,
  53. is_featured, show_on_index
  54. FROM shops
  55. WHERE is_active = 1
  56. AND show_on_index = 1
  57. ORDER BY is_featured DESC, name ASC
  58. ");
  59.  
  60. $shops = $stmt->fetchAll(PDO::FETCH_ASSOC);
  61.  
  62. // Save new cache
  63. @file_put_contents($cacheFile, json_encode($shops));
  64.  
  65. } catch (PDOException $e) {
  66.  
  67. if (str_contains($e->getMessage(), 'max_questions')) {
  68.  
  69. echo '
  70. <style>
  71. body {
  72. margin: 0;
  73. padding: 0;
  74. background: url("assets/images/bg.jpg") no-repeat center center fixed;
  75. background-size: cover;
  76. font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  77. color: #e5ecff;
  78. }
  79. .error-overlay {
  80. position: fixed;
  81. inset: 0;
  82. background: rgba(0, 0, 0, 0.65);
  83. backdrop-filter: blur(3px);
  84. display: flex;
  85. justify-content: center;
  86. align-items: center;
  87. text-align: center;
  88. padding: 20px;
  89. }
  90. .error-box {
  91. background: rgba(10, 16, 25, 0.85);
  92. border: 1px solid #2a3546;
  93. border-radius: 14px;
  94. padding: 25px 35px;
  95. max-width: 420px;
  96. box-shadow: 0 10px 40px rgba(0,0,0,0.5);
  97. }
  98. .error-title {
  99. font-size: 20px;
  100. font-weight: 600;
  101. margin-bottom: 10px;
  102. }
  103. .error-msg {
  104. font-size: 14px;
  105. color: #cbd5e1;
  106. margin-bottom: 15px;
  107. line-height: 1.5;
  108. }
  109. .error-btn a {
  110. display: inline-block;
  111. padding: 10px 18px;
  112. background: #0f66d6;
  113. border-radius: 999px;
  114. color: white;
  115. font-weight: 600;
  116. font-size: 14px;
  117. text-decoration: none;
  118. }
  119. .error-btn a:hover {
  120. background: #1877f2;
  121. }
  122. </style>
  123.  
  124. <div class="error-overlay">
  125. <div class="error-box">
  126. <div class="error-title">⚠️ Too Many Database Requests</div>
  127. <div class="error-msg">
  128. The database query limit has been reached temporarily.<br>
  129. Please try again in a few minutes.
  130. </div>
  131. <div class="error-btn">
  132. <a href="index.php">↺ Retry</a>
  133. </div>
  134. </div>
  135. </div>
  136. ';
  137. exit;
  138. }
  139.  
  140. die("Database error: " . htmlspecialchars($e->getMessage()));
  141. }
  142. }
  143.  
  144. // Split into featured + normal
  145. $featuredShops = [];
  146. $normalShops = [];
  147.  
  148. foreach ($shops as $shop) {
  149. if (!empty($shop['is_featured'])) $featuredShops[] = $shop;
  150. else $normalShops[] = $shop;
  151. }
  152.  
  153. // ---------- MARKETPLACE STATS ----------
  154. $marketStats = [
  155. 'shops' => count($shops),
  156. 'wtb' => 0,
  157. 'traits' => 0,
  158. 'creatures' => 0,
  159. 'blueprints' => 0,
  160. ];
  161.  
  162. try {
  163. qc_increment();
  164.  
  165. $statsStmt = $pdo->query("
  166. SELECT
  167. (SELECT COUNT() FROM mp_stockboard ms
  168. JOIN shops s1 ON s1.id = ms.shop_id
  169. WHERE s1.is_active = 1
  170. AND s1.show_on_index = 1) AS creatures_count,
  171.  
  172. (SELECT COUNT() FROM mp_traits mt
  173. JOIN shops s2 ON s2.id = mt.shop_id
  174. WHERE mt.is_active = 1
  175. AND s2.is_active = 1
  176. AND s2.show_on_index = 1) AS traits_count,
  177.  
  178. (SELECT COUNT() FROM mp_blueprints mb
  179. JOIN shops s3 ON s3.id = mb.shop_id
  180. WHERE s3.is_active = 1
  181. AND s3.show_on_index = 1) AS blueprints_count,
  182.  
  183. (SELECT COUNT() FROM mp_wtb mw
  184. JOIN shops s4 ON s4.id = mw.shop_id
  185. WHERE mw.is_open = 1
  186. AND s4.is_active = 1
  187. AND s4.show_on_index = 1) AS wtb_count
  188. ");
  189.  
  190. $statsRow = $statsStmt->fetch(PDO::FETCH_ASSOC);
  191.  
  192. if ($statsRow) {
  193. $marketStats['wtb'] = (int)($statsRow['wtb_count'] ?? 0);
  194. $marketStats['traits'] = (int)($statsRow['traits_count'] ?? 0);
  195. $marketStats['creatures'] = (int)($statsRow['creatures_count'] ?? 0);
  196. $marketStats['blueprints'] = (int)($statsRow['blueprints_count'] ?? 0);
  197. }
  198. } catch (Throwable $e) {
  199. // silent fallback
  200. }
  201.  
  202. // ---------- FEATURED SPOTLIGHT ----------
  203. $spotlightShop = null;
  204.  
  205. if (!empty($featuredShops)) {
  206. $spotlightShop = $featuredShops[array_rand($featuredShops)];
  207. }
  208.  
  209. // ---------- PUBLIC WTB BOARD ----------
  210. $wtbRows = [];
  211. try {
  212. qc_increment();
  213.  
  214. $stmt = $pdo->query("
  215. SELECT
  216. w.*,
  217. s.name AS shop_name,
  218. s.slug AS shop_slug,
  219. s.discord_user_id
  220. FROM mp_wtb w
  221. JOIN shops s ON s.id = w.shop_id
  222. WHERE w.is_open = 1
  223. AND s.is_active = 1
  224. ORDER BY w.created_at DESC
  225. LIMIT 50
  226. ");
  227. $wtbRows = $stmt->fetchAll(PDO::FETCH_ASSOC);
  228. } catch (PDOException $e) {
  229. $wtbRows = [];
  230. }
  231.  
  232. // ---------- RECENT MARKETPLACE ACTIVITY ----------
  233. $recentActivity = [];
  234. try {
  235. qc_increment();
  236.  
  237. $activityStmt = $pdo->query("
  238. SELECT * FROM (
  239. SELECT
  240. 'creature' AS activity_type,
  241. ms.species AS title,
  242. s.name AS shop_name,
  243. s.slug AS shop_slug,
  244. ms.created_at AS created_at
  245. FROM mp_stockboard ms
  246. JOIN shops s ON s.id = ms.shop_id
  247. WHERE s.is_active = 1
  248. AND s.show_on_index = 1
  249.  
  250. UNION ALL
  251.  
  252. SELECT
  253. 'trait' AS activity_type,
  254. CONCAT(mt.species, ' • ', mt.trait_name) AS title,
  255. s.name AS shop_name,
  256. s.slug AS shop_slug,
  257. mt.created_at AS created_at
  258. FROM mp_traits mt
  259. JOIN shops s ON s.id = mt.shop_id
  260. WHERE mt.is_active = 1
  261. AND s.is_active = 1
  262. AND s.show_on_index = 1
  263.  
  264. UNION ALL
  265.  
  266. SELECT
  267. 'blueprint' AS activity_type,
  268. mb.name AS title,
  269. s.name AS shop_name,
  270. s.slug AS shop_slug,
  271. mb.created_at AS created_at
  272. FROM mp_blueprints mb
  273. JOIN shops s ON s.id = mb.shop_id
  274. WHERE s.is_active = 1
  275. AND s.show_on_index = 1
  276.  
  277. UNION ALL
  278.  
  279. SELECT
  280. 'wtb' AS activity_type,
  281. mw.title AS title,
  282. s.name AS shop_name,
  283. s.slug AS shop_slug,
  284. mw.created_at AS created_at
  285. FROM mp_wtb mw
  286. JOIN shops s ON s.id = mw.shop_id
  287. WHERE mw.is_open = 1
  288. AND s.is_active = 1
  289. AND s.show_on_index = 1
  290. ) AS activity_feed
  291. ORDER BY created_at DESC
  292. LIMIT 10
  293. ");
  294.  
  295. $recentActivity = $activityStmt->fetchAll(PDO::FETCH_ASSOC);
  296. } catch (Throwable $e) {
  297. $recentActivity = [];
  298. }
  299.  
  300. // ---------- SELECTED SHOP HANDLING ----------
  301. $selectedSlug = $_GET['shop'] ?? null;
  302. $selectedShop = null;
  303.  
  304. if ($selectedSlug && $shops) {
  305. foreach ($shops as $shop) {
  306. if ($shop['slug'] === $selectedSlug) {
  307. $selectedShop = $shop;
  308. break;
  309. }
  310. }
  311. }
  312.  
  313. $registerOld = $_SESSION['register_old'] ?? [];
  314. $loginPrefill = $_SESSION['login_prefill'] ?? '';
  315.  
  316. $loggedInUser = panel_current_user($pdo);
  317. $isLoggedIn = !!$loggedInUser;
  318.  
  319. $canAccessShopPanel = false;
  320. $shopPanelUrl = '/panel.php';
  321.  
  322. $userHasShopMembership = false;
  323. $shopRequestEnabled = true; // admins can later change this in DB/config if you want
  324. $canRequestShop = false;
  325. $hasPendingShopRequest = false;
  326.  
  327. if ($isLoggedIn) {
  328. if (panel_is_global_admin($loggedInUser) || (($loggedInUser['role'] ?? '') === 'owner')) {
  329. $canAccessShopPanel = true;
  330. $userHasShopMembership = true;
  331. } else {
  332. $st = $pdo->prepare('SELECT 1 FROM shop_members WHERE user_id = ? LIMIT 1');
  333. $st->execute([(int)$loggedInUser['id']]);
  334. $userHasShopMembership = (bool)$st->fetchColumn();
  335. $canAccessShopPanel = $userHasShopMembership;
  336. }
  337.  
  338. if (!$userHasShopMembership) {
  339. $st = $pdo->prepare("
  340. SELECT 1
  341. FROM shop_requests
  342. WHERE user_id = ?
  343. AND status = 'pending'
  344. LIMIT 1
  345. ");
  346. $st->execute([(int)$loggedInUser['id']]);
  347. $hasPendingShopRequest = (bool)$st->fetchColumn();
  348. }
  349.  
  350. $canRequestShop = !$userHasShopMembership && !$hasPendingShopRequest && $shopRequestEnabled;
  351. }
  352. ?>
  353. <!doctype html>
  354. <html lang="en">
  355. <head>
  356. <meta charset="utf-8">
  357. <title>Ghost Division • Marketplace</title>
  358. <meta name="viewport" content="width=device-width, initial-scale=1">
  359. <style>
  360. html, body{
  361. height: 100%;
  362. margin: 0;
  363. padding: 0;
  364. background: url('assets/images/bg.jpg') no-repeat center center fixed;
  365. background-size: cover;
  366. background-color: #0b1016;
  367. font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  368. }
  369.  
  370. /* background video */
  371. #bgVideo{
  372. position: fixed;
  373. top: 0; left: 0;
  374. width: 100%;
  375. height: 100%;
  376. object-fit: cover;
  377. z-index: 0;
  378. background: #000;
  379. }
  380.  
  381. /* dark overlay */
  382. .background-overlay{
  383. position: fixed;
  384. inset: 0;
  385. background: rgba(0,0,0,0.55);
  386. backdrop-filter: blur(2px);
  387. z-index: 1;
  388. }
  389.  
  390. /* content */
  391. .page{
  392. position: relative;
  393. z-index: 2;
  394. min-height: 100%;
  395. display: flex;
  396. flex-direction: column;
  397. align-items: center;
  398. justify-content: flex-start;
  399. padding-top: 60px;
  400. color: #e8f1ff;
  401. text-align: center;
  402. }
  403.  
  404. /* global logo */
  405. .logo-wrapper{ margin-bottom: 28px; }
  406. .logo{
  407. width: 250px;
  408. max-width: 80vw;
  409. filter: drop-shadow(0 0 18px rgba(0,0,0,0.6));
  410. }
  411.  
  412. /* shop name / subtitle */
  413. .shop-title{
  414. font-size: 22px;
  415. font-weight: 600;
  416. margin-bottom: 18px;
  417. }
  418. .shop-subtitle{
  419. font-size: 13px;
  420. color: #b4c2dd;
  421. margin-bottom: 20px;
  422. }
  423.  
  424. /* main menu (buttons stacked) */
  425. .menu-wrapper{
  426. display: flex;
  427. flex-direction: column;
  428. gap: 10px;
  429. width: 320px;
  430. max-width: 90vw;
  431. text-align: center;
  432. }
  433.  
  434. /* generic pill button */
  435. .btn{
  436. display: block;
  437. padding: 14px 22px;
  438. background: #131c25;
  439. border: 1px solid #1f2a33;
  440. border-radius: 12px;
  441. font-size: 15px;
  442. font-weight: 600;
  443. letter-spacing: 0.2px;
  444. color: #e8f1ff;
  445. text-decoration: none;
  446. transition: 0.18s ease;
  447. }
  448. .btn span.icon{ margin-right: 8px; }
  449. .btn:hover{
  450. background: #1b2733;
  451. border-color: #3b4d60;
  452. color: #fff;
  453. box-shadow: 0 0 12px rgba(0,180,255,0.25);
  454. }
  455. .btn-outline{ background: rgba(10,15,25,0.85); }
  456.  
  457. /* glossy / glass buttons */
  458. .btn{
  459. position: relative;
  460. overflow: hidden;
  461. background:
  462. 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%),
  463. linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
  464. border: 1px solid rgba(255,255,255,0.14);
  465. box-shadow:
  466. 0 18px 55px rgba(0,0,0,0.55),
  467. inset 0 1px 0 rgba(255,255,255,0.14),
  468. inset 0 -1px 0 rgba(0,0,0,0.35);
  469. backdrop-filter: blur(12px);
  470. -webkit-backdrop-filter: blur(12px);
  471. }
  472.  
  473. .btn::before{
  474. content:"";
  475. position:absolute;
  476. inset:-60% -35% auto -35%;
  477. height: 160%;
  478. background: linear-gradient(
  479. 120deg,
  480. rgba(255,255,255,0.26),
  481. rgba(255,255,255,0.06) 55%,
  482. transparent 75%
  483. );
  484. transform: rotate(-8deg);
  485. opacity: .65;
  486. pointer-events:none;
  487. }
  488.  
  489. .btn::after{
  490. content:"";
  491. position:absolute;
  492. inset:0;
  493. background:
  494. radial-gradient(circle at 20% 15%, rgba(255,255,255,0.10), transparent 35%),
  495. radial-gradient(circle at 85% 35%, rgba(59,130,246,0.10), transparent 40%);
  496. opacity: .55;
  497. pointer-events:none;
  498. }
  499.  
  500. .btn:hover{
  501. background:
  502. 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%),
  503. linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
  504. border-color: rgba(96,165,250,0.45);
  505. box-shadow:
  506. 0 22px 66px rgba(0,0,0,0.60),
  507. 0 0 18px rgba(0,180,255,0.16),
  508. inset 0 1px 0 rgba(255,255,255,0.16),
  509. inset 0 -1px 0 rgba(0,0,0,0.35);
  510. }
  511.  
  512. .btn-outline{
  513. background:
  514. 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%),
  515. linear-gradient(180deg, rgba(0,0,0,0.22), rgba(0,0,0,0.10));
  516. border-color: rgba(255,255,255,0.12);
  517. }
  518.  
  519. @media (prefers-reduced-motion: reduce){
  520. .btn{ transition: none; }
  521. }
  522.  
  523. /* small "back" link */
  524. .back-link{
  525. margin-top: 18px;
  526. font-size: 13px;
  527. }
  528. .back-link a{
  529. color: #9fb7ff;
  530. text-decoration: none;
  531. }
  532. .back-link a:hover{ text-decoration: underline; }
  533.  
  534. /* error box for DB issues */
  535. .db-error{
  536. max-width: 420px;
  537. margin: 0 auto 18px;
  538. padding: 10px 14px;
  539. border-radius: 10px;
  540. background: rgba(127,29,29,0.9);
  541. border: 1px solid #fecaca;
  542. color: #fee2e2;
  543. font-size: 13px;
  544. }
  545.  
  546. /* ===== NEW LAYOUT ===== */
  547. .home-layout{
  548. position:relative;
  549. width:100%;
  550. max-width:1400px;
  551. min-height:720px;
  552. }
  553.  
  554. .home-left{
  555. width:460px;
  556. margin:0 auto;
  557. display:flex;
  558. flex-direction:column;
  559. gap:16px;
  560. }
  561.  
  562. .home-left{
  563. width:460px;
  564. margin:0 auto;
  565. display:flex;
  566. flex-direction:column;
  567. gap:16px;
  568. align-items:center;
  569. }
  570.  
  571. .home-right{
  572. position:absolute;
  573. top:0;
  574. left:calc(50% + 270px);
  575. width:460px;
  576. display:flex;
  577. align-items:flex-start;
  578. }
  579.  
  580. /* marketplace stats */
  581. .market-stats{
  582. display:flex;
  583. flex-direction:column;
  584. gap:12px;
  585. align-items:center;
  586. width:100%;
  587. max-width:100%;
  588. margin:0 auto;
  589. }
  590.  
  591. .market-stats-row{
  592. display:grid;
  593. gap:12px;
  594. justify-content:center;
  595. width:fit-content;
  596. max-width:100%;
  597. }
  598.  
  599. .market-stats-top{
  600. grid-template-columns:repeat(2, minmax(0, 210px));
  601. }
  602.  
  603. .market-stats-bottom{
  604. grid-template-columns:repeat(3, minmax(0, 160px));
  605. }
  606.  
  607. .stat-card{
  608. box-sizing:border-box;
  609. width:100%;
  610. min-height:88px;
  611. border-radius:16px;
  612. padding:14px;
  613. text-align:center;
  614. display:flex;
  615. flex-direction:column;
  616. align-items:center;
  617. justify-content:center;
  618.  
  619. background: linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
  620. border:1px solid rgba(255,255,255,0.10);
  621. box-shadow: 0 18px 55px rgba(0,0,0,0.45);
  622. }
  623.  
  624. .stat-label{
  625. font-size:11px;
  626. color:#b4c2dd;
  627. margin-bottom:4px;
  628. text-transform:uppercase;
  629. letter-spacing:.08em;
  630. }
  631.  
  632. .stat-value{
  633. font-size:22px;
  634. font-weight:800;
  635. color:#f8fbff;
  636. }
  637.  
  638. /* spotlight */
  639. .spotlight-card{
  640. width:460px;
  641. max-width:92vw;
  642. margin:0;
  643. padding:14px 16px;
  644. border-radius:18px;
  645.  
  646. text-align:center;
  647.  
  648. background: linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
  649. border:1px solid rgba(250,204,21,.22);
  650.  
  651. box-shadow:
  652. 0 24px 80px rgba(0,0,0,0.55),
  653. 0 0 18px rgba(250,204,21,.14);
  654.  
  655. backdrop-filter: blur(12px);
  656. -webkit-backdrop-filter: blur(12px);
  657.  
  658. display:flex;
  659. flex-direction:column;
  660. align-items:center;
  661. }
  662.  
  663. .spotlight-kicker{
  664. font-size:11px;
  665. color:#fde68a;
  666. text-transform:uppercase;
  667. letter-spacing:.08em;
  668. font-weight:800;
  669. margin-bottom:6px;
  670. }
  671.  
  672. .spotlight-title{
  673. font-size:20px;
  674. font-weight:800;
  675. margin-bottom:6px;
  676. color:#fff8dc;
  677. }
  678.  
  679. .spotlight-sub{
  680. font-size:13px;
  681. color:#d7e3f8;
  682. margin-bottom:10px;
  683. }
  684.  
  685. /* activity */
  686. .activity-panel{
  687. width:460px;
  688. max-width:92vw;
  689. box-sizing:border-box;
  690. margin:0;
  691. padding:14px 16px;
  692. border-radius:18px;
  693. text-align:left;
  694. background:linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
  695. border:1px solid rgba(255,255,255,.10);
  696. box-shadow:0 24px 80px rgba(0,0,0,0.55);
  697. backdrop-filter:blur(12px);
  698. -webkit-backdrop-filter:blur(12px);
  699.  
  700. display:flex;
  701. flex-direction:column;
  702. height:auto;
  703. min-height:0;
  704. }
  705.  
  706. .activity-title{
  707. font-size:15px;
  708. font-weight:800;
  709. margin-bottom:10px;
  710. }
  711.  
  712. .activity-list{
  713. display:flex;
  714. flex-direction:column;
  715. gap:8px;
  716. flex:1 1 auto;
  717. position:relative;
  718. }
  719.  
  720. .activity-row{
  721. padding:10px 12px;
  722. border-radius:12px;
  723. background:rgba(255,255,255,.04);
  724. border:1px solid rgba(255,255,255,.06);
  725. transition:
  726. transform .38s ease,
  727. opacity .30s ease,
  728. background-color .25s ease,
  729. border-color .25s ease;
  730. will-change: transform, opacity;
  731. }
  732.  
  733. .activity-row.is-entering{
  734. animation: activitySlideDown .46s ease;
  735. }
  736.  
  737. .activity-row.is-highlight{
  738. background:rgba(59,130,246,.10);
  739. border-color:rgba(96,165,250,.22);
  740. }
  741.  
  742. .activity-row.is-leaving{
  743. animation: activitySlideUpFade .34s ease forwards;
  744. }
  745.  
  746. @keyframes activitySlideDown{
  747. from{
  748. opacity:0;
  749. transform:translateY(-18px);
  750. }
  751. to{
  752. opacity:1;
  753. transform:translateY(0);
  754. }
  755. }
  756.  
  757. @keyframes activitySlideUpFade{
  758. from{
  759. opacity:1;
  760. transform:translateY(0);
  761. }
  762. to{
  763. opacity:0;
  764. transform:translateY(-16px);
  765. }
  766. }
  767.  
  768. .activity-main{
  769. font-size:13px;
  770. color:#f3f7ff;
  771. }
  772.  
  773. .activity-main a{
  774. color:#93c5fd;
  775. text-decoration:none;
  776. font-weight:700;
  777. }
  778.  
  779. .activity-main a:hover{
  780. text-decoration:underline;
  781. }
  782.  
  783. .activity-meta{
  784. margin-top:4px;
  785. font-size:11px;
  786. color:#94a3b8;
  787. }
  788.  
  789. .activity-empty{
  790. font-size:13px;
  791. color:#94a3b8;
  792. padding:6px 0 2px;
  793. }
  794.  
  795. /* ===== SHOP PANEL ===== */
  796. .shop-panel{
  797. width: 460px;
  798. max-width: 92vw;
  799. padding: 16px;
  800. border-radius: 20px;
  801. background: linear-gradient(180deg, rgba(255,255,255,0.07), rgba(255,255,255,0.03));
  802. border: 1px solid rgba(255,255,255,0.10);
  803. box-shadow: 0 32px 110px rgba(0,0,0,0.65);
  804. backdrop-filter: blur(12px);
  805. -webkit-backdrop-filter: blur(12px);
  806. display: flex;
  807. flex-direction: column;
  808. max-height: 72vh;
  809. overflow: hidden;
  810. }
  811.  
  812. .shop-panel .wtb-inline{ margin: 0 0 10px; }
  813.  
  814. .shop-search-wrap{
  815. margin: 0 0 12px;
  816. }
  817.  
  818. .shop-search{
  819. width:100%;
  820. box-sizing:border-box;
  821. padding:12px 14px;
  822. border-radius:14px;
  823. border:1px solid rgba(255,255,255,.12);
  824. background:
  825. linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.04)),
  826. linear-gradient(180deg, rgba(12,18,30,.88), rgba(5,10,18,.92));
  827. color:#e8f1ff;
  828. font-size:14px;
  829. outline:none;
  830. box-shadow:
  831. inset 0 1px 0 rgba(255,255,255,.12),
  832. inset 0 -1px 0 rgba(0,0,0,.35),
  833. 0 8px 22px rgba(0,0,0,.28);
  834. }
  835.  
  836. .shop-search::placeholder{
  837. color:#9fb1cc;
  838. }
  839.  
  840. .shop-search:focus{
  841. border-color:rgba(96,165,250,.75);
  842. box-shadow:
  843. inset 0 1px 0 rgba(255,255,255,.14),
  844. inset 0 -1px 0 rgba(0,0,0,.35),
  845. 0 0 0 1px rgba(96,165,250,.35),
  846. 0 10px 28px rgba(0,0,0,.35);
  847. }
  848.  
  849. .shop-scroll{
  850. margin-top: 12px;
  851. flex: 1 1 auto;
  852. overflow: auto;
  853. padding-bottom: 8px;
  854. -webkit-overflow-scrolling: touch;
  855. }
  856.  
  857. .shop-scroll::-webkit-scrollbar{ width: 10px; }
  858. .shop-scroll::-webkit-scrollbar-track{
  859. background: rgba(255,255,255,0.04);
  860. border-radius: 10px;
  861. }
  862. .shop-scroll::-webkit-scrollbar-thumb{
  863. background: rgba(255,255,255,0.14);
  864. border-radius: 10px;
  865. }
  866. .shop-scroll::-webkit-scrollbar-thumb:hover{ background: rgba(255,255,255,0.22); }
  867. .shop-scroll{
  868. scrollbar-color: rgba(255,255,255,0.18) rgba(255,255,255,0.05);
  869. scrollbar-width: thin;
  870. }
  871.  
  872. .shop-empty{
  873. padding: 14px;
  874. color: #b4c2dd;
  875. font-size: 13px;
  876. text-align: center;
  877. }
  878.  
  879. .shop-grid{
  880. display: grid;
  881. grid-template-columns: repeat(2, minmax(0, 1fr));
  882. gap: 8px;
  883. grid-auto-flow: row;
  884. }
  885.  
  886. .shop-item{
  887. height: 52px;
  888. border-radius: 14px;
  889. display: flex;
  890. align-items: center;
  891. justify-content: center;
  892. padding: 0 12px;
  893. text-decoration: none;
  894. color: #e8f1ff;
  895. background: rgba(0,0,0,0.14);
  896. border: 1px solid rgba(255,255,255,0.06);
  897. transition: 0.18s ease;
  898. position: relative;
  899. overflow: hidden;
  900. }
  901.  
  902. .shop-item:hover{
  903. background: rgba(255,255,255,0.05);
  904. border-color: rgba(96,165,250,0.28);
  905. box-shadow: 0 0 16px rgba(0,180,255,0.10);
  906. transform: translateY(-2px);
  907. }
  908.  
  909. .shop-name{
  910. font-size: 14px;
  911. font-weight: 650;
  912. text-align: center;
  913. width: 100%;
  914. overflow: hidden;
  915. text-overflow: ellipsis;
  916. white-space: nowrap;
  917. }
  918.  
  919. .shop-hidden{
  920. display:none !important;
  921. }
  922.  
  923. /* featured */
  924. .shop-item.is-featured{
  925. border-color: rgba(250,204,21,0.35);
  926. box-shadow: 0 0 18px rgba(250,204,21,0.18);
  927. font-weight:700;
  928. }
  929.  
  930. .shop-item.is-featured::before{
  931. content:"";
  932. position:absolute;
  933. inset:-40%;
  934. background:
  935. radial-gradient(circle at 20% 30%, rgba(250,204,21,0.20), transparent 45%),
  936. radial-gradient(circle at 80% 60%, rgba(255,255,255,0.10), transparent 40%),
  937. radial-gradient(circle at 50% 90%, rgba(250,204,21,0.14), transparent 50%);
  938. filter: blur(2px);
  939. animation: sparkleFloat 5.5s ease-in-out infinite;
  940. pointer-events:none;
  941. }
  942.  
  943. @keyframes sparkleFloat{
  944. 0% { transform: translate3d(-1.5%, -1%, 0) scale(1); opacity: .75; }
  945. 50% { transform: translate3d( 1.5%, 1%, 0) scale(1.02); opacity: 1; }
  946. 100% { transform: translate3d(-1.5%, -1%, 0) scale(1); opacity: .75; }
  947. }
  948.  
  949. @media (prefers-reduced-motion: reduce){
  950. .shop-item.is-featured::before{ animation:none; }
  951. }
  952.  
  953. /* .corner-ribbon{
  954. position: absolute;
  955. top: 12px;
  956. left: -38px;
  957. transform: rotate(-35deg);
  958. font-size: 10px;
  959. font-weight: 800;
  960. text-transform: uppercase;
  961. letter-spacing: .6px;
  962. padding: 4px 44px;
  963. background: linear-gradient(180deg, rgba(250,204,21,0.35), rgba(250,204,21,0.18));
  964. border: 1px solid rgba(250,204,21,0.45);
  965. color: #fde68a;
  966. box-shadow: 0 0 10px rgba(250,204,21,0.25), inset 0 0 6px rgba(255,255,255,0.15);
  967. pointer-events: none;
  968. } */
  969.  
  970. .corner-ribbon{
  971. display:none;
  972. }
  973.  
  974. .featured-grid{
  975. display:grid;
  976. grid-template-columns: repeat(2, minmax(0, 1fr));
  977. gap: 10px;
  978. margin: 10px 0 14px;
  979. }
  980. .featured-grid .shop-item{ height: 52px; }
  981.  
  982. /* LOGIN BUTTON */
  983. .top-login{
  984. position:absolute;
  985. top:20px;
  986. right:20px;
  987. z-index:5;
  988. }
  989.  
  990. .login-btn{
  991. padding:7px 14px;
  992. border-radius:999px;
  993. border:1px solid rgba(255,255,255,.16);
  994. background:rgba(255,255,255,.05);
  995. color:#e8f1ff;
  996. font-weight:700;
  997. font-size:13px;
  998. line-height:1.1;
  999. cursor:pointer;
  1000. backdrop-filter:blur(10px);
  1001. -webkit-backdrop-filter:blur(10px);
  1002. transition:.18s ease;
  1003. }
  1004.  
  1005. .login-btn:hover{
  1006. background:#0f66d6;
  1007. border-color:#0f66d6;
  1008. }
  1009.  
  1010. /* modal backdrop */
  1011. .login-modal{
  1012. position:fixed;
  1013. inset:0;
  1014. background:rgba(0,0,0,.68);
  1015. backdrop-filter:blur(8px);
  1016. -webkit-backdrop-filter:blur(8px);
  1017. display:none;
  1018. align-items:center;
  1019. justify-content:center;
  1020. padding:18px;
  1021. z-index:1000;
  1022. }
  1023.  
  1024. .login-modal.open{
  1025. display:flex;
  1026. }
  1027.  
  1028. /* modal box */
  1029. .login-box{
  1030. width:min(380px, 94vw);
  1031. border-radius:22px;
  1032. padding:22px;
  1033. background:
  1034. linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
  1035. border:1px solid rgba(255,255,255,.12);
  1036. backdrop-filter:blur(14px);
  1037. -webkit-backdrop-filter:blur(14px);
  1038. box-shadow:
  1039. 0 28px 80px rgba(0,0,0,.65),
  1040. inset 0 1px 0 rgba(255,255,255,.08);
  1041. position:relative;
  1042. overflow:hidden;
  1043. }
  1044.  
  1045. .login-box::before{
  1046. content:"";
  1047. position:absolute;
  1048. inset:0;
  1049. border-radius:inherit;
  1050. background:linear-gradient(
  1051. 120deg,
  1052. rgba(255,255,255,.12),
  1053. rgba(255,255,255,0) 40%
  1054. );
  1055. opacity:.22;
  1056. pointer-events:none;
  1057. }
  1058.  
  1059. .login-header{
  1060. display:flex;
  1061. justify-content:space-between;
  1062. align-items:center;
  1063. gap:12px;
  1064. margin-bottom:18px;
  1065. position:relative;
  1066. z-index:1;
  1067. }
  1068.  
  1069. .login-title{
  1070. font-weight:900;
  1071. font-size:18px;
  1072. color:#f3f7ff;
  1073. letter-spacing:.2px;
  1074. }
  1075.  
  1076. .login-close{
  1077. width:36px;
  1078. height:36px;
  1079. border-radius:999px;
  1080. border:1px solid rgba(255,255,255,.14);
  1081. background:rgba(255,255,255,.04);
  1082. color:#f3f7ff;
  1083. font-size:18px;
  1084. line-height:1;
  1085. cursor:pointer;
  1086. transition:.15s ease;
  1087. }
  1088.  
  1089. .login-close:hover{
  1090. background:rgba(255,255,255,.08);
  1091. border-color:rgba(255,255,255,.22);
  1092. }
  1093.  
  1094. /* form */
  1095. .login-form{
  1096. display:flex;
  1097. flex-direction:column;
  1098. gap:12px;
  1099. position:relative;
  1100. z-index:1;
  1101. }
  1102.  
  1103. .login-form input{
  1104. width:100%;
  1105. box-sizing:border-box;
  1106. padding:12px 14px;
  1107. border-radius:12px;
  1108. border:1px solid rgba(255,255,255,.14);
  1109. background:
  1110. linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.04)),
  1111. linear-gradient(180deg, rgba(12,18,30,.88), rgba(5,10,18,.92));
  1112. color:#e8f1ff;
  1113. font-size:14px;
  1114. outline:none;
  1115. backdrop-filter: blur(10px);
  1116. -webkit-backdrop-filter: blur(10px);
  1117. box-shadow:
  1118. inset 0 1px 0 rgba(255,255,255,.18),
  1119. inset 0 -1px 0 rgba(0,0,0,.35),
  1120. 0 6px 16px rgba(0,0,0,.25);
  1121. transition:.18s ease;
  1122. }
  1123.  
  1124. .login-form input:hover{
  1125. border-color:rgba(255,255,255,.22);
  1126. box-shadow:
  1127. inset 0 1px 0 rgba(255,255,255,.20),
  1128. inset 0 -1px 0 rgba(0,0,0,.35),
  1129. 0 8px 20px rgba(0,0,0,.30);
  1130. }
  1131.  
  1132. .login-form input::placeholder{
  1133. color:#9fb1cc;
  1134. }
  1135.  
  1136. .login-form input:focus{
  1137. border-color:rgba(96,165,250,.75);
  1138. background:
  1139. linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.05)),
  1140. linear-gradient(180deg, rgba(12,18,30,.94), rgba(5,10,18,.96));
  1141. box-shadow:
  1142. inset 0 1px 0 rgba(255,255,255,.22),
  1143. inset 0 -1px 0 rgba(0,0,0,.35),
  1144. 0 0 0 1px rgba(96,165,250,.35),
  1145. 0 10px 28px rgba(0,0,0,.35);
  1146. }
  1147. .login-form input:-webkit-autofill,
  1148. .login-form input:-webkit-autofill:hover,
  1149. .login-form input:-webkit-autofill:focus,
  1150. .login-form input:-webkit-autofill:active{
  1151. -webkit-text-fill-color:#e8f1ff;
  1152. caret-color:#e8f1ff;
  1153.  
  1154. border:1px solid rgba(255,255,255,.14);
  1155.  
  1156. -webkit-box-shadow:
  1157. 0 0 0 1000px rgba(18,24,36,.92) inset,
  1158. inset 0 1px 0 rgba(255,255,255,.18),
  1159. inset 0 -1px 0 rgba(0,0,0,.35),
  1160. 0 6px 16px rgba(0,0,0,.25);
  1161.  
  1162. box-shadow:
  1163. 0 0 0 1000px rgba(18,24,36,.92) inset,
  1164. inset 0 1px 0 rgba(255,255,255,.18),
  1165. inset 0 -1px 0 rgba(0,0,0,.35),
  1166. 0 6px 16px rgba(0,0,0,.25);
  1167.  
  1168. -webkit-transition: background-color 9999s ease-out 0s;
  1169. transition: background-color 9999s ease-out 0s;
  1170. }
  1171. .login-submit{
  1172. margin-top:4px;
  1173. width:100%;
  1174. padding:12px 14px;
  1175. border-radius:999px;
  1176. border:none;
  1177. background:#0f66d6;
  1178. color:#fff;
  1179. font-weight:800;
  1180. font-size:14px;
  1181. cursor:pointer;
  1182. transition:.15s ease;
  1183. }
  1184.  
  1185. .login-submit:hover{
  1186. background:#1877f2;
  1187. box-shadow:0 12px 28px rgba(24,119,242,.35);
  1188. }
  1189.  
  1190. .login-footer{
  1191. margin-top:16px;
  1192. font-size:13px;
  1193. color:#b8c6dd;
  1194. text-align:center;
  1195. position:relative;
  1196. z-index:1;
  1197. }
  1198.  
  1199. .login-footer a{
  1200. color:#93c5fd;
  1201. text-decoration:none;
  1202. font-weight:700;
  1203. }
  1204.  
  1205. .login-footer a:hover{
  1206. text-decoration:underline;
  1207. }
  1208. .login-link-btn{
  1209. background:none;
  1210. border:none;
  1211. padding:0;
  1212. margin:0;
  1213. color:#93c5fd;
  1214. font-weight:700;
  1215. font-size:13px;
  1216. cursor:pointer;
  1217. text-decoration:none;
  1218. }
  1219.  
  1220. .login-link-btn:hover{
  1221. text-decoration:underline;
  1222. }
  1223. .login-success{
  1224. margin-bottom:12px;
  1225. padding:10px 12px;
  1226. border-radius:12px;
  1227. background:rgba(22,163,74,.18);
  1228. border:1px solid rgba(34,197,94,.35);
  1229. color:#bbf7d0;
  1230. font-size:13px;
  1231. position:relative;
  1232. z-index:1;
  1233. }
  1234.  
  1235. .top-userbar{
  1236. display:flex;
  1237. align-items:center;
  1238. gap:10px;
  1239. flex-wrap:wrap;
  1240. justify-content:flex-end;
  1241. }
  1242.  
  1243. .top-usertext{
  1244. font-size:13px;
  1245. color:#dbe7ff;
  1246. padding:8px 12px;
  1247. border-radius:999px;
  1248. border:1px solid rgba(255,255,255,.12);
  1249. background:rgba(255,255,255,.04);
  1250. backdrop-filter:blur(10px);
  1251. -webkit-backdrop-filter:blur(10px);
  1252. }
  1253.  
  1254. .panel-btn{
  1255. text-decoration:none;
  1256. display:inline-block;
  1257. }
  1258.  
  1259. .logout-btn{
  1260. padding:7px 12px;
  1261. text-decoration:none;
  1262. display:inline-block;
  1263. border-color:rgba(220,38,38,.45);
  1264. color:#fecaca;
  1265. }
  1266.  
  1267. .logout-btn:hover{
  1268. background:#dc2626;
  1269. border-color:#dc2626;
  1270. color:#fff;
  1271. }
  1272. .top-user-trigger{
  1273. text-align:left;
  1274. }
  1275.  
  1276. .modal-backdrop{
  1277. position: fixed;
  1278. inset: 0;
  1279. background: rgba(0,0,0,.62);
  1280. backdrop-filter: blur(8px);
  1281. -webkit-backdrop-filter: blur(8px);
  1282. display: none;
  1283. align-items: center;
  1284. justify-content: center;
  1285. padding: 18px;
  1286. z-index: 1100;
  1287. }
  1288.  
  1289. .modal-backdrop.open{
  1290. display:flex;
  1291. }
  1292.  
  1293. .modal-panel{
  1294. width:min(520px, 94vw);
  1295. max-height:88vh;
  1296. overflow:hidden;
  1297. border-radius:18px;
  1298. border:1px solid rgba(255,255,255,.12);
  1299. background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03));
  1300. box-shadow:0 30px 90px rgba(0,0,0,.65);
  1301. display:flex;
  1302. flex-direction:column;
  1303. }
  1304.  
  1305. .modal-head{
  1306. display:flex;
  1307. align-items:center;
  1308. justify-content:space-between;
  1309. gap:12px;
  1310. padding:14px 16px;
  1311. border-bottom:1px solid rgba(255,255,255,.08);
  1312. background:rgba(0,0,0,.18);
  1313. }
  1314.  
  1315. .modal-title{
  1316. font-size:16px;
  1317. font-weight:900;
  1318. }
  1319.  
  1320. .modal-sub{
  1321. font-size:12px;
  1322. opacity:.75;
  1323. margin-top:2px;
  1324. }
  1325.  
  1326. .modal-close{
  1327. width:36px;
  1328. height:36px;
  1329. border-radius:999px;
  1330. border:1px solid rgba(255,255,255,.14);
  1331. background:rgba(255,255,255,.04);
  1332. color:#e5ecff;
  1333. cursor:pointer;
  1334. font-size:16px;
  1335. font-weight:900;
  1336. }
  1337.  
  1338. .modal-close:hover{
  1339. background:rgba(255,255,255,.08);
  1340. }
  1341.  
  1342. .modal-body{
  1343. padding:16px;
  1344. overflow:auto;
  1345. }
  1346.  
  1347. .setting-group{
  1348. margin-bottom:14px;
  1349. }
  1350.  
  1351. .setting-label{
  1352. font-size:12px;
  1353. font-weight:700;
  1354. opacity:.85;
  1355. margin-bottom:6px;
  1356. text-align:left;
  1357. }
  1358.  
  1359. .setting-input{
  1360. width:100%;
  1361. box-sizing:border-box;
  1362. background:#050a12;
  1363. border:1px solid #283343;
  1364. color:#e5ecff;
  1365. border-radius:10px;
  1366. padding:10px 12px;
  1367. font-size:13px;
  1368. outline:none;
  1369. }
  1370.  
  1371. .setting-input:focus{
  1372. border-color:#3b82f6;
  1373. box-shadow:0 0 0 1px rgba(59,130,246,0.4);
  1374. }
  1375.  
  1376. .modal-actions{
  1377. display:flex;
  1378. gap:10px;
  1379. flex-wrap:wrap;
  1380. margin-top:16px;
  1381. align-items:center;
  1382. justify-content:flex-end;
  1383. }
  1384.  
  1385. .modal-link-btn{
  1386. display:inline-block;
  1387. padding:8px 12px;
  1388. border-radius:999px;
  1389. text-decoration:none;
  1390. font-size:13px;
  1391. font-weight:800;
  1392. color:#fff;
  1393. background:#0f66d6;
  1394. border:1px solid #0f66d6;
  1395. cursor:pointer;
  1396. font-family:inherit;
  1397. }
  1398.  
  1399. .modal-link-btn:hover{
  1400. background:#1877f2;
  1401. border-color:#1877f2;
  1402. }
  1403.  
  1404. .modal-status{
  1405. font-size:13px;
  1406. margin-top:10px;
  1407. opacity:.9;
  1408. text-align:left;
  1409. }
  1410.  
  1411. .modal-status.ok{
  1412. color:#86efac;
  1413. }
  1414.  
  1415. .modal-status.err{
  1416. color:#fca5a5;
  1417. }
  1418.  
  1419. #accountModal .modal-title{
  1420. color:#f3f7ff;
  1421. }
  1422.  
  1423. #accountModal .modal-sub{
  1424. color:#b8c6dd;
  1425. opacity:1;
  1426. }
  1427.  
  1428. #accountModal .setting-label{
  1429. color:#dbe7ff;
  1430. opacity:1;
  1431. }
  1432.  
  1433. #accountModal .setting-input{
  1434. color:#e8f1ff;
  1435. }
  1436.  
  1437. #accountModal .setting-input::placeholder{
  1438. color:#93a6c4;
  1439. }
  1440.  
  1441. #accountModal .modal-body{
  1442. color:#e8f1ff;
  1443. }
  1444.  
  1445. #createShopModal .modal-title{
  1446. color:#f3f7ff;
  1447. }
  1448. #createShopModal .modal-sub{
  1449. color:#b8c6dd;
  1450. opacity:1;
  1451. }
  1452. #createShopModal .setting-label{
  1453. color:#dbe7ff;
  1454. opacity:1;
  1455. }
  1456. #createShopModal .setting-input{
  1457. color:#e8f1ff;
  1458. }
  1459. #createShopModal .setting-input::placeholder{
  1460. color:#93a6c4;
  1461. }
  1462. #createShopModal .modal-body{
  1463. color:#e8f1ff;
  1464. }
  1465. #createShopModal textarea.setting-input{
  1466. min-height:110px;
  1467. resize:vertical;
  1468. }
  1469.  
  1470. #accountModal .setting-input:-webkit-autofill,
  1471. #accountModal .setting-input:-webkit-autofill:hover,
  1472. #accountModal .setting-input:-webkit-autofill:focus,
  1473. #accountModal .setting-input:-webkit-autofill:active{
  1474. -webkit-text-fill-color:#e8f1ff;
  1475. caret-color:#e8f1ff;
  1476. -webkit-box-shadow: 0 0 0 1000px #050a12 inset;
  1477. box-shadow: 0 0 0 1000px #050a12 inset;
  1478. transition: background-color 9999s ease-out 0s;
  1479. }
  1480.  
  1481. /* ===== Responsive ===== */
  1482. @media (max-width: 1250px){
  1483. .home-layout{
  1484. max-width:960px;
  1485. min-height:unset;
  1486. }
  1487.  
  1488. .home-right{
  1489. position:static;
  1490. transform:none;
  1491. width:460px;
  1492. margin:16px auto 0;
  1493. }
  1494. }
  1495.  
  1496. @media (max-width: 900px){
  1497. .shop-grid{
  1498. grid-auto-columns: minmax(160px, 1fr);
  1499. grid-template-rows: repeat(5, 52px);
  1500. }
  1501. }
  1502.  
  1503. @media (max-width: 600px){
  1504. .shop-panel{ max-height: 78vh; }
  1505. .featured-grid{ grid-template-columns: 1fr; }
  1506.  
  1507. .shop-grid{
  1508. grid-auto-flow: row;
  1509. grid-template-columns: 1fr;
  1510. grid-template-rows: none;
  1511. }
  1512.  
  1513. @media (max-width: 700px){
  1514. .market-stats{
  1515. width:100%;
  1516. }
  1517.  
  1518. .market-stats-top,
  1519. .market-stats-bottom{
  1520. grid-template-columns:1fr;
  1521. width:100%;
  1522. }
  1523.  
  1524. .stat-card{
  1525. width:100%;
  1526. }
  1527.  
  1528. .home-left,
  1529. .home-right,
  1530. .activity-panel{
  1531. width:100%;
  1532. max-width:92vw;
  1533. }
  1534. }
  1535.  
  1536. .home-left,
  1537. .home-right,
  1538. .activity-panel{
  1539. width:100%;
  1540. max-width:92vw;
  1541. }
  1542. }
  1543.  
  1544. .pending-btn{
  1545. background: linear-gradient(180deg, rgba(245,158,11,.28), rgba(217,119,6,.20));
  1546. border-color: rgba(251,191,36,.42);
  1547. color: #fde68a;
  1548. cursor: not-allowed;
  1549. box-shadow:
  1550. 0 0 18px rgba(245,158,11,.18),
  1551. inset 0 1px 0 rgba(255,255,255,.08);
  1552. }
  1553.  
  1554. .pending-btn:hover{
  1555. background: linear-gradient(180deg, rgba(245,158,11,.28), rgba(217,119,6,.20));
  1556. border-color: rgba(251,191,36,.42);
  1557. color: #fde68a;
  1558. transform: none;
  1559. box-shadow:
  1560. 0 0 18px rgba(245,158,11,.18),
  1561. inset 0 1px 0 rgba(255,255,255,.08);
  1562. }
  1563. </style>
  1564. </head>
  1565. <body>
  1566. <!-- Video background -->
  1567. <video autoplay muted loop playsinline id="bgVideo">
  1568. <source src="assets/video/bg.mp4" type="video/mp4">
  1569. </video>
  1570. <div class="background-overlay"></div>
  1571.  
  1572. <div class="page">
  1573. <div class="logo-wrapper">
  1574. <img src="assets/images/header.png"
  1575. alt="The Disturbed Cavemen Marketplace"
  1576. class="logo"
  1577. onerror="this.style.display='none'">
  1578.  
  1579. <div class="top-login">
  1580. <?php if (!$isLoggedIn): ?>
  1581. <button class="login-btn" id="openLogin" type="button">Login</button>
  1582. <?php else: ?>
  1583. <div class="top-userbar">
  1584.  
  1585. <button
  1586. type="button"
  1587. class="login-btn top-user-trigger js-open-account"
  1588. data-username="<?php echo h($loggedInUser['username']); ?>"
  1589. data-role="<?php echo h(ucfirst($loggedInUser['role'] ?? 'user')); ?>"
  1590. data-discord-id="<?php echo h($loggedInUser['discord_user_id'] ?? ''); ?>"
  1591. >
  1592. Logged in as <b><?php echo h($loggedInUser['username']); ?></b>
  1593. </button>
  1594.  
  1595. <?php if ($canAccessShopPanel): ?>
  1596. <a class="login-btn panel-btn" href="<?php echo h($shopPanelUrl); ?>">Shop Panel</a>
  1597. <?php endif; ?>
  1598.  
  1599. <?php if ($canRequestShop): ?>
  1600. <button type="button" class="login-btn" id="openShopRequest">Create Shop</button>
  1601. <?php elseif ($hasPendingShopRequest): ?>
  1602. <button type="button" class="login-btn pending-btn" disabled>Request Pending</button>
  1603. <?php endif; ?>
  1604.  
  1605. <a class="login-btn logout-btn" href="/panel_logout.php">Logout</a>
  1606. </div>
  1607. <?php endif; ?>
  1608. </div>
  1609. </div>
  1610.  
  1611. <?php if (!$selectedShop): ?>
  1612. <!-- NO shop selected yet: show list of shops -->
  1613. <div class="shop-subtitle">
  1614. The Unofficial Ghost Division Marketplace.
  1615. </div>
  1616.  
  1617. <div class="home-layout">
  1618. <div class="home-left">
  1619. <div class="market-stats">
  1620. <div class="market-stats-row market-stats-top">
  1621. <div class="stat-card">
  1622. <div class="stat-label">Active Shops</div>
  1623. <div class="stat-value"><?php echo (int)$marketStats['shops']; ?></div>
  1624. </div>
  1625. <div class="stat-card">
  1626. <div class="stat-label">Open WTB Posts</div>
  1627. <div class="stat-value"><?php echo (int)$marketStats['wtb']; ?></div>
  1628. </div>
  1629. </div>
  1630.  
  1631. <div class="market-stats-row market-stats-bottom">
  1632. <div class="stat-card">
  1633. <div class="stat-label">Trait Listings</div>
  1634. <div class="stat-value"><?php echo (int)$marketStats['traits']; ?></div>
  1635. </div>
  1636. <div class="stat-card">
  1637. <div class="stat-label">Creature Listings</div>
  1638. <div class="stat-value"><?php echo (int)$marketStats['creatures']; ?></div>
  1639. </div>
  1640. <div class="stat-card">
  1641. <div class="stat-label">Blueprint Listings</div>
  1642. <div class="stat-value"><?php echo (int)$marketStats['blueprints']; ?></div>
  1643. </div>
  1644. </div>
  1645. </div>
  1646.  
  1647. <?php if ($spotlightShop): ?>
  1648. <div class="spotlight-card">
  1649. <div class="spotlight-kicker">Featured Spotlight</div>
  1650. <div class="spotlight-title"><?php echo h($spotlightShop['name']); ?></div>
  1651. <div class="spotlight-sub">One of the featured shops currently highlighted on the marketplace.</div>
  1652. <a class="btn btn-outline" href="?shop=<?php echo h($spotlightShop['slug']); ?>">
  1653. <span class="icon">⭐</span> Visit featured shop
  1654. </a>
  1655. </div>
  1656. <?php endif; ?>
  1657.  
  1658. <div class="shop-panel" id="shopPanel">
  1659.  
  1660. <div class="wtb-inline">
  1661. <a class="btn btn-outline" href="wtbboard.php">
  1662. <span class="icon">📜</span> Public WTB Board
  1663. </a>
  1664. </div>
  1665.  
  1666. <!-- <div class="shop-search-wrap">
  1667. <input
  1668. type="text"
  1669. id="shopSearch"
  1670. class="shop-search"
  1671. placeholder="Search shops... (press / to focus)"
  1672. autocomplete="off"
  1673. >
  1674. </div> -->
  1675.  
  1676. <div class="shop-scroll">
  1677.  
  1678. <?php if (!empty($featuredShops)): ?>
  1679. <div class="featured-grid" id="featuredGrid">
  1680. <?php foreach ($featuredShops as $shop): ?>
  1681. <?php $name = (string)($shop['name'] ?? ''); ?>
  1682. <a
  1683. class="shop-item is-featured shop-search-item"
  1684. href="?shop=<?php echo h($shop['slug']); ?>"
  1685. data-shop-name="<?php echo h(mb_strtolower($name)); ?>"
  1686. >
  1687. <span class="corner-ribbon">Featured</span>
  1688. <span class="shop-name"><?php echo h($name); ?></span>
  1689. </a>
  1690. <?php endforeach; ?>
  1691. </div>
  1692. <?php endif; ?>
  1693.  
  1694. <div class="shop-grid" id="shopGrid">
  1695. <?php if (!empty($normalShops)): ?>
  1696. <?php foreach ($normalShops as $shop): ?>
  1697. <?php $name = (string)($shop['name'] ?? ''); ?>
  1698. <a
  1699. class="shop-item shop-search-item"
  1700. href="?shop=<?php echo h($shop['slug']); ?>"
  1701. data-shop-name="<?php echo h(mb_strtolower($name)); ?>"
  1702. >
  1703. <span class="shop-name"><?php echo h($name); ?></span>
  1704. </a>
  1705. <?php endforeach; ?>
  1706. <?php elseif (empty($shops)): ?>
  1707. <div class="shop-empty">No shops configured yet.</div>
  1708. <?php endif; ?>
  1709. </div>
  1710.  
  1711. <div class="shop-empty" id="shopSearchEmpty" style="display:none;">
  1712. No shops matched your search.
  1713. </div>
  1714. </div>
  1715. </div>
  1716. </div>
  1717.  
  1718. <div class="home-right">
  1719. <div class="activity-panel">
  1720. <div class="activity-title">Latest Marketplace Activity</div>
  1721.  
  1722. <?php if (!empty($recentActivity)): ?>
  1723. <div class="activity-list" id="activityList">
  1724. <?php foreach ($recentActivity as $a): ?>
  1725. <div class="activity-row">
  1726. <div class="activity-main">
  1727. <?php if (($a['activity_type'] ?? '') === 'creature'): ?>
  1728. 🐲 New creature listing:
  1729. <?php elseif (($a['activity_type'] ?? '') === 'blueprint'): ?>
  1730. 📘 New blueprint listing:
  1731. <?php elseif (($a['activity_type'] ?? '') === 'trait'): ?>
  1732. 🧬 New trait listing:
  1733. <?php else: ?>
  1734. 📜 New WTB listing:
  1735. <?php endif; ?>
  1736.  
  1737. <strong><?php echo h($a['title'] ?? 'Untitled'); ?></strong>
  1738. by
  1739. <a href="?shop=<?php echo h($a['shop_slug'] ?? ''); ?>">
  1740. <?php echo h($a['shop_name'] ?? 'Unknown Shop'); ?>
  1741. </a>
  1742. </div>
  1743.  
  1744. <div class="activity-meta js-local-time"
  1745. data-time="<?php echo h(gmdate('Y-m-d\TH:i:s\Z', strtotime((string)($a['created_at'] ?? '')))); ?>">
  1746. <?php echo h(date('d-m-Y H:i', strtotime((string)($a['created_at'] ?? '')))); ?>
  1747. </div>
  1748. </div>
  1749. <?php endforeach; ?>
  1750. </div>
  1751. <?php else: ?>
  1752. <div class="activity-empty" id="activityEmpty">No recent marketplace activity yet.</div>
  1753. <?php endif; ?>
  1754. </div>
  1755. </div>
  1756. </div>
  1757.  
  1758. <?php else: ?>
  1759. <!-- Shop selected: show its boards -->
  1760. <div class="shop-title">
  1761. <?php echo h($selectedShop['name']); ?>
  1762. </div>
  1763. <div class="shop-subtitle">
  1764. Select what you want to view for this shop.
  1765. </div>
  1766.  
  1767. <div class="menu-wrapper">
  1768.  
  1769. <?php if (!empty($selectedShop['enable_traitboard'])): ?>
  1770. <a class="btn" href="traitsboard.php?shop=<?php echo h($selectedShop['slug']); ?>">
  1771. <span class="icon">🧬</span> Trait Board
  1772. </a>
  1773. <?php endif; ?>
  1774.  
  1775. <?php if (!empty($selectedShop['enable_stockboard'])): ?>
  1776. <a class="btn" href="stockboard.php?shop=<?php echo h($selectedShop['slug']); ?>">
  1777. <span class="icon">🐲</span> Creature Board
  1778. </a>
  1779. <?php endif; ?>
  1780.  
  1781. <?php if (!empty($selectedShop['enable_blueprintboard'])): ?>
  1782. <a class="btn" href="bpboard.php?shop=<?php echo h($selectedShop['slug']); ?>">
  1783. <span class="icon">📘</span> Blueprint Board
  1784. </a>
  1785. <?php endif; ?>
  1786.  
  1787. <?php if (!empty($selectedShop['enable_bundleboard'])): ?>
  1788. <a class="btn" href="bundleboard.php?shop=<?php echo h($selectedShop['slug']); ?>">
  1789. <span class="icon">📦</span> Bundle Board
  1790. </a>
  1791. <?php endif; ?>
  1792.  
  1793. </div>
  1794.  
  1795. <div class="back-link">
  1796. <a href="index.php">← Back to shop list</a>
  1797. </div>
  1798. <?php endif; ?>
  1799. </div>
  1800.  
  1801. <div class="login-modal" id="loginModal">
  1802. <div class="login-box">
  1803. <div class="login-header">
  1804. <div class="login-title">Login</div>
  1805. <button class="login-close" id="closeLogin" type="button">✕</button>
  1806. </div>
  1807. <?php if (!empty($_SESSION['login_success'])): ?>
  1808. <div class="login-success">
  1809. <?php
  1810. echo h($_SESSION['login_success']);
  1811. unset($_SESSION['login_success']);
  1812. ?>
  1813. </div>
  1814. <?php endif; ?>
  1815. <form method="post" action="/panel_login.php" class="login-form">
  1816. <input type="hidden" name="login_from" value="index">
  1817. <input type="text" name="username" placeholder="Username" value="<?php echo h($loginPrefill); ?>" required>
  1818. <input type="password" name="password" placeholder="Password" required>
  1819. <button type="submit" class="login-submit">Login</button>
  1820. </form>
  1821.  
  1822. <div class="login-footer">
  1823. Don’t have an account?
  1824. <button type="button" class="login-link-btn" id="openRegisterFromLogin">Create account</button>
  1825. </div>
  1826. </div>
  1827. </div>
  1828.  
  1829. <div class="login-modal" id="registerModal">
  1830. <div class="login-box">
  1831. <div class="login-header">
  1832. <div class="login-title">Create Account</div>
  1833. <button class="login-close" id="closeRegister" type="button">✕</button>
  1834. </div>
  1835. <?php if (!empty($_SESSION['register_error'])): ?>
  1836. <div class="db-error" style="margin-bottom:12px;">
  1837. <?php
  1838. echo h($_SESSION['register_error']);
  1839. unset($_SESSION['register_error']);
  1840. ?>
  1841. </div>
  1842. <?php endif; ?>
  1843. <form method="post" action="/create_user.php" class="login-form">
  1844. <input type="text" name="username" placeholder="Type Your Survivor Name" value="<?php echo h($registerOld['username'] ?? ''); ?>" required>
  1845. <input type="password" name="password" placeholder="Password" required>
  1846. <input type="password" name="password_confirm" placeholder="Confirm password" required>
  1847.  
  1848. <input
  1849. type="text"
  1850. name="website"
  1851. placeholder="Leave this empty"
  1852. tabindex="-1"
  1853. autocomplete="off"
  1854. style="display:none;"
  1855. >
  1856.  
  1857. <input
  1858. type="text"
  1859. name="captcha"
  1860. placeholder="What is <?php echo h($_SESSION['register_captcha']['question']); ?>?"
  1861. required
  1862. >
  1863.  
  1864. <button type="submit" class="login-submit">Create Account</button>
  1865. </form>
  1866.  
  1867. <div class="login-footer">
  1868. Already have an account?
  1869. <button type="button" class="login-link-btn" id="openLoginFromRegister">Login</button>
  1870. </div>
  1871. </div>
  1872. </div>
  1873.  
  1874. <div class="modal-backdrop" id="accountModal">
  1875. <div class="modal-panel" role="dialog">
  1876.  
  1877. <div class="modal-head">
  1878. <div>
  1879. <div class="modal-title">Account Settings</div>
  1880. <div class="modal-sub" id="accountModalSub"></div>
  1881. </div>
  1882. <button class="modal-close" id="accountModalClose">✕</button>
  1883. </div>
  1884.  
  1885. <div class="modal-body">
  1886.  
  1887. <form id="accountForm">
  1888.  
  1889. <div class="setting-group">
  1890. <div class="setting-label">Discord User ID</div>
  1891. <input
  1892. class="setting-input"
  1893. name="discord_user_id"
  1894. id="accountDiscordId"
  1895. placeholder="Example: 12345678910548458"
  1896. >
  1897. </div>
  1898.  
  1899. <div class="setting-group">
  1900. <div class="setting-label">Current Password</div>
  1901. <input
  1902. class="setting-input"
  1903. type="password"
  1904. name="current_password"
  1905. autocomplete="new-password"
  1906. autocorrect="off"
  1907. autocapitalize="off"
  1908. spellcheck="false"
  1909. >
  1910. </div>
  1911.  
  1912. <div class="setting-group">
  1913. <div class="setting-label">New Password</div>
  1914. <input class="setting-input" type="password" name="new_password">
  1915. </div>
  1916.  
  1917. <div class="setting-group">
  1918. <div class="setting-label">Confirm Password</div>
  1919. <input class="setting-input" type="password" name="confirm_password">
  1920. </div>
  1921.  
  1922. <div class="modal-actions">
  1923. <button type="submit" class="modal-link-btn">Save</button>
  1924. </div>
  1925.  
  1926. <div class="modal-status" id="accountStatus" style="display:none;"></div>
  1927.  
  1928. </form>
  1929.  
  1930. </div>
  1931. </div>
  1932. </div>
  1933.  
  1934. <div class="modal-backdrop" id="createShopModal">
  1935. <div class="modal-panel" role="dialog">
  1936. <div class="modal-head">
  1937. <div>
  1938. <div class="modal-title">Create Shop Request</div>
  1939. <div class="modal-sub">Send a request for a new shop</div>
  1940. </div>
  1941. <button class="modal-close" id="createShopModalClose" type="button">✕</button>
  1942. </div>
  1943.  
  1944. <div class="modal-body">
  1945. <form id="createShopForm">
  1946. <div class="setting-group">
  1947. <div class="setting-label">Shop name</div>
  1948. <input
  1949. class="setting-input"
  1950. type="text"
  1951. name="shop_name"
  1952. id="createShopName"
  1953. placeholder="Example: The Disturbed Cavemen"
  1954. required
  1955. >
  1956. </div>
  1957.  
  1958. <div class="setting-group">
  1959. <div class="setting-label">Preferred slug</div>
  1960. <input
  1961. class="setting-input"
  1962. type="text"
  1963. name="shop_slug"
  1964. id="createShopSlug"
  1965. placeholder="Example: rdy = Ready Or Not | pur = Purple People"
  1966. required
  1967. >
  1968. </div>
  1969.  
  1970. <div class="setting-group">
  1971. <div class="setting-label">Discord Username / Contact</div>
  1972. <input
  1973. class="setting-input"
  1974. type="text"
  1975. name="contact_info"
  1976. id="createShopContact"
  1977. placeholder="Example: Cherie"
  1978. >
  1979. </div>
  1980.  
  1981. <div class="setting-group">
  1982. <div class="setting-label">Request details</div>
  1983. <textarea
  1984. class="setting-input"
  1985. name="request_message"
  1986. id="createShopMessage"
  1987. placeholder="Why do u want a shop here..."
  1988. ></textarea>
  1989. </div>
  1990.  
  1991. <div class="modal-actions">
  1992. <button type="submit" class="modal-link-btn">Send Request</button>
  1993. </div>
  1994.  
  1995. <div class="modal-status" id="createShopStatus" style="display:none;"></div>
  1996. </form>
  1997. </div>
  1998. </div>
  1999. </div>
  2000.  
  2001. <script>
  2002. const vid = document.getElementById('bgVideo');
  2003. if (vid) {
  2004. vid.addEventListener('error', () => {
  2005. vid.style.display = 'none';
  2006. });
  2007. }
  2008.  
  2009. const loginModal = document.getElementById("loginModal");
  2010. const registerModal = document.getElementById("registerModal");
  2011.  
  2012. const openLogin = document.getElementById("openLogin");
  2013. const closeLogin = document.getElementById("closeLogin");
  2014. const closeRegister = document.getElementById("closeRegister");
  2015. const openRegisterFromLogin = document.getElementById("openRegisterFromLogin");
  2016. const openLoginFromRegister = document.getElementById("openLoginFromRegister");
  2017.  
  2018. function openModal(modal){
  2019. modal.classList.add("open");
  2020. }
  2021.  
  2022. function closeModal(modal){
  2023. modal.classList.remove("open");
  2024. }
  2025.  
  2026. function switchModal(fromModal, toModal){
  2027. closeModal(fromModal);
  2028. openModal(toModal);
  2029. }
  2030.  
  2031. if (openLogin) {
  2032. openLogin.onclick = () => openModal(loginModal);
  2033. }
  2034.  
  2035. if (closeLogin) {
  2036. closeLogin.onclick = () => closeModal(loginModal);
  2037. }
  2038.  
  2039. if (closeRegister) {
  2040. closeRegister.onclick = () => closeModal(registerModal);
  2041. }
  2042.  
  2043. if (openRegisterFromLogin) {
  2044. openRegisterFromLogin.onclick = () => switchModal(loginModal, registerModal);
  2045. }
  2046.  
  2047. if (openLoginFromRegister) {
  2048. openLoginFromRegister.onclick = () => switchModal(registerModal, loginModal);
  2049. }
  2050.  
  2051. document.addEventListener("keydown", (e) => {
  2052. if (e.key === "Escape") {
  2053. closeModal(loginModal);
  2054. closeModal(registerModal);
  2055. }
  2056. });
  2057.  
  2058. <?php if (!empty($_GET['register'])): ?>
  2059. openModal(registerModal);
  2060. <?php endif; ?>
  2061.  
  2062. <?php if (!empty($_GET['login'])): ?>
  2063. openModal(loginModal);
  2064. <?php endif; ?>
  2065.  
  2066. // shop search
  2067. (() => {
  2068. const searchInput = document.getElementById('shopSearch');
  2069. const items = Array.from(document.querySelectorAll('.shop-search-item'));
  2070. const emptyState = document.getElementById('shopSearchEmpty');
  2071.  
  2072. if (!searchInput || !items.length) return;
  2073.  
  2074. function runSearch() {
  2075. const q = (searchInput.value || '').trim().toLowerCase();
  2076. let visible = 0;
  2077.  
  2078. items.forEach(item => {
  2079. const name = (item.dataset.shopName || '').toLowerCase();
  2080. const show = !q || name.includes(q);
  2081. item.classList.toggle('shop-hidden', !show);
  2082. if (show) visible++;
  2083. });
  2084.  
  2085. if (emptyState) {
  2086. emptyState.style.display = visible ? 'none' : 'block';
  2087. }
  2088. }
  2089.  
  2090. searchInput.addEventListener('input', runSearch);
  2091.  
  2092. document.addEventListener('keydown', (e) => {
  2093. const activeTag = document.activeElement ? document.activeElement.tagName : '';
  2094. if (e.key === '/' && activeTag !== 'INPUT' && activeTag !== 'TEXTAREA') {
  2095. e.preventDefault();
  2096. searchInput.focus();
  2097. searchInput.select();
  2098. }
  2099. });
  2100. })();
  2101.  
  2102. (() => {
  2103. const modal = document.getElementById('accountModal');
  2104. const closeBtn = document.getElementById('accountModalClose');
  2105. const subEl = document.getElementById('accountModalSub');
  2106. const discordInput = document.getElementById('accountDiscordId');
  2107. const form = document.getElementById('accountForm');
  2108. const statusEl = document.getElementById('accountStatus');
  2109.  
  2110. if (!modal || !form) return;
  2111.  
  2112. function openAccountModal(btn) {
  2113.  
  2114. const discordId = btn.dataset.discordId || '';
  2115.  
  2116. subEl.textContent =
  2117. (btn.dataset.username || '') + ' • ' + (btn.dataset.role || 'User');
  2118.  
  2119. discordInput.value = discordId;
  2120.  
  2121. const currentDiscordEl = document.getElementById('accountDiscordCurrent');
  2122.  
  2123. if (currentDiscordEl) {
  2124. if (discordId) {
  2125. currentDiscordEl.textContent = 'Current Discord ID: ' + discordId;
  2126. } else {
  2127. currentDiscordEl.textContent = 'No Discord ID saved yet.';
  2128. }
  2129. }
  2130.  
  2131. statusEl.style.display = 'none';
  2132. statusEl.textContent = '';
  2133. statusEl.className = 'modal-status';
  2134.  
  2135. modal.classList.add('open');
  2136. document.body.style.overflow = 'hidden';
  2137. }
  2138.  
  2139. function closeAccountModal() {
  2140. modal.classList.remove('open');
  2141. document.body.style.overflow = '';
  2142. }
  2143.  
  2144. document.querySelectorAll('.js-open-account').forEach(btn => {
  2145. btn.addEventListener('click', () => openAccountModal(btn));
  2146. });
  2147.  
  2148. if (closeBtn) {
  2149. closeBtn.addEventListener('click', closeAccountModal);
  2150. }
  2151.  
  2152. window.addEventListener('keydown', e => {
  2153. if (e.key === 'Escape' && modal.classList.contains('open')) {
  2154. closeAccountModal();
  2155. }
  2156. });
  2157.  
  2158. form.addEventListener('submit', async (e) => {
  2159. e.preventDefault();
  2160.  
  2161. const formData = new FormData(form);
  2162.  
  2163. const res = await fetch('/api/panel_account_api.php', {
  2164. method: 'POST',
  2165. body: formData
  2166. });
  2167.  
  2168. const text = await res.text();
  2169.  
  2170. statusEl.style.display = 'block';
  2171.  
  2172. if (!res.ok) {
  2173. statusEl.className = 'modal-status err';
  2174. statusEl.textContent = text || 'Could not save settings.';
  2175. return;
  2176. }
  2177.  
  2178. statusEl.className = 'modal-status ok';
  2179. statusEl.textContent = text || 'Account settings updated.';
  2180. });
  2181. })();
  2182.  
  2183. (() => {
  2184. const activityList = document.getElementById('activityList');
  2185. const activityEmpty = document.getElementById('activityEmpty');
  2186.  
  2187. if (!activityList) return;
  2188.  
  2189. let knownKeys = [];
  2190. let firstLoadDone = false;
  2191. let isRendering = false;
  2192.  
  2193. function iconFor(type) {
  2194. if (type === 'creature') return '🐲';
  2195. if (type === 'blueprint') return '📘';
  2196. if (type === 'trait') return '🧬';
  2197. return '📜';
  2198. }
  2199.  
  2200. function labelFor(type) {
  2201. if (type === 'creature') return 'New creature listing:';
  2202. if (type === 'blueprint') return 'New blueprint listing:';
  2203. if (type === 'trait') return 'New trait listing:';
  2204. return 'New WTB listing:';
  2205. }
  2206.  
  2207. function escapeHtml(str) {
  2208. return String(str ?? '')
  2209. .replaceAll('&', '&amp;')
  2210. .replaceAll('<', '&lt;')
  2211. .replaceAll('>', '&gt;')
  2212. .replaceAll('"', '&quot;')
  2213. .replaceAll("'", '&#039;');
  2214. }
  2215.  
  2216. function rowKey(item) {
  2217. return [
  2218. item.activity_type || '',
  2219. item.title || '',
  2220. item.shop_slug || '',
  2221. item.created_at || ''
  2222. ].join('|');
  2223. }
  2224.  
  2225. function formatLocalDate(isoString) {
  2226. if (!isoString) return '';
  2227.  
  2228. const d = new Date(isoString);
  2229. if (isNaN(d.getTime())) return isoString;
  2230.  
  2231. const day = String(d.getDate()).padStart(2, '0');
  2232. const month = String(d.getMonth() + 1).padStart(2, '0');
  2233. const year = d.getFullYear();
  2234. const hours = String(d.getHours()).padStart(2, '0');
  2235. const minutes = String(d.getMinutes()).padStart(2, '0');
  2236.  
  2237. return `${day}-${month}-${year} ${hours}:${minutes}`;
  2238. }
  2239.  
  2240. function applyLocalTimes(scope = document) {
  2241. scope.querySelectorAll('.js-local-time').forEach(el => {
  2242. const raw = el.dataset.time || '';
  2243. el.textContent = formatLocalDate(raw);
  2244. });
  2245. }
  2246.  
  2247. function buildRowHtml(item, extraClass = '') {
  2248. return `
  2249. <div class="activity-row ${extraClass}" data-key="${escapeHtml(rowKey(item))}">
  2250. <div class="activity-main">
  2251. ${iconFor(item.activity_type)} ${labelFor(item.activity_type)}
  2252. <strong>${escapeHtml(item.title || 'Untitled')}</strong>
  2253. by
  2254. <a href="?shop=${encodeURIComponent(item.shop_slug || '')}">
  2255. ${escapeHtml(item.shop_name || 'Unknown Shop')}
  2256. </a>
  2257. </div>
  2258. <div class="activity-meta js-local-time" data-time="${escapeHtml(item.created_at || '')}">
  2259. ${escapeHtml(formatLocalDate(item.created_at || ''))}
  2260. </div>
  2261. </div>
  2262. `;
  2263. }
  2264.  
  2265. function renderInitial(items) {
  2266. if (!Array.isArray(items) || !items.length) {
  2267. activityList.innerHTML = '';
  2268. if (activityEmpty) activityEmpty.style.display = 'block';
  2269. knownKeys = [];
  2270. return;
  2271. }
  2272.  
  2273. if (activityEmpty) activityEmpty.style.display = 'none';
  2274. activityList.innerHTML = items.map(item => buildRowHtml(item)).join('');
  2275. knownKeys = items.map(rowKey);
  2276. applyLocalTimes(activityList);
  2277. }
  2278.  
  2279. function animateUpdate(items) {
  2280. if (isRendering) return;
  2281. isRendering = true;
  2282.  
  2283. if (!Array.isArray(items) || !items.length) {
  2284. activityList.innerHTML = '';
  2285. if (activityEmpty) activityEmpty.style.display = 'block';
  2286. knownKeys = [];
  2287. isRendering = false;
  2288. return;
  2289. }
  2290.  
  2291. if (activityEmpty) activityEmpty.style.display = 'none';
  2292.  
  2293. const newKeys = items.map(rowKey);
  2294. const insertedKeys = newKeys.filter(k => !knownKeys.includes(k));
  2295. const removedKeys = knownKeys.filter(k => !newKeys.includes(k));
  2296.  
  2297. const currentRows = Array.from(activityList.querySelectorAll('.activity-row'));
  2298. const currentTopMap = new Map();
  2299.  
  2300. currentRows.forEach(row => {
  2301. const key = row.dataset.key || '';
  2302. currentTopMap.set(key, row.getBoundingClientRect().top);
  2303. });
  2304.  
  2305. if (removedKeys.length) {
  2306. currentRows.forEach(row => {
  2307. const key = row.dataset.key || '';
  2308. if (removedKeys.includes(key)) {
  2309. row.classList.add('is-leaving');
  2310. }
  2311. });
  2312. }
  2313.  
  2314. setTimeout(() => {
  2315. activityList.innerHTML = items.map(item => {
  2316. const key = rowKey(item);
  2317. const isInserted = insertedKeys.includes(key);
  2318. return buildRowHtml(item, isInserted ? 'is-entering is-highlight' : '');
  2319. }).join('');
  2320.  
  2321. const nextRows = Array.from(activityList.querySelectorAll('.activity-row'));
  2322.  
  2323. nextRows.forEach((row) => {
  2324. const key = row.dataset.key || '';
  2325. const oldTop = currentTopMap.get(key);
  2326. const newTop = row.getBoundingClientRect().top;
  2327.  
  2328. if (oldTop != null) {
  2329. const delta = oldTop - newTop;
  2330. if (delta !== 0) {
  2331. row.style.transform = `translateY(${delta}px)`;
  2332. }
  2333. }
  2334. });
  2335.  
  2336. requestAnimationFrame(() => {
  2337. nextRows.forEach(row => {
  2338. row.style.transform = '';
  2339. });
  2340. });
  2341.  
  2342. nextRows.forEach(row => {
  2343. if (row.classList.contains('is-highlight')) {
  2344. setTimeout(() => row.classList.remove('is-highlight'), 1400);
  2345. }
  2346.  
  2347. if (row.classList.contains('is-entering')) {
  2348. row.addEventListener('animationend', () => {
  2349. row.classList.remove('is-entering');
  2350. }, { once: true });
  2351. }
  2352. });
  2353.  
  2354. applyLocalTimes(activityList);
  2355.  
  2356. knownKeys = newKeys;
  2357.  
  2358. setTimeout(() => {
  2359. isRendering = false;
  2360. }, 500);
  2361. }, removedKeys.length ? 180 : 0);
  2362. }
  2363.  
  2364. async function fetchActivity() {
  2365. try {
  2366. const res = await fetch('/api/latest_activity.php', {
  2367. cache: 'no-store',
  2368. headers: { 'X-Requested-With': 'XMLHttpRequest' }
  2369. });
  2370.  
  2371. if (!res.ok) return;
  2372.  
  2373. const data = await res.json();
  2374. if (!data || !data.ok || !Array.isArray(data.items)) return;
  2375.  
  2376. if (!firstLoadDone) {
  2377. renderInitial(data.items);
  2378. firstLoadDone = true;
  2379. } else {
  2380. animateUpdate(data.items);
  2381. }
  2382. } catch (err) {
  2383. // silent fail
  2384. }
  2385. }
  2386.  
  2387. applyLocalTimes(document);
  2388. fetchActivity();
  2389. setInterval(fetchActivity, 10000);
  2390. })();
  2391.  
  2392. (() => {
  2393. const modal = document.getElementById('createShopModal');
  2394. const openBtn = document.getElementById('openShopRequest');
  2395. const closeBtn = document.getElementById('createShopModalClose');
  2396. const form = document.getElementById('createShopForm');
  2397. const statusEl = document.getElementById('createShopStatus');
  2398. const slugEl = document.getElementById('createShopSlug');
  2399. const nameEl = document.getElementById('createShopName');
  2400.  
  2401. if (!modal || !form || !openBtn) return;
  2402.  
  2403. function slugify(text) {
  2404. return String(text || '')
  2405. .toLowerCase()
  2406. .trim()
  2407. .replace(/[^a-z0-9\s-]/g, '')
  2408. .replace(/\s+/g, '-')
  2409. .replace(/-+/g, '-');
  2410. }
  2411.  
  2412. function openCreateShopModal() {
  2413. statusEl.style.display = 'none';
  2414. statusEl.textContent = '';
  2415. statusEl.className = 'modal-status';
  2416. modal.classList.add('open');
  2417. document.body.style.overflow = 'hidden';
  2418. }
  2419.  
  2420. function closeCreateShopModal() {
  2421. modal.classList.remove('open');
  2422. document.body.style.overflow = '';
  2423. }
  2424.  
  2425. openBtn.addEventListener('click', openCreateShopModal);
  2426.  
  2427. if (closeBtn) {
  2428. closeBtn.addEventListener('click', closeCreateShopModal);
  2429. }
  2430.  
  2431. modal.addEventListener('click', (e) => {
  2432. if (e.target === modal) closeCreateShopModal();
  2433. });
  2434.  
  2435. window.addEventListener('keydown', (e) => {
  2436. if (e.key === 'Escape' && modal.classList.contains('open')) {
  2437. closeCreateShopModal();
  2438. }
  2439. });
  2440.  
  2441. if (nameEl && slugEl) {
  2442. nameEl.addEventListener('input', () => {
  2443. if (!slugEl.dataset.userEdited) {
  2444. slugEl.value = slugify(nameEl.value);
  2445. }
  2446. });
  2447.  
  2448. slugEl.addEventListener('input', () => {
  2449. slugEl.dataset.userEdited = '1';
  2450. slugEl.value = slugify(slugEl.value);
  2451. });
  2452. }
  2453.  
  2454. form.addEventListener('submit', async (e) => {
  2455. e.preventDefault();
  2456.  
  2457. statusEl.style.display = 'block';
  2458. statusEl.className = 'modal-status';
  2459. statusEl.textContent = 'Sending request...';
  2460.  
  2461. const formData = new FormData(form);
  2462.  
  2463. try {
  2464. const res = await fetch('/api/create_shop_request.php', {
  2465. method: 'POST',
  2466. body: formData
  2467. });
  2468.  
  2469. const text = await res.text();
  2470.  
  2471. if (!res.ok) {
  2472. statusEl.className = 'modal-status err';
  2473. statusEl.textContent = text || 'Could not send request.';
  2474. return;
  2475. }
  2476.  
  2477. statusEl.className = 'modal-status ok';
  2478. statusEl.textContent = text || 'Shop request sent successfully.';
  2479. form.reset();
  2480.  
  2481. if (slugEl) {
  2482. delete slugEl.dataset.userEdited;
  2483. }
  2484.  
  2485. setTimeout(() => {
  2486. closeCreateShopModal();
  2487. window.location.reload();
  2488. }, 1200);
  2489.  
  2490. } catch (err) {
  2491. statusEl.className = 'modal-status err';
  2492. statusEl.textContent = 'Could not send request.';
  2493. }
  2494. });
  2495. })();
  2496. </script>
  2497. <?php
  2498. // Show debug overlay only when ?debug=1 is in the URL
  2499. if (!empty($_GET['debug'])) {
  2500. require __DIR__ . '/query_debug_overlay.php';
  2501. }
  2502. ?>
  2503. <?php
  2504. unset($_SESSION['register_old'], $_SESSION['login_prefill']);
  2505. ?>
  2506. </body>
  2507. </html>
  2508.  
  2509. File: Latest Activity
  2510.  
  2511. <?php
  2512. declare(strict_types=1);
  2513.  
  2514. require __DIR__ . '/../includes/config/db.php';
  2515.  
  2516. header('Content-Type: application/json; charset=utf-8');
  2517.  
  2518. try {
  2519. $stmt = $pdo->query("
  2520. SELECT * FROM (
  2521. SELECT
  2522. 'creature' AS activity_type,
  2523. ms.species AS title,
  2524. s.name AS shop_name,
  2525. s.slug AS shop_slug,
  2526. ms.created_at AS created_at
  2527. FROM mp_stockboard ms
  2528. JOIN shops s ON s.id = ms.shop_id
  2529. WHERE s.is_active = 1
  2530. AND s.show_on_index = 1
  2531.  
  2532. UNION ALL
  2533.  
  2534. SELECT
  2535. 'trait' AS activity_type,
  2536. CONCAT(mt.species, ' • ', mt.trait_name) AS title,
  2537. s.name AS shop_name,
  2538. s.slug AS shop_slug,
  2539. mt.created_at AS created_at
  2540. FROM mp_traits mt
  2541. JOIN shops s ON s.id = mt.shop_id
  2542. WHERE mt.is_active = 1
  2543. AND s.is_active = 1
  2544. AND s.show_on_index = 1
  2545.  
  2546. UNION ALL
  2547.  
  2548. SELECT
  2549. 'blueprint' AS activity_type,
  2550. mb.name AS title,
  2551. s.name AS shop_name,
  2552. s.slug AS shop_slug,
  2553. mb.created_at AS created_at
  2554. FROM mp_blueprints mb
  2555. JOIN shops s ON s.id = mb.shop_id
  2556. WHERE s.is_active = 1
  2557. AND s.show_on_index = 1
  2558.  
  2559. UNION ALL
  2560.  
  2561. SELECT
  2562. 'wtb' AS activity_type,
  2563. mw.title AS title,
  2564. s.name AS shop_name,
  2565. s.slug AS shop_slug,
  2566. mw.created_at AS created_at
  2567. FROM mp_wtb mw
  2568. JOIN shops s ON s.id = mw.shop_id
  2569. WHERE mw.is_open = 1
  2570. AND s.is_active = 1
  2571. AND s.show_on_index = 1
  2572. ) AS activity_feed
  2573. ORDER BY created_at DESC
  2574. LIMIT 10
  2575. ");
  2576.  
  2577. $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
  2578.  
  2579. foreach ($rows as &$row) {
  2580. $raw = (string)($row['created_at'] ?? '');
  2581. $row['created_at_raw'] = $raw;
  2582. $row['created_at'] = $raw
  2583. ? gmdate('Y-m-d\TH:i:s\Z', strtotime($raw))
  2584. : '';
  2585. }
  2586. unset($row);
  2587.  
  2588. echo json_encode([
  2589. 'ok' => true,
  2590. 'items' => $rows,
  2591. 'server_time' => gmdate('Y-m-d\TH:i:s\Z'),
  2592. ], JSON_UNESCAPED_UNICODE);
  2593.  
  2594. } catch (Throwable $e) {
  2595. http_response_code(500);
  2596. echo json_encode([
  2597. 'ok' => false,
  2598. 'error' => 'Could not load activity.'
  2599. ]);
  2600. }
Advertisement
Add Comment
Please, Sign In to add comment