mirror of
https://github.com/dsec-hub/dsec-notebook.git
synced 2026-09-22 15:43:58 +00:00
updated search, added hover to buttons
This commit is contained in:
parent
830a04893a
commit
f807ae9500
11 changed files with 3302 additions and 3351 deletions
|
|
@ -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
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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">;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue