feat: list view on courses page (#269)
* working on the list view * working on the list view navigation * worked on the list view for the course page * worked on the list view for the course page * clean up * rename store --------- Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
import Share from 'carbon-icons-svelte/lib/Share.svelte';
|
import Share from 'carbon-icons-svelte/lib/Share.svelte';
|
||||||
import UserFollow from 'carbon-icons-svelte/lib/UserFollow.svelte';
|
import UserFollow from 'carbon-icons-svelte/lib/UserFollow.svelte';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
export let bannerImage: string | undefined;
|
export let bannerImage: string | undefined;
|
||||||
export let id = '';
|
export let id = '';
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
export let isOnLandingPage = false;
|
export let isOnLandingPage = false;
|
||||||
export let isLMS = false;
|
export let isLMS = false;
|
||||||
export let progressRate = 45;
|
export let progressRate = 45;
|
||||||
export let showContextMenu = false;
|
|
||||||
let target: any;
|
let target: any;
|
||||||
|
|
||||||
$: formatter = getCurrencyFormatter(currency);
|
$: formatter = getCurrencyFormatter(currency);
|
||||||
@@ -52,9 +53,13 @@
|
|||||||
// TODO: Delete course functionality
|
// TODO: Delete course functionality
|
||||||
alert('WIP: Delete course');
|
alert('WIP: Delete course');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
console.log('unmounting card');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showContextMenu}
|
{#if !isLMS && !isOnLandingPage}
|
||||||
<ContextMenu {target}>
|
<ContextMenu {target}>
|
||||||
<ContextMenuOption
|
<ContextMenuOption
|
||||||
indented
|
indented
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="my-2">
|
||||||
|
<SkeletonText heading />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
StructuredListRow,
|
||||||
|
StructuredListCell,
|
||||||
|
Tag,
|
||||||
|
OverflowMenuItem,
|
||||||
|
OverflowMenu
|
||||||
|
} from 'carbon-components-svelte';
|
||||||
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
|
||||||
|
export let id = '';
|
||||||
|
export let title = '';
|
||||||
|
export let description = '';
|
||||||
|
export let isPublished = false;
|
||||||
|
export let totalLessons = 0;
|
||||||
|
export let totalStudents = 0;
|
||||||
|
|
||||||
|
function handleCloneCourse(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// TODO: Clone course functionality
|
||||||
|
alert('WIP: Clone course');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShareCourse(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// TODO: Share course functionality
|
||||||
|
alert('WIP: Share course');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInvite(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// TODO: Invite functionality
|
||||||
|
alert('WIP: Invite people to course');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteCourse(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// TODO: Delete course functionality
|
||||||
|
alert('WIP: Delete course');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<StructuredListRow label for="row-{id}" on:click={() => goto(`/courses/${id}`)}>
|
||||||
|
<StructuredListCell><p class="font-semibold">{title}</p></StructuredListCell>
|
||||||
|
<StructuredListCell>
|
||||||
|
<p class="line-clamp-2">{description}</p>
|
||||||
|
</StructuredListCell>
|
||||||
|
{#if !$isMobile}
|
||||||
|
<StructuredListCell>{totalLessons}</StructuredListCell>
|
||||||
|
<StructuredListCell>{totalStudents}</StructuredListCell>
|
||||||
|
<StructuredListCell>
|
||||||
|
<Tag class="break-normal" type={isPublished ? 'green' : 'cool-gray'}>
|
||||||
|
{#if isPublished}
|
||||||
|
Published
|
||||||
|
{:else}
|
||||||
|
Unpublished
|
||||||
|
{/if}
|
||||||
|
</Tag>
|
||||||
|
</StructuredListCell>
|
||||||
|
{/if}
|
||||||
|
<StructuredListCell>
|
||||||
|
<OverflowMenu
|
||||||
|
id="course-list"
|
||||||
|
flipped
|
||||||
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OverflowMenuItem
|
||||||
|
text={$t('courses.lesson_card.context_menu.clone')}
|
||||||
|
on:click={handleCloneCourse}
|
||||||
|
/>
|
||||||
|
<OverflowMenuItem
|
||||||
|
text={$t('courses.lesson_card.context_menu.share')}
|
||||||
|
on:click={handleShareCourse}
|
||||||
|
/>
|
||||||
|
<OverflowMenuItem
|
||||||
|
text={$t('courses.lesson_card.context_menu.invite')}
|
||||||
|
on:click={handleInvite}
|
||||||
|
/>
|
||||||
|
<OverflowMenuItem
|
||||||
|
danger
|
||||||
|
text={$t('courses.lesson_card.context_menu.delete')}
|
||||||
|
on:click={handleDeleteCourse}
|
||||||
|
/>
|
||||||
|
</OverflowMenu>
|
||||||
|
</StructuredListCell>
|
||||||
|
</StructuredListRow>
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Box from '../Box/index.svelte';
|
import Box from '../Box/index.svelte';
|
||||||
import Card from './components/Card/index.svelte';
|
import Card from './components/Card/index.svelte';
|
||||||
|
import List from './components/List/index.svelte';
|
||||||
import CardLoader from './components/Card/Loader.svelte';
|
import CardLoader from './components/Card/Loader.svelte';
|
||||||
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
|
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
|
||||||
import { courseMetaDeta } from './store';
|
import { courseMetaDeta } from './store';
|
||||||
import type { Course } from '$lib/utils/types';
|
import type { Course } from '$lib/utils/types';
|
||||||
import { globalStore } from '$lib/utils/store/app';
|
import { globalStore } from '$lib/utils/store/app';
|
||||||
|
import {
|
||||||
|
StructuredList,
|
||||||
|
StructuredListHead,
|
||||||
|
StructuredListRow,
|
||||||
|
StructuredListCell,
|
||||||
|
StructuredListBody
|
||||||
|
} from 'carbon-components-svelte';
|
||||||
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
|
||||||
export let courses: Course[] = [];
|
export let courses: Course[] = [];
|
||||||
@@ -23,30 +32,60 @@
|
|||||||
|
|
||||||
<!-- <CopyCourseModal /> -->
|
<!-- <CopyCourseModal /> -->
|
||||||
|
|
||||||
<div class={`w-full ${$courseMetaDeta.isLoading || courses ? 'cards-container' : ''} my-4 mx-auto`}>
|
<div class={`w-full my-4 mx-auto`}>
|
||||||
{#if $courseMetaDeta.isLoading}
|
{#if $courseMetaDeta.isLoading}
|
||||||
<CardLoader />
|
<section class={`${$courseMetaDeta.isLoading || courses ? 'cards-container' : ''} `}>
|
||||||
<CardLoader />
|
<CardLoader />
|
||||||
<CardLoader />
|
<CardLoader />
|
||||||
|
<CardLoader />
|
||||||
|
</section>
|
||||||
|
{:else if $courseMetaDeta.view === 'list'}
|
||||||
|
<StructuredList selection class="w-full">
|
||||||
|
<StructuredListHead>
|
||||||
|
<StructuredListRow head>
|
||||||
|
<StructuredListCell head>Title</StructuredListCell>
|
||||||
|
<StructuredListCell head>Description</StructuredListCell>
|
||||||
|
{#if !$isMobile}
|
||||||
|
<StructuredListCell head>Lessons</StructuredListCell>
|
||||||
|
<StructuredListCell head>Students</StructuredListCell>
|
||||||
|
<StructuredListCell head>Published</StructuredListCell>
|
||||||
|
{/if}
|
||||||
|
<StructuredListCell head>{''}</StructuredListCell>
|
||||||
|
</StructuredListRow>
|
||||||
|
</StructuredListHead>
|
||||||
|
<StructuredListBody>
|
||||||
|
{#each courses as courseData}
|
||||||
|
<List
|
||||||
|
id={courseData.id}
|
||||||
|
title={courseData.title}
|
||||||
|
description={courseData.description}
|
||||||
|
isPublished={courseData.is_published}
|
||||||
|
totalLessons={courseData.total_lessons}
|
||||||
|
totalStudents={courseData.total_students}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</StructuredListBody>
|
||||||
|
</StructuredList>
|
||||||
{:else}
|
{:else}
|
||||||
{#each courses as courseData}
|
<section class={`${$courseMetaDeta.isLoading || courses ? 'cards-container' : ''} `}>
|
||||||
{#key courseData.id}
|
{#each courses as courseData}
|
||||||
<Card
|
{#key courseData.id}
|
||||||
showContextMenu
|
<Card
|
||||||
id={courseData.id}
|
id={courseData.id}
|
||||||
bannerImage={courseData.logo || '/images/classroomio-course-img-template.jpg'}
|
bannerImage={courseData.logo || '/images/classroomio-course-img-template.jpg'}
|
||||||
title={courseData.title}
|
title={courseData.title}
|
||||||
description={courseData.description}
|
description={courseData.description}
|
||||||
isPublished={courseData.is_published}
|
isPublished={courseData.is_published}
|
||||||
cost={courseData.cost}
|
cost={courseData.cost}
|
||||||
currency={courseData.currency}
|
currency={courseData.currency}
|
||||||
totalLessons={courseData.total_lessons}
|
totalLessons={courseData.total_lessons}
|
||||||
totalStudents={courseData.total_students}
|
totalStudents={courseData.total_students}
|
||||||
isLMS={$globalStore.isOrgSite}
|
isLMS={$globalStore.isOrgSite}
|
||||||
progressRate={calcProgressRate(courseData.progress_rate, courseData.total_lessons)}
|
progressRate={calcProgressRate(courseData.progress_rate, courseData.total_lessons)}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !$courseMetaDeta.isLoading && !courses.length}
|
{#if !$courseMetaDeta.isLoading && !courses.length}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { writable, derived } from 'svelte/store';
|
|||||||
import type { Course } from '$lib/utils/types';
|
import type { Course } from '$lib/utils/types';
|
||||||
|
|
||||||
export const courses = writable<Course[]>([]);
|
export const courses = writable<Course[]>([]);
|
||||||
|
export const view = writable('grid');
|
||||||
export const coursesInProgress = derived(courses, ($courses) =>
|
export const coursesInProgress = derived(courses, ($courses) =>
|
||||||
$courses.length > 0 ? $courses.slice(0, 3) : []
|
$courses.length > 0 ? $courses.slice(0, 3) : []
|
||||||
);
|
);
|
||||||
@@ -12,8 +13,12 @@ export const coursesComplete = derived(courses, ($courses) =>
|
|||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
export const courseMetaDeta = writable({
|
export const courseMetaDeta = writable<{
|
||||||
isLoading: true
|
isLoading: boolean;
|
||||||
|
view: 'grid' | 'list';
|
||||||
|
}>({
|
||||||
|
isLoading: true,
|
||||||
|
view: 'grid'
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createCourseModal = writable({
|
export const createCourseModal = writable({
|
||||||
|
|||||||
@@ -15,8 +15,13 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { t } from '$lib/utils/functions/translations.js';
|
import { t } from '$lib/utils/functions/translations.js';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
|
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||||
|
import Grid from 'carbon-icons-svelte/lib/Grid.svelte';
|
||||||
|
import List from 'carbon-icons-svelte/lib/List.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
let { cantFetch } = data;
|
let { cantFetch } = data;
|
||||||
let searchValue = '';
|
let searchValue = '';
|
||||||
let selectedId: string;
|
let selectedId: string;
|
||||||
@@ -29,7 +34,7 @@
|
|||||||
$createCourseModal.open = true;
|
$createCourseModal.open = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCourses(userId: string | null, orgId: string) {
|
async function getCourses(userId: string | undefined, orgId: string) {
|
||||||
if (cantFetch && typeof cantFetch === 'boolean' && orgId && !hasFetched) {
|
if (cantFetch && typeof cantFetch === 'boolean' && orgId && !hasFetched) {
|
||||||
// only show is loading when fetching for the first time
|
// only show is loading when fetching for the first time
|
||||||
if (!$courses.length) {
|
if (!$courses.length) {
|
||||||
@@ -76,6 +81,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setViewPreference = (preference: 'grid' | 'list') => {
|
||||||
|
$courseMetaDeta.view = preference;
|
||||||
|
localStorage.setItem('courseView', preference);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const courseView = localStorage.getItem('courseView') as 'grid' | 'list' | null;
|
||||||
|
|
||||||
|
if (courseView) {
|
||||||
|
$courseMetaDeta.view = courseView;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$: filterCourses(searchValue, selectedId, $courses);
|
$: filterCourses(searchValue, selectedId, $courses);
|
||||||
$: getCourses($profile.id, $currentOrg.id);
|
$: getCourses($profile.id, $currentOrg.id);
|
||||||
</script>
|
</script>
|
||||||
@@ -118,11 +136,19 @@
|
|||||||
{ id: '2', text: $t('courses.course_filter.lessons') }
|
{ id: '2', text: $t('courses.course_filter.lessons') }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{#if $courseMetaDeta.view === 'list'}
|
||||||
|
<IconButton onClick={() => setViewPreference('grid')}>
|
||||||
|
<Grid size={24} />
|
||||||
|
</IconButton>
|
||||||
|
{:else}
|
||||||
|
<IconButton onClick={() => setViewPreference('list')}>
|
||||||
|
<List size={24} />
|
||||||
|
</IconButton>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NewCourseModal />
|
<NewCourseModal />
|
||||||
|
|
||||||
<Courses bind:courses={filteredCourses} />
|
<Courses bind:courses={filteredCourses} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user