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

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