feat: added draggable virualized lessons list

This commit is contained in:
Sergey Yarkov 2021-11-28 21:41:29 +03:00
parent cc5581bbc4
commit c14f0cb8c7
9 changed files with 345 additions and 114 deletions

111
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View 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;

View File

@ -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);

View File

@ -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>

View File

@ -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

View 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;
}
}

View File

@ -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
*/ */