diff --git a/apps/api/src/routes/organization/organization.ts b/apps/api/src/routes/organization/organization.ts index 01498aa4..e059db2b 100644 --- a/apps/api/src/routes/organization/organization.ts +++ b/apps/api/src/routes/organization/organization.ts @@ -74,6 +74,7 @@ import { searchRouter } from '@api/routes/organization/search'; import { tagsRouter } from '@api/routes/organization/tags'; import { widgetsRouter } from '@api/routes/organization/widgets'; import { zValidator } from '@hono/zod-validator'; +import { ZGetRecommendedCourses } from '@cio/utils/validation/course'; export const organizationRouter = new Hono() /** @@ -433,24 +434,31 @@ export const organizationRouter = new Hono() * Gets recommended courses (published courses user isn't enrolled in) for a user in an organization (used in lms) * Requires authentication and organization membership */ - .get('/courses/recommended', authMiddleware, orgMemberMiddleware, async (c) => { - try { - const user = c.get('user')!; + .get( + '/courses/recommended', + authMiddleware, + orgMemberMiddleware, + zValidator('query', ZGetRecommendedCourses), + async (c) => { + try { + const user = c.get('user')!; + const { limit } = c.req.valid('query'); - const orgId = c.req.header('cio-org-id')!; - const result = await getRecommendedCourses(orgId, user.id); + const orgId = c.req.header('cio-org-id')!; + const result = await getRecommendedCourses(orgId, user.id, limit); - return c.json( - { - success: true, - data: result - }, - 200 - ); - } catch (error) { - return handleError(c, error, 'Failed to fetch recommended courses'); + return c.json( + { + success: true, + data: result + }, + 200 + ); + } catch (error) { + return handleError(c, error, 'Failed to fetch recommended courses'); + } } - }) + ) /** * GET /organization/courses * Gets courses for an organization with role-based filtering (used in dashboard) diff --git a/apps/api/src/services/organization.ts b/apps/api/src/services/organization.ts index 5b9b44d4..bebc0651 100644 --- a/apps/api/src/services/organization.ts +++ b/apps/api/src/services/organization.ts @@ -469,9 +469,9 @@ export async function getUserEnrolledCourses(orgId: string, userId: string) { * @param userId - User ID to exclude enrolled courses * @returns Array of recommended courses */ -export async function getRecommendedCourses(orgId: string, userId: string) { +export async function getRecommendedCourses(orgId: string, userId: string, limit?: number) { try { - return getExploreCourses({ orgId, profileId: userId }); + return getExploreCourses({ orgId, profileId: userId, limit }); } catch (error) { if (error instanceof AppError) throw error; throw new AppError( diff --git a/apps/dashboard/src/lib/features/course/api/courses.svelte.ts b/apps/dashboard/src/lib/features/course/api/courses.svelte.ts index ea6377ec..e3f21899 100644 --- a/apps/dashboard/src/lib/features/course/api/courses.svelte.ts +++ b/apps/dashboard/src/lib/features/course/api/courses.svelte.ts @@ -135,9 +135,12 @@ export class CoursesApi extends BaseApiWithErrors { * Fetches recommended courses (published courses user isn't enrolled in) for the current organization * Org ID is automatically added from currentOrg store */ - async getRecommendedCourses() { + async getRecommendedCourses(options?: { limit?: number }) { await this.execute({ - requestFn: () => classroomio.organization.courses.recommended.$get({}), + requestFn: () => + classroomio.organization.courses.recommended.$get({ + query: options?.limit ? { limit: String(options.limit) } : {} + }), logContext: 'fetching recommended courses', onSuccess: (response) => { if (response.data) { diff --git a/apps/dashboard/src/lib/features/course/components/card-list.svelte b/apps/dashboard/src/lib/features/course/components/card-list.svelte index 86039aca..3b4b5665 100644 --- a/apps/dashboard/src/lib/features/course/components/card-list.svelte +++ b/apps/dashboard/src/lib/features/course/components/card-list.svelte @@ -2,15 +2,16 @@ import * as Item from '@cio/ui/base/item'; import Card, { type Props as CardProps } from './card.svelte'; - type Props = Omit & { + type Props = Omit & { courses: CardProps['course'][]; + onCardClick?: (course: CardProps['course']) => void; }; - let { courses, ...rest }: Props = $props(); + let { courses, onCardClick, ...rest }: Props = $props(); {#each courses as course (course.id)} - + onCardClick(course) : undefined} /> {/each} diff --git a/apps/dashboard/src/lib/features/course/components/card.svelte b/apps/dashboard/src/lib/features/course/components/card.svelte index e8ff50c8..d4ab45e4 100644 --- a/apps/dashboard/src/lib/features/course/components/card.svelte +++ b/apps/dashboard/src/lib/features/course/components/card.svelte @@ -38,9 +38,10 @@ isExplore?: boolean; href?: string; actions?: Snippet; + onExploreClick?: () => void; } - let { course, isOnLandingPage, isLMS, isExplore, href, actions }: Props = $props(); + let { course, isOnLandingPage, isLMS, isExplore, href, actions, onExploreClick }: Props = $props(); let { bannerImage, @@ -141,6 +142,10 @@ let cost = $derived(calcCourseDiscount(pricingData.discount, pricingData.cost ?? 0, !!pricingData.showDiscount)); let courseUrl = $derived.by(() => { + if (onExploreClick && isLMS && isExplore) { + return undefined; + } + if (href) { return href; } @@ -299,7 +304,7 @@ {#if isLMS} - + + + diff --git a/apps/dashboard/src/lib/features/lms/pages/dashboard.svelte b/apps/dashboard/src/lib/features/lms/pages/dashboard.svelte index 8ea43a55..afbd0459 100644 --- a/apps/dashboard/src/lib/features/lms/pages/dashboard.svelte +++ b/apps/dashboard/src/lib/features/lms/pages/dashboard.svelte @@ -17,7 +17,10 @@ import { currentOrg, currentOrgDomain } from '$lib/utils/store/org'; import { profile } from '$lib/utils/store/user'; import { coursesApi } from '$features/course/api'; + import { CourseCard } from '$features/course/components'; import CoursePublicBadge from '$features/course/components/course-public-badge.svelte'; + import CoursePreviewModal from '$features/lms/components/course-preview-modal.svelte'; + import type { RecommendedCourses } from '$features/course/types'; import { IconButton } from '@cio/ui/custom/icon-button'; import ExternalLinkIcon from '@lucide/svelte/icons/external-link'; import CopyIcon from '@lucide/svelte/icons/copy'; @@ -34,6 +37,8 @@ let loginStreak = $state(null); let hasLoadedLoginStreak = $state(false); + let selectedCourse = $state(null); + let previewOpen = $state(false); let totalCompleted = $derived( coursesApi.enrolledCourses.reduce((acc, course) => acc + getCourseCompletedItems(course), 0) @@ -58,6 +63,7 @@ if (!$profile.id || !$currentOrg.id) return; coursesApi.getEnrolledCourses(); + coursesApi.getRecommendedCourses({ limit: 6 }); }); $effect(() => { @@ -395,4 +401,41 @@ {/if} + + {#if coursesApi.isLoading || coursesApi.recommendedCourses.length > 0} +
+
+

+ {$t('dashboard.explore_more_courses')} +

+ +
+ + {#if coursesApi.isLoading} +
+ +
+ {:else} +
+ {#each coursesApi.recommendedCourses as course (course.id)} + { + selectedCourse = course; + previewOpen = true; + }} + /> + {/each} +
+ {/if} +
+ {/if} + + {#if selectedCourse} + + {/if} diff --git a/apps/dashboard/src/lib/features/lms/pages/explore.svelte b/apps/dashboard/src/lib/features/lms/pages/explore.svelte index 3362558f..05856e7b 100644 --- a/apps/dashboard/src/lib/features/lms/pages/explore.svelte +++ b/apps/dashboard/src/lib/features/lms/pages/explore.svelte @@ -9,9 +9,12 @@ import { coursesApi } from '$features/course/api'; import type { RecommendedCourses } from '$features/course/types'; import { CourseSortBy, DEFAULT_COURSE_SORT, parseCourseSortValue } from '$features/course/utils/constants'; + import CoursePreviewModal from '$features/lms/components/course-preview-modal.svelte'; let searchValue = $state(''); let sortKey = $state(DEFAULT_COURSE_SORT); + let selectedCourse = $state(null); + let previewOpen = $state(false); $effect(() => { if (!browser) return; @@ -67,4 +70,12 @@ isLoading={coursesApi.isLoading} bind:searchValue bind:sortKey + onCardClick={(course) => { + selectedCourse = course as RecommendedCourses[number]; + previewOpen = true; + }} /> + +{#if selectedCourse} + +{/if} diff --git a/apps/dashboard/src/lib/features/lms/pages/mylearning.svelte b/apps/dashboard/src/lib/features/lms/pages/mylearning.svelte index 574cc99e..e21dd521 100644 --- a/apps/dashboard/src/lib/features/lms/pages/mylearning.svelte +++ b/apps/dashboard/src/lib/features/lms/pages/mylearning.svelte @@ -1,5 +1,6 @@