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,9 +1,9 @@
<script lang="ts">
import * as Avatar from '@cio/ui/base/avatar';
import TrashIcon from '@lucide/svelte/icons/trash';
import UserIcon from '@lucide/svelte/icons/user';
import { calDateDiff } from '$lib/utils/functions/date';
import { IconButton } from '@cio/ui/custom/icon-button';
import { shortenName } from '$lib/utils/functions/string';
import { Vote } from '$features/ui';
import { CircleCheckIcon } from '$features/ui/icons';
import { SafeHtmlContent } from '@cio/ui/custom/safe-html-content';
@@ -40,11 +40,15 @@
<header class="flex items-center justify-between p-2 leading-none">
<div class="flex items-center text-black">
<Avatar.Root class="size-7">
<Avatar.Image
src={comment.author?.avatarUrl ? comment.author.avatarUrl : '/logo-192.png'}
alt={comment.author?.fullname ? comment.author.fullname : 'User'}
/>
<Avatar.Fallback>{shortenName(comment.author?.fullname) || 'U'}</Avatar.Fallback>
{#if comment.author?.avatarUrl}
<Avatar.Image
src={comment.author.avatarUrl}
alt={comment.author?.fullname ? comment.author.fullname : 'User'}
/>
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-3.5 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<p class="ml-2 text-sm dark:text-white">{comment.author?.fullname}</p>
@@ -5,6 +5,7 @@
import * as Avatar from '@cio/ui/base/avatar';
import { Skeleton } from '@cio/ui/base/skeleton';
import TrashIcon from '@lucide/svelte/icons/trash';
import UserIcon from '@lucide/svelte/icons/user';
import { t } from '$lib/utils/functions/translations';
import { calDateDiff } from '$lib/utils/functions/date';
@@ -17,19 +18,20 @@
import { Vote } from '$features/ui';
import { IconButton } from '@cio/ui/custom/icon-button';
import { InputField } from '@cio/ui/custom/input-field';
import { shortenName } from '$lib/utils/functions/string';
import { TextEditor } from '$features/ui';
import { CommunityDeleteModal, CommunityCommentItem } from '$features/community/components';
import { BackButton } from '@cio/ui';
import type { CommunityQuestionSuccess } from '../utils/types';
import { currentCommunityQuestion } from '../utils/store';
import { SafeHtmlContent } from '@cio/ui/custom/safe-html-content';
interface Props {
slug: string;
backHref?: string;
question?: CommunityQuestionSuccess['data'] | null;
}
let { question }: Props = $props();
let { question, backHref }: Props = $props();
let commentEditor: TiptapEditor | undefined = $state();
let isValidAnswer = false; // V2 allow admin mark an answer as accepted
@@ -100,6 +102,9 @@
<Page.Root class="mx-auto w-[90%] md:mx-10 md:max-w-3xl lg:mb-20">
<Page.Header>
<Page.HeaderContent>
{#if backHref}
<BackButton href={backHref} label={$t('community.ask.go_back')} />
{/if}
{#if communityApi.isEditMode}
<div class="flex w-full items-center gap-2">
<InputField
@@ -175,13 +180,15 @@
<div class="flex items-center justify-between p-2 leading-none">
<div class="flex items-center text-black no-underline hover:underline">
<Avatar.Root class="h-7 w-7">
<Avatar.Image
src={communityApi.question?.authorAvatarUrl
? communityApi.question?.authorAvatarUrl
: '/logo-192.png'}
alt={communityApi.question?.authorFullname ? communityApi.question?.authorFullname : 'User'}
/>
<Avatar.Fallback>{shortenName(communityApi.question?.authorFullname) || 'U'}</Avatar.Fallback>
{#if communityApi.question?.authorAvatarUrl}
<Avatar.Image
src={communityApi.question.authorAvatarUrl}
alt={communityApi.question?.authorFullname ? communityApi.question.authorFullname : 'User'}
/>
{/if}
<Avatar.Fallback>
<UserIcon class="ui:size-3.5 ui:text-muted-foreground" />
</Avatar.Fallback>
</Avatar.Root>
<p class="ml-2 text-sm dark:text-white">{communityApi.question?.authorFullname}</p>
<p class="ml-2 text-sm text-gray-500 dark:text-white">
@@ -4,6 +4,8 @@
import { Empty } from '@cio/ui/custom/empty';
import * as Item from '@cio/ui/base/item';
import { Search } from '@cio/ui/custom/search';
import { Button } from '@cio/ui/base/button';
import { resolve } from '$app/paths';
import MessageSquareMoreIcon from '@lucide/svelte/icons/message-square-more';
import MessageCirclePlusIcon from '@lucide/svelte/icons/message-circle-plus';
@@ -62,10 +64,8 @@
</Select.Trigger>
<Select.Content>
<Select.Item value="">{$t('community.all')}</Select.Item>
{#each communityApi.courses as course}
{#if course.id}
<Select.Item value={course.id}>{course.title}</Select.Item>
{/if}
{#each communityApi.courses as course (course.id)}
<Select.Item value={course.id}>{course.title}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
@@ -77,10 +77,10 @@
<CommunityListLoader />
<CommunityListLoader />
{:else}
{#each filteredQuestions as question}
{#each filteredQuestions as question (question.slug)}
<Item.Root variant="outline">
{#snippet child({ props })}
<a href="{$basePath}/community/{question.slug}" {...props}>
<a href={resolve(`${$basePath}/community/[slug]`, { slug: question.slug })} {...props}>
<Vote value={question.votes} />
<Item.Content class="gap-y-0.5">
<Item.Title class="mt-0">
@@ -89,11 +89,15 @@
<Item.Description>
{question?.authorFullname} asked {calDateDiff(question.createdAt || new Date())}
</Item.Description>
<a class="m-0" href="/courses/{question.courseId}" onclick={(e) => e.stopPropagation()}>
<Button
class="m-0! justify-start! px-0! py-0! underline"
href={resolve(`/courses/[id]`, { id: question.courseId })}
variant="ghost"
>
<span class="text-muted-foreground p-0 text-xs">
#{question?.courseTitle}
</span>
</a>
</Button>
</Item.Content>
<Item.Actions>
<div class="flex items-center">