diff --git a/src/Components/PackageOverview/PackageOverview.jsx b/src/Components/PackageOverview/PackageOverview.jsx index 972e8ea0..452e6b2f 100644 --- a/src/Components/PackageOverview/PackageOverview.jsx +++ b/src/Components/PackageOverview/PackageOverview.jsx @@ -24,11 +24,18 @@ const PackageOverview = ({ data }) => { translatedWordLanguage: data.translatedWordLanguage, //using fixed value until server gives us this property languagePackageId: data.id, + groupIds: [], vocabsToday: data.stats.vocabularies.learnedToday.dueToday, staged, + onlyActivated: !staged, + customLearning: false, }) ); - history.push("/learn/direction/"); + if (staged) { + history.push("/learn/selection/staged/"); + } else { + history.push("/learn/direction/"); + } }, [ data.foreignWordLanguage, diff --git a/src/i18n/locales/en/default.json b/src/i18n/locales/en/default.json index 96f7f42b..639e8c08 100644 --- a/src/i18n/locales/en/default.json +++ b/src/i18n/locales/en/default.json @@ -31,6 +31,7 @@ "serverNotResponding": "The server is not responding", "successMessage": "Success", "fetchError": "Something went wrong, please try again later", + "internalServerError": "Internal Server Error", "paginationPage": "Page {{page}} of {{pageLength}}", "paginationShow": "Show {{size}}", "learn": "Learn", @@ -104,6 +105,9 @@ "unactivated": "Not activated:", "today": "Today:" }, + "groupSelection": { + "startActivating": "Start activating" + }, "validServerIndicator": { "validServer": "✓ valid server (v{{version}})", "serverNotVocascanServer": "✗ server isn't a vocascan server", @@ -363,10 +367,11 @@ "title": "Progress" }, "custom": { - "title": "Custom learning" + "title": "Custom learning", + "onlyActivatedVocabs": "Only activated vocabs" }, "about": { - "title": "About vocascan" + "title": "About Vocascan" } }, "nav": { diff --git a/src/redux/Actions/index.js b/src/redux/Actions/index.js index da796de8..66a8dd47 100644 --- a/src/redux/Actions/index.js +++ b/src/redux/Actions/index.js @@ -23,3 +23,4 @@ export const SET_QUERY_CORRECT = "SET_QUERY_CORRECT"; export const SET_QUERY_WRONG = "SET_QUERY_WRONG"; export const SET_ACTUAL_PROGRESS = "SET_ACTUAL_PROGRESS"; export const CLEAR_QUERY = "CLEAR_QUERY"; +export const SET_GROUP_IDS = "SET_GROUP_IDS"; diff --git a/src/redux/Actions/query.js b/src/redux/Actions/query.js index aeba9f0c..4019a264 100644 --- a/src/redux/Actions/query.js +++ b/src/redux/Actions/query.js @@ -4,14 +4,18 @@ import { SET_QUERY_WRONG, CLEAR_QUERY, SET_ACTUAL_PROGRESS, + SET_GROUP_IDS, } from "./index.js"; export const setLearnedPackage = ({ foreignWordLanguage, translatedWordLanguage, languagePackageId, + groupIds, vocabsToday, staged, + onlyActivated, + customLearning, }) => { return { type: SET_LEARNED_PACKAGE, @@ -19,8 +23,11 @@ export const setLearnedPackage = ({ foreignWordLanguage, translatedWordLanguage, languagePackageId, + groupIds, vocabsToday, staged, + onlyActivated, + customLearning, }, }; }; @@ -52,3 +59,12 @@ export const clearQuery = () => { payload: {}, }; }; + +export const setGroupIds = ({ groupIds }) => { + return { + type: SET_GROUP_IDS, + payload: { + groupIds, + }, + }; +}; diff --git a/src/redux/Reducers/query.js b/src/redux/Reducers/query.js index 6dd6fb65..946ca114 100644 --- a/src/redux/Reducers/query.js +++ b/src/redux/Reducers/query.js @@ -3,6 +3,7 @@ import { SET_QUERY_CORRECT, SET_QUERY_WRONG, CLEAR_QUERY, + SET_GROUP_IDS, SET_ACTUAL_PROGRESS, } from "../Actions/index.js"; @@ -12,6 +13,9 @@ const initialState = { languagePackageId: "", vocabsToday: 0, staged: false, + onlyActivated: false, + customLearning: false, + groupIds: [], correct: 0, wrong: 0, actualProgress: 0, @@ -25,8 +29,11 @@ const queryReducer = (state = initialState, action) => { foreignWordLanguage: action.payload.foreignWordLanguage, translatedWordLanguage: action.payload.translatedWordLanguage, languagePackageId: action.payload.languagePackageId, + groupIds: action.payload.groupIds, vocabsToday: action.payload.vocabsToday, staged: action.payload.staged, + onlyActivated: action.payload.onlyActivated, + customLearning: action.payload.customLearning, }; case SET_QUERY_CORRECT: @@ -51,6 +58,12 @@ const queryReducer = (state = initialState, action) => { ...initialState, }; + case SET_GROUP_IDS: + return { + ...state, + groupIds: action.payload.groupIds, + }; + default: return state; } diff --git a/src/screens/Custom/Custom.jsx b/src/screens/Custom/Custom.jsx index dd1016d1..f035dd58 100644 --- a/src/screens/Custom/Custom.jsx +++ b/src/screens/Custom/Custom.jsx @@ -1,16 +1,124 @@ -import React from "react"; +import React, { useEffect, useState, useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { useHistory } from "react-router-dom"; + +import Switch from "../../Components/Form/Switch/Switch.jsx"; +import Table from "../../Components/Table/Table.jsx"; +import Tooltip from "../../Components/Tooltip/Tooltip.jsx"; + +import { clearQuery } from "../../redux/Actions/query.js"; +import { setLearnedPackage } from "../../redux/Actions/query.js"; import "./Custom.scss"; +import Button from "../../Components/Button/Button"; +import { getPackages } from "../../utils/api"; + const Custom = () => { const { t } = useTranslation(); + const history = useHistory(); + const dispatch = useDispatch(); + + const [onlyActivated, setOnlyActivated] = useState(false); + const [packages, setPackages] = useState([]); + + useEffect(() => { + getPackages(false, false, onlyActivated).then((response) => { + setPackages(response.data); + }); + }, [onlyActivated]); + + const selectPackage = useCallback( + (languagePackage) => { + dispatch(clearQuery()); + dispatch( + setLearnedPackage({ + foreignWordLanguage: languagePackage.foreignWordLanguage, + translatedWordLanguage: languagePackage.translatedWordLanguage, + //using fixed value until server gives us this property + languagePackageId: languagePackage.id, + groupIds: [], + vocabsToday: 0, + staged: false, + onlyActivated: onlyActivated, + customLearning: true, + }) + ); + + history.push("/learn/selection/custom/"); + }, + [dispatch, history, onlyActivated] + ); + + const onChangeOnlyActivated = useCallback(() => { + setOnlyActivated(!onlyActivated); + + // refetch packages + getPackages(false, false, onlyActivated).then((response) => { + console.log(response.data); + setPackages(response.data); + }); + }, [onlyActivated]); + + const columns = useMemo( + () => [ + { + Header: t("screens.allGroups.groupName"), + accessor: "name", + Cell: ({ row }) => row.original.name, + headerClassName: "w-25", + }, + { + Header: t("screens.allGroups.groupDescription"), + accessor: "description", + Cell: ({ row }) => ( + <> +

+ {row.original.description} +

+ + + ), + headerClassName: "w-50", + }, + { + Header: "", + accessor: "select", + Cell: ({ row }) => ( + + ), + }, + ], + [selectPackage, t] + ); return ( -
-
-

{t("screens.custom.title")}

-

{t("global.comingSoon")}

+
+
+
+ +
+
+ + ); diff --git a/src/screens/Custom/Custom.scss b/src/screens/Custom/Custom.scss index ec999b4c..82ee3aac 100644 --- a/src/screens/Custom/Custom.scss +++ b/src/screens/Custom/Custom.scss @@ -1,15 +1,30 @@ @import "../../constants"; -.custom { - display: flex; +.custom-learning { grid-area: main; - align-items: center; - justify-content: center; - width: 100%; - height: 100vh; - text-align: center; - - @media screen and (min-width: $bp-md) { - height: 100%; + + .custom-learning-wrapper { + padding: 50px 12px; + + @media screen and (min-width: $bp-md) { + padding: 50px; + } + + .custom-learning-switch-wrapper { + width: 100%; + + @media screen and (min-width: $bp-md) { + display: flex; + justify-content: flex-end; + } + } + + .table-wrapper { + overflow: scroll; + + @media screen and (min-width: $bp-md) { + overflow: hidden; + } + } } } diff --git a/src/screens/Learn/GroupSelection/GroupSelection.jsx b/src/screens/Learn/GroupSelection/GroupSelection.jsx new file mode 100644 index 00000000..979e66d0 --- /dev/null +++ b/src/screens/Learn/GroupSelection/GroupSelection.jsx @@ -0,0 +1,146 @@ +import React, { useEffect, useState, useMemo, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector, useDispatch } from "react-redux"; +import { useHistory } from "react-router-dom"; + +import CheckCircleIcon from "@material-ui/icons/CheckCircle"; +import RemoveCircleIcon from "@material-ui/icons/RemoveCircle"; + +import "./GroupSelection.scss"; + +import Button from "../../../Components/Button/Button"; +import Switch from "../../../Components/Form/Switch/Switch"; +import Table from "../../../Components/Table/Table"; +import Tooltip from "../../../Components/Tooltip/Tooltip"; +import useSnack from "../../../hooks/useSnack"; +import { setGroupIds } from "../../../redux/Actions/query"; +import { getGroups } from "../../../utils/api"; + +const GroupSelection = () => { + const { showSnack } = useSnack(); + const history = useHistory(); + const dispatch = useDispatch(); + const { t } = useTranslation(); + + const [groups, setGroups] = useState([]); + const [selectedGroups, setSelectedGroups] = useState([]); + + const languagePackageId = useSelector( + (state) => state.query.languagePackageId + ); + const staged = useSelector((state) => state.query.staged); + const onlyActivated = useSelector((state) => state.query.onlyActivated); + + const triggerSelection = useCallback( + (groupId) => { + if (selectedGroups.find((id) => id === groupId)) { + setSelectedGroups(selectedGroups.filter((id) => id !== groupId)); + } else { + setSelectedGroups((oldArray) => [...oldArray, groupId]); + } + }, + [selectedGroups] + ); + + const columns = useMemo( + () => [ + { + Header: "Selected", + accessor: "selected", + Cell: ({ row }) => ( + { + triggerSelection(row.original.id); + }} + checked={selectedGroups.find( + (groupId) => groupId === row.original.id + )} + /> + ), + }, + { + Header: t("screens.allGroups.groupName"), + accessor: "name", + Cell: ({ row }) => row.original.name, + headerClassName: "w-25", + }, + { + Header: t("screens.allGroups.groupDescription"), + accessor: "description", + Cell: ({ row }) => ( + <> +

+ {row.original.description} +

+ + + ), + headerClassName: "w-50", + }, + { + Header: t("screens.allGroups.active"), + accessor: "active", + Cell: ({ row }) => ( +
+ {row.original.active ? ( + + ) : ( + + )} +
+ ), + }, + ], + [selectedGroups, t, triggerSelection] + ); + + useEffect(() => { + getGroups(languagePackageId, staged, onlyActivated) + .then((response) => { + setGroups(response.data); + }) + .catch((event) => { + if (event.response?.status === 401 || event.response?.status === 404) { + showSnack("error", "Error fetching stats"); + return; + } + showSnack("error", "Internal Server Error"); + }); + }, [languagePackageId, onlyActivated, showSnack, staged]); + + const startActivating = useCallback(() => { + dispatch( + setGroupIds({ + groupIds: selectedGroups, + }) + ); + history.push("/learn/direction/"); + }, [dispatch, history, selectedGroups]); + + return ( +
+
+
+
+ + +
+ +
+ + ); +}; + +export default GroupSelection; diff --git a/src/screens/Learn/GroupSelection/GroupSelection.scss b/src/screens/Learn/GroupSelection/GroupSelection.scss new file mode 100644 index 00000000..a415fef9 --- /dev/null +++ b/src/screens/Learn/GroupSelection/GroupSelection.scss @@ -0,0 +1,30 @@ +@import "../../../colors"; +@import "../../../constants"; + +.group-selection { + .group-select-wrapper { + + padding: 50px 12px; + + @media screen and (min-width: $bp-md) { + padding: 50px; + } + + .table-wrapper { + overflow: scroll; + + @media screen and (min-width: $bp-md) { + overflow: hidden; + } + } + } + + .button-wrapper { + width: 90%; + margin: 0 auto; + + @media screen and (min-width: $bp-md) { + width: 30%; + } + } +} diff --git a/src/screens/Learn/Learn.jsx b/src/screens/Learn/Learn.jsx index 287e64d7..60ffbddc 100644 --- a/src/screens/Learn/Learn.jsx +++ b/src/screens/Learn/Learn.jsx @@ -4,6 +4,7 @@ import { Route, Switch, useRouteMatch } from "react-router"; import Dashboard from "./Dashboard/Dashboard.jsx"; import Direction from "./Direction/Direction.jsx"; import End from "./End/End.jsx"; +import GroupSelection from "./GroupSelection/GroupSelection.jsx"; import Query from "./Query/Query.jsx"; import "./Learn.scss"; @@ -15,6 +16,8 @@ const Learn = () => {
+ + diff --git a/src/screens/Learn/Query/Query.jsx b/src/screens/Learn/Query/Query.jsx index 4ab25e16..1b9565a7 100644 --- a/src/screens/Learn/Query/Query.jsx +++ b/src/screens/Learn/Query/Query.jsx @@ -1,4 +1,5 @@ import React, { useState, useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router"; import { useHistory } from "react-router-dom"; @@ -17,6 +18,7 @@ import { getQueryVocabulary, checkQuery } from "../../../utils/api.js"; import "./Query.scss"; const Query = () => { + const { t } = useTranslation(); const { showSnack } = useSnack(); const { direction } = useParams(); const dispatch = useDispatch(); @@ -26,7 +28,10 @@ const Query = () => { (state) => state.query.languagePackageId ); const staged = useSelector((state) => state.query.staged); + const onlyActivated = useSelector((state) => state.query.onlyActivated); + const groupIds = useSelector((state) => state.query.groupIds); const limit = useSelector((state) => state.query.vocabsToday); + const customLearning = useSelector((state) => state.query.customLearning); const [vocabs, setVocabs] = useState([]); const [vocabSize, setVocabSize] = useState(0); @@ -39,7 +44,13 @@ const Query = () => { const [buttonDisabled, setButtonDisabled] = useState(false); const getVocabulary = useCallback(() => { - getQueryVocabulary(languagePackageId, staged, limit) + getQueryVocabulary( + languagePackageId, + staged, + onlyActivated, + limit, + groupIds + ) .then((response) => { //store stats setVocabs(response.data); @@ -47,25 +58,32 @@ const Query = () => { }) .catch((event) => { if (event.response?.status === 401 || event.response?.status === 404) { - showSnack("error", "Error fetching stats"); + showSnack("error", t("global.fetchError")); return; } - showSnack("error", "Internal Server Error"); + showSnack("error", t("global.internalServerError")); }); - }, [languagePackageId, limit, showSnack, staged]); + }, [groupIds, languagePackageId, limit, onlyActivated, showSnack, staged, t]); const sendVocabCheck = useCallback( (vocabularyCardId, answer, progress) => { // send result to server - checkQuery(vocabularyCardId, answer, progress).catch((event) => { - if (event.response?.status === 401 || event.response?.status === 404) { - showSnack("error", "Error fetching stats"); - return; - } - showSnack("error", "Internal Server Error"); - }); + // if custom learning disable sending progress to server + if (!customLearning) { + checkQuery(vocabularyCardId, answer, progress).catch((event) => { + if ( + event.response?.status === 401 || + event.response?.status === 404 + ) { + showSnack("error", "Error fetching stats"); + return; + } + + showSnack("error", "Internal Server Error"); + }); + } setButtonDisabled(true); setTimeout(() => setButtonDisabled(false), 260); @@ -100,13 +118,14 @@ const Query = () => { } }, [ + customLearning, + direction, + wrongVocabs, correctVocabs, - dispatch, - showSnack, vocabSize, + showSnack, + dispatch, vocabs, - wrongVocabs, - direction, ] ); diff --git a/src/screens/Library/AllGroups/AllGroups.jsx b/src/screens/Library/AllGroups/AllGroups.jsx index 35468989..0385234e 100644 --- a/src/screens/Library/AllGroups/AllGroups.jsx +++ b/src/screens/Library/AllGroups/AllGroups.jsx @@ -112,7 +112,7 @@ const AllGroups = () => { }, [packageId]); const groupSubmitted = useCallback(() => { - getGroups(packageId).then((response) => { + getGroups(packageId, false, false).then((response) => { setShowGroupModal(false); setData(response.data); }); @@ -128,7 +128,7 @@ const AllGroups = () => { deleteGroup(currentGroup.id) .then(() => { setCurrentGroup(null); - getGroups(packageId).then((response) => { + getGroups(packageId, false, false).then((response) => { setData(response.data); }); setShowDeleteConfirmationModal(false); @@ -292,7 +292,7 @@ const AllGroups = () => { ), }); }); - getGroups(packageId).then((response) => { + getGroups(packageId, false, false).then((response) => { setData(response.data); }); }, [packageId]); diff --git a/src/screens/Library/AllPackages/AllPackages.jsx b/src/screens/Library/AllPackages/AllPackages.jsx index 6fe761c6..db694e7c 100644 --- a/src/screens/Library/AllPackages/AllPackages.jsx +++ b/src/screens/Library/AllPackages/AllPackages.jsx @@ -247,7 +247,7 @@ const AllPackages = () => { ); useEffect(() => { - getPackages().then((response) => { + getPackages(false, false, false).then((response) => { setData(response.data); }); }, []); diff --git a/src/screens/Library/AllVocabs/AllVocabs.jsx b/src/screens/Library/AllVocabs/AllVocabs.jsx index db95284b..7325478e 100644 --- a/src/screens/Library/AllVocabs/AllVocabs.jsx +++ b/src/screens/Library/AllVocabs/AllVocabs.jsx @@ -38,7 +38,7 @@ const AllVocabs = () => { const debouncedSearch = useDebounce(search, 200); const fetchVocabs = useCallback(() => { - getGroupVocabulary(groupId, debouncedSearch).then((response) => { + getGroupVocabulary(groupId, false, debouncedSearch).then((response) => { setData(() => response.data.map((elem) => { return { diff --git a/src/utils/api.js b/src/utils/api.js index d7a74c8f..3fe1fa28 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -39,8 +39,14 @@ export const deleteUser = () => api.delete("/user"); // Language package export const createPackage = (data) => api.post("/languagePackage", data); -export const getPackages = (groups = false, stats = false) => - api.get(`/languagePackage?groups=${groups}&stats=${stats}`); +export const getPackages = ( + groups = false, + stats = false, + onlyActivated = false +) => + api.get( + `/languagePackage?groups=${groups}&stats=${stats}&onlyActivated=${onlyActivated}` + ); export const modifyPackage = (data) => api.put(`/languagePackage/${data.id}`, data); export const deletePackage = (languagePackageId) => @@ -49,8 +55,10 @@ export const deletePackage = (languagePackageId) => // Language package group export const createGroup = (languagePackageId, data) => api.post(`/languagePackage/${languagePackageId}/group`, data); -export const getGroups = (languagePackageId) => - api.get(`/languagePackage/${languagePackageId}/group`); +export const getGroups = (languagePackageId, onlyStaged, onlyActivated) => + api.get( + `/languagePackage/${languagePackageId}/group?onlyStaged=${onlyStaged}&onlyActivated=${onlyActivated}` + ); export const modifyGroup = (data) => api.put(`/group/${data.id}`, data); export const deleteGroup = (groupId) => api.delete(`/group/${groupId}`); @@ -65,8 +73,10 @@ export const createVocabulary = ( `/languagePackage/${languagePackageId}/group/${groupId}/vocabulary?activate=${activate}`, data ); -export const getGroupVocabulary = (groupId, search) => - api.get(`/group/${groupId}/vocabulary?search=${search}`); +export const getGroupVocabulary = (groupId, onlyStaged, search) => + api.get( + `/group/${groupId}/vocabulary?onlyStaged=${onlyStaged}&search=${search}` + ); export const modifyVocabulary = (data) => api.put(`/vocabulary/${data.id}`, data); export const deleteVocabulary = (vocabularyId) => @@ -75,11 +85,15 @@ export const deleteVocabulary = (vocabularyId) => // Query Vocabulary export const getQueryVocabulary = ( languagePackageId, - staged = false, - limit = defaultLimit + onlyStaged = false, + onlyActivated = false, + limit = defaultLimit, + groupIds = null ) => api.get( - `/languagePackage/${languagePackageId}/query?staged=${staged}&limit=${limit}` + `/languagePackage/${languagePackageId}/query?onlyStaged=${onlyStaged}&onlyActivated=${onlyActivated}&limit=${limit}${ + groupIds ? groupIds.map((groupId) => `&groupId=${groupId}`).join("") : "" + }` ); export const checkQuery = (vocabularyId, answer = false, progress = false) => api.patch(