feat: certificates, landing pages, ai tutor, website update, compliance

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
@@ -0,0 +1,9 @@
<script lang="ts">
import { OrgAiTutorSettingsPage } from '$features/ai-tutor-settings';
</script>
<svelte:head>
<title>AI Tutor Settings - ClassroomIO</title>
</svelte:head>
<OrgAiTutorSettingsPage />
@@ -0,0 +1,13 @@
<script lang="ts">
import * as Page from '@cio/ui/base/page';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<Page.Root class="w-full pb-10">
{@render children?.()}
</Page.Root>
@@ -2,21 +2,7 @@
import { LandingpagePage } from '$features/settings/pages';
import { VisitOrgSiteButton } from '$features/ui';
import { t } from '$lib/utils/functions/translations';
import { Button } from '@cio/ui/base/button';
import * as Page from '@cio/ui/base/page';
let landingPageComponent: LandingpagePage | null = $state(null);
let hasUnsavedChanges = $state(false);
let isSaving = $state(false);
async function handleSave() {
isSaving = true;
try {
await landingPageComponent?.handleSave();
} finally {
isSaving = false;
}
}
</script>
<svelte:head>
@@ -30,14 +16,10 @@
</Page.HeaderContent>
<Page.Action>
<VisitOrgSiteButton labelKey="markdown_editor.preview" variant="secondary" />
<Button variant="default" loading={isSaving} disabled={isSaving || !hasUnsavedChanges} onclick={handleSave}>
{$t('settings.landing_page.save_changes')}
</Button>
</Page.Action>
</Page.Header>
<Page.Body>
{#snippet child()}
<LandingpagePage bind:this={landingPageComponent} bind:hasUnsavedChanges />
<LandingpagePage />
{/snippet}
</Page.Body>
@@ -1,26 +1,67 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { onMount } from 'svelte';
import { onMount, type Component } from 'svelte';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import type { AccountOrg } from '$features/app/types';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { orgApi } from '$features/org/api/org.svelte';
import { landingPageSettings } from '$features/settings/utils/store';
import { landingPageSettings, landingPageEditorSelection } from '$features/settings/utils/store';
import { buildOrgLandingPageProps, normalizeLandingPageSettings } from '$features/org/utils/landing-page';
import { landingPageThemeComponents } from '$features/org/utils/landing-page-components';
import { landingPageThemes } from '$features/org/utils/landing-page';
import LandingpageEditor from '$features/settings/pages/landingpage-editor.svelte';
import * as Sidebar from '@cio/ui/base/sidebar';
import { setLandingPageEditContext, type LandingSectionKey } from '@cio/ui/custom/org-landing-page';
import {
ContentIcon,
ExploreIcon,
ExternalLinkIcon,
GoalIcon,
HeaderIcon,
SettingsIcon
} from '@cio/ui/custom/moving-icons';
import { basePath } from '$lib/utils/store/app';
import { t } from '$lib/utils/functions/translations';
import { user } from '$lib/utils/store/user';
const sectionIcons: Record<LandingSectionKey, Component> = {
navigation: ContentIcon,
hero: HeaderIcon,
links: ExploreIcon,
embed: ExternalLinkIcon,
callout: GoalIcon,
footer: SettingsIcon
};
// Reset selection on every mount so the editor opens to the section list.
landingPageEditorSelection.set(null);
// Mirror the store into a $state so the preview's wrappers ($derived) react
// to selection changes. Reading store via get() inside an arrow is non-reactive.
let selectedSectionKey = $state<LandingSectionKey | null>(null);
$effect(() => landingPageEditorSelection.subscribe((value) => (selectedSectionKey = value)));
setLandingPageEditContext({
selectedKey: () => selectedSectionKey,
selectKey: (key) => landingPageEditorSelection.set(key),
labelFor: (key) => t.get(`settings.landing_page.editor.sections.${key}`),
iconFor: (key) => sectionIcons[key]
});
function handlePreviewClick(event: MouseEvent) {
if (!(event.target instanceof Element)) return;
if (event.target.closest('[data-landing-section]')) return;
landingPageEditorSelection.set(null);
}
let sidebarOpen = $state(true);
let hasInitialized = $state(false);
function isLandingPageTheme(value: string | null): value is 'minimal' | 'bold' | 'classic' {
return value === 'minimal' || value === 'bold' || value === 'classic';
function isLandingPageTheme(value: string | null): value is (typeof landingPageThemes)[number] {
if (value === null) return false;
return (landingPageThemes as readonly string[]).includes(value);
}
const savedLandingPageSettings = $derived(normalizeLandingPageSettings($currentOrg.landingpage));
@@ -94,14 +135,14 @@
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<Sidebar.Provider bind:open={sidebarOpen} style="--sidebar-width: 280px">
<Sidebar.Provider bind:open={sidebarOpen} style="--sidebar-width: 360px; --sidebar-width-icon: 4rem">
<Sidebar.Root side="left" collapsible="icon" class="h-full">
<LandingpageEditor
bind:settings={$landingPageSettings}
onSave={async () => {
const updatedLandingPage = normalizeLandingPageSettings({
...$landingPageSettings,
theme: page.url.searchParams.get('theme') ? savedLandingPageSettings.theme : $landingPageSettings.theme
theme: previewTheme
});
await orgApi.update($currentOrg.id, {
@@ -114,6 +155,7 @@
<Sidebar.Inset
class="relative h-screen! w-[calc(100vw-var(--sidebar-width))] overflow-y-auto group-data-[collapsible=icon]:w-[calc(100vw-var(--sidebar-width-icon))]"
onclick={handlePreviewClick}
>
<div class="absolute top-2 left-2 z-60">
<Sidebar.Trigger variant="secondary" />
@@ -0,0 +1,22 @@
<script lang="ts">
import * as Page from '@cio/ui/base/page';
import { WorkspacesPage } from '$features/account';
import { t } from '$lib/utils/functions/translations';
</script>
<svelte:head>
<title>{$t('account.workspaces.page_title')} - ClassroomIO</title>
</svelte:head>
<Page.Header>
<Page.HeaderContent>
<Page.Title>{$t('account.workspaces.page_title')}</Page.Title>
<Page.Subtitle>{$t('account.workspaces.page_description')}</Page.Subtitle>
</Page.HeaderContent>
</Page.Header>
<Page.Body>
{#snippet child()}
<WorkspacesPage />
{/snippet}
</Page.Body>