feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.
This commit is contained in:
@@ -5,10 +5,9 @@
|
||||
import HourglassIcon from '@lucide/svelte/icons/hourglass';
|
||||
import { openUpgradeModal } from '$lib/utils/functions/org';
|
||||
import ReceiptIcon from '@lucide/svelte/icons/receipt';
|
||||
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
||||
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Item from '@cio/ui/base/item';
|
||||
import { ExternalLinkIcon, HoverableItem } from '@cio/ui/custom/moving-icons';
|
||||
|
||||
let isRedirecting = $state(false);
|
||||
|
||||
@@ -28,7 +27,7 @@
|
||||
if ($currentOrgPlan?.provider === 'lmz') {
|
||||
isRedirecting = true;
|
||||
const url = new URL('/api/lmz/subscription', window.location.origin);
|
||||
url.searchParams.set('subscriptionId', $currentOrgPlan?.subscriptionId);
|
||||
url.searchParams.set('subscriptionId', $currentOrgPlan?.subscriptionId || '');
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const result = await response.json();
|
||||
@@ -70,10 +69,14 @@
|
||||
<Item.Description>{$t('settings.billing.provider')}</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<Button size="sm" variant="outline" onclick={onOpenBilling} loading={isRedirecting}>
|
||||
<ExternalLinkIcon class="size-4" />
|
||||
{$t('settings.billing.open_billing')}
|
||||
</Button>
|
||||
<HoverableItem>
|
||||
{#snippet children(isHovered)}
|
||||
<Button size="sm" variant="outline" onclick={onOpenBilling} loading={isRedirecting}>
|
||||
<ExternalLinkIcon {isHovered} size={16} ariaHidden={true} />
|
||||
{$t('settings.billing.open_billing')}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</HoverableItem>
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
{/if}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import isValidDomain from 'is-valid-domain';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { parse } from 'tldts';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
@@ -157,6 +156,7 @@
|
||||
}
|
||||
|
||||
const sanitizedDomain = sanitizeDomain(customDomain);
|
||||
const { parse } = await import('tldts/dist/es6/index.js');
|
||||
const details = parse(sanitizedDomain);
|
||||
|
||||
if (!details.subdomain) {
|
||||
@@ -338,7 +338,7 @@
|
||||
{#if domainSetup?.dnsRecords?.length}
|
||||
<Field.Field>
|
||||
<div class="space-y-3 rounded-md border px-4 py-3">
|
||||
{#each domainSetup.dnsRecords as record}
|
||||
{#each domainSetup.dnsRecords as record (record.name)}
|
||||
<div class="grid gap-3 sm:grid-cols-[80px,1fr,1fr] sm:items-center">
|
||||
<div>
|
||||
<Field.Label class="font-light">{$t('components.settings.domains.dns_type')}</Field.Label>
|
||||
|
||||
@@ -4,6 +4,7 @@ export { default as TeamsPage } from './teams.svelte';
|
||||
export { default as CustomizeLmsPage } from './customize-lms.svelte';
|
||||
export { default as OrgPage } from './org.svelte';
|
||||
export { default as LandingpagePage } from './landingpage.svelte';
|
||||
export { default as LandingpageEditor } from './landingpage-editor.svelte';
|
||||
export { default as BillingPage } from './billing.svelte';
|
||||
export { default as IntegrationsPage } from './integrations.svelte';
|
||||
export { default as AuthPage } from './auth.svelte';
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { CloseButton, UnsavedChanges } from '$features/ui';
|
||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||
import { useSidebar } from '@cio/ui/base/sidebar';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import type { OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
import { ContentIcon, ExternalLinkIcon, GoalIcon, HeaderIcon, SettingsIcon } from '@cio/ui/custom/moving-icons';
|
||||
|
||||
import HeroSection from './landingpage-editor/hero-section.svelte';
|
||||
import NavigationSection from './landingpage-editor/navigation-section.svelte';
|
||||
import CalloutSection from './landingpage-editor/callout-section.svelte';
|
||||
import EmbedSection from './landingpage-editor/embed-section.svelte';
|
||||
import FooterSection from './landingpage-editor/footer-section.svelte';
|
||||
|
||||
interface Props {
|
||||
settings: OrgLandingPageJson;
|
||||
onSave: () => Promise<void> | void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type SectionKey = 'hero' | 'navigation' | 'callout' | 'embed' | 'footer';
|
||||
|
||||
interface SectionDefinition {
|
||||
key: SectionKey;
|
||||
title: string;
|
||||
icon: typeof HeaderIcon;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
let { settings = $bindable(), onSave, onClose }: Props = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
let selectedSection = $state<SectionKey | null>(null);
|
||||
let isSaving = $state(false);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
|
||||
const sections: SectionDefinition[] = [
|
||||
{
|
||||
key: 'navigation',
|
||||
title: t.get('settings.landing_page.editor.sections.navigation'),
|
||||
icon: ContentIcon,
|
||||
component: NavigationSection
|
||||
},
|
||||
{
|
||||
key: 'hero',
|
||||
title: t.get('settings.landing_page.editor.sections.hero'),
|
||||
icon: HeaderIcon,
|
||||
component: HeroSection
|
||||
},
|
||||
{
|
||||
key: 'embed',
|
||||
title: t.get('settings.landing_page.editor.sections.embed'),
|
||||
icon: ExternalLinkIcon,
|
||||
component: EmbedSection
|
||||
},
|
||||
{
|
||||
key: 'callout',
|
||||
title: t.get('settings.landing_page.editor.sections.callout'),
|
||||
icon: GoalIcon,
|
||||
component: CalloutSection
|
||||
},
|
||||
{
|
||||
key: 'footer',
|
||||
title: t.get('settings.landing_page.editor.sections.footer'),
|
||||
icon: SettingsIcon,
|
||||
component: FooterSection
|
||||
}
|
||||
];
|
||||
|
||||
const selectedSectionDefinition = $derived(sections.find((section) => section.key === selectedSection) ?? null);
|
||||
|
||||
function markDirty() {
|
||||
hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
if (selectedSection) {
|
||||
selectedSection = null;
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
async function handleSaveClick() {
|
||||
isSaving = true;
|
||||
|
||||
try {
|
||||
await onSave();
|
||||
hasUnsavedChanges = false;
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
|
||||
<Sidebar.Header
|
||||
class="flex flex-row! items-center {sidebar.open ? 'justify-between' : 'justify-center'} border-b px-2 py-2"
|
||||
>
|
||||
{#if !selectedSection}
|
||||
{#if sidebar.open}
|
||||
<CloseButton onClick={handleClose} />
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" onclick={handleSaveClick} loading={isSaving} disabled={isSaving}>
|
||||
{$t('settings.landing_page.editor.save')}
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<CloseButton onClick={handleClose} />
|
||||
{/if}
|
||||
{:else if sidebar.open || !sidebar.isMobile}
|
||||
<div class="flex items-center gap-2">
|
||||
<IconButton onclick={() => (selectedSection = null)}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
</IconButton>
|
||||
<h3 class="text-sm font-semibold">
|
||||
{selectedSectionDefinition?.title}
|
||||
</h3>
|
||||
</div>
|
||||
{:else}
|
||||
<IconButton onclick={() => (selectedSection = null)} tooltip={selectedSectionDefinition?.title ?? ''}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</Sidebar.Header>
|
||||
|
||||
<Sidebar.Content class="flex-1 overflow-y-auto">
|
||||
{#if !selectedSection}
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel class="px-2">
|
||||
{$t('settings.landing_page.editor.page_builder')}
|
||||
</Sidebar.GroupLabel>
|
||||
<Sidebar.GroupContent>
|
||||
<Sidebar.Menu>
|
||||
{#each sections as section (section.key)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton onclick={() => (selectedSection = section.key)} tooltipContent={section.title}>
|
||||
{@const SectionIcon = section.icon}
|
||||
<SectionIcon size={16} />
|
||||
<span>{section.title}</span>
|
||||
<ChevronRightIcon size={16} />
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.GroupContent>
|
||||
</Sidebar.Group>
|
||||
{:else if selectedSectionDefinition}
|
||||
{@const SectionComponent = selectedSectionDefinition.component}
|
||||
<div class="p-4">
|
||||
<SectionComponent bind:settings {markDirty} />
|
||||
</div>
|
||||
{/if}
|
||||
</Sidebar.Content>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
href: string;
|
||||
labelFieldLabel: string;
|
||||
hrefFieldLabel: string;
|
||||
labelPlaceholder?: string;
|
||||
hrefPlaceholder?: string;
|
||||
className?: string;
|
||||
removeAriaLabel?: string;
|
||||
onLabelInput: (value: string) => void;
|
||||
onHrefInput: (value: string) => void;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
href,
|
||||
labelFieldLabel,
|
||||
hrefFieldLabel,
|
||||
labelPlaceholder,
|
||||
hrefPlaceholder,
|
||||
className = 'space-y-4',
|
||||
removeAriaLabel,
|
||||
onLabelInput,
|
||||
onHrefInput,
|
||||
onRemove
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
{#if onRemove}
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
class="absolute! -top-3! right-2!"
|
||||
aria-label={removeAriaLabel}
|
||||
onclick={() => onRemove()}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{labelFieldLabel}</Field.Label>
|
||||
<Input value={label} placeholder={labelPlaceholder} oninput={(event) => onLabelInput(event.currentTarget.value)} />
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{hrefFieldLabel}</Field.Label>
|
||||
<Input value={href} placeholder={hrefPlaceholder} oninput={(event) => onHrefInput(event.currentTarget.value)} />
|
||||
</Field.Field>
|
||||
</div>
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import type { OrgLandingPageCallout, OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
|
||||
interface Props {
|
||||
settings: OrgLandingPageJson;
|
||||
markDirty: () => void;
|
||||
}
|
||||
|
||||
let { settings = $bindable(), markDirty }: Props = $props();
|
||||
|
||||
function setter(value: unknown, setterKey: string) {
|
||||
if (typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSettings = cloneDeep(settings);
|
||||
set(nextSettings, setterKey, value);
|
||||
settings = nextSettings;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function ensureCallout(): OrgLandingPageCallout {
|
||||
if (settings.callout) {
|
||||
return settings.callout;
|
||||
}
|
||||
|
||||
const callout: OrgLandingPageCallout = {
|
||||
heading: t.get('settings.landing_page.editor.callout.heading_default'),
|
||||
description: '',
|
||||
action: {
|
||||
label: t.get('settings.landing_page.editor.callout.action_label_default'),
|
||||
href: '#'
|
||||
}
|
||||
};
|
||||
|
||||
settings = {
|
||||
...settings,
|
||||
callout
|
||||
};
|
||||
markDirty();
|
||||
|
||||
return callout;
|
||||
}
|
||||
|
||||
function setCalloutEnabled(enabled: boolean) {
|
||||
if (!enabled) {
|
||||
settings = {
|
||||
...settings,
|
||||
callout: undefined
|
||||
};
|
||||
markDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
setter(ensureCallout(), 'callout');
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field.Group class="space-y-6">
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.editor.sections.callout')}</Field.Legend>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch
|
||||
checked={Boolean(settings.callout)}
|
||||
onCheckedChange={(checked) => setCalloutEnabled(checked === true)}
|
||||
/>
|
||||
<Field.Label>{$t('settings.landing_page.editor.callout.toggle')}</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
{#if settings.callout}
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.editor.callout.heading')}</Field.Label>
|
||||
<Input
|
||||
value={settings.callout.heading}
|
||||
placeholder={$t('settings.landing_page.editor.callout.heading_placeholder')}
|
||||
oninput={(event) => setter(event.currentTarget.value, 'callout.heading')}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.editor.callout.description')}</Field.Label>
|
||||
<Textarea
|
||||
class="ui:[field-sizing:fixed] ui:min-w-0 ui:w-full ui:max-w-full ui:box-border ui:resize-y"
|
||||
value={settings.callout.description}
|
||||
placeholder={$t('settings.landing_page.editor.callout.description_placeholder')}
|
||||
oninput={(event) => setter(event.currentTarget.value, 'callout.description')}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.editor.callout.action_label')}</Field.Label>
|
||||
<Input
|
||||
value={settings.callout.action.label}
|
||||
placeholder={$t('settings.landing_page.editor.callout.action_label_placeholder')}
|
||||
oninput={(event) => setter(event.currentTarget.value, 'callout.action.label')}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.editor.callout.action_href')}</Field.Label>
|
||||
<Input
|
||||
value={settings.callout.action.href}
|
||||
placeholder={$t('settings.landing_page.editor.callout.action_href_placeholder')}
|
||||
oninput={(event) => setter(event.currentTarget.value, 'callout.action.href')}
|
||||
/>
|
||||
</Field.Field>
|
||||
{/if}
|
||||
</div>
|
||||
</Field.Set>
|
||||
</Field.Group>
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import type { OrgLandingPageEmbed, OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
import ButtonLinkFields from './button-link-fields.svelte';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
|
||||
interface Props {
|
||||
settings: OrgLandingPageJson;
|
||||
markDirty: () => void;
|
||||
}
|
||||
|
||||
let { settings = $bindable(), markDirty }: Props = $props();
|
||||
|
||||
function setter(value: unknown, setterKey: string) {
|
||||
if (typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSettings = cloneDeep(settings);
|
||||
set(nextSettings, setterKey, value);
|
||||
settings = nextSettings;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function ensureEmbed(): OrgLandingPageEmbed {
|
||||
if (settings.embed) {
|
||||
return settings.embed;
|
||||
}
|
||||
|
||||
const embed = {
|
||||
title: t.get('settings.landing_page.editor.embed.title'),
|
||||
description: '',
|
||||
code: '',
|
||||
secondaryAction: undefined
|
||||
};
|
||||
|
||||
settings = {
|
||||
...settings,
|
||||
embed
|
||||
};
|
||||
markDirty();
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
function setEmbedEnabled(enabled: boolean) {
|
||||
if (!enabled) {
|
||||
settings = {
|
||||
...settings,
|
||||
embed: undefined
|
||||
};
|
||||
markDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
setter(ensureEmbed(), 'embed');
|
||||
}
|
||||
|
||||
function updateSecondaryAction(label: string, href: string) {
|
||||
if (!settings.embed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setter(
|
||||
{
|
||||
label,
|
||||
href
|
||||
},
|
||||
'embed.secondaryAction'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field.Group class="space-y-6">
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.editor.sections.embed')}</Field.Legend>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch checked={Boolean(settings.embed)} onCheckedChange={(checked) => setEmbedEnabled(checked === true)} />
|
||||
<Field.Label>{$t('settings.landing_page.editor.embed.toggle')}</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
{#if settings.embed}
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.editor.embed.title')}</Field.Label>
|
||||
<Input
|
||||
value={settings.embed.title}
|
||||
placeholder={$t('settings.landing_page.editor.embed.title_placeholder')}
|
||||
oninput={(event) => setter(event.currentTarget.value, 'embed.title')}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field class="min-w-0">
|
||||
<Field.Label>{$t('settings.landing_page.editor.embed.description')}</Field.Label>
|
||||
<Textarea
|
||||
class="ui:[field-sizing:fixed] ui:min-w-0 ui:w-full ui:max-w-full ui:box-border ui:resize-y"
|
||||
value={settings.embed.description ?? ''}
|
||||
placeholder={$t('settings.landing_page.editor.embed.description_placeholder')}
|
||||
oninput={(event) => setter(event.currentTarget.value, 'embed.description')}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field class="min-w-0">
|
||||
<Field.Label>{$t('settings.landing_page.editor.embed.code')}</Field.Label>
|
||||
<Field.Description>{$t('settings.landing_page.editor.embed.code_description')}</Field.Description>
|
||||
<Textarea
|
||||
class="ui:[field-sizing:fixed] ui:min-w-0 ui:w-full ui:max-w-full ui:box-border ui:resize-y"
|
||||
value={settings.embed.code}
|
||||
placeholder={$t('settings.landing_page.editor.embed.code_placeholder')}
|
||||
oninput={(event) => setter(event.currentTarget.value, 'embed.code')}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<ButtonLinkFields
|
||||
label={settings.embed.secondaryAction?.label ?? ''}
|
||||
href={settings.embed.secondaryAction?.href ?? ''}
|
||||
labelFieldLabel={$t('settings.landing_page.editor.embed.secondary_label')}
|
||||
hrefFieldLabel={$t('settings.landing_page.editor.embed.secondary_href')}
|
||||
labelPlaceholder={$t('settings.landing_page.editor.embed.secondary_label')}
|
||||
hrefPlaceholder={$t('settings.landing_page.editor.embed.secondary_href')}
|
||||
onLabelInput={(value) => updateSecondaryAction(value, settings.embed.secondaryAction?.href ?? '')}
|
||||
onHrefInput={(value) => updateSecondaryAction(settings.embed.secondaryAction?.label ?? '', value)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Field.Set>
|
||||
</Field.Group>
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import type { OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
import ButtonLinkFields from './button-link-fields.svelte';
|
||||
|
||||
interface Props {
|
||||
settings: OrgLandingPageJson;
|
||||
markDirty: () => void;
|
||||
}
|
||||
|
||||
let { settings = $bindable(), markDirty }: Props = $props();
|
||||
|
||||
function setter(value: unknown, setterKey: string) {
|
||||
if (typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSettings = cloneDeep(settings);
|
||||
set(nextSettings, setterKey, value);
|
||||
settings = nextSettings;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function addFooterLink() {
|
||||
settings = {
|
||||
...settings,
|
||||
footerLinks: [
|
||||
...settings.footerLinks,
|
||||
{
|
||||
label: t.get('settings.landing_page.editor.footer.default_label'),
|
||||
href: '#'
|
||||
}
|
||||
]
|
||||
};
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function removeFooterLink(index: number) {
|
||||
settings = {
|
||||
...settings,
|
||||
footerLinks: settings.footerLinks.filter((_, linkIndex) => linkIndex !== index)
|
||||
};
|
||||
markDirty();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field.Group class="space-y-6">
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.editor.sections.footer')}</Field.Legend>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Field.Field class="mb-6">
|
||||
<Field.Label>{$t('settings.landing_page.editor.footer.text')}</Field.Label>
|
||||
<Textarea
|
||||
value={settings.footerText}
|
||||
oninput={(event) => {
|
||||
settings = {
|
||||
...settings,
|
||||
footerText: event.currentTarget.value
|
||||
};
|
||||
markDirty();
|
||||
}}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<div class="space-y-4">
|
||||
{#each settings.footerLinks as footerLink, index (index)}
|
||||
<ButtonLinkFields
|
||||
label={footerLink.label}
|
||||
href={footerLink.href}
|
||||
labelFieldLabel={$t('settings.landing_page.editor.footer.link_label')}
|
||||
hrefFieldLabel={$t('settings.landing_page.editor.footer.link_href')}
|
||||
className="group ui:border-border relative space-y-3 border-b pb-5"
|
||||
removeAriaLabel={$t('settings.landing_page.editor.footer.remove')}
|
||||
onLabelInput={(value) => setter(value, `footerLinks.${index}.label`)}
|
||||
onHrefInput={(value) => setter(value, `footerLinks.${index}.href`)}
|
||||
onRemove={() => removeFooterLink(index)}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<Button type="button" variant="outline" onclick={addFooterLink}>
|
||||
{$t('settings.landing_page.editor.footer.add')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Field.Set>
|
||||
</Field.Group>
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<script lang="ts">
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import { UploadWidget } from '$features/ui';
|
||||
import { handleOpenWidget } from '$features/ui/course-landing-page/store';
|
||||
import type { OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
import ButtonLinkFields from './button-link-fields.svelte';
|
||||
|
||||
interface Props {
|
||||
settings: OrgLandingPageJson;
|
||||
markDirty: () => void;
|
||||
}
|
||||
|
||||
let { settings = $bindable(), markDirty }: Props = $props();
|
||||
|
||||
let activeWidget = $state<'hero-image' | ''>('');
|
||||
|
||||
function setter(value: unknown, setterKey: string) {
|
||||
if (typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSettings = cloneDeep(settings);
|
||||
set(nextSettings, setterKey, value);
|
||||
settings = nextSettings;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function toggleSecondaryAction(enabled: boolean) {
|
||||
if (!enabled) {
|
||||
settings = {
|
||||
...settings,
|
||||
hero: {
|
||||
...settings.hero,
|
||||
secondaryAction: undefined
|
||||
}
|
||||
};
|
||||
markDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
setter(
|
||||
settings.hero.secondaryAction ?? {
|
||||
label: t.get('settings.landing_page.editor.hero.secondary_label'),
|
||||
href: '/courses'
|
||||
},
|
||||
'hero.secondaryAction'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field.Group class="space-y-6">
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.editor.sections.hero')}</Field.Legend>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.editor.hero.heading')}</Field.Label>
|
||||
<Input value={settings.hero.heading} oninput={(event) => setter(event.currentTarget.value, 'hero.heading')} />
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.editor.hero.subheading')}</Field.Label>
|
||||
<Textarea
|
||||
value={settings.hero.subheading}
|
||||
oninput={(event) => setter(event.currentTarget.value, 'hero.subheading')}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<ButtonLinkFields
|
||||
label={settings.hero.primaryAction.label}
|
||||
href={settings.hero.primaryAction.href}
|
||||
labelFieldLabel={$t('settings.landing_page.editor.hero.primary_label')}
|
||||
hrefFieldLabel={$t('settings.landing_page.editor.hero.primary_href')}
|
||||
labelPlaceholder={$t('settings.landing_page.editor.hero.primary_label')}
|
||||
hrefPlaceholder={$t('settings.landing_page.editor.hero.primary_href')}
|
||||
onLabelInput={(value) => setter(value, 'hero.primaryAction.label')}
|
||||
onHrefInput={(value) => setter(value, 'hero.primaryAction.href')}
|
||||
/>
|
||||
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch
|
||||
checked={Boolean(settings.hero.secondaryAction)}
|
||||
onCheckedChange={(checked) => toggleSecondaryAction(checked === true)}
|
||||
/>
|
||||
<Field.Label>{$t('settings.landing_page.editor.hero.secondary_toggle')}</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
{#if settings.hero.secondaryAction}
|
||||
<ButtonLinkFields
|
||||
label={settings.hero.secondaryAction.label}
|
||||
href={settings.hero.secondaryAction.href}
|
||||
labelFieldLabel={$t('settings.landing_page.editor.hero.secondary_label')}
|
||||
hrefFieldLabel={$t('settings.landing_page.editor.hero.secondary_href')}
|
||||
labelPlaceholder={$t('settings.landing_page.editor.hero.secondary_label')}
|
||||
hrefPlaceholder={$t('settings.landing_page.editor.hero.secondary_href')}
|
||||
onLabelInput={(value) => setter(value, 'hero.secondaryAction.label')}
|
||||
onHrefInput={(value) => setter(value, 'hero.secondaryAction.href')}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.editor.hero.image')}</Field.Label>
|
||||
{#if settings.hero.image}
|
||||
<img src={settings.hero.image} alt="" class="border-border/60 mb-3 w-full rounded-lg border object-cover" />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
activeWidget = 'hero-image';
|
||||
$handleOpenWidget.open = true;
|
||||
}}
|
||||
>
|
||||
{$t('settings.landing_page.editor.hero.upload')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onclick={() => {
|
||||
setter('', 'hero.image');
|
||||
}}
|
||||
>
|
||||
{$t('settings.landing_page.editor.hero.clear')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if $handleOpenWidget.open && activeWidget === 'hero-image'}
|
||||
<UploadWidget
|
||||
imageURL={settings.hero.image}
|
||||
onchange={(imageUrl) => {
|
||||
setter(imageUrl, 'hero.image');
|
||||
$handleOpenWidget.open = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</Field.Field>
|
||||
</div>
|
||||
</Field.Set>
|
||||
</Field.Group>
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import type { OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
import ButtonLinkFields from './button-link-fields.svelte';
|
||||
|
||||
interface Props {
|
||||
settings: OrgLandingPageJson;
|
||||
markDirty: () => void;
|
||||
}
|
||||
|
||||
let { settings = $bindable(), markDirty }: Props = $props();
|
||||
|
||||
function setter(value: unknown, setterKey: string) {
|
||||
if (typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSettings = cloneDeep(settings);
|
||||
set(nextSettings, setterKey, value);
|
||||
settings = nextSettings;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function addNavigationItem() {
|
||||
settings = {
|
||||
...settings,
|
||||
navItems: [
|
||||
...settings.navItems,
|
||||
{
|
||||
label: t.get('settings.landing_page.editor.navigation.default_label'),
|
||||
href: '#'
|
||||
}
|
||||
]
|
||||
};
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function removeNavigationItem(index: number) {
|
||||
settings = {
|
||||
...settings,
|
||||
navItems: settings.navItems.filter((_, itemIndex) => itemIndex !== index)
|
||||
};
|
||||
markDirty();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-7">
|
||||
<Field.Legend>{$t('settings.landing_page.editor.sections.navigation')}</Field.Legend>
|
||||
|
||||
{#each settings.navItems as navItem, index (index)}
|
||||
<ButtonLinkFields
|
||||
label={navItem.label}
|
||||
href={navItem.href}
|
||||
labelFieldLabel={$t('settings.landing_page.editor.navigation.link_label')}
|
||||
hrefFieldLabel={$t('settings.landing_page.editor.navigation.link_href')}
|
||||
className="group ui:border-border relative space-y-3 border-b pb-5"
|
||||
removeAriaLabel={$t('settings.landing_page.editor.navigation.remove')}
|
||||
onLabelInput={(value) => setter(value, `navItems.${index}.label`)}
|
||||
onHrefInput={(value) => setter(value, `navItems.${index}.href`)}
|
||||
onRemove={() => removeNavigationItem(index)}
|
||||
/>
|
||||
{/each}
|
||||
<Button type="button" variant="outline" onclick={addNavigationItem}>
|
||||
{$t('settings.landing_page.editor.navigation.add')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1,761 +1,177 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import * as RadioGroup from '@cio/ui/base/radio-group';
|
||||
|
||||
import { landingPageSettings } from '../utils/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
||||
import { currentOrg, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
||||
import { openUpgradeModal } from '$lib/utils/functions/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { orgApi } from '$features/org/api/org.svelte';
|
||||
import { handleOpenWidget } from '$features/ui/course-landing-page/store';
|
||||
|
||||
import { Checkbox } from '@cio/ui/base/checkbox';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { UploadWidget, UnsavedChanges } from '$features/ui';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import type { OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
import * as RadioGroup from '@cio/ui/base/radio-group';
|
||||
import { UnsavedChanges } from '$features/ui';
|
||||
import { orgApi } from '$features/org/api/org.svelte';
|
||||
import {
|
||||
createDefaultLandingPageSettings,
|
||||
landingPageThemes,
|
||||
normalizeLandingPageSettings
|
||||
} from '$features/org/utils/landing-page';
|
||||
import { landingPageSettings } from '../utils/store';
|
||||
import { PremiumIcon } from '@cio/ui/custom/moving-icons';
|
||||
import type { AccountOrg } from '$features/app/types';
|
||||
|
||||
let creatingNewQuestion = $state(false);
|
||||
let creatingNewCustomLink = $state(false);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
let widgetKey = $state('');
|
||||
const banner = [
|
||||
{ value: 'video', label: `${$t('settings.landing_page.actions.banner_type.video')}` },
|
||||
{ value: 'image', label: `${$t('settings.landing_page.actions.banner_type.image')}` }
|
||||
];
|
||||
let hasInitialized = $state(false);
|
||||
let skipUnsavedPrompt = $state(false);
|
||||
let { hasUnsavedChanges = $bindable(false) }: { hasUnsavedChanges?: boolean } = $props();
|
||||
|
||||
let newQuestion = $state({
|
||||
title: '',
|
||||
content: ''
|
||||
});
|
||||
const themeCards = [
|
||||
{
|
||||
value: 'minimal',
|
||||
preview: 'https://assets.cdn.clsrio.com/www/templates/minimal.png',
|
||||
titleKey: 'settings.landing_page.theme.cards.minimal.title',
|
||||
descriptionKey: 'settings.landing_page.theme.cards.minimal.description'
|
||||
},
|
||||
{
|
||||
value: 'bold',
|
||||
preview: 'https://assets.cdn.clsrio.com/www/templates/bold.png',
|
||||
titleKey: 'settings.landing_page.theme.cards.bold.title',
|
||||
descriptionKey: 'settings.landing_page.theme.cards.bold.description'
|
||||
},
|
||||
{
|
||||
value: 'classic',
|
||||
preview: 'https://assets.cdn.clsrio.com/www/templates/classic.png',
|
||||
titleKey: 'settings.landing_page.theme.cards.classic.title',
|
||||
descriptionKey: 'settings.landing_page.theme.cards.classic.description'
|
||||
}
|
||||
] as const;
|
||||
|
||||
let newCustomLink = $state({
|
||||
label: '',
|
||||
url: '',
|
||||
openInNewTab: false
|
||||
});
|
||||
|
||||
function widgetControl(key: string) {
|
||||
widgetKey = key;
|
||||
$handleOpenWidget.open = true;
|
||||
function isPaidTheme(theme: string): boolean {
|
||||
return theme !== 'minimal';
|
||||
}
|
||||
|
||||
function createNewFaq() {
|
||||
newQuestion = {
|
||||
title: '',
|
||||
content: ''
|
||||
function selectTheme(theme: (typeof landingPageThemes)[number]) {
|
||||
if ($isFreePlan && isPaidTheme(theme)) {
|
||||
openUpgradeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
$landingPageSettings.theme = theme;
|
||||
hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
function isLandingPageTheme(value: string | null): value is (typeof landingPageThemes)[number] {
|
||||
return value === 'minimal' || value === 'bold' || value === 'classic';
|
||||
}
|
||||
|
||||
function getEditContentUrl() {
|
||||
const query = new SvelteURLSearchParams(page.url.searchParams);
|
||||
query.set('theme', $landingPageSettings.theme);
|
||||
return `${$currentOrgPath}/settings/landingpage/edit?${query.toString()}`;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (hasInitialized || !$currentOrg.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedLandingPage = normalizeLandingPageSettings($currentOrg.landingpage);
|
||||
const orgDefaultLandingPageSettings = createDefaultLandingPageSettings();
|
||||
const queryTheme = page.url.searchParams.get('theme');
|
||||
$landingPageSettings = {
|
||||
...orgDefaultLandingPageSettings,
|
||||
...normalizedLandingPage
|
||||
};
|
||||
creatingNewQuestion = true;
|
||||
}
|
||||
|
||||
function saveNewFAQ() {
|
||||
if (newQuestion.title !== '' && newQuestion.content !== '') {
|
||||
$landingPageSettings.faq.questions = [
|
||||
...$landingPageSettings.faq.questions,
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
title: newQuestion.title,
|
||||
content: newQuestion.content
|
||||
}
|
||||
];
|
||||
creatingNewQuestion = false;
|
||||
if (isLandingPageTheme(queryTheme)) {
|
||||
$landingPageSettings.theme = queryTheme;
|
||||
hasUnsavedChanges = queryTheme !== normalizedLandingPage.theme;
|
||||
} else {
|
||||
hasUnsavedChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelNewFAQ() {
|
||||
creatingNewQuestion = false;
|
||||
}
|
||||
|
||||
function deleteFaq(id: number) {
|
||||
let faqs = $landingPageSettings.faq.questions;
|
||||
const filteredFaq = faqs.filter((faq) => faq.id !== id);
|
||||
$landingPageSettings.faq.questions = filteredFaq;
|
||||
}
|
||||
|
||||
function createNewCustomLink() {
|
||||
newCustomLink = {
|
||||
label: '',
|
||||
url: '',
|
||||
openInNewTab: false
|
||||
};
|
||||
creatingNewCustomLink = true;
|
||||
}
|
||||
|
||||
function saveNewCustomLink() {
|
||||
if (newCustomLink.label !== '' && newCustomLink.url !== '') {
|
||||
$landingPageSettings.customLinks.links = [
|
||||
...$landingPageSettings.customLinks.links,
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
label: newCustomLink.label,
|
||||
url: newCustomLink.url,
|
||||
openInNewTab: newCustomLink.openInNewTab
|
||||
}
|
||||
];
|
||||
creatingNewCustomLink = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelNewCustomLink() {
|
||||
creatingNewCustomLink = false;
|
||||
}
|
||||
|
||||
function deleteCustomLink(id: number) {
|
||||
let customLinks = $landingPageSettings.customLinks.links;
|
||||
const filteredCustomLinks = customLinks.filter((link) => link.id !== id);
|
||||
$landingPageSettings.customLinks.links = filteredCustomLinks;
|
||||
}
|
||||
|
||||
const checkPrefix = (inputValue: string) => {
|
||||
if (!inputValue) return;
|
||||
|
||||
if (inputValue.trim() !== '') {
|
||||
if (!inputValue.startsWith('https://')) {
|
||||
inputValue = 'https://' + inputValue;
|
||||
}
|
||||
}
|
||||
return inputValue;
|
||||
};
|
||||
hasInitialized = true;
|
||||
});
|
||||
|
||||
export async function handleSave() {
|
||||
$landingPageSettings.footer.twitter = checkPrefix($landingPageSettings.footer.twitter) || '';
|
||||
$landingPageSettings.footer.linkedin = checkPrefix($landingPageSettings.footer.linkedin) || '';
|
||||
$landingPageSettings.footer.facebook = checkPrefix($landingPageSettings.footer.facebook) || '';
|
||||
const updatedLandingPage = normalizeLandingPageSettings($landingPageSettings as AccountOrg['landingpage']);
|
||||
|
||||
await orgApi.update($currentOrg.id, {
|
||||
landingpage: $landingPageSettings as AccountOrg['landingpage']
|
||||
landingpage: updatedLandingPage as AccountOrg['landingpage']
|
||||
});
|
||||
|
||||
if (orgApi.success) {
|
||||
$currentOrg.landingpage = $landingPageSettings as AccountOrg['landingpage'];
|
||||
$landingPageSettings = updatedLandingPage;
|
||||
$currentOrg.landingpage = updatedLandingPage as AccountOrg['landingpage'];
|
||||
hasUnsavedChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set default from store
|
||||
currentOrg.subscribe((cOrg) => {
|
||||
if (cOrg.landingpage && Object.keys(cOrg.landingpage).length) {
|
||||
const landingpage = { ...(cOrg.landingpage as unknown as OrgLandingPageJson) };
|
||||
|
||||
// Fallbacks for new keys we added into JSON, in case a user already saved the old JSON
|
||||
if (!landingpage?.header?.banner) {
|
||||
landingpage.header.banner = $landingPageSettings.header.banner;
|
||||
}
|
||||
|
||||
if (!landingpage?.header?.background) {
|
||||
landingpage.header.background = $landingPageSettings.header.background;
|
||||
}
|
||||
|
||||
if (!landingpage.customLinks) {
|
||||
landingpage.customLinks = $landingPageSettings.customLinks;
|
||||
}
|
||||
$landingPageSettings = {
|
||||
...landingpage
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
<UnsavedChanges bind:hasUnsavedChanges skipPrompt={skipUnsavedPrompt} />
|
||||
|
||||
<Field.Group class="w-full max-w-md! px-2">
|
||||
<Field.Group class="w-full px-2">
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.heading')}</Field.Legend>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch bind:checked={$landingPageSettings.header.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.header.show
|
||||
? $t('settings.landing_page.show_section')
|
||||
: $t('settings.landing_page.hide_section')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<Field.Legend>{$t('settings.landing_page.theme.heading')}</Field.Legend>
|
||||
<Field.Description>{$t('settings.landing_page.theme.description')}</Field.Description>
|
||||
</div>
|
||||
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.title')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.header.title}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
class="w-full"
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.title_highlight')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
|
||||
class="w-full"
|
||||
bind:value={$landingPageSettings.header.titleHighlight}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.subtitle')}</Field.Label>
|
||||
<Textarea
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
|
||||
class="w-full"
|
||||
bind:value={$landingPageSettings.header.subtitle}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={async () => {
|
||||
skipUnsavedPrompt = true;
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.actions.heading')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.label')}
|
||||
class="w-full"
|
||||
bind:value={$landingPageSettings.header.action.label}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.actions.link')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.link')}
|
||||
bind:value={$landingPageSettings.header.action.link}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch
|
||||
bind:checked={$landingPageSettings.header.action.redirect}
|
||||
onCheckedChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.header.action.redirect
|
||||
? $t('settings.landing_page.actions.redirect')
|
||||
: $t('settings.landing_page.actions.no_redirect')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
try {
|
||||
await goto(resolve(getEditContentUrl(), {}));
|
||||
} finally {
|
||||
skipUnsavedPrompt = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$t('settings.landing_page.content.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.actions.banner_type.heading')}</Field.Label>
|
||||
<RadioGroup.Root
|
||||
bind:value={$landingPageSettings.header.banner.type}
|
||||
onValueChange={() => {
|
||||
if (hasUnsavedChanges) return;
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
>
|
||||
{#each banner as item}
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value={item.value} id={item.value} />
|
||||
<Label for={item.value} class="cursor-pointer">{item.label}</Label>
|
||||
<Field.Field>
|
||||
<RadioGroup.Root
|
||||
bind:value={$landingPageSettings.theme}
|
||||
class="grid gap-4 md:grid-cols-3"
|
||||
onValueChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
>
|
||||
{#each themeCards as themeCard (themeCard.value)}
|
||||
{@const isLocked = $isFreePlan && isPaidTheme(themeCard.value)}
|
||||
<RadioGroup.Item value={themeCard.value} id={themeCard.value} class="sr-only" />
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => selectTheme(themeCard.value)}
|
||||
aria-pressed={$landingPageSettings.theme === themeCard.value}
|
||||
class={`group relative cursor-pointer rounded-xl border transition ${
|
||||
isLocked
|
||||
? 'border-border opacity-75 hover:border-blue-500/60'
|
||||
: $landingPageSettings.theme === themeCard.value
|
||||
? 'border-primary ring-primary/20 ring-2'
|
||||
: 'border-border hover:border-primary/60'
|
||||
}`}
|
||||
>
|
||||
{#if isLocked}
|
||||
<div
|
||||
class="absolute top-2 right-2 z-10 flex items-center gap-1 rounded-full bg-blue-600 px-2 py-0.5 text-xs font-medium text-white shadow-sm"
|
||||
>
|
||||
<PremiumIcon size={14} color="white" />
|
||||
Upgrade
|
||||
</div>
|
||||
{/if}
|
||||
<div class="bg-background mb-4 overflow-hidden rounded-xl">
|
||||
<img src={themeCard.preview} alt={$t(themeCard.titleKey)} class="h-50 w-full object-cover object-top" />
|
||||
</div>
|
||||
{/each}
|
||||
</RadioGroup.Root>
|
||||
</Field.Field>
|
||||
|
||||
{#if $landingPageSettings.header.banner.type === 'video'}
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.actions.link')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.video')}
|
||||
bind:value={$landingPageSettings.header.banner.video}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
{:else}
|
||||
<Field.Field>
|
||||
<Button variant="outline" onclick={() => widgetControl('header.banner.image')}>
|
||||
{$t('settings.landing_page.about.select_image')}
|
||||
</Button>
|
||||
</Field.Field>
|
||||
{/if}
|
||||
{#if $landingPageSettings.header.banner.image && $landingPageSettings.header.banner.type === 'image'}
|
||||
<img alt="bannerImage" src={$landingPageSettings.header.banner.image} class="mt-2 w-full rounded-md" />
|
||||
{/if}
|
||||
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch
|
||||
bind:checked={$landingPageSettings.header.banner.show}
|
||||
onCheckedChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.header.banner.show
|
||||
? $t('settings.landing_page.actions.show_banner')
|
||||
: $t('settings.landing_page.actions.hide_banner')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
{#if $handleOpenWidget.open && widgetKey === 'banner'}
|
||||
<UploadWidget
|
||||
bind:imageURL={$landingPageSettings.header.banner.image}
|
||||
onchange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.background.title')}</Field.Label>
|
||||
<Button variant="outline" onclick={() => widgetControl('background')}>
|
||||
{$t('settings.landing_page.about.select_image')}
|
||||
</Button>
|
||||
</Field.Field>
|
||||
{#if $landingPageSettings.header.background?.image}
|
||||
<img alt="backgroundImage" src={$landingPageSettings.header.background.image} class="mt-2 w-full rounded-md" />
|
||||
{/if}
|
||||
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch
|
||||
checked={$landingPageSettings.header.background?.show}
|
||||
onCheckedChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
if (!$landingPageSettings.header.background) {
|
||||
$landingPageSettings.header.background = {
|
||||
image: '',
|
||||
show: true
|
||||
};
|
||||
} else {
|
||||
$landingPageSettings.header.background.show = !$landingPageSettings.header.background.show;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.header.background?.show
|
||||
? $t('settings.landing_page.background.show_background')
|
||||
: $t('settings.landing_page.background.hide_background')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
{#if $handleOpenWidget.open && widgetKey === 'background'}
|
||||
<UploadWidget
|
||||
imageURL={$landingPageSettings.header.background?.image}
|
||||
onchange={(imgUrl) => {
|
||||
hasUnsavedChanges = true;
|
||||
|
||||
if (!$landingPageSettings.header.background) {
|
||||
$landingPageSettings.header.background = {
|
||||
image: imgUrl,
|
||||
show: true
|
||||
};
|
||||
} else {
|
||||
$landingPageSettings.header.background.image = imgUrl;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
|
||||
<Field.Separator />
|
||||
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.about.heading')}</Field.Legend>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch bind:checked={$landingPageSettings.aboutUs.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.aboutUs.show
|
||||
? $t('settings.landing_page.show_section')
|
||||
: $t('settings.landing_page.hide_section')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.about.title')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.aboutUs.title}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.about.content')}</Field.Label>
|
||||
<Textarea
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.content')}
|
||||
bind:value={$landingPageSettings.aboutUs.content}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label class="font-bold">{$t('settings.landing_page.about.upload_an_image')}</Field.Label>
|
||||
<Button variant="outline" onclick={() => widgetControl('about-us')}>
|
||||
{$t('settings.landing_page.about.select_image')}
|
||||
</Button>
|
||||
{#if $landingPageSettings.aboutUs.imageUrl}
|
||||
<img alt="About us" src={$landingPageSettings.aboutUs.imageUrl} class="mt-2 w-full rounded-md" />
|
||||
{/if}
|
||||
{#if $handleOpenWidget.open && widgetKey === 'about-us'}
|
||||
<UploadWidget bind:imageURL={$landingPageSettings.aboutUs.imageUrl} />
|
||||
{/if}
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
|
||||
<Field.Separator />
|
||||
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.courses.heading')}</Field.Legend>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch bind:checked={$landingPageSettings.courses.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.courses.show
|
||||
? $t('settings.landing_page.courses.show_section')
|
||||
: $t('settings.landing_page.courses.hide_section')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.courses.title')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.courses.title}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.courses.title_highlight')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
|
||||
bind:value={$landingPageSettings.courses.titleHighlight}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.courses.subtitle')}</Field.Label>
|
||||
<Textarea
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
|
||||
bind:value={$landingPageSettings.courses.subtitle}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
|
||||
<Field.Separator />
|
||||
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.faq.heading')}</Field.Legend>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch bind:checked={$landingPageSettings.faq.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.faq.show
|
||||
? $t('settings.landing_page.faq.show_section')
|
||||
: $t('settings.landing_page.faq.hide_section')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.faq.title')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.faq.title}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
{#each $landingPageSettings.faq.questions as item (item.id)}
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.faq.question')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_question_here')}
|
||||
bind:value={item.title}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.faq.answer')}</Field.Label>
|
||||
<Textarea
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_answer_here')}
|
||||
bind:value={item.content}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Button variant="ghost" size="icon" onclick={() => deleteFaq(item.id)}>
|
||||
<TrashIcon size={16} />
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
{#if creatingNewQuestion}
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.faq.question')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_question_here')}
|
||||
bind:value={newQuestion.title}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.faq.answer')}</Field.Label>
|
||||
<Textarea
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_answer_here')}
|
||||
bind:value={newQuestion.content}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Button variant="outline" onclick={saveNewFAQ}>
|
||||
{$t('settings.landing_page.faq.save')}
|
||||
</Button>
|
||||
<Button variant="outline" onclick={cancelNewFAQ}>
|
||||
{$t('settings.landing_page.faq.cancel')}
|
||||
</Button>
|
||||
</Field.Field>
|
||||
{:else}
|
||||
<Field.Field orientation="horizontal">
|
||||
<Button variant="outline" onclick={createNewFaq}>
|
||||
{$t('settings.landing_page.faq.button')}
|
||||
</Button>
|
||||
</Field.Field>
|
||||
{/if}
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
|
||||
<Field.Separator />
|
||||
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.contact_us.heading')}</Field.Legend>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch bind:checked={$landingPageSettings.contact.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.contact.show
|
||||
? $t('settings.landing_page.contact_us.show_section')
|
||||
: $t('settings.landing_page.contact_us.hide_section')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.contact_us.title')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.contact.title}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.contact_us.title_highlight')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
|
||||
bind:value={$landingPageSettings.contact.titleHighlight}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.contact_us.subtitle')}</Field.Label>
|
||||
<Textarea
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
|
||||
bind:value={$landingPageSettings.contact.subtitle}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.contact_us.address')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.address')}
|
||||
bind:value={$landingPageSettings.contact.address}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.contact_us.phone_number')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.number')}
|
||||
bind:value={$landingPageSettings.contact.phone}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.contact_us.email')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.email')}
|
||||
bind:value={$landingPageSettings.contact.email}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
|
||||
<Field.Separator />
|
||||
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.mailing_list.heading')}</Field.Legend>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch bind:checked={$landingPageSettings.mailinglist.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.mailinglist.show
|
||||
? $t('settings.landing_page.mailing_list.show_section')
|
||||
: $t('settings.landing_page.mailing_list.hide_section')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.mailing_list.title')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.mailinglist.title}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.mailing_list.subtitle')}</Field.Label>
|
||||
<Textarea
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
|
||||
bind:value={$landingPageSettings.mailinglist.subtitle}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.mailing_list.button_label')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.label')}
|
||||
bind:value={$landingPageSettings.mailinglist.buttonLabel}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
|
||||
<Field.Separator />
|
||||
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.custom_links.heading')}</Field.Legend>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch bind:checked={$landingPageSettings.customLinks.show} />
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.customLinks.show
|
||||
? $t('settings.landing_page.custom_links.show_links')
|
||||
: $t('settings.landing_page.custom_links.hide_links')}
|
||||
</Field.Label>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Description>{$t('settings.landing_page.custom_links.description')}</Field.Description>
|
||||
|
||||
<Field.Group>
|
||||
{#each $landingPageSettings.customLinks.links as link (link.id)}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.custom_links.label')}</Field.Label>
|
||||
<Input placeholder={$t('settings.landing_page.custom_links.label_placeholder')} bind:value={link.label} />
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.custom_links.url')}</Field.Label>
|
||||
<Input placeholder={$t('settings.landing_page.custom_links.url_placeholder')} bind:value={link.url} />
|
||||
</Field.Field>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Field.Field orientation="horizontal">
|
||||
<Checkbox id="new-tab-{link.id}" bind:checked={link.openInNewTab} />
|
||||
<Field.Label class="text-sm">{$t('settings.landing_page.custom_links.new_tab')}</Field.Label>
|
||||
</Field.Field>
|
||||
<Button variant="ghost" size="icon" onclick={() => deleteCustomLink(link.id)}>
|
||||
<TrashIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Field.Separator />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if creatingNewCustomLink}
|
||||
<div class="mb-4 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<div class="mb-3 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.custom_links.label')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('settings.landing_page.custom_links.label_placeholder')}
|
||||
bind:value={newCustomLink.label}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.custom_links.url')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('settings.landing_page.custom_links.url_placeholder')}
|
||||
bind:value={newCustomLink.url}
|
||||
/>
|
||||
</Field.Field>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Field.Field orientation="horizontal">
|
||||
<input type="checkbox" bind:checked={newCustomLink.openInNewTab} class="mr-2" />
|
||||
<Field.Label class="text-sm">{$t('settings.landing_page.custom_links.new_tab')}</Field.Label>
|
||||
</Field.Field>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" onclick={saveNewCustomLink} class="px-3 py-1 text-sm">
|
||||
{$t('settings.landing_page.custom_links.save')}
|
||||
</Button>
|
||||
<Button variant="outline" onclick={cancelNewCustomLink} class="px-3 py-1 text-sm">
|
||||
{$t('settings.landing_page.custom_links.cancel')}
|
||||
</Button>
|
||||
<div class="space-y-1 px-2 pb-2">
|
||||
<p class="text-base font-semibold">{$t(themeCard.titleKey)}</p>
|
||||
<p class="ui:text-muted-foreground h-[40px] text-sm">{$t(themeCard.descriptionKey)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Field.Field orientation="horizontal">
|
||||
<Button variant="outline" onclick={createNewCustomLink}>
|
||||
{$t('settings.landing_page.custom_links.add')}
|
||||
</Button>
|
||||
</Field.Field>
|
||||
{/if}
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
|
||||
<Field.Separator />
|
||||
|
||||
<Field.Set>
|
||||
<Field.Legend>{$t('settings.landing_page.footer.heading')}</Field.Legend>
|
||||
<Field.Field orientation="horizontal">
|
||||
<Switch bind:checked={$landingPageSettings.footer.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Field.Label class="text-gray-600">
|
||||
{$landingPageSettings.footer.show
|
||||
? $t('settings.landing_page.footer.show_section')
|
||||
: $t('settings.landing_page.footer.hide_section')}
|
||||
</Field.Label>
|
||||
</button>
|
||||
{/each}
|
||||
</RadioGroup.Root>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Group>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.footer.facebook')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.facebook')}
|
||||
bind:value={$landingPageSettings.footer.facebook}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<!-- <InputField
|
||||
label="Instagram"
|
||||
placeholder="Write your Instagram link here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.footer.instagram}
|
||||
/> -->
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.footer.twitter')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.twitter')}
|
||||
bind:value={$landingPageSettings.footer.twitter}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('settings.landing_page.footer.linkedin')}</Field.Label>
|
||||
<Input
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.linkedin')}
|
||||
bind:value={$landingPageSettings.footer.linkedin}
|
||||
oninput={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</Field.Set>
|
||||
</Field.Group>
|
||||
|
||||
<style>
|
||||
:global(.dark) {
|
||||
--cp-text-color: #fff;
|
||||
--cp-border-color: white;
|
||||
--cp-text-color: white;
|
||||
--cp-input-color: #555;
|
||||
--cp-button-hover-color: #777;
|
||||
}
|
||||
|
||||
:global(.dark .alpha) {
|
||||
background: #333 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,116 +1,7 @@
|
||||
import type { OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const landingPageSettings: Writable<OrgLandingPageJson> = writable({
|
||||
header: {
|
||||
title: 'Web Development Courses for',
|
||||
titleHighlight: 'Everyone',
|
||||
subtitle: 'Practical project-based courses that are easy to understand and straight to the point',
|
||||
action: {
|
||||
label: 'Start learning Today',
|
||||
link: '#contact',
|
||||
redirect: false
|
||||
},
|
||||
banner: {
|
||||
video: '',
|
||||
image: '/images/learn-on-cio.jpg',
|
||||
type: 'image',
|
||||
show: true
|
||||
},
|
||||
background: {
|
||||
image: '',
|
||||
show: true
|
||||
},
|
||||
show: true
|
||||
},
|
||||
aboutUs: {
|
||||
title: 'Our Story',
|
||||
content:
|
||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.',
|
||||
imageUrl: '/images/riverside.jpeg',
|
||||
show: true
|
||||
},
|
||||
courses: {
|
||||
title: 'Explore our',
|
||||
titleHighlight: 'Courses',
|
||||
subtitle: 'Find courses you will love from best teachers all over the world🌎.',
|
||||
show: true
|
||||
},
|
||||
faq: {
|
||||
title: 'Frequently Asked Questions.',
|
||||
questions: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Lorem ipsum dolor sit, amet con',
|
||||
content:
|
||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Lorem ipsum dolor sit, amet con',
|
||||
content:
|
||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Lorem ipsum dolor sit, amet con',
|
||||
content:
|
||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.'
|
||||
}
|
||||
],
|
||||
show: true
|
||||
},
|
||||
contact: {
|
||||
title: 'Have you got any',
|
||||
titleHighlight: 'Question?',
|
||||
subtitle: 'Reach out to us with any question you might have.',
|
||||
address: 'Ashwoodfield House Farm, Kidderminster Rd, Stourbridge, Kingswinford DY6 0AA',
|
||||
phone: '+4407339904995',
|
||||
email: '[email protected]',
|
||||
show: true
|
||||
},
|
||||
mailinglist: {
|
||||
title: 'Join our mailing list',
|
||||
subtitle:
|
||||
'We are constantly releasing new courses and sharing them with our email list. Subscribe to get notified once we release a new course',
|
||||
buttonLabel: 'Subscribe',
|
||||
show: true
|
||||
},
|
||||
customLinks: {
|
||||
show: true,
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
label: 'About Us',
|
||||
url: '#aboutus',
|
||||
openInNewTab: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Courses',
|
||||
url: '/courses',
|
||||
openInNewTab: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'FAQ',
|
||||
url: '#faq',
|
||||
openInNewTab: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'Contact Us',
|
||||
url: '#contact',
|
||||
openInNewTab: false
|
||||
}
|
||||
]
|
||||
},
|
||||
footer: {
|
||||
facebook: '#',
|
||||
instagram: '#',
|
||||
twitter: '#',
|
||||
linkedin: '#',
|
||||
show: true
|
||||
}
|
||||
});
|
||||
import type { OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
import { createDefaultLandingPageSettings } from '$features/org/utils/landing-page';
|
||||
|
||||
export const landingPageSettings: Writable<OrgLandingPageJson> = writable(createDefaultLandingPageSettings());
|
||||
|
||||
Reference in New Issue
Block a user