9.4 KiB
Org Landing Page Theme Picker + Unified Content Editor
Status
- Done (shipped)
Context
The org has 3 landing page theme templates (Minimal, Bold, Classic) in packages/ui/ that all accept OrgLandingPageProps uniformly. However:
- The templates are only used in Storybook — the actual public landing page (
landing-page.svelte) renders its own custom legacy HTML - The stored
OrgLandingPageJsonis a complex format (header, aboutUs, faq, contact, mailinglist, etc.) that doesn't align with what the templates need - There is no theme selection UI
- There is no edit flow like the course landing page (split-screen with live preview)
This plan: simplifies the stored format to match OrgLandingPageProps, replaces the legacy public landing page renderer with the theme components, adds a theme picker in settings, and adds a split-screen content editor modeled after the course landing page editor.
Simplified Data Model
Replace OrgLandingPageJson (the DB-stored type) with a structure that maps 1:1 to OrgLandingPageProps:
// apps/dashboard/src/lib/utils/types/org.ts
export interface OrgLandingPageJson {
theme: 'minimal' | 'bold' | 'classic';
hero: {
heading: string;
subheading: string;
primaryAction: { label: string; href: string };
secondaryAction?: { label: string; href: string };
image?: string;
};
navItems: Array<{ label: string; href: string }>;
footerLinks: Array<{ label: string; href: string }>;
footerText: string;
}
Sections removed from old format: header (with banner/background/show toggles), aboutUs, courses, faq, contact, mailinglist, customLinks, footer (social links only).
Fields from org data (not stored in landingpage): orgName → org.name, logoUrl → org.avatarUrl, courses → orgApi.publicCourses.
Changes
1. Update OrgLandingPageJson type
File: apps/dashboard/src/lib/utils/types/org.ts
Replace the full type with the simplified version above.
2. Update settings store with new defaults
File: apps/dashboard/src/lib/features/settings/utils/store.ts
Replace landingPageSettings writable defaults with the new schema:
{
theme: 'minimal',
hero: {
heading: 'Master the Skills of Tomorrow',
subheading: 'Join thousands of learners and accelerate your career.',
primaryAction: { label: 'Explore Courses', href: '#courses' },
secondaryAction: { label: 'Learn More', href: '#about' },
image: ''
},
navItems: [
{ label: 'Courses', href: '#courses' },
{ label: 'About', href: '#about' }
],
footerLinks: [
{ label: 'Terms', href: '#terms' },
{ label: 'Privacy', href: '#privacy' }
],
footerText: '© 2026 All rights reserved.'
}
Also update (or delete and inline) the old store at:
apps/dashboard/src/lib/features/org/components/settings/landing-page-store.ts
(currently used by the legacy landing-page.svelte — this store will go away when we replace the renderer).
3. Redesign landingpage.svelte settings page — theme picker only
File: apps/dashboard/src/lib/features/settings/pages/landingpage.svelte
Remove all existing form fields. The page now shows only:
- Theme picker (
Field.Set"Choose a Theme"): 3 cards ingrid grid-cols-3 gap-4usingRadioGroup.Root bind:value={$landingPageSettings.theme}. Each card:- Inline SVG thumbnail (distinct per theme — see SVG specs below)
- Theme name label
- Radio selection state:
border-primary ring-2 ring-primary/20vsborder-border onValueChange={() => (hasUnsavedChanges = true)}
- Save button already in the route page header — saves only
theme(plus full JSON as fallback) - "Edit Content" button below the theme picker (
Button variant="outline") → navigates to/org/[slug]/settings/landingpage/edit UnsavedChangescomponent stays
SVG thumbnail specs:
- Minimal: white bg, light gray nav bar top, 2 text lines (heading/subheading), 2 course card outlines in a 2-col grid, thin footer line
- Bold: dark
#111bg top-half, large bold heading text block, course cards with image-left layout, dark inverted footer - Classic: dark navy
#1e3a5fheader bar, white body, centered heading, 3-col course card grid, minimal footer
Subscriber fallback for old data (users who previously saved the old format):
if (!landingpage.theme || !landingpage.hero) {
// Set minimal defaults — discard old incompatible structure
$landingPageSettings = { ...defaultLandingPageSettings };
} else {
$landingPageSettings = landingpage;
}
4. New split-screen content editor route
File (new): apps/dashboard/src/routes/org/[slug]/settings/landingpage/edit/+page.svelte
Modeled after apps/dashboard/src/routes/courses/[id]/landingpage/+page.svelte.
Layout:
<!-- Fixed full-screen overlay, fly transition -->
<div class="fixed inset-0 z-250 h-screen w-screen">
<Sidebar.Provider>
<Sidebar.Root side="left" collapsible="icon">
<OrgLandingPageEditor bind:settings={landingPageSettings} {org} onSave={handleSave} onClose={handleClose} />
</Sidebar.Root>
<Sidebar.Inset class="relative h-screen overflow-y-auto">
<!-- Live theme preview -->
{#if $landingPageSettings.theme === 'bold'}
<BoldLandingPage {...previewProps} />
{:else if $landingPageSettings.theme === 'classic'}
<ClassicLandingPage {...previewProps} />
{:else}
<MinimalLandingPage {...previewProps} />
{/if}
</Sidebar.Inset>
</Sidebar.Provider>
</div>
previewProps is derived from $landingPageSettings + $currentOrg + orgApi.publicCourses (mapped to OrgLandingPageProps).
handleClose() → goto('/org/[slug]/settings/landingpage').
5. New editor sidebar component
File (new): apps/dashboard/src/lib/features/settings/pages/landingpage-editor.svelte
Mirrors the section-based structure of the course landing page editor.svelte.
Sections (left sidebar menu → click to edit):
- Hero — heading, subheading, primaryAction (label + href), secondaryAction (optional label + href toggle), image (upload widget, used by Bold/Classic)
- Navigation — list of nav items (label + href), add/remove rows
- Footer — footer text, list of footer links (label + href), add/remove rows
Header: "Save" button + close button (back to settings).
Section form pattern: inline Input / Textarea / UploadWidget fields, two-way bound to settings prop, changes reflect live in the preview panel.
6. Replace legacy public landing page renderer
File: apps/dashboard/src/lib/features/org/components/landing-page/landing-page.svelte
Replace the full custom HTML renderer with:
- Import
MinimalLandingPage,BoldLandingPage,ClassicLandingPagefrom@cio/ui/custom/org-landing-page - Load
org.landingpageinto local state with defaults fallback - Build
previewProps: OrgLandingPageProps={ orgName: org.name, logoUrl: org.avatarUrl, ...landingpage, courses: orgApi.publicCourses } - Render the correct theme component based on
landingpage.theme - Remove all legacy section rendering (aboutUs, faq, contact, mailinglist, etc.)
- Keep
orgApi.getPublicCoursesBySiteName(orgSiteName)call for course loading - Keep
<svelte:head>and<PoweredBy />
Critical Files
| File | Change |
|---|---|
apps/dashboard/src/lib/utils/types/org.ts |
Replace OrgLandingPageJson type |
apps/dashboard/src/lib/features/settings/utils/store.ts |
New defaults matching new type |
apps/dashboard/src/lib/features/org/components/settings/landing-page-store.ts |
Delete or update (old format no longer needed) |
apps/dashboard/src/lib/features/settings/pages/landingpage.svelte |
Theme picker + Edit Content button only |
apps/dashboard/src/routes/org/[slug]/settings/landingpage/+page.svelte |
Minor adjustments if needed |
apps/dashboard/src/lib/features/org/components/landing-page/landing-page.svelte |
Replace renderer with theme components |
apps/dashboard/src/routes/org/[slug]/settings/landingpage/edit/+page.svelte |
New — split-screen editor route |
apps/dashboard/src/lib/features/settings/pages/landingpage-editor.svelte |
New — editor sidebar component |
Reused Patterns
Sidebar.Provider/Root/Insetfrom@cio/ui/base/sidebar— same as course landing page editor routeRadioGroup.Root/Item— already imported inlandingpage.svelteField.Set/Field.Legend/Field.Separator— already imported inlandingpage.svelteUploadWidget— already imported inlandingpage.svelteorgApi.update($currentOrg.id, { landingpage: ... })— same save patternUnsavedChangescomponent — already inlandingpage.sveltegoto()for close/navigate — same as course editor
Verification
- Theme picker:
/org/[slug]/settings/landingpageshows 3 cards with distinct SVG previews; clicking a card highlights it; saving persists the selection; reload reflects saved theme - Edit Content: clicking "Edit Content" navigates to
/org/[slug]/settings/landingpage/edit; the editor opens as a full-screen overlay with the selected theme visible on the right; editing fields updates the preview live - Public landing page: visiting the org's public URL renders the selected theme (Minimal/Bold/Classic) using actual org courses; switching theme in settings and saving changes what public visitors see
- Backward compat: orgs with old
OrgLandingPageJsonformat get reset to minimal defaults gracefully (no crash)