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==",
|
||||
"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": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
|
||||
@ -1150,6 +1159,15 @@
|
||||
"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": {
|
||||
"version": "17.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz",
|
||||
@ -1168,6 +1186,17 @@
|
||||
"@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": {
|
||||
"version": "5.1.17",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.4",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
@ -2028,6 +2080,20 @@
|
||||
"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": {
|
||||
"version": "1.2.5",
|
||||
"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",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"react-movable": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-movable/-/react-movable-3.0.2.tgz",
|
||||
"integrity": "sha512-dDDYm3CRnDy8YLXMyyaR2MbcQiTwhPOP+dfl3fZukiI6mN1flVatcjSozT7HXjVk2yHwBC67ZOWGVAmjY6F/dA=="
|
||||
"react-redux": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
|
||||
"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": {
|
||||
"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": {
|
||||
"version": "0.13.9",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz",
|
||||
|
||||
@ -22,14 +22,15 @@
|
||||
"moment": "^2.29.1",
|
||||
"qs": "^6.10.1",
|
||||
"react": "^17.0.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.12.2",
|
||||
"react-icons": "^3.11.0",
|
||||
"react-movable": "^3.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-spinners": "^0.11.0",
|
||||
"react-virtuoso": "^2.2.8",
|
||||
"validator": "^13.6.0",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
@ -38,6 +39,7 @@
|
||||
"@types/node": "^16.11.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-helmet": "^6.1.2",
|
||||
"@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]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import useAsync from './useAsync';
|
||||
import { useRootStore } from './useRootStore';
|
||||
|
||||
@ -17,17 +16,10 @@ const useFetchCourseQuery = (id: string) => {
|
||||
const {
|
||||
courseStore: { courseService },
|
||||
} = useRootStore();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
const fetch = async (id: string) => {
|
||||
try {
|
||||
const result = await courseService.fetchById(id);
|
||||
return result.data;
|
||||
} catch (error: any) {
|
||||
if (!error.response) {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
const result = await courseService.fetchById(id);
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const { execute: fetchCourseById, ...state } = useAsync<QueryResponseDataType, Parameters<typeof fetch>>(fetch);
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
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 { List, arrayMove } from 'react-movable';
|
||||
import { Flex, Box, Stack, Text, IconButton, Icon, Button } from '@chakra-ui/react';
|
||||
import * as helpers from '@educt/helpers';
|
||||
import { Flex, Box, Text, IconButton, Icon, Button } from '@chakra-ui/react';
|
||||
import { DeleteIcon, DragHandleIcon } from '@chakra-ui/icons';
|
||||
|
||||
/**
|
||||
* Types
|
||||
@ -12,11 +15,10 @@ import type { ICourse, ILesson } from '@educt/interfaces';
|
||||
* Hooks
|
||||
*/
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import { useColorMode } from '@chakra-ui/react';
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
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 = {
|
||||
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`);
|
||||
|
||||
return (
|
||||
<Box textAlign='center' p='10px 0'>
|
||||
<Button onClick={handleCreateLesson} mt='3' size='sm' colorScheme='blue' variant='outline'>
|
||||
Create new lesson
|
||||
</Button>
|
||||
</Box>
|
||||
<Button onClick={handleCreateLesson} size='sm' colorScheme='blue' variant='outline'>
|
||||
Create new lesson
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const LessonList: React.FC<LessonListPropsType> = ({ course }) => {
|
||||
const history = useHistory();
|
||||
const { lessonService } = useRootStore();
|
||||
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 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 (
|
||||
<Box>
|
||||
{course.lessons.length !== 0 ? (
|
||||
<Box>
|
||||
<List
|
||||
transitionDuration={100}
|
||||
lockVertically
|
||||
values={lessons}
|
||||
onChange={({ oldIndex, newIndex }) => setLessons(arrayMove(lessons, oldIndex, newIndex))}
|
||||
renderList={({ children, props }) => <Stack {...props}>{children}</Stack>}
|
||||
renderItem={({ value: lesson, props, isDragged, index }) => (
|
||||
<Box
|
||||
{...props}
|
||||
zIndex='1'
|
||||
bg={colorMode === 'dark' ? 'gray.700' : 'gray.50'}
|
||||
borderRadius='lg'
|
||||
position='relative'
|
||||
p='3'
|
||||
borderBottomWidth='1px'
|
||||
setLessons(helpers.arrayMove(lessons, result.source.index, result.destination.index));
|
||||
};
|
||||
|
||||
const LessonItem = React.useMemo(() => {
|
||||
return ({
|
||||
provided,
|
||||
lesson,
|
||||
index,
|
||||
}: {
|
||||
provided: DraggableProvided;
|
||||
lesson: ILesson;
|
||||
isDragging: boolean;
|
||||
index: number;
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
return (
|
||||
<Box
|
||||
{...provided.draggableProps}
|
||||
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
|
||||
data-movable-handle
|
||||
cursor={isDragged ? 'grabbing' : 'grab'}
|
||||
position='absolute'
|
||||
bottom='50%'
|
||||
left='-20px'
|
||||
>
|
||||
<DragHandleIcon />
|
||||
<Box mr='3'>
|
||||
<Box bg={lesson.color?.hex || 'gray.100'} minH='50px' minW='50px' borderRadius='md' />
|
||||
</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 mr='3'>
|
||||
<Box bg={lesson.color?.hex || 'gray.100'} minH='50px' minW='50px' borderRadius='md' />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontSize='lg' fontWeight='medium' mt={{ base: '3', md: '0' }}>
|
||||
<Text as='span' color='gray.500' mr='1' fontSize='md' fontWeight='normal'>
|
||||
{index}.
|
||||
</Text>
|
||||
{lesson.title}
|
||||
</Text>
|
||||
<Text mt='2' color='gray.500'>
|
||||
Sergey Yarkov
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontSize='lg' fontWeight='medium' mt={{ base: '3', md: '0' }}>
|
||||
<Text as='span' color='gray.500' mr='1' fontSize='md' fontWeight='normal'>
|
||||
{index + 1}.
|
||||
</Text>
|
||||
{lesson.title}
|
||||
</Text>
|
||||
<Text mt='2' color='gray.500'>
|
||||
{course.teacher.fullname}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<Flex flexDirection='column' alignItems={{ base: 'center' }} mt={{ base: '4' }}>
|
||||
<Flex flexDirection={{ base: 'column', md: 'row' }}>
|
||||
<Button size='md' p='0 60px' mr='1'>
|
||||
Edit
|
||||
</Button>
|
||||
<IconButton 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>
|
||||
<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>
|
||||
</Box>
|
||||
)}
|
||||
/>
|
||||
<CreateLessonButton id={course.id} />
|
||||
</Flex>
|
||||
</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>
|
||||
<CreateLessonButton id={course.id} />
|
||||
<Box p='10px 0'>
|
||||
<CreateLessonButton id={course.id} />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@ -34,7 +34,9 @@ const EditCoursePage: React.FC<IPageProps> = () => {
|
||||
/**
|
||||
* Not Found
|
||||
*/
|
||||
if (error?.response?.status === 404) return <Redirect to='/404' />;
|
||||
if (error?.response?.status === 404) {
|
||||
return <Redirect to='/404' />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 CategoryStore from './CategoryStore';
|
||||
import CourseStore from './CourseStore';
|
||||
import LessonService from '@educt/services/LessonService';
|
||||
|
||||
export default class RootStore {
|
||||
public courseStore: CourseStore;
|
||||
@ -28,12 +29,16 @@ export default class RootStore {
|
||||
|
||||
public apiService: ApiService;
|
||||
|
||||
public lessonService: LessonService;
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* Api service
|
||||
*/
|
||||
this.apiService = new ApiService(this, apiConfig);
|
||||
|
||||
this.lessonService = new LessonService(this.apiService.api);
|
||||
|
||||
/**
|
||||
* Stores
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user