158 lines
5.9 KiB
HTML
158 lines
5.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Bookmark Search — samantha42.xyz</title>
|
|
|
|
<!-- Link your site's existing stylesheet here -->
|
|
<link rel="stylesheet" href="/static/output.css"/>
|
|
|
|
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
|
|
|
<style>
|
|
/* Minimal fallback styles, scoped to this page only.
|
|
Safe to delete once your stylesheet covers these classes. */
|
|
.search-page { max-width: 720px; margin: 0 auto; padding: 2rem 1rem; gap: 1rem;}
|
|
.search-page__title { margin-bottom: 1.25rem; }
|
|
|
|
.search-bar { display: flex; gap: 0.5rem; margin-bottom: 1.25rem; }
|
|
.search-bar__input {
|
|
flex: 1; padding: 0.6rem 0.9rem; font-size: 1rem;
|
|
border: 1px solid #ccc; border-radius: 6px;
|
|
}
|
|
.search-bar__button {
|
|
padding: 0.6rem 1.1rem; font-size: 1rem;
|
|
border: 1px solid #333; background: #333; color: #fff;
|
|
border-radius: 6px; cursor: pointer;
|
|
}
|
|
.search-bar__button:hover { opacity: 0.9; }
|
|
|
|
.add-form { display: flex; gap: 0.5rem; margin-bottom: 1.25rem; flex-wrap: wrap; }
|
|
.add-form input, .add-form select {
|
|
padding: 0.5rem 0.7rem; font-size: 0.95rem;
|
|
border: 1px solid #ccc; border-radius: 6px;
|
|
}
|
|
.add-form input[name="title"] { flex: 2; min-width: 160px; }
|
|
.add-form input[name="url"] { flex: 2; min-width: 160px; }
|
|
|
|
.categories { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem; }
|
|
.category {
|
|
padding: 0.4rem 0.85rem; font-size: 0.9rem;
|
|
border: 1px solid #ccc; border-radius: 999px;
|
|
background: #fff; cursor: pointer;
|
|
}
|
|
.category[aria-pressed="true"] { background: #333; color: #fff; border-color: #333; }
|
|
|
|
.results { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
.result-card {
|
|
display: block; padding: 0.85rem 1rem; border: 1px solid #e2e2e2;
|
|
border-radius: 8px; text-decoration: none; color: inherit;
|
|
}
|
|
.result-card:hover { border-color: #333; }
|
|
.result-card__title { font-weight: 600; margin: 0 0 0.2rem; }
|
|
.result-card__meta { font-size: 0.85rem; color: #777; }
|
|
|
|
.empty-state { padding: 2rem 0; text-align: center; color: #777; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<main class="search-page">
|
|
<section class="hero" id="about">
|
|
<p class="hero-eyebrow">bookmarked · crawled data</p>
|
|
<h1 >Personal web<br><span>Value self reliance</span>
|
|
</h1>
|
|
</section>
|
|
|
|
<!--
|
|
The form issues the request. Its hx-trigger fires on typing (debounced),
|
|
on submit, and on a custom "category-changed" event dispatched by the
|
|
category buttons below. hx-include picks up both the "q" input and the
|
|
hidden "category" input, since both live inside the form.
|
|
|
|
Server contract: GET /api/bookmarks/search?q=&category=
|
|
returns an HTML fragment — a list of .result-card elements (or the
|
|
.empty-state markup) — which htmx swaps into #results.
|
|
-->
|
|
<form
|
|
class="search-bar"
|
|
role="search"
|
|
id="searchForm"
|
|
hx-get="/api/bookmarks/search"
|
|
hx-trigger="input changed delay:400ms from:#searchInput, submit, category-changed"
|
|
hx-target="#results"
|
|
hx-swap="innerHTML"
|
|
>
|
|
<input
|
|
type="search"
|
|
name="q"
|
|
id="searchInput"
|
|
class="search-bar__input"
|
|
placeholder="Search your bookmarks…"
|
|
autocomplete="off"
|
|
>
|
|
<input type="hidden" name="category" id="categoryInput" value="all">
|
|
<button type="submit" class="search-bar__button">Search</button>
|
|
</form>
|
|
|
|
<!--
|
|
Server contract: POST /api/bookmarks
|
|
Body: title, url, category (form-encoded)
|
|
Returns: a single .result-card fragment, which htmx prepends into
|
|
#results. hx-on::after-request resets the form once the card lands.
|
|
-->
|
|
<form
|
|
class="add-form"
|
|
id="addForm"
|
|
hx-post="/api/bookmarks"
|
|
hx-target="#results"
|
|
hx-swap="afterbegin"
|
|
hx-on::after-request="if(event.detail.successful) this.reset()"
|
|
>
|
|
<input type="text" name="title" placeholder="Title" required>
|
|
<input type="url" name="url" placeholder="https://…" required>
|
|
<select name="category" required>
|
|
<option value="books-and-sources">Books and sources</option>
|
|
<option value="market-data-and-filling-dbs">Market data and filling DBs</option>
|
|
<option value="fashion">Fashion</option>
|
|
<option value="diy-wiki-and-vods">DIY wiki and VODs</option>
|
|
</select>
|
|
<button type="submit" class="search-bar__button">Add</button>
|
|
</form>
|
|
|
|
<div class="categories" id="categories" role="group" aria-label="Filter by category">
|
|
<button type="button" class="category" data-category="all" aria-pressed="true">All</button>
|
|
<button type="button" class="category" data-category="books-and-sources">Books and sources</button>
|
|
<button type="button" class="category" data-category="market-data-and-filling-dbs">Market data and filling DBs</button>
|
|
<button type="button" class="category" data-category="fashion">Fashion</button>
|
|
<button type="button" class="category" data-category="diy-wiki-and-vods">DIY wiki and VODs</button>
|
|
</div>
|
|
|
|
<!--
|
|
Populated by the server response. On first load this is empty; add
|
|
hx-get="/api/bookmarks/search" hx-trigger="load" hx-target="#results"
|
|
to the <main> tag (or to this div) once you want it to load results
|
|
immediately instead of waiting for the first search.
|
|
-->
|
|
<div class="results" id="results"></div>
|
|
</main>
|
|
|
|
<script>
|
|
// Category buttons only manage local UI state (which pill is active) and
|
|
// then ask the form to re-run its htmx request. No fetch/JSON handling
|
|
// here — htmx + the server own that part now.
|
|
document.getElementById('categories').addEventListener('click', (e) => {
|
|
const btn = e.target.closest('.category');
|
|
if (!btn) return;
|
|
|
|
document.querySelectorAll('.category').forEach(c => c.setAttribute('aria-pressed', 'false'));
|
|
btn.setAttribute('aria-pressed', 'true');
|
|
|
|
document.getElementById('categoryInput').value = btn.dataset.category;
|
|
htmx.trigger(document.getElementById('searchForm'), 'category-changed');
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html> |