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 <best@classroomio.com>

* 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 <103611443+canhamzacode@users.noreply.github.com>
This commit is contained in:
Rotimi Best
2025-12-22 02:36:48 +01:00
committed by GitHub
co-authored by Abdulmuizz Hamzat
parent e6a3cf9332
commit 9fb6c7cbaa
401 changed files with 10489 additions and 11679 deletions
@@ -1,45 +1,37 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { Component } from 'svelte';
import get from 'lodash/get';
import { resolve } from '$app/paths';
import { Badge } from '@cio/ui/base/badge';
import UserIcon from '@lucide/svelte/icons/user';
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import CircleDotIcon from '@lucide/svelte/icons/circle-dot';
import TrendingUpIcon from '@lucide/svelte/icons/trending-up';
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import ImageRenderer from '$lib/components/Org/ImageRenderer.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { Button } from '@cio/ui/base/button';
import { Separator } from '@cio/ui/base/separator';
import * as Item from '@cio/ui/base/item';
import { Progress } from '@cio/ui/base/progress';
import { COURSE_TYPE } from '$lib/utils/types';
import { Image } from '$features/ui';
import { COURSE_TYPE, type Course } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
import { calcCourseDiscount } from '$lib/utils/functions/course';
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
import { copyCourseModal, deleteCourseModal } from '$lib/features/course/utils/store';
import { calcProgressRate } from '$features/course/utils/functions';
import CardDropdown from './card-dropdown.svelte';
interface Props {
bannerImage: string | undefined;
id?: string;
slug?: string;
title?: string;
description?: string;
isPublished?: boolean;
totalLessons?: number;
totalStudents?: number;
currency?: string;
export interface Props {
course: Course;
isOnLandingPage?: boolean;
isLMS?: boolean;
isExplore?: boolean;
progressRate?: number;
type: COURSE_TYPE;
pricingData?: {
cost: number;
currency?: string;
showDiscount?: boolean;
discount?: number;
};
}
let { course, isOnLandingPage, isLMS, isExplore }: Props = $props();
$inspect('isLMS', isLMS);
let {
bannerImage,
id = '',
@@ -50,45 +42,40 @@
totalLessons = 0,
totalStudents = 0,
currency = 'USD',
isOnLandingPage = false,
isLMS = false,
isExplore = false,
progressRate = 45,
type,
pricingData = {
cost: 0
cost: 0,
discount: 0,
showDiscount: false
}
}: Props = $props();
} = $derived({
id: course.id,
slug: course.slug,
bannerImage: course.logo || '/images/classroomio-course-img-template.jpg',
title: course.title,
type: course.type,
description: course.description,
isPublished: course.is_published,
pricingData: {
cost: course.cost,
discount: course.metadata?.discount || 0,
showDiscount: course.metadata?.showDiscount || false
},
currency: course.currency,
totalLessons: get(course, 'lessons[0].count', 0),
progressRate: calcProgressRate(course.progress_rate, course.total_lessons),
totalStudents: course.total_students
});
let formatter = $derived(getCurrencyFormatter(currency));
function handleCloneCourse() {
$copyCourseModal.open = true;
$copyCourseModal.id = id;
$copyCourseModal.title = title;
$copyCourseModal.description = description;
}
function handleShareCourse() {
goto(`/courses/${id}/settings#share`);
}
function handleInvite() {
goto(`/courses/${id}/people?add=true`);
}
function handleDeleteCourse() {
$deleteCourseModal.open = true;
$deleteCourseModal.id = id;
$deleteCourseModal.title = title;
}
const COURSE_TAG: Record<
string,
{
style: string;
label: string;
icon: any;
icon: Component;
iconStyle?: string;
}
> = {
@@ -102,7 +89,7 @@
style: '',
label: $t('course.navItem.settings.self_paced'),
icon: UserIcon,
iconStyle: 'custom text-primary-700'
iconStyle: 'custom ui:text-primary'
},
SPECIALIZATION: {
style: '',
@@ -118,146 +105,91 @@
);
</script>
<div
role="button"
tabindex="0"
onclick={() => {
goto(courseUrl);
}}
onkeydown={(e) => {
if (e.key === 'Enter') {
goto(courseUrl);
}
}}
class="border-gray group relative w-full max-w-[320px] rounded border text-black dark:border-neutral-600"
>
<div class="p-4">
<div class="relative mb-5">
<Item.Root variant="outline" class="p-3! group relative max-w-[320px]">
{#snippet child({ props })}
<a href={resolve(courseUrl, {})} {...props}>
{#if !isLMS && !isOnLandingPage}
<DropdownMenu.Root>
<DropdownMenu.Trigger
class="absolute right-2 top-2 z-40 flex items-center justify-center rounded-full bg-gray-200 p-2 opacity-0 transition-all delay-150 duration-200 ease-in-out group-hover:opacity-100 dark:bg-neutral-800"
onclick={(e) => e.stopPropagation()}
>
<EllipsisVerticalIcon size={16} />
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onclick={handleCloneCourse}>
{$t('courses.course_card.context_menu.clone')}
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleShareCourse}>
{$t('courses.course_card.context_menu.share')}
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleInvite}>
{$t('courses.course_card.context_menu.invite')}
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-red-600" onclick={handleDeleteCourse}>
{$t('courses.course_card.context_menu.delete')}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CardDropdown {id} {title} {description} />
{/if}
<ImageRenderer
src={bannerImage}
alt="Banner Image"
className="relative h-[200px] w-full rounded dark:border dark:border-neutral-600"
/>
<Item.Header>
<Item.Media variant="image" class="h-50! w-full! relative">
<Image src={bannerImage} alt="Course banner image" className="w-full h-full rounded-sm object-cover" />
{#if type}
{@const tag = COURSE_TAG[type]}
<span
class="bg-primary-50 absolute bottom-2 left-2 z-10 flex items-center gap-1 rounded-sm p-1 font-mono text-xs capitalize"
>
<tag.icon class={tag.iconStyle} />
{tag.label}
</span>
{/if}
</div>
{#if type}
{@const tag = COURSE_TAG[type]}
<Badge class="rounded-md! absolute bottom-2 left-2 z-10 flex items-center capitalize" variant="secondary">
<tag.icon class={tag.iconStyle} />
{tag.label}
</Badge>
{/if}
</Item.Media>
</Item.Header>
<h3 class="title text-xl dark:text-white">{title}</h3>
<p class="description mt-2 text-sm text-gray-500 dark:text-gray-300">
{description}
</p>
</div>
<Item.Content>
<Item.Title class="text-base! line-clamp-1 min-h-[44px]">
{title}
</Item.Title>
<div
class="border-gray flex justify-between border border-b-0 border-l-0 border-r-0 px-4 py-2 dark:border-neutral-600 {isLMS &&
'items-center'}"
>
<div>
<p class="text-xs {!isLMS && 'pl-2'} dark:text-white">
{totalLessons}
{$t('courses.course_card.lessons_number')}
</p>
<p class="py-2 text-xs">
{#if isOnLandingPage}
<span class="px-2">
{#if !cost}
{$t('course.navItem.landing_page.pricing_section.free')}
{:else if pricingData.showDiscount}
{formatter.format(cost)}
<span class="line-through">
{formatter?.format(pricingData?.cost)}
</span>
{:else}
{formatter.format(cost)}
{/if}
</span>
{:else if isLMS}
{#if !isExplore}
<div class="flex items-center gap-2">
<div class=" relative h-1 w-[50px] bg-[#EAEAEA]">
<div style="width:{progressRate}%" class="bg-primary-700 absolute left-0 top-0 h-full"></div>
</div>
<p class="text-xs text-[#656565] dark:text-white">{progressRate}%</p>
<Item.Description class="min-h-[63px]">{description}</Item.Description>
<Separator class="my-3" />
<div class="flex justify-between {isLMS && 'items-center'} w-full">
<div class="w-[50%]">
<p class="text-xs {!isLMS && 'pl-2'} dark:text-white">
{totalLessons}
{$t('courses.course_card.lessons_number')}
</p>
<div class="py-2 text-xs">
{#if isOnLandingPage}
<span class="px-2">
{#if !cost}
{$t('course.navItem.landing_page.pricing_section.free')}
{:else if pricingData.showDiscount}
{formatter.format(cost)}
<span class="line-through">
{formatter?.format(pricingData?.cost)}
</span>
{:else}
{formatter.format(cost)}
{/if}
</span>
{:else if isLMS}
{#if !isExplore}
<div class="flex w-3/4 items-center gap-2">
<Progress value={50} />
<p class="ui:text-muted-foreground text-xs">{progressRate}%</p>
</div>
{/if}
{:else}
<Badge variant={isPublished ? 'default' : 'outline'}>
{#if isPublished}
{$t('courses.course_card.published')}
{:else}
{$t('courses.course_card.unpublished')}
{/if}
</Badge>
{/if}
</div>
</div>
{#if isLMS}
<Button variant="outline">
{isExplore ? $t('courses.course_card.learn_more') : $t('courses.course_card.continue_course')}
<ArrowRightIcon class="custom" />
</Button>
{:else if !isOnLandingPage}
<div class="flex flex-col justify-between">
<p class="pl-2 text-xs dark:text-white">
{totalStudents}
{$t('courses.course_card.students')}
</p>
</div>
{/if}
{:else}
<Badge variant={isPublished ? 'default' : 'secondary'}>
{#if isPublished}
{$t('courses.course_card.published')}
{:else}
{$t('courses.course_card.unpublished')}
{/if}
</Badge>
{/if}
</p>
</div>
{#if isLMS}
<PrimaryButton
label={isExplore ? $t('courses.course_card.learn_more') : $t('courses.course_card.continue_course')}
variant={VARIANTS.OUTLINED}
className="rounded-none"
/>
{:else if !isOnLandingPage}
<div class="flex flex-col justify-between">
<p class="pl-2 text-xs dark:text-white">
{totalStudents}
{$t('courses.course_card.students')}
</p>
<div></div>
</div>
{/if}
</div>
</div>
<style>
.title,
.description {
height: 42px;
line-height: 20px;
display: -webkit-box;
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2;
-moz-line-clamp: 2;
-ms-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
</style>
</div>
</Item.Content>
</a>
{/snippet}
</Item.Root>