mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: added draggable virualized lessons list
This commit is contained in:
parent
cc5581bbc4
commit
c14f0cb8c7
111
package-lock.json
generated
111
package-lock.json
generated
@ -1099,6 +1099,15 @@
|
|||||||
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
|
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/js-cookie": {
|
"@types/js-cookie": {
|
||||||
"version": "2.2.7",
|
"version": "2.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
|
||||||
@ -1150,6 +1159,15 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-beautiful-dnd": {
|
||||||
|
"version": "13.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz",
|
||||||
|
"integrity": "sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-dom": {
|
"@types/react-dom": {
|
||||||
"version": "17.0.9",
|
"version": "17.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz",
|
||||||
@ -1168,6 +1186,17 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-redux": {
|
||||||
|
"version": "7.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.20.tgz",
|
||||||
|
"integrity": "sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw==",
|
||||||
|
"requires": {
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.0",
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
|
"redux": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-router": {
|
"@types/react-router": {
|
||||||
"version": "5.1.17",
|
"version": "5.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz",
|
||||||
@ -1205,6 +1234,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
|
||||||
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
|
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
|
||||||
},
|
},
|
||||||
|
"@virtuoso.dev/react-urx": {
|
||||||
|
"version": "0.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.8.tgz",
|
||||||
|
"integrity": "sha512-QV9ZJ9Yla88ySgN+DuHLDYoQa0UDRrEq88sLqQESZUWzLughrkpg1BLmQT7FuiU2M78A+rQ9DR3GBlWrJIx+Bw==",
|
||||||
|
"requires": {
|
||||||
|
"@virtuoso.dev/urx": "^0.2.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@virtuoso.dev/urx": {
|
||||||
|
"version": "0.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@virtuoso.dev/urx/-/urx-0.2.8.tgz",
|
||||||
|
"integrity": "sha512-40uiTn5z1J/7at9Ao/aesyFhE7sS1cpb2MrbP/JSS6dcLMsQJcAZGedCY3bKkyjANH9DX/ciUpkpmT3TXmUBOQ=="
|
||||||
|
},
|
||||||
"@vitejs/plugin-react": {
|
"@vitejs/plugin-react": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.0.4.tgz",
|
||||||
@ -1836,6 +1878,11 @@
|
|||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"memoize-one": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||||
|
},
|
||||||
"mini-create-react-context": {
|
"mini-create-react-context": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
||||||
@ -2019,6 +2066,11 @@
|
|||||||
"side-channel": "^1.0.4"
|
"side-channel": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||||
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||||
@ -2028,6 +2080,20 @@
|
|||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-beautiful-dnd": {
|
||||||
|
"version": "13.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
|
||||||
|
"integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.9.2",
|
||||||
|
"css-box-model": "^1.2.0",
|
||||||
|
"memoize-one": "^5.1.1",
|
||||||
|
"raf-schd": "^4.0.2",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
|
"redux": "^4.0.4",
|
||||||
|
"use-memo-one": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-clientside-effect": {
|
"react-clientside-effect": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz",
|
||||||
@ -2101,10 +2167,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
"react-movable": {
|
"react-redux": {
|
||||||
"version": "3.0.2",
|
"version": "7.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-movable/-/react-movable-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
|
||||||
"integrity": "sha512-dDDYm3CRnDy8YLXMyyaR2MbcQiTwhPOP+dfl3fZukiI6mN1flVatcjSozT7HXjVk2yHwBC67ZOWGVAmjY6F/dA=="
|
"integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.15.4",
|
||||||
|
"@types/react-redux": "^7.1.20",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^17.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"react-refresh": {
|
"react-refresh": {
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
@ -2208,6 +2289,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-virtuoso": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-DArJ29ZrhdygdLKdXs/GB3VNDxaAF7YjdgIjlWPuK6UsV848hNmtw9px3LPdcQBErxaz7XCtHNfc7yInX+/M7g==",
|
||||||
|
"requires": {
|
||||||
|
"@virtuoso.dev/react-urx": "^0.2.8",
|
||||||
|
"@virtuoso.dev/urx": "^0.2.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.13.9",
|
"version": "0.13.9",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||||
@ -2464,6 +2562,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
||||||
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
|
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
|
||||||
},
|
},
|
||||||
|
"use-memo-one": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ=="
|
||||||
|
},
|
||||||
"use-sidecar": {
|
"use-sidecar": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz",
|
||||||
|
|||||||
@ -22,14 +22,15 @@
|
|||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"qs": "^6.10.1",
|
"qs": "^6.10.1",
|
||||||
"react": "^17.0.0",
|
"react": "^17.0.0",
|
||||||
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^17.0.0",
|
"react-dom": "^17.0.0",
|
||||||
"react-error-boundary": "^3.1.3",
|
"react-error-boundary": "^3.1.3",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-hook-form": "^7.12.2",
|
"react-hook-form": "^7.12.2",
|
||||||
"react-icons": "^3.11.0",
|
"react-icons": "^3.11.0",
|
||||||
"react-movable": "^3.0.2",
|
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-spinners": "^0.11.0",
|
"react-spinners": "^0.11.0",
|
||||||
|
"react-virtuoso": "^2.2.8",
|
||||||
"validator": "^13.6.0",
|
"validator": "^13.6.0",
|
||||||
"yup": "^0.32.9"
|
"yup": "^0.32.9"
|
||||||
},
|
},
|
||||||
@ -38,6 +39,7 @@
|
|||||||
"@types/node": "^16.11.0",
|
"@types/node": "^16.11.0",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
|
"@types/react-beautiful-dnd": "^13.1.2",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@types/react-helmet": "^6.1.2",
|
"@types/react-helmet": "^6.1.2",
|
||||||
"@types/react-router-dom": "^5.1.8",
|
"@types/react-router-dom": "^5.1.8",
|
||||||
|
|||||||
@ -69,3 +69,18 @@ export function transformBytes(bytes: number, decimals = 2): string {
|
|||||||
|
|
||||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param array Array
|
||||||
|
* @param from From index
|
||||||
|
* @param to To index
|
||||||
|
* @returns Reordered array
|
||||||
|
*/
|
||||||
|
export function arrayMove<T>(array: T[], from: number, to: number) {
|
||||||
|
const result = array.slice();
|
||||||
|
const [removed] = result.splice(from, 1);
|
||||||
|
|
||||||
|
result.splice(to, 0, removed);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
15
src/hooks/useDidMountEffect.ts
Normal file
15
src/hooks/useDidMountEffect.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
const useDidMountEffect = (effect: React.EffectCallback, deps?: React.DependencyList | undefined) => {
|
||||||
|
const didMount = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (didMount.current) {
|
||||||
|
effect();
|
||||||
|
} else {
|
||||||
|
didMount.current = true;
|
||||||
|
}
|
||||||
|
}, deps);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDidMountEffect;
|
||||||
@ -7,7 +7,6 @@ import { useEffect } from 'react';
|
|||||||
/**
|
/**
|
||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
|
||||||
import useAsync from './useAsync';
|
import useAsync from './useAsync';
|
||||||
import { useRootStore } from './useRootStore';
|
import { useRootStore } from './useRootStore';
|
||||||
|
|
||||||
@ -17,17 +16,10 @@ const useFetchCourseQuery = (id: string) => {
|
|||||||
const {
|
const {
|
||||||
courseStore: { courseService },
|
courseStore: { courseService },
|
||||||
} = useRootStore();
|
} = useRootStore();
|
||||||
const handleError = useErrorHandler();
|
|
||||||
|
|
||||||
const fetch = async (id: string) => {
|
const fetch = async (id: string) => {
|
||||||
try {
|
const result = await courseService.fetchById(id);
|
||||||
const result = await courseService.fetchById(id);
|
return result.data;
|
||||||
return result.data;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (!error.response) {
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { execute: fetchCourseById, ...state } = useAsync<QueryResponseDataType, Parameters<typeof fetch>>(fetch);
|
const { execute: fetchCourseById, ...state } = useAsync<QueryResponseDataType, Parameters<typeof fetch>>(fetch);
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { ItemProps, Virtuoso } from 'react-virtuoso';
|
||||||
|
import { DragDropContext, Draggable, DraggableProvided, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { List, arrayMove } from 'react-movable';
|
import * as helpers from '@educt/helpers';
|
||||||
import { Flex, Box, Stack, Text, IconButton, Icon, Button } from '@chakra-ui/react';
|
import { Flex, Box, Text, IconButton, Icon, Button } from '@chakra-ui/react';
|
||||||
|
import { DeleteIcon, DragHandleIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types
|
* Types
|
||||||
@ -12,11 +15,10 @@ import type { ICourse, ILesson } from '@educt/interfaces';
|
|||||||
* Hooks
|
* Hooks
|
||||||
*/
|
*/
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
import { useColorMode } from '@chakra-ui/react';
|
import { useColorMode } from '@chakra-ui/react';
|
||||||
import { DeleteIcon } from '@chakra-ui/icons';
|
|
||||||
import { MdTimer, MdAttachment } from 'react-icons/md';
|
import { MdTimer, MdAttachment } from 'react-icons/md';
|
||||||
import { DragHandleIcon } from '@chakra-ui/icons';
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
|
import useDidMountEffect from '@educt/hooks/useDidMountEffect';
|
||||||
|
|
||||||
type LessonListPropsType = {
|
type LessonListPropsType = {
|
||||||
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
course: Omit<ICourse, 'students_count' | 'likes_count' | 'lessons_count'>;
|
||||||
@ -28,117 +30,189 @@ const CreateLessonButton: React.FC<{ id: string }> = ({ id }) => {
|
|||||||
const handleCreateLesson = (): void => history.push(`${id}/create-lesson`);
|
const handleCreateLesson = (): void => history.push(`${id}/create-lesson`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box textAlign='center' p='10px 0'>
|
<Button onClick={handleCreateLesson} size='sm' colorScheme='blue' variant='outline'>
|
||||||
<Button onClick={handleCreateLesson} mt='3' size='sm' colorScheme='blue' variant='outline'>
|
Create new lesson
|
||||||
Create new lesson
|
</Button>
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
|
const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { lessonService } = useRootStore();
|
||||||
const [lessons, setLessons] = useState<ILesson[]>(course.lessons);
|
const [lessons, setLessons] = useState<ILesson[]>(course.lessons);
|
||||||
const { colorMode } = useColorMode();
|
|
||||||
|
|
||||||
|
const handleCreateLesson = (): void => history.push('/lessons/create');
|
||||||
const handleEditLesson = (id: string): void => history.push(`/lessons/edit/${id}`);
|
const handleEditLesson = (id: string): void => history.push(`/lessons/edit/${id}`);
|
||||||
const handleDeleteLesson = (id: string): void => undefined;
|
const handleDeleteLesson = (id: string): void => undefined;
|
||||||
|
const handleChangeOrder = async (ids: string[]) => {
|
||||||
|
try {
|
||||||
|
const data = await lessonService.saveOrder(ids);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
setLessons(helpers.arrayMove(lessons, result.source.index, result.destination.index));
|
||||||
<Box>
|
};
|
||||||
{course.lessons.length !== 0 ? (
|
|
||||||
<Box>
|
const LessonItem = React.useMemo(() => {
|
||||||
<List
|
return ({
|
||||||
transitionDuration={100}
|
provided,
|
||||||
lockVertically
|
lesson,
|
||||||
values={lessons}
|
index,
|
||||||
onChange={({ oldIndex, newIndex }) => setLessons(arrayMove(lessons, oldIndex, newIndex))}
|
}: {
|
||||||
renderList={({ children, props }) => <Stack {...props}>{children}</Stack>}
|
provided: DraggableProvided;
|
||||||
renderItem={({ value: lesson, props, isDragged, index }) => (
|
lesson: ILesson;
|
||||||
<Box
|
isDragging: boolean;
|
||||||
{...props}
|
index: number;
|
||||||
zIndex='1'
|
}) => {
|
||||||
bg={colorMode === 'dark' ? 'gray.700' : 'gray.50'}
|
const { colorMode } = useColorMode();
|
||||||
borderRadius='lg'
|
return (
|
||||||
position='relative'
|
<Box
|
||||||
p='3'
|
{...provided.draggableProps}
|
||||||
borderBottomWidth='1px'
|
ref={provided.innerRef}
|
||||||
|
style={{ ...provided.draggableProps.style, padding: '0 20px 5px 20px' }}
|
||||||
|
zIndex='1'
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
bg={colorMode === 'dark' ? 'gray.700' : 'gray.50'}
|
||||||
|
borderRadius='lg'
|
||||||
|
position='relative'
|
||||||
|
p='3'
|
||||||
|
borderBottomWidth='1px'
|
||||||
|
>
|
||||||
|
<Box position='absolute' top='40px' left='-20px' {...provided.dragHandleProps}>
|
||||||
|
<DragHandleIcon />
|
||||||
|
</Box>
|
||||||
|
<Flex flexDirection={{ base: 'column', md: 'row' }} justifyContent='space-between'>
|
||||||
|
<Flex
|
||||||
|
flexDirection={{ base: 'column', md: 'row' }}
|
||||||
|
textAlign={{ base: 'center', md: 'left' }}
|
||||||
|
alignItems='center'
|
||||||
|
mr={{ md: '4' }}
|
||||||
>
|
>
|
||||||
<Box
|
<Box mr='3'>
|
||||||
data-movable-handle
|
<Box bg={lesson.color?.hex || 'gray.100'} minH='50px' minW='50px' borderRadius='md' />
|
||||||
cursor={isDragged ? 'grabbing' : 'grab'}
|
|
||||||
position='absolute'
|
|
||||||
bottom='50%'
|
|
||||||
left='-20px'
|
|
||||||
>
|
|
||||||
<DragHandleIcon />
|
|
||||||
</Box>
|
</Box>
|
||||||
<Flex flexDirection={{ base: 'column', md: 'row' }} justifyContent='space-between'>
|
<Box>
|
||||||
<Flex
|
<Text fontSize='lg' fontWeight='medium' mt={{ base: '3', md: '0' }}>
|
||||||
flexDirection={{ base: 'column', md: 'row' }}
|
<Text as='span' color='gray.500' mr='1' fontSize='md' fontWeight='normal'>
|
||||||
textAlign={{ base: 'center', md: 'left' }}
|
{index + 1}.
|
||||||
alignItems='center'
|
</Text>
|
||||||
mr={{ md: '4' }}
|
{lesson.title}
|
||||||
>
|
</Text>
|
||||||
<Box mr='3'>
|
<Text mt='2' color='gray.500'>
|
||||||
<Box bg={lesson.color?.hex || 'gray.100'} minH='50px' minW='50px' borderRadius='md' />
|
{course.teacher.fullname}
|
||||||
</Box>
|
</Text>
|
||||||
<Box>
|
</Box>
|
||||||
<Text fontSize='lg' fontWeight='medium' mt={{ base: '3', md: '0' }}>
|
</Flex>
|
||||||
<Text as='span' color='gray.500' mr='1' fontSize='md' fontWeight='normal'>
|
<Box>
|
||||||
{index}.
|
<Flex flexDirection='column' alignItems={{ base: 'center' }} mt={{ base: '4' }}>
|
||||||
</Text>
|
<Flex flexDirection={{ base: 'column', md: 'row' }}>
|
||||||
{lesson.title}
|
<Button size='md' p='0 60px' mr='1'>
|
||||||
</Text>
|
Edit
|
||||||
<Text mt='2' color='gray.500'>
|
</Button>
|
||||||
Sergey Yarkov
|
<IconButton aria-label='Delete lesson' variant='ghost' colorScheme='red' icon={<DeleteIcon />} />
|
||||||
</Text>
|
</Flex>
|
||||||
</Box>
|
<Flex justifyContent='flex-end' mt={{ base: '2' }}>
|
||||||
|
<Text as='span' color='gray.500' mr='3'>
|
||||||
|
<Text as='span' verticalAlign='middle' mr='1'>
|
||||||
|
<Icon as={MdTimer} />
|
||||||
|
</Text>
|
||||||
|
<Text as='small' verticalAlign='middle'>
|
||||||
|
~{moment.duration(lesson.duration, 'minutes').humanize()}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Text as='span' color='gray.500'>
|
||||||
|
<Text as='span' verticalAlign='middle' mr='1'>
|
||||||
|
<Icon as={MdAttachment} />
|
||||||
|
</Text>
|
||||||
|
<Text as='small' verticalAlign='middle'>
|
||||||
|
4 attachments
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
|
||||||
<Flex flexDirection='column' alignItems={{ base: 'center' }} mt={{ base: '4' }}>
|
|
||||||
<Flex flexDirection={{ base: 'column', md: 'row' }}>
|
|
||||||
<Button onClick={() => handleEditLesson(lesson.id)} size='md' p='0 60px' mr='1'>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => handleDeleteLesson(lesson.id)}
|
|
||||||
aria-label='Delete lesson'
|
|
||||||
variant='ghost'
|
|
||||||
colorScheme='red'
|
|
||||||
icon={<DeleteIcon />}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Flex justifyContent='flex-end' mt={{ base: '2' }}>
|
|
||||||
<Text as='span' color='gray.500' mr='3'>
|
|
||||||
<Text as='span' verticalAlign='middle' mr='1'>
|
|
||||||
<Icon as={MdTimer} />
|
|
||||||
</Text>
|
|
||||||
<Text as='small' verticalAlign='middle'>
|
|
||||||
~{moment.duration(lesson.duration, 'minutes').humanize()}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
<Text as='span' color='gray.500'>
|
|
||||||
<Text as='span' verticalAlign='middle' mr='1'>
|
|
||||||
<Icon as={MdAttachment} />
|
|
||||||
</Text>
|
|
||||||
<Text as='small' verticalAlign='middle'>
|
|
||||||
4 attachments
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
</Flex>
|
||||||
/>
|
</Box>
|
||||||
<CreateLessonButton id={course.id} />
|
|
||||||
</Box>
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const HeightPreservingItem: React.ComponentType<ItemProps> = React.useMemo(() => {
|
||||||
|
return ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
// the height is necessary to prevent the item container from collapsing, which confuses Virtuoso measurements
|
||||||
|
<div {...props} style={{ height: props['data-known-size'] || undefined }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useDidMountEffect(() => {
|
||||||
|
handleChangeOrder(lessons.map(lesson => lesson.id));
|
||||||
|
}, [lessons]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box style={{ overflow: 'auto' }}>
|
||||||
|
{lessons.length !== 0 ? (
|
||||||
|
<>
|
||||||
|
<Flex mt='2' mb='3' padding='0 20px' alignItems='center' justifyContent='space-between'>
|
||||||
|
<Text fontWeight='medium'>Total: ({lessons.length})</Text>
|
||||||
|
<Button onClick={handleCreateLesson}>Create new</Button>
|
||||||
|
</Flex>
|
||||||
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
|
<Droppable
|
||||||
|
droppableId='droppable'
|
||||||
|
mode='virtual'
|
||||||
|
renderClone={(provided, snapshot, rubric) => (
|
||||||
|
<LessonItem
|
||||||
|
provided={provided}
|
||||||
|
isDragging={snapshot.isDragging}
|
||||||
|
lesson={lessons[rubric.source.index]}
|
||||||
|
index={rubric.source.index}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{provided => {
|
||||||
|
return (
|
||||||
|
<div ref={provided.innerRef}>
|
||||||
|
<Virtuoso
|
||||||
|
useWindowScroll
|
||||||
|
components={{
|
||||||
|
Item: HeightPreservingItem,
|
||||||
|
}}
|
||||||
|
data={lessons}
|
||||||
|
itemContent={(index, lesson) => {
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={lesson.id} index={index} key={lesson.id}>
|
||||||
|
{provided => (
|
||||||
|
<LessonItem provided={provided} lesson={lesson} index={index} isDragging={false} />
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Box mt='10'>
|
<Box mt='10' textAlign='center'>
|
||||||
<Text>No lessons have been added to this course yet</Text>
|
<Text>No lessons have been added to this course yet</Text>
|
||||||
<CreateLessonButton id={course.id} />
|
<Box p='10px 0'>
|
||||||
|
<CreateLessonButton id={course.id} />
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -34,7 +34,9 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
|||||||
/**
|
/**
|
||||||
* Not Found
|
* Not Found
|
||||||
*/
|
*/
|
||||||
if (error?.response?.status === 404) return <Redirect to='/404' />;
|
if (error?.response?.status === 404) {
|
||||||
|
return <Redirect to='/404' />;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loading
|
* Loading
|
||||||
|
|||||||
23
src/services/LessonService.ts
Normal file
23
src/services/LessonService.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { IApiRespose } from '@educt/interfaces';
|
||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
|
export default class LessonService {
|
||||||
|
public api: AxiosInstance;
|
||||||
|
|
||||||
|
constructor(api: AxiosInstance) {
|
||||||
|
this.api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save order of lessons
|
||||||
|
*
|
||||||
|
* @param ids Ordered ids of lessons
|
||||||
|
* @returns Empty data
|
||||||
|
*/
|
||||||
|
public async saveOrder(ids: string[]): Promise<IApiRespose<any>> {
|
||||||
|
const result = await this.api.post('/v1/lessons/save-order', {
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ import UserStore from './UserStore';
|
|||||||
import UIStore from './UIStore';
|
import UIStore from './UIStore';
|
||||||
import CategoryStore from './CategoryStore';
|
import CategoryStore from './CategoryStore';
|
||||||
import CourseStore from './CourseStore';
|
import CourseStore from './CourseStore';
|
||||||
|
import LessonService from '@educt/services/LessonService';
|
||||||
|
|
||||||
export default class RootStore {
|
export default class RootStore {
|
||||||
public courseStore: CourseStore;
|
public courseStore: CourseStore;
|
||||||
@ -28,12 +29,16 @@ export default class RootStore {
|
|||||||
|
|
||||||
public apiService: ApiService;
|
public apiService: ApiService;
|
||||||
|
|
||||||
|
public lessonService: LessonService;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
/**
|
/**
|
||||||
* Api service
|
* Api service
|
||||||
*/
|
*/
|
||||||
this.apiService = new ApiService(this, apiConfig);
|
this.apiService = new ApiService(this, apiConfig);
|
||||||
|
|
||||||
|
this.lessonService = new LessonService(this.apiService.api);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores
|
* Stores
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user