mirror of
https://github.com/dsec-hub/dsec-notebook.git
synced 2026-09-22 07:24:27 +00:00
Merge pull request #15 from dsec-hub/feature/navigation
improved navigation
This commit is contained in:
commit
a730cf6b4f
7 changed files with 78 additions and 302 deletions
|
|
@ -9,10 +9,10 @@
|
||||||
|
|
||||||
let {
|
let {
|
||||||
units,
|
units,
|
||||||
selectedUnitId = $bindable(""),
|
selectedUnitIds = $bindable([]),
|
||||||
}: {
|
}: {
|
||||||
units: UnitDoc[];
|
units: UnitDoc[];
|
||||||
selectedUnitId?: string;
|
selectedUnitIds?: string[];
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let pinnedIds = $state<string[]>([]);
|
let pinnedIds = $state<string[]>([]);
|
||||||
|
|
@ -41,6 +41,12 @@
|
||||||
|
|
||||||
const canPinMore = $derived(pinnedIds.length < MAX_PINS);
|
const canPinMore = $derived(pinnedIds.length < MAX_PINS);
|
||||||
|
|
||||||
|
function toggleUnit(unitId: string) {
|
||||||
|
selectedUnitIds = selectedUnitIds.includes(unitId)
|
||||||
|
? selectedUnitIds.filter((id) => id !== unitId)
|
||||||
|
: [...selectedUnitIds, unitId];
|
||||||
|
}
|
||||||
|
|
||||||
async function togglePin(unit: UnitDoc) {
|
async function togglePin(unit: UnitDoc) {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|
@ -84,20 +90,13 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="flex scrollbar-thin items-center gap-x-1 gap-y-2 overflow-x-scroll pb-2">
|
<div class="flex scrollbar-thin items-center gap-x-1 gap-y-2 overflow-x-scroll pb-2">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="chip {selectedUnitId === '' ? 'chip-active' : ''}"
|
|
||||||
onclick={() => (selectedUnitId = "")}
|
|
||||||
>
|
|
||||||
All units
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#each pinnedUnits as unit (unit._id)}
|
{#each pinnedUnits as unit (unit._id)}
|
||||||
<span class="inline-flex items-center">
|
<span class="inline-flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="chip {selectedUnitId === unit._id ? 'chip-active' : ''}"
|
class="chip {selectedUnitIds.includes(unit._id) ? 'chip-active' : ''}"
|
||||||
onclick={() => (selectedUnitId = unit._id)}
|
aria-pressed={selectedUnitIds.includes(unit._id)}
|
||||||
|
onclick={() => toggleUnit(unit._id)}
|
||||||
>
|
>
|
||||||
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
|
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -118,8 +117,9 @@
|
||||||
<span class="inline-flex items-center">
|
<span class="inline-flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="chip {selectedUnitId === unit._id ? 'chip-active' : ''}"
|
class="chip {selectedUnitIds.includes(unit._id) ? 'chip-active' : ''}"
|
||||||
onclick={() => (selectedUnitId = unit._id)}
|
aria-pressed={selectedUnitIds.includes(unit._id)}
|
||||||
|
onclick={() => toggleUnit(unit._id)}
|
||||||
>
|
>
|
||||||
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
|
{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { query, mutation } from "$lib/api";
|
|
||||||
import { getToken, isAuthenticated, initAuth } from "$lib/stores/auth";
|
|
||||||
import { get } from "svelte/store";
|
|
||||||
import type { UnitDoc } from "$lib/types";
|
|
||||||
|
|
||||||
const MAX_PINS = 10;
|
|
||||||
|
|
||||||
let {
|
|
||||||
units,
|
|
||||||
selectedUnitId = $bindable(""),
|
|
||||||
}: {
|
|
||||||
units: UnitDoc[];
|
|
||||||
selectedUnitId?: string;
|
|
||||||
} = $props();
|
|
||||||
|
|
||||||
let pinnedIds = $state<string[]>([]);
|
|
||||||
let authed = $state(false);
|
|
||||||
let busy = $state(false);
|
|
||||||
let error = $state("");
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await initAuth();
|
|
||||||
if (!get(isAuthenticated)) return;
|
|
||||||
authed = true;
|
|
||||||
const token = getToken();
|
|
||||||
if (!token) return;
|
|
||||||
try {
|
|
||||||
pinnedIds = (await query("units:getPinned", { token })) as string[];
|
|
||||||
selectedUnitId =
|
|
||||||
units.find((unit) => pinnedIds.includes(unit._id))?._id ?? units[0]?._id ?? "";
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isPinned = (id: string) => pinnedIds.includes(id);
|
|
||||||
const canPinMore = $derived(pinnedIds.length < MAX_PINS);
|
|
||||||
const pinnedUnits = $derived(units.filter((u) => pinnedIds.includes(u._id)));
|
|
||||||
const unpinnedUnits = $derived(units.filter((u) => !pinnedIds.includes(u._id)));
|
|
||||||
|
|
||||||
async function togglePin(unit: UnitDoc) {
|
|
||||||
const token = getToken();
|
|
||||||
if (!token) {
|
|
||||||
error = "Sign in to pin units";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
busy = true;
|
|
||||||
error = "";
|
|
||||||
try {
|
|
||||||
const pinned = pinnedIds.includes(unit._id);
|
|
||||||
pinnedIds = (await mutation(pinned ? "units:unpin" : "units:pin", {
|
|
||||||
token,
|
|
||||||
unitId: unit._id,
|
|
||||||
})) as string[];
|
|
||||||
} catch (err: any) {
|
|
||||||
error = err?.message ?? "Failed to update pin";
|
|
||||||
} finally {
|
|
||||||
busy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#snippet pinIcon()}
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M12 17v5"></path>
|
|
||||||
<path
|
|
||||||
d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16h14v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<div class="unit-rail">
|
|
||||||
{#each pinnedUnits as unit (unit._id)}
|
|
||||||
<div class="unit-card {selectedUnitId === unit._id ? 'unit-card-active' : ''}">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex h-full w-full flex-col items-start gap-1 text-left"
|
|
||||||
onclick={() => (selectedUnitId = unit._id)}
|
|
||||||
aria-label="Select {unit.code}"
|
|
||||||
>
|
|
||||||
<span class="text-ink font-serif text-base leading-tight"
|
|
||||||
>{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}</span
|
|
||||||
>
|
|
||||||
<span class="text-muted line-clamp-2 text-xs leading-snug">{unit.name}</span>
|
|
||||||
</button>
|
|
||||||
{#if authed}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pin-btn {isPinned(unit._id)
|
|
||||||
? 'pin-btn-active'
|
|
||||||
: ''} absolute top-1.5 right-1.5 z-10"
|
|
||||||
onclick={() => togglePin(unit)}
|
|
||||||
disabled={busy || (!isPinned(unit._id) && !canPinMore)}
|
|
||||||
title={isPinned(unit._id)
|
|
||||||
? "Unpin unit"
|
|
||||||
: canPinMore
|
|
||||||
? "Pin unit"
|
|
||||||
: "You can pin up to 10 units"}
|
|
||||||
aria-label={isPinned(unit._id) ? "Unpin unit" : "Pin unit"}
|
|
||||||
>
|
|
||||||
{@render pinIcon()}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#each unpinnedUnits as unit (unit._id)}
|
|
||||||
<div class="unit-card {selectedUnitId === unit._id ? 'unit-card-active' : ''}">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex h-full w-full flex-col items-start gap-1 text-left"
|
|
||||||
onclick={() => (selectedUnitId = unit._id)}
|
|
||||||
aria-label="Select {unit.code}"
|
|
||||||
>
|
|
||||||
<span class="text-ink font-serif text-base leading-tight"
|
|
||||||
>{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}</span
|
|
||||||
>
|
|
||||||
<span class="text-muted line-clamp-2 text-xs leading-snug">{unit.name}</span>
|
|
||||||
</button>
|
|
||||||
{#if authed}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pin-btn {isPinned(unit._id)
|
|
||||||
? 'pin-btn-active'
|
|
||||||
: ''} absolute top-1.5 right-1.5 z-10"
|
|
||||||
onclick={() => togglePin(unit)}
|
|
||||||
disabled={busy || (!isPinned(unit._id) && !canPinMore)}
|
|
||||||
title={isPinned(unit._id)
|
|
||||||
? "Unpin unit"
|
|
||||||
: canPinMore
|
|
||||||
? "Pin unit"
|
|
||||||
: "You can pin up to 10 units"}
|
|
||||||
aria-label={isPinned(unit._id) ? "Unpin unit" : "Pin unit"}
|
|
||||||
>
|
|
||||||
{@render pinIcon()}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<p class="text-primary mt-2 text-xs">{error}</p>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -1,22 +1,41 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { query } from "$lib/api";
|
import { query } from "$lib/api";
|
||||||
import { unitPath } from "$lib/paths";
|
import { unitPath } from "$lib/paths";
|
||||||
|
import { getToken, initAuth, isAuthenticated } from "$lib/stores/auth";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import { get } from "svelte/store";
|
||||||
import type { UnitDoc } from "$lib/types";
|
import type { UnitDoc } from "$lib/types";
|
||||||
|
|
||||||
let units: UnitDoc[] = $state([]);
|
let units: UnitDoc[] = $state([]);
|
||||||
|
let pinnedUnits: UnitDoc[] = $state([]);
|
||||||
|
let authed = $state(false);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let error = $state("");
|
let error = $state("");
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
units = (await query("units:getAll")) as UnitDoc[];
|
await initAuth();
|
||||||
|
authed = get(isAuthenticated);
|
||||||
|
const token = getToken();
|
||||||
|
const [all, pinIds] = await Promise.all([
|
||||||
|
query("units:getAll") as Promise<UnitDoc[]>,
|
||||||
|
token
|
||||||
|
? (query("units:getPinned", { token }) as Promise<UnitDoc["_id"][]>)
|
||||||
|
: Promise.resolve([] as UnitDoc["_id"][]),
|
||||||
|
]);
|
||||||
|
units = all;
|
||||||
|
const unitMap = new Map(all.map((unit) => [unit._id, unit]));
|
||||||
|
pinnedUnits = pinIds
|
||||||
|
.map((id) => unitMap.get(id))
|
||||||
|
.filter((unit): unit is UnitDoc => !!unit);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error = err.message ?? "Failed to load units";
|
error = err.message ?? "Failed to load units";
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shownUnits = $derived(authed ? pinnedUnits : units);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -42,14 +61,23 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="border-rule border-t pt-10">
|
<section class="border-rule border-t pt-10">
|
||||||
<p class="kicker mb-6">Units</p>
|
<div class="mb-6 flex items-end justify-between gap-4">
|
||||||
|
<p class="kicker">{authed ? "Pinned units" : "Units"}</p>
|
||||||
|
<a href="/units" class="nav-link">All units</a>
|
||||||
|
</div>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="kicker">Loading</p>
|
<p class="kicker">Loading</p>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<p class="kicker">{error}</p>
|
<p class="kicker">{error}</p>
|
||||||
|
{:else if authed && pinnedUnits.length === 0}
|
||||||
|
<p class="text-muted text-sm">
|
||||||
|
Pin units from the
|
||||||
|
<a href="/units" class="text-secondary hover:text-secondary-dark">Units</a>
|
||||||
|
page to keep them here.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="unit-rail">
|
<div class="unit-rail">
|
||||||
{#each units as unit}
|
{#each shownUnits as unit}
|
||||||
<a href={unitPath(unit.code)} class="unit-card group flex flex-col gap-1">
|
<a href={unitPath(unit.code)} class="unit-card group flex flex-col gap-1">
|
||||||
<span
|
<span
|
||||||
class="text-ink group-hover:text-primary font-serif text-base leading-tight"
|
class="text-ink group-hover:text-primary font-serif text-base leading-tight"
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-rail {
|
.unit-rail {
|
||||||
@apply flex scrollbar-thin gap-3 overflow-x-scroll py-1 pb-2;
|
@apply flex scrollbar-thin gap-3 overflow-x-auto py-1 pb-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-rail::-webkit-scrollbar {
|
.unit-rail::-webkit-scrollbar {
|
||||||
|
|
@ -143,7 +143,11 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-card {
|
.unit-card {
|
||||||
@apply border-rule bg-surface hover:border-ink relative w-44 shrink-0 rounded-sm border p-3 transition-colors;
|
@apply border-rule bg-surface hover:border-ink relative min-w-0 rounded-sm border p-3 transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-rail .unit-card {
|
||||||
|
@apply w-44 shrink-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-card-active {
|
.unit-card-active {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
let notes: (NoteDoc & { unit?: UnitDoc })[] = $state([]);
|
let notes: (NoteDoc & { unit?: UnitDoc })[] = $state([]);
|
||||||
let units: UnitDoc[] = $state([]);
|
let units: UnitDoc[] = $state([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let selectedUnitId = $state("");
|
let selectedUnitIds: string[] = $state([]);
|
||||||
let sort = $state<"newest" | "top">("newest");
|
let sort = $state<"newest" | "top">("newest");
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state("");
|
||||||
|
|
||||||
|
|
@ -24,7 +24,10 @@
|
||||||
|
|
||||||
const visible = $derived.by(() => {
|
const visible = $derived.by(() => {
|
||||||
const q = searchQuery.trim().toLowerCase();
|
const q = searchQuery.trim().toLowerCase();
|
||||||
let list = selectedUnitId ? notes.filter((n) => n.unitId === selectedUnitId) : notes;
|
let list =
|
||||||
|
selectedUnitIds.length > 0
|
||||||
|
? notes.filter((n) => selectedUnitIds.includes(n.unitId))
|
||||||
|
: notes;
|
||||||
if (q) {
|
if (q) {
|
||||||
list = list.filter(
|
list = list.filter(
|
||||||
(n) => n.title.toLowerCase().includes(q) || n.content.toLowerCase().includes(q),
|
(n) => n.title.toLowerCase().includes(q) || n.content.toLowerCase().includes(q),
|
||||||
|
|
@ -56,7 +59,7 @@
|
||||||
</div>
|
</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">
|
||||||
<UnitFilter bind:selectedUnitId {units} />
|
<UnitFilter bind:selectedUnitIds {units} />
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
let questions: (QuestionDoc & { unit?: UnitDoc })[] = $state([]);
|
let questions: (QuestionDoc & { unit?: UnitDoc })[] = $state([]);
|
||||||
let units: UnitDoc[] = $state([]);
|
let units: UnitDoc[] = $state([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let selectedUnitId = $state("");
|
let selectedUnitIds: string[] = $state([]);
|
||||||
let sort = $state<"newest" | "top">("newest");
|
let sort = $state<"newest" | "top">("newest");
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state("");
|
||||||
|
|
||||||
|
|
@ -27,8 +27,8 @@
|
||||||
|
|
||||||
const visible = $derived.by(() => {
|
const visible = $derived.by(() => {
|
||||||
const q = searchQuery.trim().toLowerCase();
|
const q = searchQuery.trim().toLowerCase();
|
||||||
let list = selectedUnitId
|
let list = selectedUnitIds.length
|
||||||
? questions.filter((q) => q.unitId === selectedUnitId)
|
? questions.filter((question) => selectedUnitIds.includes(question.unitId))
|
||||||
: questions;
|
: questions;
|
||||||
if (q) {
|
if (q) {
|
||||||
list = list.filter(
|
list = list.filter(
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
</div>
|
</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">
|
||||||
<UnitFilter bind:selectedUnitId {units} />
|
<UnitFilter bind:selectedUnitIds {units} />
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,19 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { query } from "$lib/api";
|
import { query } from "$lib/api";
|
||||||
|
import { unitPath } from "$lib/paths";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import FeedRow from "$lib/components/FeedRow.svelte";
|
import type { UnitDoc } from "$lib/types";
|
||||||
import UnitRail from "$lib/components/UnitRail.svelte";
|
|
||||||
import { postPath } from "$lib/paths";
|
|
||||||
import { timeAgo } from "$lib/time";
|
|
||||||
import type { NoteDoc, QuestionDoc, UnitDoc } from "$lib/types";
|
|
||||||
|
|
||||||
let units: UnitDoc[] = $state([]);
|
let units: UnitDoc[] = $state([]);
|
||||||
let notes: NoteDoc[] = $state([]);
|
|
||||||
let questions: QuestionDoc[] = $state([]);
|
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let contentLoading = $state(false);
|
|
||||||
let selectedUnitId = $state("");
|
|
||||||
|
|
||||||
const selectedUnit = $derived(units.find((u) => u._id === selectedUnitId) ?? null);
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
units = (await query("units:getAll")) as UnitDoc[];
|
units = (await query("units:getAll")) as UnitDoc[];
|
||||||
selectedUnitId = units[0]?._id ?? "";
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
loadContent(selectedUnitId);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadContent(id: string) {
|
|
||||||
if (!id) {
|
|
||||||
notes = [];
|
|
||||||
questions = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
contentLoading = true;
|
|
||||||
try {
|
|
||||||
const [n, q] = await Promise.all([
|
|
||||||
query("notes:list", { unitId: id }),
|
|
||||||
query("questions:list", { unitId: id }),
|
|
||||||
]);
|
|
||||||
notes = n as NoteDoc[];
|
|
||||||
questions = q as QuestionDoc[];
|
|
||||||
} finally {
|
|
||||||
contentLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -63,92 +30,23 @@
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p class="kicker py-16">Loading</p>
|
<p class="kicker py-16">Loading</p>
|
||||||
|
{:else if units.length === 0}
|
||||||
|
<p class="text-muted py-16 text-sm">No units yet.</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="border-rule border-b py-5">
|
<div class="grid grid-cols-1 gap-3 pt-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<UnitRail {units} bind:selectedUnitId />
|
{#each units as unit (unit._id)}
|
||||||
</div>
|
<a href={unitPath(unit.code)} class="unit-card flex h-full w-full flex-col gap-2">
|
||||||
|
<span class="text-ink font-serif text-base leading-tight"
|
||||||
{#if selectedUnit}
|
>{unit.code}{unit.code2 ? ` / ${unit.code2}` : ""}</span
|
||||||
<div class="pt-6">
|
|
||||||
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
|
||||||
<a
|
|
||||||
href={`/${selectedUnit.code}`}
|
|
||||||
class="text-ink hover:text-primary font-serif text-2xl font-medium"
|
|
||||||
>
|
>
|
||||||
{selectedUnit.code}{selectedUnit.code2 ? ` / ${selectedUnit.code2}` : ""}
|
<span class="text-ink text-sm leading-snug">{unit.name}</span>
|
||||||
</a>
|
{#if unit.description}
|
||||||
<p class="text-muted text-sm">{selectedUnit.name}</p>
|
<p class="text-muted line-clamp-3 text-xs leading-snug">
|
||||||
</div>
|
{unit.description}
|
||||||
{#if selectedUnit.description}
|
|
||||||
<p class="text-muted mt-1 text-sm">
|
|
||||||
{selectedUnit.description}
|
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
</a>
|
||||||
{#if contentLoading}
|
|
||||||
<p class="kicker py-16">Loading</p>
|
|
||||||
{:else}
|
|
||||||
{#if notes.length > 0}
|
|
||||||
<p class="kicker border-rule mt-8 border-t pt-8">Notes</p>
|
|
||||||
{#each notes as note}
|
|
||||||
<FeedRow
|
|
||||||
href={postPath(selectedUnit.code, note._id)}
|
|
||||||
title={note.title}
|
|
||||||
content={note.content}
|
|
||||||
unitCode={selectedUnit.code +
|
|
||||||
(selectedUnit.code2 ? ` / ${selectedUnit.code2}` : "")}
|
|
||||||
meta="{note.authorName} · {timeAgo(
|
|
||||||
note.createdAt,
|
|
||||||
)} · {note.commentCount} comment{note.commentCount === 1
|
|
||||||
? ''
|
|
||||||
: 's'}"
|
|
||||||
voteCount={note.voteCount}
|
|
||||||
targetType="note"
|
|
||||||
targetId={note._id}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if questions.length > 0}
|
|
||||||
<p class="kicker border-rule mt-8 border-t pt-8">Questions</p>
|
|
||||||
{#each questions as question}
|
|
||||||
<FeedRow
|
|
||||||
href={postPath(selectedUnit.code, question._id)}
|
|
||||||
title={question.title}
|
|
||||||
content={question.content}
|
|
||||||
unitCode={selectedUnit.code +
|
|
||||||
(selectedUnit.code2 ? ` / ${selectedUnit.code2}` : "")}
|
|
||||||
meta="{question.authorName} · {timeAgo(
|
|
||||||
question.createdAt,
|
|
||||||
)} · {question.answerCount} answer{question.answerCount === 1
|
|
||||||
? ''
|
|
||||||
: 's'}"
|
|
||||||
voteCount={question.voteCount}
|
|
||||||
targetType="question"
|
|
||||||
targetId={question._id}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if notes.length === 0 && questions.length === 0}
|
|
||||||
<p class="text-muted mt-8 text-sm">No content for this unit yet.</p>
|
|
||||||
<div class="mt-4 flex gap-4">
|
|
||||||
<a
|
|
||||||
href="/post/note"
|
|
||||||
class="text-secondary hover:text-secondary-dark text-sm"
|
|
||||||
>Post a note</a
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/post/question"
|
|
||||||
class="text-secondary hover:text-secondary-dark text-sm"
|
|
||||||
>Ask a question</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<p class="text-muted py-16 text-sm">Select a unit to see its notes and questions.</p>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue