Enhance audience management features with new components and API integrations

- Added `AssignCoursesModal`, `AudienceMemberRow`, `AudienceTable`, and related components for improved audience management UI.
- Implemented audience invite actions including resend and revoke functionality.
- Updated audience API to support importing members and assigning courses.
- Refactored audience data handling to include status and invite management.
- Improved overall user experience with pagination and selection features in the audience table.
This commit is contained in:
Rotimi Best
2026-03-22 18:42:23 +01:00
parent e9c4082f89
commit 3919ff6c2f
157 changed files with 13183 additions and 872 deletions
@@ -1,12 +1,9 @@
<script lang="ts">
import { getExerciseQuestionLabel, type ExerciseQuestionRendererProps } from '@cio/question-types';
import OptionImage from '../option-image.svelte';
import type { ExerciseQuestionRendererProps } from '@cio/question-types';
import { getOptionImageUrl, hasOptionImages } from '../option-image-utils';
import { cn } from '../../../../tools';
import McqPreviewOptionRow from '../shared/mcq-preview-option-row.svelte';
let { question, labels }: ExerciseQuestionRendererProps = $props();
const label = (key: Parameters<typeof getExerciseQuestionLabel>[1], fallback = '') =>
getExerciseQuestionLabel(labels, key, fallback);
const optionsHaveImages = $derived(hasOptionImages(question.options));
</script>
@@ -14,22 +11,7 @@
<div class="ui:space-y-2">
<ul class={optionsHaveImages ? 'ui:grid ui:grid-cols-2 ui:gap-3' : 'ui:space-y-1'}>
{#each question.options ?? [] as option, index}
{@const optionImageUrl = getOptionImageUrl(option)}
<li class={cn(optionsHaveImages && 'ui:space-y-2 ui:rounded-md ui:p-2', 'ui:text-sm')}>
<OptionImage
src={optionImageUrl}
alt={label('question.edit.image_alt')}
variant="preview"
hasAnyImageInOptions={optionsHaveImages}
/>
<div>
{option.label || option.value || [label('common.option_prefix'), String(index + 1)].filter(Boolean).join(' ')}
{#if option.isCorrect}
<span class="ui:ml-2 ui:text-xs ui:text-green-600">{label('common.correct_badge')}</span>
{/if}
</div>
</li>
<McqPreviewOptionRow {option} {index} optionImageUrl={getOptionImageUrl(option)} {optionsHaveImages} {labels} />
{/each}
</ul>
</div>
@@ -0,0 +1,59 @@
<script lang="ts">
import { getExerciseQuestionLabel, type ExerciseQuestionRendererProps } from '@cio/question-types';
import { Checkbox } from '../../../../base/checkbox';
import { Label } from '../../../../base/label';
import OptionImage from '../option-image.svelte';
import { getOptionImageUrl, hasOptionImages } from '../option-image-utils';
import { getMcqReviewRowBorderClass, getOptionNumericId } from '../option-review-utils';
import OptionCorrectnessBadges from '../shared/option-correctness-badges.svelte';
import { cn } from '../../../../tools';
let { question, answer = null, labels }: ExerciseQuestionRendererProps = $props();
const label = (key: Parameters<typeof getExerciseQuestionLabel>[1], fallback = '') =>
getExerciseQuestionLabel(labels, key, fallback);
const selectedIds = $derived.by(() => {
if (answer?.type !== 'CHECKBOX') return [] as number[];
return answer.optionIds;
});
const optionsHaveImages = $derived(hasOptionImages(question.options));
</script>
<div class="ui:space-y-2">
<div class={optionsHaveImages ? 'ui:grid ui:grid-cols-2 ui:gap-3' : 'ui:space-y-2'}>
{#each question.options ?? [] as option, index}
{@const optionId = `checkbox-review-${question.id ?? 'q'}-${index}`}
{@const optionImageUrl = getOptionImageUrl(option)}
{@const oid = getOptionNumericId(option)}
{@const isSelected = oid !== null && selectedIds.includes(oid)}
{@const isCorrect = option.isCorrect === true}
<div
class={cn(
'ui:rounded-md ui:border ui:p-2',
optionsHaveImages ? 'ui:space-y-2' : 'ui:flex ui:items-center ui:gap-2',
getMcqReviewRowBorderClass(isSelected, isCorrect)
)}
>
<OptionImage
src={optionImageUrl}
alt={label('question.edit.image_alt')}
variant="take"
hasAnyImageInOptions={optionsHaveImages}
/>
<div class="ui:flex ui:items-center ui:gap-2">
<Checkbox id={optionId} checked={isSelected} disabled={true} />
<Label class="ui:cursor-default ui:font-normal ui:w-full" for={optionId}>
{option.label ||
option.value ||
[label('common.option_prefix'), String(index + 1)].filter(Boolean).join(' ')}
<OptionCorrectnessBadges {labels} showCorrect={isCorrect} showIncorrect={isSelected && !isCorrect} />
</Label>
</div>
</div>
{/each}
</div>
</div>