dsec-notebook/src/routes/notes/+page.svelte

117 lines
3.2 KiB
Svelte

<script lang="ts">
import { query } from "$lib/api";
import { onMount } from "svelte";
import FeedRow from "$lib/components/FeedRow.svelte";
import UnitFilter from "$lib/components/UnitFilter.svelte";
import { postPath } from "$lib/paths";
import { unitMatchesQuery } from "$lib/search";
import { timeAgo } from "$lib/time";
import type { NoteDoc, UnitDoc } from "$lib/types";
let notes: (NoteDoc & { unit?: UnitDoc })[] = $state([]);
let units: UnitDoc[] = $state([]);
let loading = $state(true);
let selectedUnitIds: string[] = $state([]);
let sort = $state<"newest" | "top">("newest");
let searchQuery = $state("");
onMount(async () => {
const [n, u] = await Promise.all([query("notes:list", {}), query("units:getAll")]);
const unitMap = new Map((u as UnitDoc[]).map((unit) => [unit._id, unit]));
notes = (n as NoteDoc[]).map((note) => ({
...note,
unit: unitMap.get(note.unitId),
}));
units = u as UnitDoc[];
loading = false;
});
const visible = $derived.by(() => {
const q = searchQuery.trim().toLowerCase();
let list =
selectedUnitIds.length > 0
? notes.filter((n) => selectedUnitIds.includes(n.unitId))
: notes;
if (q) {
list = list.filter(
(n) =>
n.title.toLowerCase().includes(q) ||
n.content.toLowerCase().includes(q) ||
(n.unit ? unitMatchesQuery(n.unit, q) : false),
);
}
if (sort === "top") list = [...list].sort((a, b) => b.voteCount - a.voteCount);
return list;
});
</script>
<svelte:head>
<title>Notes — DSEC Notebook</title>
</svelte:head>
<div class="page">
<div class="border-rule flex items-end justify-between gap-4 border-b pb-6">
<h1 class="text-ink font-serif text-4xl font-medium">Notes</h1>
<a href="/post/note" class="btn-primary">Post a note</a>
</div>
<div class="border-rule border-b py-4">
<input
type="search"
bind:value={searchQuery}
placeholder="Search notes and units..."
aria-label="Search notes and units"
class="field"
/>
</div>
<div class="border-rule flex flex-wrap items-center justify-between gap-4 border-b py-4">
<UnitFilter bind:selectedUnitIds {units} />
<div class="flex gap-4">
<button
type="button"
class="kicker {sort === 'newest' ? 'text-primary' : ''}"
onclick={() => (sort = "newest")}
>
Newest
</button>
<button
type="button"
class="kicker {sort === 'top' ? 'text-primary' : ''}"
onclick={() => (sort = "top")}
>
Top
</button>
</div>
</div>
{#if loading}
<p class="kicker py-16">Loading</p>
{:else}
{#each visible as note}
<FeedRow
href={postPath(note.unit!.code, note._id)}
title={note.title}
content={note.content}
unitCode={note.unit
? note.unit.code + (note.unit.code2 ? ` / ${note.unit.code2}` : "")
: undefined}
meta="{note.authorName} · {timeAgo(
note.createdAt,
)} · {note.commentCount} comment{note.commentCount === 1 ? '' : 's'}"
voteCount={note.voteCount}
targetType="note"
targetId={note._id}
/>
{:else}
<div class="py-16">
<p class="text-muted text-sm">No notes yet.</p>
<a
href="/post/note"
class="text-secondary hover:text-secondary-dark mt-4 inline-block text-sm"
>Post a note</a
>
</div>
{/each}
{/if}
</div>