mirror of
https://github.com/dsec-hub/dsec-notebook.git
synced 2026-09-22 07:24:27 +00:00
157 lines
4.2 KiB
Svelte
157 lines
4.2 KiB
Svelte
<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}
|