feat: implement automation feature
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||
import { calcCourseProgress, calcProgressRate } from '$features/course/utils/functions';
|
||||
import CardDropdown from './card-dropdown.svelte';
|
||||
import CoursePublishBadge from './course-publish-badge.svelte';
|
||||
import type { OrgCourses, UserEnrolledCourses } from '$features/course/types';
|
||||
import type { OrgPublicCourses } from '$features/org/utils/types';
|
||||
|
||||
@@ -196,13 +197,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Badge variant={isPublished ? 'default' : 'outline'}>
|
||||
{#if isPublished}
|
||||
{$t('courses.course_card.published')}
|
||||
{:else}
|
||||
{$t('courses.course_card.unpublished')}
|
||||
{/if}
|
||||
</Badge>
|
||||
<CoursePublishBadge {isPublished} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { COURSE_SORT_OPTIONS } from '../utils/constants';
|
||||
import * as Popover from '@cio/ui/base/popover';
|
||||
import * as ToggleGroup from '@cio/ui/base/toggle-group';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import { Checkbox } from '@cio/ui/base/checkbox';
|
||||
@@ -27,6 +28,7 @@
|
||||
|
||||
interface Props {
|
||||
selectedId?: string;
|
||||
selectedOrder?: 'asc' | 'desc';
|
||||
selectedTags?: string[];
|
||||
tagGroups?: FilterTagGroup[];
|
||||
sortOptions?: readonly SortOption[];
|
||||
@@ -37,6 +39,7 @@
|
||||
|
||||
let {
|
||||
selectedId = $bindable('0'),
|
||||
selectedOrder = $bindable('desc' as 'asc' | 'desc'),
|
||||
selectedTags = [],
|
||||
tagGroups = [],
|
||||
sortOptions = COURSE_SORT_OPTIONS,
|
||||
@@ -47,8 +50,7 @@
|
||||
|
||||
let isOpen = $state(false);
|
||||
|
||||
const hasActiveFilters = $derived(selectedId !== '0' || selectedTags.length > 0);
|
||||
const hasActiveTagFilters = $derived(selectedTags.length > 0);
|
||||
const hasActiveFilters = $derived(selectedId !== '0' || selectedOrder !== 'desc' || selectedTags.length > 0);
|
||||
|
||||
function isTagSelected(tagSlug: string) {
|
||||
return selectedTags.includes(tagSlug);
|
||||
@@ -66,7 +68,7 @@
|
||||
<FilterIcon size={16} />
|
||||
{/if}
|
||||
</IconButton>
|
||||
{#if hasActiveTagFilters}
|
||||
{#if hasActiveFilters}
|
||||
<span
|
||||
class="ui:bg-primary absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border border-white"
|
||||
aria-hidden="true"
|
||||
@@ -103,6 +105,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="ui:text-muted-foreground text-xs font-semibold uppercase">
|
||||
{$t('courses.tag_filters.order')}
|
||||
</p>
|
||||
<ToggleGroup.Root
|
||||
type="single"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
value={selectedOrder}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'asc' || value === 'desc') {
|
||||
selectedOrder = value;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroup.Item value="asc">{$t('courses.tag_filters.ascending')}</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="desc">{$t('courses.tag_filters.descending')}</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<p class="ui:text-muted-foreground text-xs font-semibold uppercase">{$t('courses.tag_filters.tags')}</p>
|
||||
{#if tagGroups.length === 0}
|
||||
|
||||
@@ -3,21 +3,35 @@
|
||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||
import BellIcon from '@lucide/svelte/icons/bell';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import * as Popover from '@cio/ui/base/popover';
|
||||
import Search from '$features/ui/search.svelte';
|
||||
import { HoverableItem, PreviewIcon } from '@cio/ui/custom/moving-icons';
|
||||
import RefreshCcwIcon from '@lucide/svelte/icons/refresh-ccw';
|
||||
import * as Empty from '@cio/ui/base/empty';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { isStudentExperience } from '$lib/utils/store/app';
|
||||
import { setupProgressApi } from '$features/setup/api/setup-progress.svelte';
|
||||
import { courseApi } from '$features/course/api';
|
||||
import { openCoursePreview } from '$features/course/utils/course-preview';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import CoursePublishBadge from './course-publish-badge.svelte';
|
||||
|
||||
const siteName = $derived($currentOrg.siteName);
|
||||
const showCoursePublishBadge = $derived(!$isStudentExperience);
|
||||
|
||||
$effect(() => {
|
||||
if (!siteName) return;
|
||||
|
||||
setupProgressApi.fetchSetupProgress(siteName);
|
||||
});
|
||||
|
||||
function handlePreview() {
|
||||
openCoursePreview({
|
||||
courseId: courseApi.course?.id ?? '',
|
||||
courseSlug: courseApi.course?.slug,
|
||||
currentOrgDomain: $currentOrgDomain
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<header
|
||||
@@ -30,14 +44,18 @@
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
|
||||
<p class="max-w-2xs truncate text-sm font-medium">
|
||||
{courseApi.course?.title || ''}
|
||||
</p>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<p class="max-w-2xs truncate text-sm font-medium">
|
||||
{courseApi.course?.title || ''}
|
||||
</p>
|
||||
|
||||
{#if showCoursePublishBadge}
|
||||
<CoursePublishBadge isPublished={courseApi.course?.isPublished} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<span class="grow"></span>
|
||||
|
||||
<Search />
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
@@ -56,11 +74,24 @@
|
||||
<Empty.Content>
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCcwIcon />
|
||||
Refresh
|
||||
{$t('common.refresh')}
|
||||
</Button>
|
||||
</Empty.Content>
|
||||
</Empty.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
<HoverableItem>
|
||||
{#snippet children(isHovered)}
|
||||
<IconButton
|
||||
onclick={handlePreview}
|
||||
disabled={!courseApi.course?.id}
|
||||
tooltip={$t('course.header.preview')}
|
||||
aria-label={$t('course.header.preview')}
|
||||
>
|
||||
<PreviewIcon {isHovered} size={16} />
|
||||
</IconButton>
|
||||
{/snippet}
|
||||
</HoverableItem>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
isPublished?: boolean;
|
||||
}
|
||||
|
||||
let { isPublished = false }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Badge variant={isPublished ? 'default' : 'outline'}>
|
||||
{#if isPublished}
|
||||
{$t('courses.course_card.published')}
|
||||
{:else}
|
||||
{$t('courses.course_card.unpublished')}
|
||||
{/if}
|
||||
</Badge>
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { questionnaire } from './store';
|
||||
import { sanitizeHtml } from '@cio/ui/tools/sanitize';
|
||||
import { SafeHtmlContent } from '@cio/ui/custom/safe-html-content';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
@@ -49,9 +49,9 @@
|
||||
</div>
|
||||
|
||||
<article class="preview prose prose-sm sm:prose mt-3 p-2">
|
||||
{@html sanitizeHtml(
|
||||
$questionnaire.description || $t('course.navItem.lessons.exercises.all_exercises.description.no')
|
||||
)}
|
||||
<SafeHtmlContent
|
||||
content={$questionnaire.description || $t('course.navItem.lessons.exercises.all_exercises.description.no')}
|
||||
/>
|
||||
</article>
|
||||
{:else}
|
||||
<InputField
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import { toApiPayload, type AnswerData } from '@cio/question-types';
|
||||
import { exerciseApi, presignApi } from '$features/course/api';
|
||||
import type { SubmissionListItem } from '$features/course/utils/types';
|
||||
import { sanitizeHtml } from '@cio/ui/tools/sanitize';
|
||||
import { SafeHtmlContent } from '@cio/ui/custom/safe-html-content';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { ContentType } from '@cio/utils/constants/content';
|
||||
@@ -422,9 +422,10 @@
|
||||
</div>
|
||||
|
||||
<article class="preview prose prose-sm sm:prose mt-3 p-2">
|
||||
{@html sanitizeHtml(
|
||||
$questionnaire.description || $t('course.navItem.lessons.exercises.all_exercises.view_mode.no_description')
|
||||
)}
|
||||
<SafeHtmlContent
|
||||
content={$questionnaire.description ||
|
||||
$t('course.navItem.lessons.exercises.all_exercises.view_mode.no_description')}
|
||||
/>
|
||||
</article>
|
||||
|
||||
<Button onclick={handleStart} type="button" class="float-right my-5">
|
||||
|
||||
+15
-6
@@ -8,7 +8,7 @@
|
||||
import { lessonApi } from '$features/course/api';
|
||||
import { diffLines } from 'diff';
|
||||
import { courseApi } from '$features/course/api';
|
||||
import { sanitizeHtml } from '@cio/ui/tools/sanitize';
|
||||
import { SafeHtmlContent } from '@cio/ui/custom/safe-html-content';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
@@ -83,7 +83,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
function fetchLessonHistory(lessonId: string, locale: string, endRange: number) {
|
||||
function fetchLessonHistory(lessonId: string, locale: TLocale, endRange: number) {
|
||||
untrack(async () => {
|
||||
try {
|
||||
isMoreHistoryLoading = true;
|
||||
@@ -100,9 +100,18 @@
|
||||
|
||||
const data = response.data;
|
||||
|
||||
// Filter out duplicates based on timestamp
|
||||
const toLessonHistory = (item: (typeof data)[number]): LessonHistory => ({
|
||||
old_content: item.oldContent ?? '',
|
||||
new_content: item.newContent ?? '',
|
||||
timestamp: item.timestamp ? new Date(item.timestamp) : new Date(),
|
||||
locale: (item.locale as TLocale) ?? 'en',
|
||||
lesson_id: item.lessonId ?? ''
|
||||
});
|
||||
|
||||
const existingTimestamps = new Set(lessonHistory.map((item) => new Date(item.timestamp).getMinutes()));
|
||||
const newEntries = data.filter((item) => !existingTimestamps.has(new Date(item.timestamp!).getMinutes()));
|
||||
const newEntries = data
|
||||
.filter((item) => !existingTimestamps.has(new Date(item.timestamp ?? 0).getMinutes()))
|
||||
.map(toLessonHistory);
|
||||
lessonHistory = removeDuplicate([...lessonHistory, ...newEntries]);
|
||||
|
||||
if (lessonHistory.length > 0) {
|
||||
@@ -206,7 +215,7 @@
|
||||
{#key lessonId}
|
||||
<HTMLRender id="display" className="m-auto">
|
||||
<div class="amen">
|
||||
{@html sanitizeHtml(content)}
|
||||
<SafeHtmlContent {content} />
|
||||
</div>
|
||||
</HTMLRender>
|
||||
{/key}
|
||||
@@ -222,7 +231,7 @@
|
||||
</p>
|
||||
|
||||
<div>
|
||||
{#each lessonHistory as version, index}
|
||||
{#each lessonHistory as version, index (index)}
|
||||
<button
|
||||
onclick={() => updateContentVersion(version, index)}
|
||||
class="flex w-full cursor-pointer items-start p-4 px-10 hover:bg-gray-200 dark:hover:bg-neutral-700 {index ==
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { HTMLRender, TextEditor } from '$features/ui';
|
||||
import { sanitizeHtml } from '@cio/ui/tools/sanitize';
|
||||
import { SafeHtmlContent } from '@cio/ui/custom/safe-html-content';
|
||||
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
|
||||
import { lessonApi } from '$features/course/api';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
@@ -43,10 +43,6 @@
|
||||
}
|
||||
|
||||
const content = $derived(lessonApi.translations[lessonId]?.[lessonApi.currentLocale] || '');
|
||||
|
||||
$effect(() => {
|
||||
console.log('content', content);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if mode === MODES.edit}
|
||||
@@ -67,7 +63,7 @@
|
||||
<!-- View Mode -->
|
||||
{#if !isHtmlValueEmpty(content)}
|
||||
<HTMLRender>
|
||||
{@html sanitizeHtml(content)}
|
||||
<SafeHtmlContent {content} />
|
||||
</HTMLRender>
|
||||
{:else if hasAtLeastOneTranslation}
|
||||
<p class="text-md py-2 font-normal italic dark:text-white">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import * as Table from '@cio/ui/base/table';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
|
||||
import { isMobileStore } from '@cio/ui/hooks/is-mobile.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import CoursePublishBadge from './course-publish-badge.svelte';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
@@ -88,13 +88,7 @@
|
||||
<Table.Cell>{totalLessons}</Table.Cell>
|
||||
<Table.Cell>{totalStudents}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant={isPublished ? 'default' : 'outline'}>
|
||||
{#if isPublished}
|
||||
{$t('courses.course_card.published')}
|
||||
{:else}
|
||||
{$t('courses.course_card.unpublished')}
|
||||
{/if}
|
||||
</Badge>
|
||||
<CoursePublishBadge {isPublished} />
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
<Table.Cell class="text-center">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
|
||||
import { sanitizeHtml } from '@cio/ui/tools/sanitize';
|
||||
import { SafeHtmlContent } from '@cio/ui/custom/safe-html-content';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import type { Feed } from '$features/course/utils/types';
|
||||
@@ -59,7 +59,7 @@
|
||||
{#if !isHtmlValueEmpty(feed.content || '')}
|
||||
<HTMLRender className="w-[80%]">
|
||||
<div>
|
||||
{@html sanitizeHtml(feed.content || '')}
|
||||
<SafeHtmlContent content={feed.content || ''} />
|
||||
</div>
|
||||
</HTMLRender>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user