feat: migrate course UI from supabase (#615)
* chore: fix storybook build * chore: implement theming * community list not showing properly * refactor: migrate Chip component to @cio/ui/custom/chip - Updated imports for the Chip component across multiple files to use the new location in the @cio/ui package. - Replaced the previous Chip implementation with a new version that utilizes a Badge for better styling and functionality. - Adjusted related components to ensure consistent usage of the updated Chip component. * fix: align course sidebar design to match organization design (#611) * fix: align course sidebar design to match organization design * feat: add hideSearch prop to AppHeader and update sidebar layout --------- Co-authored-by: Rotimi Best <[email protected]> * refactor: replace PrimaryButton with Button component across multiple files - Updated various components to use the new Button component from @cio/ui/base/button instead of the deprecated PrimaryButton. - Ensured consistent button usage throughout the application for improved styling and functionality. - Removed the PrimaryButton component and its associated constants from the codebase. * refactor: standardize button and input usage across components - Replaced instances of deprecated components such as TextField, TextArea, and IconButton with their updated counterparts (InputField, TextareaField, and updated IconButton). - Ensured consistent event handling by changing `onClick` to `onclick` for better alignment with Svelte's event binding conventions. - Removed unused components and streamlined imports to enhance code maintainability and readability. * chore: themeing, course card, use $features * refactor: centralize UI components and update modal implementations - Refactored various components to centralize UI elements, improving discoverability and maintainability. - Replaced deprecated modal components with the new Dialog component for consistent modal behavior across the application. - Removed unused components and streamlined imports to enhance code clarity and reduce complexity. * feat: add new documentation and refactor org and lms UI components - Introduced new documentation files for Auth Flow, Breadcrumbs Refactoring, Page and Route Creation, and Route Page Setup to enhance developer onboarding and reference. - Refactored various UI components, including the Course Sidebar and Lesson Modal, to improve layout and functionality. - Removed deprecated components and streamlined imports for better maintainability. - Updated translations across multiple languages to ensure consistency in user experience. * refactor: update UI components and improve styling consistency - Refactored various components to enhance styling consistency, including updates to button and text color classes. - Removed deprecated components and streamlined imports for better maintainability. - Replaced instances of PageBody with a more flexible Page component structure across multiple routes. - Updated translations and UI elements to align with the new design system. - Deleted unused components to reduce code complexity and improve clarity. * update output.css --------- Co-authored-by: Abdulmuizz Hamzat <[email protected]>
This commit is contained in:
co-authored by
Abdulmuizz Hamzat
parent
e6a3cf9332
commit
9fb6c7cbaa
@@ -5,29 +5,29 @@
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$lib/features/course/utils/store';
|
||||
import { courses } from '$features/course/utils/store';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { currentOrg, isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { askCommunityValidation, commentInCommunityValidation } from '$lib/utils/functions/validator';
|
||||
|
||||
import Vote from '$lib/components/Vote/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { Vote } from '$features/ui';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { shortenName } from '$lib/utils/functions/string';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import { CommunityDeleteModal } from '$lib/features/community/components';
|
||||
import { TextEditor } from '$features/ui';
|
||||
import { CircleCheckIcon } from '$features/ui/icons';
|
||||
import { CommunityDeleteModal } from '$features/community/components';
|
||||
import { currentCommunityQuestion } from '$features/community/utils/store';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
interface Comment {
|
||||
id: string;
|
||||
@@ -164,6 +164,8 @@
|
||||
untrack(() => {
|
||||
question = mapResToQuestion(data);
|
||||
question.totalComments = question.comments.length;
|
||||
// Update store for breadcrumb
|
||||
currentCommunityQuestion.set({ title: question.title });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -365,6 +367,13 @@
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear store when component unmounts
|
||||
$effect(() => {
|
||||
return () => {
|
||||
currentCommunityQuestion.set({ title: null });
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -382,168 +391,163 @@
|
||||
isQuestion={true}
|
||||
/>
|
||||
|
||||
<section class="mx-auto max-w-3xl md:mx-10 lg:mb-20">
|
||||
{#if !question}
|
||||
<div class="mb-3 px-5 py-10">
|
||||
<Skeleton class="h-4 w-[25%]" />
|
||||
<Skeleton class="mb-2 h-4 w-full" />
|
||||
<Skeleton class="h-80 w-full" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="px-5 py-10">
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href="/lms/community">
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="my-5 flex items-center justify-between">
|
||||
{#if !question}
|
||||
<div class="mb-3 px-5 py-10">
|
||||
<Skeleton class="h-4 w-[25%]" />
|
||||
<Skeleton class="mb-2 h-4 w-full" />
|
||||
<Skeleton class="h-80 w-full" />
|
||||
</div>
|
||||
{:else}
|
||||
{@const currentQuestion = question}
|
||||
<Page.Root class="mx-auto max-w-3xl md:mx-10 lg:mb-20">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
{#if isEditMode}
|
||||
<TextField bind:value={editContent.title} className="w-full mr-2" errorMessage={errors.title} />
|
||||
<Select.Root type="single" bind:value={editContent.courseId}>
|
||||
<Select.Trigger class="h-full w-[25%]">
|
||||
<p>
|
||||
{editContent.courseId
|
||||
? fetchedCourses.find((course) => course.id === editContent.courseId)?.title
|
||||
: 'Select Course'}
|
||||
</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each fetchedCourses as course}
|
||||
{#if course.id}
|
||||
<Select.Item value={course.id}>{course.title}</Select.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{:else}
|
||||
<div class="flex items-center">
|
||||
<Vote value={question.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
|
||||
<h2 class="text-3xl">{question.title}</h2>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if question.author.id === $profile.id}
|
||||
<PrimaryButton
|
||||
label={isEditMode ? 'Save' : 'Edit'}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleQuestionEdit}
|
||||
className="h-fit"
|
||||
/>
|
||||
{#if isEditMode}
|
||||
<PrimaryButton
|
||||
label="Cancel"
|
||||
variant={VARIANTS.TEXT}
|
||||
onClick={() => (isEditMode = !isEditMode)}
|
||||
className="py-3 px-6 rounded-sm h-fit"
|
||||
disablePadding={true}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="border-1 border-gray my-1 rounded-lg border px-1">
|
||||
<header 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={question.author.avatar ? question.author.avatar : '/logo-192.png'}
|
||||
alt={question.author.name ? question.author.name : 'User'}
|
||||
/>
|
||||
<Avatar.Fallback>{shortenName(question.author.name) || 'U'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<p class="ml-2 text-sm dark:text-white">{question.author.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{question.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
{#if question.author.id === $profile.id || $isOrgAdmin}
|
||||
<IconButton
|
||||
value="delete-question"
|
||||
onClick={() => {
|
||||
if (!question) return;
|
||||
|
||||
deleteQuestion.shouldDelete = true;
|
||||
deleteQuestion.questionId = question.id;
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
{#if isEditMode && editorInstance}
|
||||
<div class="my-2">
|
||||
<TextEditor
|
||||
placeholder="Give an answer"
|
||||
content={editContent.body}
|
||||
onChange={(content) => (editContent.body = content)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<section class="prose prose-sm sm:prose p-2">
|
||||
{@html question.body}
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="my-8">
|
||||
{pluralize($t('community.answers'), question.totalComments, true)}
|
||||
</div>
|
||||
|
||||
{#each question.comments as comment}
|
||||
<div class="my-5 flex items-start px-1">
|
||||
<Vote
|
||||
value={comment.votes}
|
||||
upVote={() => upvoteQuestion('comment', comment.id)}
|
||||
disabled={voted.comment[comment.id]}
|
||||
/>
|
||||
<div class="border-1 border-gray w-full rounded-lg border">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black">
|
||||
<Avatar.Root class="h-7 w-7">
|
||||
<Avatar.Image
|
||||
src={comment.avatar ? comment.avatar : '/logo-192.png'}
|
||||
alt={comment.name ? comment.name : 'User'}
|
||||
/>
|
||||
<Avatar.Fallback>{shortenName(comment.name) || 'U'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<p class="ml-2 text-sm dark:text-white">{comment.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{comment.createdAt}
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<InputField bind:value={editContent.title} className="flex-1" errorMessage={errors.title} />
|
||||
<Select.Root type="single" bind:value={editContent.courseId}>
|
||||
<Select.Trigger class="h-full w-[25%]">
|
||||
<p>
|
||||
{editContent.courseId
|
||||
? fetchedCourses.find((course) => course.id === editContent.courseId)?.title
|
||||
: $t('community.ask.select_course')}
|
||||
</p>
|
||||
</div>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each fetchedCourses as course}
|
||||
{#if course.id}
|
||||
<Select.Item value={course.id}>{course.title}</Select.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<Vote value={currentQuestion.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
|
||||
<Page.Title>{currentQuestion.title}</Page.Title>
|
||||
</div>
|
||||
{/if}
|
||||
</Page.HeaderContent>
|
||||
{#if currentQuestion.author.id === $profile.id}
|
||||
<Page.Action>
|
||||
<Button variant="outline" onclick={handleQuestionEdit}>
|
||||
{isEditMode ? $t('community.ask.save') : $t('community.ask.edit')}
|
||||
</Button>
|
||||
{#if isEditMode}
|
||||
<Button variant="ghost" onclick={() => (isEditMode = !isEditMode)}>
|
||||
{$t('community.ask.cancel')}
|
||||
</Button>
|
||||
{/if}
|
||||
</Page.Action>
|
||||
{/if}
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<div class="border-gray my-1 rounded-lg border px-1">
|
||||
<header 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={currentQuestion.author.avatar ? currentQuestion.author.avatar : '/logo-192.png'}
|
||||
alt={currentQuestion.author.name ? currentQuestion.author.name : 'User'}
|
||||
/>
|
||||
<Avatar.Fallback>{shortenName(currentQuestion.author.name) || 'U'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<p class="ml-2 text-sm dark:text-white">{currentQuestion.author.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{currentQuestion.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
{#if currentQuestion.author.id === $profile.id || $isOrgAdmin}
|
||||
<IconButton
|
||||
onclick={() => {
|
||||
deleteQuestion.shouldDelete = true;
|
||||
deleteQuestion.questionId = currentQuestion.id;
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
{#if isEditMode && editorInstance}
|
||||
<div class="my-2">
|
||||
<TextEditor
|
||||
placeholder="Give an answer"
|
||||
content={editContent.body}
|
||||
onChange={(content) => (editContent.body = content)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<section class="prose prose-sm sm:prose p-2">
|
||||
{@html currentQuestion.body}
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isValidAnswer}
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
<div class="my-8">
|
||||
{pluralize($t('community.answers'), currentQuestion.totalComments, true)}
|
||||
</div>
|
||||
|
||||
{#if comment.authorId === $profile.id || $isOrgAdmin}
|
||||
<IconButton
|
||||
value="delete-comment"
|
||||
onClick={() => {
|
||||
deleteComment.shouldDelete = true;
|
||||
deleteComment.commentId = comment.id;
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
<article class="prose prose-sm sm:prose p-2">
|
||||
{@html comment.comment}
|
||||
</article>
|
||||
{#each currentQuestion.comments as comment}
|
||||
<div class="my-5 flex items-start px-1">
|
||||
<Vote
|
||||
value={comment.votes}
|
||||
upVote={() => upvoteQuestion('comment', comment.id)}
|
||||
disabled={voted.comment[comment.id]}
|
||||
/>
|
||||
<div class="border-gray w-full rounded-lg border">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black">
|
||||
<Avatar.Root class="h-7 w-7">
|
||||
<Avatar.Image
|
||||
src={comment.avatar ? comment.avatar : '/logo-192.png'}
|
||||
alt={comment.name ? comment.name : 'User'}
|
||||
/>
|
||||
<Avatar.Fallback>{shortenName(comment.name) || 'U'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<p class="ml-2 text-sm dark:text-white">{comment.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{comment.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if isValidAnswer}
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
|
||||
{#if comment.authorId === $profile.id || $isOrgAdmin}
|
||||
<IconButton
|
||||
onclick={() => {
|
||||
deleteComment.shouldDelete = true;
|
||||
deleteComment.commentId = comment.id;
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
<article class="prose prose-sm sm:prose p-2">
|
||||
{@html comment.comment}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="mt-4">
|
||||
{#if !editorInstance}
|
||||
<TextEditor placeholder="Give an answer" content={comment} onChange={(content) => (comment = content)} />
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Button onclick={submitComment}>
|
||||
{$t('community.ask.comment')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
{#if !editorInstance}
|
||||
<TextEditor placeholder="Give an answer" content={comment} onChange={(content) => (comment = content)} />
|
||||
{/if}
|
||||
|
||||
<div class="mr-2 flex justify-end">
|
||||
<PrimaryButton label="Comment" onClick={submitComment} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user