From 697d20ba2200539d73cdbab5a7ee3e6ee516459e Mon Sep 17 00:00:00 2001 From: Shiv Raj Bhagat Date: Thu, 6 Feb 2025 18:09:08 +0530 Subject: [PATCH] Feat: Implement flagged lesson functionality with updates to DTOs, frontend components, and language strings --- .../src/common/services/basic-crud.service.ts | 15 ++++ apps/quick-learn-backend/src/lang/en.ts | 4 + .../src/routes/lesson/dto/get-lesson.dto.ts | 13 +++ .../src/routes/lesson/dto/index.ts | 1 + .../src/routes/lesson/lesson.controller.ts | 38 ++++++-- .../src/routes/lesson/lesson.service.ts | 51 ++++++++--- .../src/routes/metadata/metadata.service.ts | 10 ++- .../src/apiServices/lessonsService.ts | 26 +++++- .../approvals/[lesson]/lessonDetails.tsx | 3 +- .../dashboard/approvals/approvalList.tsx | 2 +- .../flagged/[lesson]/lessonDetails.tsx | 90 +++++++++++++++++++ .../dashboard/flagged/[lesson]/page.tsx | 13 +++ .../dashboard/flagged/flaggedList.tsx | 83 +++++++++-------- .../src/constants/lang/en.ts | 1 + .../src/context/contextHelperService.ts | 5 +- apps/quick-learn-frontend/src/middleware.ts | 2 +- .../src/shared/components/Breadcrumb.tsx | 10 +-- .../src/shared/components/Navbar.tsx | 43 ++++++--- .../src/shared/components/ViewLesson.tsx | 49 +++++++--- .../src/shared/types/breadcrumbType.ts | 1 + .../src/shared/types/contentRepository.ts | 8 +- .../src/shared/types/utilTypes.ts | 19 +--- .../store/features/systemPreferenceSlice.ts | 16 ++-- .../quick-learn-frontend/src/utils/HiLink.tsx | 13 ++- 24 files changed, 386 insertions(+), 130 deletions(-) create mode 100644 apps/quick-learn-backend/src/routes/lesson/dto/get-lesson.dto.ts create mode 100644 apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/[lesson]/lessonDetails.tsx create mode 100644 apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/[lesson]/page.tsx diff --git a/apps/quick-learn-backend/src/common/services/basic-crud.service.ts b/apps/quick-learn-backend/src/common/services/basic-crud.service.ts index 32c7169b..da0693af 100644 --- a/apps/quick-learn-backend/src/common/services/basic-crud.service.ts +++ b/apps/quick-learn-backend/src/common/services/basic-crud.service.ts @@ -30,6 +30,21 @@ export class BasicCrudService { }); } + /** + * Retrieves a count based on the provided options. + * @param {FindOptionsWhere | FindOptionsWhere[]} [options] - The options for finding the entity. + * @returns {Promise} A promise that resolves with the retrieved entity. + */ + async count( + options?: FindOptionsWhere | FindOptionsWhere[], + relations: string[] = [], + ): Promise { + return await this.repository.count({ + where: options, + relations: [...relations], + }); + } + /** * Retrieves multiple entities based on the provided options. * @param {FindOptionsWhere | FindOptionsWhere[]} [options] - The options for finding the entities. diff --git a/apps/quick-learn-backend/src/lang/en.ts b/apps/quick-learn-backend/src/lang/en.ts index b4d487d4..540adc2d 100644 --- a/apps/quick-learn-backend/src/lang/en.ts +++ b/apps/quick-learn-backend/src/lang/en.ts @@ -109,4 +109,8 @@ export const en = { successdullyPasswordUpdated: 'Password updated successfully.', successPreferencesUpdated: 'Email preference updated successfully.', successPreferences: 'Successfully got user preferences.', + + // flagged lesson + invalidLesson: 'Invalid lesson is provided.', + successUnflagLesson: 'Successfully unflagged the lesson.', }; diff --git a/apps/quick-learn-backend/src/routes/lesson/dto/get-lesson.dto.ts b/apps/quick-learn-backend/src/routes/lesson/dto/get-lesson.dto.ts new file mode 100644 index 00000000..b3c20c65 --- /dev/null +++ b/apps/quick-learn-backend/src/routes/lesson/dto/get-lesson.dto.ts @@ -0,0 +1,13 @@ +import { IsIn, IsOptional, IsString, ValidateIf } from 'class-validator'; + +export class GetLessonDto { + @ValidateIf((o) => !o?.flagged || o?.flagged != 'true') + @IsString() + @IsIn(['true', 'false']) + approved?: string; + + @IsOptional() + @IsString() + @IsIn(['true', 'false']) + flagged?: string; +} diff --git a/apps/quick-learn-backend/src/routes/lesson/dto/index.ts b/apps/quick-learn-backend/src/routes/lesson/dto/index.ts index b5601297..e8c11a12 100644 --- a/apps/quick-learn-backend/src/routes/lesson/dto/index.ts +++ b/apps/quick-learn-backend/src/routes/lesson/dto/index.ts @@ -1,2 +1,3 @@ export * from './create-lesson.dto'; export * from './update-lesson.dto'; +export * from './get-lesson.dto'; diff --git a/apps/quick-learn-backend/src/routes/lesson/lesson.controller.ts b/apps/quick-learn-backend/src/routes/lesson/lesson.controller.ts index d1e86625..3059aa51 100644 --- a/apps/quick-learn-backend/src/routes/lesson/lesson.controller.ts +++ b/apps/quick-learn-backend/src/routes/lesson/lesson.controller.ts @@ -14,7 +14,7 @@ import { LessonService } from './lesson.service'; import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { BasePaginationDto, SuccessResponse } from '@src/common/dto'; import { en } from '@src/lang/en'; -import { CreateLessonDto, UpdateLessonDto } from './dto'; +import { CreateLessonDto, GetLessonDto, UpdateLessonDto } from './dto'; import { JwtAuthGuard } from '../auth/guards'; import { CurrentUser } from '@src/common/decorators/current-user.decorators'; import { UserEntity } from '@src/entities'; @@ -25,6 +25,7 @@ import { RolesGuard } from '../auth/guards/roles.guard'; import { Roles } from '@src/common/decorators/roles.decorator'; import { UserTypeId } from '@src/common/enum/user_role.enum'; import { LessonProgressService } from '../lesson-progress/lesson-progress.service'; +import { MoreThan } from 'typeorm'; @ApiTags('Lessons') @Controller({ @@ -113,12 +114,21 @@ export class LessonController { */ async get( @Param('id') id: string, - @Query('approved') approved: string, + @Query() getLessonDto: GetLessonDto, ): Promise { - const lesson = await this.service.get( - { id: +id, approved: approved == 'true' }, - ['created_by_user', 'course'], - ); + const conditions = { id: +id }; + const relations = ['created_by_user', 'course']; + if (getLessonDto.approved) + conditions['approved'] = getLessonDto.approved == 'true'; + if (getLessonDto.flagged) { + conditions['flagged_lesson'] = { + id: MoreThan(0), + }; + relations.push('flagged_lesson'); + relations.push('flagged_lesson.user'); + } + + const lesson = await this.service.get(conditions, relations); if (!lesson) { throw new BadRequestException(en.lessonNotFound); } @@ -162,6 +172,22 @@ export class LessonController { return new SuccessResponse(en.approveLesson); } + @UseGuards(RolesGuard) + @Roles(UserTypeId.SUPER_ADMIN, UserTypeId.ADMIN) + @ApiOperation({ summary: 'Unflag an lessons.' }) + @Patch('/:id/unflag') + /** + * Approves an existing lesson. + * @param id The id of the lesson that needs to be approved. + * @param user The user approving the lesson. + * @throws BadRequestException if the lesson doesn't exist + * @returns A promise that resolves to a success response. + */ + async unFlag(@Param('id') id: string): Promise { + await this.service.unFlagLesson(+id); + return new SuccessResponse(en.successUnflagLesson); + } + @ApiOperation({ summary: 'Archive an lessons.' }) @Patch('/:id/archive') /** diff --git a/apps/quick-learn-backend/src/routes/lesson/lesson.service.ts b/apps/quick-learn-backend/src/routes/lesson/lesson.service.ts index caa14d85..5a1b9a89 100644 --- a/apps/quick-learn-backend/src/routes/lesson/lesson.service.ts +++ b/apps/quick-learn-backend/src/routes/lesson/lesson.service.ts @@ -13,7 +13,7 @@ import { en } from '@src/lang/en'; import { UserTypeIdEnum } from '@quick-learn/shared'; import { PaginationDto } from '../users/dto'; import { PaginatedResult } from '@src/common/interfaces'; -import { Repository, DataSource, ILike } from 'typeorm'; +import { Repository, DataSource, ILike, MoreThan } from 'typeorm'; import Helpers from '@src/common/utils/helper'; import { FileService } from '@src/file/file.service'; import { DailyLessonEnum } from '@src/common/enum/daily_lesson.enum'; @@ -25,7 +25,7 @@ export class LessonService extends PaginationService { @InjectRepository(LessonTokenEntity) private readonly LessonTokenRepository: Repository, @InjectRepository(FlaggedLessonEntity) - private flaggedLessonEnity: Repository, + private flaggedLessonRepository: Repository, private readonly courseService: CourseService, private readonly FileService: FileService, private readonly dataSource: DataSource, @@ -448,13 +448,13 @@ export class LessonService extends PaginationService { } // Create new flagged lesson entry - const flaggedLesson = this.flaggedLessonEnity.create({ + const flaggedLesson = this.flaggedLessonRepository.create({ user_id: lessonToken.user_id, lesson_id: lessonToken.lesson_id, course_id: lessonToken.course_id, }); - return await this.flaggedLessonEnity.save(flaggedLesson); + return await this.flaggedLessonRepository.save(flaggedLesson); } async findAllFlaggedLesson(page = 1, limit = 10, search = '') { @@ -490,18 +490,18 @@ export class LessonService extends PaginationService { // Get both lessons and count in parallel for better performance const [lessons, total] = await Promise.all([ - this.flaggedLessonEnity.find(findOptions), - this.flaggedLessonEnity.count({ + this.flaggedLessonRepository.find(findOptions), + this.flaggedLessonRepository.count({ where: findOptions.where, }), ]); return { - lessons, - totalCount: total, + items: lessons, + total, page, limit, - totalPages: Math.ceil(total / limit), + total_pages: Math.ceil(total / limit), }; } catch (error) { console.error('Error querying flagged lessons:', error); @@ -509,16 +509,41 @@ export class LessonService extends PaginationService { } } + async unFlagLesson(id: number): Promise { + const isValid = await this.flaggedLessonRepository.find({ + where: { lesson_id: id }, + }); + + if (!isValid) throw new BadRequestException(en.invalidLesson); + + await this.flaggedLessonRepository.delete({ lesson_id: id }); + } + async getUnApprovedLessonCount() { - return await this.repository.count({ - where: { + return await this.count( + { archived: false, approved: false, course: { archived: false, }, }, - relations: ['course'], - }); + ['course'], + ); + } + + async getFlaggedLessonCount() { + return await this.count( + { + archived: false, + flagged_lesson: { + id: MoreThan(0), + }, + course: { + archived: false, + }, + }, + ['flagged_lesson'], + ); } } diff --git a/apps/quick-learn-backend/src/routes/metadata/metadata.service.ts b/apps/quick-learn-backend/src/routes/metadata/metadata.service.ts index 7f0336ad..21c402e5 100644 --- a/apps/quick-learn-backend/src/routes/metadata/metadata.service.ts +++ b/apps/quick-learn-backend/src/routes/metadata/metadata.service.ts @@ -23,11 +23,15 @@ export class MetadataService { return metadata; } + async getLessonMetaData() { - const unapprovedLesson = - await this.lessonService.getUnApprovedLessonCount(); + const [unapproved_lessons, flagged_lessons] = await Promise.all([ + await this.lessonService.getUnApprovedLessonCount(), + await this.lessonService.getFlaggedLessonCount(), + ]); return { - unapprovedLessons: unapprovedLesson, + unapproved_lessons, + flagged_lessons, }; } } diff --git a/apps/quick-learn-frontend/src/apiServices/lessonsService.ts b/apps/quick-learn-frontend/src/apiServices/lessonsService.ts index d04921da..c93138cf 100644 --- a/apps/quick-learn-frontend/src/apiServices/lessonsService.ts +++ b/apps/quick-learn-frontend/src/apiServices/lessonsService.ts @@ -1,4 +1,5 @@ import { + TFlaggedLesson, TLesson, TUserDailyProgress, } from '@src/shared/types/contentRepository'; @@ -13,6 +14,7 @@ import { TDailyLessonResponse, UserLessonProgress, } from '@src/shared/types/LessonProgressTypes'; +import { PaginateWrapper } from '@src/shared/types/utilTypes'; export const getArchivedLessons = async (): Promise< AxiosSuccessResponse @@ -42,6 +44,15 @@ export const getLessonDetails = async ( return response.data; }; +export const getFlaggedLessonDetails = async ( + id: string, +): Promise> => { + const response = await axiosInstance.get>( + ContentRepositoryApiEnum.LESSON + `/${id}?flagged=true`, + ); + return response.data; +}; + export const approveLesson = async ( id: string, ): Promise => { @@ -85,8 +96,10 @@ export const getFlaggedLessons = async ( page = 1, limit = 10, search = '', -): Promise => { - const response = await axiosInstance.get( +): Promise>> => { + const response = await axiosInstance.get< + AxiosSuccessResponse> + >( `${ContentRepositoryApiEnum.GET_FLAGGED_LESSON}?page=${page}&limit=${limit}&q=${search}`, ); return response.data; @@ -192,3 +205,12 @@ export const flagLesson = async ( ); return response.data; }; + +export const markLessonAsUnFlagged = async ( + id: string, +): Promise => { + const response = await axiosInstance.patch( + ContentRepositoryApiEnum.LESSON + `/${id}/unflag`, + ); + return response.data; +}; diff --git a/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/approvals/[lesson]/lessonDetails.tsx b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/approvals/[lesson]/lessonDetails.tsx index 8a62394c..8975873b 100644 --- a/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/approvals/[lesson]/lessonDetails.tsx +++ b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/approvals/[lesson]/lessonDetails.tsx @@ -41,7 +41,8 @@ const LessonDetails = () => { ? defaultLinks : [ ...defaultLinks, - { name: lesson.name, link: `${RouteEnum.APPROVALS}/${lesson.id}` }, + { name: lesson.course.name, link: '#', disabled: true }, + { name: lesson.name, link: `${RouteEnum.FLAGGED}/${lesson.id}` }, ], [lesson], ); diff --git a/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/approvals/approvalList.tsx b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/approvals/approvalList.tsx index 68bd9192..fe86c8ed 100644 --- a/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/approvals/approvalList.tsx +++ b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/approvals/approvalList.tsx @@ -32,7 +32,7 @@ const ApprovalList = () => { if (!isLoading) { dispatch( updateSystemPreferencesData({ - unapprovedLessons: lessons?.length ?? 0, + unapproved_lessons: lessons?.length ?? 0, }), ); } diff --git a/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/[lesson]/lessonDetails.tsx b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/[lesson]/lessonDetails.tsx new file mode 100644 index 00000000..58171c4c --- /dev/null +++ b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/[lesson]/lessonDetails.tsx @@ -0,0 +1,90 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useMemo, useState } from 'react'; +import { RouteEnum } from '@src/constants/route.enum'; +import { + getFlaggedLessonDetails, + markLessonAsUnFlagged, +} from '@src/apiServices/lessonsService'; +import { FullPageLoader } from '@src/shared/components/UIElements'; +import ViewLesson from '@src/shared/components/ViewLesson'; +import { TBreadcrumb } from '@src/shared/types/breadcrumbType'; +import { TLesson } from '@src/shared/types/contentRepository'; +import { + showApiErrorInToast, + showApiMessageInToast, +} from '@src/utils/toastUtils'; +import { useAppDispatch } from '@src/store/hooks'; +import { setHideNavbar } from '@src/store/features/uiSlice'; + +const defaultLinks = [{ name: 'Flagged Lessons', link: RouteEnum.FLAGGED }]; + +const LessonDetails = () => { + const { lesson: id } = useParams<{ lesson: string }>(); + const router = useRouter(); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setHideNavbar(true)); + return () => { + dispatch(setHideNavbar(false)); + }; + }, [setHideNavbar]); + + const [loading, setLoading] = useState(true); + const [lesson, setLesson] = useState(); + const [isFlagged, setIsFlagged] = useState(false); + const links = useMemo( + () => + !lesson + ? defaultLinks + : [ + ...defaultLinks, + { name: lesson.course.name, link: '#', disabled: true }, + { name: lesson.name, link: `${RouteEnum.FLAGGED}/${lesson.id}` }, + ], + [lesson], + ); + + useEffect(() => { + if (isNaN(+id)) return; + setLoading(true); + getFlaggedLessonDetails(id) + .then((res) => setLesson(res.data)) + .catch((err) => { + showApiErrorInToast(err); + router.push(RouteEnum.FLAGGED); + }) + .finally(() => setLoading(false)); + }, [id]); + + function markAsUnFlagged(value: boolean) { + if (isFlagged) return; + setIsFlagged(value); + setLoading(true); + markLessonAsUnFlagged(id) + .then((res) => showApiMessageInToast(res)) + .catch((err) => showApiErrorInToast(err)) + .finally(() => { + setLoading(false); + router.push(RouteEnum.FLAGGED); + }); + } + + if (!lesson) return null; + + return ( +
+ {loading && } + +
+ ); +}; + +export default LessonDetails; diff --git a/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/[lesson]/page.tsx b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/[lesson]/page.tsx new file mode 100644 index 00000000..f20c2b61 --- /dev/null +++ b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/[lesson]/page.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import LessonDetails from './lessonDetails'; + +export const metadata = { + title: 'Lesson Details • Quick Learn', + description: 'Lesson Details quick learn', +}; + +const page = () => { + return ; +}; + +export default page; diff --git a/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/flaggedList.tsx b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/flaggedList.tsx index f79b9e1d..d29a724b 100644 --- a/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/flaggedList.tsx +++ b/apps/quick-learn-frontend/src/app/(Dashboard)/dashboard/flagged/flaggedList.tsx @@ -1,16 +1,17 @@ 'use client'; -import { DateFormats } from '@src/constants/dateFormats'; +import { useEffect, useState } from 'react'; +import { format } from 'date-fns'; +import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'; import { en } from '@src/constants/lang/en'; +import { DateFormats } from '@src/constants/dateFormats'; import { RouteEnum } from '@src/constants/route.enum'; -import { format } from 'date-fns'; -import Link from 'next/link'; -import { useEffect, useState } from 'react'; -import FlaggedListSkeleton from './flaggedSkeleton'; import { getFlaggedLessons } from '@src/apiServices/lessonsService'; import { showApiErrorInToast } from '@src/utils/toastUtils'; -import { AxiosErrorObject } from '@src/apiServices/axios'; -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'; -import { FlaggedLesson } from '@src/shared/types/utilTypes'; +import FlaggedListSkeleton from './flaggedSkeleton'; +import { TFlaggedLesson } from '@src/shared/types/contentRepository'; +import { useAppDispatch } from '@src/store/hooks'; +import { updateSystemPreferencesData } from '@src/store/features/systemPreferenceSlice'; +import { SuperLink } from '@src/utils/HiLink'; const columns = [ en.common.lesson, @@ -23,7 +24,8 @@ const columns = [ const ITEMS_PER_PAGE = 10; const FlaggedList = () => { - const [flaggedLessons, setFlaggedLessons] = useState([]); + const dispatch = useAppDispatch(); + const [flaggedLessons, setFlaggedLessons] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); const [currentPage, setCurrentPage] = useState(1); @@ -32,6 +34,17 @@ const FlaggedList = () => { const [searchInputValue, setSearchInputValue] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); + useEffect(() => { + if (!isLoading) { + dispatch( + updateSystemPreferencesData({ + flagged_lessons: flaggedLessons?.length ?? 0, + }), + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]); + // Debounce search input to reduce unnecessary API calls useEffect(() => { const handler = setTimeout(() => { @@ -45,30 +58,18 @@ const FlaggedList = () => { useEffect(() => { const fetchFlaggedLessons = async () => { - try { - setIsLoading(true); - const response = await getFlaggedLessons( - currentPage, - ITEMS_PER_PAGE, - debouncedSearch, - ); - - if (response?.data) { - setFlaggedLessons(response.data.lessons); - setTotalLessons(response.data.totalCount); - setTotalPages(response.data.totalPages); - } else { - setFlaggedLessons([]); - setTotalLessons(0); - } - } catch (error) { - showApiErrorInToast(error as AxiosErrorObject); - setFlaggedLessons([]); - setTotalLessons(0); - } finally { - setIsLoading(false); - setIsInitialLoad(false); - } + setIsLoading(true); + await getFlaggedLessons(currentPage, ITEMS_PER_PAGE, debouncedSearch) + .then(({ data }) => { + setFlaggedLessons(data.items || []); + setTotalLessons(data.total || 0); + setTotalPages(data.total_pages || 0); + }) + .catch((err) => showApiErrorInToast(err)) + .finally(() => { + setIsLoading(false); + setIsInitialLoad(false); + }); }; fetchFlaggedLessons(); @@ -144,19 +145,23 @@ const FlaggedList = () => { className="px-4 py-2 font-medium text-gray-900 whitespace-nowrap" >
- - {flaggedLesson.lesson?.name || '-'} - + {flaggedLesson.lesson?.name ?? '-'} +
- {formatDate(flaggedLesson.lesson.updated_at)} + {(flaggedLesson?.lesson?.updated_at && + formatDate(flaggedLesson?.lesson?.updated_at)) || + '-'} - {formatDate(flaggedLesson.lesson.created_at)} + {(flaggedLesson?.lesson?.created_at && + formatDate(flaggedLesson.lesson.created_at)) || + '-'} {formatDate(flaggedLesson.flagged_on)} diff --git a/apps/quick-learn-frontend/src/constants/lang/en.ts b/apps/quick-learn-frontend/src/constants/lang/en.ts index 8790fb20..4dd7b5fd 100644 --- a/apps/quick-learn-frontend/src/constants/lang/en.ts +++ b/apps/quick-learn-frontend/src/constants/lang/en.ts @@ -207,6 +207,7 @@ export const en = { subHeading: 'Following lessons are waiting for approval after being created or updated by the team.', approveThisLesson: 'Approve this lesson', + unFlagThisLesson: 'Unflag this lesson', approvalPendingExclamation: 'Approval pending!', approvalPendingInfo: 'This lesson is awaiting approval from the team. After approval this will be available to all the team members.', diff --git a/apps/quick-learn-frontend/src/context/contextHelperService.ts b/apps/quick-learn-frontend/src/context/contextHelperService.ts index 052770f0..7adec2d8 100644 --- a/apps/quick-learn-frontend/src/context/contextHelperService.ts +++ b/apps/quick-learn-frontend/src/context/contextHelperService.ts @@ -30,10 +30,7 @@ export const useFetchContentRepositoryMetadata = (forceFetch = false) => { }; const fetchApprovalData = async (user_type: number) => { - if ( - path !== RouteEnum.APPROVALS && - ![UserTypeIdEnum.EDITOR, UserTypeIdEnum.MEMBER].includes(user_type) - ) { + if (![UserTypeIdEnum.EDITOR, UserTypeIdEnum.MEMBER].includes(user_type)) { const res = await getSystemPreferences(); dispatch(updateSystemPreferencesData(res.data)); } diff --git a/apps/quick-learn-frontend/src/middleware.ts b/apps/quick-learn-frontend/src/middleware.ts index d3ee0c98..14efffcd 100644 --- a/apps/quick-learn-frontend/src/middleware.ts +++ b/apps/quick-learn-frontend/src/middleware.ts @@ -24,7 +24,7 @@ const ADMIN_AND_SUPERADMIN_ROUTES = [ RouteEnum.APPROVALS, RouteEnum.FLAGGED, ]; -const EDITOR_ROUTES = [RouteEnum.CONTENT, RouteEnum.FLAGGED]; +const EDITOR_ROUTES = [RouteEnum.CONTENT]; // Helper functions const isPublicRoute = (path: string) => PUBLIC_ROUTES.includes(path); diff --git a/apps/quick-learn-frontend/src/shared/components/Breadcrumb.tsx b/apps/quick-learn-frontend/src/shared/components/Breadcrumb.tsx index e7a0b97e..a3d01d46 100644 --- a/apps/quick-learn-frontend/src/shared/components/Breadcrumb.tsx +++ b/apps/quick-learn-frontend/src/shared/components/Breadcrumb.tsx @@ -9,11 +9,11 @@ interface Props { } function customLink( - { link, name }: TBreadcrumb, + { link, name, disabled: linkDisabled }: TBreadcrumb, isLast = false, disabled = false, ) { - if (isLast || disabled) { + if (isLast || disabled || linkDisabled) { return ( {name} @@ -35,14 +35,14 @@ const Breadcrumb: FC = ({ links, disabled = false }) => {
@@ -355,7 +378,7 @@ const Navbar = () => { {item.name} - {showApprovalCount(item, 'mobile')} + {showCount(item, 'mobile')} ))} diff --git a/apps/quick-learn-frontend/src/shared/components/ViewLesson.tsx b/apps/quick-learn-frontend/src/shared/components/ViewLesson.tsx index b99ccccd..fe7511c8 100644 --- a/apps/quick-learn-frontend/src/shared/components/ViewLesson.tsx +++ b/apps/quick-learn-frontend/src/shared/components/ViewLesson.tsx @@ -8,6 +8,7 @@ import { DateFormats } from '@src/constants/dateFormats'; import { en } from '@src/constants/lang/en'; import { TLesson } from '../types/contentRepository'; import { TBreadcrumb } from '../types/breadcrumbType'; +import { FlagIcon } from '@heroicons/react/24/outline'; // Separate components for better performance const LessonHeader = memo( ({ @@ -54,26 +55,28 @@ LessonContent.displayName = 'LessonContent'; const ApprovalCheckbox = memo( ({ - isApproved, - setIsApproved, + value, + setValue, + text = en.approvals.approveThisLesson, }: { - isApproved?: boolean; - setIsApproved?: (value: boolean) => void; + value?: boolean; + setValue?: (value: boolean) => void; + text?: string; }) => (
setIsApproved?.(true)} - disabled={isApproved} + checked={value} + onChange={() => setValue?.(true)} + disabled={value} className="w-8 h-8 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 disabled:cursor-not-allowed" />
), @@ -120,12 +123,16 @@ interface Props { isPending?: boolean; showCreatedBy?: boolean; disableLink?: boolean; + isFlagged?: boolean; + setIsFlagged?: (value: boolean) => void; } const ViewLesson: FC = ({ lesson, isApproved, setIsApproved, + isFlagged, + setIsFlagged, links, isPending = false, showCreatedBy = true, @@ -151,10 +158,28 @@ const ViewLesson: FC = ({ {setIsApproved && ( - + + )} + + {setIsFlagged && ( + <> +
+
+ +
+ {`The Lesson is flagged by ${ + lesson?.flagged_lesson?.user?.display_name ?? 'Unknown' + } on ${format( + lesson?.flagged_lesson?.flagged_on ?? Date.now(), + DateFormats.shortDate, + )}`} +
+ + )} {isPending && } diff --git a/apps/quick-learn-frontend/src/shared/types/breadcrumbType.ts b/apps/quick-learn-frontend/src/shared/types/breadcrumbType.ts index 2eea1405..f195121f 100644 --- a/apps/quick-learn-frontend/src/shared/types/breadcrumbType.ts +++ b/apps/quick-learn-frontend/src/shared/types/breadcrumbType.ts @@ -1,4 +1,5 @@ export type TBreadcrumb = { name: string; link: string; + disabled?: boolean; }; diff --git a/apps/quick-learn-frontend/src/shared/types/contentRepository.ts b/apps/quick-learn-frontend/src/shared/types/contentRepository.ts index 89d7ade1..d1043f58 100644 --- a/apps/quick-learn-frontend/src/shared/types/contentRepository.ts +++ b/apps/quick-learn-frontend/src/shared/types/contentRepository.ts @@ -154,6 +154,12 @@ export type SearchedQuery = { Lessons: SearchedCourseOrRoadpmap[]; }; +export enum SystemPreferencesKey { + UNAPPROVED_LESSONS = 'unapproved_lessons', + FLAGGED_LESSONS = 'flagged_lessons', +} + export type SystemPreferences = { - unapprovedLessons: number; + [SystemPreferencesKey.UNAPPROVED_LESSONS]: number; + [SystemPreferencesKey.FLAGGED_LESSONS]: number; }; diff --git a/apps/quick-learn-frontend/src/shared/types/utilTypes.ts b/apps/quick-learn-frontend/src/shared/types/utilTypes.ts index ee8bc272..f751e1f0 100644 --- a/apps/quick-learn-frontend/src/shared/types/utilTypes.ts +++ b/apps/quick-learn-frontend/src/shared/types/utilTypes.ts @@ -7,25 +7,8 @@ export type PaginateWrapper = { limit: number; total_pages: number; }; + export type FileUploadResponse = { file: string; type: string; }; - -export interface FlaggedLesson { - id: number; - flagged_on: string; - updated_at: string; - created_at: string; - course_id: string; - lesson_id: string; - lesson: { - name: string; - created_at: string; - updated_at: string; - }; - user: { - first_name: string; - last_name: string; - }; -} diff --git a/apps/quick-learn-frontend/src/store/features/systemPreferenceSlice.ts b/apps/quick-learn-frontend/src/store/features/systemPreferenceSlice.ts index 88f6bc0d..966b2b8a 100644 --- a/apps/quick-learn-frontend/src/store/features/systemPreferenceSlice.ts +++ b/apps/quick-learn-frontend/src/store/features/systemPreferenceSlice.ts @@ -3,10 +3,7 @@ import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { RootState } from '../store'; import { REHYDRATE } from 'redux-persist'; import type { PersistedState } from 'redux-persist'; - -export type SystemPreferences = { - unapprovedLessons: number; -}; +import { SystemPreferences } from '@src/shared/types/contentRepository'; interface SystemPreferencesState { metadata: SystemPreferences; @@ -25,7 +22,8 @@ interface RehydrateAction { const initialState: SystemPreferencesState = { metadata: { - unapprovedLessons: 0, + unapproved_lessons: 0, + flagged_lessons: 0, }, status: 'idle', error: null, @@ -52,9 +50,9 @@ const systemPreferenceSlice = createSlice({ reducers: { updateSystemPreferencesData: ( state, - action: PayloadAction, + action: PayloadAction>, ) => { - state.metadata.unapprovedLessons = action.payload.unapprovedLessons; + state.metadata = { ...state.metadata, ...action.payload }; }, }, extraReducers: (builder) => { @@ -87,7 +85,5 @@ const systemPreferenceSlice = createSlice({ export const { updateSystemPreferencesData } = systemPreferenceSlice.actions; export default systemPreferenceSlice.reducer; -export const getUnapprovedLessonCount = (state: RootState) => - state?.systemPreference?.metadata?.unapprovedLessons; export const getSystemPreferencesState = (state: RootState) => - state?.systemPreference?.status; + state?.systemPreference; diff --git a/apps/quick-learn-frontend/src/utils/HiLink.tsx b/apps/quick-learn-frontend/src/utils/HiLink.tsx index 508e0f9d..336ea93f 100644 --- a/apps/quick-learn-frontend/src/utils/HiLink.tsx +++ b/apps/quick-learn-frontend/src/utils/HiLink.tsx @@ -1,10 +1,12 @@ 'use client'; - +import React, { forwardRef, type ComponentPropsWithRef } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { type ComponentPropsWithRef } from 'react'; -export const SuperLink = (props: ComponentPropsWithRef) => { +export const SuperLink = forwardRef< + HTMLAnchorElement, + ComponentPropsWithRef +>((props, ref) => { const router = useRouter(); const strHref = typeof props.href === 'string' ? props.href : props.href.href; @@ -17,6 +19,7 @@ export const SuperLink = (props: ComponentPropsWithRef) => { return ( { conditionalPrefetch(); @@ -36,4 +39,6 @@ export const SuperLink = (props: ComponentPropsWithRef) => { }} /> ); -}; +}); + +SuperLink.displayName = 'SuperLink';