templates/website/page/indexes/test-selection.html.twig line 1

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8" />
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6.     <title>Select a TOEFL Test - Vrshikyans</title>
  7.     <link rel="icon" href="/students/Vrshikyan_logo.ico" />
  8.     <style>
  9.         :root {
  10.             --bg: #0f1320;
  11.             --bg-2: #101626;
  12.             --surface: rgba(255, 255, 255, 0.06);
  13.             --surface-strong: rgba(255, 255, 255, 0.10);
  14.             --border: rgba(255, 255, 255, 0.18);
  15.             --text: #eef3ff;
  16.             --muted: #b8c2e6;
  17.             --accent: #74a3ff;
  18.             /* default blue */
  19.             --accent-2: #66e6d2;
  20.             /* teal */
  21.             --green: #35d49f;
  22.             /* completed */
  23.             --amber: #ffd166;
  24.             /* partial */
  25.             --red: #ff6b7c;
  26.             /* not started */
  27.             --radius-sm: 10px;
  28.             --radius-md: 14px;
  29.             --radius-lg: 18px;
  30.             --shadow-1: 0 6px 20px rgba(0, 0, 0, .25);
  31.             --shadow-2: 0 14px 40px rgba(0, 0, 0, .35);
  32.             --blur: 14px;
  33.             --speed-fast: .15s;
  34.             --speed: .25s;
  35.             --font: system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
  36.         }
  37.         * {
  38.             box-sizing: border-box;
  39.         }
  40.         html,
  41.         body {
  42.             margin: 0;
  43.             padding: 0;
  44.             min-height: 100%;
  45.             color: var(--text);
  46.             font-family: var(--font);
  47.             background:
  48.                     radial-gradient(1000px 1000px at 10% -10%, #1a2240, transparent 60%),
  49.                     radial-gradient(1400px 800px at 100% 10%, #18244a, transparent 40%),
  50.                     linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
  51.             background-attachment: fixed;
  52.         }
  53.         body::before {
  54.             content: "";
  55.             position: fixed;
  56.             inset: 0;
  57.             background-image:
  58.                     radial-gradient(2px 2px at 10% 20%, rgba(255, 255, 255, .14) 50%, transparent 51%),
  59.                     radial-gradient(2px 2px at 70% 30%, rgba(255, 255, 255, .09) 50%, transparent 51%),
  60.                     radial-gradient(2px 2px at 30% 80%, rgba(255, 255, 255, .07) 50%, transparent 51%),
  61.                     radial-gradient(2px 2px at 80% 60%, rgba(255, 255, 255, .10) 50%, transparent 51%);
  62.             pointer-events: none;
  63.             opacity: .35;
  64.             z-index: 0;
  65.         }
  66.         .topp{
  67.             margin: auto;
  68.             /* border: 2px solid whitesmoke; */
  69.             backdrop-filter: blur(8px);
  70.             -webkit-backdrop-filter: blur(9px);
  71.             width: min(1200px, 94%);
  72.             height: 10vh;
  73.             border-bottom-left-radius: 15px;
  74.             border-bottom-right-radius: 15px;
  75.         }
  76.         .shell {
  77.             width: min(1200px, 94%);
  78.             margin: 32px auto 80px;
  79.             position: relative;
  80.             z-index: 1;
  81.         }
  82.         .header {
  83.             background: var(--surface);
  84.             border: 1px solid var(--border);
  85.             border-radius: var(--radius-lg);
  86.             padding: 16px;
  87.             box-shadow: var(--shadow-2);
  88.             backdrop-filter: blur(var(--blur));
  89.             display: grid;
  90.             grid-template-columns: 1fr auto;
  91.             gap: 12px;
  92.             align-items: center;
  93.         }
  94.         .header h2 {
  95.             margin: 0;
  96.             font-size: 28px;
  97.             letter-spacing: .2px;
  98.         }
  99.         .tools {
  100.             display: flex;
  101.             gap: 10px;
  102.             align-items: center;
  103.         }
  104.         .search {
  105.             position: relative;
  106.         }
  107.         .search input {
  108.             width: 220px;
  109.             height: 40px;
  110.             padding: 0 12px;
  111.             border-radius: 12px;
  112.             border: 1px solid var(--border);
  113.             background: var(--surface-strong);
  114.             color: var(--text);
  115.             outline: none;
  116.             transition: box-shadow var(--speed), transform var(--speed-fast);
  117.         }
  118.         .search input::placeholder {
  119.             color: rgba(255, 255, 255, .55);
  120.         }
  121.         .search input:focus {
  122.             box-shadow: 0 0 0 3px rgba(116, 163, 255, .25);
  123.             transform: translateY(-1px);
  124.         }
  125.         .filter {
  126.             height: 40px;
  127.             border-radius: 12px;
  128.             border: 1px solid var(--border);
  129.             background: var(--surface-strong);
  130.             color: var(--text);
  131.             padding: 0 12px;
  132.             color: gray;
  133.             outline: none;
  134.             min-width: 150px;
  135.         }
  136.         #testList {
  137.             display: grid;
  138.             grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  139.             gap: 14px;
  140.             margin-top: 16px;
  141.             background: var(--surface);
  142.             border: 1px solid var(--border);
  143.             border-radius: var(--radius-lg);
  144.             padding: 16px;
  145.             box-shadow: var(--shadow-2);
  146.             backdrop-filter: blur(var(--blur));
  147.             min-height: 160px;
  148.         }
  149.         .emptyState {
  150.             grid-column: 1 / -1;
  151.             text-align: center;
  152.             color: var(--muted);
  153.             padding: 26px 0;
  154.             font-size: 14px;
  155.         }
  156.         .testButton {
  157.             height: 120px;
  158.             border: 1px solid var(--border);
  159.             border-radius: 16px;
  160.             background: linear-gradient(180deg, rgba(255, 255, 255, .08), rgba(255, 255, 255, .02));
  161.             color: var(--text);
  162.             font-weight: 900;
  163.             font-size: 22px;
  164.             letter-spacing: .3px;
  165.             display: flex;
  166.             align-items: center;
  167.             justify-content: center;
  168.             cursor: pointer;
  169.             position: relative;
  170.             overflow: hidden;
  171.             transition: transform var(--speed-fast), box-shadow var(--speed), border-color var(--speed), background var(--speed);
  172.             box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255, 255, 255, .06);
  173.             user-select: none;
  174.             animation: popIn .35s ease forwards;
  175.             opacity: 0;
  176.             transform: translateY(4px) scale(.98);
  177.         }
  178.         .testButton::before {
  179.             content: "";
  180.             position: absolute;
  181.             inset: 0;
  182.             background: radial-gradient(600px 300px at -10% -10%, rgba(116, 163, 255, .15), transparent 60%);
  183.             opacity: .4;
  184.             pointer-events: none;
  185.         }
  186.         .testButton:hover {
  187.             transform: translateY(-2px) scale(1.02);
  188.             box-shadow: 0 16px 36px rgba(0, 0, 0, .30), inset 0 1px 0 rgba(255, 255, 255, .08);
  189.             border-color: rgba(116, 163, 255, .55);
  190.             background: linear-gradient(180deg, rgba(255, 255, 255, .12), rgba(255, 255, 255, .04));
  191.         }
  192.         @keyframes popIn {
  193.             to {
  194.                 opacity: 1;
  195.                 transform: translateY(0) scale(1);
  196.             }
  197.         }
  198.         .ribbon {
  199.             position: absolute;
  200.             top: 10px;
  201.             right: -38px;
  202.             width: 140px;
  203.             text-align: center;
  204.             transform: rotate(35deg);
  205.             font-size: 11px;
  206.             font-weight: 900;
  207.             letter-spacing: .3px;
  208.             color: #071b14;
  209.             padding: 4px 0;
  210.             box-shadow: 0 4px 14px rgba(0, 0, 0, .20);
  211.             border: 1px solid rgba(0, 0, 0, .1);
  212.         }
  213.         .ribbon.done {
  214.             background: linear-gradient(180deg, #35d49f, #18b98a);
  215.         }
  216.         .ribbon.part {
  217.             background: linear-gradient(180deg, #ffd166, #ffbf55);
  218.         }
  219.         .ribbon.none {
  220.             background: linear-gradient(180deg, #ff9aa8, #ff6b7c);
  221.         }
  222.         .testButton.completed {
  223.             box-shadow: 0 12px 26px rgba(53, 212, 159, .20), inset 0 1px 0 rgba(255, 255, 255, .08);
  224.             border-color: rgba(53, 212, 159, .45);
  225.         }
  226.         .testButton.partial {
  227.             box-shadow: 0 12px 26px rgba(255, 209, 102, .20), inset 0 1px 0 rgba(255, 255, 255, .08);
  228.             border-color: rgba(255, 209, 102, .45);
  229.         }
  230.         .testButton.not-started {
  231.             box-shadow: 0 12px 26px rgba(255, 107, 124, .20), inset 0 1px 0 rgba(255, 255, 255, .08);
  232.             border-color: rgba(255, 107, 124, .45);
  233.         }
  234.         .legend {
  235.             display: flex;
  236.             gap: 10px;
  237.             align-items: center;
  238.             margin-top: 10px;
  239.             color: var(--muted);
  240.             font-size: 13px;
  241.         }
  242.         .topp button{min-width: 35%; background:linear-gradient(180deg,#35d49f,#18b98a);color:#041b12;box-shadow:0 10px 20px rgba(24,185,138,.18), inset 0 1px 0 rgba(255,255,255,.5); height:46px;min-width:160px;padding:0 18px;border:0;border-radius:14px;font-weight:900;letter-spacing:.2px;cursor:pointer;transition:transform var(--speed-fast), box-shadow var(--speed)}
  243.         .topp button:hover{transform:translateY(-1px)}
  244.         .dot {
  245.             width: 10px;
  246.             height: 10px;
  247.             border-radius: 50%;
  248.             display: inline-block;
  249.             margin-right: 6px;
  250.         }
  251.         .dot.done {
  252.             background: var(--green);
  253.         }
  254.         .dot.part {
  255.             background: var(--amber);
  256.         }
  257.         .dot.none {
  258.             background: var(--red);
  259.         }
  260.         .testButton[title] {
  261.             position: relative;
  262.         }
  263.         .testButton[title]:hover::after {
  264.             content: attr(title);
  265.             position: absolute;
  266.             bottom: -34px;
  267.             left: 50%;
  268.             transform: translateX(-50%);
  269.             background: rgba(0, 0, 0, .75);
  270.             color: #fff;
  271.             padding: 6px 10px;
  272.             border-radius: 8px;
  273.             font-size: 12px;
  274.             white-space: nowrap;
  275.             pointer-events: none;
  276.         }
  277.         #go{height:40px;width:20%;text-align:center; padding:0 18px;border:0;border-radius:14px;font-weight:900;letter-spacing:.2px;cursor:pointer;transition:transform var(--speed-fast), box-shadow var(--speed), filter var(--speed); background:linear-gradient(180deg, rgb(244, 228, 228), rgb(98, 91, 91));color:#041b12;box-shadow:0 10px 24px rgba(24,185,138,.22), inset 0 1px 0 rgba(255,255,255,.5)}
  278.         #go:hover{transform: translateY(-5px);}
  279.         @media (max-width: 700px) {
  280.             .header {
  281.                 grid-template-columns: 1fr;
  282.             }
  283.             #go{width: 100%; margin-top: 10px;}
  284.             .tools {
  285.                 display:contents;
  286.             }
  287.             .search input {
  288.                 width: 100%;
  289.             }
  290.         }
  291.     </style>
  292.     <link rel="stylesheet" href="{{ asset('css/navbar.css') }}" />
  293.     <script>
  294.   (() => {
  295.     const saved = localStorage.getItem('theme');
  296.     if (saved) {
  297.       document.documentElement.setAttribute('data-theme', saved);
  298.     } else {
  299.       document.documentElement.setAttribute(
  300.         'data-theme',
  301.         matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  302.       );
  303.     }
  304.   })();
  305. </script>
  306. </head>
  307. <body>
  308.   <nav class="nav" role="navigation" aria-label="Primary">
  309.   <div class="nav-inner">
  310.     <a class="brand" href="/" aria-label="Exam-hub">
  311.       <span class="logo" aria-hidden="true" style="overflow: hidden;">
  312.         <img src="../students/Vrshikyan_logo.ico" width="39px" alt="Vrshikyans logo">
  313.       </span>
  314.       <span>Vrshikyans</span>&nbsp;&nbsp;
  315.     </a>
  316.     <div class="nav-links">
  317.       <a class="nav-link" data-section="toefl" href="./test-selection?type=toefl" id="toeflNav">TOEFL</a>
  318.       <a class="nav-link" data-section="sat" href="./test-sat-selection#sat" >SAT</a>
  319.       <a class="nav-link" data-section="act" href="./test-sat-selection#act">ACT</a>
  320.       <a class="nav-link" data-section="reading" href="./test-selection?type=readingPractice" id="readingPrNav">Reading Practice</a>
  321.       <a class="nav-link" data-section="support" href="./support">Support</a>
  322.       <a class="nav-link" data-section="reviews" href="./reviews">Reviews</a>
  323.     </div>
  324.     <button class="mobile-menu-btn" id="mobileMenuBtn">☰</button>
  325.     <div class="spacer"></div>
  326.     <button id="themeToggle" class="theme-btn" aria-label="Toggle theme">🌑</button>
  327.     <a class="cta" id="startTop" href="../indexes/account" aria-label="Account"
  328.       style="width: 40px; height: 40px; background: whitesmoke;">👤</a>
  329.   </div>
  330. </nav>
  331. <div class="mobile-menu" id="mobileMenu">
  332.   <div class="mobile-nav-links">
  333.     <a class="mobile-nav-link" href="./test-selection?type=toefl">TOEFL</a>
  334.     <a class="mobile-nav-link" href="./test-sat-selection#sat">SAT</a>
  335.     <a class="mobile-nav-link" href="./test-sat-selection#act">ACT</a>
  336.     <a class="mobile-nav-link" href="./test-selection?type=readingPractice">Reading Practice</a>
  337.     <a class="mobile-nav-link" href="./support">Support</a>
  338.     <a class="mobile-nav-link" href="./reviews">Reviews</a>
  339.   </div>
  340. </div>
  341. <div class="shell">
  342.     <div class="header">
  343.         <h2>Select a Test</h2>
  344.         <div class="tools">
  345.             <select id="typeFilter" class="filter" title="Test type">
  346.                 <option value="toefl">TPO</option>
  347.                 <option value="sh">SH</option>
  348.                 <option value="toefl2026">TOEFL 2026</option>
  349.                 <option value="readingPractice">Reading practice</option>
  350.             </select>
  351.             <div class="search">
  352.                 <input id="quickFind" type="number" min="1" placeholder="Go to test #" />
  353.                 <button id="go">Go</button>
  354.             </div>
  355.             <select id="statusFilter" class="filter" title="Filter by status">
  356.                 <option value="all">All</option>
  357.                 <option value="completed">Completed</option>
  358.                 <option value="partial">In progress</option>
  359.                 <option value="not-started">Not started</option>
  360.             </select>
  361.         </div>
  362.     </div>
  363.     <div class="legend">
  364.         <span><span class="dot done"></span>Completed</span>
  365.         <span><span class="dot part"></span>In progress</span>
  366.         <span><span class="dot none"></span>Not started</span>
  367.     </div>
  368.     <div id="testList"></div>
  369. </div>
  370. <script src="{{ asset('js/navbar.js') }}"></script>
  371. <script>
  372.     const TEST_TYPES = {
  373.         toefl: { label: 'TPO', total: 65 },
  374.         sh: { label: 'SH', total: 4 },
  375.         toefl2026: { label: 'TOEFL 2026', total: 10 },
  376.         readingPractice: { label: 'Reading practice', total: 66 }
  377.     };
  378.     const SECTION_PAGE = '/indexes/section-selection';
  379.     const testListEl = document.getElementById('testList');
  380.     const quickFind = document.getElementById('quickFind');
  381.     const statusFilter = document.getElementById('statusFilter');
  382.     const typeFilter = document.getElementById('typeFilter');
  383. const qs = new URLSearchParams(location.search);
  384. // ✅ don't blindly toLowerCase() because readingPractice is camelCase
  385. let initialType = qs.get('type') || localStorage.getItem('TEST_TYPE') || 'toefl';
  386. // if it's not a known key, try lowercase fallback, else default to toefl
  387. if (!TEST_TYPES[initialType]) {
  388.   const lower = String(initialType).toLowerCase();
  389.   if (TEST_TYPES[lower]) {
  390.     initialType = lower;
  391.   } else {
  392.     initialType = 'toefl';
  393.   }
  394. }
  395. typeFilter.value = initialType;
  396.     function updateQuickFindPlaceholder() {
  397.         const t = typeFilter.value;
  398.         const total = TEST_TYPES[t].total;
  399.         quickFind.placeholder = `Go to ${TEST_TYPES[t].label} test # (1–${total})`;
  400.         quickFind.max = String(total);
  401.     }
  402.     updateQuickFindPlaceholder();
  403.     function getKey(type, id, section) {
  404.         return `result_${type}_${id}_${section}`;
  405.     }
  406.     function getLegacyKey(id, section) {
  407.         return `result_${id}_${section}`;
  408.     }
  409.     function getStatusForTest(type, id) {
  410.         // Reading practice: only reading section matters
  411.         if (type === 'readingPractice') {
  412.             const r = localStorage.getItem(getKey(type, id, 'reading'));
  413.             if (r) return 'completed';
  414.             return 'not-started';
  415.         }
  416.         // For TOEFL 2026, we'll use the new key format only
  417.         if (type === 'toefl2026') {
  418.             const r = localStorage.getItem(getKey(type, id, 'reading'));
  419.             const l = localStorage.getItem(getKey(type, id, 'listening'));
  420.             const s = localStorage.getItem(getKey(type, id, 'speaking'));
  421.             const w = localStorage.getItem(getKey(type, id, 'writing'));
  422.             const hasAny = r || l || s || w;
  423.             const hasAll = r && l && w;
  424.             if (hasAll) return 'completed';
  425.             if (hasAny) return 'partial';
  426.             return 'not-started';
  427.         }
  428.         // For TPO and SH, keep the existing logic with legacy support
  429.         const r = localStorage.getItem(getKey(type, id, 'reading')) || (type === 'toefl' ? localStorage.getItem(getLegacyKey(id, 'reading')) : null);
  430.         const l = localStorage.getItem(getKey(type, id, 'listening')) || (type === 'toefl' ? localStorage.getItem(getLegacyKey(id, 'listening')) : null);
  431.         const s = localStorage.getItem(getKey(type, id, 'speaking')) || (type === 'toefl' ? localStorage.getItem(getLegacyKey(id, 'speaking')) : null);
  432.         const w = localStorage.getItem(getKey(type, id, 'writing')) || (type === 'toefl' ? localStorage.getItem(getLegacyKey(id, 'writing')) : null);
  433.         const hasAny = r || l || s || w;
  434.         const hasAll = r && l && w;
  435.         if (hasAll) return 'completed';
  436.         if (hasAny) return 'partial';
  437.         return 'not-started';
  438.     }
  439.     function ribbonFor(status) {
  440.         if (status === 'completed') return '<span class="ribbon done">COMPLETED</span>';
  441.         if (status === 'partial') return '<span class="ribbon part">IN&nbsp;PROGRESS</span>';
  442.         return '<span class="ribbon none">NOT&nbsp;STARTED</span>';
  443.     }
  444.     function titleFor(status) {
  445.         if (status === 'completed') return 'All sections recorded for this test';
  446.         if (status === 'partial') return 'You started this test';
  447.         return 'No sections done yet';
  448.     }
  449.     function renderGrid() {
  450.         const curType = typeFilter.value;
  451.         const total = TEST_TYPES[curType].total;
  452.         testListEl.innerHTML = '';
  453.         if (!total || total < 1) {
  454.             const empty = document.createElement('div');
  455.             empty.className = 'emptyState';
  456.             empty.textContent = `${TEST_TYPES[curType].label} tests are not defined yet.`;
  457.             testListEl.appendChild(empty);
  458.             return;
  459.         }
  460.         let rendered = 0;
  461.         for (let i = 1; i <= total; i++) {
  462.             const status = getStatusForTest(curType, i);
  463.             if (statusFilter.value !== 'all' && statusFilter.value !== status) continue;
  464.             const btn = document.createElement('button');
  465.             btn.className = `testButton ${status}`;
  466.             btn.title = titleFor(status);
  467.             btn.style.animationDelay = `${i * 8}ms`;
  468.             btn.innerHTML = `${ribbonFor(status)}${TEST_TYPES[curType].label} ${i}`;
  469.             btn.onclick = () => {
  470.                 sessionStorage.setItem('TEST_ID', String(i));
  471.                 sessionStorage.setItem('TEST_TYPE', curType);
  472.                 const url = `${SECTION_PAGE}?type=${encodeURIComponent(curType)}&test=${i}`;
  473.                 window.location.href = url;
  474.             };
  475.             testListEl.appendChild(btn);
  476.             rendered++;
  477.         }
  478.         if (!rendered) {
  479.             const empty = document.createElement('div');
  480.             empty.className = 'emptyState';
  481.             empty.textContent = 'No tests match the current filters.';
  482.             testListEl.appendChild(empty);
  483.         }
  484.     }
  485.     quickFind.addEventListener('keydown', (e) => {
  486.         if (e.key === 'Enter') {
  487.             findTest()
  488.         }
  489.     });
  490.     function findTest() {
  491.         const n = parseInt(quickFind.value, 10);
  492.         const curType = typeFilter.value;
  493.         const total = TEST_TYPES[curType].total;
  494.         if (!Number.isNaN(n) && n >= 1 && n <= total) {
  495.             sessionStorage.setItem('TEST_ID', String(n));
  496.             sessionStorage.setItem('TEST_TYPE', curType);
  497.             const url = `${SECTION_PAGE}?type=${encodeURIComponent(curType)}&test=${n}`;
  498.             window.location.href = url;
  499.         } else {
  500.             quickFind.style.transition = '0.5s';
  501.             quickFind.style.color = 'red';
  502.             setTimeout(() => { quickFind.style.color = ''; }, 2000);
  503.         }
  504.     }
  505.     const go = document.getElementById("go")
  506.     go.addEventListener("click", findTest);
  507.     statusFilter.addEventListener('change', renderGrid);
  508.     typeFilter.addEventListener('change', () => {
  509.         // persist selection, update UI, re-render
  510.         localStorage.setItem('TEST_TYPE', typeFilter.value);
  511.         updateQuickFindPlaceholder();
  512.         renderGrid();
  513.     });
  514. function updateActiveNav() {
  515.   const toeflNav = document.getElementById('toeflNav');
  516.   const readingPrNav = document.getElementById('readingPrNav');
  517.   toeflNav.classList.toggle('active', typeFilter.value === 'toefl');
  518.   readingPrNav.classList.toggle('active', typeFilter.value === 'readingPractice');
  519. }
  520. updateActiveNav();
  521. typeFilter.addEventListener('change', updateActiveNav);
  522.     const headingEl = document.querySelector('.header h2');
  523.     function setTypeInURL(t) {
  524.         const url = new URL(location.href);
  525.         url.searchParams.set('type', t);
  526.         history.replaceState(null, '', url);
  527.     }
  528.     function updateTitles(t) {
  529.         const label = TEST_TYPES[t].label;
  530.         headingEl.textContent = `Select ${label} Tests`;
  531.         document.title = `Select ${label} Tests`;
  532.     }
  533.     (function normalizeWeirdParam() {
  534.         const sp = new URLSearchParams(location.search);
  535.         if (!sp.has('type') && location.search.startsWith('?=')) {
  536.             const val = location.search.slice(2); // after '?='
  537.             if (val === 'toefl' || val === 'sh' || val === 'toefl2026' || val === 'readingPractice') {
  538.                 const url = new URL(location.href);
  539.                 url.search = `?type=${val}`;
  540.                 history.replaceState(null, '', url);
  541.             }
  542.         }
  543.     })();
  544.     updateTitles(typeFilter.value);
  545.     setTypeInURL(typeFilter.value);
  546.     typeFilter.addEventListener('change', () => {
  547.         localStorage.setItem('TEST_TYPE', typeFilter.value);
  548.         updateQuickFindPlaceholder();
  549.         updateTitles(typeFilter.value);
  550.         setTypeInURL(typeFilter.value);
  551.         renderGrid();
  552.     });
  553.     renderGrid();
  554.     document.addEventListener("contextmenu", (e)=> e.preventDefault());
  555. </script>
  556. </body>
  557. </html>