diff --git a/app/(main)/spaces/[slug]/(profile)/-/program/page.tsx b/app/(main)/spaces/[slug]/(profile)/-/program/page.tsx new file mode 100644 index 0000000000..2e4b436ec5 --- /dev/null +++ b/app/(main)/spaces/[slug]/(profile)/-/program/page.tsx @@ -0,0 +1,71 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { getCallerProfile } from '@/lib/auth' +import { getVisibleSpaceBySlug } from '@/lib/spaces/store' +import { resolveSpaceManageAccess } from '@/lib/spaces/entitlements' +import { spaceFunctionDef, spaceFunctionEnabled } from '@/lib/spaces/functions' +import { spaceProfileMetadata } from '@/lib/spaces/profile-metadata' +import { spaceManageHref } from '@/lib/spaces/types' +import { spaceProgramHref } from '@/lib/spaces/primary-nav' +import { LEARNING_KINDS, LEARNING_PAGE_SIZE, learningPageNumber, readSpaceLearningPage, type LearningKind } from '@/lib/spaces/program-catalog' +import { SectionHeader } from '@/components/ui/section-header' +import { EmptyState } from '@/components/ui/empty-state' +import { buttonClasses } from '@/components/ui/button' +import { EntityCard } from '@/components/cards/entity-card' + +const LABEL: Record = { program: 'Programs', journey: 'Journeys', practice: 'Practices' } +const FUNCTION: Record = { program: 'program', journey: 'journeys', practice: 'practices' } + +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { + const { slug } = await params + return spaceProfileMetadata(slug, { + segment: '-/program', label: 'Program', + describe: (name) => `Explore public Programs, Journeys and Practices from ${name}.`, + }) +} + +export default async function SpaceLearningPage({ params, searchParams }: { + params: Promise<{ slug: string }> + searchParams: Promise<{ programs?: string | string[]; journeys?: string | string[]; practices?: string | string[] }> +}) { + const [{ slug }, query, caller] = await Promise.all([params, searchParams, getCallerProfile()]) + const space = await getVisibleSpaceBySlug(slug, caller?.id ?? null) + if (!space) notFound() + const access = await resolveSpaceManageAccess(space, caller?.id ?? null, caller?.webRole ?? null) + const pages = { program: learningPageNumber(query.programs), journey: learningPageNumber(query.journeys), practice: learningPageNumber(query.practices) } + const enabled = LEARNING_KINDS.filter((kind) => { + const def = spaceFunctionDef(FUNCTION[kind]) + return !!def && spaceFunctionEnabled(space, def) + }) + const groups = await Promise.all(enabled.map((kind) => readSpaceLearningPage(space.id, kind, pages[kind]))) + const href = (kind: LearningKind, page: number) => { + const q = new URLSearchParams() + for (const k of LEARNING_KINDS) { + const n = k === kind ? page : pages[k] + if (n > 1) q.set(`${k}s`, String(n)) + } + return `${spaceProgramHref(slug)}${q.size ? `?${q}` : ''}` + } + const nothing = groups.every((group) => !group.total && !group.unavailable) + return ( +
+
+ Manage learning : undefined} /> +

Programs, Journeys and Practices published for everyone by {space.brandName ?? space.name}.

+
+ {nothing ? : null} + {groups.filter((group) => group.total > 0 || group.unavailable).map((group) => ( +
+ + {group.unavailable ? Try again} /> : group.items.length ?
{group.items.map((item) => )}
: First page} />} + {!group.unavailable && group.items.length > 0 && group.total > LEARNING_PAGE_SIZE ? : null} +
+ ))} +
+ ) +} diff --git a/app/(main)/spaces/[slug]/(profile)/full/page.tsx b/app/(main)/spaces/[slug]/(profile)/full/page.tsx index e96a4a84d7..b7199d5e06 100644 --- a/app/(main)/spaces/[slug]/(profile)/full/page.tsx +++ b/app/(main)/spaces/[slug]/(profile)/full/page.tsx @@ -10,6 +10,8 @@ import { getMyMembership, listMembershipTiers } from '@/lib/spaces/memberships' import { SpaceFrontDoor } from '@/components/spaces/space-front-door' import type { SpaceViewer } from '@/lib/spaces/front-door' import { SpaceProfileModules } from '@/components/widgets/space-profile/space-profile-modules' +import { SpaceContextLinks } from '@/components/spaces/space-context-links' +import { buildSpaceProfileNav } from '@/lib/spaces/profile-nav' import { OwnerSpaceLayoutPreview } from '@/components/spaces/owner-space-layout-preview' import { ProfileBodySkeleton } from '@/components/spaces/profile-body-skeleton' import { SpaceBodyPanel } from '@/components/spaces/workspace/space-body-panel' @@ -74,7 +76,8 @@ async function SpaceProfileBody({ slug, panel, area, tab, view }: { slug: string // A visitor / non-owner never sees a panel (canSeeAsOwner is false). if (canSeeAsOwner && isPanelId(panel)) return - if (canSeeAsOwner) return + const { contextLinks } = await buildSpaceProfileNav(space) + if (canSeeAsOwner) return <> // 🔴 THE MEMBER TREE IS THE ONLY ONE THAT CAN TELL A MEMBER FROM A STRANGER (LIVE-524), and until // now it never asked. A signed-in non-member and a paying member got BYTE-IDENTICAL bodies here, @@ -91,6 +94,7 @@ async function SpaceProfileBody({ slug, panel, area, tab, view }: { slug: string return ( <> + + {/* THE VISITOR'S FRONT DOOR (LIVE-524), and it sits BELOW the operator's blocks on purpose. LIVE-500's probe fails if ` @@ -114,19 +115,17 @@ export function SiteMenuEditor({ slug, className }: { slug: string; className?:

- {data.hasWebsite - ? 'One menu for your Space page and your website. Edit it here or in the other one and both stay in sync.' - : 'The menu across the top of your Space page. When you publish a website, it uses this menu too.'} + Your website keeps its own pages and menu. Links shown on your Space appear below Home; its main menu stays Home, Community, Calendar and Program, with Manage separate for your team.

{!data.canEdit && (

- Your menu builds itself from your pages and the features you turn on. Reordering, custom links and dropdowns come + Your website menu builds itself from your pages and the features you turn on. Reordering, custom links and dropdowns come with the Business plan.

)} @@ -230,7 +229,7 @@ function MenuItemRow({ {!isDropdown && ( onChange({ target })} /> )} - +