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(