Spaces:
Sleeping
Sleeping
| <!-- Search page (index.html) — input, autocomplete, and theme toggle --> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Subtitle Search</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <style> | |
| :root{ | |
| --maxw: 720px; | |
| --radius: 8px; | |
| --blue: #0b5fff; | |
| --blue-hover: #0848c9; | |
| --border: #d0d7de; | |
| } | |
| body { | |
| font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; | |
| padding: 30px; | |
| color: #222; | |
| background: #fff; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 100vh; | |
| align-items: center; | |
| padding-top: 22vh; | |
| } | |
| h1 { margin-bottom: 16px; color: #222; } | |
| .search-wrap { | |
| position: relative; | |
| max-width: 600px; | |
| width: 100%; | |
| margin: 0 auto; | |
| } | |
| .search-row { | |
| display: flex; | |
| gap: 0; | |
| align-items: stretch; | |
| } | |
| #queryInput { | |
| flex: 1 1 auto; | |
| min-width: 0; | |
| height: 42px; | |
| box-sizing: border-box; | |
| padding: 10px 12px 10px 40px; | |
| font-size: 16px; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius) 0 0 var(--radius); | |
| background: | |
| url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2399a3ad' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='8'/><line x1='21' y1='21' x2='16.65' y2='16.65'/></svg>") | |
| no-repeat 12px center / 18px 18px #fff; | |
| color: #111; | |
| background-color: #fff; | |
| } | |
| #queryInput::placeholder { color: #6b7280; } | |
| #queryInput:focus { | |
| outline: none; | |
| border-color: var(--blue); | |
| box-shadow: 0 0 0 3px rgba(11,95,255,.15); | |
| } | |
| .search-btn { | |
| height: 42px; | |
| box-sizing: border-box; | |
| line-height: 42px; | |
| padding: 0 16px; | |
| font-size: 15px; | |
| font-weight: 600; | |
| color: #fff; | |
| background: var(--blue); | |
| border: 1px solid var(--blue); | |
| border-radius: 0 var(--radius) var(--radius) 0; | |
| cursor: pointer; | |
| flex: 0 0 auto; | |
| } | |
| .search-btn:hover { background: var(--blue-hover); border-color: var(--blue-hover); } | |
| #suggestions { | |
| border: 1px solid #ccc; | |
| border-radius: 6px; | |
| max-width: var(--maxw); | |
| margin-top: 6px; | |
| padding: 0; | |
| list-style: none; | |
| background: #fff; | |
| position: absolute; | |
| top: calc(42px + 6px); | |
| left: 0; | |
| width: 100%; | |
| z-index: 10; | |
| display: none; | |
| box-shadow: 0 8px 16px rgba(0,0,0,0.08); | |
| overflow: hidden; | |
| color: #111; | |
| } | |
| #suggestions.show { display: block; } | |
| #suggestions li { | |
| padding: 10px 12px; | |
| cursor: pointer; | |
| line-height: 1.3; | |
| } | |
| #suggestions li:hover, | |
| #suggestions li.selected { background: #f0f6ff; } | |
| .no-suggestions { | |
| color: #666; | |
| font-style: italic; | |
| padding: 10px 12px; | |
| } | |
| #loading { | |
| font-size: 14px; | |
| color: #666; | |
| margin-top: 6px; | |
| display: none; | |
| } | |
| body, #queryInput, #suggestions, .search-btn, .theme-toggle { | |
| transition: background-color .2s, color .2s, border-color .2s, box-shadow .2s; | |
| } | |
| .theme-toggle { | |
| position: fixed; top: 16px; right: 16px; | |
| background: #f9f9f9; color: #222; | |
| border: 1px solid #00000022; padding: 8px 12px; border-radius: 8px; | |
| font-weight: 600; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.08); | |
| } | |
| .theme-toggle:hover { box-shadow: 0 6px 18px rgba(0,0,0,0.15); } | |
| html[data-theme="dark"] body { background: #0e0f12; color: #e7e9ee; } | |
| html[data-theme="dark"] h1 { color: #e7e9ee; } | |
| html[data-theme="dark"] #queryInput { | |
| background-color: #15171c; | |
| color: #e7e9ee; | |
| border-color: #333; | |
| } | |
| html[data-theme="dark"] #queryInput::placeholder { color: #b3b8c4; } | |
| html[data-theme="dark"] #suggestions { | |
| background: #15171c; | |
| color: #e7e9ee; | |
| border-color: #333; | |
| box-shadow: 0 8px 16px rgba(0,0,0,0.4); | |
| } | |
| html[data-theme="dark"] #suggestions li { color: #e7e9ee; } | |
| html[data-theme="dark"] #suggestions li:hover, | |
| html[data-theme="dark"] li.selected { background: #1d2026; } | |
| html[data-theme="dark"] .no-suggestions { color: #b3b8c4; } | |
| html[data-theme="dark"] #loading { color: #b3b8c4; } | |
| html[data-theme="dark"] .search-btn { | |
| background: #2d7ed8; | |
| border-color: #2d7ed8; | |
| color: #fff; | |
| } | |
| html[data-theme="dark"] .search-btn:hover { | |
| background: #2464ac; | |
| border-color: #2464ac; | |
| } | |
| html[data-theme="dark"] .theme-toggle { | |
| background: #15171c; color: #e7e9ee; border-color: #333; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.4); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Theme toggle --> | |
| <button id="themeToggle" class="theme-toggle" onclick="__toggleTheme()">🌙 Dark</button> | |
| <h1>Keyword Search</h1> | |
| <!-- Search form with autocomplete --> | |
| <form action="/search" method="POST" autocomplete="off" role="search" class="search-wrap"> | |
| <div class="search-row"> | |
| <input | |
| type="text" | |
| name="query" | |
| id="queryInput" | |
| placeholder="Enter your query here (e.g., neural networks)" | |
| size="50" | |
| aria-label="Search input" | |
| aria-autocomplete="list" | |
| aria-controls="suggestions" | |
| aria-expanded="false" | |
| aria-haspopup="listbox"> | |
| <button type="submit" class="search-btn">Search</button> | |
| </div> | |
| <div id="loading" aria-live="polite">Loading suggestions…</div> | |
| <ul id="suggestions" role="listbox" aria-labelledby="queryInput"></ul> | |
| </form> | |
| <!-- Version tag --> | |
| <div style="font-size:0.8em; color:#888; margin-top:6px; text-align:center;"> | |
| Version 1.1 | |
| </div> | |
| <!-- Theme toggle logic --> | |
| <script> | |
| (function() { | |
| const saved = localStorage.getItem('theme'); | |
| if (saved) document.documentElement.dataset.theme = saved; | |
| function setTheme(t) { | |
| document.documentElement.dataset.theme = t; | |
| localStorage.setItem('theme', t); | |
| const btn = document.getElementById('themeToggle'); | |
| if (btn) btn.textContent = t === 'dark' ? ' Light' : ' Dark'; | |
| } | |
| window.__toggleTheme = function() { | |
| const next = (document.documentElement.dataset.theme === 'dark') ? 'light' : 'dark'; | |
| setTheme(next); | |
| }; | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const cur = document.documentElement.dataset.theme || 'light'; | |
| const btn = document.getElementById('themeToggle'); | |
| if (btn) btn.textContent = cur === 'dark' ? ' Light' : ' Dark'; | |
| }); | |
| })(); | |
| </script> | |
| <!-- Autocomplete logic --> | |
| <script> | |
| const input = document.getElementById('queryInput'); | |
| const suggestionBox = document.getElementById('suggestions'); | |
| const loadingEl = document.getElementById('loading'); | |
| const escapeHtml = (str) => | |
| str.replace(/[&<>"']/g, t => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[t])); | |
| let selectedIndex = -1; | |
| input.addEventListener('input', async () => { | |
| const term = input.value.trim(); | |
| suggestionBox.classList.remove('show'); | |
| suggestionBox.innerHTML = ''; | |
| input.setAttribute('aria-expanded', 'false'); | |
| selectedIndex = -1; | |
| if (term.length < 2) { | |
| loadingEl.style.display = 'none'; | |
| return; | |
| } | |
| loadingEl.style.display = 'block'; | |
| try { | |
| const res = await fetch(`/autocomplete?term=${encodeURIComponent(term)}`); | |
| const suggestions = await res.json(); | |
| loadingEl.style.display = 'none'; | |
| if (!Array.isArray(suggestions) || suggestions.length === 0) { | |
| suggestionBox.innerHTML = '<li class="no-suggestions" role="option" aria-disabled="true">No suggestions found</li>'; | |
| suggestionBox.classList.add('show'); | |
| input.setAttribute('aria-expanded', 'true'); | |
| return; | |
| } | |
| suggestionBox.innerHTML = suggestions | |
| .map(s => `<li role="option">${escapeHtml(s)}</li>`) | |
| .join(''); | |
| suggestionBox.classList.add('show'); | |
| input.setAttribute('aria-expanded', 'true'); | |
| } catch (err) { | |
| console.error('Autocomplete error:', err); | |
| loadingEl.style.display = 'none'; | |
| } | |
| }); | |
| suggestionBox.addEventListener('click', (e) => { | |
| const li = e.target.closest('li[role="option"]'); | |
| if (!li || li.classList.contains('no-suggestions')) return; | |
| input.value = li.textContent; | |
| suggestionBox.classList.remove('show'); | |
| suggestionBox.innerHTML = ''; | |
| input.setAttribute('aria-expanded', 'false'); | |
| }); | |
| input.addEventListener('keydown', (e) => { | |
| const items = suggestionBox.querySelectorAll('li[role="option"]:not(.no-suggestions)'); | |
| if (!items.length) return; | |
| if (e.key === 'ArrowDown') { | |
| e.preventDefault(); | |
| selectedIndex = Math.min(selectedIndex + 1, items.length - 1); | |
| updateSelection(items); | |
| } else if (e.key === 'ArrowUp') { | |
| e.preventDefault(); | |
| selectedIndex = Math.max(selectedIndex - 1, 0); | |
| updateSelection(items); | |
| } else if (e.key === 'Enter') { | |
| if (selectedIndex >= 0) { | |
| e.preventDefault(); | |
| input.value = items[selectedIndex].textContent; | |
| suggestionBox.classList.remove('show'); | |
| suggestionBox.innerHTML = ''; | |
| input.setAttribute('aria-expanded', 'false'); | |
| } | |
| } else if (e.key === 'Escape') { | |
| suggestionBox.classList.remove('show'); | |
| suggestionBox.innerHTML = ''; | |
| input.setAttribute('aria-expanded', 'false'); | |
| } | |
| }); | |
| function updateSelection(items) { | |
| items.forEach((item, i) => item.classList.toggle('selected', i === selectedIndex)); | |
| const active = items[selectedIndex]; | |
| if (active) active.scrollIntoView({ block: 'nearest' }); | |
| } | |
| document.addEventListener('click', (e) => { | |
| if (!e.target.closest('form')) { | |
| suggestionBox.classList.remove('show'); | |
| suggestionBox.innerHTML = ''; | |
| input.setAttribute('aria-expanded', 'false'); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |