users screen + userCard component
Some checks failed
platform/bro/pipeline/pr-master This commit looks good
platform/bro/pipeline/head This commit looks good
platform/gitea-bro-js/journal.pl/pipeline/head There was a failure building this commit
platform/bro-js/journal.pl/pipeline/head This commit looks good

This commit is contained in:
2024-04-03 22:00:17 +03:00
parent 927bf3929d
commit 58342561ee
9 changed files with 340 additions and 244 deletions

View File

@@ -0,0 +1 @@
export { UserCard } from './user-card'

View File

@@ -0,0 +1,53 @@
import styled from '@emotion/styled'
import { css, keyframes } from '@emotion/react'
export const Avatar = styled.img`
width: 96px;
height: 96px;
margin: 0 auto;
border-radius: 6px;
`
export const Wrapper = styled.div<{ warn?: boolean; width?: string | number }>`
list-style: none;
background-color: #ffffff;
padding: 16px;
border-radius: 12px;
box-shadow: 2px 2px 6px #0000005c;
transition: all 0.5;
position: relative;
width: 180px;
min-height: 190px;
max-height: 200px;
margin-right: 12px;
padding-bottom: 22px;
${({ width }) =>
width
? css`
width: ${width};
`
: ''}
${(props) =>
props.warn
? css`
background-color: #000000;
opacity: 0.7;
color: #e4e4e4;
`
: ''}
`
export const AddMissedButton = styled.button`
position: absolute;
bottom: 8px;
right: 12px;
border: none;
background-color: #00000000;
opacity: 0.2;
:hover {
cursor: pointer;
opacity: 1;
}
`

View File

@@ -0,0 +1,47 @@
import React from 'react'
import { sha256 } from 'js-sha256'
import { User } from '../../__data__/model'
import { AddMissedButton, Avatar, Wrapper } from './style'
export function getGravatarURL(email, user) {
if (!email) return void 0
const address = String(email).trim().toLowerCase()
const hash = sha256(address)
return `https://www.gravatar.com/avatar/${hash}?d=robohash`
}
export const UserCard = ({
student,
present,
onAddUser,
wrapperAS,
width
}: {
student: User
present: boolean
width?: string | number
onAddUser?: (user: User) => void
wrapperAS?: React.ElementType<any, keyof React.JSX.IntrinsicElements>;
}) => {
return (
<Wrapper warn={!present} as={wrapperAS} width={width}>
<Avatar src={student.picture || getGravatarURL(student.email, null)} />
<p style={{ marginTop: 6 }}>
{student.name || student.preferred_username}{' '}
</p>
{onAddUser && !present && (
<AddMissedButton onClick={() => onAddUser(student)}>
add
</AddMissedButton>
)}
</Wrapper>
)
}
UserCard.defaultProps = {
wrapperAS: 'div',
onAddUser: void 0,
}

View File

@@ -1,6 +1,8 @@
import React, { useEffect, Suspense } from 'react'
import { Routes, Route, useNavigate } from 'react-router-dom'
import { Provider } from 'react-redux'
import { getNavigationsValue } from '@ijl/cli'
import { Box, Container, Spinner, VStack } from '@chakra-ui/react'
import {
CourseListPage,
@@ -8,20 +10,8 @@ import {
LessonListPage,
UserPage,
} from './pages'
import { getNavigationsValue } from '@ijl/cli'
import { Box, Container, Spinner, VStack } from '@chakra-ui/react'
const Redirect = ({ path }) => {
const navigate = useNavigate()
useEffect(() => {
navigate(path)
}, [])
return null
}
const Wrapper = ({ children }) => (
const Wrapper = ({ children }: { children: React.ReactElement }) => (
<Suspense
fallback={
<Container>

View File

@@ -29,7 +29,7 @@ import {
} from '@chakra-ui/react'
import { useForm, Controller } from 'react-hook-form'
import { ErrorSpan, MainWrapper } from './style'
import { ErrorSpan } from './style'
import { useAppSelector } from '../__data__/store'
import { api } from '../__data__/api/api'

View File

@@ -10,22 +10,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
Container,
HStack,
VStack,
Heading,
Stack,
} from '@chakra-ui/react'
import { api } from '../__data__/api/api'
import { User } from '../__data__/model'
import { UserCard } from '../components/user-card'
import {
QRCanvas,
LessonItem,
Lessonname,
AddMissedButton,
UnorderList,
StudentList,
BreadcrumbsWrapper,
Avatar,
} from './style'
import { api } from '../__data__/api/api'
import { User } from '../__data__/model'
export function getGravatarURL(email, user) {
if (!email) return void 0
@@ -39,13 +37,11 @@ export function getGravatarURL(email, user) {
const LessonDetail = () => {
const { lessonId, courseId } = useParams()
const canvRef = useRef(null)
const [lesson, setLesson] = useState(null)
const {
isFetching,
isLoading,
data: accessCode,
error,
isSuccess,
refetch,
} = api.useCreateAccessCodeQuery(
{ lessonId },
{
@@ -60,6 +56,13 @@ const LessonDetail = () => {
() => `${location.origin}/journal/u/${lessonId}/${accessCode?.body?._id}`,
[accessCode, lessonId],
)
useEffect(() => {
if (manualAddRqst.isSuccess) {
refetch()
}
}, [manualAddRqst.isSuccess])
useEffect(() => {
if (!isFetching && isSuccess) {
QRCode.toCanvas(
@@ -136,32 +139,22 @@ const LessonDetail = () => {
человек
</Box>
</VStack>
<HStack spacing="8" sx={{ flexDirection: { sm: 'column', md: 'row' }}} >
<Stack spacing="8" sx={{ flexDirection: { sm: 'column', md: 'row' } }}>
<a href={userUrl}>
<QRCanvas ref={canvRef} />
</a>
<UnorderList>
<StudentList>
{studentsArr.map((student) => (
<LessonItem key={student.sub} warn={!student.present}>
<Lessonname>
<Avatar
src={student.picture || getGravatarURL(student.email, null)}
/>
<p style={{ marginTop: 6 }}>
{student.name || student.preferred_username}{' '}
</p>
{!student.present && (
<AddMissedButton
onClick={() => manualAdd({ lessonId, user: student })}
>
add
</AddMissedButton>
)}
</Lessonname>
</LessonItem>
<UserCard
wrapperAS="li"
key={student.sub}
student={student}
present={student.present}
onAddUser={(user: User) => manualAdd({ lessonId, user })}
/>
))}
</UnorderList>
</HStack>
</StudentList>
</Stack>
</Container>
</>
)

View File

@@ -1,63 +1,8 @@
import styled from '@emotion/styled'
import { css, keyframes } from '@emotion/react'
import {
Card
} from '@chakra-ui/react'
import { keyframes } from '@emotion/react'
export const BreadcrumbsWrapper = styled.div`
padding: 12px;
`;
export const MainWrapper = styled.main`
display: flex;
justify-content: center;
height: 100%;
`
export const InputWrapper = styled.div`
position: relative;
padding: 12px;
display: flex;
align-items: center;
@media screen and (max-width: 600px) {
flex-direction: column;
}
`
export const InputLabel = styled.label`
position: absolute;
top: -8px;
left: 24px;
z-index: 2;
`
export const InputElement = styled.input`
border: 1px solid #ccc;
padding: 12px;
font-size: 24px;
border-radius: 8px;
color: #117623;
max-width: 90vw;
box-shadow: inset 7px 8px 20px 8px #4990db12;
`
export const StyledCard = styled(Card)`
box-shadow: 2px 2px 6px #0000005c;
padding: 16px;
border-radius: 12px;
min-width: 400px;
`
export const ArrowImg = styled.img`
width: 48px;
height: 48px;
`
export const IconButton = styled.button`
border: none;
background-color: rgba(0, 0, 0, 0);
display: flex;
align-items: center;
height: 100%;
`
const reveal = keyframes`
@@ -70,23 +15,9 @@ const reveal = keyframes`
}
`
export const StartWrapper = styled.div`
animation: ${reveal} 0.4s ease forwards;
height: calc(100vh - 300px);
position: relative;
`
export const Wrapper = styled.div`
display: flex;
flex-direction: row;
gap: 20px;
width: auto;
`
export const UnorderList = styled.ul`
export const StudentList = styled.ul`
padding-left: 0px;
height: 600px;
/* overflow: auto; */
justify-content: space-evenly;
padding-right: 20px;
display: flex;
@@ -95,65 +26,6 @@ export const UnorderList = styled.ul`
gap: 8px;
`
export const Svg = styled.svg`
position: absolute;
top: 50%;
left: 50%;
transform-origin: 50% 50%;
transform: translate(-50%, -50%);
`
export const Papper = styled.div`
position: relative;
background-color: #ffffff;
border-radius: 12px;
padding: 32px 16px 16px;
box-shadow: 2px 2px 6px #0000005c;
`
export const LessonItem = styled.li<{ warn?: boolean }>`
list-style: none;
background-color: #ffffff;
padding: 16px;
border-radius: 12px;
box-shadow: 2px 2px 6px #0000005c;
margin-bottom: 12px;
transition: all 0.5;
position: relative;
/* padding-bottom: 32px; */
width: 180px;
max-height: 200px;
${(props) =>
props.warn
? css`
background-color: #000000;
opacity: .7;
color: #e4e4e4;
`
: ''}
`
export const AddMissedButton = styled.button`
position: absolute;
bottom: 8px;
right: 12px;
border: none;
background-color: #00000000;
opacity: 0.2;
:hover {
cursor: pointer;
opacity: 1;
}
`
export const Lessonname = styled.span`
display: inline-box;
margin-right: 12px;
margin-bottom: 20px;
`
export const QRCanvas = styled.canvas`
display: block;
`
@@ -165,35 +37,3 @@ export const ErrorSpan = styled.span`
background-color: #d32f0b;
border-radius: 11px;
`
export const Cross = styled.button`
position: absolute;
right: 19px;
top: 14px;
font-size: 24px;
padding: 7px;
cursor: pointer;
background-color: #fff;
border: none;
:hover {
background-color: #d7181812;
border-radius: 20px;
}
`
export const AddButton = styled.button`
background-color: transparent;
border: none;
cursor: pointer;
:hover {
box-shadow: 3px 2px 5px #00000038;
}
`
export const Avatar = styled.img`
width: 96px;
height: 96px;
margin: 0 auto;
`;

View File

@@ -1,15 +1,20 @@
import React from 'react'
import { useParams } from 'react-router-dom'
import {
ErrorSpan,
LessonItem,
Lessonname,
MainWrapper,
StartWrapper,
} from './style'
import { api } from '../__data__/api/api'
import dayjs from 'dayjs'
import {
Alert,
AlertIcon,
Box,
Center,
Container,
Heading,
Spinner,
Text,
Stack,
} from '@chakra-ui/react'
import { UserCard } from '../components/user-card'
const UserPage = () => {
const { lessonId, accessId } = useParams()
@@ -20,38 +25,67 @@ const UserPage = () => {
skipPollingIfUnfocused: true,
})
if (acc.isLoading) {
return (
<Container maxW="container.xl">
<Center h="300px">
<Spinner
thickness="4px"
speed="0.65s"
emptyColor="gray.200"
color="blue.500"
size="xl"
/>
</Center>
</Container>
)
}
return (
<MainWrapper>
<StartWrapper>
{acc.isLoading && <h1>Отправляем запрос</h1>}
{acc.isSuccess && <h1>Успешно</h1>}
<Container>
{acc.isLoading && <h1>Отправляем запрос</h1>}
{acc.isSuccess && <h1>Успешно</h1>}
{acc.error && (
<Box mb="6" mt="2">
<Alert status="warning">
<AlertIcon />
{(acc as any).error?.data?.body?.errorMessage ===
'Code is expired' ? (
'Не удалось активировать код доступа. Попробуйте отсканировать код ещё раз'
) : (
<pre>{JSON.stringify(acc.error, null, 4)}</pre>
)}
</Alert>
</Box>
)}
<Box mb={6}>
<Text fontSize={18} fontWeight={600} as="h1" mt="4" mb="3">
Тема занятия: {ls.data?.body?.name}
</Text>
{acc.error && (
<div style={{ padding: 12, marginTop: 24, fontSize: 36 }}>
<ErrorSpan>
{(acc as any).error?.data?.body?.errorMessage ===
'Code is expired' ? (
'Не удалось активировать код доступа. Попробуйте отсканировать код ещё раз'
) : (
<pre>{JSON.stringify(acc.error, null, 4)}</pre>
)}
</ErrorSpan>
</div>
)}
<h1>Тема занятия - {ls.data?.body?.name}</h1>
<span>{dayjs(ls.data?.body?.date).format('DD MMMM YYYYг.')}</span>
</Box>
<ul style={{ paddingLeft: 0 }}>
{ls.data?.body?.students?.map((student, index) => (
<LessonItem key={index}>
<Lessonname>
{student.name || student.preferred_username}
</Lessonname>
</LessonItem>
))}
</ul>
</StartWrapper>
</MainWrapper>
<Box
as="ul"
display="flex"
flexWrap="wrap"
justifyContent="center"
gap={3}
>
{ls.data?.body?.students?.map((student) => (
<UserCard
width="40%"
wrapperAS="li"
key={student.sub}
student={student}
present
/>
))}
</Box>
</Container>
)
}