From c98183082fbf4f9551a9c8b0c783c12ddfd95718 Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Fri, 21 Mar 2025 17:13:38 -0400 Subject: [PATCH 01/45] feat: Implement new MFE for learning pathways --- catalog-info.yaml | 6 +- package.json | 12 +- src/example/ExamplePage.jsx | 12 -- src/example/index.scss | 0 src/index.jsx | 18 +- src/index.scss | 2 +- src/learningpath/CourseCard.jsx | 91 ++++++++ .../ExamplePage.test.jsx | 0 src/learningpath/LearningPathCard.jsx | 113 ++++++++++ src/learningpath/LearningPathDetails.jsx | 199 ++++++++++++++++++ src/learningpath/LearningPathList.jsx | 52 +++++ src/{example => learningpath}/data/.gitkeep | 0 src/{example => learningpath}/data/README.rst | 0 src/learningpath/data/api.js | 105 +++++++++ src/learningpath/data/slice.js | 71 +++++++ src/learningpath/data/thunks.js | 65 ++++++ src/learningpath/index.scss | 167 +++++++++++++++ src/store.js | 11 + src/util/assetUrl.js | 6 + 19 files changed, 906 insertions(+), 24 deletions(-) delete mode 100644 src/example/ExamplePage.jsx delete mode 100644 src/example/index.scss create mode 100644 src/learningpath/CourseCard.jsx rename src/{example => learningpath}/ExamplePage.test.jsx (100%) create mode 100644 src/learningpath/LearningPathCard.jsx create mode 100644 src/learningpath/LearningPathDetails.jsx create mode 100644 src/learningpath/LearningPathList.jsx rename src/{example => learningpath}/data/.gitkeep (100%) rename src/{example => learningpath}/data/README.rst (100%) create mode 100644 src/learningpath/data/api.js create mode 100644 src/learningpath/data/slice.js create mode 100644 src/learningpath/data/thunks.js create mode 100644 src/learningpath/index.scss create mode 100644 src/store.js create mode 100644 src/util/assetUrl.js diff --git a/catalog-info.yaml b/catalog-info.yaml index 2c87f95..2d8df03 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -4,10 +4,10 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: "frontend-template-application" - description: "A template for Open edX micro-frontend applications." + name: "frontend-app-learning-path" + description: "The edX learning platform's frontend for learning paths." links: - - url: "https://github.com/openedx/frontend-template-application/blob/master/README.rst" + - url: "https://github.com/open-craft/frontend-app-learning-path/blob/master/README.rst" title: "README" icon: "Article" annotations: diff --git a/package.json b/package.json index af516d5..0a20231 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@edx/frontend-template-application", + "name": "@open-craft/frontend-app-learning-path", "version": "0.1.0", "description": "Frontend application template", "repository": { "type": "git", - "url": "git+https://github.com/openedx/frontend-template-application.git" + "url": "git+https://github.com/open-craft/frontend-app-learning-path.git" }, "browserslist": [ "extends @edx/browserslist-config" @@ -15,7 +15,7 @@ "lint": "fedx-scripts eslint --ext .js --ext .jsx .", "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", "snapshot": "fedx-scripts jest --updateSnapshot", - "start": "fedx-scripts webpack-dev-server --progress", + "start": "fedx-scripts webpack-dev-server --host apps.local.edly.io --port 8080", "start:with-theme": "paragon install-theme && npm start && npm install", "test": "fedx-scripts jest --coverage --passWithNoTests" }, @@ -26,17 +26,18 @@ }, "author": "edX", "license": "AGPL-3.0", - "homepage": "https://github.com/openedx/frontend-template-application#readme", + "homepage": "https://github.com/open-craft/frontend-app-learning-path#readme", "publishConfig": { "access": "public" }, "bugs": { - "url": "https://github.com/openedx/frontend-template-application/issues" + "url": "https://github.com/open-craft/frontend-app-learning-path/issues" }, "dependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-header": "^5.6.0", "@edx/frontend-platform": "^8.0.0", + "@edx/paragon": "^21.5.6", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.15.4", @@ -44,6 +45,7 @@ "@fortawesome/react-fontawesome": "0.2.2", "@openedx/frontend-slot-footer": "^1.0.2", "@openedx/paragon": "^22.0.0", + "@reduxjs/toolkit": "^2.6.1", "core-js": "3.40.0", "prop-types": "15.8.1", "react": "17.0.2", diff --git a/src/example/ExamplePage.jsx b/src/example/ExamplePage.jsx deleted file mode 100644 index 5f36f95..0000000 --- a/src/example/ExamplePage.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Container } from '@openedx/paragon'; - -const ExamplePage = () => ( -
- -

Example Page

-

Hello world!

-
-
-); - -export default ExamplePage; diff --git a/src/example/index.scss b/src/example/index.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/index.jsx b/src/index.jsx index 4c3b0a7..76a6417 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -6,19 +6,31 @@ import { } from '@edx/frontend-platform'; import { AppProvider, ErrorPage } from '@edx/frontend-platform/react'; import ReactDOM from 'react-dom'; +import { Routes, Route } from 'react-router-dom'; import Header from '@edx/frontend-component-header'; import FooterSlot from '@openedx/frontend-slot-footer'; import messages from './i18n'; -import ExamplePage from './example/ExamplePage'; +import store from './store'; +import LearningPathList from './learningpath/LearningPathList'; +import LearningPathDetailPage from './learningpath/LearningPathDetails'; import './index.scss'; subscribe(APP_READY, () => { ReactDOM.render( - +
- + + } + /> + } + /> + , document.getElementById('root'), diff --git a/src/index.scss b/src/index.scss index ff645de..b26d3ca 100644 --- a/src/index.scss +++ b/src/index.scss @@ -3,7 +3,7 @@ @import "@openedx/paragon/scss/core/core.scss"; @import "@edx/brand/paragon/overrides.scss"; -@import './example/index.scss'; +@import './learningpath/index.scss'; @import "~@edx/frontend-component-header/dist/index"; @import "~@edx/frontend-component-footer/dist/footer"; diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx new file mode 100644 index 0000000..189720f --- /dev/null +++ b/src/learningpath/CourseCard.jsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { Card, Button, Row, Col, Icon, Badge } from '@edx/paragon'; +import { buildAssetUrl } from '../util/assetUrl'; +import { + LmsBook, + AccessTime, + CheckCircle, + LmsCompletionSolid, + Timelapse, +} from '@openedx/paragon/icons'; + +const CourseCard = ({ course }) => { + const { + name, + org, + course_image_asset_path, + end_date, + status, + } = course; + + let statusVariant = 'dark'; // default + let statusIcon = 'fa-circle'; // default icon + switch (status?.toLowerCase()) { + case 'completed': + statusVariant = 'success'; + statusIcon = CheckCircle; + break; + case 'not started': + statusVariant = 'secondary'; + statusIcon = LmsCompletionSolid; + break; + case 'in progress': + statusVariant = 'info'; + statusIcon = Timelapse; + break; + default: + // fallback if unknown + statusVariant = 'dark'; + statusIcon = 'fa-circle'; + break; + } + const endDateFormatted = end_date + ? new Date(end_date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }) + : null; + return ( + +
+ + + {status} + +
+ + + {course_image_asset_path && ( + + )} + + +
+ + Course +
+ + {org &&

{org}

} + {endDateFormatted && ( +
+ + Access until {endDateFormatted} +
+ )} + + + + +
+
+ ); +}; + +export default CourseCard; \ No newline at end of file diff --git a/src/example/ExamplePage.test.jsx b/src/learningpath/ExamplePage.test.jsx similarity index 100% rename from src/example/ExamplePage.test.jsx rename to src/learningpath/ExamplePage.test.jsx diff --git a/src/learningpath/LearningPathCard.jsx b/src/learningpath/LearningPathCard.jsx new file mode 100644 index 0000000..c92fa7b --- /dev/null +++ b/src/learningpath/LearningPathCard.jsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + Card, + Button, + Row, + Col, + Badge, + Icon, +} from '@edx/paragon'; +import { + LmsCompletionSolid, + CheckCircle, + Timelapse, + FormatListBulleted, + AccessTime, +} from '@openedx/paragon/icons'; +import { buildAssetUrl } from '../util/assetUrl'; + + +const LearningPathCard = ({ learningPath }) => { + const { + uuid, + image_url, + display_name, + subtitle, + duration, + num_courses, + status, + } = learningPath; + + let statusVariant = 'dark'; // default + let statusIcon = 'fa-circle'; // default icon + switch (status?.toLowerCase()) { + case 'completed': + statusVariant = 'success'; + statusIcon = CheckCircle; + break; + case 'not started': + statusVariant = 'secondary'; + statusIcon = LmsCompletionSolid; + break; + case 'in progress': + statusVariant = 'info'; + statusIcon = Timelapse; + break; + default: + // fallback if unknown + statusVariant = 'dark'; + statusIcon = 'fa-circle'; + break; + } + + const subtitleLine = subtitle && duration + ? `${subtitle} • ${duration} days` + : subtitle || duration || ''; + + return ( + +
+ + + {status} + +
+ + + {image_url && ( + + )} + + +
+ + Learning Path +
+ + {subtitleLine && ( +

+ {subtitleLine} +

+ )} +
+ {num_courses && ( +
+ + {num_courses} courses +
+ )} + + {/* Access expiry */} + +
+ + Access until +
+
+ + + + + + +
+
+ ); +}; + +export default LearningPathCard; \ No newline at end of file diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx new file mode 100644 index 0000000..c651e1f --- /dev/null +++ b/src/learningpath/LearningPathDetails.jsx @@ -0,0 +1,199 @@ +// src/learningpath/LearningPathDetailPage.jsx +import React, { useEffect, useState, useMemo } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { Button, Row, Col, Spinner, Nav, Icon } from '@edx/paragon'; +import { buildAssetUrl } from '../util/assetUrl'; +import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; +import CourseCard from './CourseCard'; +import { + LmsCompletionSolid, + CheckCircle, + Timelapse, + FormatListBulleted, + AccessTime, +} from '@openedx/paragon/icons'; + +export default function LearningPathDetailPage() { + const { uuid } = useParams(); + const [detail, setDetail] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [coursesForPath, setCoursesForPath] = useState([]); + const [loadingCourses, setLoadingCourses] = useState(false); + const [coursesError, setCoursesError] = useState(null); + + useEffect(() => { + async function loadDetail() { + try { + setLoading(true); + const data = await fetchLearningPathDetail(uuid); + setDetail(data); + } catch (err) { + console.error('Failed to fetch learning path detail:', err); + setError(err.message); + } finally { + setLoading(false); + } + } + loadDetail(); + }, [uuid]); + + const courseIds = useMemo(() => { + return detail && detail.steps ? detail.steps.map(step => step.course_key) : []; + }, [detail]); + + useEffect(() => { + if (courseIds.length === 0) return; + async function loadCourses() { + try { + setLoadingCourses(true); + setCoursesError(null); + const courses = await fetchCoursesByIds(courseIds); + setCoursesForPath(courses); + } catch (err) { + console.error('Failed to fetch courses:', err); + setCoursesError(err.message); + } finally { + setLoadingCourses(false); + } + } + loadCourses(); + }, [courseIds]); + + let content; + if (loading) { + content = ; + } else if (error) { + content = ( +
+

Failed to load detail

+ Explore +
+ ); + } else { + console.log("checking item"); + console.log(detail); + + const { + display_name, + image_url, + subtitle, + duration_in_days, + required_skills, + description, + } = detail; + + const durationText = duration_in_days ? `${duration_in_days} days` : null; + const handleTabSelect = (selectedKey) => { + const el = document.getElementById(selectedKey); + if (el) { + el.scrollIntoView({ behavior: 'smooth' }); + } + }; + content = ( +
+
+
+ + Explore + +
+ + +
+ + Learning Path +
+

{display_name}

+ {subtitle && ( +

+ {subtitle} +

+ )} + + + {image_url && ( + {display_name} + )} + +
+ + +

+ {'February 28, 2024'} +

+

Access ends

+ + +

Earn a certificate

+

Some subtext

+ + +

+ {durationText || '6 months'} +

+

Duration

+ + +

Self-paced

+

Some subtext

+ +
+
+
+ +
+
+
+

About

+

+ {description || ''} +

+
+
+

Courses

+ {loadingCourses && } + {!loadingCourses && !coursesError && coursesForPath.length === 0 && ( +

No sub-courses found in this learning path.

+ )} + {!loadingCourses && !coursesError && coursesForPath.length > 0 && ( + coursesForPath.map(course => ( +
+ +
+ )) + )} +
+
+

Requirements

+ {required_skills.map(skill => ( +

+ {skill} +

+ ))} +
+
+
+ ); + } + + return content; +} diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx new file mode 100644 index 0000000..2459d37 --- /dev/null +++ b/src/learningpath/LearningPathList.jsx @@ -0,0 +1,52 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Spinner, Alert, Row, Col } from '@edx/paragon'; +import { fetchLearningPathways, fetchCourses } from './data/thunks'; +import LearningPathCard from './LearningPathCard'; +import CourseCard from './CourseCard'; + +export default function LearningPathList() { + const dispatch = useDispatch(); + const { + fetching: lpFetching, + learningPathways, + errors: lpErrors, + } = useSelector(state => state.learningPath); + const { + fetching: coursesFetching, + courses, + error: coursesErrors, + } = useSelector(state => state.courses); + + useEffect(() => { + dispatch(fetchLearningPathways()); + dispatch(fetchCourses()); + }, [dispatch]); + + if (lpFetching || coursesFetching) { + return ; + } + + const allErrors = [].concat(lpErrors || [], coursesErrors || []); + if (allErrors.length > 0) { + console.error('Error loading learning pathways:', allErrors); + } + + return ( +
+

Learning Pathways

+ + {learningPathways.map(path => ( + + + + ))} + {courses.map(course => ( + + + + ))} + +
+ ); +} diff --git a/src/example/data/.gitkeep b/src/learningpath/data/.gitkeep similarity index 100% rename from src/example/data/.gitkeep rename to src/learningpath/data/.gitkeep diff --git a/src/example/data/README.rst b/src/learningpath/data/README.rst similarity index 100% rename from src/example/data/README.rst rename to src/learningpath/data/README.rst diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js new file mode 100644 index 0000000..157a81e --- /dev/null +++ b/src/learningpath/data/api.js @@ -0,0 +1,105 @@ +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { logInfo } from '@edx/frontend-platform/logging'; + + +export async function fetchLearningPaths() { + const client = getAuthenticatedHttpClient(); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/v1/learning-paths/`); + return response.data.results || response.data; +} + +export async function fetchLearningPathDetail(uuid) { + const client = getAuthenticatedHttpClient(); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/v1/learning-paths/${uuid}/`); + return response.data; +} + +export async function fetchLearningPathProgress(uuid) { + const client = getAuthenticatedHttpClient(); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/v1/learning-paths/${uuid}/progress/`); + return response.data; +} + +export async function fetchCourses(courseId) { + const client = getAuthenticatedHttpClient(); + let url; + if (courseId) { + url = `${process.env.LMS_BASE_URL}/api/courses/v1/courses/${encodeURIComponent(courseId)}/`; + } else { + url = `${process.env.LMS_BASE_URL}/api/courses/v1/courses/`; + } + const response = await client.get(url); + if (courseId) { + const course = response.data; + return { + course_id: course.course_id, + name: course.name, + }; + } + return (response.data.results || []).map(course => ({ + course_id: course.course_id, + name: course.name, + })); +} + +export async function fetchCourseDetails(courseId) { + const client = getAuthenticatedHttpClient(); + const response = await client.get( + `${process.env.CMS_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}` + ); + return response.data; +} + +export async function fetchAllCourseDetails() { + const courses = await fetchCourses(); + const details = await Promise.all( + courses.map(course => + fetchCourseDetails(course.course_id).then(detail => ({ + ...detail, + name: course.name, + })) + ) + ); + return details; +} + +export async function fetchCourseCompletion(courseId) { + const client = getAuthenticatedHttpClient(); + const response = await client.get( + `${process.env.LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/` + ); + if (response.data.results && response.data.results.length > 0) { + return response.data.results[0].completion.percent; + } + return 0.0; +} + +export async function fetchCombinedCourseInfo(courseId) { + const basicInfo = await fetchCourses(courseId); + const details = await fetchCourseDetails(courseId); + return { + ...basicInfo, + ...details, + }; +} + +export async function fetchCoursesByIds(courseIds) { + const combined = await Promise.all( + courseIds.map(async (courseId) => { + const combinedInfo = await fetchCombinedCourseInfo(courseId); + const percent = await fetchCourseCompletion(courseId); + let status = 'In progress'; + if (percent === 0.0) { + status = 'Not started'; + } else if (percent === 100.0) { + status = 'Completed'; + } + return { + ...combinedInfo, + status, + }; + }) + ); + return combined; +} diff --git a/src/learningpath/data/slice.js b/src/learningpath/data/slice.js new file mode 100644 index 0000000..3d12364 --- /dev/null +++ b/src/learningpath/data/slice.js @@ -0,0 +1,71 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialLearningPathState = () => ({ + fetching: false, + learningPathways: [], + errors: [], + detailFetching: false, + detailError: null, + detailItem: null, +}); + +const initialCoursesState = () => ({ + fetching: false, + courses: [], + errors: [], +}); + +const coursesSlice = createSlice({ + name: 'courses', + initialState: initialCoursesState(), + reducers: { + fetchCoursesRequest(state) { + state.fetching = true; + state.errors = []; + state.courses = []; + }, + fetchCoursesSuccess(state, action) { + state.fetching = false; + state.courses = action.payload.courses; + }, + fetchCoursesFailure(state, action) { + state.fetching = false; + state.errors = action.payload.errors; + }, + }, +}); + +const learningPathSlice = createSlice({ + name: 'learningPath', + initialState: initialLearningPathState(), + reducers: { + fetchLearningPathwaysRequest(state) { + state.fetching = true; + state.errors = []; + state.learningPathways = []; + }, + fetchLearningPathwaysSuccess(state, action) { + state.fetching = false; + state.learningPathways = action.payload.pathways; + }, + fetchLearningPathwaysFailure(state, action) { + state.fetching = false; + state.errors = action.payload.errors; + }, + }, +}); + +export const { + fetchLearningPathwaysRequest, + fetchLearningPathwaysSuccess, + fetchLearningPathwaysFailure, +} = learningPathSlice.actions; + +export const { + fetchCoursesRequest, + fetchCoursesSuccess, + fetchCoursesFailure, +} = coursesSlice.actions; + +export const learningPathReducer = learningPathSlice.reducer; +export const coursesReducer = coursesSlice.reducer; diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js new file mode 100644 index 0000000..dde05e2 --- /dev/null +++ b/src/learningpath/data/thunks.js @@ -0,0 +1,65 @@ +import * as api from './api'; +import { + fetchLearningPathwaysRequest, + fetchLearningPathwaysSuccess, + fetchLearningPathwaysFailure, + fetchCoursesRequest, + fetchCoursesSuccess, + fetchCoursesFailure, +} from './slice'; + +export const fetchLearningPathways = () => async (dispatch) => { + try { + dispatch(fetchLearningPathwaysRequest()); + const pathwaylist = await api.fetchLearningPaths(); + const pathways = await Promise.all( + pathwaylist.map(async (lp) => { + const lpdetail = await api.fetchLearningPathDetail(lp.uuid); + const lpprogress = await api.fetchLearningPathProgress(lp.uuid); + let status = "In Progress"; + if (lpprogress.progress == 0.0){ + status = "Not started"; + } else if (lpprogress.progress >= lpprogress.required_completion) { + status = "Completed"; + } + return { + ...lp, + ...lpdetail, + status, + }; + }) + ); + dispatch(fetchLearningPathwaysSuccess({ pathways })); + } catch (error) { + console.error('Failed to fetch learning pathways:', error); + dispatch(fetchLearningPathwaysFailure({ errors: [String(error)] })); + } +}; + +export const fetchCourses = () => async (dispatch) => { + try { + dispatch(fetchCoursesRequest()); + const courses = await api.fetchAllCourseDetails(); + const coursesWithStatus = await Promise.all( + courses.map(async (course) => { + const course_key = "course-v1:"+ course.org + "+" + course.course_id + "+" + course.run + const percent = await api.fetchCourseCompletion(course_key); + let status = 'In progress'; + if (percent === 0.0) { + status = 'Not started'; + } else if (percent === 100.0) { + status = 'Completed'; + } + return { + ...course, + status, + }; + }) + ); + dispatch(fetchCoursesSuccess({ courses: coursesWithStatus })); + } catch (error) { + console.error('Failed to fetch courses:', error); + dispatch(fetchCoursesFailure({ errors: [String(error)] })); + } +}; + diff --git a/src/learningpath/index.scss b/src/learningpath/index.scss new file mode 100644 index 0000000..ba864e2 --- /dev/null +++ b/src/learningpath/index.scss @@ -0,0 +1,167 @@ +@import '@edx/paragon/scss/core/core'; // Import Paragon base styles (if not globally imported) + +// Custom styles for the learning pathways list +.learningpath-list { + margin: 2rem; // add some outer margin/padding as needed +} +.learningpath-list .list-group-item { + // Optionally adjust item styles (font size, hover, etc.) + font-size: 1rem; +} + +.learning-path-card { + border: 1px solid #ddd; + border-radius: 4px; + + .lp-image-col { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1rem; + + @media (min-width: 768px) { + margin-bottom: 0; + } + } + + .lp-image { + width: 100%; + height: auto; + object-fit: cover; + max-height: 180px; + border-radius: 4px; + } + + .lp-type-label { + font-size: 0.75rem; + font-weight: 600; + } + + .lp-meta { + font-size: 0.9rem; + color: #555; + } +} + +.lp-status-badge { + position: absolute; + top: 0.5rem; + right: 0.5rem; +} + +.learning-path-detail-page { + // The overall container + font-family: "Open Sans", sans-serif; // or whichever font your mockup uses + margin: 3rem; + + .hero-section { + padding: 2rem; // Some spacing around the hero + + // Explore link + .explore-link { + font-weight: 600; + margin-bottom: 1rem; + display: inline-block; + } + + .hero-content { + display: flex; + flex-wrap: wrap; // Support responsiveness + margin-bottom: 2rem; // Space below the row of heading + image + + .hero-text { + flex: 1 1 0; + min-width: 300px; // Ensures it doesn't collapse too much + margin-right: 1rem; + + h1 { + font-size: 2rem; + margin-bottom: 1rem; + } + + .subtitle { + color: #666; + font-size: 1rem; + max-width: 80%; + margin-bottom: 1.5rem; + } + + .hero-button { + margin-bottom: 1rem; + } + } + + .hero-image { + flex: 0 0 300px; + margin-left: auto; + display: flex; + align-items: center; + justify-content: center; + + img { + max-height: 250px; + width: 100%; + object-fit: cover; + border-radius: 4px; + } + } + } + + .hero-info-row { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + .info-col { + flex: 0 0 calc(25% - 1rem); // 4 columns with some spacing + margin-bottom: 1rem; + h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.25rem; + } + .info-subtext { + color: #666; + font-size: 0.875rem; + } + } + } + } + + // The tab bar + .lp-tabs { + background-color: #f2f0ef; + border-bottom: 1px solid #ddd; + padding: 0 2rem; // horizontal padding + display: flex; + align-items: center; + + .nav { + // remove default bottom border on the nav + border-bottom: 0; + + .nav-link { + font-weight: 600; + padding: 1rem 1.5rem; + } + } + + .tab-button { + margin-left: auto; + } + } + + // The main content area + .lp-info { + margin-top: 3rem; + margin-left: 10rem; + margin-right: 10rem; + section { + margin-bottom: 2rem; + h2 { + font-size: 1.5rem; + margin-bottom: 1rem; + } + } + } +} diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..7b43333 --- /dev/null +++ b/src/store.js @@ -0,0 +1,11 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { learningPathReducer, coursesReducer } from './learningpath/data/slice'; + +const store = configureStore({ + reducer: { + learningPath: learningPathReducer, + courses: coursesReducer, + }, +}); + +export default store; diff --git a/src/util/assetUrl.js b/src/util/assetUrl.js new file mode 100644 index 0000000..7ad0c17 --- /dev/null +++ b/src/util/assetUrl.js @@ -0,0 +1,6 @@ +import { getConfig } from '@edx/frontend-platform'; + +export function buildAssetUrl(assetPath) { + const base = getConfig().LMS_BASE_URL || ''; + return `${base.replace(/\/+$/, '')}/${assetPath.replace(/^\/+/, '')}`; +} From 10594f51ad57577dbd9afddedb28d901a65a2630 Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Mon, 24 Mar 2025 15:35:54 -0400 Subject: [PATCH 02/45] fix: update as per design --- src/learningpath/CourseCard.jsx | 2 +- src/learningpath/LearningPathCard.jsx | 43 ++++++++------- src/learningpath/LearningPathDetails.jsx | 68 +++++++++++++++++------- src/learningpath/LearningPathList.jsx | 2 +- src/learningpath/data/slice.js | 3 -- src/learningpath/data/thunks.js | 16 ++++++ src/learningpath/index.scss | 32 ++++++++--- 7 files changed, 118 insertions(+), 48 deletions(-) diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index 189720f..a43c676 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -65,7 +65,7 @@ const CourseCard = ({ course }) => { )} -
+
Course
diff --git a/src/learningpath/LearningPathCard.jsx b/src/learningpath/LearningPathCard.jsx index c92fa7b..36e2eec 100644 --- a/src/learningpath/LearningPathCard.jsx +++ b/src/learningpath/LearningPathCard.jsx @@ -27,10 +27,11 @@ const LearningPathCard = ({ learningPath }) => { duration, num_courses, status, + maxDate, } = learningPath; - let statusVariant = 'dark'; // default - let statusIcon = 'fa-circle'; // default icon + let statusVariant = 'dark'; + let statusIcon = 'fa-circle'; switch (status?.toLowerCase()) { case 'completed': statusVariant = 'success'; @@ -45,12 +46,17 @@ const LearningPathCard = ({ learningPath }) => { statusIcon = Timelapse; break; default: - // fallback if unknown statusVariant = 'dark'; statusIcon = 'fa-circle'; break; } + const currentDate = new Date(); + const accessDateObj = new Date(maxDate); + const accessText = currentDate > accessDateObj + ? "Access ended" + : `Access until ${accessDateObj.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`; + const subtitleLine = subtitle && duration ? `${subtitle} • ${duration} days` : subtitle || duration || ''; @@ -74,7 +80,7 @@ const LearningPathCard = ({ learningPath }) => { )} -
+
Learning Path
@@ -84,22 +90,21 @@ const LearningPathCard = ({ learningPath }) => { {subtitleLine}

)} -
- {num_courses && ( -
- - {num_courses} courses -
- )} - - {/* Access expiry */} - -
- - Access until + +
+ {num_courses && ( +
+ + {num_courses} courses +
+ )} + {maxDate && ( +
+ + {accessText} +
+ )}
-
- diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index c651e1f..406d72c 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -6,11 +6,11 @@ import { buildAssetUrl } from '../util/assetUrl'; import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; import CourseCard from './CourseCard'; import { - LmsCompletionSolid, - CheckCircle, - Timelapse, + Person, + Award, + Calendar, FormatListBulleted, - AccessTime, + AccessTimeFilled, } from '@openedx/paragon/icons'; export default function LearningPathDetailPage() { @@ -60,6 +60,19 @@ export default function LearningPathDetailPage() { loadCourses(); }, [courseIds]); + const accessUntilDate = useMemo(() => { + let maxDate = null; + for (const c of coursesForPath) { + if (c.end_date) { + const endDateObj = new Date(c.end_date); + if (!maxDate || endDateObj > maxDate) { + maxDate = endDateObj; + } + } + } + return maxDate; + }, [coursesForPath]); + let content; if (loading) { content = ; @@ -71,9 +84,6 @@ export default function LearningPathDetailPage() {
); } else { - console.log("checking item"); - console.log(detail); - const { display_name, image_url, @@ -100,7 +110,7 @@ export default function LearningPathDetailPage() {
-
+
Learning Path
@@ -122,26 +132,48 @@ export default function LearningPathDetailPage() { + {accessUntilDate && ( -

- {'February 28, 2024'} -

-

Access ends

+
+ +
+

+ {accessUntilDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} +

+

Access ends

+
+
- + )} + +
+ +

Earn a certificate

Some subtext

- - +
+
+ + +
+ +

{durationText || '6 months'}

Duration

- - +
+
+ + +
+ +

Self-paced

Some subtext

- +
+
+
diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx index 2459d37..91aa267 100644 --- a/src/learningpath/LearningPathList.jsx +++ b/src/learningpath/LearningPathList.jsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Spinner, Alert, Row, Col } from '@edx/paragon'; +import { Spinner, Row, Col } from '@edx/paragon'; import { fetchLearningPathways, fetchCourses } from './data/thunks'; import LearningPathCard from './LearningPathCard'; import CourseCard from './CourseCard'; diff --git a/src/learningpath/data/slice.js b/src/learningpath/data/slice.js index 3d12364..00ffdb1 100644 --- a/src/learningpath/data/slice.js +++ b/src/learningpath/data/slice.js @@ -4,9 +4,6 @@ const initialLearningPathState = () => ({ fetching: false, learningPathways: [], errors: [], - detailFetching: false, - detailError: null, - detailItem: null, }); const initialCoursesState = () => ({ diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index dde05e2..c454985 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -22,10 +22,26 @@ export const fetchLearningPathways = () => async (dispatch) => { } else if (lpprogress.progress >= lpprogress.required_completion) { status = "Completed"; } + let maxDate = null; + const num_courses = lpdetail.steps.length; + for (const course of lpdetail.steps) { + if (course.due_date) { + const dueDateObj = new Date(course.due_date); + if (!maxDate || dueDateObj > maxDate) { + maxDate = dueDateObj; + } + } + } + // Convert maxDate to an ISO string for serialization + if (maxDate) { + maxDate = maxDate.toISOString(); + } return { ...lp, ...lpdetail, + num_courses, status, + maxDate, }; }) ); diff --git a/src/learningpath/index.scss b/src/learningpath/index.scss index ba864e2..f844c20 100644 --- a/src/learningpath/index.scss +++ b/src/learningpath/index.scss @@ -32,23 +32,44 @@ border-radius: 4px; } - .lp-type-label { - font-size: 0.75rem; - font-weight: 600; - } - .lp-meta { font-size: 0.9rem; color: #555; } } +.lp-type-label { + background-color: #821122; + border-radius: 5px; + display: inline-flex; + color: white; + padding-left: 2px; + padding-right: 5px; + align-items: center; + font-size: 0.75rem; + font-weight: 600; +} + .lp-status-badge { position: absolute; top: 0.5rem; right: 0.5rem; } +.course-type-label { + background-color: #8C8179; + border-radius: 5px; + display: inline-flex; + color: white; + padding-left: 2px; + padding-right: 5px; + align-items: center; + font-size: 0.85rem; + font-weight: 600; + padding-top: 2px; + padding-bottom: 2px; +} + .learning-path-detail-page { // The overall container font-family: "Open Sans", sans-serif; // or whichever font your mockup uses @@ -151,7 +172,6 @@ } } - // The main content area .lp-info { margin-top: 3rem; margin-left: 10rem; From b35d22e3cb57a6dab3b52d3979a61a3505848dbe Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Tue, 25 Mar 2025 16:16:21 -0400 Subject: [PATCH 03/45] feat: Add course details page --- src/index.jsx | 5 + src/learningpath/CourseCard.jsx | 8 +- src/learningpath/CourseDetails.jsx | 233 +++++++++++++++++++++++++++++ src/learningpath/index.scss | 119 ++++++++++----- 4 files changed, 326 insertions(+), 39 deletions(-) create mode 100644 src/learningpath/CourseDetails.jsx diff --git a/src/index.jsx b/src/index.jsx index 76a6417..6f224ae 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -14,6 +14,7 @@ import messages from './i18n'; import store from './store'; import LearningPathList from './learningpath/LearningPathList'; import LearningPathDetailPage from './learningpath/LearningPathDetails'; +import CourseDetailPage from './learningpath/CourseDetails'; import './index.scss'; @@ -30,6 +31,10 @@ subscribe(APP_READY, () => { path="/learningpath/:uuid" element={} /> + } + /> , diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index a43c676..4a47740 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import { Card, Button, Row, Col, Icon, Badge } from '@edx/paragon'; import { buildAssetUrl } from '../util/assetUrl'; import { @@ -10,6 +11,7 @@ import { } from '@openedx/paragon/icons'; const CourseCard = ({ course }) => { + const course_key = "course-v1:"+ course.org + "+" + course.course_id + "+" + course.run const { name, org, @@ -78,9 +80,9 @@ const CourseCard = ({ course }) => {
)} - + + +
diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx new file mode 100644 index 0000000..8a9885b --- /dev/null +++ b/src/learningpath/CourseDetails.jsx @@ -0,0 +1,233 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { + Spinner, + Card, + Row, + Col, + Nav, + Button, + Icon, + Modal +} from '@edx/paragon'; +import { fetchCoursesByIds } from './data/api'; +import { + LmsBook, + AccessTimeFilled, + Award, + Calendar, + Person, +} from '@openedx/paragon/icons'; +import { buildAssetUrl } from '../util/assetUrl'; + +export default function CourseDetailPage() { + const { courseKey } = useParams(); + const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function loadCourse() { + try { + setLoading(true); + const data = await fetchCoursesByIds([courseKey]); + setCourse(data[0]); + } catch (err) { + console.error('Failed to load course detail:', err); + setError(err.message); + } finally { + setLoading(false); + } + } + loadCourse(); + }, [courseKey]); + + if (loading) { + return ; + } + + if (error) { + return ( +
+ Failed to load course detail: {error} + Back +
+ ); + } + + if (!course) { + return ; + } + + return ( +
+ +
+ ); +} + +function CourseDetailContent({course}) { + const { + name, + short_description, + end_date, + duration, + self_paced, + course_image_asset_path, + description, + learning_info, + instructor_info, + } = course; + + const dateDisplay = end_date + ? new Date(end_date).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + }) + : null; + + const handleTabSelect = (selectedKey) => { + const el = document.getElementById(selectedKey); + if (el) { + el.scrollIntoView({ behavior: 'smooth' }); + } + }; + + return ( + <> +
+
+ Explore +
+ + +
+ + Course +
+

{name}

+ {short_description && ( +

{short_description}

+ )} + + + {course_image_asset_path && ( + + )} + +
+ + {dateDisplay && ( + +
+ +
+

{dateDisplay}

+

Access ends

+
+
+ + )} + +
+ +
+

Earn a certificate

+

Courses include certification

+
+
+ + {duration && ( + +
+ +
+

{duration}

+

Approx. duration

+
+
+ + )} + {self_paced == true && ( + +
+ +
+

Self-paced

+

Learn at your own speed

+
+
+ + )} +
+
+ +
+ +
+ +
+
+

About

+

+ {description || short_description} +

+
+ +
+

What you'll learn

+ {learning_info.map((learning) => ( +

+ * {learning} +

+ ))} +
+ +
+

Instructors

+ + {instructor_info && instructor_info.instructors.length > 0 ? ( + instructor_info.instructors.map((inst, index) => ( + +
+ {inst.name} +

+ {inst.name} +

+ {inst.title &&

{inst.title}

} + {inst.organization && ( +

{inst.organization}

+ )} +
+ + )) + ) : ( + +

No instructors listed for this course.

+ + )} +
+
+
+ + ); +} diff --git a/src/learningpath/index.scss b/src/learningpath/index.scss index f844c20..68a42c4 100644 --- a/src/learningpath/index.scss +++ b/src/learningpath/index.scss @@ -1,11 +1,10 @@ -@import '@edx/paragon/scss/core/core'; // Import Paragon base styles (if not globally imported) +@import '@edx/paragon/scss/core/core'; + -// Custom styles for the learning pathways list .learningpath-list { - margin: 2rem; // add some outer margin/padding as needed + margin: 2rem; } .learningpath-list .list-group-item { - // Optionally adjust item styles (font size, hover, etc.) font-size: 1rem; } @@ -71,14 +70,12 @@ } .learning-path-detail-page { - // The overall container - font-family: "Open Sans", sans-serif; // or whichever font your mockup uses - margin: 3rem; + font-family: "Open Sans", sans-serif; + margin: 6rem; .hero-section { - padding: 2rem; // Some spacing around the hero + padding: 2rem; - // Explore link .explore-link { font-weight: 600; margin-bottom: 1rem; @@ -87,12 +84,12 @@ .hero-content { display: flex; - flex-wrap: wrap; // Support responsiveness - margin-bottom: 2rem; // Space below the row of heading + image + flex-wrap: wrap; + margin-bottom: 2rem; .hero-text { flex: 1 1 0; - min-width: 300px; // Ensures it doesn't collapse too much + min-width: 300px; margin-right: 1rem; h1 { @@ -134,7 +131,7 @@ justify-content: space-between; .info-col { - flex: 0 0 calc(25% - 1rem); // 4 columns with some spacing + flex: 0 0 calc(25% - 1rem); margin-bottom: 1rem; h3 { font-size: 1rem; @@ -149,29 +146,6 @@ } } - // The tab bar - .lp-tabs { - background-color: #f2f0ef; - border-bottom: 1px solid #ddd; - padding: 0 2rem; // horizontal padding - display: flex; - align-items: center; - - .nav { - // remove default bottom border on the nav - border-bottom: 0; - - .nav-link { - font-weight: 600; - padding: 1rem 1.5rem; - } - } - - .tab-button { - margin-left: auto; - } - } - .lp-info { margin-top: 3rem; margin-left: 10rem; @@ -185,3 +159,76 @@ } } } + +.lp-tabs { + background-color: #f2f0ef; + border-bottom: 1px solid #ddd; + padding: 0 2rem; + display: flex; + align-items: center; + + .nav { + border-bottom: 0; + + .nav-link { + font-weight: 600; + padding: 1rem 1.5rem; + } + } + + .tab-button { + margin-left: auto; + } +} + +.course-detail-page { + font-family: "Open Sans", sans-serif; + margin: 6rem; +} + +.course-card-image { + flex: 0 0 300px; + margin-left: auto; + display: flex; + align-items: center; + justify-content: center; + + img { + max-height: 250px; + width: 100%; + object-fit: cover; + border-radius: 4px; + } +} + +.instructor-card { + border: 1px solid #ddd; + border-radius: 4px; + padding: 1rem; + background-color: #fff; + text-align: center; + + .instructor-image { + width: 100%; + height: 180px; + object-fit: cover; + border-radius: 50%; + display: block; + margin: 0 auto; + } + .instructor-name { + font-size: 1.1rem; + font-weight: 600; + } + .instructor-title { + color: #555; + font-weight: 500; + } + .instructor-org { + color: #888; + margin-bottom: 0.5rem; + } + .instructor-bio { + font-size: 0.9rem; + } +} From 887e8b805252c9837a8569ce4fa06c6c195312ce Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Wed, 26 Mar 2025 16:27:20 -0400 Subject: [PATCH 04/45] fix: course overlay on learning path detail page --- src/index.jsx | 2 +- src/learningpath/CourseCard.jsx | 22 ++++++----- src/learningpath/CourseDetails.jsx | 50 +++++++++++++++++++----- src/learningpath/LearningPathDetails.jsx | 31 +++++++++++++-- src/learningpath/data/api.js | 2 - src/learningpath/index.scss | 10 +++++ 6 files changed, 92 insertions(+), 25 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index 6f224ae..b0d96bf 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -28,7 +28,7 @@ subscribe(APP_READY, () => { element={} /> } /> { +const CourseCard = ({ course, parentPath }) => { const course_key = "course-v1:"+ course.org + "+" + course.course_id + "+" + course.run const { name, @@ -19,6 +19,9 @@ const CourseCard = ({ course }) => { end_date, status, } = course; + const linkTo = parentPath + ? `${parentPath}/course/${encodeURIComponent(course_key)}` + : `/course/${encodeURIComponent(course_key)}`; let statusVariant = 'dark'; // default let statusIcon = 'fa-circle'; // default icon @@ -36,7 +39,6 @@ const CourseCard = ({ course }) => { statusIcon = Timelapse; break; default: - // fallback if unknown statusVariant = 'dark'; statusIcon = 'fa-circle'; break; @@ -73,14 +75,16 @@ const CourseCard = ({ course }) => {
{org &&

{org}

} - {endDateFormatted && ( -
- - Access until {endDateFormatted} + +
+ {endDateFormatted && ( +
+ + Access until {endDateFormatted} +
+ )}
- )} - - + diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index 8a9885b..8ad6c1d 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -1,14 +1,15 @@ import React, { useEffect, useState } from 'react'; -import { useParams, Link } from 'react-router-dom'; +import { useParams, Link, useNavigate } from 'react-router-dom'; +import { getConfig } from '@edx/frontend-platform'; import { Spinner, Card, Row, Col, Nav, - Button, Icon, - Modal + ModalCloseButton, + Button } from '@edx/paragon'; import { fetchCoursesByIds } from './data/api'; import { @@ -17,10 +18,11 @@ import { Award, Calendar, Person, + Close, } from '@openedx/paragon/icons'; import { buildAssetUrl } from '../util/assetUrl'; -export default function CourseDetailPage() { +export default function CourseDetailPage({ isModalView = false, onClose }) { const { courseKey } = useParams(); const [course, setCourse] = useState(null); const [loading, setLoading] = useState(true); @@ -61,12 +63,16 @@ export default function CourseDetailPage() { return (
- + {isModalView ? ( + + ) : ( + + )}
); } -function CourseDetailContent({course}) { +function CourseDetailContent({course, isModalView, onClose}) { const { name, short_description, @@ -93,13 +99,32 @@ function CourseDetailContent({course}) { el.scrollIntoView({ behavior: 'smooth' }); } }; + const navigate = useNavigate(); + const handleClose = onClose || (() => navigate(-1)); + const { courseKey } = useParams(); + const learningMfeBase = getConfig().LEARNING_BASE_URL; + const buildCourseHomeUrl = (key) => { + return `${learningMfeBase}/learning/course/${key}/home`; + }; + const handleViewClick = () => { + window.location.href = buildCourseHomeUrl(courseKey); + }; return ( <>
-
- Explore -
+ {!isModalView && ( +
+ Explore +
+ )} + {isModalView && ( +
+ + + +
+ )}
@@ -179,6 +204,13 @@ function CourseDetailContent({course}) { Instructors +
diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index 406d72c..350e37e 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -1,10 +1,11 @@ // src/learningpath/LearningPathDetailPage.jsx import React, { useEffect, useState, useMemo } from 'react'; -import { useParams, Link } from 'react-router-dom'; -import { Button, Row, Col, Spinner, Nav, Icon } from '@edx/paragon'; +import { useParams, Link, useNavigate, Routes, Route } from 'react-router-dom'; +import { Button, Row, Col, Spinner, Nav, Icon, ModalLayer } from '@edx/paragon'; import { buildAssetUrl } from '../util/assetUrl'; import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; import CourseCard from './CourseCard'; +import CourseDetailPage from './CourseDetails'; import { Person, Award, @@ -15,6 +16,7 @@ import { export default function LearningPathDetailPage() { const { uuid } = useParams(); + const navigate = useNavigate(); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -209,7 +211,7 @@ export default function LearningPathDetailPage() { {!loadingCourses && !coursesError && coursesForPath.length > 0 && ( coursesForPath.map(course => (
- +
)) )} @@ -227,5 +229,26 @@ export default function LearningPathDetailPage() { ); } - return content; + return ( + <> + {content} + + navigate(`/learningpath/${uuid}`)} + className="lp-course-modal-layer" + > + navigate(`/learningpath/${uuid}`)} + /> + + } + /> + + + ); } diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index 157a81e..88d719f 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -1,6 +1,4 @@ -import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { logInfo } from '@edx/frontend-platform/logging'; export async function fetchLearningPaths() { diff --git a/src/learningpath/index.scss b/src/learningpath/index.scss index 68a42c4..166d35d 100644 --- a/src/learningpath/index.scss +++ b/src/learningpath/index.scss @@ -232,3 +232,13 @@ font-size: 0.9rem; } } + +.pgn__modal-content-container { + .course-detail-page { + background-color: white; + max-width: 90rem; + left: 14rem; + bottom: 6rem; + padding-top: 2rem; + } +} From 43dcb24d41b9042aab02ddff55690d21e321e258 Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Tue, 1 Apr 2025 10:03:09 +0100 Subject: [PATCH 05/45] fix: update repo name --- package-lock.json | 187 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 8 +- 2 files changed, 189 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d9542e..69857f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { - "name": "@edx/frontend-template-application", + "name": "@open-craft/frontend-app-learning-paths", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@edx/frontend-template-application", + "name": "@open-craft/frontend-app-learning-paths", "version": "0.1.0", "license": "AGPL-3.0", "dependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-header": "^5.6.0", "@edx/frontend-platform": "^8.0.0", + "@edx/paragon": "^21.5.6", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.15.4", @@ -19,6 +20,7 @@ "@fortawesome/react-fontawesome": "0.2.2", "@openedx/frontend-slot-footer": "^1.0.2", "@openedx/paragon": "^22.0.0", + "@reduxjs/toolkit": "^2.6.1", "core-js": "3.40.0", "prop-types": "15.8.1", "react": "17.0.2", @@ -2761,6 +2763,132 @@ "@newrelic/publish-sourcemap": "^5.0.1" } }, + "node_modules/@edx/paragon": { + "version": "21.5.6", + "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-21.5.6.tgz", + "integrity": "sha512-CWR9mFBQAnZ29GeP8igPk3dBLgIQmZJ6tZQiou6855TjHIXcvgmbIvtchKw9SgzhW+D5B0hQJet94zsm+GG/Rg==", + "license": "Apache-2.0", + "workspaces": [ + "example", + "component-generator", + "www", + "icons", + "dependent-usage-analyzer" + ], + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.1.1", + "@fortawesome/react-fontawesome": "^0.1.18", + "@popperjs/core": "^2.11.4", + "bootstrap": "^4.6.2", + "chalk": "^4.1.2", + "child_process": "^1.0.2", + "classnames": "^2.3.1", + "email-prop-type": "^3.0.0", + "file-selector": "^0.6.0", + "font-awesome": "^4.7.0", + "glob": "^8.0.3", + "inquirer": "^8.2.5", + "lodash.uniqby": "^4.7.0", + "mailto-link": "^2.0.0", + "prop-types": "^15.8.1", + "react-bootstrap": "^1.6.5", + "react-colorful": "^5.6.1", + "react-dropzone": "^14.2.1", + "react-focus-on": "^3.5.4", + "react-loading-skeleton": "^3.1.0", + "react-popper": "^2.2.5", + "react-proptype-conditional-require": "^1.0.4", + "react-responsive": "^8.2.0", + "react-table": "^7.7.0", + "react-transition-group": "^4.4.2", + "tabbable": "^5.3.3", + "uncontrollable": "^7.2.1", + "uuid": "^9.0.0" + }, + "bin": { + "paragon": "bin/paragon-scripts.js" + }, + "peerDependencies": { + "react": "^16.8.6 || ^17.0.0", + "react-dom": "^16.8.6 || ^17.0.0", + "react-intl": "^5.25.1 || ^6.4.0" + } + }, + "node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@edx/paragon/node_modules/@fortawesome/react-fontawesome": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", + "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.x" + } + }, + "node_modules/@edx/paragon/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@edx/paragon/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@edx/paragon/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@edx/reactifex": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz", @@ -4314,6 +4442,55 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.1.tgz", + "integrity": "sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@reduxjs/toolkit/node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit/node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", @@ -15103,6 +15280,12 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/package.json b/package.json index 0a20231..ecb5b6c 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@open-craft/frontend-app-learning-path", + "name": "@open-craft/frontend-app-learning-paths", "version": "0.1.0", "description": "Frontend application template", "repository": { "type": "git", - "url": "git+https://github.com/open-craft/frontend-app-learning-path.git" + "url": "git+https://github.com/open-craft/frontend-app-learning-paths.git" }, "browserslist": [ "extends @edx/browserslist-config" @@ -26,12 +26,12 @@ }, "author": "edX", "license": "AGPL-3.0", - "homepage": "https://github.com/open-craft/frontend-app-learning-path#readme", + "homepage": "https://github.com/open-craft/frontend-app-learning-paths#readme", "publishConfig": { "access": "public" }, "bugs": { - "url": "https://github.com/open-craft/frontend-app-learning-path/issues" + "url": "https://github.com/open-craft/frontend-app-learning-paths/issues" }, "dependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", From 7b0c4e05f3ac5e4446fc3a84ed66e4a1edd8b10e Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Tue, 1 Apr 2025 12:13:46 +0100 Subject: [PATCH 06/45] feat: Add progress bar to cards --- src/learningpath/CourseCard.jsx | 11 ++++++++++- src/learningpath/LearningPathCard.jsx | 12 +++++++++++- src/learningpath/data/thunks.js | 10 ++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index fd8cc71..5f7b750 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Card, Button, Row, Col, Icon, Badge } from '@edx/paragon'; +import { Card, Button, Row, Col, Icon, Badge, ProgressBar } from '@edx/paragon'; import { buildAssetUrl } from '../util/assetUrl'; import { LmsBook, @@ -18,6 +18,7 @@ const CourseCard = ({ course, parentPath }) => { course_image_asset_path, end_date, status, + percent, } = course; const linkTo = parentPath ? `${parentPath}/course/${encodeURIComponent(course_key)}` @@ -75,6 +76,14 @@ const CourseCard = ({ course, parentPath }) => {
{org &&

{org}

} + {status == 'In progress' && ( + + )}
{endDateFormatted && ( diff --git a/src/learningpath/LearningPathCard.jsx b/src/learningpath/LearningPathCard.jsx index 36e2eec..432421b 100644 --- a/src/learningpath/LearningPathCard.jsx +++ b/src/learningpath/LearningPathCard.jsx @@ -6,7 +6,8 @@ import { Row, Col, Badge, - Icon, + Icon, + ProgressBar } from '@edx/paragon'; import { LmsCompletionSolid, @@ -28,6 +29,7 @@ const LearningPathCard = ({ learningPath }) => { num_courses, status, maxDate, + percent, } = learningPath; let statusVariant = 'dark'; @@ -90,6 +92,14 @@ const LearningPathCard = ({ learningPath }) => { {subtitleLine}

)} + {status === 'In progress' && ( + + )}
{num_courses && ( diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index c454985..32552b8 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -22,6 +22,14 @@ export const fetchLearningPathways = () => async (dispatch) => { } else if (lpprogress.progress >= lpprogress.required_completion) { status = "Completed"; } + let percent = 0; + if (lpprogress.required_completion) { + percent = lpprogress.required_completion > 0 + ? Math.round((lpprogress.progress / lpprogress.required_completion) * 100) + : 0; + } else { + percent = lpprogress.percent; + } let maxDate = null; const num_courses = lpdetail.steps.length; for (const course of lpdetail.steps) { @@ -42,6 +50,7 @@ export const fetchLearningPathways = () => async (dispatch) => { num_courses, status, maxDate, + percent, }; }) ); @@ -69,6 +78,7 @@ export const fetchCourses = () => async (dispatch) => { return { ...course, status, + percent, }; }) ); From 0b43f563ec0cfc0309b04baa57b58f0e899860e5 Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Tue, 1 Apr 2025 16:12:13 +0100 Subject: [PATCH 07/45] feat: Add filtering --- src/learningpath/FilterPanel.jsx | 82 ++++++++++++++++++++++++ src/learningpath/LearningPathList.jsx | 90 +++++++++++++++++++++------ src/learningpath/data/thunks.js | 10 ++- src/learningpath/index.scss | 37 +++++++++++ 4 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 src/learningpath/FilterPanel.jsx diff --git a/src/learningpath/FilterPanel.jsx b/src/learningpath/FilterPanel.jsx new file mode 100644 index 0000000..d92e5f1 --- /dev/null +++ b/src/learningpath/FilterPanel.jsx @@ -0,0 +1,82 @@ +// src/learningpath/components/FilterPanel.js +import React from 'react'; +import { Button, ButtonGroup, Form, Icon } from '@edx/paragon'; +import { FilterList } from '@openedx/paragon/icons'; + +function FilterPanel({ + selectedContentType, + onSelectContentType, + selectedStatuses, + onChangeStatus, + onClose +}) { + return ( +
+
+

Filter

+ +
+ + {/* Content Type Tabs */} +
+ + + + + +
+ + {/* Status Checkboxes */} +
+
Status
+
+
+ onChangeStatus('In Progress', e.target.checked)} + > + In progress + + onChangeStatus('Not started', e.target.checked)} + > + Not started + + onChangeStatus('Completed', e.target.checked)} + > + Completed + +
+
+
+
+ ); +} + +export default FilterPanel; diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx index 91aa267..3cb43a5 100644 --- a/src/learningpath/LearningPathList.jsx +++ b/src/learningpath/LearningPathList.jsx @@ -1,9 +1,10 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Spinner, Row, Col } from '@edx/paragon'; +import { Spinner, Row, Col, Button } from '@edx/paragon'; import { fetchLearningPathways, fetchCourses } from './data/thunks'; import LearningPathCard from './LearningPathCard'; import CourseCard from './CourseCard'; +import FilterPanel from './FilterPanel'; export default function LearningPathList() { const dispatch = useDispatch(); @@ -23,30 +24,83 @@ export default function LearningPathList() { dispatch(fetchCourses()); }, [dispatch]); - if (lpFetching || coursesFetching) { - return ; - } + const isLoading = lpFetching || coursesFetching; const allErrors = [].concat(lpErrors || [], coursesErrors || []); if (allErrors.length > 0) { console.error('Error loading learning pathways:', allErrors); } + const items = useMemo(() => { + return [...courses, ...learningPathways]; + }, [courses, learningPathways]); + + const [showFilters, setShowFilters] = useState(false); + const [selectedContentType, setSelectedContentType] = useState('All'); + const [selectedStatuses, setSelectedStatuses] = useState([]); + + const handleStatusChange = (status, isChecked) => { + setSelectedStatuses(prev => { + if (isChecked) { + return [...prev, status]; + } else { + return prev.filter(s => s !== status); + } + }); + }; + + const filteredItems = useMemo(() => { + return items.filter(item => { + const typeMatch = + selectedContentType === 'All' || + (selectedContentType === 'course' && item.type === 'course') || + (selectedContentType === 'learning_path' && item.type === 'learning_path'); + const statusMatch = + selectedStatuses.length === 0 || selectedStatuses.includes(item.status); + return typeMatch && statusMatch; + }); + }, [items, selectedContentType, selectedStatuses]); + return (
-

Learning Pathways

- - {learningPathways.map(path => ( - - - - ))} - {courses.map(course => ( - - - - ))} - + {isLoading ? ( + + ) : ( + <> + {showFilters && ( +
+ setShowFilters(false)} + /> +
+ )} +
+

My Learning

+ {!showFilters && ( + + )} + + {filteredItems.map(item => + item.type == 'course' ? ( + + + + ) : ( + + + + ) + )} + +
+ + )}
); } diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index 32552b8..6a91957 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -11,6 +11,7 @@ import { export const fetchLearningPathways = () => async (dispatch) => { try { dispatch(fetchLearningPathwaysRequest()); + const type = "learning_path"; const pathwaylist = await api.fetchLearningPaths(); const pathways = await Promise.all( pathwaylist.map(async (lp) => { @@ -51,6 +52,7 @@ export const fetchLearningPathways = () => async (dispatch) => { status, maxDate, percent, + type, }; }) ); @@ -64,21 +66,23 @@ export const fetchLearningPathways = () => async (dispatch) => { export const fetchCourses = () => async (dispatch) => { try { dispatch(fetchCoursesRequest()); + const type = "course"; const courses = await api.fetchAllCourseDetails(); const coursesWithStatus = await Promise.all( courses.map(async (course) => { const course_key = "course-v1:"+ course.org + "+" + course.course_id + "+" + course.run const percent = await api.fetchCourseCompletion(course_key); - let status = 'In progress'; + let status = "In progress"; if (percent === 0.0) { - status = 'Not started'; + status = "Not started"; } else if (percent === 100.0) { - status = 'Completed'; + status = "Completed"; } return { ...course, status, percent, + type, }; }) ); diff --git a/src/learningpath/index.scss b/src/learningpath/index.scss index 166d35d..3090792 100644 --- a/src/learningpath/index.scss +++ b/src/learningpath/index.scss @@ -242,3 +242,40 @@ padding-top: 2rem; } } + +.main-content { + transition: margin-left 0.3s ease; + button { + margin-top: 1rem; + margin-bottom: 20px; + } +} +.main-content.shifted { + margin-left: 350px; +} + +.filter-panel.sidebar { + position: fixed; + top: 4rem; + left: -350px; + width: 350px; + height: 100vh; + background-color: #fff; + z-index: 999; + box-shadow: -2px 0 6px rgba(0,0,0,0.15); + transition: left 0.3s ease-in-out; + overflow-y: auto; +} + +.filter-panel.sidebar.open { + left: 0; +} + +.status-options { + display: flex; + flex-direction: column; + + .pgn__form-checkbox { + margin-bottom: 0.75rem; + } +} From 97b57bd72c56315bea2d950385ca61981802dfcd Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 14:06:17 +0200 Subject: [PATCH 08/45] fix: replace `edx/paragon` with `openedx/paragon`, add `Atlas` --- package-lock.json | 130 ++--------------------- package.json | 2 +- src/learningpath/CourseCard.jsx | 6 +- src/learningpath/CourseDetails.jsx | 6 +- src/learningpath/FilterPanel.jsx | 26 ++--- src/learningpath/LearningPathCard.jsx | 6 +- src/learningpath/LearningPathDetails.jsx | 2 +- src/learningpath/LearningPathList.jsx | 4 +- src/learningpath/index.scss | 2 +- 9 files changed, 33 insertions(+), 151 deletions(-) diff --git a/package-lock.json b/package-lock.json index 69857f7..d03aa21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-header": "^5.6.0", "@edx/frontend-platform": "^8.0.0", - "@edx/paragon": "^21.5.6", + "@edx/openedx-atlas": "^0.6.0", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.15.4", @@ -2763,130 +2763,12 @@ "@newrelic/publish-sourcemap": "^5.0.1" } }, - "node_modules/@edx/paragon": { - "version": "21.5.6", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-21.5.6.tgz", - "integrity": "sha512-CWR9mFBQAnZ29GeP8igPk3dBLgIQmZJ6tZQiou6855TjHIXcvgmbIvtchKw9SgzhW+D5B0hQJet94zsm+GG/Rg==", - "license": "Apache-2.0", - "workspaces": [ - "example", - "component-generator", - "www", - "icons", - "dependent-usage-analyzer" - ], - "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.1.1", - "@fortawesome/react-fontawesome": "^0.1.18", - "@popperjs/core": "^2.11.4", - "bootstrap": "^4.6.2", - "chalk": "^4.1.2", - "child_process": "^1.0.2", - "classnames": "^2.3.1", - "email-prop-type": "^3.0.0", - "file-selector": "^0.6.0", - "font-awesome": "^4.7.0", - "glob": "^8.0.3", - "inquirer": "^8.2.5", - "lodash.uniqby": "^4.7.0", - "mailto-link": "^2.0.0", - "prop-types": "^15.8.1", - "react-bootstrap": "^1.6.5", - "react-colorful": "^5.6.1", - "react-dropzone": "^14.2.1", - "react-focus-on": "^3.5.4", - "react-loading-skeleton": "^3.1.0", - "react-popper": "^2.2.5", - "react-proptype-conditional-require": "^1.0.4", - "react-responsive": "^8.2.0", - "react-table": "^7.7.0", - "react-transition-group": "^4.4.2", - "tabbable": "^5.3.3", - "uncontrollable": "^7.2.1", - "uuid": "^9.0.0" - }, + "node_modules/@edx/openedx-atlas": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.6.2.tgz", + "integrity": "sha512-28Q8vzJDMS4wUxdkbIUBQpzWJ3HTdMaGlaEhFjrVGfuZkh++1AG6Tn/7FMD88cegalYAkphu530VQCHEkMZQhw==", "bin": { - "paragon": "bin/paragon-scripts.js" - }, - "peerDependencies": { - "react": "^16.8.6 || ^17.0.0", - "react-dom": "^16.8.6 || ^17.0.0", - "react-intl": "^5.25.1 || ^6.4.0" - } - }, - "node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", - "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", - "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", - "license": "MIT", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.7.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@edx/paragon/node_modules/@fortawesome/react-fontawesome": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", - "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~1 || ~6", - "react": ">=16.x" - } - }, - "node_modules/@edx/paragon/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@edx/paragon/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@edx/paragon/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "atlas": "atlas" } }, "node_modules/@edx/reactifex": { diff --git a/package.json b/package.json index ecb5b6c..4929bc4 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-header": "^5.6.0", "@edx/frontend-platform": "^8.0.0", - "@edx/paragon": "^21.5.6", + "@edx/openedx-atlas": "^0.6.0", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.15.4", diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index 5f7b750..5987751 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Card, Button, Row, Col, Icon, Badge, ProgressBar } from '@edx/paragon'; +import { Card, Button, Row, Col, Icon, Badge, ProgressBar } from '@openedx/paragon'; import { buildAssetUrl } from '../util/assetUrl'; import { LmsBook, @@ -62,7 +62,7 @@ const CourseCard = ({ course, parentPath }) => { {course_image_asset_path && ( - { ); }; -export default CourseCard; \ No newline at end of file +export default CourseCard; diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index 8ad6c1d..d9bb1ed 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -10,7 +10,7 @@ import { Icon, ModalCloseButton, Button -} from '@edx/paragon'; +} from '@openedx/paragon'; import { fetchCoursesByIds } from './data/api'; import { LmsBook, @@ -92,7 +92,7 @@ function CourseDetailContent({course, isModalView, onClose}) { year: 'numeric', }) : null; - + const handleTabSelect = (selectedKey) => { const el = document.getElementById(selectedKey); if (el) { @@ -138,7 +138,7 @@ function CourseDetailContent({course, isModalView, onClose}) { {course_image_asset_path && ( - @@ -22,22 +22,22 @@ function FilterPanel({ {/* Content Type Tabs */}
- - - - + ); }; -export default LearningPathCard; \ No newline at end of file +export default LearningPathCard; diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index 350e37e..a55daa2 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -1,7 +1,7 @@ // src/learningpath/LearningPathDetailPage.jsx import React, { useEffect, useState, useMemo } from 'react'; import { useParams, Link, useNavigate, Routes, Route } from 'react-router-dom'; -import { Button, Row, Col, Spinner, Nav, Icon, ModalLayer } from '@edx/paragon'; +import { Row, Col, Spinner, Nav, Icon, ModalLayer } from '@openedx/paragon'; import { buildAssetUrl } from '../util/assetUrl'; import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; import CourseCard from './CourseCard'; diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx index 3cb43a5..964eeef 100644 --- a/src/learningpath/LearningPathList.jsx +++ b/src/learningpath/LearningPathList.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Spinner, Row, Col, Button } from '@edx/paragon'; +import { Spinner, Row, Col, Button } from '@openedx/paragon'; import { fetchLearningPathways, fetchCourses } from './data/thunks'; import LearningPathCard from './LearningPathCard'; import CourseCard from './CourseCard'; @@ -8,7 +8,7 @@ import FilterPanel from './FilterPanel'; export default function LearningPathList() { const dispatch = useDispatch(); - const { + const { fetching: lpFetching, learningPathways, errors: lpErrors, diff --git a/src/learningpath/index.scss b/src/learningpath/index.scss index 3090792..09866a3 100644 --- a/src/learningpath/index.scss +++ b/src/learningpath/index.scss @@ -1,4 +1,4 @@ -@import '@edx/paragon/scss/core/core'; +@import '@openedx/paragon/scss/core/core'; .learningpath-list { From 6c66cc722aa8858c821f691dc4c724a7b430cf04 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 15:17:43 +0200 Subject: [PATCH 09/45] refactor: replace UUIDs with keys --- src/index.jsx | 12 ++++++------ src/learningpath/LearningPathCard.jsx | 4 ++-- src/learningpath/LearningPathDetails.jsx | 12 ++++++------ src/learningpath/LearningPathList.jsx | 2 +- src/learningpath/data/api.js | 12 ++++++------ src/learningpath/data/thunks.js | 10 +++++----- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index b0d96bf..b3b3f75 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -23,13 +23,13 @@ subscribe(APP_READY, () => {
- } + } /> - } + } /> { const { - uuid, + key, image_url, display_name, subtitle, @@ -115,7 +115,7 @@ const LearningPathCard = ({ learningPath }) => {
)}
- +
diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index a55daa2..b346176 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -15,7 +15,7 @@ import { } from '@openedx/paragon/icons'; export default function LearningPathDetailPage() { - const { uuid } = useParams(); + const { key } = useParams(); const navigate = useNavigate(); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); @@ -28,7 +28,7 @@ export default function LearningPathDetailPage() { async function loadDetail() { try { setLoading(true); - const data = await fetchLearningPathDetail(uuid); + const data = await fetchLearningPathDetail(key); setDetail(data); } catch (err) { console.error('Failed to fetch learning path detail:', err); @@ -38,7 +38,7 @@ export default function LearningPathDetailPage() { } } loadDetail(); - }, [uuid]); + }, [key]); const courseIds = useMemo(() => { return detail && detail.steps ? detail.steps.map(step => step.course_key) : []; @@ -211,7 +211,7 @@ export default function LearningPathDetailPage() { {!loadingCourses && !coursesError && coursesForPath.length > 0 && ( coursesForPath.map(course => (
- +
)) )} @@ -238,12 +238,12 @@ export default function LearningPathDetailPage() { element={ navigate(`/learningpath/${uuid}`)} + onClose={() => navigate(`/learningpath/${key}`)} className="lp-course-modal-layer" > navigate(`/learningpath/${uuid}`)} + onClose={() => navigate(`/learningpath/${key}`)} /> } diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx index 964eeef..82d8c92 100644 --- a/src/learningpath/LearningPathList.jsx +++ b/src/learningpath/LearningPathList.jsx @@ -92,7 +92,7 @@ export default function LearningPathList() { ) : ( - + ) diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index 88d719f..1816365 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -3,19 +3,19 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; export async function fetchLearningPaths() { const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/v1/learning-paths/`); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/learning-paths/`); return response.data.results || response.data; } -export async function fetchLearningPathDetail(uuid) { +export async function fetchLearningPathDetail(key) { const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/v1/learning-paths/${uuid}/`); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/learning-paths/${key}/`); return response.data; } -export async function fetchLearningPathProgress(uuid) { +export async function fetchLearningPathProgress(key) { const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/v1/learning-paths/${uuid}/progress/`); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/${key}/progress/`); return response.data; } @@ -44,7 +44,7 @@ export async function fetchCourses(courseId) { export async function fetchCourseDetails(courseId) { const client = getAuthenticatedHttpClient(); const response = await client.get( - `${process.env.CMS_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}` + `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}` ); return response.data; } diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index 6a91957..9afd9f4 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -1,7 +1,7 @@ import * as api from './api'; -import { - fetchLearningPathwaysRequest, - fetchLearningPathwaysSuccess, +import { + fetchLearningPathwaysRequest, + fetchLearningPathwaysSuccess, fetchLearningPathwaysFailure, fetchCoursesRequest, fetchCoursesSuccess, @@ -15,8 +15,8 @@ export const fetchLearningPathways = () => async (dispatch) => { const pathwaylist = await api.fetchLearningPaths(); const pathways = await Promise.all( pathwaylist.map(async (lp) => { - const lpdetail = await api.fetchLearningPathDetail(lp.uuid); - const lpprogress = await api.fetchLearningPathProgress(lp.uuid); + const lpdetail = await api.fetchLearningPathDetail(lp.key); + const lpprogress = await api.fetchLearningPathProgress(lp.key); let status = "In Progress"; if (lpprogress.progress == 0.0){ status = "Not started"; From b8040e221314239e45f6e6b56e98cdb61dd2904c Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 15:49:50 +0200 Subject: [PATCH 10/45] style: linting fixes --- src/learningpath/CourseCard.jsx | 180 ++++++++++++----------- src/learningpath/CourseDetails.jsx | 99 ++++++------- src/learningpath/FilterPanel.jsx | 132 ++++++++--------- src/learningpath/LearningPathCard.jsx | 5 +- src/learningpath/LearningPathDetails.jsx | 78 +++++----- src/learningpath/LearningPathList.jsx | 52 +++---- src/learningpath/data/api.js | 147 +++++++++--------- src/learningpath/data/slice.js | 9 +- src/learningpath/data/thunks.js | 33 +++-- 9 files changed, 367 insertions(+), 368 deletions(-) diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index 5987751..2bb1952 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -1,106 +1,108 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Card, Button, Row, Col, Icon, Badge, ProgressBar } from '@openedx/paragon'; -import { buildAssetUrl } from '../util/assetUrl'; import { - LmsBook, - AccessTime, - CheckCircle, - LmsCompletionSolid, - Timelapse, + Card, Button, Row, Col, Icon, Badge, ProgressBar, +} from '@openedx/paragon'; +import { + LmsBook, + AccessTime, + CheckCircle, + LmsCompletionSolid, + Timelapse, } from '@openedx/paragon/icons'; +import { buildAssetUrl } from '../util/assetUrl'; const CourseCard = ({ course, parentPath }) => { - const course_key = "course-v1:"+ course.org + "+" + course.course_id + "+" + course.run - const { - name, - org, - course_image_asset_path, - end_date, - status, - percent, - } = course; - const linkTo = parentPath - ? `${parentPath}/course/${encodeURIComponent(course_key)}` - : `/course/${encodeURIComponent(course_key)}`; + const course_key = `course-v1:${course.org}+${course.course_id}+${course.run}`; + const { + name, + org, + course_image_asset_path, + end_date, + status, + percent, + } = course; + const linkTo = parentPath + ? `${parentPath}/course/${encodeURIComponent(course_key)}` + : `/course/${encodeURIComponent(course_key)}`; - let statusVariant = 'dark'; // default - let statusIcon = 'fa-circle'; // default icon - switch (status?.toLowerCase()) { + let statusVariant = 'dark'; // default + let statusIcon = 'fa-circle'; // default icon + switch (status?.toLowerCase()) { case 'completed': - statusVariant = 'success'; - statusIcon = CheckCircle; - break; + statusVariant = 'success'; + statusIcon = CheckCircle; + break; case 'not started': - statusVariant = 'secondary'; - statusIcon = LmsCompletionSolid; - break; + statusVariant = 'secondary'; + statusIcon = LmsCompletionSolid; + break; case 'in progress': - statusVariant = 'info'; - statusIcon = Timelapse; - break; + statusVariant = 'info'; + statusIcon = Timelapse; + break; default: - statusVariant = 'dark'; - statusIcon = 'fa-circle'; - break; - } - const endDateFormatted = end_date + statusVariant = 'dark'; + statusIcon = 'fa-circle'; + break; + } + const endDateFormatted = end_date ? new Date(end_date).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }) + month: 'short', + day: 'numeric', + year: 'numeric', + }) : null; - return ( - -
- - - {status} - + return ( + +
+ + + {status} + +
+ + + {course_image_asset_path && ( + + )} + + +
+ + Course +
+ + {org &&

{org}

} + {status === 'In progress' && ( + + )} + +
+ {endDateFormatted && ( +
+ + Access until {endDateFormatted} +
+ )}
- - - {course_image_asset_path && ( - - )} - - -
- - Course -
- - {org &&

{org}

} - {status == 'In progress' && ( - - )} - -
- {endDateFormatted && ( -
- - Access until {endDateFormatted} -
- )} -
- - - -
- -
-
- ); + + + + + + + + ); }; export default CourseCard; diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index d9bb1ed..8b43e8f 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -9,9 +9,8 @@ import { Nav, Icon, ModalCloseButton, - Button + Button, } from '@openedx/paragon'; -import { fetchCoursesByIds } from './data/api'; import { LmsBook, AccessTimeFilled, @@ -20,6 +19,7 @@ import { Person, Close, } from '@openedx/paragon/icons'; +import { fetchCoursesByIds } from './data/api'; import { buildAssetUrl } from '../util/assetUrl'; export default function CourseDetailPage({ isModalView = false, onClose }) { @@ -35,7 +35,8 @@ export default function CourseDetailPage({ isModalView = false, onClose }) { const data = await fetchCoursesByIds([courseKey]); setCourse(data[0]); } catch (err) { - console.error('Failed to load course detail:', err); + // eslint-disable-next-line no-console + console.error('Failed to load course details:', err); setError(err.message); } finally { setLoading(false); @@ -64,7 +65,7 @@ export default function CourseDetailPage({ isModalView = false, onClose }) { return (
{isModalView ? ( - + ) : ( )} @@ -72,7 +73,7 @@ export default function CourseDetailPage({ isModalView = false, onClose }) { ); } -function CourseDetailContent({course, isModalView, onClose}) { +const CourseDetailContent = ({ course, isModalView, onClose }) => { const { name, short_description, @@ -87,10 +88,10 @@ function CourseDetailContent({course, isModalView, onClose}) { const dateDisplay = end_date ? new Date(end_date).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - }) + month: 'long', + day: 'numeric', + year: 'numeric', + }) : null; const handleTabSelect = (selectedKey) => { @@ -103,9 +104,7 @@ function CourseDetailContent({course, isModalView, onClose}) { const handleClose = onClose || (() => navigate(-1)); const { courseKey } = useParams(); const learningMfeBase = getConfig().LEARNING_BASE_URL; - const buildCourseHomeUrl = (key) => { - return `${learningMfeBase}/learning/course/${key}/home`; - }; + const buildCourseHomeUrl = (key) => `${learningMfeBase}/learning/course/${key}/home`; const handleViewClick = () => { window.location.href = buildCourseHomeUrl(courseKey); }; @@ -128,8 +127,8 @@ function CourseDetailContent({course, isModalView, onClose}) {
- - Course + + Course

{name}

{short_description && ( @@ -138,55 +137,55 @@ function CourseDetailContent({course, isModalView, onClose}) { {course_image_asset_path && ( - + )}
- {dateDisplay && ( - -
- -
-

{dateDisplay}

-

Access ends

-
-
- - )} + {dateDisplay && ( -
- -
-

Earn a certificate

-

Courses include certification

-
+
+ +
+

{dateDisplay}

+

Access ends

+
+ )} + +
+ +
+

Earn a certificate

+

Courses include certification

+
+
+ {duration && ( -
- -
-

{duration}

-

Approx. duration

-
+
+ +
+

{duration}

+

Approx. duration

+
)} - {self_paced == true && ( + {self_paced === true && ( -
- -
-

Self-paced

-

Learn at your own speed

-
+
+ +
+

Self-paced

+

Learn at your own speed

+
)} @@ -262,4 +261,4 @@ function CourseDetailContent({course, isModalView, onClose}) {
); -} +}; diff --git a/src/learningpath/FilterPanel.jsx b/src/learningpath/FilterPanel.jsx index cb4d5d2..a262c73 100644 --- a/src/learningpath/FilterPanel.jsx +++ b/src/learningpath/FilterPanel.jsx @@ -1,82 +1,82 @@ // src/learningpath/components/FilterPanel.js import React from 'react'; -import { Button, ButtonGroup, Form, Icon } from '@openedx/paragon'; +import { + Button, ButtonGroup, Form, Icon, +} from '@openedx/paragon'; import { FilterList } from '@openedx/paragon/icons'; -function FilterPanel({ +const FilterPanel = ({ selectedContentType, onSelectContentType, selectedStatuses, onChangeStatus, - onClose -}) { - return ( -
-
-

Filter

- +
+ + {/* Content Type Tabs */} +
+ + + -
+ + +
- {/* Content Type Tabs */} -
- - - - - -
- - {/* Status Checkboxes */} -
-
Status
- -
- onChangeStatus('In Progress', e.target.checked)} - > - In progress - - onChangeStatus('Not started', e.target.checked)} - > - Not started - - onChangeStatus('Completed', e.target.checked)} - > - Completed - -
- -
+ Completed + +
+
- ); -} +
+); export default FilterPanel; diff --git a/src/learningpath/LearningPathCard.jsx b/src/learningpath/LearningPathCard.jsx index 89b34e1..c88a7da 100644 --- a/src/learningpath/LearningPathCard.jsx +++ b/src/learningpath/LearningPathCard.jsx @@ -7,7 +7,7 @@ import { Col, Badge, Icon, - ProgressBar + ProgressBar, } from '@openedx/paragon'; import { LmsCompletionSolid, @@ -18,7 +18,6 @@ import { } from '@openedx/paragon/icons'; import { buildAssetUrl } from '../util/assetUrl'; - const LearningPathCard = ({ learningPath }) => { const { key, @@ -56,7 +55,7 @@ const LearningPathCard = ({ learningPath }) => { const currentDate = new Date(); const accessDateObj = new Date(maxDate); const accessText = currentDate > accessDateObj - ? "Access ended" + ? 'Access ended' : `Access until ${accessDateObj.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`; const subtitleLine = subtitle && duration diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index b346176..128af3b 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -1,11 +1,11 @@ // src/learningpath/LearningPathDetailPage.jsx import React, { useEffect, useState, useMemo } from 'react'; -import { useParams, Link, useNavigate, Routes, Route } from 'react-router-dom'; -import { Row, Col, Spinner, Nav, Icon, ModalLayer } from '@openedx/paragon'; -import { buildAssetUrl } from '../util/assetUrl'; -import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; -import CourseCard from './CourseCard'; -import CourseDetailPage from './CourseDetails'; +import { + useParams, Link, useNavigate, Routes, Route, +} from 'react-router-dom'; +import { + Row, Col, Spinner, Nav, Icon, ModalLayer, +} from '@openedx/paragon'; import { Person, Award, @@ -13,6 +13,10 @@ import { FormatListBulleted, AccessTimeFilled, } from '@openedx/paragon/icons'; +import { buildAssetUrl } from '../util/assetUrl'; +import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; +import CourseCard from './CourseCard'; +import CourseDetailPage from './CourseDetails'; export default function LearningPathDetailPage() { const { key } = useParams(); @@ -31,7 +35,8 @@ export default function LearningPathDetailPage() { const data = await fetchLearningPathDetail(key); setDetail(data); } catch (err) { - console.error('Failed to fetch learning path detail:', err); + // eslint-disable-next-line no-console + console.error('Failed to fetch learning path details:', err); setError(err.message); } finally { setLoading(false); @@ -40,12 +45,10 @@ export default function LearningPathDetailPage() { loadDetail(); }, [key]); - const courseIds = useMemo(() => { - return detail && detail.steps ? detail.steps.map(step => step.course_key) : []; - }, [detail]); + const courseIds = useMemo(() => (detail && detail.steps ? detail.steps.map(step => step.course_key) : []), [detail]); useEffect(() => { - if (courseIds.length === 0) return; + if (courseIds.length === 0) { return; } async function loadCourses() { try { setLoadingCourses(true); @@ -53,6 +56,7 @@ export default function LearningPathDetailPage() { const courses = await fetchCoursesByIds(courseIds); setCoursesForPath(courses); } catch (err) { + // eslint-disable-next-line no-console console.error('Failed to fetch courses:', err); setCoursesError(err.message); } finally { @@ -106,32 +110,34 @@ export default function LearningPathDetailPage() {
- - Explore - + + Explore +
- -
- - Learning Path -
-

{display_name}

- {subtitle && ( + +
+ + Learning Path +
+

{display_name}

+ {subtitle && (

{subtitle}

- )} - - - {image_url && ( - {display_name} - )} - + )} + + + {image_url && ( + {display_name} + )} +
{accessUntilDate && ( @@ -161,7 +167,7 @@ export default function LearningPathDetailPage() {

- {durationText || '6 months'} + {durationText || '6 months'}

Duration

@@ -235,9 +241,9 @@ export default function LearningPathDetailPage() { navigate(`/learningpath/${key}`)} className="lp-course-modal-layer" > @@ -246,7 +252,7 @@ export default function LearningPathDetailPage() { onClose={() => navigate(`/learningpath/${key}`)} /> - } + )} /> diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx index 82d8c92..60dcaeb 100644 --- a/src/learningpath/LearningPathList.jsx +++ b/src/learningpath/LearningPathList.jsx @@ -1,6 +1,8 @@ import React, { useEffect, useState, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { Spinner, Row, Col, Button } from '@openedx/paragon'; +import { + Spinner, Row, Col, Button, +} from '@openedx/paragon'; import { fetchLearningPathways, fetchCourses } from './data/thunks'; import LearningPathCard from './LearningPathCard'; import CourseCard from './CourseCard'; @@ -28,12 +30,11 @@ export default function LearningPathList() { const allErrors = [].concat(lpErrors || [], coursesErrors || []); if (allErrors.length > 0) { - console.error('Error loading learning pathways:', allErrors); + // eslint-disable-next-line no-console + console.error('Error loading learning paths:', allErrors); } - const items = useMemo(() => { - return [...courses, ...learningPathways]; - }, [courses, learningPathways]); + const items = useMemo(() => [...courses, ...learningPathways], [courses, learningPathways]); const [showFilters, setShowFilters] = useState(false); const [selectedContentType, setSelectedContentType] = useState('All'); @@ -43,23 +44,18 @@ export default function LearningPathList() { setSelectedStatuses(prev => { if (isChecked) { return [...prev, status]; - } else { - return prev.filter(s => s !== status); } + return prev.filter(s => s !== status); }); }; - const filteredItems = useMemo(() => { - return items.filter(item => { - const typeMatch = - selectedContentType === 'All' || - (selectedContentType === 'course' && item.type === 'course') || - (selectedContentType === 'learning_path' && item.type === 'learning_path'); - const statusMatch = - selectedStatuses.length === 0 || selectedStatuses.includes(item.status); - return typeMatch && statusMatch; - }); - }, [items, selectedContentType, selectedStatuses]); + const filteredItems = useMemo(() => items.filter(item => { + const typeMatch = selectedContentType === 'All' + || (selectedContentType === 'course' && item.type === 'course') + || (selectedContentType === 'learning_path' && item.type === 'learning_path'); + const statusMatch = selectedStatuses.length === 0 || selectedStatuses.includes(item.status); + return typeMatch && statusMatch; + }), [items, selectedContentType, selectedStatuses]); return (
@@ -86,17 +82,15 @@ export default function LearningPathList() { )} - {filteredItems.map(item => - item.type == 'course' ? ( - - - - ) : ( - - - - ) - )} + {filteredItems.map(item => (item.type === 'course' ? ( + + + + ) : ( + + + + )))}
diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index 1816365..f0053b0 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -1,103 +1,100 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; - export async function fetchLearningPaths() { - const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/learning-paths/`); - return response.data.results || response.data; + const client = getAuthenticatedHttpClient(); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/learning-paths/`); + return response.data.results || response.data; } export async function fetchLearningPathDetail(key) { - const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/learning-paths/${key}/`); - return response.data; + const client = getAuthenticatedHttpClient(); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/learning-paths/${key}/`); + return response.data; } export async function fetchLearningPathProgress(key) { - const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/${key}/progress/`); - return response.data; + const client = getAuthenticatedHttpClient(); + const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/${key}/progress/`); + return response.data; } export async function fetchCourses(courseId) { - const client = getAuthenticatedHttpClient(); - let url; - if (courseId) { - url = `${process.env.LMS_BASE_URL}/api/courses/v1/courses/${encodeURIComponent(courseId)}/`; - } else { - url = `${process.env.LMS_BASE_URL}/api/courses/v1/courses/`; - } - const response = await client.get(url); - if (courseId) { - const course = response.data; - return { - course_id: course.course_id, - name: course.name, - }; - } - return (response.data.results || []).map(course => ({ - course_id: course.course_id, - name: course.name, - })); + const client = getAuthenticatedHttpClient(); + let url; + if (courseId) { + url = `${process.env.LMS_BASE_URL}/api/courses/v1/courses/${encodeURIComponent(courseId)}/`; + } else { + url = `${process.env.LMS_BASE_URL}/api/courses/v1/courses/`; + } + const response = await client.get(url); + if (courseId) { + const course = response.data; + return { + course_id: course.course_id, + name: course.name, + }; + } + return (response.data.results || []).map(course => ({ + course_id: course.course_id, + name: course.name, + })); } export async function fetchCourseDetails(courseId) { - const client = getAuthenticatedHttpClient(); - const response = await client.get( - `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}` - ); - return response.data; + const client = getAuthenticatedHttpClient(); + const response = await client.get( + `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}`, + ); + return response.data; } export async function fetchAllCourseDetails() { - const courses = await fetchCourses(); - const details = await Promise.all( - courses.map(course => - fetchCourseDetails(course.course_id).then(detail => ({ - ...detail, - name: course.name, - })) - ) - ); - return details; + const courses = await fetchCourses(); + const details = await Promise.all( + courses.map(course => fetchCourseDetails(course.course_id).then(detail => ({ + ...detail, + name: course.name, + }))), + ); + return details; } export async function fetchCourseCompletion(courseId) { - const client = getAuthenticatedHttpClient(); - const response = await client.get( - `${process.env.LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/` - ); - if (response.data.results && response.data.results.length > 0) { - return response.data.results[0].completion.percent; - } - return 0.0; + const client = getAuthenticatedHttpClient(); + const response = await client.get( + `${process.env.LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/`, + ); + if (response.data.results && response.data.results.length > 0) { + return response.data.results[0].completion.percent; + } + return 0.0; } export async function fetchCombinedCourseInfo(courseId) { - const basicInfo = await fetchCourses(courseId); - const details = await fetchCourseDetails(courseId); - return { - ...basicInfo, - ...details, - }; + const basicInfo = await fetchCourses(courseId); + const details = await fetchCourseDetails(courseId); + return { + ...basicInfo, + ...details, + }; } export async function fetchCoursesByIds(courseIds) { - const combined = await Promise.all( - courseIds.map(async (courseId) => { - const combinedInfo = await fetchCombinedCourseInfo(courseId); - const percent = await fetchCourseCompletion(courseId); - let status = 'In progress'; - if (percent === 0.0) { - status = 'Not started'; - } else if (percent === 100.0) { - status = 'Completed'; - } - return { - ...combinedInfo, - status, - }; - }) - ); - return combined; + const combined = await Promise.all( + courseIds.map(async (courseId) => { + const combinedInfo = await fetchCombinedCourseInfo(courseId); + const percent = await fetchCourseCompletion(courseId); + let status = 'In progress'; + if (percent === 0.0) { + status = 'Not started'; + } else if (percent === 100.0) { + status = 'Completed'; + } + return { + ...combinedInfo, + status, + }; + }), + ); + return combined; } diff --git a/src/learningpath/data/slice.js b/src/learningpath/data/slice.js index 00ffdb1..0dca42c 100644 --- a/src/learningpath/data/slice.js +++ b/src/learningpath/data/slice.js @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import { createSlice } from '@reduxjs/toolkit'; const initialLearningPathState = () => ({ @@ -52,10 +53,10 @@ const learningPathSlice = createSlice({ }, }); -export const { - fetchLearningPathwaysRequest, - fetchLearningPathwaysSuccess, - fetchLearningPathwaysFailure, +export const { + fetchLearningPathwaysRequest, + fetchLearningPathwaysSuccess, + fetchLearningPathwaysFailure, } = learningPathSlice.actions; export const { diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index 9afd9f4..acfb5c1 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -11,23 +11,23 @@ import { export const fetchLearningPathways = () => async (dispatch) => { try { dispatch(fetchLearningPathwaysRequest()); - const type = "learning_path"; + const type = 'learning_path'; const pathwaylist = await api.fetchLearningPaths(); const pathways = await Promise.all( pathwaylist.map(async (lp) => { const lpdetail = await api.fetchLearningPathDetail(lp.key); const lpprogress = await api.fetchLearningPathProgress(lp.key); - let status = "In Progress"; - if (lpprogress.progress == 0.0){ - status = "Not started"; + let status = 'In Progress'; + if (lpprogress.progress === 0.0) { + status = 'Not started'; } else if (lpprogress.progress >= lpprogress.required_completion) { - status = "Completed"; + status = 'Completed'; } let percent = 0; if (lpprogress.required_completion) { percent = lpprogress.required_completion > 0 - ? Math.round((lpprogress.progress / lpprogress.required_completion) * 100) - : 0; + ? Math.round((lpprogress.progress / lpprogress.required_completion) * 100) + : 0; } else { percent = lpprogress.percent; } @@ -54,11 +54,12 @@ export const fetchLearningPathways = () => async (dispatch) => { percent, type, }; - }) + }), ); dispatch(fetchLearningPathwaysSuccess({ pathways })); } catch (error) { - console.error('Failed to fetch learning pathways:', error); + // eslint-disable-next-line no-console + console.error('Failed to fetch learning paths:', error); dispatch(fetchLearningPathwaysFailure({ errors: [String(error)] })); } }; @@ -66,17 +67,17 @@ export const fetchLearningPathways = () => async (dispatch) => { export const fetchCourses = () => async (dispatch) => { try { dispatch(fetchCoursesRequest()); - const type = "course"; + const type = 'course'; const courses = await api.fetchAllCourseDetails(); const coursesWithStatus = await Promise.all( courses.map(async (course) => { - const course_key = "course-v1:"+ course.org + "+" + course.course_id + "+" + course.run + const course_key = `course-v1:${course.org}+${course.course_id}+${course.run}`; const percent = await api.fetchCourseCompletion(course_key); - let status = "In progress"; + let status = 'In progress'; if (percent === 0.0) { - status = "Not started"; + status = 'Not started'; } else if (percent === 100.0) { - status = "Completed"; + status = 'Completed'; } return { ...course, @@ -84,12 +85,12 @@ export const fetchCourses = () => async (dispatch) => { percent, type, }; - }) + }), ); dispatch(fetchCoursesSuccess({ courses: coursesWithStatus })); } catch (error) { + // eslint-disable-next-line no-console console.error('Failed to fetch courses:', error); dispatch(fetchCoursesFailure({ errors: [String(error)] })); } }; - From b2fa8cd58a9cb58a1c0b3981ec72dcdacdff5054 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 19:55:24 +0200 Subject: [PATCH 11/45] fix: center the Course Detail modal Moving it to the right was causing the modal to go outside the viewport on narrow screens. --- src/learningpath/index.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/learningpath/index.scss b/src/learningpath/index.scss index 09866a3..b0f89f9 100644 --- a/src/learningpath/index.scss +++ b/src/learningpath/index.scss @@ -237,7 +237,6 @@ .course-detail-page { background-color: white; max-width: 90rem; - left: 14rem; bottom: 6rem; padding-top: 2rem; } From 0bb186ee8943ff5aab1adebf052eb8621750d732 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 19:56:30 +0200 Subject: [PATCH 12/45] fix: add username to the Completion Aggregator query It was pulling a random completion for staff users otherwise. --- src/learningpath/data/api.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index f0053b0..d425fd2 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -1,4 +1,4 @@ -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth'; export async function fetchLearningPaths() { const client = getAuthenticatedHttpClient(); @@ -60,9 +60,10 @@ export async function fetchAllCourseDetails() { } export async function fetchCourseCompletion(courseId) { + const { username } = getAuthenticatedUser(); const client = getAuthenticatedHttpClient(); const response = await client.get( - `${process.env.LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/`, + `${process.env.LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/?username=${username}`, ); if (response.data.results && response.data.results.length > 0) { return response.data.results[0].completion.percent; From decc294bfc42ad95ec593aca4f2886616016ccf5 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 19:57:47 +0200 Subject: [PATCH 13/45] fix: pass completion percent when fetching courses The completion was showing a NaN on the LearningPathDetails page. --- src/learningpath/data/api.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index d425fd2..a64b8db 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -94,6 +94,7 @@ export async function fetchCoursesByIds(courseIds) { return { ...combinedInfo, status, + percent, }; }), ); From 1da603aca24b7d25f91af75d070af58951f82b60 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 19:58:21 +0200 Subject: [PATCH 14/45] fix: assign unique keys for courses This gets rid of the browser's console error. --- src/learningpath/LearningPathList.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx index 60dcaeb..3cd4dac 100644 --- a/src/learningpath/LearningPathList.jsx +++ b/src/learningpath/LearningPathList.jsx @@ -83,7 +83,7 @@ export default function LearningPathList() { )} {filteredItems.map(item => (item.type === 'course' ? ( - + ) : ( From 2be6b0b54dcaa912b02c5ecc35b7df77692dcea5 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 20:36:30 +0200 Subject: [PATCH 15/45] fix: rename title in base template --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 0941852..c5b4691 100644 --- a/public/index.html +++ b/public/index.html @@ -1,7 +1,7 @@ - Application Template <%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? '| ' + process.env.SITE_NAME : '' %> + Learning Paths <%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? '| ' + process.env.SITE_NAME : '' %> From 8120d8bb89f2643b37db047a2be170c288981645 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 20:36:57 +0200 Subject: [PATCH 16/45] refactor: use `getConfig` instead of directly checking `process.env` --- src/learningpath/data/api.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index a64b8db..9782b07 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -1,20 +1,21 @@ import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { getConfig } from '@edx/frontend-platform'; export async function fetchLearningPaths() { const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/learning-paths/`); + const response = await client.get(`${getConfig().LMS_BASE_URL}/api/learning_paths/v1/learning-paths/`); return response.data.results || response.data; } export async function fetchLearningPathDetail(key) { const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/learning-paths/${key}/`); + const response = await client.get(`${getConfig().LMS_BASE_URL}/api/learning_paths/v1/learning-paths/${key}/`); return response.data; } export async function fetchLearningPathProgress(key) { const client = getAuthenticatedHttpClient(); - const response = await client.get(`${process.env.LMS_BASE_URL}/api/learning_paths/v1/${key}/progress/`); + const response = await client.get(`${getConfig().LMS_BASE_URL}/api/learning_paths/v1/${key}/progress/`); return response.data; } @@ -22,9 +23,9 @@ export async function fetchCourses(courseId) { const client = getAuthenticatedHttpClient(); let url; if (courseId) { - url = `${process.env.LMS_BASE_URL}/api/courses/v1/courses/${encodeURIComponent(courseId)}/`; + url = `${getConfig().LMS_BASE_URL}/api/courses/v1/courses/${encodeURIComponent(courseId)}/`; } else { - url = `${process.env.LMS_BASE_URL}/api/courses/v1/courses/`; + url = `${getConfig().LMS_BASE_URL}/api/courses/v1/courses/`; } const response = await client.get(url); if (courseId) { @@ -43,7 +44,7 @@ export async function fetchCourses(courseId) { export async function fetchCourseDetails(courseId) { const client = getAuthenticatedHttpClient(); const response = await client.get( - `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}`, + `${getConfig().STUDIO_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}`, ); return response.data; } @@ -63,7 +64,7 @@ export async function fetchCourseCompletion(courseId) { const { username } = getAuthenticatedUser(); const client = getAuthenticatedHttpClient(); const response = await client.get( - `${process.env.LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/?username=${username}`, + `${getConfig().LMS_BASE_URL}/completion-aggregator/v1/course/${encodeURIComponent(courseId)}/?username=${username}`, ); if (response.data.results && response.data.results.length > 0) { return response.data.results[0].completion.percent; From 744d289dff8006911b76613627527f3a2f7ce976 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 21:35:48 +0200 Subject: [PATCH 17/45] style: refactor variables to camelCase --- src/learningpath/CourseCard.jsx | 18 ++++++------ src/learningpath/CourseDetails.jsx | 37 ++++++++++++------------ src/learningpath/LearningPathCard.jsx | 18 ++++++------ src/learningpath/LearningPathDetails.jsx | 34 ++++++++++++---------- src/learningpath/LearningPathList.jsx | 8 +++-- src/learningpath/data/api.js | 31 ++++++++++---------- src/learningpath/data/thunks.js | 20 ++++++------- 7 files changed, 86 insertions(+), 80 deletions(-) diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index 2bb1952..5093bbc 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -13,18 +13,18 @@ import { import { buildAssetUrl } from '../util/assetUrl'; const CourseCard = ({ course, parentPath }) => { - const course_key = `course-v1:${course.org}+${course.course_id}+${course.run}`; + const courseKey = `course-v1:${course.org}+${course.courseId}+${course.run}`; const { name, org, - course_image_asset_path, - end_date, + courseImageAssetPath, + endDate, status, percent, } = course; const linkTo = parentPath - ? `${parentPath}/course/${encodeURIComponent(course_key)}` - : `/course/${encodeURIComponent(course_key)}`; + ? `${parentPath}/course/${encodeURIComponent(courseKey)}` + : `/course/${encodeURIComponent(courseKey)}`; let statusVariant = 'dark'; // default let statusIcon = 'fa-circle'; // default icon @@ -46,8 +46,8 @@ const CourseCard = ({ course, parentPath }) => { statusIcon = 'fa-circle'; break; } - const endDateFormatted = end_date - ? new Date(end_date).toLocaleDateString('en-US', { + const endDateFormatted = endDate + ? new Date(endDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', @@ -63,9 +63,9 @@ const CourseCard = ({ course, parentPath }) => {
- {course_image_asset_path && ( + {courseImageAssetPath && ( diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index 8b43e8f..714184f 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -10,6 +10,7 @@ import { Icon, ModalCloseButton, Button, + Alert, } from '@openedx/paragon'; import { LmsBook, @@ -76,18 +77,18 @@ export default function CourseDetailPage({ isModalView = false, onClose }) { const CourseDetailContent = ({ course, isModalView, onClose }) => { const { name, - short_description, - end_date, + shortDescription, + endDate, duration, - self_paced, - course_image_asset_path, + selfPaced, + courseImageAssetPath, description, - learning_info, - instructor_info, + learningInfo, + instructorInfo, } = course; - const dateDisplay = end_date - ? new Date(end_date).toLocaleDateString('en-US', { + const dateDisplay = endDate + ? new Date(endDate).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', @@ -131,14 +132,14 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { Course

{name}

- {short_description && ( -

{short_description}

+ {shortDescription && ( +

{shortDescription}

)} - {course_image_asset_path && ( + {courseImageAssetPath && ( @@ -177,7 +178,7 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => {
)} - {self_paced === true && ( + {selfPaced === true && (
@@ -197,7 +198,7 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { About - What you’ll learn + What you'll learn Instructors @@ -216,13 +217,13 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => {

About

- {description || short_description} + {description || shortDescription}

What you'll learn

- {learning_info.map((learning) => ( + {learningInfo.map((learning) => (

* {learning}

@@ -232,8 +233,8 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => {

Instructors

- {instructor_info && instructor_info.instructors.length > 0 ? ( - instructor_info.instructors.map((inst, index) => ( + {instructorInfo && instructorInfo.instructors.length > 0 ? ( + instructorInfo.instructors.map((inst, index) => (
{ const { key, - image_url, - display_name, + imageUrl, + displayName, subtitle, duration, - num_courses, + numCourses, status, maxDate, percent, @@ -72,10 +72,10 @@ const LearningPathCard = ({ learningPath }) => {
- {image_url && ( + {imageUrl && ( )} @@ -85,7 +85,7 @@ const LearningPathCard = ({ learningPath }) => { Learning Path
- + {subtitleLine && (

{subtitleLine} @@ -101,10 +101,10 @@ const LearningPathCard = ({ learningPath }) => { )}

- {num_courses && ( + {numCourses && (
- {num_courses} courses + {numCourses} courses
)} {maxDate && ( diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index 128af3b..c036c39 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -18,7 +18,7 @@ import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; import CourseCard from './CourseCard'; import CourseDetailPage from './CourseDetails'; -export default function LearningPathDetailPage() { +const LearningPathDetailPage = () => { const { key } = useParams(); const navigate = useNavigate(); const [detail, setDetail] = useState(null); @@ -45,7 +45,7 @@ export default function LearningPathDetailPage() { loadDetail(); }, [key]); - const courseIds = useMemo(() => (detail && detail.steps ? detail.steps.map(step => step.course_key) : []), [detail]); + const courseIds = useMemo(() => (detail && detail.steps ? detail.steps.map(step => step.courseKey) : []), [detail]); useEffect(() => { if (courseIds.length === 0) { return; } @@ -69,8 +69,8 @@ export default function LearningPathDetailPage() { const accessUntilDate = useMemo(() => { let maxDate = null; for (const c of coursesForPath) { - if (c.end_date) { - const endDateObj = new Date(c.end_date); + if (c.endDate) { + const endDateObj = new Date(c.endDate); if (!maxDate || endDateObj > maxDate) { maxDate = endDateObj; } @@ -91,15 +91,15 @@ export default function LearningPathDetailPage() { ); } else { const { - display_name, - image_url, + displayName, + imageUrl, subtitle, - duration_in_days, - required_skills, + durationInDays, + requiredSkills, description, } = detail; - const durationText = duration_in_days ? `${duration_in_days} days` : null; + const durationText = durationInDays ? `${durationInDays} days` : null; const handleTabSelect = (selectedKey) => { const el = document.getElementById(selectedKey); if (el) { @@ -120,7 +120,7 @@ export default function LearningPathDetailPage() { Learning Path
-

{display_name}

+

{displayName}

{subtitle && (

{subtitle} @@ -128,10 +128,10 @@ export default function LearningPathDetailPage() { )} - {image_url && ( + {imageUrl && ( {display_name} 0 && ( coursesForPath.map(course => ( -

+
)) @@ -224,7 +224,7 @@ export default function LearningPathDetailPage() {

Requirements

- {required_skills.map(skill => ( + {requiredSkills.map(skill => (

{skill}

@@ -257,4 +257,6 @@ export default function LearningPathDetailPage() { ); -} +}; + +export default LearningPathDetailPage; diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx index 3cd4dac..e83c867 100644 --- a/src/learningpath/LearningPathList.jsx +++ b/src/learningpath/LearningPathList.jsx @@ -8,7 +8,7 @@ import LearningPathCard from './LearningPathCard'; import CourseCard from './CourseCard'; import FilterPanel from './FilterPanel'; -export default function LearningPathList() { +const LearningPathList = () => { const dispatch = useDispatch(); const { fetching: lpFetching, @@ -83,7 +83,7 @@ export default function LearningPathList() { )} {filteredItems.map(item => (item.type === 'course' ? ( - + ) : ( @@ -97,4 +97,6 @@ export default function LearningPathList() { )}
); -} +}; + +export default LearningPathList; diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index 9782b07..8b9e830 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -1,22 +1,23 @@ import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth'; -import { getConfig } from '@edx/frontend-platform'; +import { getConfig, camelCaseObject } from '@edx/frontend-platform'; export async function fetchLearningPaths() { const client = getAuthenticatedHttpClient(); const response = await client.get(`${getConfig().LMS_BASE_URL}/api/learning_paths/v1/learning-paths/`); - return response.data.results || response.data; + const data = response.data.results || response.data; + return camelCaseObject(data); } export async function fetchLearningPathDetail(key) { const client = getAuthenticatedHttpClient(); const response = await client.get(`${getConfig().LMS_BASE_URL}/api/learning_paths/v1/learning-paths/${key}/`); - return response.data; + return camelCaseObject(response.data); } export async function fetchLearningPathProgress(key) { const client = getAuthenticatedHttpClient(); const response = await client.get(`${getConfig().LMS_BASE_URL}/api/learning_paths/v1/${key}/progress/`); - return response.data; + return camelCaseObject(response.data); } export async function fetchCourses(courseId) { @@ -30,15 +31,15 @@ export async function fetchCourses(courseId) { const response = await client.get(url); if (courseId) { const course = response.data; - return { + return camelCaseObject({ course_id: course.course_id, name: course.name, - }; + }); } - return (response.data.results || []).map(course => ({ + return camelCaseObject((response.data.results || []).map(course => ({ course_id: course.course_id, name: course.name, - })); + }))); } export async function fetchCourseDetails(courseId) { @@ -46,18 +47,18 @@ export async function fetchCourseDetails(courseId) { const response = await client.get( `${getConfig().STUDIO_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}`, ); - return response.data; + return camelCaseObject(response.data); } export async function fetchAllCourseDetails() { const courses = await fetchCourses(); const details = await Promise.all( - courses.map(course => fetchCourseDetails(course.course_id).then(detail => ({ + courses.map(course => fetchCourseDetails(course.courseId).then(detail => ({ ...detail, name: course.name, }))), ); - return details; + return camelCaseObject(details); } export async function fetchCourseCompletion(courseId) { @@ -75,10 +76,10 @@ export async function fetchCourseCompletion(courseId) { export async function fetchCombinedCourseInfo(courseId) { const basicInfo = await fetchCourses(courseId); const details = await fetchCourseDetails(courseId); - return { + return camelCaseObject({ ...basicInfo, ...details, - }; + }); } export async function fetchCoursesByIds(courseIds) { @@ -92,11 +93,11 @@ export async function fetchCoursesByIds(courseIds) { } else if (percent === 100.0) { status = 'Completed'; } - return { + return camelCaseObject({ ...combinedInfo, status, percent, - }; + }); }), ); return combined; diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index acfb5c1..00745c9 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -20,22 +20,22 @@ export const fetchLearningPathways = () => async (dispatch) => { let status = 'In Progress'; if (lpprogress.progress === 0.0) { status = 'Not started'; - } else if (lpprogress.progress >= lpprogress.required_completion) { + } else if (lpprogress.progress >= lpprogress.requiredCompletion) { status = 'Completed'; } let percent = 0; - if (lpprogress.required_completion) { - percent = lpprogress.required_completion > 0 - ? Math.round((lpprogress.progress / lpprogress.required_completion) * 100) + if (lpprogress.requiredCompletion) { + percent = lpprogress.requiredCompletion > 0 + ? Math.round((lpprogress.progress / lpprogress.requiredCompletion) * 100) : 0; } else { percent = lpprogress.percent; } let maxDate = null; - const num_courses = lpdetail.steps.length; + const numCourses = lpdetail.steps.length; for (const course of lpdetail.steps) { - if (course.due_date) { - const dueDateObj = new Date(course.due_date); + if (course.dueDate) { + const dueDateObj = new Date(course.dueDate); if (!maxDate || dueDateObj > maxDate) { maxDate = dueDateObj; } @@ -48,7 +48,7 @@ export const fetchLearningPathways = () => async (dispatch) => { return { ...lp, ...lpdetail, - num_courses, + numCourses, status, maxDate, percent, @@ -71,8 +71,8 @@ export const fetchCourses = () => async (dispatch) => { const courses = await api.fetchAllCourseDetails(); const coursesWithStatus = await Promise.all( courses.map(async (course) => { - const course_key = `course-v1:${course.org}+${course.course_id}+${course.run}`; - const percent = await api.fetchCourseCompletion(course_key); + const courseKey = `course-v1:${course.org}+${course.courseId}+${course.run}`; + const percent = await api.fetchCourseCompletion(courseKey); let status = 'In progress'; if (percent === 0.0) { status = 'Not started'; From cd5d0ab585b27c1c46f812f9b5466b32d9472465 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 21:40:33 +0200 Subject: [PATCH 18/45] style: add prop types --- src/learningpath/CourseCard.jsx | 19 +++++++++++ src/learningpath/CourseDetails.jsx | 45 +++++++++++++++++++++++++-- src/learningpath/FilterPanel.jsx | 9 ++++++ src/learningpath/LearningPathCard.jsx | 15 +++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index 5093bbc..dd8a80b 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { Card, Button, Row, Col, Icon, Badge, ProgressBar, @@ -105,4 +106,22 @@ const CourseCard = ({ course, parentPath }) => { ); }; +CourseCard.propTypes = { + course: PropTypes.shape({ + name: PropTypes.string.isRequired, + org: PropTypes.string.isRequired, + courseId: PropTypes.string.isRequired, + run: PropTypes.string.isRequired, + courseImageAssetPath: PropTypes.string, + endDate: PropTypes.string, + status: PropTypes.string.isRequired, + percent: PropTypes.number.isRequired, + }).isRequired, + parentPath: PropTypes.string, +}; + +CourseCard.defaultProps = { + parentPath: undefined, +}; + export default CourseCard; diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index 714184f..53e0c0c 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; import { @@ -23,7 +24,7 @@ import { import { fetchCoursesByIds } from './data/api'; import { buildAssetUrl } from '../util/assetUrl'; -export default function CourseDetailPage({ isModalView = false, onClose }) { +const CourseDetailPage = ({ isModalView = false, onClose }) => { const { courseKey } = useParams(); const [course, setCourse] = useState(null); const [loading, setLoading] = useState(true); @@ -72,7 +73,17 @@ export default function CourseDetailPage({ isModalView = false, onClose }) { )}
); -} +}; + +CourseDetailPage.propTypes = { + isModalView: PropTypes.bool, + onClose: PropTypes.func, +}; + +CourseDetailPage.defaultProps = { + isModalView: false, + onClose: undefined, +}; const CourseDetailContent = ({ course, isModalView, onClose }) => { const { @@ -263,3 +274,33 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { ); }; + +CourseDetailContent.propTypes = { + course: PropTypes.shape({ + name: PropTypes.string.isRequired, + shortDescription: PropTypes.string, + endDate: PropTypes.string, + duration: PropTypes.string, + selfPaced: PropTypes.bool, + courseImageAssetPath: PropTypes.string, + description: PropTypes.string, + learningInfo: PropTypes.arrayOf(PropTypes.string).isRequired, + instructorInfo: PropTypes.shape({ + instructors: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + title: PropTypes.string, + organization: PropTypes.string, + image: PropTypes.string, + })).isRequired, + }).isRequired, + }).isRequired, + isModalView: PropTypes.bool, + onClose: PropTypes.func, +}; + +CourseDetailContent.defaultProps = { + isModalView: false, + onClose: undefined, +}; + +export default CourseDetailPage; diff --git a/src/learningpath/FilterPanel.jsx b/src/learningpath/FilterPanel.jsx index a262c73..b4f896c 100644 --- a/src/learningpath/FilterPanel.jsx +++ b/src/learningpath/FilterPanel.jsx @@ -1,5 +1,6 @@ // src/learningpath/components/FilterPanel.js import React from 'react'; +import PropTypes from 'prop-types'; import { Button, ButtonGroup, Form, Icon, } from '@openedx/paragon'; @@ -79,4 +80,12 @@ const FilterPanel = ({
); +FilterPanel.propTypes = { + selectedContentType: PropTypes.oneOf(['All', 'course', 'learning_path']).isRequired, + onSelectContentType: PropTypes.func.isRequired, + selectedStatuses: PropTypes.arrayOf(PropTypes.string).isRequired, + onChangeStatus: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, +}; + export default FilterPanel; diff --git a/src/learningpath/LearningPathCard.jsx b/src/learningpath/LearningPathCard.jsx index 28873d8..9d7026e 100644 --- a/src/learningpath/LearningPathCard.jsx +++ b/src/learningpath/LearningPathCard.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { Card, @@ -124,4 +125,18 @@ const LearningPathCard = ({ learningPath }) => { ); }; +LearningPathCard.propTypes = { + learningPath: PropTypes.shape({ + key: PropTypes.string.isRequired, + imageUrl: PropTypes.string, + displayName: PropTypes.string.isRequired, + subtitle: PropTypes.string, + duration: PropTypes.string, + numCourses: PropTypes.number, + status: PropTypes.string.isRequired, + maxDate: PropTypes.string, + percent: PropTypes.number, + }).isRequired, +}; + export default LearningPathCard; From 2901b570701f5b5b71acf4d0698d6be8e0db85ec Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Thu, 3 Apr 2025 21:42:27 +0200 Subject: [PATCH 19/45] style: fix more eslint errors --- src/learningpath/CourseDetails.jsx | 142 +++++++++++------------ src/learningpath/FilterPanel.jsx | 1 - src/learningpath/LearningPathDetails.jsx | 1 - 3 files changed, 71 insertions(+), 73 deletions(-) diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index 53e0c0c..1efbb46 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -24,67 +24,6 @@ import { import { fetchCoursesByIds } from './data/api'; import { buildAssetUrl } from '../util/assetUrl'; -const CourseDetailPage = ({ isModalView = false, onClose }) => { - const { courseKey } = useParams(); - const [course, setCourse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function loadCourse() { - try { - setLoading(true); - const data = await fetchCoursesByIds([courseKey]); - setCourse(data[0]); - } catch (err) { - // eslint-disable-next-line no-console - console.error('Failed to load course details:', err); - setError(err.message); - } finally { - setLoading(false); - } - } - loadCourse(); - }, [courseKey]); - - if (loading) { - return ; - } - - if (error) { - return ( -
- Failed to load course detail: {error} - Back -
- ); - } - - if (!course) { - return ; - } - - return ( -
- {isModalView ? ( - - ) : ( - - )} -
- ); -}; - -CourseDetailPage.propTypes = { - isModalView: PropTypes.bool, - onClose: PropTypes.func, -}; - -CourseDetailPage.defaultProps = { - isModalView: false, - onClose: undefined, -}; - const CourseDetailContent = ({ course, isModalView, onClose }) => { const { name, @@ -209,7 +148,7 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { About - What you'll learn + What you'll learn Instructors @@ -233,7 +172,7 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => {
-

What you'll learn

+

What you'll learn

{learningInfo.map((learning) => (

* {learning} @@ -245,20 +184,20 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => {

Instructors

{instructorInfo && instructorInfo.instructors.length > 0 ? ( - instructorInfo.instructors.map((inst, index) => ( - + instructorInfo.instructors.map((instructor) => ( +
{inst.name}

- {inst.name} + {instructor.name}

- {inst.title &&

{inst.title}

} - {inst.organization && ( -

{inst.organization}

+ {instructor.title &&

{instructor.title}

} + {instructor.organization && ( +

{instructor.organization}

)}
@@ -303,4 +242,65 @@ CourseDetailContent.defaultProps = { onClose: undefined, }; +const CourseDetailPage = ({ isModalView = false, onClose }) => { + const { courseKey } = useParams(); + const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function loadCourse() { + try { + setLoading(true); + const data = await fetchCoursesByIds([courseKey]); + setCourse(data[0]); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Failed to load course details:', err); + setError(err.message); + } finally { + setLoading(false); + } + } + loadCourse(); + }, [courseKey]); + + if (loading) { + return ; + } + + if (error) { + return ( +
+ Failed to load course detail: {error} + Back +
+ ); + } + + if (!course) { + return ; + } + + return ( +
+ {isModalView ? ( + + ) : ( + + )} +
+ ); +}; + +CourseDetailPage.propTypes = { + isModalView: PropTypes.bool, + onClose: PropTypes.func, +}; + +CourseDetailPage.defaultProps = { + isModalView: false, + onClose: undefined, +}; + export default CourseDetailPage; diff --git a/src/learningpath/FilterPanel.jsx b/src/learningpath/FilterPanel.jsx index b4f896c..4750e2a 100644 --- a/src/learningpath/FilterPanel.jsx +++ b/src/learningpath/FilterPanel.jsx @@ -1,4 +1,3 @@ -// src/learningpath/components/FilterPanel.js import React from 'react'; import PropTypes from 'prop-types'; import { diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index c036c39..cc8d026 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -1,4 +1,3 @@ -// src/learningpath/LearningPathDetailPage.jsx import React, { useEffect, useState, useMemo } from 'react'; import { useParams, Link, useNavigate, Routes, Route, From b99a540200e32ddf70baa86251e50223739f680d Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Fri, 4 Apr 2025 13:55:08 +0200 Subject: [PATCH 20/45] refactor: fix Learning Paths names --- src/learningpath/LearningPathList.jsx | 8 ++--- src/learningpath/data/slice.js | 18 ++++++------ src/learningpath/data/thunks.js | 42 +++++++++++++-------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/learningpath/LearningPathList.jsx b/src/learningpath/LearningPathList.jsx index e83c867..dc2313b 100644 --- a/src/learningpath/LearningPathList.jsx +++ b/src/learningpath/LearningPathList.jsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Spinner, Row, Col, Button, } from '@openedx/paragon'; -import { fetchLearningPathways, fetchCourses } from './data/thunks'; +import { fetchLearningPaths, fetchCourses } from './data/thunks'; import LearningPathCard from './LearningPathCard'; import CourseCard from './CourseCard'; import FilterPanel from './FilterPanel'; @@ -12,7 +12,7 @@ const LearningPathList = () => { const dispatch = useDispatch(); const { fetching: lpFetching, - learningPathways, + learningPaths, errors: lpErrors, } = useSelector(state => state.learningPath); const { @@ -22,7 +22,7 @@ const LearningPathList = () => { } = useSelector(state => state.courses); useEffect(() => { - dispatch(fetchLearningPathways()); + dispatch(fetchLearningPaths()); dispatch(fetchCourses()); }, [dispatch]); @@ -34,7 +34,7 @@ const LearningPathList = () => { console.error('Error loading learning paths:', allErrors); } - const items = useMemo(() => [...courses, ...learningPathways], [courses, learningPathways]); + const items = useMemo(() => [...courses, ...learningPaths], [courses, learningPaths]); const [showFilters, setShowFilters] = useState(false); const [selectedContentType, setSelectedContentType] = useState('All'); diff --git a/src/learningpath/data/slice.js b/src/learningpath/data/slice.js index 0dca42c..2bbc400 100644 --- a/src/learningpath/data/slice.js +++ b/src/learningpath/data/slice.js @@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; const initialLearningPathState = () => ({ fetching: false, - learningPathways: [], + learningPaths: [], errors: [], }); @@ -37,16 +37,16 @@ const learningPathSlice = createSlice({ name: 'learningPath', initialState: initialLearningPathState(), reducers: { - fetchLearningPathwaysRequest(state) { + fetchLearningPathsRequest(state) { state.fetching = true; state.errors = []; - state.learningPathways = []; + state.learningPaths = []; }, - fetchLearningPathwaysSuccess(state, action) { + fetchLearningPathsSuccess(state, action) { state.fetching = false; - state.learningPathways = action.payload.pathways; + state.learningPaths = action.payload.learningPaths; }, - fetchLearningPathwaysFailure(state, action) { + fetchLearningPathsFailure(state, action) { state.fetching = false; state.errors = action.payload.errors; }, @@ -54,9 +54,9 @@ const learningPathSlice = createSlice({ }); export const { - fetchLearningPathwaysRequest, - fetchLearningPathwaysSuccess, - fetchLearningPathwaysFailure, + fetchLearningPathsRequest, + fetchLearningPathsSuccess, + fetchLearningPathsFailure, } = learningPathSlice.actions; export const { diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index 00745c9..f6fed6c 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -1,39 +1,39 @@ import * as api from './api'; import { - fetchLearningPathwaysRequest, - fetchLearningPathwaysSuccess, - fetchLearningPathwaysFailure, + fetchLearningPathsRequest, + fetchLearningPathsSuccess, + fetchLearningPathsFailure, fetchCoursesRequest, fetchCoursesSuccess, fetchCoursesFailure, } from './slice'; -export const fetchLearningPathways = () => async (dispatch) => { +export const fetchLearningPaths = () => async (dispatch) => { try { - dispatch(fetchLearningPathwaysRequest()); + dispatch(fetchLearningPathsRequest()); const type = 'learning_path'; - const pathwaylist = await api.fetchLearningPaths(); - const pathways = await Promise.all( - pathwaylist.map(async (lp) => { - const lpdetail = await api.fetchLearningPathDetail(lp.key); - const lpprogress = await api.fetchLearningPathProgress(lp.key); + const learningPathList = await api.fetchLearningPaths(); + const learningPaths = await Promise.all( + learningPathList.map(async (lp) => { + const lpDetail = await api.fetchLearningPathDetail(lp.key); + const lpProgress = await api.fetchLearningPathProgress(lp.key); let status = 'In Progress'; - if (lpprogress.progress === 0.0) { + if (lpProgress.progress === 0.0) { status = 'Not started'; - } else if (lpprogress.progress >= lpprogress.requiredCompletion) { + } else if (lpProgress.progress >= lpProgress.requiredCompletion) { status = 'Completed'; } let percent = 0; - if (lpprogress.requiredCompletion) { - percent = lpprogress.requiredCompletion > 0 - ? Math.round((lpprogress.progress / lpprogress.requiredCompletion) * 100) + if (lpProgress.requiredCompletion) { + percent = lpProgress.requiredCompletion > 0 + ? Math.round((lpProgress.progress / lpProgress.requiredCompletion) * 100) : 0; } else { - percent = lpprogress.percent; + percent = lpProgress.percent; } let maxDate = null; - const numCourses = lpdetail.steps.length; - for (const course of lpdetail.steps) { + const numCourses = lpDetail.steps.length; + for (const course of lpDetail.steps) { if (course.dueDate) { const dueDateObj = new Date(course.dueDate); if (!maxDate || dueDateObj > maxDate) { @@ -47,7 +47,7 @@ export const fetchLearningPathways = () => async (dispatch) => { } return { ...lp, - ...lpdetail, + ...lpDetail, numCourses, status, maxDate, @@ -56,11 +56,11 @@ export const fetchLearningPathways = () => async (dispatch) => { }; }), ); - dispatch(fetchLearningPathwaysSuccess({ pathways })); + dispatch(fetchLearningPathsSuccess({ learningPaths })); } catch (error) { // eslint-disable-next-line no-console console.error('Failed to fetch learning paths:', error); - dispatch(fetchLearningPathwaysFailure({ errors: [String(error)] })); + dispatch(fetchLearningPathsFailure({ errors: [String(error)] })); } }; From bda6a948b2e45e5c0bdb5bcc5130dae36a8b2a18 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Fri, 4 Apr 2025 14:30:03 +0200 Subject: [PATCH 21/45] refactor: use CSS instead of SCSS SCSS styling will be deprecated after Teak. Also, CSS natively supports nesting: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting --- src/index.scss | 2 +- src/learningpath/{index.scss => index.css} | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) rename src/learningpath/{index.scss => index.css} (97%) diff --git a/src/index.scss b/src/index.scss index b26d3ca..c5cf57e 100644 --- a/src/index.scss +++ b/src/index.scss @@ -3,7 +3,7 @@ @import "@openedx/paragon/scss/core/core.scss"; @import "@edx/brand/paragon/overrides.scss"; -@import './learningpath/index.scss'; +@import 'learningpath/index.css'; @import "~@edx/frontend-component-header/dist/index"; @import "~@edx/frontend-component-footer/dist/footer"; diff --git a/src/learningpath/index.scss b/src/learningpath/index.css similarity index 97% rename from src/learningpath/index.scss rename to src/learningpath/index.css index b0f89f9..f828fe7 100644 --- a/src/learningpath/index.scss +++ b/src/learningpath/index.css @@ -1,11 +1,9 @@ -@import '@openedx/paragon/scss/core/core'; - - .learningpath-list { margin: 2rem; -} -.learningpath-list .list-group-item { - font-size: 1rem; + + .list-group-item { + font-size: 1rem; + } } .learning-path-card { @@ -133,11 +131,13 @@ .info-col { flex: 0 0 calc(25% - 1rem); margin-bottom: 1rem; + h3 { font-size: 1rem; font-weight: 600; margin-bottom: 0.25rem; } + .info-subtext { color: #666; font-size: 0.875rem; @@ -150,8 +150,10 @@ margin-top: 3rem; margin-left: 10rem; margin-right: 10rem; + section { margin-bottom: 2rem; + h2 { font-size: 1.5rem; margin-bottom: 1rem; @@ -216,18 +218,22 @@ display: block; margin: 0 auto; } + .instructor-name { font-size: 1.1rem; font-weight: 600; } + .instructor-title { color: #555; font-weight: 500; } + .instructor-org { color: #888; margin-bottom: 0.5rem; } + .instructor-bio { font-size: 0.9rem; } @@ -244,11 +250,13 @@ .main-content { transition: margin-left 0.3s ease; + button { margin-top: 1rem; margin-bottom: 20px; } } + .main-content.shifted { margin-left: 350px; } From 8521ea1e1090ca08ab8afab39920f78fe5ef2aa8 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Fri, 4 Apr 2025 14:52:50 +0200 Subject: [PATCH 22/45] docs: update name in catalog-info.yaml --- catalog-info.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catalog-info.yaml b/catalog-info.yaml index 2d8df03..92a46bd 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -4,10 +4,10 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: "frontend-app-learning-path" + name: "frontend-app-learning-paths" description: "The edX learning platform's frontend for learning paths." links: - - url: "https://github.com/open-craft/frontend-app-learning-path/blob/master/README.rst" + - url: "https://github.com/open-craft/frontend-app-learning-paths/blob/main/README.rst" title: "README" icon: "Article" annotations: From 76a8883759a43e831d390068494886f6a9924313 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Fri, 4 Apr 2025 15:04:04 +0200 Subject: [PATCH 23/45] temp: turn off codecov --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66e2473..c5fc154 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,8 @@ jobs: run: npm run build - name: i18n_extract run: npm run i18n_extract - - name: Coverage - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true + # - name: Coverage + # uses: codecov/codecov-action@v4 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # fail_ci_if_error: true From 3ce6f52aab9b9acc2748da5475f141dbafa37008 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Fri, 4 Apr 2025 15:05:24 +0200 Subject: [PATCH 24/45] build: change the default branch to `main` Note: we may need to consider using the `release/*` branches. --- .github/workflows/ci.yml | 5 ++--- .github/workflows/lockfileversion-check.yml | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5fc154..f1b0597 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,7 @@ name: Default CI -on: +on: push: - branches: - - 'master' + branches: [main] pull_request: branches: - '**' diff --git a/.github/workflows/lockfileversion-check.yml b/.github/workflows/lockfileversion-check.yml index 916dcb4..b7908f2 100644 --- a/.github/workflows/lockfileversion-check.yml +++ b/.github/workflows/lockfileversion-check.yml @@ -4,8 +4,7 @@ name: Lockfile Version check on: push: - branches: - - master + branches: [main] pull_request: jobs: From c2ba3d76b2ee28665dae39a9d356596bb3cd5e89 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Fri, 4 Apr 2025 16:16:28 +0200 Subject: [PATCH 25/45] temp: add comments about the bugs that need to be addressed in the future --- src/learningpath/data/api.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index 8b9e830..0878bb1 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -3,6 +3,7 @@ import { getConfig, camelCaseObject } from '@edx/frontend-platform'; export async function fetchLearningPaths() { const client = getAuthenticatedHttpClient(); + // FIXME: This API has pagination. const response = await client.get(`${getConfig().LMS_BASE_URL}/api/learning_paths/v1/learning-paths/`); const data = response.data.results || response.data; return camelCaseObject(data); @@ -24,8 +25,10 @@ export async function fetchCourses(courseId) { const client = getAuthenticatedHttpClient(); let url; if (courseId) { + // FIXME: This returns 404 when `COURSE_CATALOG_VISIBILITY_PERMISSION` is not set to `about` or `both` for a course. url = `${getConfig().LMS_BASE_URL}/api/courses/v1/courses/${encodeURIComponent(courseId)}/`; } else { + // FIXME: This API has pagination. url = `${getConfig().LMS_BASE_URL}/api/courses/v1/courses/`; } const response = await client.get(url); @@ -44,6 +47,7 @@ export async function fetchCourses(courseId) { export async function fetchCourseDetails(courseId) { const client = getAuthenticatedHttpClient(); + // FIXME: Non-staff users cannot use this API. const response = await client.get( `${getConfig().STUDIO_BASE_URL}/api/contentstore/v1/course_details/${encodeURIComponent(courseId)}`, ); From 24a035d8ccea9eb44469489f4d7d43c596472674 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Sat, 5 Apr 2025 20:25:16 +0200 Subject: [PATCH 26/45] fix: downgrade `@edx/frontend-platform` The newer version does not work with Redwood header. --- package-lock.json | 191 ++++++++++++++++++++++++++++++++++++++-------- package.json | 2 +- 2 files changed, 160 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index d03aa21..bf76f14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-header": "^5.6.0", - "@edx/frontend-platform": "^8.0.0", + "@edx/frontend-platform": "^7.1.2", "@edx/openedx-atlas": "^0.6.0", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", @@ -2715,16 +2715,15 @@ } }, "node_modules/@edx/frontend-platform": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.1.5.tgz", - "integrity": "sha512-tihHh4CQZrAerU9DeHIIfuN6vtZd5ahjLK9hFMb7o1aac/grjVUPueLONrdxLHSxb2LQsjga3YMXA5WIy5vOJQ==", - "license": "AGPL-3.0", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-7.1.4.tgz", + "integrity": "sha512-ahsz9CCrBdcWJPMX/YM9cQCCWL7PeVGy+zYNvdvPoy/zVxQc9byNTzpZTsQYuTcg8E1zXRiAci4IUBqD9EMQNQ==", "dependencies": { "@cospired/i18n-iso-languages": "4.2.0", "@formatjs/intl-pluralrules": "4.3.3", "@formatjs/intl-relativetimeformat": "10.0.1", - "axios": "1.6.7", - "axios-cache-interceptor": "1.6.2", + "axios": "0.27.2", + "axios-cache-interceptor": "0.10.7", "form-urlencoded": "4.1.4", "glob": "7.2.3", "history": "4.10.1", @@ -2736,8 +2735,8 @@ "lodash.memoize": "4.1.2", "lodash.merge": "4.6.2", "lodash.snakecase": "4.1.1", - "pubsub-js": "1.9.5", - "react-intl": "6.8.9", + "pubsub-js": "1.9.4", + "react-intl": "6.6.5", "universal-cookie": "4.0.4" }, "bin": { @@ -2745,9 +2744,9 @@ "transifex-utils.js": "i18n/scripts/transifex-utils.js" }, "peerDependencies": { - "@openedx/frontend-build": ">= 14.0.0", + "@openedx/frontend-build": ">= 13.0.15", "@openedx/paragon": ">= 21.5.7 < 23.0.0", - "prop-types": ">=15.7.2 <16.0.0", + "prop-types": "^15.7.2", "react": "^16.9.0 || ^17.0.0", "react-dom": "^16.9.0 || ^17.0.0", "react-redux": "^7.1.1 || ^8.1.1", @@ -2755,6 +2754,119 @@ "redux": "^4.0.4" } }, + "node_modules/@edx/frontend-platform/node_modules/@formatjs/ecma402-abstract": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", + "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", + "dependencies": { + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@edx/frontend-platform/node_modules/@formatjs/fast-memoize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", + "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@edx/frontend-platform/node_modules/@formatjs/intl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.1.tgz", + "integrity": "sha512-dsLG15U7xDi8yzKf4hcAWSsCaez3XrjTO2oaRHPyHtXLm1aEzYbDw6bClo/HMHu+iwS5GbDqT3DV+hYP2ylScg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.6", + "@formatjs/intl-displaynames": "6.6.6", + "@formatjs/intl-listformat": "7.5.5", + "intl-messageformat": "10.5.11", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "typescript": "^4.7 || 5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@edx/frontend-platform/node_modules/@formatjs/intl-displaynames": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.6.6.tgz", + "integrity": "sha512-Dg5URSjx0uzF8VZXtHb6KYZ6LFEEhCbAbKoYChYHEOnMFTw/ZU3jIo/NrujzQD2EfKPgQzIq73LOUvW6Z/LpFA==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@edx/frontend-platform/node_modules/@formatjs/intl-listformat": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.5.tgz", + "integrity": "sha512-XoI52qrU6aBGJC9KJddqnacuBbPlb/bXFN+lIFVFhQ1RnFHpzuFrlFdjD9am2O7ZSYsyqzYRpkVcXeT1GHkwDQ==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/intl-localematcher": "0.5.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@edx/frontend-platform/node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@edx/frontend-platform/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/@edx/frontend-platform/node_modules/intl-messageformat": { + "version": "10.5.11", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz", + "integrity": "sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/fast-memoize": "2.2.0", + "@formatjs/icu-messageformat-parser": "2.7.6", + "tslib": "^2.4.0" + } + }, + "node_modules/@edx/frontend-platform/node_modules/react-intl": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.6.5.tgz", + "integrity": "sha512-OErDPbGqus0QKVj77MGCC9Plbnys3CDQrq6Lw41c60pmeTdn41AhoS1SIzXG6SUlyF7qNN2AVqfrrIvHUgSyLQ==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.18.2", + "@formatjs/icu-messageformat-parser": "2.7.6", + "@formatjs/intl": "2.10.1", + "@formatjs/intl-displaynames": "6.6.6", + "@formatjs/intl-listformat": "7.5.5", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/react": "16 || 17 || 18", + "hoist-non-react-statics": "^3.3.2", + "intl-messageformat": "10.5.11", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^16.6.0 || 17 || 18", + "typescript": "^4.7 || 5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@edx/new-relic-source-map-webpack-plugin": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@edx/new-relic-source-map-webpack-plugin/-/new-relic-source-map-webpack-plugin-2.1.0.tgz", @@ -2939,6 +3051,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz", "integrity": "sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "2" } @@ -3001,6 +3114,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.15.tgz", "integrity": "sha512-i6+xVqT+6KCz7nBfk4ybMXmbKO36tKvbMKtgFz9KV+8idYFyFbfwKooYk8kGjyA5+T5f1kEPQM5IDLXucTAQ9g==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/fast-memoize": "2.2.3", @@ -3024,6 +3138,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.8.5.tgz", "integrity": "sha512-85b+GdAKCsleS6cqVxf/Aw/uBd+20EM0wDpgaxzHo3RIR3bxF4xCJqH/Grbzx8CXurTgDDZHPdPdwJC+May41w==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/intl-localematcher": "0.5.8", @@ -3035,6 +3150,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -3046,6 +3162,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "2" } @@ -3055,6 +3172,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.7.5.tgz", "integrity": "sha512-Wzes10SMNeYgnxYiKsda4rnHP3Q3II4XT2tZyOgnH5fWuHDtIkceuWlRQNsvrI3uiwP4hLqp2XdQTCsfkhXulg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/intl-localematcher": "0.5.8", @@ -3066,6 +3184,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -3077,6 +3196,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "2" } @@ -3114,6 +3234,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -3125,6 +3246,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/icu-skeleton-parser": "1.8.8", @@ -3136,6 +3258,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "tslib": "2" @@ -3146,6 +3269,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "2" } @@ -6022,6 +6146,7 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "peer": true, "dependencies": { "follow-redirects": "^1.15.4", "form-data": "^4.0.0", @@ -6029,23 +6154,16 @@ } }, "node_modules/axios-cache-interceptor": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.6.2.tgz", - "integrity": "sha512-YLbAODIHZZIcD4b3WYFVQOa5W2TY/WnJ6sBHqAg6Z+hx+RVj8/OcjQyRopO6awn7/kOkGL5X9TP16AucnlJ/lw==", - "license": "MIT", + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-0.10.7.tgz", + "integrity": "sha512-UjpxChG5DpF6Kf1IPGMLOzRDNL8ZNS6TOn1jTaVvCE7cWFU904jJwi0T1s+IbijpnLEjK2iq5uLIuR8Sj+RsFQ==", "dependencies": { - "cache-parser": "1.2.5", - "fast-defer": "1.1.8", - "object-code": "1.3.3" - }, - "engines": { - "node": ">=12" + "cache-parser": "^1.2.4", + "fast-defer": "^1.1.7", + "object-code": "^1.2.4" }, "funding": { - "url": "https://github.com/arthurfiorette/axios-cache-interceptor?sponsor=1" - }, - "peerDependencies": { - "axios": "^1" + "url": "https://github.com/ArthurFiorette/axios-cache-interceptor?sponsor=1" } }, "node_modules/axios-mock-adapter": { @@ -6609,8 +6727,7 @@ "node_modules/cache-parser": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.5.tgz", - "integrity": "sha512-Md/4VhAHByQ9frQ15WD6LrMNiVw9AEl/J7vWIXw+sxT6fSOpbtt6LHTp76vy8+bOESPBO94117Hm2bIjlI7XjA==", - "license": "MIT" + "integrity": "sha512-Md/4VhAHByQ9frQ15WD6LrMNiVw9AEl/J7vWIXw+sxT6fSOpbtt6LHTp76vy8+bOESPBO94117Hm2bIjlI7XjA==" }, "node_modules/call-bind": { "version": "1.0.7", @@ -10738,6 +10855,7 @@ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.7.tgz", "integrity": "sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/fast-memoize": "2.2.3", @@ -10750,6 +10868,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -10761,6 +10880,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/icu-skeleton-parser": "1.8.8", @@ -10772,6 +10892,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "tslib": "2" @@ -10782,6 +10903,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "2" } @@ -14243,7 +14365,8 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "peer": true }, "node_modules/psl": { "version": "1.9.0", @@ -14251,10 +14374,9 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/pubsub-js": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.5.tgz", - "integrity": "sha512-5MZ0I9i5JWVO7SizvOviKvZU2qaBbl2KQX150FAA+fJBwYpwOUId7aNygURWSdPzlsA/xZ/InUKXqBbzM0czTA==", - "license": "MIT" + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.4.tgz", + "integrity": "sha512-hJYpaDvPH4w8ZX/0Fdf9ma1AwRgU353GfbaVfPjfJQf1KxZ2iHaHl3fAUw1qlJIR5dr4F3RzjGaWohYUEyoh7A==" }, "node_modules/pump": { "version": "3.0.0", @@ -14680,6 +14802,7 @@ "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.8.9.tgz", "integrity": "sha512-TUfj5E7lyUDvz/GtovC9OMh441kBr08rtIbgh3p0R8iF3hVY+V2W9Am7rb8BpJ/29BH1utJOqOOhmvEVh3GfZg==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/icu-messageformat-parser": "2.9.4", @@ -14707,6 +14830,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -14718,6 +14842,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/icu-skeleton-parser": "1.8.8", @@ -14729,6 +14854,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "tslib": "2" @@ -14739,6 +14865,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "2" } diff --git a/package.json b/package.json index 4929bc4..ee98c78 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "dependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/frontend-component-header": "^5.6.0", - "@edx/frontend-platform": "^8.0.0", + "@edx/frontend-platform": "^7.1.2", "@edx/openedx-atlas": "^0.6.0", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", From f35420df4f68f5c5a9a7bfea018b48694e629a86 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 7 Apr 2025 17:23:04 +0200 Subject: [PATCH 27/45] refactor: rename LearningPathList to Dashboard It's more accurate, as this component displays courses, too. --- src/index.jsx | 4 ++-- src/learningpath/{LearningPathList.jsx => Dashboard.jsx} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/learningpath/{LearningPathList.jsx => Dashboard.jsx} (98%) diff --git a/src/index.jsx b/src/index.jsx index b3b3f75..2e23f5e 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -12,7 +12,7 @@ import Header from '@edx/frontend-component-header'; import FooterSlot from '@openedx/frontend-slot-footer'; import messages from './i18n'; import store from './store'; -import LearningPathList from './learningpath/LearningPathList'; +import Dashboard from './learningpath/Dashboard'; import LearningPathDetailPage from './learningpath/LearningPathDetails'; import CourseDetailPage from './learningpath/CourseDetails'; @@ -25,7 +25,7 @@ subscribe(APP_READY, () => { } + element={} /> { +const Dashboard = () => { const dispatch = useDispatch(); const { fetching: lpFetching, @@ -99,4 +99,4 @@ const LearningPathList = () => { ); }; -export default LearningPathList; +export default Dashboard; From ed93292ccb3e2ae4f3a65f844a1ead33a3420584 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 8 Apr 2025 21:35:26 +0200 Subject: [PATCH 28/45] perf: retrieve all LP details in one request on the Dashboard --- src/learningpath/data/thunks.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index f6fed6c..1ee307e 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -15,7 +15,6 @@ export const fetchLearningPaths = () => async (dispatch) => { const learningPathList = await api.fetchLearningPaths(); const learningPaths = await Promise.all( learningPathList.map(async (lp) => { - const lpDetail = await api.fetchLearningPathDetail(lp.key); const lpProgress = await api.fetchLearningPathProgress(lp.key); let status = 'In Progress'; if (lpProgress.progress === 0.0) { @@ -32,8 +31,8 @@ export const fetchLearningPaths = () => async (dispatch) => { percent = lpProgress.percent; } let maxDate = null; - const numCourses = lpDetail.steps.length; - for (const course of lpDetail.steps) { + const numCourses = lp.steps.length; + for (const course of lp.steps) { if (course.dueDate) { const dueDateObj = new Date(course.dueDate); if (!maxDate || dueDateObj > maxDate) { @@ -47,7 +46,6 @@ export const fetchLearningPaths = () => async (dispatch) => { } return { ...lp, - ...lpDetail, numCourses, status, maxDate, From b0fc060ab5fceac7d65327f804360edf6b58da75 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 8 Apr 2025 22:14:46 +0200 Subject: [PATCH 29/45] perf: retrieve course completions in one request --- src/learningpath/CourseCard.jsx | 9 ++- src/learningpath/Dashboard.jsx | 11 ++-- src/learningpath/LearningPathDetails.jsx | 15 ++++- src/learningpath/data/api.js | 39 +++++++++++-- src/learningpath/data/slice.js | 36 ++++++++++++ src/learningpath/data/thunks.js | 70 +++++++++++++++++------- src/store.js | 3 +- 7 files changed, 150 insertions(+), 33 deletions(-) diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index dd8a80b..0cf69ae 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { @@ -23,6 +23,9 @@ const CourseCard = ({ course, parentPath }) => { status, percent, } = course; + + const progressBarPercent = useMemo(() => Math.round(percent * 100), [percent]); + const linkTo = parentPath ? `${parentPath}/course/${encodeURIComponent(courseKey)}` : `/course/${encodeURIComponent(courseKey)}`; @@ -81,8 +84,8 @@ const CourseCard = ({ course, parentPath }) => { {org &&

{org}

} {status === 'In progress' && ( diff --git a/src/learningpath/Dashboard.jsx b/src/learningpath/Dashboard.jsx index b8f80d8..b3fe061 100644 --- a/src/learningpath/Dashboard.jsx +++ b/src/learningpath/Dashboard.jsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Spinner, Row, Col, Button, } from '@openedx/paragon'; -import { fetchLearningPaths, fetchCourses } from './data/thunks'; +import { fetchLearningPaths, fetchCourses, fetchCompletions } from './data/thunks'; import LearningPathCard from './LearningPathCard'; import CourseCard from './CourseCard'; import FilterPanel from './FilterPanel'; @@ -20,13 +20,16 @@ const Dashboard = () => { courses, error: coursesErrors, } = useSelector(state => state.courses); + const { fetching: completionsFetching } = useSelector(state => state.completions); useEffect(() => { - dispatch(fetchLearningPaths()); - dispatch(fetchCourses()); + dispatch(fetchCompletions()).then(() => { + dispatch(fetchLearningPaths()); + dispatch(fetchCourses()); + }); }, [dispatch]); - const isLoading = lpFetching || coursesFetching; + const isLoading = lpFetching || coursesFetching || completionsFetching; const allErrors = [].concat(lpErrors || [], coursesErrors || []); if (allErrors.length > 0) { diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index cc8d026..41215a2 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -12,14 +12,17 @@ import { FormatListBulleted, AccessTimeFilled, } from '@openedx/paragon/icons'; +import { useSelector, useDispatch } from 'react-redux'; import { buildAssetUrl } from '../util/assetUrl'; import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; +import { fetchCompletions } from './data/thunks'; import CourseCard from './CourseCard'; import CourseDetailPage from './CourseDetails'; const LearningPathDetailPage = () => { const { key } = useParams(); const navigate = useNavigate(); + const dispatch = useDispatch(); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -27,6 +30,14 @@ const LearningPathDetailPage = () => { const [loadingCourses, setLoadingCourses] = useState(false); const [coursesError, setCoursesError] = useState(null); + const { completions, fetching: fetchingCompletions } = useSelector((state) => state.completions); + + useEffect(() => { + if (!fetchingCompletions && Object.keys(completions).length === 0) { + dispatch(fetchCompletions()); + } + }, [dispatch, completions, fetchingCompletions]); + useEffect(() => { async function loadDetail() { try { @@ -52,7 +63,7 @@ const LearningPathDetailPage = () => { try { setLoadingCourses(true); setCoursesError(null); - const courses = await fetchCoursesByIds(courseIds); + const courses = await fetchCoursesByIds(courseIds, completions); setCoursesForPath(courses); } catch (err) { // eslint-disable-next-line no-console @@ -63,7 +74,7 @@ const LearningPathDetailPage = () => { } } loadCourses(); - }, [courseIds]); + }, [courseIds, completions]); const accessUntilDate = useMemo(() => { let maxDate = null; diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index 0878bb1..3ece0c6 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -77,6 +77,29 @@ export async function fetchCourseCompletion(courseId) { return 0.0; } +export async function fetchAllCourseCompletions() { + const { username } = getAuthenticatedUser(); + const client = getAuthenticatedHttpClient(); + + let allResults = []; + let nextUrl = `${getConfig().LMS_BASE_URL}/completion-aggregator/v1/course/?username=${username}&page_size=10000`; + + while (nextUrl) { + // eslint-disable-next-line no-await-in-loop + const response = await client.get(nextUrl); + const results = response.data.results || []; + + allResults = [...allResults, ...results]; + + nextUrl = response.data.pagination?.next ? response.data.pagination.next : null; + } + + return camelCaseObject(allResults.map(item => ({ + course_key: item.course_key, + completion: item.completion, + }))); +} + export async function fetchCombinedCourseInfo(courseId) { const basicInfo = await fetchCourses(courseId); const details = await fetchCourseDetails(courseId); @@ -86,17 +109,25 @@ export async function fetchCombinedCourseInfo(courseId) { }); } -export async function fetchCoursesByIds(courseIds) { +export async function fetchCoursesByIds(courseIds, completions = {}) { const combined = await Promise.all( courseIds.map(async (courseId) => { const combinedInfo = await fetchCombinedCourseInfo(courseId); - const percent = await fetchCourseCompletion(courseId); + + let percent = 0; + if (completions[courseId]?.percent !== undefined) { + percent = completions[courseId].percent; + } else { + percent = await fetchCourseCompletion(courseId); + } + let status = 'In progress'; - if (percent === 0.0) { + if (percent <= 0.0) { status = 'Not started'; - } else if (percent === 100.0) { + } else if (percent >= 1.0) { status = 'Completed'; } + return camelCaseObject({ ...combinedInfo, status, diff --git a/src/learningpath/data/slice.js b/src/learningpath/data/slice.js index 2bbc400..1797f2a 100644 --- a/src/learningpath/data/slice.js +++ b/src/learningpath/data/slice.js @@ -13,6 +13,12 @@ const initialCoursesState = () => ({ errors: [], }); +const initialCompletionsState = () => ({ + fetching: false, + completions: {}, + errors: [], +}); + const coursesSlice = createSlice({ name: 'courses', initialState: initialCoursesState(), @@ -33,6 +39,29 @@ const coursesSlice = createSlice({ }, }); +const completionsSlice = createSlice({ + name: 'completions', + initialState: initialCompletionsState(), + reducers: { + fetchCompletionsRequest(state) { + state.fetching = true; + state.errors = []; + }, + fetchCompletionsSuccess(state, action) { + state.fetching = false; + const completionsMap = {}; + action.payload.completions.forEach(completion => { + completionsMap[completion.courseKey] = completion.completion; + }); + state.completions = completionsMap; + }, + fetchCompletionsFailure(state, action) { + state.fetching = false; + state.errors = action.payload.errors; + }, + }, +}); + const learningPathSlice = createSlice({ name: 'learningPath', initialState: initialLearningPathState(), @@ -65,5 +94,12 @@ export const { fetchCoursesFailure, } = coursesSlice.actions; +export const { + fetchCompletionsRequest, + fetchCompletionsSuccess, + fetchCompletionsFailure, +} = completionsSlice.actions; + export const learningPathReducer = learningPathSlice.reducer; export const coursesReducer = coursesSlice.reducer; +export const completionsReducer = completionsSlice.reducer; diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js index 1ee307e..98493dd 100644 --- a/src/learningpath/data/thunks.js +++ b/src/learningpath/data/thunks.js @@ -6,11 +6,39 @@ import { fetchCoursesRequest, fetchCoursesSuccess, fetchCoursesFailure, + fetchCompletionsRequest, + fetchCompletionsSuccess, + fetchCompletionsFailure, } from './slice'; +const shouldFetchCompletions = (state) => { + const { fetching, completions, errors } = state.completions; + return !fetching && (Object.keys(completions).length === 0 || errors.length > 0); +}; + +export const fetchCompletions = () => async (dispatch, getState) => { + if (!shouldFetchCompletions(getState())) { + const { completions: { completions } } = getState(); + return Promise.resolve(completions); + } + + try { + dispatch(fetchCompletionsRequest()); + const completions = await api.fetchAllCourseCompletions(); + dispatch(fetchCompletionsSuccess({ completions })); + return completions; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to fetch course completions:', error); + dispatch(fetchCompletionsFailure({ errors: [String(error)] })); + return []; + } +}; + export const fetchLearningPaths = () => async (dispatch) => { try { dispatch(fetchLearningPathsRequest()); + await dispatch(fetchCompletions()); const type = 'learning_path'; const learningPathList = await api.fetchLearningPaths(); const learningPaths = await Promise.all( @@ -62,29 +90,33 @@ export const fetchLearningPaths = () => async (dispatch) => { } }; -export const fetchCourses = () => async (dispatch) => { +export const fetchCourses = () => async (dispatch, getState) => { try { dispatch(fetchCoursesRequest()); + await dispatch(fetchCompletions()); const type = 'course'; const courses = await api.fetchAllCourseDetails(); - const coursesWithStatus = await Promise.all( - courses.map(async (course) => { - const courseKey = `course-v1:${course.org}+${course.courseId}+${course.run}`; - const percent = await api.fetchCourseCompletion(courseKey); - let status = 'In progress'; - if (percent === 0.0) { - status = 'Not started'; - } else if (percent === 100.0) { - status = 'Completed'; - } - return { - ...course, - status, - percent, - type, - }; - }), - ); + const { completions } = getState().completions; + + const coursesWithStatus = courses.map(course => { + const courseKey = `course-v1:${course.org}+${course.courseId}+${course.run}`; + const completion = completions[courseKey]?.percent || 0; + + let status = 'In progress'; + if (completion <= 0.0) { + status = 'Not started'; + } else if (completion >= 1.0) { + status = 'Completed'; + } + + return { + ...course, + status, + percent: completion, + type, + }; + }); + dispatch(fetchCoursesSuccess({ courses: coursesWithStatus })); } catch (error) { // eslint-disable-next-line no-console diff --git a/src/store.js b/src/store.js index 7b43333..fcec2f0 100644 --- a/src/store.js +++ b/src/store.js @@ -1,10 +1,11 @@ import { configureStore } from '@reduxjs/toolkit'; -import { learningPathReducer, coursesReducer } from './learningpath/data/slice'; +import { learningPathReducer, coursesReducer, completionsReducer } from './learningpath/data/slice'; const store = configureStore({ reducer: { learningPath: learningPathReducer, courses: coursesReducer, + completions: completionsReducer, }, }); From c1aba163f896068c1e26256791ed4857bdbd656e Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 8 Apr 2025 22:33:42 +0200 Subject: [PATCH 30/45] chore: upgrade dependencies --- package-lock.json | 3799 ++++++++++++++++++++++++++------------------- package.json | 24 +- 2 files changed, 2208 insertions(+), 1615 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf76f14..b910502 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,22 +9,22 @@ "version": "0.1.0", "license": "AGPL-3.0", "dependencies": { - "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", - "@edx/frontend-component-header": "^5.6.0", - "@edx/frontend-platform": "^7.1.2", - "@edx/openedx-atlas": "^0.6.0", + "@edx/brand": "npm:@openedx/brand-openedx@^1.2.3", + "@edx/frontend-component-header": "^6.3.0", + "@edx/frontend-platform": "^8.3.4", + "@edx/openedx-atlas": "^0.7.0", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.2", - "@openedx/frontend-slot-footer": "^1.0.2", - "@openedx/paragon": "^22.0.0", + "@openedx/frontend-slot-footer": "^1.2.0", + "@openedx/paragon": "^22.17.0", "@reduxjs/toolkit": "^2.6.1", - "core-js": "3.40.0", + "core-js": "3.41.0", "prop-types": "15.8.1", - "react": "17.0.2", - "react-dom": "17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-redux": "7.2.9", "react-router": "6.29.0", "react-router-dom": "6.29.0", @@ -32,9 +32,9 @@ "regenerator-runtime": "0.14.1" }, "devDependencies": { - "@edx/browserslist-config": "^1.1.1", - "@edx/reactifex": "^2.1.1", - "@openedx/frontend-build": "14.2.2", + "@edx/browserslist-config": "^1.5.0", + "@edx/reactifex": "^2.2.0", + "@openedx/frontend-build": "14.4.2", "glob": "7.2.3", "husky": "7.0.4", "jest": "29.7.0" @@ -96,12 +96,12 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "license": "MIT", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -171,27 +171,26 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "dependencies": { - "@babel/types": "^7.25.0", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -332,13 +331,12 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -375,10 +373,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", - "license": "MIT", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "engines": { "node": ">=6.9.0" } @@ -456,28 +453,25 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "engines": { "node": ">=6.9.0" } @@ -510,97 +504,13 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dependencies": { - "has-flag": "^3.0.0" + "@babel/types": "^7.27.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz", - "integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==", - "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -842,12 +752,11 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1594,12 +1503,11 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", - "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz", + "integrity": "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1609,16 +1517,15 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", - "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz", + "integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1628,12 +1535,11 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", - "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz", + "integrity": "sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==", "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.24.7" + "@babel/plugin-transform-react-jsx": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1643,13 +1549,12 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", - "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz", + "integrity": "sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1955,17 +1860,16 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", - "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", - "license": "MIT", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.26.3.tgz", + "integrity": "sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.24.7", - "@babel/plugin-transform-react-jsx-development": "^7.24.7", - "@babel/plugin-transform-react-pure-annotations": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-transform-react-display-name": "^7.25.9", + "@babel/plugin-transform-react-jsx": "^7.25.9", + "@babel/plugin-transform-react-jsx-development": "^7.25.9", + "@babel/plugin-transform-react-pure-annotations": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2009,9 +1913,9 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.24.0.tgz", - "integrity": "sha512-HxiRMOncx3ly6f3fcZ1GVKf+/EROcI9qwPgmij8Czqy6Okm/0T37T4y2ZIlLUuEUFjtM7NRsfdCO8Y3tAiJZew==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz", + "integrity": "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==", "dependencies": { "core-js-pure": "^3.30.2", "regenerator-runtime": "^0.14.0" @@ -2021,30 +1925,28 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.1.tgz", - "integrity": "sha512-LrHHoWq08ZpmmFqBAzN+hUdWwy5zt7FGa/hVwMcOqW6OVtwqaoD5utfuGYU87JYxdZgLUvktAsn37j/sYR9siA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2053,14 +1955,12 @@ } }, "node_modules/@babel/types": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.0.tgz", - "integrity": "sha512-LcnxQSsd9aXOIgmmSpvZ/1yo46ra2ESYyqLcryaBZOghxy5qqOBjvCWP5JfkI8yl9rlxRgdLTTMCQQRcN2hdCg==", - "license": "MIT", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2205,525 +2105,180 @@ } }, "node_modules/@edx/frontend-component-footer": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-13.2.0.tgz", - "integrity": "sha512-oXcNSZ+1o1TFIolBmd3mdQsDQ0fzJZMK5hNvEIwWbyLNX6977t7QjyihocYlLY+0LMl3LmqdSJpSuvNzBrvCyA==", + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.4.0.tgz", + "integrity": "sha512-JGMCTV77+Mb0CBqfE+vv82F2nCHpY3SULRaRcJnralsoMuAWGo8eYzV0yRnwye7isv2siMetBAAZ9yebJzISNw==", "peer": true, "dependencies": { - "@fortawesome/fontawesome-svg-core": "6.5.2", - "@fortawesome/free-brands-svg-icons": "6.5.2", - "@fortawesome/free-regular-svg-icons": "6.5.2", - "@fortawesome/free-solid-svg-icons": "6.5.2", - "@fortawesome/react-fontawesome": "0.2.0", + "@fortawesome/fontawesome-svg-core": "6.7.2", + "@fortawesome/free-brands-svg-icons": "6.7.2", + "@fortawesome/free-regular-svg-icons": "6.7.2", + "@fortawesome/free-solid-svg-icons": "6.7.2", + "@fortawesome/react-fontawesome": "0.2.2", + "classnames": "^2.5.1", "jest-environment-jsdom": "^29.7.0", "lodash": "^4.17.21", "ts-jest": "^29.1.2" }, "peerDependencies": { "@edx/frontend-platform": "^7.0.0 || ^8.0.0", - "@openedx/paragon": ">= 21.11.3 < 23.0.0", + "@openedx/paragon": ">= 21.11.3 < 24.0.0", "prop-types": "^15.5.10", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0" + "react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", - "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", - "hasInstallScript": true, + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", "peer": true, "engines": { "node": ">=6" } }, "node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", - "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", - "hasInstallScript": true, + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "peer": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, "node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-brands-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz", - "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==", - "hasInstallScript": true, + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz", + "integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==", "peer": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, "node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.5.2.tgz", - "integrity": "sha512-iabw/f5f8Uy2nTRtJ13XZTS1O5+t+anvlamJ3zJGLEVE2pKsAWhPv2lq01uQlfgCX7VaveT3EVs515cCN9jRbw==", - "hasInstallScript": true, + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", + "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", "peer": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, "node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", - "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", - "hasInstallScript": true, + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", "peer": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, - "node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/react-fontawesome": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", - "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", - "license": "MIT", - "peer": true, + "node_modules/@edx/frontend-component-header": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.3.0.tgz", + "integrity": "sha512-sN59uaK7rV7csFKyEE24+Xi4dsWRwaIotv2O7vcOXMr5UV4bNAiiYduOR1YFMHgOsUIdZiQkqwykLlg+Hk7Hug==", "dependencies": { - "prop-types": "^15.8.1" + "@fortawesome/fontawesome-svg-core": "6.6.0", + "@fortawesome/free-brands-svg-icons": "6.6.0", + "@fortawesome/free-regular-svg-icons": "6.6.0", + "@fortawesome/free-solid-svg-icons": "6.6.0", + "@fortawesome/react-fontawesome": "^0.2.0", + "@openedx/frontend-plugin-framework": "^1.6.0", + "axios-mock-adapter": "1.22.0", + "babel-polyfill": "6.26.0", + "classnames": "^2.5.1", + "jest-environment-jsdom": "^29.7.0", + "react-responsive": "8.2.0", + "react-transition-group": "4.4.5" }, "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~1 || ~6", - "react": ">=16.3" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "@edx/frontend-platform": "^7.0.0 || ^8.0.0", + "@openedx/paragon": ">= 22.0.0 < 24.0.0", + "prop-types": "^15.5.10", + "react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react-router-dom": "^6.14.2" } }, - "node_modules/@edx/frontend-component-footer/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "peer": true, + "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", + "license": "MIT", "engines": { - "node": ">= 10" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "peer": true, - "dependencies": { - "@types/yargs-parser": "*" + "node": ">=6" } }, - "node_modules/@edx/frontend-component-footer/node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "peer": true, + "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "license": "MIT", "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/@edx/frontend-component-footer/node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "peer": true - }, - "node_modules/@edx/frontend-component-footer/node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "peer": true, + "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", + "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", + "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/@edx/frontend-component-footer/node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "peer": true, + "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", + "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", + "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "webidl-conversions": "^7.0.0" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/@edx/frontend-component-footer/node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "peer": true, + "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "whatwg-encoding": "^2.0.0" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/@edx/frontend-component-footer/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "peer": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", - "peer": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "peer": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "peer": true, - "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "peer": true, - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "peer": true, - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "peer": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "peer": true, - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "peer": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "peer": true, - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@edx/frontend-component-footer/node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@edx/frontend-component-header": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-5.8.2.tgz", - "integrity": "sha512-JfIzlZ5p9c6K5GQls8X8NPhih1lLdL2eojxqx3VMdfVoVrMw2sDgcX2YDzuebNP7HJAWm4VJ95wJtSEXg5KD6g==", - "license": "AGPL-3.0", - "dependencies": { - "@fortawesome/fontawesome-svg-core": "6.6.0", - "@fortawesome/free-brands-svg-icons": "6.6.0", - "@fortawesome/free-regular-svg-icons": "6.6.0", - "@fortawesome/free-solid-svg-icons": "6.6.0", - "@fortawesome/react-fontawesome": "^0.2.0", - "@openedx/frontend-plugin-framework": "^1.3.0", - "axios-mock-adapter": "1.22.0", - "babel-polyfill": "6.26.0", - "classnames": "^2.5.1", - "jest-environment-jsdom": "^29.7.0", - "react-responsive": "8.2.0", - "react-transition-group": "4.4.5" - }, - "peerDependencies": { - "@edx/frontend-platform": "^7.0.0 || ^8.0.0", - "@openedx/paragon": ">= 21.5.7 < 23.0.0", - "prop-types": "^15.5.10", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0", - "react-router-dom": "^6.14.2" - } - }, - "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", - "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", - "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", - "license": "MIT", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", - "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", - "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", - "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@edx/frontend-platform": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-7.1.4.tgz", - "integrity": "sha512-ahsz9CCrBdcWJPMX/YM9cQCCWL7PeVGy+zYNvdvPoy/zVxQc9byNTzpZTsQYuTcg8E1zXRiAci4IUBqD9EMQNQ==", + "node_modules/@edx/frontend-platform": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.4.tgz", + "integrity": "sha512-V3XtTo3KP8QSmId+Vvi4+qzpOVkxvTMNA6jH/i3Bfz+/jHjHBRnmp/Cc2pjTxiTgGNoKX4D1twiZkOBO+kWw1Q==", "dependencies": { "@cospired/i18n-iso-languages": "4.2.0", "@formatjs/intl-pluralrules": "4.3.3", "@formatjs/intl-relativetimeformat": "10.0.1", - "axios": "0.27.2", - "axios-cache-interceptor": "0.10.7", + "axios": "1.8.4", + "axios-cache-interceptor": "1.6.2", "form-urlencoded": "4.1.4", "glob": "7.2.3", "history": "4.10.1", @@ -2735,8 +2290,8 @@ "lodash.memoize": "4.1.2", "lodash.merge": "4.6.2", "lodash.snakecase": "4.1.1", - "pubsub-js": "1.9.4", - "react-intl": "6.6.5", + "pubsub-js": "1.9.5", + "react-intl": "6.8.9", "universal-cookie": "4.0.4" }, "bin": { @@ -2744,129 +2299,16 @@ "transifex-utils.js": "i18n/scripts/transifex-utils.js" }, "peerDependencies": { - "@openedx/frontend-build": ">= 13.0.15", - "@openedx/paragon": ">= 21.5.7 < 23.0.0", - "prop-types": "^15.7.2", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0", + "@openedx/frontend-build": ">= 14.0.0", + "@openedx/paragon": ">= 21.5.7 < 24.0.0", + "prop-types": ">=15.7.2 <16.0.0", + "react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0", "react-redux": "^7.1.1 || ^8.1.1", "react-router-dom": "^6.0.0", "redux": "^4.0.4" } }, - "node_modules/@edx/frontend-platform/node_modules/@formatjs/ecma402-abstract": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz", - "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==", - "dependencies": { - "@formatjs/intl-localematcher": "0.5.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@edx/frontend-platform/node_modules/@formatjs/fast-memoize": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz", - "integrity": "sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@edx/frontend-platform/node_modules/@formatjs/intl": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.1.tgz", - "integrity": "sha512-dsLG15U7xDi8yzKf4hcAWSsCaez3XrjTO2oaRHPyHtXLm1aEzYbDw6bClo/HMHu+iwS5GbDqT3DV+hYP2ylScg==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.18.2", - "@formatjs/fast-memoize": "2.2.0", - "@formatjs/icu-messageformat-parser": "2.7.6", - "@formatjs/intl-displaynames": "6.6.6", - "@formatjs/intl-listformat": "7.5.5", - "intl-messageformat": "10.5.11", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "typescript": "^4.7 || 5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@edx/frontend-platform/node_modules/@formatjs/intl-displaynames": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.6.6.tgz", - "integrity": "sha512-Dg5URSjx0uzF8VZXtHb6KYZ6LFEEhCbAbKoYChYHEOnMFTw/ZU3jIo/NrujzQD2EfKPgQzIq73LOUvW6Z/LpFA==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.18.2", - "@formatjs/intl-localematcher": "0.5.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@edx/frontend-platform/node_modules/@formatjs/intl-listformat": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.5.5.tgz", - "integrity": "sha512-XoI52qrU6aBGJC9KJddqnacuBbPlb/bXFN+lIFVFhQ1RnFHpzuFrlFdjD9am2O7ZSYsyqzYRpkVcXeT1GHkwDQ==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.18.2", - "@formatjs/intl-localematcher": "0.5.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@edx/frontend-platform/node_modules/@formatjs/intl-localematcher": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", - "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@edx/frontend-platform/node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/@edx/frontend-platform/node_modules/intl-messageformat": { - "version": "10.5.11", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.11.tgz", - "integrity": "sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.18.2", - "@formatjs/fast-memoize": "2.2.0", - "@formatjs/icu-messageformat-parser": "2.7.6", - "tslib": "^2.4.0" - } - }, - "node_modules/@edx/frontend-platform/node_modules/react-intl": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.6.5.tgz", - "integrity": "sha512-OErDPbGqus0QKVj77MGCC9Plbnys3CDQrq6Lw41c60pmeTdn41AhoS1SIzXG6SUlyF7qNN2AVqfrrIvHUgSyLQ==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.18.2", - "@formatjs/icu-messageformat-parser": "2.7.6", - "@formatjs/intl": "2.10.1", - "@formatjs/intl-displaynames": "6.6.6", - "@formatjs/intl-listformat": "7.5.5", - "@types/hoist-non-react-statics": "^3.3.1", - "@types/react": "16 || 17 || 18", - "hoist-non-react-statics": "^3.3.2", - "intl-messageformat": "10.5.11", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "react": "^16.6.0 || 17 || 18", - "typescript": "^4.7 || 5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@edx/new-relic-source-map-webpack-plugin": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@edx/new-relic-source-map-webpack-plugin/-/new-relic-source-map-webpack-plugin-2.1.0.tgz", @@ -2876,9 +2318,9 @@ } }, "node_modules/@edx/openedx-atlas": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.6.2.tgz", - "integrity": "sha512-28Q8vzJDMS4wUxdkbIUBQpzWJ3HTdMaGlaEhFjrVGfuZkh++1AG6Tn/7FMD88cegalYAkphu530VQCHEkMZQhw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.7.0.tgz", + "integrity": "sha512-jqv0IV1pHsSn9+RO8Rdsr8jm3SOd84CCzzmo2QC9yvh1MK1+p4YDURQLpmmgKJ0JzE5Cb6ImhnNL/ogpJ2wetQ==", "bin": { "atlas": "atlas" } @@ -2914,6 +2356,34 @@ "typescript": "^4.9.4" } }, + "node_modules/@emnapi/core": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.0.tgz", + "integrity": "sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz", + "integrity": "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", + "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3050,8 +2520,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz", "integrity": "sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==", - "license": "MIT", - "peer": true, "dependencies": { "tslib": "2" } @@ -3113,8 +2581,6 @@ "version": "2.10.15", "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.15.tgz", "integrity": "sha512-i6+xVqT+6KCz7nBfk4ybMXmbKO36tKvbMKtgFz9KV+8idYFyFbfwKooYk8kGjyA5+T5f1kEPQM5IDLXucTAQ9g==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/fast-memoize": "2.2.3", @@ -3137,8 +2603,6 @@ "version": "6.8.5", "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.8.5.tgz", "integrity": "sha512-85b+GdAKCsleS6cqVxf/Aw/uBd+20EM0wDpgaxzHo3RIR3bxF4xCJqH/Grbzx8CXurTgDDZHPdPdwJC+May41w==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/intl-localematcher": "0.5.8", @@ -3149,8 +2613,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -3161,8 +2623,6 @@ "version": "0.5.8", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", - "license": "MIT", - "peer": true, "dependencies": { "tslib": "2" } @@ -3171,8 +2631,6 @@ "version": "7.7.5", "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.7.5.tgz", "integrity": "sha512-Wzes10SMNeYgnxYiKsda4rnHP3Q3II4XT2tZyOgnH5fWuHDtIkceuWlRQNsvrI3uiwP4hLqp2XdQTCsfkhXulg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/intl-localematcher": "0.5.8", @@ -3183,8 +2641,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -3195,8 +2651,6 @@ "version": "0.5.8", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", - "license": "MIT", - "peer": true, "dependencies": { "tslib": "2" } @@ -3233,8 +2687,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -3245,8 +2697,6 @@ "version": "2.9.4", "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/icu-skeleton-parser": "1.8.8", @@ -3257,8 +2707,6 @@ "version": "1.8.8", "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "tslib": "2" @@ -3268,8 +2716,6 @@ "version": "0.5.8", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", - "license": "MIT", - "peer": true, "dependencies": { "tslib": "2" } @@ -3914,6 +3360,17 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz", + "integrity": "sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@tybys/wasm-util": "^0.9.0" + } + }, "node_modules/@newrelic/publish-sourcemap": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@newrelic/publish-sourcemap/-/publish-sourcemap-5.1.0.tgz", @@ -4026,10 +3483,9 @@ } }, "node_modules/@openedx/frontend-build": { - "version": "14.2.2", - "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.2.2.tgz", - "integrity": "sha512-RLhRoYE8+9A4YknEZyZwLreXeUV4u+QKQXLFf07H8YkW2U6A+8f/ANMDb6Vqw4NsQ/s6eg8SjzpWKwX1FO7qwg==", - "license": "AGPL-3.0", + "version": "14.4.2", + "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.4.2.tgz", + "integrity": "sha512-RWAsaYq88cGlqO4eDDo/ylY6dJsbeBsI+4LxmKPrW4MPh0rIFZILvH+X3z/t9SVTlGTx4UkUQV9LXPGLKdcA1g==", "dependencies": { "@babel/cli": "7.24.8", "@babel/core": "7.24.9", @@ -4038,7 +3494,7 @@ "@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/preset-env": "7.24.8", - "@babel/preset-react": "7.24.7", + "@babel/preset-react": "7.26.3", "@edx/eslint-config": "^4.3.0", "@edx/new-relic-source-map-webpack-plugin": "2.1.0", "@edx/typescript-config": "1.1.0", @@ -4050,8 +3506,8 @@ "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0", "autoprefixer": "10.4.20", - "babel-jest": "29.6.1", - "babel-loader": "9.1.3", + "babel-jest": "29.7.0", + "babel-loader": "9.2.1", "babel-plugin-formatjs": "^10.4.0", "babel-plugin-transform-imports": "2.0.0", "babel-polyfill": "6.26.0", @@ -4064,18 +3520,19 @@ "eslint": "8.44.0", "eslint-config-airbnb": "19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-import-resolver-typescript": "^4.2.1", "eslint-plugin-formatjs": "^4.12.2", - "eslint-plugin-import": "2.27.5", + "eslint-plugin-import": "2.31.0", "eslint-plugin-jsx-a11y": "6.7.1", - "eslint-plugin-react": "7.32.2", - "eslint-plugin-react-hooks": "4.6.0", + "eslint-plugin-react": "7.33.2", + "eslint-plugin-react-hooks": "4.6.1", "express": "^4.18.2", "file-loader": "6.2.0", "html-webpack-plugin": "5.6.3", "identity-obj-proxy": "3.0.0", "image-minimizer-webpack-plugin": "3.8.3", - "jest": "29.6.1", - "jest-environment-jsdom": "29.6.1", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", "mini-css-extract-plugin": "1.6.2", "parse5": "7.1.2", "postcss": "8.4.49", @@ -4083,17 +3540,18 @@ "postcss-loader": "7.3.4", "postcss-rtlcss": "5.1.2", "react-dev-utils": "12.0.1", - "react-refresh": "0.14.2", + "react-refresh": "0.16.0", "resolve-url-loader": "5.0.0", - "sass": "1.69.7", + "sass": "1.85.1", "sass-loader": "13.3.3", "sharp": "0.32.6", "source-map-loader": "4.0.2", "style-loader": "3.3.4", "ts-jest": "29.1.4", + "tsconfig-paths-webpack-plugin": "^4.2.0", "typescript": "4.9.5", "url-loader": "4.1.1", - "webpack": "^5.89.0", + "webpack": "^5.97.1", "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", @@ -4104,81 +3562,26 @@ "fedx-scripts": "bin/fedx-scripts.js" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0" - } - }, - "node_modules/@openedx/frontend-build/node_modules/jest": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.1.tgz", - "integrity": "sha512-Nirw5B4nn69rVUZtemCQhwxOBhm0nsp3hmtF4rzCeWD7BkjAXRIji7xWQfnTNbz9g0aVsBX6aZK3n+23LM6uDw==", - "license": "MIT", - "dependencies": { - "@jest/core": "^29.6.1", - "@jest/types": "^29.6.1", - "import-local": "^3.0.2", - "jest-cli": "^29.6.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@openedx/frontend-build/node_modules/jest-environment-jsdom": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.6.1.tgz", - "integrity": "sha512-PoY+yLaHzVRhVEjcVKSfJ7wXmJW4UqPYNhR05h7u/TK0ouf6DmRNZFBL/Z00zgQMyWGMBXn69/FmOvhEJu8cIw==", - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.6.1", - "@jest/fake-timers": "^29.6.1", - "@jest/types": "^29.6.1", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.6.1", - "jest-util": "^29.6.1", - "jsdom": "^20.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } + "react": "^16.9.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@openedx/frontend-plugin-framework": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.3.0.tgz", - "integrity": "sha512-qLtX/4HIuWXiIhBdtBuL6mPVbV2un0rsFYx3I5+3tIUf7+T7WRq81a6JHU5QGyAmZy9dfiv7QwbqwiEQOVXVuQ==", - "license": "AGPL-3.0", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.6.0.tgz", + "integrity": "sha512-zgP+/hs/cvcPmFOgVm2xt/qgX1nheNsfipzCO7I3bON4hHyOhmOyzwFZJ7pz7GzCJwKlMVguh3HcJgf4p/BPKQ==", "dependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "classnames": "^2.3.2", "core-js": "3.37.1", - "react-redux": "7.2.9", - "redux": "4.2.1", - "regenerator-runtime": "0.14.1" + "react-redux": "8.1.1", + "redux": "4.2.1" }, "peerDependencies": { "@edx/frontend-platform": "^7.0.0 || ^8.0.0", - "@openedx/paragon": "^21.0.0 || ^22.0.0", + "@openedx/paragon": "^21.0.0 || ^22.0.0 || ^23.0.0", "prop-types": "^15.8.0", - "react": "^17.0.0", - "react-dom": "^17.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", "react-error-boundary": "^4.0.11" } }, @@ -4187,37 +3590,66 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, + "node_modules/@openedx/frontend-plugin-framework/node_modules/react-redux": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz", + "integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/@openedx/frontend-slot-footer": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@openedx/frontend-slot-footer/-/frontend-slot-footer-1.0.7.tgz", - "integrity": "sha512-lRVl8R0KkW+Y1pL0KzfwoXjYMMz6Q4CgquSuDOJNOQlFpvbaT9DB4g1blGQjxK/hT5qZOZ1LcBSZCpMy+ggXKA==", - "license": "AGPL-3.0", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@openedx/frontend-slot-footer/-/frontend-slot-footer-1.2.0.tgz", + "integrity": "sha512-bJuqgdiAlPRj1QuUOJWtNqGTCTcdsk4vHeOM3jRkxtWycq+j1JpGnnZEWAmjoRv9dDKr39vt2buNrmvj0sCTbA==", "dependencies": { - "@openedx/frontend-plugin-framework": "^1.1.2" + "@openedx/frontend-plugin-framework": "^1.5.0" }, "peerDependencies": { "@edx/frontend-component-footer": "*", - "react": "^17.0.0" + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" } }, "node_modules/@openedx/paragon": { - "version": "22.15.1", - "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.15.1.tgz", - "integrity": "sha512-cyd3JZYzsM3WChcgdQ4UIZxxXKkiU+QhIGDQ5jszuJR4mZR7CsU6DnANVcrcIqZgz4i4wUhG15NOAMbUt9LCzQ==", - "license": "Apache-2.0", - "workspaces": [ - "example", - "component-generator", - "www", - "icons", - "dependent-usage-analyzer" - ], + "version": "22.17.0", + "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.17.0.tgz", + "integrity": "sha512-MzOLQ0myaOErwumPJwxVZXTw7zJKrARtu4YMSaISF5Sz6pE1/dYz9qfRcqaraYRcJGNdbPRzOG0v3iqbZo1uHQ==", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", @@ -4262,7 +3694,6 @@ "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", - "license": "MIT", "engines": { "node": ">=6" } @@ -4271,7 +3702,6 @@ "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", - "license": "MIT", "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, @@ -4283,56 +3713,355 @@ "version": "0.1.19", "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", - "license": "MIT", "dependencies": { "prop-types": "^15.8.1" }, - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~1 || ~6", - "react": ">=16.x" + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.x" + } + }, + "node_modules/@openedx/paragon/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@openedx/paragon/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@openedx/paragon/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@openedx/paragon/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@openedx/paragon/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": ">= 10.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@openedx/paragon/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" }, "engines": { - "node": ">=10" + "node": ">=0.10" } }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "optional": true + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", @@ -4525,6 +4254,11 @@ "react": ">=16.8.0" } }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4805,6 +4539,15 @@ "node": ">=10.13.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4908,9 +4651,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" }, "node_modules/@types/express": { "version": "4.17.21", @@ -5111,10 +4854,10 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", - "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", - "dependencies": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "peerDependencies": { "@types/react": "*" } }, @@ -5178,6 +4921,11 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", @@ -5485,134 +5233,317 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.4.1.tgz", + "integrity": "sha512-8Tv+Bsd0BjGwfEedIyor4inw8atppRxM5BdUnIt+3mAm/QXUm7Dw74CHnXpfZKXkp07EXJGiA8hStqCINAWhdw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.4.1.tgz", + "integrity": "sha512-X8c3PhWziEMKAzZz+YAYWfwawi5AEgzy/hmfizAB4C70gMHLKmInJcp1270yYAOs7z07YVFI220pp50z24Jk3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.4.1.tgz", + "integrity": "sha512-UUr/nREy1UdtxXQnmLaaTXFGOcGxPwNIzeJdb3KXai3TKtC1UgNOB9s8KOA4TaxOUBR/qVgL5BvBwmUjD5yuVA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.4.1.tgz", + "integrity": "sha512-e3pII53dEeS8inkX6A1ad2UXE0nuoWCqik4kOxaDnls0uJUq0ntdj5d9IYd+bv5TDwf9DSge/xPOvCmRYH+Tsw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.4.1.tgz", + "integrity": "sha512-e/AKKd9gR+HNmVyDEPI/PIz2t0DrA3cyonHNhHVjrkxe8pMCiYiqhtn1+h+yIpHUtUlM6Y1FNIdivFa+r7wrEQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.4.1.tgz", + "integrity": "sha512-vtIu34luF1jRktlHtiwm2mjuE8oJCsFiFr8hT5+tFQdqFKjPhbJXn83LswKsOhy0GxAEevpXDI4xxEwkjuXIPA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.4.1.tgz", + "integrity": "sha512-H3PaOuGyhFXiyJd+09uPhGl4gocmhyi1BRzvsP8Lv5AQO3p3/ZY7WjV4t2NkBksm9tMjf3YbOVHyPWi2eWsNYw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.4.1.tgz", + "integrity": "sha512-4+GmJcaaFntCi1S01YByqp8wLMjV/FyQyHVGm0vedIhL1Vfx7uHkz/sZmKsidRwokBGuxi92GFmSzqT2O8KcNA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.4.1.tgz", + "integrity": "sha512-6RDQVCmtFYTlhy89D5ixTqo9bTQqFhvNN0Ey1wJs5r+01Dq15gPHRXv2jF2bQATtMrOfYwv+R2ZR9ew1N1N3YQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.4.1.tgz", + "integrity": "sha512-XpU9uzIkD86+19NjCXxlVPISMUrVXsXo5htxtuG+uJ59p5JauSRZsIxQxzzfKzkxEjdvANPM/lS1HFoX6A6QeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.4.1.tgz", + "integrity": "sha512-3CDjG/spbTKCSHl66QP2ekHSD+H34i7utuDIM5gzoNBcZ1gTO0Op09Wx5cikXnhORRf9+HyDWzm37vU1PLSM1A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.4.1.tgz", + "integrity": "sha512-50tYhvbCTnuzMn7vmP8IV2UKF7ITo1oihygEYq9wW2DUb/Y+QMqBHJUSCABRngATjZ4shOK6f2+s0gQX6ElENQ==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.8" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.4.1.tgz", + "integrity": "sha512-KyJiIne/AqV4IW0wyQO34wSMuJwy3VxVQOfIXIPyQ/Up6y/zi2P/WwXb78gHsLiGRUqCA9LOoCX+6dQZde0g1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.4.1.tgz", + "integrity": "sha512-y2NUD7pygrBolN2NoXUrwVqBpKPhF8DiSNE5oB5/iFO49r2DpoYqdj5HPb3F42fPBH5qNqj6Zg63+xCEzAD2hw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.4.1.tgz", + "integrity": "sha512-hVXaObGI2lGFmrtT77KSbPQ3I+zk9IU500wobjk0+oX59vg/0VqAzABNtt3YSQYgXTC2a/LYxekLfND/wlt0yQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -5686,9 +5617,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "bin": { "acorn": "bin/acorn" }, @@ -5705,14 +5636,6 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -5931,12 +5854,12 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -5985,6 +5908,26 @@ "node": ">=0.10.0" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", @@ -6032,18 +5975,17 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -6062,6 +6004,14 @@ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6143,27 +6093,32 @@ } }, "node_modules/axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", - "peer": true, + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "node_modules/axios-cache-interceptor": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-0.10.7.tgz", - "integrity": "sha512-UjpxChG5DpF6Kf1IPGMLOzRDNL8ZNS6TOn1jTaVvCE7cWFU904jJwi0T1s+IbijpnLEjK2iq5uLIuR8Sj+RsFQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.6.2.tgz", + "integrity": "sha512-YLbAODIHZZIcD4b3WYFVQOa5W2TY/WnJ6sBHqAg6Z+hx+RVj8/OcjQyRopO6awn7/kOkGL5X9TP16AucnlJ/lw==", "dependencies": { - "cache-parser": "^1.2.4", - "fast-defer": "^1.1.7", - "object-code": "^1.2.4" + "cache-parser": "1.2.5", + "fast-defer": "1.1.8", + "object-code": "1.3.3" + }, + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/ArthurFiorette/axios-cache-interceptor?sponsor=1" + "url": "https://github.com/arthurfiorette/axios-cache-interceptor?sponsor=1" + }, + "peerDependencies": { + "axios": "^1" } }, "node_modules/axios-mock-adapter": { @@ -6192,14 +6147,14 @@ "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" }, "node_modules/babel-jest": { - "version": "29.6.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.1.tgz", - "integrity": "sha512-qu+3bdPEQC6KZSPz+4Fyjbga5OODNcp49j6GKzG1EKbkfyJBxEYGVUmVGpwCSeGouG52R4EgYMLb6p9YeEEQ4A==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dependencies": { - "@jest/transform": "^29.6.1", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -6220,9 +6175,9 @@ } }, "node_modules/babel-loader": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", - "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", "dependencies": { "find-cache-dir": "^4.0.0", "schema-utils": "^4.0.0" @@ -6236,14 +6191,14 @@ } }, "node_modules/babel-loader/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -6267,9 +6222,9 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/babel-loader/node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -6277,7 +6232,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -6638,9 +6593,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "funding": [ { "type": "opencollective", @@ -6655,12 +6610,11 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -6730,15 +6684,41 @@ "integrity": "sha512-Md/4VhAHByQ9frQ15WD6LrMNiVw9AEl/J7vWIXw+sxT6fSOpbtt6LHTp76vy8+bOESPBO94117Hm2bIjlI7XjA==" }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -6803,8 +6783,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/cast-array": { "version": "1.0.1", @@ -7229,11 +7208,10 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" }, "node_modules/core-js": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", - "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz", + "integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==", "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -7329,9 +7307,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -7611,13 +7589,13 @@ } }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7627,27 +7605,27 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -7664,11 +7642,11 @@ "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -7679,6 +7657,11 @@ } } }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -8114,6 +8097,19 @@ "webpack": "^4 || ^5" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -8125,10 +8121,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.27", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz", - "integrity": "sha512-o37j1vZqCoEgBuWWXLHQgTN/KDKe7zwpiY5CPeq2RvUqOyJw9xnrULzZAEVQ5p4h+zjMk7hgtOoPdnLxr7m/jw==", - "license": "ISC" + "version": "1.5.134", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.134.tgz", + "integrity": "sha512-zSwzrLg3jNP3bwsLqWHmS5z2nIOQ5ngMnfMZOWWtXnqqQkPVyOipxK98w+1beLw1TB+EImPNcG8wVP/cLVs2Og==" }, "node_modules/email-prop-type": { "version": "3.0.1", @@ -8187,9 +8182,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -8237,56 +8232,61 @@ } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -8296,12 +8296,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -8314,15 +8311,41 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==" }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dependencies": { "es-errors": "^1.3.0" }, @@ -8331,34 +8354,38 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -8368,9 +8395,9 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -8545,10 +8572,42 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.2.tgz", + "integrity": "sha512-T2LqBXj87ndEC9t1LrDiPkzalSFzD4rrXr6BTzGdgMx1jdQM4T972guQvg7Ih+LNO51GURXI/qMHS5GF3h1ilw==", + "dependencies": { + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.12", + "unrs-resolver": "^1.4.1" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dependencies": { "debug": "^3.2.7" }, @@ -8761,31 +8820,35 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/eslint-plugin-import": { - "version": "2.27.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", - "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "array.prototype.flatmap": "^1.3.1", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.7", - "eslint-module-utils": "^2.7.4", - "has": "^1.0.3", - "is-core-module": "^2.11.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.6", - "resolve": "^1.22.1", - "semver": "^6.3.0", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -8842,14 +8905,15 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/eslint-plugin-react": { - "version": "7.32.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", - "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", + "version": "7.33.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", + "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", "dependencies": { "array-includes": "^3.1.6", "array.prototype.flatmap": "^1.3.1", "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.12", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", @@ -8859,7 +8923,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "^6.3.0", + "semver": "^6.3.1", "string.prototype.matchall": "^4.0.8" }, "engines": { @@ -8870,9 +8934,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.1.tgz", + "integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==", "engines": { "node": ">=10" }, @@ -9337,6 +9401,21 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -9574,9 +9653,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", @@ -9601,11 +9680,17 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/fork-ts-checker-webpack-plugin": { @@ -9827,14 +9912,16 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -9868,15 +9955,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -9901,6 +9993,18 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -9913,13 +10017,13 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -9928,6 +10032,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -9937,6 +10052,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -10012,11 +10128,12 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -10053,11 +10170,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10073,13 +10190,6 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, - "node_modules/growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", - "optional": true, - "peer": true - }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -10113,9 +10223,12 @@ } }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10140,9 +10253,12 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -10151,9 +10267,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -10673,9 +10789,9 @@ } }, "node_modules/immutable": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", - "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", + "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==" }, "node_modules/import-fresh": { "version": "3.3.0", @@ -10830,13 +10946,13 @@ } }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -10854,8 +10970,6 @@ "version": "10.7.7", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.7.tgz", "integrity": "sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==", - "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/fast-memoize": "2.2.3", @@ -10867,8 +10981,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -10879,8 +10991,6 @@ "version": "2.9.4", "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/icu-skeleton-parser": "1.8.8", @@ -10891,8 +11001,6 @@ "version": "1.8.8", "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "tslib": "2" @@ -10902,8 +11010,6 @@ "version": "0.5.8", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", - "license": "MIT", - "peer": true, "dependencies": { "tslib": "2" } @@ -10925,12 +11031,13 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -10944,12 +11051,33 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10967,12 +11095,12 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11003,6 +11131,25 @@ "node": ">=4" } }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -11015,21 +11162,26 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -11040,11 +11192,12 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11075,6 +11228,20 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -11088,7 +11255,24 @@ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "engines": { - "node": ">=6" + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-glob": { @@ -11140,10 +11324,10 @@ "node": ">=0.10.0" } }, - "node_modules/is-negative-zero": { + "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "engines": { "node": ">= 0.4" }, @@ -11160,11 +11344,12 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11239,12 +11424,14 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -11261,12 +11448,23 @@ "node": ">=6" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -11287,11 +11485,12 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11301,11 +11500,13 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -11315,11 +11516,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -11350,12 +11551,41 @@ "node": ">=0.10.0" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11503,6 +11733,22 @@ "node": ">=8" } }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -11655,26 +11901,6 @@ } } }, - "node_modules/jest-config/node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, "node_modules/jest-config/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12211,14 +12437,14 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -12598,6 +12824,14 @@ "css-mediaquery": "^0.1.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -12901,72 +13135,10 @@ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, - "node_modules/node-notifier": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", - "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", - "optional": true, - "peer": true, - "dependencies": { - "growly": "^1.3.0", - "is-wsl": "^2.2.0", - "semver": "^7.3.2", - "shellwords": "^0.1.1", - "uuid": "^8.3.0", - "which": "^2.0.2" - } - }, - "node_modules/node-notifier/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-notifier/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "optional": true, - "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-notifier/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "optional": true, - "peer": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/node-notifier/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true, - "peer": true - }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "license": "MIT" + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -13030,9 +13202,12 @@ "integrity": "sha512-NahvP2vZcy1ZiiYah30CEPw0FpDcSkSePJBMpzl5EQgCmISijiGuJm3SPYp7U+Lf2TljyaIw3E5EgkEx/TNEVA==" }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -13046,13 +13221,15 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -13092,6 +13269,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.hasown": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", @@ -13240,6 +13430,22 @@ "node": ">=0.10.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -13540,9 +13746,9 @@ } }, "node_modules/pkg-dir/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", "engines": { "node": ">=12.20" }, @@ -13629,9 +13835,9 @@ } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "engines": { "node": ">= 0.4" } @@ -14365,8 +14571,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "peer": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/psl": { "version": "1.9.0", @@ -14374,9 +14579,9 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "node_modules/pubsub-js": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.4.tgz", - "integrity": "sha512-hJYpaDvPH4w8ZX/0Fdf9ma1AwRgU353GfbaVfPjfJQf1KxZ2iHaHl3fAUw1qlJIR5dr4F3RzjGaWohYUEyoh7A==" + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/pubsub-js/-/pubsub-js-1.9.5.tgz", + "integrity": "sha512-5MZ0I9i5JWVO7SizvOviKvZU2qaBbl2KQX150FAA+fJBwYpwOUId7aNygURWSdPzlsA/xZ/InUKXqBbzM0czTA==" }, "node_modules/pump": { "version": "3.0.0", @@ -14582,12 +14787,11 @@ } }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" @@ -14684,16 +14888,15 @@ } }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.3.1" } }, "node_modules/react-dropzone": { @@ -14713,9 +14916,9 @@ } }, "node_modules/react-error-boundary": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", - "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", "peer": true, "dependencies": { "@babel/runtime": "^7.12.5" @@ -14801,8 +15004,6 @@ "version": "6.8.9", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.8.9.tgz", "integrity": "sha512-TUfj5E7lyUDvz/GtovC9OMh441kBr08rtIbgh3p0R8iF3hVY+V2W9Am7rb8BpJ/29BH1utJOqOOhmvEVh3GfZg==", - "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/icu-messageformat-parser": "2.9.4", @@ -14829,8 +15030,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.3", "@formatjs/intl-localematcher": "0.5.8", @@ -14841,8 +15040,6 @@ "version": "2.9.4", "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "@formatjs/icu-skeleton-parser": "1.8.8", @@ -14853,8 +15050,6 @@ "version": "1.8.8", "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", - "license": "MIT", - "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.2.4", "tslib": "2" @@ -14864,8 +15059,6 @@ "version": "0.5.8", "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", - "license": "MIT", - "peer": true, "dependencies": { "tslib": "2" } @@ -14956,10 +15149,9 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "license": "MIT", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.16.0.tgz", + "integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==", "engines": { "node": ">=0.10.0" } @@ -15161,6 +15353,27 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -15197,14 +15410,16 @@ "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -15335,6 +15550,14 @@ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-url-loader": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", @@ -15462,13 +15685,14 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -15502,14 +15726,34 @@ } ] }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -15524,12 +15768,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.69.7", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz", - "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==", + "version": "1.85.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", + "integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -15537,6 +15781,9 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, "node_modules/sass-loader": { @@ -15575,6 +15822,32 @@ } } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -15587,12 +15860,11 @@ } }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { @@ -15793,8 +16065,21 @@ "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -15900,22 +16185,66 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "optional": true, - "peer": true - }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -16148,6 +16477,11 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -16264,14 +16598,17 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16281,14 +16618,18 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -16607,9 +16948,9 @@ } }, "node_modules/terser": { - "version": "5.30.4", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz", - "integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==", + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -16624,15 +16965,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -16656,6 +16997,32 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/terser-webpack-plugin/node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -16669,6 +17036,29 @@ "node": ">= 10.13.0" } }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/terser-webpack-plugin/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -16743,6 +17133,45 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "node_modules/tinyglobby": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", + "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "dependencies": { + "fdir": "^6.4.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -16759,14 +17188,6 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -16926,6 +17347,41 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tsconfig-paths/node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -17023,28 +17479,28 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -17054,16 +17510,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -17073,16 +17530,16 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -17104,14 +17561,17 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17213,10 +17673,35 @@ "node": ">= 0.8" } }, + "node_modules/unrs-resolver": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.4.1.tgz", + "integrity": "sha512-MhPB3wBI5BR8TGieTb08XuYlE8oFVEXdSAgat3psdlRyejl8ojQ8iqPcjh094qCZ1r+TnkxzP6BeCd/umfHckQ==", + "funding": { + "url": "https://github.com/sponsors/JounQin" + }, + "optionalDependencies": { + "@unrs/resolver-binding-darwin-arm64": "1.4.1", + "@unrs/resolver-binding-darwin-x64": "1.4.1", + "@unrs/resolver-binding-freebsd-x64": "1.4.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.4.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.4.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.4.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.4.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.4.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.4.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.4.1", + "@unrs/resolver-binding-linux-x64-musl": "1.4.1", + "@unrs/resolver-binding-wasm32-wasi": "1.4.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.4.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.4.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.4.1" + } + }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", @@ -17231,10 +17716,9 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -17327,6 +17811,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -17452,20 +17944,19 @@ } }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", + "version": "5.99.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.4.tgz", + "integrity": "sha512-TkHfuepzYbQ02GMRzZABrkguDyG6p5gaw4qxFb/cuE+XjN21lecJsNATdphBMXawFxIZxUvDHJfrs8BEfyZFzg==", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -17475,9 +17966,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -17851,6 +18342,55 @@ "node": ">=0.10.0" } }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/webpack/node_modules/webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", @@ -17937,29 +18477,82 @@ } }, "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { diff --git a/package.json b/package.json index ee98c78..1301b12 100644 --- a/package.json +++ b/package.json @@ -34,22 +34,22 @@ "url": "https://github.com/open-craft/frontend-app-learning-paths/issues" }, "dependencies": { - "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", - "@edx/frontend-component-header": "^5.6.0", - "@edx/frontend-platform": "^7.1.2", - "@edx/openedx-atlas": "^0.6.0", + "@edx/brand": "npm:@openedx/brand-openedx@^1.2.3", + "@edx/frontend-component-header": "^6.3.0", + "@edx/frontend-platform": "^8.3.4", + "@edx/openedx-atlas": "^0.7.0", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.2.2", - "@openedx/frontend-slot-footer": "^1.0.2", - "@openedx/paragon": "^22.0.0", + "@openedx/frontend-slot-footer": "^1.2.0", + "@openedx/paragon": "^22.17.0", "@reduxjs/toolkit": "^2.6.1", - "core-js": "3.40.0", + "core-js": "3.41.0", "prop-types": "15.8.1", - "react": "17.0.2", - "react-dom": "17.0.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-redux": "7.2.9", "react-router": "6.29.0", "react-router-dom": "6.29.0", @@ -57,9 +57,9 @@ "regenerator-runtime": "0.14.1" }, "devDependencies": { - "@edx/browserslist-config": "^1.1.1", - "@edx/reactifex": "^2.1.1", - "@openedx/frontend-build": "14.2.2", + "@edx/browserslist-config": "^1.5.0", + "@edx/reactifex": "^2.2.0", + "@openedx/frontend-build": "14.4.2", "glob": "7.2.3", "husky": "7.0.4", "jest": "29.7.0" From 56a72023d20fbc4b4d04ad3381e0884ba4b508c0 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Tue, 8 Apr 2025 23:57:05 +0200 Subject: [PATCH 31/45] feat: migrate from Redux to React Query https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0067/decisions/frontend/0010-react-query.html --- package-lock.json | 107 ++++---- package.json | 3 +- src/index.jsx | 50 ++-- src/learningpath/CourseCard.jsx | 9 +- src/learningpath/CourseDetails.jsx | 83 +++--- src/learningpath/Dashboard.jsx | 41 ++- src/learningpath/LearningPathCard.jsx | 9 +- src/learningpath/LearningPathDetails.jsx | 82 ++---- src/learningpath/data/queries.js | 308 +++++++++++++++++++++++ src/learningpath/data/slice.js | 105 -------- src/learningpath/data/thunks.js | 126 ---------- src/queryClient.js | 14 ++ src/store.js | 12 - 13 files changed, 499 insertions(+), 450 deletions(-) create mode 100644 src/learningpath/data/queries.js delete mode 100644 src/learningpath/data/slice.js delete mode 100644 src/learningpath/data/thunks.js create mode 100644 src/queryClient.js delete mode 100644 src/store.js diff --git a/package-lock.json b/package-lock.json index b910502..3510991 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "@fortawesome/react-fontawesome": "0.2.2", "@openedx/frontend-slot-footer": "^1.2.0", "@openedx/paragon": "^22.17.0", - "@reduxjs/toolkit": "^2.6.1", + "@tanstack/react-query": "^5.72.1", + "@tanstack/react-query-devtools": "^5.72.1", "core-js": "3.41.0", "prop-types": "15.8.1", "react": "^18.3.1", @@ -4177,55 +4178,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@reduxjs/toolkit": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.1.tgz", - "integrity": "sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw==", - "license": "MIT", - "dependencies": { - "immer": "^10.0.3", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/@reduxjs/toolkit/node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/@reduxjs/toolkit/node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, "node_modules/@remix-run/router": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", @@ -4523,6 +4475,55 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tanstack/query-core": { + "version": "5.72.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.72.1.tgz", + "integrity": "sha512-nOu0EEkZuJ0BZnYgeaEfo44+psq1jBO7/zp3KudixD4dvgOVerrhAhDEKsWx2N7MxB59mjO4r0ddP/VqWGPK+Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.72.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.72.1.tgz", + "integrity": "sha512-D0vEoQaiVq9ayCqvvxA9XkDq7TIesyPpvgP69arRtt5FQF6n/Hrta4SlkfXC4m9BCvFLlhLDcKGYa2eMQ4ZIIA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.72.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.72.1.tgz", + "integrity": "sha512-4UEMyRx54xj144D2nDvDIMiXSG5BrqyCJrmyNoGbymNS+VWODcBDFrmRk9p2fe12UGZ4JtKPTNuW2Jg0aisUgQ==", + "dependencies": { + "@tanstack/query-core": "5.72.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.72.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.72.1.tgz", + "integrity": "sha512-ckNRgABst3MLjpM2nD/CzQToCiaT3jb3Xhtf+GP/0/9ij9SPT/SC+lc3wUDSkT0OupnHobBBF5E1/Xp6B+XZLg==", + "dependencies": { + "@tanstack/query-devtools": "5.72.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.72.1", + "react": "^18 || ^19" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -15504,12 +15505,6 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/package.json b/package.json index 1301b12..e8d8c3f 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "@fortawesome/react-fontawesome": "0.2.2", "@openedx/frontend-slot-footer": "^1.2.0", "@openedx/paragon": "^22.17.0", - "@reduxjs/toolkit": "^2.6.1", + "@tanstack/react-query": "^5.72.1", + "@tanstack/react-query-devtools": "^5.72.1", "core-js": "3.41.0", "prop-types": "15.8.1", "react": "^18.3.1", diff --git a/src/index.jsx b/src/index.jsx index 2e23f5e..c10c430 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -5,13 +5,15 @@ import { APP_INIT_ERROR, APP_READY, subscribe, initialize, } from '@edx/frontend-platform'; import { AppProvider, ErrorPage } from '@edx/frontend-platform/react'; -import ReactDOM from 'react-dom'; +import ReactDOM from 'react-dom/client'; import { Routes, Route } from 'react-router-dom'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import Header from '@edx/frontend-component-header'; import FooterSlot from '@openedx/frontend-slot-footer'; import messages from './i18n'; -import store from './store'; +import queryClient from './queryClient'; import Dashboard from './learningpath/Dashboard'; import LearningPathDetailPage from './learningpath/LearningPathDetails'; import CourseDetailPage from './learningpath/CourseDetails'; @@ -19,31 +21,35 @@ import CourseDetailPage from './learningpath/CourseDetails'; import './index.scss'; subscribe(APP_READY, () => { - ReactDOM.render( - -
- - } - /> - } - /> - } - /> - - + const root = ReactDOM.createRoot(document.getElementById('root')); + root.render( + + +
+ + } + /> + } + /> + } + /> + + + {process.env.NODE_ENV === 'development' && } + , - document.getElementById('root'), ); }); subscribe(APP_INIT_ERROR, (error) => { - ReactDOM.render(, document.getElementById('root')); + const root = ReactDOM.createRoot(document.getElementById('root')); + root.render(); }); initialize({ diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index 0cf69ae..2e6559f 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -12,6 +12,7 @@ import { Timelapse, } from '@openedx/paragon/icons'; import { buildAssetUrl } from '../util/assetUrl'; +import { usePrefetchCourseDetail } from './data/queries'; const CourseCard = ({ course, parentPath }) => { const courseKey = `course-v1:${course.org}+${course.courseId}+${course.run}`; @@ -24,6 +25,12 @@ const CourseCard = ({ course, parentPath }) => { percent, } = course; + // Prefetch the course detail when the user hovers over the card. + const prefetchCourseDetail = usePrefetchCourseDetail(courseKey); + const handleMouseEnter = () => { + prefetchCourseDetail(); + }; + const progressBarPercent = useMemo(() => Math.round(percent * 100), [percent]); const linkTo = parentPath @@ -58,7 +65,7 @@ const CourseCard = ({ course, parentPath }) => { }) : null; return ( - +
diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index 1efbb46..0b6f5ee 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; @@ -21,7 +21,7 @@ import { Person, Close, } from '@openedx/paragon/icons'; -import { fetchCoursesByIds } from './data/api'; +import { useCourseDetail } from './data/queries'; import { buildAssetUrl } from '../util/assetUrl'; const CourseDetailContent = ({ course, isModalView, onClose }) => { @@ -167,29 +167,33 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => {

About

- {description || shortDescription} + {description || shortDescription || 'No description available.'}

What you'll learn

- {learningInfo.map((learning) => ( -

- * {learning} -

- ))} + {learningInfo && learningInfo.length > 0 ? ( + learningInfo.map((learning) => ( +

+ * {learning} +

+ )) + ) : ( +

No learning objectives listed for this course.

+ )}

Instructors

- {instructorInfo && instructorInfo.instructors.length > 0 ? ( + {instructorInfo && instructorInfo.instructors && instructorInfo.instructors.length > 0 ? ( instructorInfo.instructors.map((instructor) => ( - +
{instructor.name}

@@ -244,51 +248,48 @@ CourseDetailContent.defaultProps = { const CourseDetailPage = ({ isModalView = false, onClose }) => { const { courseKey } = useParams(); - const [course, setCourse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - useEffect(() => { - async function loadCourse() { - try { - setLoading(true); - const data = await fetchCoursesByIds([courseKey]); - setCourse(data[0]); - } catch (err) { - // eslint-disable-next-line no-console - console.error('Failed to load course details:', err); - setError(err.message); - } finally { - setLoading(false); - } - } - loadCourse(); - }, [courseKey]); + const { + data: course, + isLoading, + error, + } = useCourseDetail(courseKey); - if (loading) { + if (isLoading) { return ; } if (error) { return ( -

- Failed to load course detail: {error} - Back -
+ + Error loading course +

{error.message}

+ Return to dashboard +
); } if (!course) { - return ; + return ( + + Course not found +

We couldn't find the requested course.

+ Return to dashboard +
+ ); } + const courseWithFallbacks = { + ...course, + shortDescription: course.shortDescription || '', + description: course.description || course.shortDescription || '', + duration: course.duration || '', + selfPaced: course.selfPaced !== undefined ? course.selfPaced : true, + }; + return (
- {isModalView ? ( - - ) : ( - - )} +
); }; diff --git a/src/learningpath/Dashboard.jsx b/src/learningpath/Dashboard.jsx index b3fe061..a81251b 100644 --- a/src/learningpath/Dashboard.jsx +++ b/src/learningpath/Dashboard.jsx @@ -1,43 +1,34 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { useState, useMemo } from 'react'; import { Spinner, Row, Col, Button, } from '@openedx/paragon'; -import { fetchLearningPaths, fetchCourses, fetchCompletions } from './data/thunks'; +import { useLearningPaths, useCourses } from './data/queries'; import LearningPathCard from './LearningPathCard'; import CourseCard from './CourseCard'; import FilterPanel from './FilterPanel'; const Dashboard = () => { - const dispatch = useDispatch(); const { - fetching: lpFetching, - learningPaths, - errors: lpErrors, - } = useSelector(state => state.learningPath); - const { - fetching: coursesFetching, - courses, - error: coursesErrors, - } = useSelector(state => state.courses); - const { fetching: completionsFetching } = useSelector(state => state.completions); + data: learningPaths, + isLoading: isLoadingPaths, + error: pathsError, + } = useLearningPaths(); - useEffect(() => { - dispatch(fetchCompletions()).then(() => { - dispatch(fetchLearningPaths()); - dispatch(fetchCourses()); - }); - }, [dispatch]); + const { + data: courses, + isLoading: isLoadingCourses, + error: coursesError, + } = useCourses(); - const isLoading = lpFetching || coursesFetching || completionsFetching; + const isLoading = isLoadingPaths || isLoadingCourses; + const error = pathsError || coursesError; - const allErrors = [].concat(lpErrors || [], coursesErrors || []); - if (allErrors.length > 0) { + if (error) { // eslint-disable-next-line no-console - console.error('Error loading learning paths:', allErrors); + console.error('Error loading data:', error); } - const items = useMemo(() => [...courses, ...learningPaths], [courses, learningPaths]); + const items = useMemo(() => [...(courses || []), ...(learningPaths || [])], [courses, learningPaths]); const [showFilters, setShowFilters] = useState(false); const [selectedContentType, setSelectedContentType] = useState('All'); diff --git a/src/learningpath/LearningPathCard.jsx b/src/learningpath/LearningPathCard.jsx index 9d7026e..062c934 100644 --- a/src/learningpath/LearningPathCard.jsx +++ b/src/learningpath/LearningPathCard.jsx @@ -18,6 +18,7 @@ import { AccessTime, } from '@openedx/paragon/icons'; import { buildAssetUrl } from '../util/assetUrl'; +import { usePrefetchLearningPathDetail } from './data/queries'; const LearningPathCard = ({ learningPath }) => { const { @@ -32,6 +33,12 @@ const LearningPathCard = ({ learningPath }) => { percent, } = learningPath; + // Prefetch the learning path detail when the user hovers over the card. + const prefetchLearningPathDetail = usePrefetchLearningPathDetail(); + const handleMouseEnter = () => { + prefetchLearningPathDetail(key); + }; + let statusVariant = 'dark'; let statusIcon = 'fa-circle'; switch (status?.toLowerCase()) { @@ -64,7 +71,7 @@ const LearningPathCard = ({ learningPath }) => { : subtitle || duration || ''; return ( - +
diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index 41215a2..d2af826 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { useParams, Link, useNavigate, Routes, Route, } from 'react-router-dom'; @@ -12,71 +12,34 @@ import { FormatListBulleted, AccessTimeFilled, } from '@openedx/paragon/icons'; -import { useSelector, useDispatch } from 'react-redux'; import { buildAssetUrl } from '../util/assetUrl'; -import { fetchCoursesByIds, fetchLearningPathDetail } from './data/api'; -import { fetchCompletions } from './data/thunks'; +import { useLearningPathDetail, useCoursesByIds } from './data/queries'; import CourseCard from './CourseCard'; import CourseDetailPage from './CourseDetails'; const LearningPathDetailPage = () => { const { key } = useParams(); const navigate = useNavigate(); - const dispatch = useDispatch(); - const [detail, setDetail] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [coursesForPath, setCoursesForPath] = useState([]); - const [loadingCourses, setLoadingCourses] = useState(false); - const [coursesError, setCoursesError] = useState(null); - const { completions, fetching: fetchingCompletions } = useSelector((state) => state.completions); - - useEffect(() => { - if (!fetchingCompletions && Object.keys(completions).length === 0) { - dispatch(fetchCompletions()); - } - }, [dispatch, completions, fetchingCompletions]); - - useEffect(() => { - async function loadDetail() { - try { - setLoading(true); - const data = await fetchLearningPathDetail(key); - setDetail(data); - } catch (err) { - // eslint-disable-next-line no-console - console.error('Failed to fetch learning path details:', err); - setError(err.message); - } finally { - setLoading(false); - } - } - loadDetail(); - }, [key]); + const { + data: detail, + isLoading: loadingDetail, + error: detailError, + } = useLearningPathDetail(key); const courseIds = useMemo(() => (detail && detail.steps ? detail.steps.map(step => step.courseKey) : []), [detail]); - useEffect(() => { - if (courseIds.length === 0) { return; } - async function loadCourses() { - try { - setLoadingCourses(true); - setCoursesError(null); - const courses = await fetchCoursesByIds(courseIds, completions); - setCoursesForPath(courses); - } catch (err) { - // eslint-disable-next-line no-console - console.error('Failed to fetch courses:', err); - setCoursesError(err.message); - } finally { - setLoadingCourses(false); - } - } - loadCourses(); - }, [courseIds, completions]); + const { + data: coursesForPath, + isLoading: loadingCourses, + error: coursesError, + } = useCoursesByIds(courseIds); const accessUntilDate = useMemo(() => { + if (!coursesForPath) { + return null; + } + let maxDate = null; for (const c of coursesForPath) { if (c.endDate) { @@ -90,9 +53,9 @@ const LearningPathDetailPage = () => { }, [coursesForPath]); let content; - if (loading) { + if (loadingDetail || loadingCourses) { content = ; - } else if (error) { + } else if (detailError || !detail) { content = (

Failed to load detail

@@ -221,10 +184,10 @@ const LearningPathDetailPage = () => {

Courses

{loadingCourses && } - {!loadingCourses && !coursesError && coursesForPath.length === 0 && ( + {!loadingCourses && !coursesError && (!coursesForPath || coursesForPath.length === 0) && (

No sub-courses found in this learning path.

)} - {!loadingCourses && !coursesError && coursesForPath.length > 0 && ( + {!loadingCourses && !coursesError && coursesForPath && coursesForPath.length > 0 && ( coursesForPath.map(course => (
@@ -234,8 +197,8 @@ const LearningPathDetailPage = () => {

Requirements

- {requiredSkills.map(skill => ( -

+ {requiredSkills && requiredSkills.map((skill) => ( +

{skill}

))} @@ -259,7 +222,6 @@ const LearningPathDetailPage = () => { > navigate(`/learningpath/${key}`)} /> )} diff --git a/src/learningpath/data/queries.js b/src/learningpath/data/queries.js new file mode 100644 index 0000000..e8c9f39 --- /dev/null +++ b/src/learningpath/data/queries.js @@ -0,0 +1,308 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback } from 'react'; +import * as api from './api'; + +// Query keys +export const QUERY_KEYS = { + ALL_LEARNING_PATHS: ['learningPaths'], + LEARNING_PATH_DETAIL: (key) => ['learningPath', key], + LEARNING_PATH_PROGRESS: (key) => ['learningPathProgress', key], + ALL_COURSES: ['courses'], + COURSE_DETAILS: (courseId) => ['course', courseId], + COURSE_COMPLETIONS: ['courseCompletions'], + COURSE_COMPLETION: (courseId) => ['courseCompletion', courseId], +}; + +// Stale time configurations +export const STALE_TIMES = { + LEARNING_PATHS: 5 * 60 * 1000, // 5 minutes + LEARNING_PATH_DETAIL: 5 * 60 * 1000, // 5 minutes + + COURSES: 5 * 60 * 1000, // 5 minutes + COURSE_DETAIL: 5 * 60 * 1000, // 5 minutes + + COMPLETIONS: 60 * 1000, // 1 minute +}; + +// Learning Paths Queries +export const useLearningPaths = () => useQuery({ + queryKey: QUERY_KEYS.ALL_LEARNING_PATHS, + queryFn: async () => { + const learningPathList = await api.fetchLearningPaths(); + + const processedPaths = await Promise.all( + learningPathList.map(async (lp) => { + const lpProgress = await api.fetchLearningPathProgress(lp.key); + + let status = 'In Progress'; + if (lpProgress.progress === 0.0) { + status = 'Not started'; + } else if (lpProgress.progress >= lpProgress.requiredCompletion) { + status = 'Completed'; + } + + let percent = 0; + if (lpProgress.requiredCompletion) { + percent = lpProgress.requiredCompletion > 0 + ? Math.round((lpProgress.progress / lpProgress.requiredCompletion) * 100) + : 0; + } else { + percent = lpProgress.percent; + } + + let maxDate = null; + const numCourses = lp.steps.length; + for (const course of lp.steps) { + if (course.dueDate) { + const dueDateObj = new Date(course.dueDate); + if (!maxDate || dueDateObj > maxDate) { + maxDate = dueDateObj; + } + } + } + const isoMaxDate = maxDate ? maxDate.toISOString() : null; + + return { + ...lp, + numCourses, + status, + maxDate: isoMaxDate, + percent, + type: 'learning_path', + }; + }), + ); + + return processedPaths; + }, +}); + +export const useLearningPathDetail = (key) => useQuery({ + queryKey: QUERY_KEYS.LEARNING_PATH_DETAIL(key), + queryFn: () => api.fetchLearningPathDetail(key), + enabled: !!key, +}); + +// Hook for prefetching learning path details and all related data +export const usePrefetchLearningPathDetail = () => { + const queryClient = useQueryClient(); + + return (key) => { + if (!key) { + return; + } + + queryClient.fetchQuery({ + queryKey: QUERY_KEYS.LEARNING_PATH_DETAIL(key), + queryFn: () => api.fetchLearningPathDetail(key), + staleTime: STALE_TIMES.LEARNING_PATH_DETAIL, + }) + .then(learningPathData => { + if (!learningPathData?.steps || learningPathData.steps.length === 0) { + return; + } + + const courseIds = learningPathData.steps.map(step => step.courseKey); + + queryClient.fetchQuery({ + queryKey: QUERY_KEYS.COURSE_COMPLETIONS, + queryFn: api.fetchAllCourseCompletions, + staleTime: STALE_TIMES.COMPLETIONS, + }) + .then(completionsData => { + const completionsMap = {}; + completionsData?.forEach?.(item => { + completionsMap[item.courseKey] = item.completion; + }); + + queryClient.fetchQuery({ + queryKey: QUERY_KEYS.ALL_COURSES, + queryFn: () => api.fetchCourses(), + staleTime: STALE_TIMES.COURSES, + }); + + courseIds.forEach(courseId => { + queryClient.fetchQuery({ + queryKey: QUERY_KEYS.COURSE_DETAILS(courseId), + queryFn: () => api.fetchCombinedCourseInfo(courseId), + staleTime: STALE_TIMES.COURSE_DETAIL, + }); + }); + + // Fetch the combined course data. + queryClient.fetchQuery({ + queryKey: ['coursesByIds', ...courseIds], + queryFn: () => api.fetchCoursesByIds(courseIds, completionsMap), + staleTime: STALE_TIMES.COURSE_DETAIL, + }); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error('Error prefetching course completions:', error); + }); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error('Error prefetching learning path:', error); + }); + }; +}; + +// Course Queries +export const useCourses = () => { + const queryClient = useQueryClient(); + + return useQuery({ + queryKey: QUERY_KEYS.ALL_COURSES, + queryFn: async () => { + await queryClient.prefetchQuery({ + queryKey: QUERY_KEYS.COURSE_COMPLETIONS, + queryFn: api.fetchAllCourseCompletions, + }); + + const completions = queryClient.getQueryData(QUERY_KEYS.COURSE_COMPLETIONS) || {}; + const completionsMap = {}; + + completions.forEach?.(item => { + completionsMap[item.courseKey] = item.completion; + }); + + const courses = await api.fetchAllCourseDetails(); + + return courses.map(course => { + const courseKey = `course-v1:${course.org}+${course.courseId}+${course.run}`; + const completion = completionsMap[courseKey]?.percent || 0; + + let status = 'In progress'; + if (completion === 0.0) { + status = 'Not started'; + } else if (completion === 1.0) { + status = 'Completed'; + } + + return { + ...course, + status, + percent: completion, + type: 'course', + }; + }); + }, + }); +}; + +export const useCourseCompletions = () => useQuery({ + queryKey: QUERY_KEYS.COURSE_COMPLETIONS, + queryFn: api.fetchAllCourseCompletions, + staleTime: STALE_TIMES.COMPLETIONS, +}); + +export const useCoursesByIds = (courseIds) => { + const queryClient = useQueryClient(); + + return useQuery({ + queryKey: ['coursesByIds', ...(courseIds || [])], + queryFn: async () => { + let completionsData = queryClient.getQueryData(QUERY_KEYS.COURSE_COMPLETIONS); + if (!completionsData) { + completionsData = await queryClient.fetchQuery({ + queryKey: QUERY_KEYS.COURSE_COMPLETIONS, + queryFn: api.fetchAllCourseCompletions, + }); + } + + const completionsMap = {}; + completionsData?.forEach?.(item => { + completionsMap[item.courseKey] = item.completion; + }); + + const results = await Promise.all( + courseIds.map(async (courseId) => { + const courseIdParts = courseId.split(':')[1]?.split('+'); + const simpleId = courseIdParts?.[1]; + + const cachedCourseDetail = simpleId + ? queryClient.getQueryData(['courseDetail', simpleId]) : null; + if (cachedCourseDetail) { + const completion = completionsMap[courseId]?.percent || 0; + + let status = 'In progress'; + if (completion <= 0.0) { + status = 'Not started'; + } else if (completion >= 1.0) { + status = 'Completed'; + } + + const basicCoursesData = queryClient.getQueryData(['basicCoursesData']) || []; + const basicData = basicCoursesData.find(c => c.courseId === simpleId) || {}; + + return { + ...basicData, + ...cachedCourseDetail, + status, + percent: completion, + }; + } + + return api.fetchCoursesByIds([courseId], completionsMap).then(data => data[0]); + }), + ); + + return results; + }, + enabled: courseIds && courseIds.length > 0, + }); +}; + +export const useCourseDetail = (courseKey) => { + const queryClient = useQueryClient(); + + return useQuery({ + queryKey: QUERY_KEYS.COURSE_DETAILS(courseKey), + queryFn: async () => { + await queryClient.prefetchQuery({ + queryKey: QUERY_KEYS.COURSE_COMPLETIONS, + queryFn: api.fetchAllCourseCompletions, + }); + + const completions = queryClient.getQueryData(QUERY_KEYS.COURSE_COMPLETIONS) || {}; + const completionsMap = {}; + + completions.forEach?.(item => { + completionsMap[item.courseKey] = item.completion; + }); + + const courses = await api.fetchCoursesByIds([courseKey], completionsMap); + return courses && courses.length > 0 ? courses[0] : null; + }, + enabled: !!courseKey, + }); +}; + +// Hook to prefetch course details when hovering +export const usePrefetchCourseDetail = (courseId) => { + const queryClient = useQueryClient(); + + const prefetchCourse = useCallback(() => { + if (courseId) { + try { + queryClient.fetchQuery({ + queryKey: QUERY_KEYS.COURSE_DETAILS(courseId), + queryFn: () => api.fetchCombinedCourseInfo(courseId), + staleTime: STALE_TIMES.COURSE_DETAIL, + }); + + queryClient.fetchQuery({ + queryKey: QUERY_KEYS.COURSE_COMPLETION(courseId), + queryFn: () => api.fetchCourseCompletion(courseId), + staleTime: STALE_TIMES.COMPLETIONS, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error prefetching course data:', error); + } + } + }, [courseId, queryClient]); + + return prefetchCourse; +}; diff --git a/src/learningpath/data/slice.js b/src/learningpath/data/slice.js deleted file mode 100644 index 1797f2a..0000000 --- a/src/learningpath/data/slice.js +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { createSlice } from '@reduxjs/toolkit'; - -const initialLearningPathState = () => ({ - fetching: false, - learningPaths: [], - errors: [], -}); - -const initialCoursesState = () => ({ - fetching: false, - courses: [], - errors: [], -}); - -const initialCompletionsState = () => ({ - fetching: false, - completions: {}, - errors: [], -}); - -const coursesSlice = createSlice({ - name: 'courses', - initialState: initialCoursesState(), - reducers: { - fetchCoursesRequest(state) { - state.fetching = true; - state.errors = []; - state.courses = []; - }, - fetchCoursesSuccess(state, action) { - state.fetching = false; - state.courses = action.payload.courses; - }, - fetchCoursesFailure(state, action) { - state.fetching = false; - state.errors = action.payload.errors; - }, - }, -}); - -const completionsSlice = createSlice({ - name: 'completions', - initialState: initialCompletionsState(), - reducers: { - fetchCompletionsRequest(state) { - state.fetching = true; - state.errors = []; - }, - fetchCompletionsSuccess(state, action) { - state.fetching = false; - const completionsMap = {}; - action.payload.completions.forEach(completion => { - completionsMap[completion.courseKey] = completion.completion; - }); - state.completions = completionsMap; - }, - fetchCompletionsFailure(state, action) { - state.fetching = false; - state.errors = action.payload.errors; - }, - }, -}); - -const learningPathSlice = createSlice({ - name: 'learningPath', - initialState: initialLearningPathState(), - reducers: { - fetchLearningPathsRequest(state) { - state.fetching = true; - state.errors = []; - state.learningPaths = []; - }, - fetchLearningPathsSuccess(state, action) { - state.fetching = false; - state.learningPaths = action.payload.learningPaths; - }, - fetchLearningPathsFailure(state, action) { - state.fetching = false; - state.errors = action.payload.errors; - }, - }, -}); - -export const { - fetchLearningPathsRequest, - fetchLearningPathsSuccess, - fetchLearningPathsFailure, -} = learningPathSlice.actions; - -export const { - fetchCoursesRequest, - fetchCoursesSuccess, - fetchCoursesFailure, -} = coursesSlice.actions; - -export const { - fetchCompletionsRequest, - fetchCompletionsSuccess, - fetchCompletionsFailure, -} = completionsSlice.actions; - -export const learningPathReducer = learningPathSlice.reducer; -export const coursesReducer = coursesSlice.reducer; -export const completionsReducer = completionsSlice.reducer; diff --git a/src/learningpath/data/thunks.js b/src/learningpath/data/thunks.js deleted file mode 100644 index 98493dd..0000000 --- a/src/learningpath/data/thunks.js +++ /dev/null @@ -1,126 +0,0 @@ -import * as api from './api'; -import { - fetchLearningPathsRequest, - fetchLearningPathsSuccess, - fetchLearningPathsFailure, - fetchCoursesRequest, - fetchCoursesSuccess, - fetchCoursesFailure, - fetchCompletionsRequest, - fetchCompletionsSuccess, - fetchCompletionsFailure, -} from './slice'; - -const shouldFetchCompletions = (state) => { - const { fetching, completions, errors } = state.completions; - return !fetching && (Object.keys(completions).length === 0 || errors.length > 0); -}; - -export const fetchCompletions = () => async (dispatch, getState) => { - if (!shouldFetchCompletions(getState())) { - const { completions: { completions } } = getState(); - return Promise.resolve(completions); - } - - try { - dispatch(fetchCompletionsRequest()); - const completions = await api.fetchAllCourseCompletions(); - dispatch(fetchCompletionsSuccess({ completions })); - return completions; - } catch (error) { - // eslint-disable-next-line no-console - console.error('Failed to fetch course completions:', error); - dispatch(fetchCompletionsFailure({ errors: [String(error)] })); - return []; - } -}; - -export const fetchLearningPaths = () => async (dispatch) => { - try { - dispatch(fetchLearningPathsRequest()); - await dispatch(fetchCompletions()); - const type = 'learning_path'; - const learningPathList = await api.fetchLearningPaths(); - const learningPaths = await Promise.all( - learningPathList.map(async (lp) => { - const lpProgress = await api.fetchLearningPathProgress(lp.key); - let status = 'In Progress'; - if (lpProgress.progress === 0.0) { - status = 'Not started'; - } else if (lpProgress.progress >= lpProgress.requiredCompletion) { - status = 'Completed'; - } - let percent = 0; - if (lpProgress.requiredCompletion) { - percent = lpProgress.requiredCompletion > 0 - ? Math.round((lpProgress.progress / lpProgress.requiredCompletion) * 100) - : 0; - } else { - percent = lpProgress.percent; - } - let maxDate = null; - const numCourses = lp.steps.length; - for (const course of lp.steps) { - if (course.dueDate) { - const dueDateObj = new Date(course.dueDate); - if (!maxDate || dueDateObj > maxDate) { - maxDate = dueDateObj; - } - } - } - // Convert maxDate to an ISO string for serialization - if (maxDate) { - maxDate = maxDate.toISOString(); - } - return { - ...lp, - numCourses, - status, - maxDate, - percent, - type, - }; - }), - ); - dispatch(fetchLearningPathsSuccess({ learningPaths })); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Failed to fetch learning paths:', error); - dispatch(fetchLearningPathsFailure({ errors: [String(error)] })); - } -}; - -export const fetchCourses = () => async (dispatch, getState) => { - try { - dispatch(fetchCoursesRequest()); - await dispatch(fetchCompletions()); - const type = 'course'; - const courses = await api.fetchAllCourseDetails(); - const { completions } = getState().completions; - - const coursesWithStatus = courses.map(course => { - const courseKey = `course-v1:${course.org}+${course.courseId}+${course.run}`; - const completion = completions[courseKey]?.percent || 0; - - let status = 'In progress'; - if (completion <= 0.0) { - status = 'Not started'; - } else if (completion >= 1.0) { - status = 'Completed'; - } - - return { - ...course, - status, - percent: completion, - type, - }; - }); - - dispatch(fetchCoursesSuccess({ courses: coursesWithStatus })); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Failed to fetch courses:', error); - dispatch(fetchCoursesFailure({ errors: [String(error)] })); - } -}; diff --git a/src/queryClient.js b/src/queryClient.js new file mode 100644 index 0000000..fd19651 --- /dev/null +++ b/src/queryClient.js @@ -0,0 +1,14 @@ +import { QueryClient } from '@tanstack/react-query'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 1, + staleTime: 5 * 60 * 1000, // 5 minutes + cacheTime: 10 * 60 * 1000, // 10 minutes + }, + }, +}); + +export default queryClient; diff --git a/src/store.js b/src/store.js deleted file mode 100644 index fcec2f0..0000000 --- a/src/store.js +++ /dev/null @@ -1,12 +0,0 @@ -import { configureStore } from '@reduxjs/toolkit'; -import { learningPathReducer, coursesReducer, completionsReducer } from './learningpath/data/slice'; - -const store = configureStore({ - reducer: { - learningPath: learningPathReducer, - courses: coursesReducer, - completions: completionsReducer, - }, -}); - -export default store; From 8bc6eea7eb75a7c4e7658df4c67db6046dc457a8 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 9 Apr 2025 12:22:23 +0200 Subject: [PATCH 32/45] fix: use API available to users for retrieving course details --- src/learningpath/CourseDetails.jsx | 66 +----------------------------- src/learningpath/data/api.js | 55 +++++++++++-------------- src/learningpath/data/queries.js | 4 +- 3 files changed, 27 insertions(+), 98 deletions(-) diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index 0b6f5ee..6b494d8 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -33,8 +33,6 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { selfPaced, courseImageAssetPath, description, - learningInfo, - instructorInfo, } = course; const dateDisplay = endDate @@ -147,12 +145,6 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { About - - What you'll learn - - - Instructors -
{org &&

{org}

} - {status === 'In progress' && ( + {status.toLowerCase() === 'in progress' && ( { {subtitleLine}

)} - {status === 'In progress' && ( + {status.toLowerCase() === 'in progress' && ( Date: Wed, 9 Apr 2025 13:42:07 +0200 Subject: [PATCH 36/45] perf: calculate learning path progress from available data --- src/learningpath/data/api.js | 6 --- src/learningpath/data/queries.js | 69 ++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/learningpath/data/api.js b/src/learningpath/data/api.js index 4073fb3..74fa2da 100644 --- a/src/learningpath/data/api.js +++ b/src/learningpath/data/api.js @@ -15,12 +15,6 @@ export async function fetchLearningPathDetail(key) { return camelCaseObject(response.data); } -export async function fetchLearningPathProgress(key) { - const client = getAuthenticatedHttpClient(); - const response = await client.get(`${getConfig().LMS_BASE_URL}/api/learning_paths/v1/${key}/progress/`); - return camelCaseObject(response.data); -} - export async function fetchCourses() { const response = await getAuthenticatedHttpClient().get(`${getConfig().LMS_BASE_URL}/api/learner_home/init/`); const courses = response.data.courses || []; diff --git a/src/learningpath/data/queries.js b/src/learningpath/data/queries.js index c174c52..eea09a3 100644 --- a/src/learningpath/data/queries.js +++ b/src/learningpath/data/queries.js @@ -26,33 +26,60 @@ export const STALE_TIMES = { }; // Learning Paths Queries -export const useLearningPaths = () => useQuery({ - queryKey: QUERY_KEYS.ALL_LEARNING_PATHS, - queryFn: async () => { - const learningPathList = await api.fetchLearningPaths(); +export const useLearningPaths = () => { + const queryClient = useQueryClient(); + + return useQuery({ + queryKey: QUERY_KEYS.ALL_LEARNING_PATHS, + queryFn: async () => { + await queryClient.prefetchQuery({ + queryKey: QUERY_KEYS.COURSE_COMPLETIONS, + queryFn: api.fetchAllCourseCompletions, + }); + + const completions = queryClient.getQueryData(QUERY_KEYS.COURSE_COMPLETIONS) || {}; + const completionsMap = createCompletionsMap(completions); + + const learningPathList = await api.fetchLearningPaths(); - const processedPaths = await Promise.all( - learningPathList.map(async (lp) => { - const lpProgress = await api.fetchLearningPathProgress(lp.key); + return learningPathList.map(lp => { + // Calculate progress based on course completions + const totalCourses = lp.steps.length; + + if (totalCourses === 0) { + return { + ...lp, + numCourses: 0, + status: 'Not started', + maxDate: null, + percent: 0, + type: 'learning_path', + }; + } + + const totalCompletion = lp.steps.reduce((sum, step) => { + const completion = completionsMap[step.courseKey]; + return sum + (completion?.percent ?? 0); + }, 0); + + const progress = totalCompletion / totalCourses; + const requiredCompletion = lp.requiredCompletion || 0; let status = 'In Progress'; - if (lpProgress.progress === 0.0) { + if (progress === 0) { status = 'Not started'; - } else if (lpProgress.progress >= lpProgress.requiredCompletion) { + } else if (progress >= requiredCompletion) { status = 'Completed'; } let percent = 0; - if (lpProgress.requiredCompletion) { - percent = lpProgress.requiredCompletion > 0 - ? Math.round((lpProgress.progress / lpProgress.requiredCompletion) * 100) - : 0; + if (requiredCompletion > 0) { + percent = Math.round((progress / requiredCompletion) * 100); } else { - percent = lpProgress.percent; + percent = Math.round(progress * 100); } let maxDate = null; - const numCourses = lp.steps.length; for (const course of lp.steps) { if (course.dueDate) { const dueDateObj = new Date(course.dueDate); @@ -65,18 +92,16 @@ export const useLearningPaths = () => useQuery({ return { ...lp, - numCourses, + numCourses: totalCourses, status, maxDate: isoMaxDate, percent, type: 'learning_path', }; - }), - ); - - return processedPaths; - }, -}); + }); + }, + }); +}; export const useLearningPathDetail = (key) => useQuery({ queryKey: QUERY_KEYS.LEARNING_PATH_DETAIL(key), From a059c95e385f973d6753c871c23569941542202c Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 9 Apr 2025 13:56:32 +0200 Subject: [PATCH 37/45] perf: simplify course caching --- src/learningpath/data/queries.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/learningpath/data/queries.js b/src/learningpath/data/queries.js index eea09a3..a0fe8ed 100644 --- a/src/learningpath/data/queries.js +++ b/src/learningpath/data/queries.js @@ -213,25 +213,21 @@ export const useCoursesByIds = (courseIds) => { const results = await Promise.all( courseIds.map(async (courseId) => { - const courseIdParts = courseId.split(':')[1]?.split('+'); - const simpleId = courseIdParts?.[1]; - - const cachedCourseDetail = simpleId - ? queryClient.getQueryData(['courseDetail', simpleId]) : null; + const cachedCourseDetail = queryClient.getQueryData(QUERY_KEYS.COURSE_DETAILS(courseId)); if (cachedCourseDetail) { - const basicCoursesData = queryClient.getQueryData(['basicCoursesData']) || []; - const basicData = basicCoursesData.find(c => c.courseId === simpleId) || {}; return { - ...addCompletionStatus( - { ...basicData, ...cachedCourseDetail }, - completionsMap, - courseId, - ), + ...cachedCourseDetail, + ...addCompletionStatus(cachedCourseDetail, completionsMap, courseId), type: 'course', }; } const detail = await api.fetchCourseDetails(courseId); + queryClient.setQueryData(QUERY_KEYS.COURSE_DETAILS(courseId), { + ...detail, + type: 'course', + }); + return { ...addCompletionStatus(detail, completionsMap, courseId), type: 'course', From d120b2a369e8967f3c257d7cdcb52486e6143ca1 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 9 Apr 2025 15:29:39 +0200 Subject: [PATCH 38/45] feat: do not use router for course modal Navigating to the previous page in the browser causes the modal to reopen, which is confusing. --- src/learningpath/CourseCard.jsx | 21 ++++++++-- src/learningpath/CourseDetails.jsx | 13 ++++-- src/learningpath/LearningPathDetails.jsx | 52 ++++++++++++++---------- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/learningpath/CourseCard.jsx b/src/learningpath/CourseCard.jsx index 90b4c5c..74393de 100644 --- a/src/learningpath/CourseCard.jsx +++ b/src/learningpath/CourseCard.jsx @@ -14,7 +14,7 @@ import { import { buildAssetUrl } from '../util/assetUrl'; import { usePrefetchCourseDetail } from './data/queries'; -const CourseCard = ({ course, parentPath }) => { +const CourseCard = ({ course, parentPath, onClick }) => { const courseKey = `course-v1:${course.org}+${course.courseId}+${course.run}`; const { name, @@ -37,6 +37,13 @@ const CourseCard = ({ course, parentPath }) => { ? `${parentPath}/course/${encodeURIComponent(courseKey)}` : `/course/${encodeURIComponent(courseKey)}`; + const handleViewClick = (e) => { + if (onClick) { + e.preventDefault(); + onClick(courseKey); + } + }; + let statusVariant = 'dark'; // default let statusIcon = 'fa-circle'; // default icon switch (status?.toLowerCase()) { @@ -106,9 +113,13 @@ const CourseCard = ({ course, parentPath }) => {
)}
- - - + {onClick ? ( + + ) : ( + + + + )}
@@ -128,10 +139,12 @@ CourseCard.propTypes = { percent: PropTypes.number.isRequired, }).isRequired, parentPath: PropTypes.string, + onClick: PropTypes.func, }; CourseCard.defaultProps = { parentPath: undefined, + onClick: undefined, }; export default CourseCard; diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index 6b494d8..d0dcd77 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -51,11 +51,12 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { }; const navigate = useNavigate(); const handleClose = onClose || (() => navigate(-1)); - const { courseKey } = useParams(); + const { courseKey: urlCourseKey } = useParams(); + const activeCourseKey = course.id || urlCourseKey; const learningMfeBase = getConfig().LEARNING_BASE_URL; const buildCourseHomeUrl = (key) => `${learningMfeBase}/learning/course/${key}/home`; const handleViewClick = () => { - window.location.href = buildCourseHomeUrl(courseKey); + window.location.href = buildCourseHomeUrl(activeCourseKey); }; return ( @@ -167,6 +168,7 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { CourseDetailContent.propTypes = { course: PropTypes.shape({ + id: PropTypes.string, name: PropTypes.string.isRequired, shortDescription: PropTypes.string, endDate: PropTypes.string, @@ -184,8 +186,9 @@ CourseDetailContent.defaultProps = { onClose: undefined, }; -const CourseDetailPage = ({ isModalView = false, onClose }) => { - const { courseKey } = useParams(); +const CourseDetailPage = ({ isModalView = false, onClose, courseKey: propCourseKey }) => { + const { courseKey: urlCourseKey } = useParams(); + const courseKey = propCourseKey || urlCourseKey; const { data: course, @@ -235,11 +238,13 @@ const CourseDetailPage = ({ isModalView = false, onClose }) => { CourseDetailPage.propTypes = { isModalView: PropTypes.bool, onClose: PropTypes.func, + courseKey: PropTypes.string, }; CourseDetailPage.defaultProps = { isModalView: false, onClose: undefined, + courseKey: undefined, }; export default CourseDetailPage; diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index d2af826..772c06a 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -1,7 +1,5 @@ -import React, { useMemo } from 'react'; -import { - useParams, Link, useNavigate, Routes, Route, -} from 'react-router-dom'; +import React, { useMemo, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; import { Row, Col, Spinner, Nav, Icon, ModalLayer, } from '@openedx/paragon'; @@ -19,7 +17,7 @@ import CourseDetailPage from './CourseDetails'; const LearningPathDetailPage = () => { const { key } = useParams(); - const navigate = useNavigate(); + const [selectedCourseKey, setSelectedCourseKey] = useState(null); const { data: detail, @@ -52,6 +50,14 @@ const LearningPathDetailPage = () => { return maxDate; }, [coursesForPath]); + const handleOpenCourseModal = (courseKey) => { + setSelectedCourseKey(courseKey); + }; + + const handleCloseCourseModal = () => { + setSelectedCourseKey(null); + }; + let content; if (loadingDetail || loadingCourses) { content = ; @@ -190,7 +196,11 @@ const LearningPathDetailPage = () => { {!loadingCourses && !coursesError && coursesForPath && coursesForPath.length > 0 && ( coursesForPath.map(course => (
- + handleOpenCourseModal(course.courseId ? `course-v1:${course.org}+${course.courseId}+${course.run}` : null)} + />
)) )} @@ -211,22 +221,20 @@ const LearningPathDetailPage = () => { return ( <> {content} - - navigate(`/learningpath/${key}`)} - className="lp-course-modal-layer" - > - - - )} - /> - + + {selectedCourseKey && ( + + + + )} ); }; From ee0db4c2b94dbc2ed5b10ff757d21092a3e3a89b Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Wed, 9 Apr 2025 15:53:19 +0200 Subject: [PATCH 39/45] fix: move the footer to the bottom of the page --- src/index.jsx | 30 ++++++++++++++++-------------- src/index.scss | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index c10c430..7f14ca8 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -26,20 +26,22 @@ subscribe(APP_READY, () => {
- - } - /> - } - /> - } - /> - +
+ + } + /> + } + /> + } + /> + +
{process.env.NODE_ENV === 'development' && } diff --git a/src/index.scss b/src/index.scss index c5cf57e..bdb3ae2 100644 --- a/src/index.scss +++ b/src/index.scss @@ -7,3 +7,22 @@ @import "~@edx/frontend-component-header/dist/index"; @import "~@edx/frontend-component-footer/dist/footer"; + +// Make the footer stick to the bottom when there is not enough content. +html, body, #root { + height: 100%; + margin: 0; +} + +#root { + display: flex; + flex-direction: column; +} + +#main-content { + flex: 1 0 auto; +} + +.footer { + flex-shrink: 0; +} From 4506543f21b4bb73651c3e27bc48da998807e80f Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Thu, 10 Apr 2025 16:22:56 -0400 Subject: [PATCH 40/45] fix: improve learning base url handling --- src/learningpath/CourseDetails.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/learningpath/CourseDetails.jsx b/src/learningpath/CourseDetails.jsx index d0dcd77..21ff6a8 100644 --- a/src/learningpath/CourseDetails.jsx +++ b/src/learningpath/CourseDetails.jsx @@ -54,7 +54,13 @@ const CourseDetailContent = ({ course, isModalView, onClose }) => { const { courseKey: urlCourseKey } = useParams(); const activeCourseKey = course.id || urlCourseKey; const learningMfeBase = getConfig().LEARNING_BASE_URL; - const buildCourseHomeUrl = (key) => `${learningMfeBase}/learning/course/${key}/home`; + const buildCourseHomeUrl = (key) => { + const trimmedBase = learningMfeBase.replace(/\/$/, ''); + const sanitizedBase = trimmedBase.endsWith('/learning') + ? trimmedBase + : `${trimmedBase}/learning`; + return `${sanitizedBase}/course/${key}/home`; + }; const handleViewClick = () => { window.location.href = buildCourseHomeUrl(activeCourseKey); }; From 3158642d1a896824b3062c9d607b3a914faa843a Mon Sep 17 00:00:00 2001 From: Pooja Kulkarni Date: Thu, 10 Apr 2025 16:39:35 -0400 Subject: [PATCH 41/45] fix: improve required skills handling --- src/learningpath/LearningPathDetails.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/learningpath/LearningPathDetails.jsx b/src/learningpath/LearningPathDetails.jsx index 772c06a..3daa441 100644 --- a/src/learningpath/LearningPathDetails.jsx +++ b/src/learningpath/LearningPathDetails.jsx @@ -207,9 +207,9 @@ const LearningPathDetailPage = () => {

Requirements

- {requiredSkills && requiredSkills.map((skill) => ( -

- {skill} + {requiredSkills && requiredSkills.map((skillObj) => ( +

+ {skillObj.skill.displayName}

))}
From 45f50a94fc6a08adaba5245904dc76fe11113837 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 14 Apr 2025 18:44:12 +0200 Subject: [PATCH 42/45] build: add Tutor support, change default port Port 8080 is commonly used, so it's likely to cause conflicts. --- .env.development | 6 +++--- package.json | 3 ++- webpack.dev-tutor.config.js | 0 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 webpack.dev-tutor.config.js diff --git a/.env.development b/.env.development index 82dcbd5..130e73e 100644 --- a/.env.development +++ b/.env.development @@ -1,7 +1,7 @@ NODE_ENV='development' -PORT=8080 +PORT=2100 ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' -BASE_URL='http://localhost:8080' +BASE_URL='http://localhost:2100' CREDENTIALS_BASE_URL='http://localhost:18150' CSRF_TOKEN_API_PATH='/csrf/api/v1/token' ECOMMERCE_BASE_URL='http://localhost:18130' @@ -17,7 +17,7 @@ MARKETING_SITE_BASE_URL='http://localhost:18000' ORDER_HISTORY_URL='http://localhost:1996/orders' REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' SEGMENT_KEY='' -SITE_NAME=localhost +SITE_NAME='edX' USER_INFO_COOKIE_NAME='edx-user-info' APP_ID='' MFE_CONFIG_API_URL='' diff --git a/package.json b/package.json index e8d8c3f..6993367 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "lint": "fedx-scripts eslint --ext .js --ext .jsx .", "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", "snapshot": "fedx-scripts jest --updateSnapshot", - "start": "fedx-scripts webpack-dev-server --host apps.local.edly.io --port 8080", + "start": "fedx-scripts webpack-dev-server --progress", + "start:local": "fedx-scripts webpack-dev-server --host apps.local.openedx.io --port 2100", "start:with-theme": "paragon install-theme && npm start && npm install", "test": "fedx-scripts jest --coverage --passWithNoTests" }, diff --git a/webpack.dev-tutor.config.js b/webpack.dev-tutor.config.js new file mode 100644 index 0000000..e69de29 From da860230300aac5cd0e49d177a4daadbcec0c242 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 14 Apr 2025 18:45:04 +0200 Subject: [PATCH 43/45] docs: use the correct README --- README-template-frontend-app.rst | 265 ------------------------------- README.rst | 261 +++++++++++++++++++----------- 2 files changed, 165 insertions(+), 361 deletions(-) delete mode 100644 README-template-frontend-app.rst diff --git a/README-template-frontend-app.rst b/README-template-frontend-app.rst deleted file mode 100644 index 15c49e6..0000000 --- a/README-template-frontend-app.rst +++ /dev/null @@ -1,265 +0,0 @@ -frontend-app-[PLACEHOLDER] -########################## - -.. note:: - - This README is a template. As a maintainer, please review its contents and - update all relevant sections. Instructions to you are marked with - "[PLACEHOLDER]" or "[TODO]". Update or remove those sections, and remove this - note when you are done. - -|license-badge| |status-badge| |ci-badge| |codecov-badge| - -.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-[PLACEHOLDER].svg - :target: https://github.com/openedx/frontend-app-[PLACEHOLDER]/blob/main/LICENSE - :alt: License - -.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen - -.. |ci-badge| image:: https://github.com/openedx/frontend-app-[PLACEHOLDER]/actions/workflows/ci.yml/badge.svg - :target: https://github.com/openedx/frontend-app-[PLACEHOLDER]/actions/workflows/ci.yml - :alt: Continuous Integration - -.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-[PLACEHOLDER]/coverage.svg?branch=main - :target: https://codecov.io/github/openedx/frontend-app[PLACEHOLDER]?branch=main - :alt: Codecov - -Purpose -======= - -.. note:: - - [TODO] - - What is this MFE? Add a 2-3 sentence description of what it is and what it - does. - -This is the Awesome MFE. It was built to provide an unmatched learning -experience, with improved tools for both randomized goodness and the ability to -directly reference amaze-blocks in existing courses. This experience is powered -by the new Fantastico storage engine. - -Getting Started -=============== - -Devstack Installation ---------------------- - -.. note:: - - [TODO] - - Describe in detail how this MFE can be installed and set up for development - in a devstack. Include as many screenshots as you can to make your guide - easier to follow! Use the following steps as an example: - -Follow these steps to provision, run, and enable an instance of the -[PLACEHOLDER] MFE for local development via the `devstack`_. - -.. _devstack: https://github.com/openedx/devstack#getting-started - -#. To start, clone the devstack repository as a child of an arbitrary ``~/workspace/`` directory. - - .. code-block:: - - mkdir -p ~/workspace/ - cd ~/workspace/ - git clone https://github.com/openedx/devstack.git - -#. Configure default services and setup devstack - - Create a ``devstack/options.local.mk`` file with only the services required. - Commonly, this will just be the LMS: - - .. code-block:: - - DEFAULT_SERVICES ?= \ - lms - -#. Start the devstack with: - - .. code-block:: - - cd devstack - make dev.pull - make dev.provision - make dev.up - -#. In an LMS shell, enable the ``ENABLE_[PLACEHOLDER]_MICROFRONTEND`` feature flag: - - .. code-block:: - - make lms-shell - vim /edx/etc/lms.yml - --- - FEATURES: - ENABLE_[PLACEHOLDER]_MICROFRONTEND: true - - Exit the shell and restart the LMS so changes take effect: - - .. code-block:: - - make lms-restart - -#. Create and enable the waffle flag required to redirect users to the MFE, - enabling it for everyone: - - .. code-block:: - - make lms-shell - ./manage.py lms waffle_flag --create --everyone [PLACEHOLDER].redirect_to_microfrontend - -#. Start this MFE with: - - .. code-block:: - - cd frontend-app-[PLACEHOLDER] - nvm use - npm ci - npm start - -#. Finally, open the MFE in a browser - - Navigate to `http://localhost:8080 `_ to open the - MFE. This is what it should look like if everything worked: - - .. figure:: ./docs/images/template.jpg - - "Polycon marking template" by mangtronix is licensed under CC BY-SA 2.0. - -Configuration -------------- - -.. note:: - - [TODO] - - Explicitly list anything that this MFE requires to function correctly. This includes: - - * A list of both required and optional .env variables, and how they each - affect the functioning of the MFE - - * A list of edx-platform `feature and waffle flags`_ that are either required - to enable use of this MFE, or affect the behavior of the MFE in some other - way - - * A list of IDAs or other MFEs that this MFE depends on to function correctly - -.. _feature and waffle flags: https://docs.openedx.org/projects/openedx-proposals/en/latest/best-practices/oep-0017-bp-feature-toggles.html - -Plugins -======= -This MFE can be customized using `Frontend Plugin Framework `_. - -The parts of this MFE that can be customized in that manner are documented `here `_. - -[PLACEHOLDER: Other Relevant Sections] -====================================== - -.. note:: - - [TODO] - - This is optional, but you might have additional sections you wish to cover. - For instance, architecture documentation, i18n notes, build process, or - more. - -Known Issues -============ - -.. note:: - - [TODO] - - If there are long-standing known issues, list them here as a bulletted list, - linking to the actual issues in the Github repository. - -Development Roadmap -=================== - -.. note:: - - [TODO] - - Include a list of current development targets, in (rough) descending order - of priority. It can be a simple bulleted list of roadmap items with links - to Github issues or wiki pages. - -Getting Help -============ - -.. note:: - - [TODO] - - Use the following as a template, but feel free to add specific places where - this MFE is commonly discussed. - -If you're having trouble, we have discussion forums at -https://discuss.openedx.org where you can connect with others in the community. - -Our real-time conversations are on Slack. You can request a `Slack -invitation`_, then join our `community Slack workspace`_. Because this is a -frontend repository, the best place to discuss it would be in the `#wg-frontend -channel`_. - -For anything non-trivial, the best path is to open an issue in this repository -with as many details about the issue you are facing as you can provide. - -https://github.com/openedx/frontend-app-[PLACEHOLDER]/issues - -For more information about these options, see the `Getting Help`_ page. - -.. _Slack invitation: https://openedx.org/slack -.. _community Slack workspace: https://openedx.slack.com/ -.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6 -.. _Getting Help: https://openedx.org/getting-help - -License -======= - -The code in this repository is licensed under the AGPLv3 unless otherwise -noted. - -Please see `LICENSE `_ for details. - -Contributing -============ - -.. note:: - - [TODO] - - Feel free to add contribution details specific to your repository. - -Contributions are very welcome. Please read `How To Contribute`_ for details. - -.. _How To Contribute: https://openedx.org/r/how-to-contribute - -This project is currently accepting all types of contributions, bug fixes, -security fixes, maintenance work, or new features. However, please make sure -to have a discussion about your new feature idea with the maintainers prior to -beginning development to maximize the chances of your change being accepted. -You can start a conversation by creating a new issue on this repo summarizing -your idea. - -The Open edX Code of Conduct -============================ - -All community members are expected to follow the `Open edX Code of Conduct`_. - -.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/ - -People -====== - -The assigned maintainers for this component and other project details may be -found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml`` -file in this repo. - -.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-app-[PLACEHOLDER] - -Reporting Security Issues -========================= - -Please do not report security issues in public. Email security@openedx.org instead. diff --git a/README.rst b/README.rst index 293567f..15c49e6 100644 --- a/README.rst +++ b/README.rst @@ -1,122 +1,199 @@ -frontend-template-application -############################# +frontend-app-[PLACEHOLDER] +########################## + +.. note:: + + This README is a template. As a maintainer, please review its contents and + update all relevant sections. Instructions to you are marked with + "[PLACEHOLDER]" or "[TODO]". Update or remove those sections, and remove this + note when you are done. |license-badge| |status-badge| |ci-badge| |codecov-badge| +.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-[PLACEHOLDER].svg + :target: https://github.com/openedx/frontend-app-[PLACEHOLDER]/blob/main/LICENSE + :alt: License + +.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen + +.. |ci-badge| image:: https://github.com/openedx/frontend-app-[PLACEHOLDER]/actions/workflows/ci.yml/badge.svg + :target: https://github.com/openedx/frontend-app-[PLACEHOLDER]/actions/workflows/ci.yml + :alt: Continuous Integration + +.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-[PLACEHOLDER]/coverage.svg?branch=main + :target: https://codecov.io/github/openedx/frontend-app[PLACEHOLDER]?branch=main + :alt: Codecov Purpose -******* +======= + +.. note:: + + [TODO] + + What is this MFE? Add a 2-3 sentence description of what it is and what it + does. -This repository is a template for Open edX micro-frontend applications. It is -flagged as a Template Repository, meaning it can be used as a basis for new -GitHub repositories by clicking the green "Use this template" button above. -The rest of this document describes how to work with your new micro-frontend -**after you've created a new repository from the template.** +This is the Awesome MFE. It was built to provide an unmatched learning +experience, with improved tools for both randomized goodness and the ability to +directly reference amaze-blocks in existing courses. This experience is powered +by the new Fantastico storage engine. Getting Started -*************** +=============== -After copying the template repository, you'll want to do a find-and-replace to -replace all instances of ``frontend-template-application`` with the name of -your new repository. Also edit index.html to replace "Application Template" -with a friendly name for this application that users will see in their browser -tab. +Devstack Installation +--------------------- -Prerequisites -============= +.. note:: -The `devstack`_ is currently recommended as a development environment for your -new MFE. If you start it with ``make dev.up.lms`` that should give you -everything you need as a companion to this frontend. + [TODO] -Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer -to the `relevant tutor-mfe documentation`_ to get started using it. + Describe in detail how this MFE can be installed and set up for development + in a devstack. Include as many screenshots as you can to make your guide + easier to follow! Use the following steps as an example: -.. _Devstack: https://github.com/openedx/devstack +Follow these steps to provision, run, and enable an instance of the +[PLACEHOLDER] MFE for local development via the `devstack`_. -.. _Tutor: https://github.com/overhangio/tutor +.. _devstack: https://github.com/openedx/devstack#getting-started -.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development +#. To start, clone the devstack repository as a child of an arbitrary ``~/workspace/`` directory. -Cloning and Startup -=================== + .. code-block:: -In the following steps, replace "[PLACEHOLDER]" with the name of the repo you -created when copying this template above. + mkdir -p ~/workspace/ + cd ~/workspace/ + git clone https://github.com/openedx/devstack.git -1. Clone your new repo: +#. Configure default services and setup devstack - ``git clone https://github.com/openedx/frontend-app-[PLACEHOLDER].git`` + Create a ``devstack/options.local.mk`` file with only the services required. + Commonly, this will just be the LMS: -2. Use node v18.x. + .. code-block:: - The current version of the micro-frontend build scripts support node 18. - Using other major versions of node *may* work, but this is unsupported. For - convenience, this repository includes an .nvmrc file to help in setting the - correct node version via `nvm `_. + DEFAULT_SERVICES ?= \ + lms -3. Install npm dependencies: +#. Start the devstack with: - ``cd frontend-app-[PLACEHOLDER] && npm install`` + .. code-block:: -4. Update the application port to use for local development: + cd devstack + make dev.pull + make dev.provision + make dev.up - Default port is 8080. If this does not work for you, update the line - `PORT=8080` to your port in all .env.* files +#. In an LMS shell, enable the ``ENABLE_[PLACEHOLDER]_MICROFRONTEND`` feature flag: -5. Start the dev server: + .. code-block:: - ``npm start`` + make lms-shell + vim /edx/etc/lms.yml + --- + FEATURES: + ENABLE_[PLACEHOLDER]_MICROFRONTEND: true -The dev server is running at `http://localhost:8080 `_ -or whatever port you setup. + Exit the shell and restart the LMS so changes take effect: -Making Your New Project's README File -===================================== + .. code-block:: -Move ``README-template-frontend-app.rst`` to your project's ``README.rst`` -file. Please fill out all the sections - this helps out all developers -understand your MFE, how to install it, and how to use it. + make lms-restart -Developing -********** +#. Create and enable the waffle flag required to redirect users to the MFE, + enabling it for everyone: -This section concerns development of ``frontend-template-application`` itself, -not the templated copy. + .. code-block:: -It should be noted that one of the goals of this repository is for it to -function correctly as an MFE (as in ``npm install && npm start``) even if no -modifications are made. This ensures that developers get a *practical* working -example, not just a theoretical one. + make lms-shell + ./manage.py lms waffle_flag --create --everyone [PLACEHOLDER].redirect_to_microfrontend -This also means, of course, that any committed code should be tested and -subject to both CI and branch protection rules. +#. Start this MFE with: -Project Structure -================= + .. code-block:: -The source for this project is organized into nested submodules according to -the `Feature-based Application Organization ADR`_. + cd frontend-app-[PLACEHOLDER] + nvm use + npm ci + npm start -.. _Feature-based Application Organization ADR: https://github.com/openedx/frontend-template-application/blob/master/docs/decisions/0002-feature-based-application-organization.rst +#. Finally, open the MFE in a browser -Build Process Notes -=================== + Navigate to `http://localhost:8080 `_ to open the + MFE. This is what it should look like if everything worked: + + .. figure:: ./docs/images/template.jpg + + "Polycon marking template" by mangtronix is licensed under CC BY-SA 2.0. + +Configuration +------------- + +.. note:: + + [TODO] + + Explicitly list anything that this MFE requires to function correctly. This includes: + + * A list of both required and optional .env variables, and how they each + affect the functioning of the MFE + + * A list of edx-platform `feature and waffle flags`_ that are either required + to enable use of this MFE, or affect the behavior of the MFE in some other + way + + * A list of IDAs or other MFEs that this MFE depends on to function correctly + +.. _feature and waffle flags: https://docs.openedx.org/projects/openedx-proposals/en/latest/best-practices/oep-0017-bp-feature-toggles.html -**Production Build** +Plugins +======= +This MFE can be customized using `Frontend Plugin Framework `_. -The production build is created with ``npm run build``. +The parts of this MFE that can be customized in that manner are documented `here `_. -Internationalization -==================== +[PLACEHOLDER: Other Relevant Sections] +====================================== -Please see refer to the `frontend-platform i18n howto`_ for documentation on -internationalization. +.. note:: -.. _frontend-platform i18n howto: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst + [TODO] + + This is optional, but you might have additional sections you wish to cover. + For instance, architecture documentation, i18n notes, build process, or + more. + +Known Issues +============ + +.. note:: + + [TODO] + + If there are long-standing known issues, list them here as a bulletted list, + linking to the actual issues in the Github repository. + +Development Roadmap +=================== + +.. note:: + + [TODO] + + Include a list of current development targets, in (rough) descending order + of priority. It can be a simple bulleted list of roadmap items with links + to Github issues or wiki pages. Getting Help -************ +============ + +.. note:: + + [TODO] + + Use the following as a template, but feel free to add specific places where + this MFE is commonly discussed. If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in the community. @@ -129,7 +206,7 @@ channel`_. For anything non-trivial, the best path is to open an issue in this repository with as many details about the issue you are facing as you can provide. -https://github.com/openedx/frontend-template-application/issues +https://github.com/openedx/frontend-app-[PLACEHOLDER]/issues For more information about these options, see the `Getting Help`_ page. @@ -139,7 +216,7 @@ For more information about these options, see the `Getting Help`_ page. .. _Getting Help: https://openedx.org/getting-help License -******* +======= The code in this repository is licensed under the AGPLv3 unless otherwise noted. @@ -147,7 +224,13 @@ noted. Please see `LICENSE `_ for details. Contributing -************ +============ + +.. note:: + + [TODO] + + Feel free to add contribution details specific to your repository. Contributions are very welcome. Please read `How To Contribute`_ for details. @@ -161,36 +244,22 @@ You can start a conversation by creating a new issue on this repo summarizing your idea. The Open edX Code of Conduct -**************************** +============================ All community members are expected to follow the `Open edX Code of Conduct`_. .. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/ People -****** +====== The assigned maintainers for this component and other project details may be found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml`` file in this repo. -.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-template-application +.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-app-[PLACEHOLDER] Reporting Security Issues -************************* - -Please do not report security issues in public, and email security@openedx.org instead. - -.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-template-application.svg - :target: https://github.com/openedx/frontend-template-application/blob/main/LICENSE - :alt: License - -.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen +========================= -.. |ci-badge| image:: https://github.com/openedx/frontend-template-application/actions/workflows/ci.yml/badge.svg - :target: https://github.com/openedx/frontend-template-application/actions/workflows/ci.yml - :alt: Continuous Integration - -.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-template-application/coverage.svg?branch=main - :target: https://codecov.io/github/openedx/frontend-template-application?branch=main - :alt: Codecov +Please do not report security issues in public. Email security@openedx.org instead. From 69100d1d50a9808d6c4cf5bb6f93ac187c6b3fac Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 14 Apr 2025 18:49:43 +0200 Subject: [PATCH 44/45] docs: update README --- README.rst | 221 ++++++++++++++++++++++------------------------------- 1 file changed, 91 insertions(+), 130 deletions(-) diff --git a/README.rst b/README.rst index 15c49e6..52b7ef2 100644 --- a/README.rst +++ b/README.rst @@ -1,200 +1,167 @@ -frontend-app-[PLACEHOLDER] -########################## - -.. note:: - - This README is a template. As a maintainer, please review its contents and - update all relevant sections. Instructions to you are marked with - "[PLACEHOLDER]" or "[TODO]". Update or remove those sections, and remove this - note when you are done. +frontend-app-learning-paths +########################### |license-badge| |status-badge| |ci-badge| |codecov-badge| -.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-[PLACEHOLDER].svg - :target: https://github.com/openedx/frontend-app-[PLACEHOLDER]/blob/main/LICENSE +.. |license-badge| image:: https://img.shields.io/github/license/open-craft/frontend-app-learning-paths.svg + :target: https://github.com/open-craft/frontend-app-learning-paths/blob/main/LICENSE :alt: License .. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen -.. |ci-badge| image:: https://github.com/openedx/frontend-app-[PLACEHOLDER]/actions/workflows/ci.yml/badge.svg - :target: https://github.com/openedx/frontend-app-[PLACEHOLDER]/actions/workflows/ci.yml +.. |ci-badge| image:: https://github.com/open-craft/frontend-app-learning-paths/actions/workflows/ci.yml/badge.svg + :target: https://github.com/open-craft/frontend-app-learning-paths/actions/workflows/ci.yml :alt: Continuous Integration -.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-[PLACEHOLDER]/coverage.svg?branch=main - :target: https://codecov.io/github/openedx/frontend-app[PLACEHOLDER]?branch=main +.. |codecov-badge| image:: https://codecov.io/github/open-craft/frontend-app-learning-paths/coverage.svg?branch=main + :target: https://codecov.io/github/open-craft/frontend-app-learning-paths?branch=main :alt: Codecov Purpose ======= -.. note:: +The Learning Paths MFE provides a specialized frontend interface for managing and displaying +learning paths in Open edX. Learning paths are curated sequences of courses that guide learners +through a structured educational journey toward mastering specific skills or knowledge areas. - [TODO] +This MFE serves as the frontend for the learning-paths-plugin_, which provides the complete backend functionality. - What is this MFE? Add a 2-3 sentence description of what it is and what it - does. - -This is the Awesome MFE. It was built to provide an unmatched learning -experience, with improved tools for both randomized goodness and the ability to -directly reference amaze-blocks in existing courses. This experience is powered -by the new Fantastico storage engine. +.. _learning-paths-plugin: https://github.com/open-craft/learning-paths-plugin/ Getting Started =============== -Devstack Installation ---------------------- +Tutor Setup +----------- -.. note:: +Follow these steps to set up the Learning Paths MFE with Tutor: - [TODO] +#. Navigate to your Tutor plugins directory: - Describe in detail how this MFE can be installed and set up for development - in a devstack. Include as many screenshots as you can to make your guide - easier to follow! Use the following steps as an example: + .. code-block:: bash -Follow these steps to provision, run, and enable an instance of the -[PLACEHOLDER] MFE for local development via the `devstack`_. + cd "$(tutor plugins printroot)" -.. _devstack: https://github.com/openedx/devstack#getting-started +#. Create a file named ``learning_paths.py`` with the following content: -#. To start, clone the devstack repository as a child of an arbitrary ``~/workspace/`` directory. + .. code-block:: python - .. code-block:: + from tutormfe.hooks import MFE_APPS - mkdir -p ~/workspace/ - cd ~/workspace/ - git clone https://github.com/openedx/devstack.git + @MFE_APPS.add() + def _add_learning_paths_mfe(mfes): + mfes["learning-paths"] = { + "repository": "https://github.com/open-craft/frontend-app-learning-paths.git", + "port": 2100, + "version": "main", # optional, will default to the Open edX current tag + } + return mfes -#. Configure default services and setup devstack +#. Enable the plugin: - Create a ``devstack/options.local.mk`` file with only the services required. - Commonly, this will just be the LMS: + .. code-block:: bash - .. code-block:: + tutor plugins enable learning_paths - DEFAULT_SERVICES ?= \ - lms +#. Build the MFE image: -#. Start the devstack with: + .. code-block:: bash - .. code-block:: + tutor images build mfe - cd devstack - make dev.pull - make dev.provision - make dev.up +#. Restart the MFE container to apply changes: -#. In an LMS shell, enable the ``ENABLE_[PLACEHOLDER]_MICROFRONTEND`` feature flag: + .. code-block:: bash - .. code-block:: + tutor dev stop mfe && tutor dev start -d - make lms-shell - vim /edx/etc/lms.yml - --- - FEATURES: - ENABLE_[PLACEHOLDER]_MICROFRONTEND: true +#. Access the Learning Paths MFE at: http://apps.local.openedx.io:2100/learning-paths/ - Exit the shell and restart the LMS so changes take effect: +Development Setup +----------------- - .. code-block:: +After completing the Tutor setup, prepare the repository for local development: - make lms-restart +#. Clone this repository: -#. Create and enable the waffle flag required to redirect users to the MFE, - enabling it for everyone: + .. code-block:: bash - .. code-block:: + git clone https://github.com/open-craft/frontend-app-learning-paths.git + cd frontend-app-learning-paths - make lms-shell - ./manage.py lms waffle_flag --create --everyone [PLACEHOLDER].redirect_to_microfrontend +#. Create `.env.private` with the following content: -#. Start this MFE with: + .. code-block:: bash - .. code-block:: + LMS_BASE_URL='http://local.openedx.io:8000' + LOGIN_URL='http://local.openedx.io:8000/login' + LOGOUT_URL='http://local.openedx.io:8000/logout' + REFRESH_ACCESS_TOKEN_ENDPOINT='http://local.openedx.io:8000/login_refresh' + TERMS_OF_SERVICE_URL='https://www.edx.org/edx-terms-service' + PRIVACY_POLICY_URL='http://local.openedx.io:8000/privacy' - cd frontend-app-[PLACEHOLDER] - nvm use - npm ci - npm start +#. Install dependencies: -#. Finally, open the MFE in a browser + .. code-block:: bash - Navigate to `http://localhost:8080 `_ to open the - MFE. This is what it should look like if everything worked: + npm install - .. figure:: ./docs/images/template.jpg +#. Mount the repository for development: - "Polycon marking template" by mangtronix is licensed under CC BY-SA 2.0. + .. code-block:: bash -Configuration -------------- + cd .. + tutor mounts add $(pwd)/frontend-app-learning-paths -.. note:: +#. Restart the MFE container (to unbind the port) and start the MFEs: - [TODO] + .. code-block:: bash - Explicitly list anything that this MFE requires to function correctly. This includes: + tutor dev stop mfe && tutor dev start -d - * A list of both required and optional .env variables, and how they each - affect the functioning of the MFE +#. Make changes to the code and see them reflected in real-time. - * A list of edx-platform `feature and waffle flags`_ that are either required - to enable use of this MFE, or affect the behavior of the MFE in some other - way +Local Development +----------------- - * A list of IDAs or other MFEs that this MFE depends on to function correctly +You can also run this MFE locally without mounting it in Tutor: -.. _feature and waffle flags: https://docs.openedx.org/projects/openedx-proposals/en/latest/best-practices/oep-0017-bp-feature-toggles.html +#. First, create a Tutor plugin to add CORS configuration: -Plugins -======= -This MFE can be customized using `Frontend Plugin Framework `_. + .. code-block:: bash -The parts of this MFE that can be customized in that manner are documented `here `_. + cd "$(tutor plugins printroot)" -[PLACEHOLDER: Other Relevant Sections] -====================================== +#. Create a file named ``learning_paths.py`` with the following content: -.. note:: + .. code-block:: python - [TODO] - - This is optional, but you might have additional sections you wish to cover. - For instance, architecture documentation, i18n notes, build process, or - more. - -Known Issues -============ + from tutor import hooks -.. note:: + hooks.Filters.ENV_PATCHES.add_item( + ( + "openedx-lms-common-settings", + 'CORS_ORIGIN_WHITELIST.append("http://apps.local.openedx.io:2100")' + ) + ) - [TODO] +#. Enable the plugin: - If there are long-standing known issues, list them here as a bulletted list, - linking to the actual issues in the Github repository. + .. code-block:: bash -Development Roadmap -=================== + tutor plugins enable learning_paths -.. note:: +#. Run the MFE locally: - [TODO] + .. code-block:: bash - Include a list of current development targets, in (rough) descending order - of priority. It can be a simple bulleted list of roadmap items with links - to Github issues or wiki pages. + cd frontend-app-learning-paths + npm install + npm start --local Getting Help ============ -.. note:: - - [TODO] - - Use the following as a template, but feel free to add specific places where - this MFE is commonly discussed. - If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in the community. @@ -206,7 +173,7 @@ channel`_. For anything non-trivial, the best path is to open an issue in this repository with as many details about the issue you are facing as you can provide. -https://github.com/openedx/frontend-app-[PLACEHOLDER]/issues +https://github.com/open-craft/frontend-app-learning-paths/issues For more information about these options, see the `Getting Help`_ page. @@ -226,12 +193,6 @@ Please see `LICENSE `_ for details. Contributing ============ -.. note:: - - [TODO] - - Feel free to add contribution details specific to your repository. - Contributions are very welcome. Please read `How To Contribute`_ for details. .. _How To Contribute: https://openedx.org/r/how-to-contribute @@ -257,7 +218,7 @@ The assigned maintainers for this component and other project details may be found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml`` file in this repo. -.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-app-[PLACEHOLDER] +.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-app-learning-paths Reporting Security Issues ========================= From a5f1f2ced02d602e42531481eb260f9000d5f1b1 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 14 Apr 2025 18:50:40 +0200 Subject: [PATCH 45/45] docs: update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index fd97f82..14f3c49 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ temp/babel-plugin-react-intl *~ /temp /.vscode + +# Local environment overrides +.env.private