\n \n \n );\n};\n\nexport default CourseFeedbackModal;\n","import { Course, Lesson } from '@/types/auth/Course';\nimport { makeGetCourseUrl, makeGetLessonUrlPrefix } from '@/utils/auth/apiPaths';\n\n/**\n * Given an ordered list of lessons and a list of completed lessons:\n *\n * – If all of them are completed, return the first\n * - If none of them are completed, return the first\n * - If only some of them are completed, return the 1st uncompleted lesson.\n *\n * @param {Lesson[]} lessons\n *\n * @returns {string}\n */\nexport const getContinueFromLesson = (lessons: Lesson[]): string => {\n if (!lessons) {\n return null;\n }\n const completedLessonIds = lessons\n .filter((lesson) => lesson.isCompleted)\n .map((lesson) => lesson.id);\n const numberOfCompletedLessons = completedLessonIds.length;\n // if no lessons were completed, return the first lesson\n if (numberOfCompletedLessons === 0) {\n return lessons[0].slug;\n }\n // if all lessons were completed, return the first lesson\n if (numberOfCompletedLessons === lessons.length) {\n return lessons[0].slug;\n }\n // 1. make sure the lessons are sorted by day\n const sortedLessons = lessons.sort((a, b) => a.day - b.day);\n // 2. pick first uncompleted lesson\n for (let index = 0; index < sortedLessons.length; index += 1) {\n // if the lessons has not been completed, return in\n if (!completedLessonIds.includes(sortedLessons[index].id)) {\n return sortedLessons[index].slug;\n }\n }\n return null;\n};\n\n/**\n * Given a lessons array and a lesson id, it returns a new lessons array\n * after setting the lesson with the given id set as completed.\n *\n * @param {Lesson[]} lessons\n * @param {string} lessonId\n * @returns {Lesson[]}\n */\nexport const mutateLessonAsCompleted = (lessons: Lesson[], lessonId: string): Lesson[] => {\n const newLessons = [...lessons];\n const lessonIndex = newLessons.findIndex((loopLesson) => loopLesson.id === lessonId);\n // safety check: if the lesson was found in the lessons array, set it as completed\n if (lessonIndex !== -1) {\n newLessons[lessonIndex].isCompleted = true;\n }\n return newLessons;\n};\n\n/**\n * This function receives the cached lesson data and the id of the lesson that was just completed\n * and expects to return the updated lesson data with the lesson marked as completed\n * which will be used to update the local cache without having to call the API again.\n *\n * @param {Lesson} cachedLessonData\n * @param {string} completedLessonId\n * @returns {Lesson}\n */\nexport const getUpdatedLessonDataAfterCompletion = (\n cachedLessonData: Lesson,\n completedLessonId: string,\n): Lesson => {\n if (cachedLessonData) {\n const updatedLessonData = { ...cachedLessonData };\n // only set the completed lesson data to completed\n if (updatedLessonData.id === completedLessonId) {\n updatedLessonData.isCompleted = true;\n }\n // if the lesson has a course, we should update the lessons array of the course\n if (cachedLessonData?.course?.lessons) {\n updatedLessonData.course.lessons = mutateLessonAsCompleted(\n updatedLessonData.course.lessons,\n completedLessonId,\n );\n }\n return updatedLessonData;\n }\n return cachedLessonData;\n};\n\n/**\n * This function receives the cached course data and the id of the lesson that was just completed\n * and expects to return the updated course data with the lesson marked as completed\n * which will be used to update the local cache without having to call the API again.\n *\n * @param {Course} cachedCourseData\n * @param {string} completedLessonId\n * @returns {Course}\n */\nexport const getUpdatedCourseDataAfterCompletion = (\n cachedCourseData: Course,\n completedLessonId: string,\n): Course => {\n if (cachedCourseData) {\n const updatedCourseData = { ...cachedCourseData };\n // if the course has lessons, we should update the lessons array\n if (updatedCourseData?.lessons) {\n const completedLessons = updatedCourseData.lessons.filter(\n (loopLesson) => loopLesson.isCompleted,\n );\n // if we are marking the last un-completed lesson in the course, we should mark the course itself as completed\n if (completedLessons.length + 1 === updatedCourseData.lessons.length) {\n updatedCourseData.isCompleted = true;\n }\n updatedCourseData.lessons = mutateLessonAsCompleted(\n updatedCourseData.lessons,\n completedLessonId,\n );\n updatedCourseData.continueFromLesson = getContinueFromLesson(updatedCourseData.lessons);\n }\n return updatedCourseData;\n }\n return cachedCourseData;\n};\n\n/**\n * we need to update all the cached lessons of the course to set the current lesson as completed\n *\n * @param {any} mutatorFunction\n * @param {string} courseSlug\n * @param {string} completedLessonId\n *\n * @returns {void}\n */\nexport const mutateCachedLessonsAfterCompletion = (\n mutatorFunction: any,\n courseSlug: string,\n completedLessonId: string,\n): void => {\n const courseLessonsUrlRegex = `^${makeGetLessonUrlPrefix(courseSlug)}/.+`;\n mutatorFunction(courseLessonsUrlRegex, (cachedLessonData: Lesson) =>\n getUpdatedLessonDataAfterCompletion(cachedLessonData, completedLessonId),\n );\n};\n\n/**\n * update local cache of the course to set the current lesson as completed in the lessons array\n *\n * @param {any} mutatorFunction\n * @param {string} courseSlug\n * @param {string} completedLessonId\n *\n * @returns {void}\n */\nexport const mutateCachedCourseAfterCompletion = (\n mutatorFunction: any,\n courseSlug: string,\n completedLessonId: string,\n): void => {\n mutatorFunction(makeGetCourseUrl(courseSlug), (cachedCourseData: Course) =>\n getUpdatedCourseDataAfterCompletion(cachedCourseData, completedLessonId),\n );\n};\n\n/**\n * we need to update all the cached lessons of the course to set the current lesson as completed\n *\n * @param {any} mutatorFunction\n * @param {string} courseSlug\n *\n * @returns {void}\n */\nexport const mutateCachedLessonsAfterFeedback = (\n mutatorFunction: any,\n courseSlug: string,\n): void => {\n const courseLessonsUrlRegex = `^${makeGetLessonUrlPrefix(courseSlug)}/.+`;\n mutatorFunction(courseLessonsUrlRegex, (cachedLessonData: Lesson) =>\n getUpdatedLessonDataAfterFeedback(cachedLessonData),\n );\n};\n\n/**\n * update local cache of the course to set the current lesson as completed in the lessons array\n *\n * @param {any} mutatorFunction\n * @param {string} courseSlug\n *\n * @returns {void}\n */\nexport const mutateCachedCourseAfterFeedback = (mutatorFunction: any, courseSlug: string): void => {\n mutatorFunction(makeGetCourseUrl(courseSlug), (cachedCourseData: Course) =>\n getUpdatedCourseDataAfterFeedback(cachedCourseData),\n );\n};\n\n/**\n * This function receives the cached course data and the id of the lesson that was just completed\n * and expects to return the updated course data with the lesson marked as completed\n * which will be used to update the local cache without having to call the API again.\n *\n * @param {Course} cachedCourseData\n * @returns {Course}\n */\nexport const getUpdatedCourseDataAfterFeedback = (cachedCourseData: Course): Course => {\n if (cachedCourseData) {\n const updatedCourseData = { ...cachedCourseData };\n updatedCourseData.userHasFeedback = true;\n return updatedCourseData;\n }\n return cachedCourseData;\n};\n\n/**\n * This function receives the cached lesson data and the id of the lesson that was just completed\n * and expects to return the updated lesson data with the lesson marked as completed\n * which will be used to update the local cache without having to call the API again.\n *\n * @param {Lesson} cachedLessonData\n * @returns {Lesson}\n */\nexport const getUpdatedLessonDataAfterFeedback = (cachedLessonData: Lesson): Lesson => {\n if (cachedLessonData) {\n const updatedLessonData = { ...cachedLessonData };\n // if the lesson has a course, we should update it to userHasFeedback = true\n if (cachedLessonData?.course) {\n updatedLessonData.course = {\n ...updatedLessonData.course,\n userHasFeedback: true,\n };\n }\n return updatedLessonData;\n }\n return cachedLessonData;\n};\n","import React, { memo } from 'react';\n\nimport useSWRImmutable from 'swr/immutable';\n\nimport Error from '@/components/Error';\nimport Spinner from '@/dls/Spinner/Spinner';\nimport { fetcher } from 'src/api';\nimport { BaseResponse } from 'types/ApiResponses';\n\ninterface Props {\n queryKey: string;\n render: (data: BaseResponse) => JSX.Element;\n renderError?: (error: any) => JSX.Element | undefined;\n initialData?: BaseResponse;\n loading?: () => JSX.Element;\n fetcher?: (queryKey: string) => Promise;\n showSpinnerOnRevalidate?: boolean;\n onFetchSuccess?: (data: BaseResponse) => void;\n}\n\n/**\n * Data fetcher is a dynamic component that serves as a container for a component\n * that depends on data from a remote API to render. This component handles:\n * 1. Calling the API.\n * 2. Caching the response (due to using useSwr).\n * 3. Handling errors if any by showing an error message.\n * 4. Handling when the user is offline while trying to fetch the API response.\n * 5. Dynamically passing the response data through render-props to the parent.\n *\n * @param {Props} props\n * @returns {JSX.Element}\n */\nconst DataFetcher: React.FC = ({\n queryKey,\n render,\n renderError,\n initialData,\n loading = () => ,\n fetcher: dataFetcher = fetcher,\n showSpinnerOnRevalidate = true,\n onFetchSuccess,\n}: Props): JSX.Element => {\n const { data, error, isValidating, mutate } = useSWRImmutable(\n queryKey,\n () =>\n dataFetcher(queryKey)\n .then((res) => {\n onFetchSuccess?.(res);\n return Promise.resolve(res);\n })\n .catch((err) => Promise.reject(err)),\n {\n fallbackData: initialData,\n },\n );\n\n // if showSpinnerOnRevalidate is true, we should show the spinner if we are revalidating the data.\n // otherwise, we should only show the spinner on initial loads.\n if (showSpinnerOnRevalidate ? isValidating : isValidating && !data) {\n return loading();\n }\n\n const onRetryClicked = () => {\n mutate();\n };\n\n /**\n * if we haven't fetched the data yet and the device is not online (because we don't want to show an offline message if the data already exists).\n * or if we had an error when calling the API.\n */\n if (error) {\n // if there is a custom error renderer, use it.\n if (renderError) {\n const errorComponent = renderError(error);\n // if the custom error renderer returns false, it means that it doesn't want to render anything special.\n if (typeof errorComponent !== 'undefined') {\n return errorComponent;\n }\n }\n return ;\n }\n\n return render(data);\n};\n\nexport default memo(DataFetcher);\n","var _path;\nfunction _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }\nimport * as React from \"react\";\nvar SvgRetry = function SvgRetry(props) {\n return /*#__PURE__*/React.createElement(\"svg\", _extends({\n width: 15,\n height: 15,\n viewBox: \"0 0 15 15\",\n fill: \"none\",\n xmlns: \"http://www.w3.org/2000/svg\"\n }, props), _path || (_path = /*#__PURE__*/React.createElement(\"path\", {\n d: \"M1.85 7.5c0-2.835 2.21-5.65 5.65-5.65 2.778 0 4.152 2.056 4.737 3.15H10.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-1 0v1.813C12.296 3.071 10.666.85 7.5.85 3.437.85.85 4.185.85 7.5c0 3.315 2.587 6.65 6.65 6.65 1.944 0 3.562-.77 4.714-1.942a6.77 6.77 0 0 0 1.428-2.167.5.5 0 1 0-.925-.38 5.77 5.77 0 0 1-1.216 1.846c-.971.99-2.336 1.643-4.001 1.643-3.44 0-5.65-2.815-5.65-5.65Z\",\n fill: \"currentColor\",\n fillRule: \"evenodd\",\n clipRule: \"evenodd\"\n })));\n};\nexport default SvgRetry;","import React from 'react';\n\nimport useTranslation from 'next-translate/useTranslation';\n\nimport styles from './Error.module.scss';\n\nimport Button, { ButtonSize, ButtonType } from '@/dls/Button/Button';\nimport RetryIcon from '@/icons/retry.svg';\nimport { OFFLINE_ERROR } from 'src/api';\n\ninterface Props {\n onRetryClicked: () => void;\n error: Error;\n}\n\nconst Error: React.FC = ({ onRetryClicked, error }) => {\n const { t } = useTranslation('common');\n return (\n
\n );\n};\n\nexport default Error;\n","import capitalize from 'lodash/capitalize';\nimport { Translate } from 'next-translate';\n\nimport { FormBuilderFormField } from './FormBuilderTypes';\n\nimport FormField, { FormFieldType } from 'types/FormField';\n\n/**\n * Transform FormField to be FormBuilderFormField\n *\n * FormField and FormBuilderFormField are the same except, FormBuilderFormField is not tied to errorId and translationId\n * - Previously FormBuilder was tied to common.json, next-translate.\n * - and it's also tied to ErrorMessageId\n * - and the `label` is also less flexible because it's tied to `field` value\n *\n * This function help to transform FormField to FormBuilderFormField for common use case.\n * But when we need a more flexible use case, we can use FormBuilderFormField directly. Without using this helper function\n *\n * check ./FormBuilderTypes.ts for more info\n *\n * Note that this function expect the `t` translate function to be used with `common.json`. And expect `form.$field` and `validation.$errorId` to exist.\n *\n * @param {FormField} formField\n * @returns {FormBuilderFormField} formBuilderFormField\n */\nconst buildFormBuilderFormField = (formField: FormField, t: Translate): FormBuilderFormField => {\n return {\n ...formField,\n ...(formField.rules && {\n rules: formField.rules.map((rule) => ({\n type: rule.type,\n value: rule.value,\n errorMessage: t(`common:validation.${rule.errorId}`, {\n field: capitalize(formField.field),\n ...rule.errorExtraParams,\n }),\n })),\n }),\n ...(formField.label && {\n label:\n formField.type === FormFieldType.Checkbox ? formField.label : t(`form.${formField.label}`),\n }),\n ...(formField.defaultValue && { defaultValue: formField.defaultValue }),\n ...(formField.placeholder && { placeholder: formField.placeholder }),\n };\n};\n\nexport default buildFormBuilderFormField;\n","import capitalize from 'lodash/capitalize';\nimport { Translate } from 'next-translate';\n\nimport ErrorMessageId from 'types/ErrorMessageId';\n\nconst DEFAULT_ERROR_ID = ErrorMessageId.InvalidField;\n\nconst buildTranslatedErrorMessageByErrorId = (\n errorId: ErrorMessageId,\n fieldName: string,\n t: Translate,\n extraParams?: Record,\n) => {\n if (Object.values(ErrorMessageId).includes(errorId)) {\n return t(`common:validation.${errorId}`, { field: capitalize(fieldName), ...extraParams });\n }\n return t(`common:validation.${DEFAULT_ERROR_ID}`, { field: capitalize(fieldName) });\n};\n\nexport default buildTranslatedErrorMessageByErrorId;\n","import React, { useEffect } from 'react';\n\nimport { defaultValueCtx, Editor, rootCtx, editorViewOptionsCtx } from '@milkdown/core';\nimport { commonmark } from '@milkdown/preset-commonmark';\nimport { Milkdown, useEditor } from '@milkdown/react';\nimport { replaceAll } from '@milkdown/utils';\n\nimport styles from '@/components/MarkdownEditor/MarkdownEditor.module.scss';\n\ntype Props = {\n isEditable?: boolean;\n defaultValue?: string;\n};\n\nconst MarkdownEditor: React.FC = ({ isEditable = true, defaultValue }) => {\n const { get } = useEditor((root) => {\n return Editor.make()\n .config((ctx) => {\n ctx.set(rootCtx, root);\n if (defaultValue) {\n ctx.set(defaultValueCtx, defaultValue);\n }\n // Add attributes to the editor container\n ctx.update(editorViewOptionsCtx, (prev) => ({\n ...prev,\n editable: () => isEditable,\n attributes: { class: styles.editor, spellcheck: 'false' },\n }));\n })\n .use(commonmark);\n }, []);\n\n useEffect(() => {\n if (defaultValue) {\n get()?.action(replaceAll(defaultValue));\n }\n }, [defaultValue, get]);\n\n return (\n
;\n};\n\nexport default PageContainer;\n","import { useRef, useImperativeHandle, ForwardedRef } from 'react';\n\nimport * as Dialog from '@radix-ui/react-dialog';\nimport classNames from 'classnames';\nimport { useRouter } from 'next/router';\n\nimport Button, { ButtonShape, ButtonVariant } from '../Button/Button';\n\nimport styles from './ContentModal.module.scss';\n\nimport ContentModalHandles from '@/dls/ContentModal/types/ContentModalHandles';\nimport CloseIcon from '@/icons/close.svg';\nimport { isRTLLocale } from '@/utils/locale';\n\nexport enum ContentModalSize {\n SMALL = 'small',\n MEDIUM = 'medium',\n}\n\ntype ContentModalProps = {\n isOpen?: boolean;\n onClose?: () => void;\n onEscapeKeyDown?: () => void;\n children: React.ReactNode;\n hasCloseButton?: boolean;\n hasHeader?: boolean;\n header?: React.ReactNode;\n innerRef?: ForwardedRef;\n // using innerRef instead of using function forwardRef so we can dynamically load this component https://github.com/vercel/next.js/issues/4957#issuecomment-413841689\n contentClassName?: string;\n size?: ContentModalSize;\n isFixedHeight?: boolean;\n};\n\nconst SCROLLBAR_WIDTH = 15;\n\nconst ContentModal = ({\n isOpen,\n onClose,\n onEscapeKeyDown,\n hasCloseButton,\n children,\n header,\n innerRef,\n contentClassName,\n size = ContentModalSize.MEDIUM,\n isFixedHeight,\n hasHeader = true,\n}: ContentModalProps) => {\n const overlayRef = useRef();\n const { locale } = useRouter();\n\n useImperativeHandle(innerRef, () => ({\n scrollToTop: () => {\n if (overlayRef.current) overlayRef.current.scrollTop = 0;\n },\n }));\n\n /**\n * We need to manually check what the user is targeting. If it lies at the\n * area where the scroll bar is (assuming the scrollbar width is equivalent\n * to SCROLLBAR_WIDTH), then we don't close the Modal, otherwise we do.\n * We also need to check if the current locale is RTL or LTR because the side\n * where the scrollbar is will be different and therefor the value of\n * {e.detail.originalEvent.offsetX} will be different.\n *\n * inspired by {@see https://github.com/radix-ui/primitives/issues/1280#issuecomment-1198248523}\n *\n * @param {any} e\n */\n const onPointerDownOutside = (e: any) => {\n const currentTarget = e.currentTarget as HTMLElement;\n\n const shouldPreventOnClose = isRTLLocale(locale)\n ? e.detail.originalEvent.offsetX < SCROLLBAR_WIDTH // left side of the screen clicked\n : e.detail.originalEvent.offsetX > currentTarget.clientWidth - SCROLLBAR_WIDTH; // right side of the screen clicked\n\n if (shouldPreventOnClose) {\n e.preventDefault();\n return;\n }\n if (onClose) {\n onClose();\n }\n };\n\n return (\n \n \n \n \n {hasHeader && (\n