feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user