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:
+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>
|
||||
Reference in New Issue
Block a user