diff --git a/api/src/exam-environment/routes/exam-environment.test.ts b/api/src/exam-environment/routes/exam-environment.test.ts index 59e92dc14ed..ca4e3f00374 100644 --- a/api/src/exam-environment/routes/exam-environment.test.ts +++ b/api/src/exam-environment/routes/exam-environment.test.ts @@ -1232,34 +1232,77 @@ describe('/exam-environment/', () => { }); }); - describe('GET /exam-environment/challenges/:challengeId/exam-mappings', () => { + describe('GET /exam-environment/exam-challenge', () => { afterAll(async () => { await fastifyTestInstance.prisma.examEnvironmentChallenge.deleteMany( {} ); }); - it('should return 200 and an empty array if no exams are mapped to the challenge', async () => { + it('should return 200 and an empty array if no mapping exists', async () => { const challengeId = mock.oid(); - const res = await superGet( - `/exam-environment/challenges/${challengeId}/exam-mappings` + const examId = mock.oid(); + + const res1 = await superGet( + `/exam-environment/exam-challenge?challengeId=${challengeId}` ); - expect(res.body).toStrictEqual([]); - expect(res.status).toBe(200); + expect(res1.body).toStrictEqual([]); + expect(res1.status).toBe(200); + + const res2 = await superGet( + `/exam-environment/exam-challenge?examId=${examId}` + ); + expect(res2.body).toStrictEqual([]); + expect(res2.status).toBe(200); + + const res3 = await superGet( + `/exam-environment/exam-challenge?challengeId=${challengeId}&examId=${examId}` + ); + expect(res3.body).toStrictEqual([]); + expect(res3.status).toBe(200); }); - it('should return 200 and a list of exams mapped to the challenge', async () => { + it('should return 200 and a list of challenge-exam mappings if one exists', async () => { await fastifyTestInstance.prisma.examEnvironmentChallenge.create({ data: mock.examEnvironmentChallenge }); - const res = await superGet( - `/exam-environment/challenges/${mock.examEnvironmentChallenge.challengeId}/exam-mappings` + const res1 = await superGet( + `/exam-environment/exam-challenge?challengeId=${mock.examEnvironmentChallenge.challengeId}` ); - expect(res.body).toEqual( + expect(res1.body).toEqual( expect.arrayContaining([ - expect.objectContaining({ examId: mock.examId }) + expect.objectContaining({ + examId: mock.examId, + challengeId: mock.examEnvironmentChallenge.challengeId + }) ]) ); - expect(res.status).toBe(200); + expect(res1.status).toBe(200); + + const res2 = await superGet( + `/exam-environment/exam-challenge?examId=${mock.examId}` + ); + expect(res2.body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + examId: mock.examId, + challengeId: mock.examEnvironmentChallenge.challengeId + }) + ]) + ); + expect(res2.status).toBe(200); + + const res3 = await superGet( + `/exam-environment/exam-challenge?challengeId=${mock.examEnvironmentChallenge.challengeId}&examId=${mock.examId}` + ); + expect(res3.body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + examId: mock.examId, + challengeId: mock.examEnvironmentChallenge.challengeId + }) + ]) + ); + expect(res3.status).toBe(200); }); }); }); diff --git a/api/src/exam-environment/routes/exam-environment.ts b/api/src/exam-environment/routes/exam-environment.ts index 50bdc85000b..4346ca412ee 100644 --- a/api/src/exam-environment/routes/exam-environment.ts +++ b/api/src/exam-environment/routes/exam-environment.ts @@ -89,11 +89,11 @@ export const examEnvironmentOpenRoutes: FastifyPluginCallbackTypebox = ( tokenMetaHandler ); fastify.get( - '/exam-environment/challenges/:challengeId/exam-mappings', + '/exam-environment/exam-challenge', { - schema: schemas.examEnvironmentGetExamMappingsByChallengeId + schema: schemas.examEnvironmentGetExamChallenge }, - getExamMappingsByChallengeId + getExamChallenge ); done(); }; @@ -1000,22 +1000,21 @@ export async function getExamAttemptsByExamIdHandler( /** * Gets all the relations for a given challenge and exam(s). */ -export async function getExamMappingsByChallengeId( +export async function getExamChallenge( this: FastifyInstance, - req: UpdateReqType< - typeof schemas.examEnvironmentGetExamMappingsByChallengeId - >, + req: UpdateReqType, reply: FastifyReply ) { const logger = this.log.child({ req }); - const { challengeId } = req.params; + const { challengeId, examId } = req.query; - logger.info({ challengeId }); + logger.info({ challengeId, examId }); const maybeData = await mapErr( this.prisma.examEnvironmentChallenge.findMany({ where: { - challengeId + challengeId: challengeId ?? undefined, + examId: examId ?? undefined } }) ); diff --git a/api/src/exam-environment/schemas/challenges.ts b/api/src/exam-environment/schemas/challenges.ts index 18377824421..db2a756dd68 100644 --- a/api/src/exam-environment/schemas/challenges.ts +++ b/api/src/exam-environment/schemas/challenges.ts @@ -1,9 +1,10 @@ import { Type } from '@fastify/type-provider-typebox'; // import { STANDARD_ERROR } from '../utils/errors'; -export const examEnvironmentGetExamMappingsByChallengeId = { - params: Type.Object({ - challengeId: Type.String({ format: 'objectid' }) +export const examEnvironmentGetExamChallenge = { + querystring: Type.Object({ + challengeId: Type.Optional(Type.String({ format: 'objectid' })), + examId: Type.Optional(Type.String({ format: 'objectid' })) }) // response: { // 200: examEnvAttempt, diff --git a/api/src/exam-environment/schemas/index.ts b/api/src/exam-environment/schemas/index.ts index 779426dfe85..9f62893a28a 100644 --- a/api/src/exam-environment/schemas/index.ts +++ b/api/src/exam-environment/schemas/index.ts @@ -7,4 +7,4 @@ export { export { examEnvironmentPostExamGeneratedExam } from './exam-environment-exam-generated-exam.js'; export { examEnvironmentTokenMeta } from './token-meta.js'; export { examEnvironmentExams } from './exam-environment-exams.js'; -export { examEnvironmentGetExamMappingsByChallengeId } from './challenges.js'; +export { examEnvironmentGetExamChallenge } from './challenges.js'; diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 1e849c855bf..77e38c1ac50 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -380,6 +380,14 @@ } }, "exam": { + "attempts": "Attempts", + "no-attempts-yet": "No attempts yet", + "date-taken": "Date Taken", + "score": "Score", + "status": "Status", + "pending": "Pending", + "passed": "Passed", + "failed": "Failed", "download-header": "Download the freeCodeCamp Exam Environment App", "explanation": "To earn a certification, you must take an exam to test your understanding of the material you have learned. Taking the exam is absolutely free of charge.", "version": "The latest version of our app is: {{version}}.", diff --git a/client/package.json b/client/package.json index bf970eeac83..72ec63cf450 100644 --- a/client/package.json +++ b/client/package.json @@ -57,6 +57,7 @@ "@loadable/component": "5.16.3", "@redux-devtools/extension": "3.3.0", "@redux-saga/core": "^1.2.2", + "@reduxjs/toolkit": "2.8.2", "@stripe/react-stripe-js": "1.16.5", "@stripe/stripe-js": "1.54.2", "algoliasearch": "4.22.1", diff --git a/client/src/redux/create-store.ts b/client/src/redux/create-store.ts index bec720fad86..40deb667bd0 100644 --- a/client/src/redux/create-store.ts +++ b/client/src/redux/create-store.ts @@ -1,10 +1,12 @@ import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; -import { composeWithDevTools } from '@redux-devtools/extension'; +// import { composeWithDevTools } from '@redux-devtools/extension'; import { createEpicMiddleware } from 'redux-observable'; import createSagaMiddleware from 'redux-saga'; +import { configureStore } from '@reduxjs/toolkit'; import envData from '../../config/env.json'; import { isBrowser } from '../../utils'; +import { examAttempts } from '../utils/ajax'; import rootEpic from './root-epic'; import rootReducer from './root-reducer'; import rootSaga from './root-saga'; @@ -32,9 +34,9 @@ const epicMiddleware = createEpicMiddleware({ } }); -const composeEnhancers = composeWithDevTools({ - // options like actionSanitizer, stateSanitizer -}); +// const composeEnhancers = composeWithDevTools({ +// // options like actionSanitizer, stateSanitizer +// }); export const createStore = (preloadedState = {}) => { let store; @@ -42,14 +44,30 @@ export const createStore = (preloadedState = {}) => { store = reduxCreateStore( rootReducer, preloadedState, - applyMiddleware(sagaMiddleware, epicMiddleware) + // @ts-expect-error RTK uses unknown, Redux uses any + applyMiddleware(sagaMiddleware, epicMiddleware, examAttempts.middleware) ); } else { - store = reduxCreateStore( - rootReducer, - preloadedState, - composeEnhancers(applyMiddleware(sagaMiddleware, epicMiddleware)) - ); + // store = reduxCreateStore( + // rootReducer, + // preloadedState, + // composeEnhancers( + // // @ts-expect-error RTK uses unknown, Redux uses any + // applyMiddleware(sagaMiddleware, epicMiddleware, examAttempts.middleware) + // ) + // ); + store = configureStore({ + // @ts-expect-error RTK uses unknown, Redux uses any + reducer: rootReducer, + // @ts-expect-error RTK uses unknown, Redux uses any + middleware: getDefaultMiddleware => { + return getDefaultMiddleware() + .concat(examAttempts.middleware) + .concat(sagaMiddleware) + .concat(epicMiddleware); + }, + preloadedState + }); } sagaMiddleware.run(rootSaga); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument diff --git a/client/src/redux/root-reducer.ts b/client/src/redux/root-reducer.ts index 056083926f1..56ae50b3856 100644 --- a/client/src/redux/root-reducer.ts +++ b/client/src/redux/root-reducer.ts @@ -13,6 +13,7 @@ import { ns as curriculumMapNameSpace, reducer as curriculumMap } from '../templates/Introduction/redux'; +import { examAttempts } from '../utils/ajax'; import { ns as appNameSpace } from './action-types'; import { ns as settingsNameSpace, reducer as settings } from './settings'; import { FlashApp as flashNameSpace } from './types'; @@ -24,5 +25,6 @@ export default combineReducers({ [curriculumMapNameSpace]: curriculumMap, [flashNameSpace]: flash, [searchNameSpace]: search, - [settingsNameSpace]: settings + [settingsNameSpace]: settings, + [examAttempts.reducerPath]: examAttempts.reducer }); diff --git a/client/src/templates/Challenges/exam-download/attempts.tsx b/client/src/templates/Challenges/exam-download/attempts.tsx new file mode 100644 index 00000000000..557af7dcd9c --- /dev/null +++ b/client/src/templates/Challenges/exam-download/attempts.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from 'react'; +import { Table } from '@freecodecamp/ui'; +import { useTranslation } from 'react-i18next'; + +import { Loader } from '../../../components/helpers'; +import { examAttempts } from '../../../utils/ajax'; + +interface AttemptsProps { + id: string; +} + +export function Attempts({ id }: AttemptsProps) { + const { t } = useTranslation(); + + const examIdsQuery = examAttempts.useGetExamIdsByChallengeIdQuery(id); + const [getAttempts, attemptsMutation] = + examAttempts.useGetExamAttemptsByExamIdMutation(); + + useEffect(() => { + if (!examIdsQuery.data) { + return; + } + + const examId = examIdsQuery.data.at(0)!.examId; + void getAttempts(examId); + }, [examIdsQuery.data, getAttempts]); + + if (examIdsQuery.isLoading || attemptsMutation.isLoading) { + return ; + } + if (examIdsQuery.error || !examIdsQuery.data) { + console.error(examIdsQuery.error); + return

{t('flash.generic-error')}

; + } + + if (attemptsMutation.error) { + console.error(attemptsMutation.error); + return

{t('flash.generic-error')}

; + } + + const attempts = attemptsMutation.data; + + if (attempts === undefined) { + return ; + } + + if (attempts.length === 0) { + return

{t('exam.no-attempts-yet')}

; + } + + return ( + + + + + + + + + + {attempts.map(attempt => ( + + + + + + ))} + +
{t('exam.date-taken')}{t('exam.score')} [%]{t('exam.status')}
{new Date(attempt.startTimeInMS).toTimeString()} + {attempt.result + ? `${attempt.result.percent}%` + : t('exam.pending')} + + {attempt.result + ? attempt.result.passed + ? t('exam.passed') + : t('exam.failed') + : t('exam.pending')} +
+ ); +} diff --git a/client/src/templates/Challenges/exam-download/show.tsx b/client/src/templates/Challenges/exam-download/show.tsx index 04d55af385f..45836b221ee 100644 --- a/client/src/templates/Challenges/exam-download/show.tsx +++ b/client/src/templates/Challenges/exam-download/show.tsx @@ -1,9 +1,16 @@ import React, { useEffect, useState } from 'react'; -import { Button, Spacer } from '@freecodecamp/ui'; +import { graphql } from 'gatsby'; +import { Button, Dropdown, MenuItem, Spacer } from '@freecodecamp/ui'; import { isEmpty } from 'lodash'; -import { useTranslation } from 'react-i18next'; +import { useTranslation, withTranslation } from 'react-i18next'; +import { createSelector } from 'reselect'; +import { connect } from 'react-redux'; + import { FullWidthRow } from '../../../components/helpers'; import useDetectOS from '../utils/use-detect-os'; +import { ChallengeNode } from '../../../redux/prop-types'; +import { isSignedInSelector } from '../../../redux/selectors'; +import { Attempts } from './attempts'; interface GitProps { tag_name: string; @@ -12,7 +19,26 @@ interface GitProps { }[]; } -function ShowExamDownload(): JSX.Element { +const mapStateToProps = createSelector( + isSignedInSelector, + (isSignedIn: boolean) => ({ + isSignedIn + }) +); + +interface ShowExamDownloadProps { + data: { challengeNode: ChallengeNode }; + isSignedIn: boolean; +} + +function ShowExamDownload({ + data: { + challengeNode: { + challenge: { id } + } + }, + isSignedIn +}: ShowExamDownloadProps): JSX.Element { const [latestVersion, setLatestVersion] = useState(null); const [downloadLink, setDownloadLink] = useState(''); @@ -22,8 +48,8 @@ function ShowExamDownload(): JSX.Element { const { t } = useTranslation(); - const handleDownloadLink = (downloadLinks: string[]) => { - const win = downloadLinks.find(link => link.match(/\.msi/)); + function handleDownloadLink(downloadLinks: string[]) { + const win = downloadLinks.find(link => link.match(/\.exe/)); const macARM = downloadLinks.find( link => link.match(/aarch64/) && link.match(/\.dmg/) ); @@ -70,41 +96,44 @@ function ShowExamDownload(): JSX.Element { } return ''; - }; + } useEffect(() => { - const checkLatestVersion = async () => { - return await fetch( - 'https://api.github.com/repos/freeCodeCamp/exam-env/releases/latest' - ); - }; - checkLatestVersion() - .then(response => { + async function checkLatestVersion() { + try { + const response = await fetch( + 'https://api.github.com/repos/freeCodeCamp/exam-env/releases/latest' + ); if (response.ok) { - void response.json().then(data => { - const { tag_name, assets } = data as GitProps; - setLatestVersion(tag_name); - - setDownloadLink( - handleDownloadLink( - assets.map(links => links.browser_download_url) - ) - ); - - setDownloadLinks(assets.map(links => links.browser_download_url)); - }); + const data = (await response.json()) as GitProps; + const { tag_name, assets } = data; + setLatestVersion(tag_name); + const urls = assets.map(link => link.browser_download_url); + setDownloadLink(handleDownloadLink(urls)); + setDownloadLinks(urls); } - }) - .catch(() => { + } catch { setLatestVersion('...'); - }); - }); + } + } + + void checkLatestVersion(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [os]); return (

{t('exam.download-header')}

{t('exam.explanation')}

+ + {isSignedIn && ( + <> +

{t('exam.attempts')}

+ + + + )}

{t('exam.version', { version: latestVersion || '...' @@ -118,29 +147,48 @@ function ShowExamDownload(): JSX.Element { > {t('buttons.download-latest-version')} - {!downloadLink && {t('exam.unable-to-detect-os')}} - -

- {t('exam.download-details')} -
    + {!downloadLink && ( + <> + + {t('exam.unable-to-detect-os')} + + )} + + + {t('exam.download-details')} + {downloadLinks .filter(link => !link.match(/\.sig|\.json/)) .map((link, index) => { return ( -
  • - - {link} - -
  • + + {link} + ); })} -
-
+ + - {t('exam.download-trouble')} + {t('exam.download-trouble')}{' '} support@freecodecamp.org
); } -export default ShowExamDownload; +export default connect(mapStateToProps)(withTranslation()(ShowExamDownload)); + +// GraphQL +export const query = graphql` + query ExamEnvironmentExam($id: String!) { + challengeNode(id: { eq: $id }) { + challenge { + id + } + } + } +`; diff --git a/client/src/utils/ajax.ts b/client/src/utils/ajax.ts index 75b45a2874f..231f16b84e8 100644 --- a/client/src/utils/ajax.ts +++ b/client/src/utils/ajax.ts @@ -1,6 +1,7 @@ import cookies from 'browser-cookies'; -import envData from '../../config/env.json'; +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import envData from '../../config/env.json'; import type { ChallengeFile, ChallengeFiles, @@ -212,6 +213,32 @@ export function getGenerateExam( return get(`/exam/${challengeId}`); } +export interface Exam { + id: string; + config: { + name: string; + }; +} + +export interface Attempt { + id: string; + examId: string; + startTimeInMS: number; + questionSets: unknown[]; + result?: { + passed: boolean; + percent: number; + }; +} + +export function getExams(): Promise> { + return get('/user/exam-environment/exams'); +} + +export function getExamAttempts(): Promise> { + return get('/user/exam-environment/exam/attempts'); +} + /** POST **/ interface Donation { @@ -393,3 +420,31 @@ export function deleteUserToken(): Promise> { export function deleteMsUsername(): Promise> { return deleteRequest('/user/ms-username', {}); } + +/** RTK */ + +export interface ExamEnvironmentChallenge { + id: string; + examId: string; + challengeId: string; +} + +export const examAttempts = createApi({ + reducerPath: 'exam-attempts', + baseQuery: fetchBaseQuery({ + baseUrl: apiLocation, + headers: { + 'CSRF-Token': getCSRFToken() + }, + credentials: 'include' + }), + endpoints: build => ({ + getExamAttemptsByExamId: build.mutation({ + query: examId => `/user/exam-environment/exams/${examId}/attempts` + }), + getExamIdsByChallengeId: build.query({ + query: challengeId => + `/exam-environment/exam-challenge?challengeId=${challengeId}` + }) + }) +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32b26157e89..d16225418b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -348,6 +348,9 @@ importers: '@redux-saga/core': specifier: ^1.2.2 version: 1.3.0 + '@reduxjs/toolkit': + specifier: 2.8.2 + version: 2.8.2(react-redux@7.2.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2) '@stripe/react-stripe-js': specifier: 1.16.5 version: 1.16.5(@stripe/stripe-js@1.54.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -3764,6 +3767,17 @@ packages: '@redux-saga/types@1.2.1': resolution: {integrity: sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==} + '@reduxjs/toolkit@2.8.2': + resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} + 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 + '@remix-run/router@1.11.0': resolution: {integrity: sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==} engines: {node: '>=14.0.0'} @@ -4234,6 +4248,12 @@ packages: resolution: {integrity: sha512-yxLC57GBDmbDmrnH+vJxsrbV4/aYUucBONkSRLZyJIVFAl/QJH+O/h+phITHDaxVZCYZAcudYJw4ERE32BJM7g==} engines: {node: '>=14.0.0'} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@stripe/react-stripe-js@1.16.5': resolution: {integrity: sha512-lVPW3IfwdacyS22pP+nBB6/GNFRRhT/4jfgAK6T2guQmtzPwJV1DogiGGaBNhiKtSY18+yS8KlHSu+PvZNclvQ==} peerDependencies: @@ -8589,6 +8609,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + immer@8.0.1: resolution: {integrity: sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==} @@ -11681,9 +11704,17 @@ packages: peerDependencies: redux: ^4 + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -11825,6 +11856,9 @@ packages: reselect@4.1.8: resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -17891,6 +17925,18 @@ snapshots: '@redux-saga/types@1.2.1': {} + '@reduxjs/toolkit@2.8.2(react-redux@7.2.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2))(react@17.0.2)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 17.0.2 + react-redux: 7.2.9(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@remix-run/router@1.11.0': {} '@rollup/rollup-android-arm-eabi@4.46.2': @@ -18581,6 +18627,10 @@ snapshots: '@smithy/types': 2.10.0 tslib: 2.6.2 + '@standard-schema/spec@1.0.0': {} + + '@standard-schema/utils@0.3.0': {} + '@stripe/react-stripe-js@1.16.5(@stripe/stripe-js@1.54.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': dependencies: '@stripe/stripe-js': 1.54.2 @@ -24568,6 +24618,8 @@ snapshots: ignore@7.0.5: {} + immer@10.1.1: {} + immer@8.0.1: {} immutable@4.3.5: {} @@ -28257,10 +28309,16 @@ snapshots: dependencies: redux: 4.2.1 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + redux@4.2.1: dependencies: '@babel/runtime': 7.23.9 + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -28487,6 +28545,8 @@ snapshots: reselect@4.1.8: {} + reselect@5.1.1: {} + resize-observer-polyfill@1.5.1: {} resolve-alpn@1.2.1: {}