updated search, added hover to buttons

This commit is contained in:
liyunze 2026-09-01 14:32:37 +08:00
parent 830a04893a
commit f807ae9500
11 changed files with 3302 additions and 3351 deletions

View file

@ -21,9 +21,9 @@ if (!self.define) {
const singleRequire = (uri, parentUri) => { const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href; uri = new URL(uri + ".js", parentUri).href;
return ( return registry[uri] || (
registry[uri] ||
new Promise((resolve) => { new Promise(resolve => {
if ("document" in self) { if ("document" in self) {
const script = document.createElement("script"); const script = document.createElement("script");
script.src = uri; script.src = uri;
@ -47,31 +47,27 @@ if (!self.define) {
}; };
self.define = (depsNames, factory) => { self.define = (depsNames, factory) => {
const uri = const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
nextDefineUri ||
("document" in self ? document.currentScript.src : "") ||
location.href;
if (registry[uri]) { if (registry[uri]) {
// Module is already loading or loaded. // Module is already loading or loaded.
return; return;
} }
let exports = {}; let exports = {};
const require = (depUri) => singleRequire(depUri, uri); const require = depUri => singleRequire(depUri, uri);
const specialDeps = { const specialDeps = {
module: { uri }, module: { uri },
exports, exports,
require, require
}; };
registry[uri] = Promise.all( registry[uri] = Promise.all(depsNames.map(
depsNames.map((depName) => specialDeps[depName] || require(depName)), depName => specialDeps[depName] || require(depName)
).then((deps) => { )).then(deps => {
factory(...deps); factory(...deps);
return exports; return exports;
}); });
}; };
} }
define(["./workbox-7e5eb42b"], function (workbox) { define(['./workbox-7e5eb42b'], (function (workbox) { 'use strict';
"use strict";
self.skipWaiting(); self.skipWaiting();
workbox.clientsClaim(); workbox.clientsClaim();
@ -80,19 +76,13 @@ define(["./workbox-7e5eb42b"], function (workbox) {
* requests for URLs in the manifest. * requests for URLs in the manifest.
* See https://goo.gl/S9QRab * See https://goo.gl/S9QRab
*/ */
workbox.precacheAndRoute( workbox.precacheAndRoute([{
[ "url": "/",
{ "revision": "0.4n2fvpih6f8"
url: "/", }], {});
revision: "0.agglep2qr98",
},
],
{},
);
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute( workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {
new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), { allowlist: [/^\/$/]
allowlist: [/^\/$/], }));
}),
); }));
});

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
voteCount = 0, voteCount = 0,
targetType, targetType,
targetId, targetId,
tag,
}: { }: {
href: string; href: string;
title: string; title: string;
@ -17,12 +18,18 @@
voteCount?: number; voteCount?: number;
targetType: "note" | "question"; targetType: "note" | "question";
targetId: string; targetId: string;
tag?: string;
} = $props(); } = $props();
</script> </script>
<article class="border-rule flex gap-4 border-b py-5"> <article class="border-rule flex gap-4 border-b py-5">
<VoteStack count={voteCount} {targetType} {targetId} /> <VoteStack count={voteCount} {targetType} {targetId} />
<a {href} class="group min-w-0 flex-1"> <a {href} class="group min-w-0 flex-1">
{#if tag}
<p class="text-secondary mb-1 text-[10px] font-semibold tracking-[0.16em] uppercase">
{tag}
</p>
{/if}
{#if unitCode} {#if unitCode}
<p class="kicker">{unitCode}</p> <p class="kicker">{unitCode}</p>
{/if} {/if}

View file

@ -7,6 +7,16 @@
let mobileMenuOpen = $state(false); let mobileMenuOpen = $state(false);
let auth = $state(false); let auth = $state(false);
let admin = $state(false); let admin = $state(false);
let searchQuery = $state("");
function submitSearch(e: SubmitEvent) {
e.preventDefault();
const q = searchQuery.trim();
if (!q) return;
mobileMenuOpen = false;
searchQuery = "";
goto(`/search?q=${encodeURIComponent(q)}`);
}
onMount(() => { onMount(() => {
initAuth(); initAuth();
@ -50,6 +60,15 @@
{:else} {:else}
<a href="/auth/login" class="nav-link">Sign in</a> <a href="/auth/login" class="nav-link">Sign in</a>
{/if} {/if}
<form onsubmit={submitSearch}>
<input
type="search"
bind:value={searchQuery}
placeholder="Search"
aria-label="Search notes and questions"
class="border-rule text-ink placeholder:text-faint focus:border-primary w-40 rounded-none border bg-white px-2.5 py-1.5 text-[11px] tracking-wide transition-colors outline-none"
/>
</form>
</nav> </nav>
<button <button
@ -71,6 +90,15 @@
{#if mobileMenuOpen} {#if mobileMenuOpen}
<nav class="border-rule space-y-3 border-t px-4 py-4 md:hidden"> <nav class="border-rule space-y-3 border-t px-4 py-4 md:hidden">
<form onsubmit={submitSearch}>
<input
type="search"
bind:value={searchQuery}
placeholder="Search"
aria-label="Search notes and questions"
class="border-rule text-ink placeholder:text-faint focus:border-primary w-full rounded-none border bg-white px-3 py-2 text-sm tracking-wide transition-colors outline-none"
/>
</form>
<a href="/notes" class="nav-link block" onclick={() => (mobileMenuOpen = false)} <a href="/notes" class="nav-link block" onclick={() => (mobileMenuOpen = false)}
>Notes</a >Notes</a
> >

View file

@ -398,6 +398,31 @@ function questionsGetById(db: Db, args: { id: string }) {
return row ? mapQuestion(row) : null; return row ? mapQuestion(row) : null;
} }
function questionsSearch(db: Db, args: { query: string; limit?: number }) {
const all = db
.prepare(`SELECT ${QUESTION_COLUMNS} FROM questions ORDER BY createdAt DESC LIMIT ?`)
.all(args.limit ?? 200) as Record<string, any>[];
const q = args.query.toLowerCase();
return all
.filter(
(qr) =>
String(qr.title).toLowerCase().includes(q) ||
String(qr.content).toLowerCase().includes(q),
)
.map(mapQuestion);
}
function searchAll(db: Db, args: { query: string; limit?: number }) {
const limit = args.limit ?? 200;
const notes = notesSearch(db, { query: args.query, limit }) as Record<string, any>[];
const questions = questionsSearch(db, { query: args.query, limit }) as Record<string, any>[];
const combined: Record<string, any>[] = [
...notes.map((n) => ({ ...n, type: "note" })),
...questions.map((q) => ({ ...q, type: "question" })),
];
return combined.sort((a, b) => b.createdAt - a.createdAt);
}
function questionsMarkSolved(db: Db, args: { token: string; id: string }) { function questionsMarkSolved(db: Db, args: { token: string; id: string }) {
const user = requireAuth(db, args.token); const user = requireAuth(db, args.token);
const question = db.prepare("SELECT id, authorId FROM questions WHERE id = ?").get(args.id) as const question = db.prepare("SELECT id, authorId FROM questions WHERE id = ?").get(args.id) as
@ -921,10 +946,12 @@ const handlers: Record<string, Handler> = {
"notes:create": notesCreate, "notes:create": notesCreate,
"notes:list": notesList, "notes:list": notesList,
"notes:search": notesSearch, "notes:search": notesSearch,
"search:all": searchAll,
"notes:getById": notesGetById, "notes:getById": notesGetById,
"notes:remove": notesRemove, "notes:remove": notesRemove,
"questions:create": questionsCreate, "questions:create": questionsCreate,
"questions:list": questionsList, "questions:list": questionsList,
"questions:search": questionsSearch,
"questions:getById": questionsGetById, "questions:getById": questionsGetById,
"questions:markSolved": questionsMarkSolved, "questions:markSolved": questionsMarkSolved,
"questions:remove": questionsRemove, "questions:remove": questionsRemove,

View file

@ -52,6 +52,8 @@ export type QuestionDoc = Doc<"questions"> & {
solved: boolean; solved: boolean;
}; };
export type SearchResult = (NoteDoc & { type: "note" }) | (QuestionDoc & { type: "question" });
export type CommentDoc = Doc<"comments"> & { export type CommentDoc = Doc<"comments"> & {
content: string; content: string;
authorId: Id<"users">; authorId: Id<"users">;

View file

@ -32,17 +32,12 @@
{@render children()} {@render children()}
</main> </main>
<footer class="border-rule mt-16 border-t"> <footer class="border-rule mt-16 border-t">
<div <div class="mx-auto max-w-4xl px-4 py-8 sm:px-6">
class="mx-auto flex max-w-4xl flex-col gap-2 px-4 py-8 sm:flex-row sm:items-end sm:justify-between sm:px-6"
>
<div>
<p class="kicker">Notebook — written by students, for students.</p> <p class="kicker">Notebook — written by students, for students.</p>
<p class="text-faint mt-2 text-xs"> <p class="text-faint mt-2 text-xs">
DSEC Notebook is a community resource for Deakin University students. Not DSEC Notebook is a community resource for Deakin University students. Not affiliated
affiliated with Deakin University. with Deakin University.
</p> </p>
</div> </div>
<a href="/search" class="kicker hover:text-primary">Search</a>
</div>
</footer> </footer>
</div> </div>

View file

@ -20,6 +20,10 @@ html {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
a, button {
cursor: pointer;
}
.page { .page {
@apply mx-auto max-w-4xl px-4 py-10 sm:px-6; @apply mx-auto max-w-4xl px-4 py-10 sm:px-6;
} }

View file

@ -10,6 +10,7 @@
let loading = $state(true); let loading = $state(true);
let selectedUnitId = $state(""); let selectedUnitId = $state("");
let sort = $state<"newest" | "top">("newest"); let sort = $state<"newest" | "top">("newest");
let searchQuery = $state("");
onMount(async () => { onMount(async () => {
const [n, u] = await Promise.all([query("notes:list", {}), query("units:getAll")]); const [n, u] = await Promise.all([query("notes:list", {}), query("units:getAll")]);
@ -25,7 +26,13 @@
}); });
const visible = $derived.by(() => { const visible = $derived.by(() => {
const q = searchQuery.trim().toLowerCase();
let list = selectedUnitId ? notes.filter((n) => n.unitId === selectedUnitId) : notes; let list = selectedUnitId ? notes.filter((n) => n.unitId === selectedUnitId) : notes;
if (q) {
list = list.filter(
(n) => n.title.toLowerCase().includes(q) || n.content.toLowerCase().includes(q),
);
}
if (sort === "top") list = [...list].sort((a, b) => b.voteCount - a.voteCount); if (sort === "top") list = [...list].sort((a, b) => b.voteCount - a.voteCount);
return list; return list;
}); });
@ -41,6 +48,16 @@
<a href="/post/note" class="btn-primary">Post a note</a> <a href="/post/note" class="btn-primary">Post a note</a>
</div> </div>
<div class="border-rule border-b py-4">
<input
type="search"
bind:value={searchQuery}
placeholder="Search notes..."
aria-label="Search notes"
class="field"
/>
</div>
<div class="border-rule flex flex-wrap items-center justify-between gap-4 border-b py-4"> <div class="border-rule flex flex-wrap items-center justify-between gap-4 border-b py-4">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<button <button

View file

@ -10,6 +10,7 @@
let loading = $state(true); let loading = $state(true);
let selectedUnitId = $state(""); let selectedUnitId = $state("");
let sort = $state<"newest" | "top">("newest"); let sort = $state<"newest" | "top">("newest");
let searchQuery = $state("");
onMount(async () => { onMount(async () => {
const [q, u] = await Promise.all([query("questions:list", {}), query("units:getAll")]); const [q, u] = await Promise.all([query("questions:list", {}), query("units:getAll")]);
@ -28,9 +29,17 @@
}); });
const visible = $derived.by(() => { const visible = $derived.by(() => {
const q = searchQuery.trim().toLowerCase();
let list = selectedUnitId let list = selectedUnitId
? questions.filter((q) => q.unitId === selectedUnitId) ? questions.filter((q) => q.unitId === selectedUnitId)
: questions; : questions;
if (q) {
list = list.filter(
(question) =>
question.title.toLowerCase().includes(q) ||
question.content.toLowerCase().includes(q),
);
}
if (sort === "top") list = [...list].sort((a, b) => b.voteCount - a.voteCount); if (sort === "top") list = [...list].sort((a, b) => b.voteCount - a.voteCount);
return list; return list;
}); });
@ -46,6 +55,16 @@
<a href="/post/question" class="btn-primary">Ask a question</a> <a href="/post/question" class="btn-primary">Ask a question</a>
</div> </div>
<div class="border-rule border-b py-4">
<input
type="search"
bind:value={searchQuery}
placeholder="Search questions..."
aria-label="Search questions"
class="field"
/>
</div>
<div class="border-rule flex flex-wrap items-center justify-between gap-4 border-b py-4"> <div class="border-rule flex flex-wrap items-center justify-between gap-4 border-b py-4">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<button <button

View file

@ -3,9 +3,9 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import FeedRow from "$lib/components/FeedRow.svelte"; import FeedRow from "$lib/components/FeedRow.svelte";
import { timeAgo } from "$lib/time"; import { timeAgo } from "$lib/time";
import type { NoteDoc } from "$lib/types"; import type { SearchResult } from "$lib/types";
let results: NoteDoc[] = $state([]); let results: SearchResult[] = $state([]);
let loading = $state(true); let loading = $state(true);
let searchQuery = $state(""); let searchQuery = $state("");
let ranQuery = $state(false); let ranQuery = $state(false);
@ -14,7 +14,7 @@
searchQuery = new URL(window.location.href).searchParams.get("q") ?? ""; searchQuery = new URL(window.location.href).searchParams.get("q") ?? "";
if (searchQuery) { if (searchQuery) {
ranQuery = true; ranQuery = true;
results = (await query("notes:search", { query: searchQuery })) as NoteDoc[]; results = (await query("search:all", { query: searchQuery })) as SearchResult[];
} }
loading = false; loading = false;
}); });
@ -40,7 +40,7 @@
id="q" id="q"
type="search" type="search"
bind:value={searchQuery} bind:value={searchQuery}
placeholder="Search notes..." placeholder="Search notes and questions..."
class="field" class="field"
/> />
</form> </form>
@ -48,22 +48,34 @@
{#if loading} {#if loading}
<p class="kicker py-16">Loading</p> <p class="kicker py-16">Loading</p>
{:else if ranQuery} {:else if ranQuery}
{#each results as note} {#each results as result}
{#if result.type === "note"}
<FeedRow <FeedRow
href="/notes/{note._id}" href="/notes/{result._id}"
title={note.title} title={result.title}
meta="{note.authorName} · {timeAgo(note.createdAt)}" meta="{result.authorName} · {timeAgo(
voteCount={note.voteCount} result.createdAt,
)} · {result.commentCount} comment{result.commentCount === 1 ? '' : 's'}"
voteCount={result.voteCount}
targetType="note" targetType="note"
targetId={note._id} targetId={result._id}
tag="Note"
/> />
{:else}
<FeedRow
href="/questions/{result._id}"
title={result.title}
meta="{result.authorName} · {timeAgo(
result.createdAt,
)} · {result.answerCount} answer{result.answerCount === 1 ? '' : 's'}"
voteCount={result.voteCount}
targetType="question"
targetId={result._id}
tag="Question"
/>
{/if}
{:else} {:else}
<p class="text-muted text-sm">No results found for “{searchQuery}”.</p> <p class="text-muted text-sm">No results found for “{searchQuery}”.</p>
<a
href="/notes"
class="text-secondary hover:text-secondary-dark mt-4 inline-block text-sm"
>Browse notes</a
>
{/each} {/each}
{/if} {/if}
</div> </div>