Support resources with kind:30023 articles
This commit is contained in:
parent
f6bea700f0
commit
b901215e1a
13 changed files with 285 additions and 75 deletions
|
|
@ -13,7 +13,9 @@
|
|||
import { onMount } from "svelte";
|
||||
import { auth, restoreSession } from "$lib/auth.svelte";
|
||||
import { loadGroup } from "$lib/group.svelte";
|
||||
import { loadGroups } from "$lib/groups.svelte";
|
||||
import { loadGroups, groupsStore } from "$lib/groups.svelte";
|
||||
import { loadResources } from "$lib/resources.svelte";
|
||||
import { loadRoomAdmins } from "$lib/admins.svelte";
|
||||
import { seedProfiles } from "$lib/profiles.svelte";
|
||||
import { startChat } from "$lib/chat.svelte";
|
||||
import { activeGroup, setActiveGroup } from "$lib/active.svelte";
|
||||
|
|
@ -25,9 +27,13 @@
|
|||
const chatEnabled = true;
|
||||
|
||||
onMount(async () => {
|
||||
loadResources();
|
||||
const tasks = [restoreSession(), loadGroup()];
|
||||
if (mode === "full") tasks.push(loadGroups());
|
||||
await Promise.all(tasks);
|
||||
// Resources are filtered by the admin set; in full mode that means every
|
||||
// room's admins, needed on every page for the sidebar.
|
||||
if (mode === "full") loadRoomAdmins(groupsStore.list.map((g) => g.id));
|
||||
seedProfiles(auth.user?.pubkey ?? null);
|
||||
});
|
||||
|
||||
|
|
@ -53,10 +59,14 @@
|
|||
(page.url.pathname.startsWith("/thread/") ? activeGroup.id : ""),
|
||||
);
|
||||
|
||||
// The full-mode landing page renders its own right column (latest
|
||||
// discussions) and has no single room to chat in, so suppress room chat there.
|
||||
const isHomeFull = $derived(mode === "full" && page.url.pathname === "/");
|
||||
const showChat = $derived(chatEnabled && !isHomeFull);
|
||||
// Article pages (both modes) and the full-mode landing have no single room to
|
||||
// chat in, so they render the latest-discussions panel instead of room chat.
|
||||
// The simple-mode home keeps its room chat, like every full-mode room.
|
||||
const isArticle = $derived(page.url.pathname.startsWith("/resource/"));
|
||||
const showDiscussions = $derived(
|
||||
isArticle || (mode === "full" && page.url.pathname === "/"),
|
||||
);
|
||||
const showChat = $derived(chatEnabled && !showDiscussions);
|
||||
|
||||
// On mobile a route change should always land on the forum pane, so opening
|
||||
// a thread or room from the menu never leaves the user stranded on chat.
|
||||
|
|
@ -85,7 +95,7 @@
|
|||
>
|
||||
<LeftSidebar {mode} {activeRoom} />
|
||||
<main
|
||||
class="min-h-[calc(100dvh_-_4rem)] bg-white px-6 pt-8 pb-20 shadow-lg md:min-h-0 md:overflow-y-auto md:rounded-t-xl md:px-10 md:pt-6 {isHomeFull
|
||||
class="min-h-[calc(100dvh_-_4rem)] bg-white px-6 pt-8 pb-20 shadow-lg md:min-h-0 md:overflow-y-auto md:rounded-t-xl md:px-10 md:pt-6 {showDiscussions
|
||||
? 'md:flex-[3]'
|
||||
: 'md:flex-1'}
|
||||
{mobileView === 'chat' ? 'hidden md:block' : 'block'}"
|
||||
|
|
@ -99,10 +109,11 @@
|
|||
onToggle={() => (chatExpanded = !chatExpanded)}
|
||||
mobileActive={mobileView === "chat"}
|
||||
/>
|
||||
{:else if isHomeFull}
|
||||
<!-- Own panel (40%) so the gray gutter matches the main↔chat gap. -->
|
||||
{:else if showDiscussions}
|
||||
<!-- Own panel (40%) so the gray gutter matches the main↔chat gap.
|
||||
Hidden on mobile — it's supplementary to the main column. -->
|
||||
<div
|
||||
class="mt-2 min-w-0 bg-white px-6 pt-8 pb-20 shadow-lg md:mt-0 md:flex-[2] md:overflow-y-auto md:rounded-t-xl md:px-8 md:pt-6"
|
||||
class="hidden min-w-0 bg-white px-6 pt-8 pb-20 shadow-lg md:mt-0 md:block md:flex-[2] md:overflow-y-auto md:rounded-t-xl md:px-8 md:pt-6"
|
||||
>
|
||||
<LatestDiscussions />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { groupStore } from "$lib/group.svelte";
|
||||
import { GROUP_ID } from "$lib/config";
|
||||
|
||||
const name = $derived(groupStore.data?.name ?? GROUP_ID);
|
||||
const about = $derived(groupStore.data?.about ?? "");
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>About — {name}</title>
|
||||
<meta name="description" content={about} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<h1 class="py-2 text-[1.65rem] text-brand">About {name}</h1>
|
||||
{#if about}
|
||||
<p class="leading-relaxed whitespace-pre-wrap text-neutral-600">{about}</p>
|
||||
{:else}
|
||||
<p class="text-neutral-400">No description available.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -2,7 +2,11 @@
|
|||
import * as nip19 from "@nostr/tools/nip19";
|
||||
import { groupStore } from "$lib/group.svelte";
|
||||
import { groupsStore } from "$lib/groups.svelte";
|
||||
import { roomAdminsStore, loadRoomAdmins } from "$lib/admins.svelte";
|
||||
import {
|
||||
roomAdminsStore,
|
||||
loadRoomAdmins,
|
||||
adminPubkeys,
|
||||
} from "$lib/admins.svelte";
|
||||
import { MODE } from "$lib/config";
|
||||
import {
|
||||
profileStore,
|
||||
|
|
@ -17,15 +21,9 @@
|
|||
if (ids.length > 0) loadRoomAdmins(ids);
|
||||
});
|
||||
|
||||
const adminPubkeys = $derived<string[]>(
|
||||
MODE === "full"
|
||||
? [...new Set(Object.values(roomAdminsStore.byRoom).flat())]
|
||||
: (groupStore.data?.admins ?? []),
|
||||
);
|
||||
|
||||
// Group data is loaded by the layout; profiles stream in reactively.
|
||||
$effect(() => {
|
||||
for (const pk of adminPubkeys) ensureProfile(pk);
|
||||
for (const pk of adminPubkeys.list) ensureProfile(pk);
|
||||
});
|
||||
|
||||
// Rooms a given admin manages (full mode only).
|
||||
|
|
@ -45,7 +43,7 @@
|
|||
type Contact = { pubkey: string; npub: string; entry?: ProfileEntry };
|
||||
|
||||
const contacts = $derived<Contact[]>(
|
||||
adminPubkeys.map((pk) => {
|
||||
adminPubkeys.list.map((pk) => {
|
||||
const entry = profileStore.profiles.get(pk);
|
||||
return { pubkey: pk, npub: entry?.npub ?? nip19.npubEncode(pk), entry };
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,24 @@ body {
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* Tighter heading rhythm for all rendered (prose) content. Overrides the
|
||||
typography plugin's em-based heading margins; the first block stays flush. */
|
||||
.prose :is(h1, h2, h3, h4, h5, h6) {
|
||||
margin-top: 0.8em;
|
||||
margin-bottom: 0.3em;
|
||||
font-weight: normal;
|
||||
}
|
||||
.prose :is(h1) {
|
||||
font-size: 1.65rem;
|
||||
color: var(--color-brand);
|
||||
}
|
||||
.prose :is(p) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.prose > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-brand: #e32a6d;
|
||||
--color-brand-hover: #c4205a;
|
||||
|
|
|
|||
63
src/routes/resource/[slug]/+page.svelte
Normal file
63
src/routes/resource/[slug]/+page.svelte
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import { resourcesStore } from "$lib/resources.svelte";
|
||||
import { adminPubkeys } from "$lib/admins.svelte";
|
||||
import PostContent from "$lib/components/PostContent.svelte";
|
||||
|
||||
const slug = $derived(page.params.slug ?? "");
|
||||
const resource = $derived(resourcesStore.list.find((r) => r.slug === slug));
|
||||
// The list is admin-filtered, so a verdict needs both fetches in.
|
||||
const ready = $derived(resourcesStore.loaded && adminPubkeys.loaded);
|
||||
const notFound = $derived(ready && !resource);
|
||||
|
||||
// First non-empty line of the body, trimmed of markdown markers, for SEO.
|
||||
const description = $derived(
|
||||
(resource?.content ?? "")
|
||||
.replace(/^#+\s*/gm, "")
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.find(Boolean)
|
||||
?.slice(0, 160) ?? "",
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{resource?.title ?? "Resource"}</title>
|
||||
{#if description}
|
||||
<meta name="description" content={description} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<a
|
||||
href="/"
|
||||
class="mb-1 inline-flex items-center gap-1 text-sm text-neutral-400 hover:text-brand"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
Home
|
||||
</a>
|
||||
|
||||
{#if resource}
|
||||
<div class="mt-2">
|
||||
<PostContent content={resource.content} headingOffset={0} />
|
||||
</div>
|
||||
{:else if notFound}
|
||||
<p class="py-12 text-center text-neutral-400">Resource not found.</p>
|
||||
{:else}
|
||||
<p class="py-12 text-center text-neutral-400">Loading…</p>
|
||||
{/if}
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue