Scripts/news/static/search.html

298 lines
9.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Search</title>
<!-- Simple styling; remove or move to an external sheet as you prefer -->
<style>
/* ─────────── Global title bar ─────────── */
.navbar{
position:fixed;
top:0; left:0; right:0;
height:3rem;
background:#0d47a1;
color:#fff;
display:flex;
align-items:center;
justify-content:space-between;
padding:0 1rem;
box-shadow:0 1px 4px rgba(0,0,0,.15);
z-index:100;
}
.navbar .brand{
font-size:1.15rem;
font-weight:700;
color:#fff;
text-decoration:none;
}
.navbar .nav-link{
color:#fff;
text-decoration:none;
margin-left:1rem;
font-size:.95rem;
}
.navbar .nav-link:hover{
text-decoration:underline;
}
:root {
--accent: #2563eb;
--border-radius: 0.5rem;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, sans-serif;
padding: 4rem 1rem 2rem;
}
/* ────────── Search bar ────────── */
.search-wrapper{
display:flex;
justify-content:center;
margin-bottom:1.5rem;
}
.search-container {
display: flex;
align-items: center;
width: min(32rem, 90vw);
border: 2px solid #e5e7eb;
border-radius: var(--border-radius);
overflow: hidden;
background: #fff;
transition: border-color 0.2s;
}
.search-container:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent);
}
.search-container input {
flex: 1;
border: none;
padding: 0.75rem 1rem;
font-size: 1rem;
outline: none;
}
.search-container button {
background: none;
border: none;
padding: 0 0.75rem;
cursor: pointer;
color: var(--accent);
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.search-container svg {
width: 1.25rem;
height: 1.25rem;
}
/* ────────── Article cards (copied from browse) ────────── */
.article{
background:#fff;
margin:0.5rem auto;
max-width:700px;
padding:1rem 1.25rem;
border-radius:4px;
box-shadow:0 1px 4px rgba(0,0,0,.08);
}
.article h2{
margin:0 0 .5rem;
font-size:1.2rem;
color:#333;
}
.article .meta{
font-size:.85rem;
color:#666;
margin-bottom:.75rem;
}
.article p{
margin:0;
line-height:1.5;
}
.article-link,
.article-link:visited,
.article-link:hover,
.article-link:active,
.article-link:focus {
text-decoration: none;
color: inherit;
}
#loader{
text-align:center;
padding:2rem 0;
color:#777;
}
#endMarker{
text-align:center;
color:#555;
padding:1.5rem 0;
}
</style>
</head>
<body>
<!-- ===== NAVBAR ===== -->
<header class="navbar">
<a href="/news/" class="brand">Newsulizer</a>
<nav>
<a href="/news/" class="nav-link">Home</a>
<a href="/news/browse" class="nav-link">Browse</a>
<a href="/news/search" class="nav-link">Search</a>
</nav>
</header>
<!-- ────────── Search bar ────────── -->
<div class="search-wrapper">
<div class="search-container">
<input
type="search"
id="searchInput"
placeholder="Search..."
autocomplete="off"
aria-label="Search"
/>
<button id="searchButton" aria-label="Submit search">
<!-- Simple magnifying-glass icon -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2">
<circle cx="11" cy="11" r="7" />
<line x1="16.65" y1="16.65" x2="21" y2="21" />
</svg>
</button>
</div>
</div>
<!-- ────────── Results feed ────────── -->
<main id="feed"></main>
<div id="loader" hidden>Loading…</div>
<div id="endMarker" hidden>End of results</div>
<!-- ────────── Logic ────────── -->
<script type="module">
const searchInput = document.getElementById('searchInput');
const searchBtn = document.getElementById('searchButton');
const feed = document.getElementById('feed');
const loader = document.getElementById('loader');
const endMarker = document.getElementById('endMarker');
const PAGE_SIZE = 5;
let currentQuery = '';
let isFetching = false;
let reachedEnd = false;
let lastId; // undefined until first batch returns
/* ---------- helpers ---------- */
function toggleLoader(show){ loader.hidden = !show; }
function resetFeed(){
feed.innerHTML = '';
lastId = undefined;
reachedEnd = false;
endMarker.hidden = true;
}
function renderArticle(a){
const [url, meta] = Object.entries(a)[0];
const link = document.createElement('a');
link.className = 'article-link';
link.href = '/news/view?url=' + encodeURIComponent(url);
const article = document.createElement('article');
const txt = meta.processed_text;
let reg = txt.replace(/(\(.*[^\w:_; '.,"\s]+.*\))/g, '')
reg = reg.replace(/(\[.*])/g, '')
reg = reg.replace(/([^\w:_; '.,"\s/]+)/g, '')
const words = reg.split(/\s/g)
reg = words.slice(0, Math.min(60, words.length)).join(' ').trim();
if (reg.endsWith('.'))
reg += "..";
else
reg += "...";
article.className = 'article';
article.innerHTML = `
<h2>${escapeHtml(meta.title)}</h2>
<p>${escapeHtml(reg ?? '')}</p>
`;
link.appendChild(article);
feed.appendChild(link);
}
/* ---------- fetch & scroll ---------- */
async function loadMore(){
if (reachedEnd || isFetching || !currentQuery.trim()) return;
isFetching = true;
toggleLoader(true);
try{
const url = new URL('/news/api/search', window.location.origin);
url.searchParams.set('text', currentQuery);
url.searchParams.set('count', PAGE_SIZE);
if (lastId !== undefined) url.searchParams.set('last', lastId);
const resp = await fetch(url);
if (!resp.ok) throw new Error(`Server responded ${resp.status}`);
const data = await resp.json(); // expecting an array
if (!Array.isArray(data)) throw new Error('Invalid payload');
if (data.length === 0){
reachedEnd = true;
endMarker.hidden = false;
} else {
data.forEach(renderArticle);
lastId = Object.entries(data[data.length - 1])[0][1].id;
console.log(data);
if (data.length < PAGE_SIZE){
reachedEnd = true;
endMarker.hidden = false;
}
}
}catch(err){
console.error(err);
}finally{
isFetching = false;
toggleLoader(false);
}
}
window.addEventListener('scroll', () => {
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 600;
if (nearBottom) loadMore();
});
/* ---------- search triggers ---------- */
function performSearch(){
const query = searchInput.value.trim();
if (!query) return;
currentQuery = query;
resetFeed();
loadMore();
}
const txt = document.createElement('textarea');
function escapeHtml(str){
txt.textContent = str;
return txt.innerHTML;
}
searchBtn.addEventListener('click', performSearch);
searchInput.addEventListener('keydown', e => {
if (e.key === 'Enter') performSearch();
});
let debounceTimer;
searchInput.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
performSearch();
}, 400); // ~0.4 s debounce; adjust as needed
});
/* optional: focus input on page load */
window.addEventListener('DOMContentLoaded', () => searchInput.focus());
</script>
</body>
</html>