diff --git a/src/plugin/flashcard/Controller/FlashcardDeckController.php b/src/plugin/flashcard/Controller/FlashcardDeckController.php index 9a6b95973cc..51fbec9bc9b 100644 --- a/src/plugin/flashcard/Controller/FlashcardDeckController.php +++ b/src/plugin/flashcard/Controller/FlashcardDeckController.php @@ -93,7 +93,6 @@ public function updateProgressionAction(Flashcard $card, User $user, Request $re $node = $deck->getResourceNode(); $resourceUserEvaluation = $this->evaluationManager->getResourceUserEvaluation($node, $user); - // Mise à jour de la progression de la carte $this->evaluationManager->updateCardDrawnProgression($card, $user, $request->get('isSuccessful')); $attempt = $this->resourceEvalRepo->findOneInProgress($node, $user); diff --git a/src/plugin/flashcard/Entity/FlashcardDeck.php b/src/plugin/flashcard/Entity/FlashcardDeck.php index 0ec07112d11..f0d347a01e5 100644 --- a/src/plugin/flashcard/Entity/FlashcardDeck.php +++ b/src/plugin/flashcard/Entity/FlashcardDeck.php @@ -51,6 +51,11 @@ class FlashcardDeck extends AbstractResource */ private ?string $wrongButtonLabel = null; + /** + * @ORM\Column(type="boolean") + */ + private bool $showLeitnerRules = false; + public function __construct() { parent::__construct(); @@ -142,4 +147,14 @@ public function setWrongButtonLabel(?string $wrongButtonLabel): void { $this->wrongButtonLabel = $wrongButtonLabel; } + + public function getShowLeitnerRules(): bool + { + return $this->showLeitnerRules; + } + + public function setShowLeitnerRules(bool $showLeitnerRules): void + { + $this->showLeitnerRules = $showLeitnerRules; + } } diff --git a/src/plugin/flashcard/Installation/Migrations/Version20231127105116.php b/src/plugin/flashcard/Installation/Migrations/Version20231127105116.php new file mode 100644 index 00000000000..f6c86f8fa5e --- /dev/null +++ b/src/plugin/flashcard/Installation/Migrations/Version20231127105116.php @@ -0,0 +1,30 @@ +addSql(' + ALTER TABLE claro_flashcard_deck + ADD showLeitnerRules TINYINT(1) NOT NULL + '); + } + + public function down(Schema $schema): void + { + $this->addSql(' + ALTER TABLE claro_flashcard_deck + DROP showLeitnerRules + '); + } +} diff --git a/src/plugin/flashcard/Manager/EvaluationManager.php b/src/plugin/flashcard/Manager/EvaluationManager.php index cfd8afd1799..a9c43014fc0 100644 --- a/src/plugin/flashcard/Manager/EvaluationManager.php +++ b/src/plugin/flashcard/Manager/EvaluationManager.php @@ -82,7 +82,6 @@ public function update(ResourceEvaluation $attempt, $attemptCards): ResourceEval } } - // Pas de cartes restantes pour cette session, on passe a la session suivante if (0 === count($cardsArray)) { ++$session; } diff --git a/src/plugin/flashcard/Manager/FlashcardManager.php b/src/plugin/flashcard/Manager/FlashcardManager.php index 9a51ba6a44a..591ff67985b 100644 --- a/src/plugin/flashcard/Manager/FlashcardManager.php +++ b/src/plugin/flashcard/Manager/FlashcardManager.php @@ -101,7 +101,6 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de $cardsSessionIds = $attempt ? $attempt->getData()['cardsSessionIds'] ?? [] : []; $cardsAnsweredIds = $attempt ? $attempt->getData()['cardsAnsweredIds'] ?? [] : []; - // Récup des entités cartes de la tentative $cards = array_map(function ($card) { return $this->om->getRepository(CardDrawnProgression::class)->findOneBy([ 'id' => $card['id'], @@ -113,7 +112,6 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de ]); }, $cardsAnsweredIds); - // Création d'une nouvelle tentative si nécessaire if (!$attempt) { $attempt = $this->resourceEvalManager->createAttempt($node, $user, [ 'status' => AbstractEvaluation::STATUS_OPENED, @@ -127,7 +125,10 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de ]); } - // Récupération des cartes de la tentative + if (0 === count($deck->getCards())) { + return $attempt; + } + $attemptCards = self::shuffleCardByAttempt($deck->getCards(), $attempt, $deck->getDraw()); foreach ($attemptCards as $card) { $progression = $this->om->getRepository(CardDrawnProgression::class)->findOneBy([ @@ -149,7 +150,6 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de } } - // Filtrage des cartes pour la session $cardsSession = []; foreach ($cards as $cardProgression) { if ($this->keepCardInSession($session, $cardProgression)) { @@ -157,7 +157,6 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de } } - // Ajout des cartes de la session 1 if (0 === count($cardsSession) && 1 === $session) { $attempt = $this->resourceEvalManager->updateAttempt($attempt, [ 'status' => AbstractEvaluation::STATUS_OPENED, @@ -177,7 +176,6 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de ]); } - // Suppression des cartes répondues de la liste des cartes de la session foreach ($cardsAnswered as $cardProgression) { foreach ($cardsSession as $key => $cardSession) { if ($cardProgression->getId() === $cardSession->getId()) { @@ -187,14 +185,10 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de } } - // Incrément de la session et réinitialisation des cartes si nécessaire while (0 === count($cardsSession) && $session < 7) { ++$session; - $cardsSession = []; $cardsAnswered = []; - - // Filtrage des cartes pour la nouvelle session foreach ($cards as $cardProgression) { if ($this->keepCardInSession($session, $cardProgression)) { $cardsSession[] = $cardProgression; @@ -202,7 +196,6 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de } } - // Calcul de la progression $successfulCards = 0; $totalCards = count($cardsSession) + count($cardsAnswered); foreach ($cardsAnswered as $cardProgression) { @@ -211,11 +204,9 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de if (0 === $totalCards) { $progression = 0; } else { - // Transformation du résultat en integer car round() retourne un float $progression = (int) min(round($successfulCards / $totalCards * 100), 100); } - // Vérification du status if (7 === $session && 0 === count($cardsSession)) { if (100 === $progression) { $status = AbstractEvaluation::STATUS_COMPLETED; @@ -226,7 +217,6 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de $status = AbstractEvaluation::STATUS_INCOMPLETE; } - // Mise à jour de la tentative $attempt = $this->resourceEvalManager->updateAttempt($attempt, [ 'status' => $status, 'progression' => $progression, @@ -244,9 +234,7 @@ public function calculateSession(?ResourceEvaluation $attempt, FlashcardDeck $de ], ]); - // Si on n'a pas de page de début ni de fin if (!$deck->getShowOverview() && !$deck->getShowEndPage()) { - // On recrée une nouvelle tentative directement une fois la précédente terminée pour enchainer if (AbstractEvaluation::STATUS_COMPLETED === $status || AbstractEvaluation::STATUS_FAILED === $status) { $attempt = $this->calculateSession(null, $deck, $user); } @@ -261,7 +249,6 @@ public function answerSessionCard(ResourceEvaluation $attempt, CardDrawnProgress $cardsSessionIds = $attempt->getData()['cardsSessionIds'] ?? []; $cardsAnsweredIds = $attempt->getData()['cardsAnsweredIds'] ?? []; - // Deserialization des cartes de la tentative $cards = array_map(function ($card) { return $this->om->getRepository(CardDrawnProgression::class)->findOneBy([ 'id' => $card['id'], @@ -272,7 +259,6 @@ public function answerSessionCard(ResourceEvaluation $attempt, CardDrawnProgress $cardsAnsweredIds[] = $cardProgression->getId(); } - // Mise à jour de la tentative return $this->resourceEvalManager->updateAttempt($attempt, [ 'status' => $attempt->getStatus(), 'progression' => $attempt->getProgression(), diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/card.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/card.jsx index 32fa30225ee..e79fd47cd63 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/card.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/card.jsx @@ -81,7 +81,7 @@ Card.propTypes = { actions: T.func, card: T.shape( CardTypes.propTypes - ).isRequired + ) } export { diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/info.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/info.jsx deleted file mode 100644 index 9f5956fd961..00000000000 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/info.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' - -import {trans} from '#/main/app/intl/translation' -import {ContentCounter} from '#/main/app/content/components/counter' -import {schemeCategory20c} from '#/main/theme/color/utils' - -const FlashcardInfo = (props) => { - const getLength = (items) => items ? items.length : 0 - const countSuccess = (items) => items ? items.filter((item) => item && item.successCount > 0).length : 0 - const countFail = (items) => items ? items.filter((item) => item && item.successCount === 0).length : 0 - - return ( -
- - - - - -
- ) -} - -export { - FlashcardInfo -} diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/menu.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/menu.jsx index 2891f871e02..5ba998d8397 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/menu.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/menu.jsx @@ -11,7 +11,6 @@ const FlashcardMenu = props => /> FlashcardMenu.propTypes = { - // from menu opened: T.bool.isRequired, toggle: T.func.isRequired, autoClose: T.func.isRequired diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/overview.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/overview.jsx index dfcae753554..8e5a4ebf07d 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/overview.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/overview.jsx @@ -6,7 +6,9 @@ import {trans} from '#/main/app/intl/translation' import {LINK_BUTTON} from '#/main/app/buttons' import {ResourceOverview} from '#/main/core/resource/components/overview' import {ResourceEvaluation as ResourceEvaluationTypes} from '#/main/evaluation/resource/prop-types' -import {FlashcardInfo} from '#/plugin/flashcard/resources/flashcard/components/info' + +import {Timeline} from '#/plugin/flashcard/resources/flashcard/components/timeline' +import {LeitnerRules} from '#/plugin/flashcard/resources/flashcard/components/rules' import {FlashcardDeck as FlashcardDeckTypes} from '#/plugin/flashcard/resources/flashcard/prop-types' const Overview = (props) => { @@ -57,9 +59,19 @@ const Overview = (props) => { [trans('session_status', {}, 'flashcard'), statusText] ]} > - + + {props.flashcardDeck.showLeitnerRules && + + } ) } diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/rules.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/rules.jsx new file mode 100644 index 00000000000..ae5ed5980c1 --- /dev/null +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/rules.jsx @@ -0,0 +1,57 @@ +import React from 'react' +import classes from 'classnames' +import {PropTypes as T} from 'prop-types' + +import {trans} from '#/main/app/intl' +import {getIcon, getRule} from '#/plugin/flashcard/resources/flashcard/utils' + +const SessionRule= props => { + const icon = getIcon(props.index) + const rule = getRule(props.index) + + let classList = ['icon-with-text-right'] + if (props.index < props.session || (props.index === props.session && props.completed)) { + classList.push('session-rule-done') + } + + return ( +
  • +
    + {icon} +
    +
    + Session {props.index} : {trans('session_cards_start', {}, 'flashcard') + ' ' + rule}. +
    +
  • + ) +} + +SessionRule.propTypes = { + session: T.number, + rule: T.number, + completed: T.bool, + index: T.number +} + +const LeitnerRules = (props) => ( + +) + +LeitnerRules.propTypes = { + session: T.number, + completed: T.bool +} + +export { + LeitnerRules +} diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/components/timeline.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/timeline.jsx new file mode 100644 index 00000000000..961b66c7493 --- /dev/null +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/components/timeline.jsx @@ -0,0 +1,70 @@ +import React from 'react' +import classes from 'classnames' +import {PropTypes as T} from 'prop-types' + +import {ProgressBar} from '#/main/app/content/components/progress-bar' +import {TooltipOverlay} from '#/main/app/overlays/tooltip/components/overlay' +import {getRule, getLabel, getClassList, getProgression} from '#/plugin/flashcard/resources/flashcard/utils' + +const SessionStep = props => { + const rule = getRule(props.index) + const label = getLabel(props.index, props.session, props.started, props.completed) + const classList = getClassList(props.index, props.session, props.started, props.completed) + + return ( +
  • + +
    + {label} +
    +
    +
  • + ) +} + +SessionStep.propTypes = { + session: T.number, + index: T.number, + started: T.bool, + completed: T.bool +} + +const Timeline = (props) => { + const progression = getProgression(props.session, props.started, props.completed, props.end) + + return ( +
    + +
      + {Array.from({ length: 7 }, (_, index) => ( + + ))} +
    +
    + ) +} + +Timeline.propTypes = { + session: T.number, + started: T.bool, + completed: T.bool, + end: T.bool +} + +export { + Timeline +} diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/containers/overview.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/containers/overview.jsx index ec765efc0ed..e1695e17c86 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/containers/overview.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/containers/overview.jsx @@ -2,8 +2,8 @@ import {connect} from 'react-redux' import {selectors as resourceSelectors} from '#/main/core/resource/store/selectors' -import {Overview as OverviewComponent} from '#/plugin/flashcard/resources/flashcard/components/overview' import {selectors} from '#/plugin/flashcard/resources/flashcard/store' +import {Overview as OverviewComponent} from '#/plugin/flashcard/resources/flashcard/components/overview' const Overview = connect( (state) => ({ diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/containers/resource.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/containers/resource.jsx index 1192eddfa6f..a75018f6107 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/containers/resource.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/containers/resource.jsx @@ -1,12 +1,11 @@ import {connect} from 'react-redux' import {withRouter} from '#/main/app/router' - -import {selectors as resourceSelectors} from '#/main/core/resource/store' import {hasPermission} from '#/main/app/security/permissions' +import {selectors as resourceSelectors} from '#/main/core/resource/store' -import {FlashcardResource as FlashcardResourceComponent} from '#/plugin/flashcard/resources/flashcard/components/resource' import {actions, selectors} from '#/plugin/flashcard/resources/flashcard/store' +import {FlashcardResource as FlashcardResourceComponent} from '#/plugin/flashcard/resources/flashcard/components/resource' const FlashcardResource = withRouter( connect( diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/editor/components/editor.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/editor/components/editor.jsx index dba7afc8726..b4c590989b8 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/editor/components/editor.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/editor/components/editor.jsx @@ -81,6 +81,10 @@ const EditorComponent = props => displayed: (flashcardDeck) => get(flashcardDeck, 'customButtons') } ] + }, { + name: 'showLeitnerRules', + type: 'boolean', + label: trans('show_leitner_rules', {}, 'flashcard') } ] },{ diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/index.js b/src/plugin/flashcard/Resources/modules/resources/flashcard/index.js index fc0e9c95ccd..7b6e6a77514 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/index.js +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/index.js @@ -1,6 +1,6 @@ -import {FlashcardResource} from '#/plugin/flashcard/resources/flashcard/containers/resource' import {reducer} from '#/plugin/flashcard/resources/flashcard/store' import {FlashcardMenu} from '#/plugin/flashcard/resources/flashcard/components/menu' +import {FlashcardResource} from '#/plugin/flashcard/resources/flashcard/containers/resource' /** * Flashcard resource application. diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/player/components/end.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/player/components/end.jsx index 463e925e2f9..69e1b251710 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/player/components/end.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/player/components/end.jsx @@ -1,20 +1,21 @@ import React from 'react' +import get from 'lodash/get' import {connect} from 'react-redux' import {PropTypes as T} from 'prop-types' -import get from 'lodash/get' import {trans} from '#/main/app/intl' import {LINK_BUTTON} from '#/main/app/buttons' import {ResourceEnd} from '#/main/core/resource/components/end' import {selectors} from '#/plugin/flashcard/resources/flashcard/store' -import {FlashcardDeck as FlashcardDeckTypes} from '#/plugin/flashcard/resources/flashcard/prop-types' -import {FlashcardInfo} from '#/plugin/flashcard/resources/flashcard/components/info' +import {Timeline} from '#/plugin/flashcard/resources/flashcard/components/timeline' import {ResourceEvaluation as ResourceEvaluationTypes} from '#/main/evaluation/resource/prop-types' +import {FlashcardDeck as FlashcardDeckTypes} from '#/plugin/flashcard/resources/flashcard/prop-types' const PlayerEndComponent = (props) => { const attemptData = props.attempt && props.attempt.data let session = attemptData ? attemptData.session : 1 + let action = action = { type: LINK_BUTTON, label: trans('start', {}, 'actions'), @@ -39,8 +40,10 @@ const PlayerEndComponent = (props) => { [trans('session_status', {}, 'flashcard'), trans('session_status_completed', {}, 'flashcard')] ]} > - ) diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/player/components/player.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/player/components/player.jsx index 0b51c357d65..f42341b0982 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/player/components/player.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/player/components/player.jsx @@ -2,11 +2,11 @@ import React, { useState, useCallback } from 'react' import { useHistory, useRouteMatch } from 'react-router-dom' import { PropTypes as T } from 'prop-types' -import { trans } from '#/main/app/intl/translation' import {Toolbar} from '#/main/app/action' import {CALLBACK_BUTTON} from '#/main/app/buttons' -import {ContentPlaceholder} from '#/main/app/content/components/placeholder' +import { trans } from '#/main/app/intl/translation' import {ProgressBar} from '#/main/app/content/components/progress-bar' +import {ContentPlaceholder} from '#/main/app/content/components/placeholder' import {Card} from '#/plugin/flashcard/resources/flashcard/components/card' import {FlashcardDeck as FlashcardDeckTypes} from '#/plugin/flashcard/resources/flashcard/prop-types' @@ -17,7 +17,7 @@ const Player = props => { const [isFlipped, setIsFlipped] = useState(false) const [isAnswering, setIsAnswering] = useState(false) const currentCardIndex = props.attempt.data.cardsAnsweredIds.length - const maxCards = props.draw > 0 ? Math.min(props.draw, props.attempt.data.cards.length) : props.attempt.data.cards.length + const maxCards = props.attempt.data.cardsSessionIds.length + props.attempt.data.cardsAnsweredIds.length const currentCardId = props.attempt.data.cardsSessionIds[0] const currentCardProgression = props.attempt.data.cards.find(card => card.id === currentCardId) const currentCard = currentCardProgression ? currentCardProgression.flashcard : null diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/player/containers/player.jsx b/src/plugin/flashcard/Resources/modules/resources/flashcard/player/containers/player.jsx index f3fcece1757..1ad0b185f48 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/player/containers/player.jsx +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/player/containers/player.jsx @@ -1,7 +1,7 @@ import {connect} from 'react-redux' -import {Player as PlayerComponent} from '#/plugin/flashcard/resources/flashcard/player/components/player' import {selectors, actions} from '#/plugin/flashcard/resources/flashcard/store' +import {Player as PlayerComponent} from '#/plugin/flashcard/resources/flashcard/player/components/player' const Player = connect( (state) => ({ diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/prop-types.js b/src/plugin/flashcard/Resources/modules/resources/flashcard/prop-types.js index c56e0862f4e..f04523561b6 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/prop-types.js +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/prop-types.js @@ -23,6 +23,10 @@ const FlashcardDeck = { id: T.string, name: T.string, showProgression: T.bool, + showLeitnerRules: T.bool, + customButtons: T.bool, + rightButtonLabel: T.string, + wrongButtonLabel: T.string, draw: T.number, overview: T.shape({ display: T.bool, diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/store/actions.js b/src/plugin/flashcard/Resources/modules/resources/flashcard/store/actions.js index 648cfd9d5ac..8a407e387c2 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/store/actions.js +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/store/actions.js @@ -1,5 +1,5 @@ -import {makeActionCreator} from '#/main/app/store/actions' import { API_REQUEST } from '#/main/app/api' +import {makeActionCreator} from '#/main/app/store/actions' import {actions as resourceActions} from '#/main/core/resource/store' @@ -13,6 +13,7 @@ actions.updateCardProgression = makeActionCreator(FLASHCARD_UPDATE_PROGRESSION, actions.updateProgression = (cardId, isSuccessful) => ({ [API_REQUEST]: { + silent: true, url: ['apiv2_flashcard_progression_update', {id: cardId, isSuccessful: isSuccessful}], request: { method: 'PUT' diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/store/index.js b/src/plugin/flashcard/Resources/modules/resources/flashcard/store/index.js index fe7bc0ecda0..e49b5a04283 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/store/index.js +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/store/index.js @@ -1,9 +1,9 @@ import {reducer} from '#/plugin/flashcard/resources/flashcard/store/reducer' -import {selectors} from '#/plugin/flashcard/resources/flashcard/store/selectors' import {actions} from '#/plugin/flashcard/resources/flashcard/store/actions' +import {selectors} from '#/plugin/flashcard/resources/flashcard/store/selectors' export { reducer, - selectors, - actions + actions, + selectors } diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/store/reducer.js b/src/plugin/flashcard/Resources/modules/resources/flashcard/store/reducer.js index cf3a50e503c..f8f3b7ae44e 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/store/reducer.js +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/store/reducer.js @@ -3,8 +3,8 @@ import cloneDeep from 'lodash/cloneDeep' import {makeInstanceAction} from '#/main/app/store/actions' import {combineReducers, makeReducer} from '#/main/app/store/reducer' -import {FORM_SUBMIT_SUCCESS} from '#/main/app/content/form/store/actions' import {RESOURCE_LOAD} from '#/main/core/resource/store/actions' +import {FORM_SUBMIT_SUCCESS} from '#/main/app/content/form/store/actions' import {ATTEMPT_LOAD} from '#/plugin/flashcard/resources/flashcard/store/actions' import {selectors as editorSelectors, reducer as editorReducer} from '#/plugin/flashcard/resources/flashcard/editor/store' diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/store/selectors.js b/src/plugin/flashcard/Resources/modules/resources/flashcard/store/selectors.js index 6178c5ea38a..6cf9fc62fbb 100644 --- a/src/plugin/flashcard/Resources/modules/resources/flashcard/store/selectors.js +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/store/selectors.js @@ -1,5 +1,5 @@ -import {createSelector} from 'reselect' import get from 'lodash/get' +import {createSelector} from 'reselect' const STORE_NAME = 'flashcard' diff --git a/src/plugin/flashcard/Resources/modules/resources/flashcard/utils.js b/src/plugin/flashcard/Resources/modules/resources/flashcard/utils.js new file mode 100644 index 00000000000..80518bd17e4 --- /dev/null +++ b/src/plugin/flashcard/Resources/modules/resources/flashcard/utils.js @@ -0,0 +1,51 @@ +import React from 'react' +import { trans } from '#/main/app/intl' + +export const getIcon = (index) => { + if (index > 1 && index < 7) { + return + } else if (index === 7) { + return + } + return +} + +export const getRule = (index) => { + switch (index) { + case 1: return trans('session_cards_all', {}, 'flashcard') + case 2: + case 4: return trans('session_cards_1x', {}, 'flashcard') + ' + ' + trans('session_cards_failed', {}, 'flashcard') + case 3: return trans('session_cards_2x', {}, 'flashcard') + ' + ' + trans('session_cards_failed', {}, 'flashcard') + case 5: return trans('session_cards_failed', {}, 'flashcard') + case 6: return trans('session_cards_1x', {}, 'flashcard') + ' + ' + trans('session_cards_2x', {}, 'flashcard') + ' + ' + trans('session_cards_failed', {}, 'flashcard') + case 7: return trans('session_cards_3x', {}, 'flashcard') + ' + ' + trans('session_cards_failed', {}, 'flashcard') + default: return index + } +} + +export const getClassList = (index, session, started, completed) => { + return [ + 'flashcard-timeline-step', + index < session ? 'flashcard-timeline-step-done' : '', + session === index && completed ? 'flashcard-timeline-step-done' : '', + session === index && started && !completed ? 'flashcard-timeline-step-current' : '' + ] +} + +export const getLabel = (index, session, started, completed) => { + if (index < session || (session === index && completed)) { + return + } else if (session === index && started) { + return + } + return index +} + +export const getProgression = (session, started, completed, end) => { + if (end) { + return (session - 2) * (100 / 6) + } else if (started || completed) { + return (session - 1) * (100 / 6) + } + return (session - 1.5) * (100 / 6) +} diff --git a/src/plugin/flashcard/Resources/styles/_variables.scss b/src/plugin/flashcard/Resources/styles/_variables.scss index 30406af1824..613e47e0586 100644 --- a/src/plugin/flashcard/Resources/styles/_variables.scss +++ b/src/plugin/flashcard/Resources/styles/_variables.scss @@ -17,3 +17,8 @@ $flashcard-box-shadow: $box-shadow-sm !default; $flashcard-height: 300px !default; $flashcard-color: $card-color !default; $flashcard-bg: $card-bg !default; + +$flashcard-timeline-icon-size: $font-size-lg*3; +$flashcard-timeline-lines-width: 8px !default; +$flashcard-timeline-lines-color: $gray-lighter !default; +$flashcard-timeline-endpoint-size: $font-size-lg*2 !default; diff --git a/src/plugin/flashcard/Resources/styles/resources/flashcard.scss b/src/plugin/flashcard/Resources/styles/resources/flashcard.scss index 7ce4a86a615..da5ffcdae7c 100644 --- a/src/plugin/flashcard/Resources/styles/resources/flashcard.scss +++ b/src/plugin/flashcard/Resources/styles/resources/flashcard.scss @@ -12,7 +12,7 @@ justify-content: center; list-style: none; padding: 0; - margin: -(map-get($spacers, 1)); // absorb items padding + margin: -(map-get($spacers, 1)); > li { padding: map-get($spacers, 1); @@ -21,10 +21,6 @@ } .flashcard-hoverable { - //width: calc(50% - 30px); - - //padding: 5px; - //position: relative; transition: all linear 185ms; &:hover { @@ -34,22 +30,16 @@ } } -/*.flashcards-info { - display: flex; -}*/ - .flashcard-actions { position: absolute; top: 0; right: 0; - //margin: 2px; } .flashcard-card { display: flex; flex-direction: column; height: $flashcard-height; - //height: 100%; width: 100%; position: relative; @@ -68,14 +58,6 @@ align-items: center } -.flashcard-player { - /*display: flex; - align-items: center; - justify-content: center; - - position: relative;*/ -} - .flashcard-counter { display: flex; justify-content: space-between; @@ -84,28 +66,13 @@ .flashcard-deck { position: relative; - //max-width: 500px; - //width: 80vw; height: 300px; perspective: 1000px; - //background-color: transparent; -} - -.flashcard-buttons { - /*display: flex; - justify-content: center; - margin: auto;*/ - - /*& > button { - margin-left: 20px; - }*/ } .flashcard-media { max-width: 100%; max-height: 100%; - - //@include border-radius($flashcard-inner-border-radius); } .flashcard-video { @@ -116,45 +83,25 @@ .flashcard-question { color: $flashcard-title-color; margin-bottom: $flashcard-title-spacer-y; - //font-weight: bold; text-align: center; } .flashcard-content { overflow-y: hidden; - //height: 100%; flex: 1; display: flex; flex-direction: column; align-items: center; - - /*audio, video { - display: block; - margin: auto; - }*/ } .flashcard { position: absolute; - //max-width: 500px; - //width: 80vw; width: 100%; height: 100%; - //margin: 10px; transition: transform 500ms; transform-style: preserve-3d; - /*display: flex; - flex-wrap: nowrap; - align-items: center; - justify-content: center; - flex-direction: column;*/ overflow: hidden; - /*.flashcard-video { - width: 100%; - border-radius: $flashcard-inner-border-radius; - }*/ - &-preview { position: static; @@ -176,3 +123,131 @@ transform: rotateY(180deg) scaleX(-1); } } + +.flashcard-timeline { + margin: 40px 33px; + + @media (max-width: 768px) { + position: relative; + height: 350px; + + .progress { + position: absolute; + left: 50%; right: 50%; + transform: rotate(90deg); + transform-origin: left; + width: 350px; + margin: auto; + } + } +} + +.flashcard-timeline-steps { + margin-left: -10px; + margin-right: -10px; + margin-top: -5px; + display: flex; + justify-content: space-between; + list-style: none; + padding: 0; + + @media (max-width: 768px) { + height: 102%; + flex-direction: column; + flex-wrap: wrap; + } +} + +.flashcard-timeline-step { + position: relative; +} + +.flashcard-timeline-step::before { + content: ''; + width: 50px; + height: 50px; + background-color: $flashcard-timeline-lines-color; + display: block; + border-radius: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.flashcard-timeline-step-done { + color: white; +} + +.flashcard-timeline-step-done::before { + background-color: $learning; +} + +.flashcard-timeline-step-current::before { + background-color: $primary; +} + +.flashcard-timeline-heading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 1.5rem; + font-weight: bold; +} + +.flashcard-timeline-status { + position: absolute; + top: 40px; + left: 50%; + transform: translateX(-50%); + background-color: #ddd; + width: 200px; + padding: 5px 10px; + border-radius: 5px; + text-align: center; +} + +.flashcard-timeline-status::before { + content: ''; + width: 10px; + height: 10px; + background-color: #ddd; + display: block; + position: absolute; + top: -8px; + margin-left: -2px; + left: 50%; + transform: rotate(-45deg) translateX(-50%); +} + +.sessions-rules { + list-style-type: none; + padding: 0; + margin-bottom: $component-margin-bottom; +} + +.session-rule { + display: flex; + background: #e9ecef; + padding: 10px $panel-body-padding; + border: 1px solid transparent; + border-radius: $border-radius-sm; + + + .session-rule { + margin-top: 5px; + } + + .session-rule-done { + color: $learning; + } + + div { + align-self: center; + } +} + +@media (max-width: 768px) { + .flashcard-timeline { + } +} diff --git a/src/plugin/flashcard/Resources/translations/flashcard.en.json b/src/plugin/flashcard/Resources/translations/flashcard.en.json index 386d09a009a..bf203cd443b 100644 --- a/src/plugin/flashcard/Resources/translations/flashcard.en.json +++ b/src/plugin/flashcard/Resources/translations/flashcard.en.json @@ -6,6 +6,7 @@ "custom_button_labels": "Custom button labels", "right_button_label": "Right button label", "wrong_button_label": "Wrong button label", + "show_leitner_rules": "Show session rules", "flashcard_options": "Cards options", "draw_options": "Number of cards to draw", "options_desc":"Defines number of cards to draw for each game. Leave empty to play with all the cards.", @@ -37,8 +38,12 @@ "session_status_completed": "Completed", "session_status_next": "Next", - "check": "successful", - "fail": "failed", + "session_cards_start" : "We present", + "session_cards_all" : "all cards", + "session_cards_1x" : "cards successful once", + "session_cards_2x" : "cards successful twice", + "session_cards_3x" : "cards successful three times", + "session_cards_failed" : "cards failed", "start_disabled_empty": "Flashcard resource is empty." } diff --git a/src/plugin/flashcard/Resources/translations/flashcard.fr.json b/src/plugin/flashcard/Resources/translations/flashcard.fr.json index 77a2ccd7c37..258253089ab 100644 --- a/src/plugin/flashcard/Resources/translations/flashcard.fr.json +++ b/src/plugin/flashcard/Resources/translations/flashcard.fr.json @@ -6,6 +6,7 @@ "custom_button_labels": "Personnaliser le label des boutons de réponse", "right_button_label": "Label du bouton de réponse correct", "wrong_button_label": "Label du bouton de réponse incorrect", + "show_leitner_rules": "Afficher les règles des sessions", "flashcard_options": "Options des cartes", "draw_options": "Nombre de cartes à tirer", "options_desc": "Permet de définir le nombre de cartes à tirer pour chaque partie. Laissez vide pour jouer avec toutes les cartes.", @@ -37,8 +38,12 @@ "session_status_completed": "Terminée", "session_status_next": "À venir", - "check": "réussies", - "fail": "échouées", + "session_cards_start": "On présente", + "session_cards_all" : "toutes les cartes", + "session_cards_1x" : "les cartes réussies 1 fois", + "session_cards_2x" : "les cartes réussies 2 fois", + "session_cards_3x" : "les cartes réussies 3 fois", + "session_cards_failed" : "les cartes échouées", "start_disabled_empty": "La resource cartes-mémoire est vide." } diff --git a/src/plugin/flashcard/Serializer/FlashcardDeckSerializer.php b/src/plugin/flashcard/Serializer/FlashcardDeckSerializer.php index 1c60b4f52a4..5bb7950ca71 100644 --- a/src/plugin/flashcard/Serializer/FlashcardDeckSerializer.php +++ b/src/plugin/flashcard/Serializer/FlashcardDeckSerializer.php @@ -43,6 +43,7 @@ public function serialize(FlashcardDeck $flashcardDeck): array 'rightButtonLabel' => $flashcardDeck->getRightButtonLabel(), 'wrongButtonLabel' => $flashcardDeck->getWrongButtonLabel(), 'draw' => $flashcardDeck->getDraw(), + 'showLeitnerRules' => $flashcardDeck->getShowLeitnerRules(), 'overview' => [ 'display' => $flashcardDeck->getShowOverview(), 'message' => $flashcardDeck->getOverviewMessage(), @@ -89,6 +90,7 @@ public function deserialize(array $data, FlashcardDeck $flashcardDeck): Flashcar $this->sipe('rightButtonLabel', 'setRightButtonLabel', $data, $flashcardDeck); $this->sipe('wrongButtonLabel', 'setWrongButtonLabel', $data, $flashcardDeck); $this->sipe('draw', 'setDraw', $data, $flashcardDeck); + $this->sipe('showLeitnerRules', 'setShowLeitnerRules', $data, $flashcardDeck); if (!empty($data['overview'])) { $this->sipe('overview.display', 'setShowOverview', $data, $flashcardDeck);