Emojy reactions
This commit is contained in:
parent
c02cf6dfc9
commit
b2121cc133
@ -89,6 +89,16 @@
|
|||||||
"journal.pl.lesson.aiGenerationError": "Error generating AI suggestions",
|
"journal.pl.lesson.aiGenerationError": "Error generating AI suggestions",
|
||||||
"journal.pl.lesson.tryAgainLater": "An error occurred while generating lesson suggestions. Please try again later.",
|
"journal.pl.lesson.tryAgainLater": "An error occurred while generating lesson suggestions. Please try again later.",
|
||||||
"journal.pl.lesson.retryGeneration": "Retry Generation",
|
"journal.pl.lesson.retryGeneration": "Retry Generation",
|
||||||
|
"journal.pl.lesson.reactions": "Reactions to the lesson:",
|
||||||
|
"journal.pl.lesson.noStudents": "No Students Yet",
|
||||||
|
"journal.pl.lesson.waitForStudents": "Students who attend the lesson will appear here",
|
||||||
|
"journal.pl.lesson.notMarked": "Not yet marked",
|
||||||
|
|
||||||
|
"journal.pl.reactions.thumbs_up": "Thumbs up",
|
||||||
|
"journal.pl.reactions.heart": "Heart",
|
||||||
|
"journal.pl.reactions.laugh": "Laugh",
|
||||||
|
"journal.pl.reactions.wow": "Wow",
|
||||||
|
"journal.pl.reactions.clap": "Clap",
|
||||||
|
|
||||||
"journal.pl.exam.title": "Exam",
|
"journal.pl.exam.title": "Exam",
|
||||||
"journal.pl.exam.startExam": "Start exam",
|
"journal.pl.exam.startExam": "Start exam",
|
||||||
|
@ -86,6 +86,16 @@
|
|||||||
"journal.pl.lesson.aiGenerationError": "Ошибка генерации рекомендаций ИИ",
|
"journal.pl.lesson.aiGenerationError": "Ошибка генерации рекомендаций ИИ",
|
||||||
"journal.pl.lesson.tryAgainLater": "Произошла ошибка при генерации рекомендаций для занятий. Пожалуйста, попробуйте позже.",
|
"journal.pl.lesson.tryAgainLater": "Произошла ошибка при генерации рекомендаций для занятий. Пожалуйста, попробуйте позже.",
|
||||||
"journal.pl.lesson.retryGeneration": "Повторить генерацию",
|
"journal.pl.lesson.retryGeneration": "Повторить генерацию",
|
||||||
|
"journal.pl.lesson.reactions": "Реакции на занятие:",
|
||||||
|
"journal.pl.lesson.noStudents": "Пока нет студентов",
|
||||||
|
"journal.pl.lesson.waitForStudents": "Студенты, посетившие занятие, появятся здесь",
|
||||||
|
"journal.pl.lesson.notMarked": "Не отмечен",
|
||||||
|
|
||||||
|
"journal.pl.reactions.thumbs_up": "Палец вверх",
|
||||||
|
"journal.pl.reactions.heart": "Сердце",
|
||||||
|
"journal.pl.reactions.laugh": "Смех",
|
||||||
|
"journal.pl.reactions.wow": "Вау",
|
||||||
|
"journal.pl.reactions.clap": "Аплодисменты",
|
||||||
|
|
||||||
"journal.pl.exam.title": "Экзамен",
|
"journal.pl.exam.title": "Экзамен",
|
||||||
"journal.pl.exam.startExam": "Начать экзамен",
|
"journal.pl.exam.startExam": "Начать экзамен",
|
||||||
|
@ -122,6 +122,15 @@ export const api = createApi({
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
sendReaction: builder.mutation<void, { lessonId: string; reaction: string }>({
|
||||||
|
query: ({ lessonId, reaction }) => ({
|
||||||
|
url: `/lesson/reaction/${lessonId}`,
|
||||||
|
method: 'POST',
|
||||||
|
body: { reaction },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
getCourseById: builder.query<PopulatedCourse, string>({
|
getCourseById: builder.query<PopulatedCourse, string>({
|
||||||
query: (courseId) => `/course/${courseId}`,
|
query: (courseId) => `/course/${courseId}`,
|
||||||
transformResponse: (response: BaseResponse<PopulatedCourse>) => response.body,
|
transformResponse: (response: BaseResponse<PopulatedCourse>) => response.body,
|
||||||
|
@ -49,10 +49,17 @@ export type BaseResponse<Data> = {
|
|||||||
body: Data;
|
body: Data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface Reaction {
|
||||||
|
_id: string;
|
||||||
|
sub: string;
|
||||||
|
reaction: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Lesson {
|
export interface Lesson {
|
||||||
id: string;
|
id: string;
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
reactions: Reaction[];
|
||||||
students: User[];
|
students: User[];
|
||||||
teachers: Teacher[];
|
teachers: Teacher[];
|
||||||
date: string;
|
date: string;
|
||||||
|
@ -53,7 +53,7 @@ export const NameOverlay = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
// Стили без интерполяций компонентов
|
// Стили без интерполяций компонентов
|
||||||
export const Wrapper = styled.div<{ warn?: boolean; width?: string | number }>`
|
export const Wrapper = styled.div<{ warn?: boolean; width?: string | number; position?: string }>`
|
||||||
list-style: none;
|
list-style: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -98,6 +98,13 @@ export const Wrapper = styled.div<{ warn?: boolean; width?: string | number }>`
|
|||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
|
${({ position }) =>
|
||||||
|
position
|
||||||
|
? css`
|
||||||
|
position: ${position};
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.warn
|
props.warn
|
||||||
? css`
|
? css`
|
||||||
|
@ -1,13 +1,24 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { sha256 } from 'js-sha256'
|
import { sha256 } from 'js-sha256'
|
||||||
import { useState } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { Box, useColorMode } from '@chakra-ui/react'
|
import { Box, useColorMode, Text } from '@chakra-ui/react'
|
||||||
import { CheckCircleIcon, AddIcon } from '@chakra-ui/icons'
|
import { CheckCircleIcon, AddIcon } from '@chakra-ui/icons'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { User } from '../../__data__/model'
|
import { User, Reaction } from '../../__data__/model'
|
||||||
|
|
||||||
import { AddMissedButton, Avatar, Wrapper, NameOverlay } from './style'
|
import { AddMissedButton, Avatar, Wrapper, NameOverlay } from './style'
|
||||||
|
|
||||||
|
// Map of reaction types to emojis
|
||||||
|
const REACTION_EMOJIS = {
|
||||||
|
thumbs_up: '👍',
|
||||||
|
heart: '❤️',
|
||||||
|
laugh: '😂',
|
||||||
|
wow: '😮',
|
||||||
|
clap: '👏'
|
||||||
|
}
|
||||||
|
|
||||||
export function getGravatarURL(email, user) {
|
export function getGravatarURL(email, user) {
|
||||||
if (!email) return void 0
|
if (!email) return void 0
|
||||||
const address = String(email).trim().toLowerCase()
|
const address = String(email).trim().toLowerCase()
|
||||||
@ -22,7 +33,8 @@ export const UserCard = ({
|
|||||||
onAddUser = undefined,
|
onAddUser = undefined,
|
||||||
wrapperAS = 'div',
|
wrapperAS = 'div',
|
||||||
width,
|
width,
|
||||||
recentlyPresent = false
|
recentlyPresent = false,
|
||||||
|
reactions = []
|
||||||
}: {
|
}: {
|
||||||
student: User
|
student: User
|
||||||
present: boolean
|
present: boolean
|
||||||
@ -30,9 +42,50 @@ export const UserCard = ({
|
|||||||
onAddUser?: (user: User) => void
|
onAddUser?: (user: User) => void
|
||||||
wrapperAS?: React.ElementType<any, keyof React.JSX.IntrinsicElements>;
|
wrapperAS?: React.ElementType<any, keyof React.JSX.IntrinsicElements>;
|
||||||
recentlyPresent?: boolean
|
recentlyPresent?: boolean
|
||||||
|
reactions?: Reaction[]
|
||||||
}) => {
|
}) => {
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const [visibleReactions, setVisibleReactions] = useState<Reaction[]>([]);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Filter reactions to only show this student's reactions
|
||||||
|
useEffect(() => {
|
||||||
|
const studentReactions = reactions.filter(r => r.sub === student.sub);
|
||||||
|
|
||||||
|
if (studentReactions.length > 0) {
|
||||||
|
// Check for new reactions
|
||||||
|
const newReactions = studentReactions.filter(
|
||||||
|
newReaction => !visibleReactions.some(
|
||||||
|
existingReaction => existingReaction._id === newReaction._id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newReactions.length > 0) {
|
||||||
|
// If there are new reactions, add them to visible reactions
|
||||||
|
setVisibleReactions(prevReactions => [...prevReactions, ...newReactions]);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a new timeout
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
setVisibleReactions([]);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up on unmount
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [reactions, student.sub, visibleReactions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
@ -40,6 +93,7 @@ export const UserCard = ({
|
|||||||
as={wrapperAS}
|
as={wrapperAS}
|
||||||
width={width}
|
width={width}
|
||||||
className={!present ? 'warn' : recentlyPresent ? 'recent' : ''}
|
className={!present ? 'warn' : recentlyPresent ? 'recent' : ''}
|
||||||
|
position="relative"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={imageError ? getGravatarURL(student.email, null) : (student.picture || getGravatarURL(student.email, null))}
|
src={imageError ? getGravatarURL(student.email, null) : (student.picture || getGravatarURL(student.email, null))}
|
||||||
@ -58,10 +112,52 @@ export const UserCard = ({
|
|||||||
)}
|
)}
|
||||||
</NameOverlay>
|
</NameOverlay>
|
||||||
{onAddUser && !present && (
|
{onAddUser && !present && (
|
||||||
<AddMissedButton onClick={() => onAddUser(student)} aria-label="Отметить присутствие">
|
<AddMissedButton onClick={() => onAddUser(student)} aria-label={t('journal.pl.common.add')}>
|
||||||
<AddIcon boxSize={3} />
|
<AddIcon boxSize={3} />
|
||||||
</AddMissedButton>
|
</AddMissedButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Student reactions animation */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{visibleReactions.map((reaction, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={reaction._id || index}
|
||||||
|
initial={{ opacity: 0, scale: 0.5, x: 0, y: 0 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.5, y: -20 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.5,
|
||||||
|
delay: index * 0.1
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '10px', // Position at the top
|
||||||
|
right: '10px', // Position at the right
|
||||||
|
zIndex: 10,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: colorMode === 'light' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.6)',
|
||||||
|
borderRadius: '50%',
|
||||||
|
padding: '2px',
|
||||||
|
boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
|
||||||
|
}}
|
||||||
|
title={t(`journal.pl.reactions.${reaction.reaction}`)}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fontSize="3xl" // Increased size
|
||||||
|
sx={{
|
||||||
|
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))',
|
||||||
|
transform: 'scale(1.2)', // Additional scaling
|
||||||
|
display: 'flex'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{REACTION_EMOJIS[reaction.reaction] || reaction.reaction}
|
||||||
|
</Text>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { api } from '../__data__/api/api'
|
import { api } from '../__data__/api/api'
|
||||||
import { User } from '../__data__/model'
|
import { User, Reaction } from '../__data__/model'
|
||||||
import { UserCard } from '../components/user-card'
|
import { UserCard } from '../components/user-card'
|
||||||
import { formatDate } from '../utils/dayjs-config'
|
import { formatDate } from '../utils/dayjs-config'
|
||||||
import { useSetBreadcrumbs } from '../components'
|
import { useSetBreadcrumbs } from '../components'
|
||||||
@ -71,6 +71,10 @@ const LessonDetail = () => {
|
|||||||
const [isPulsing, setIsPulsing] = useState(false)
|
const [isPulsing, setIsPulsing] = useState(false)
|
||||||
// Отслеживаем предыдущее количество студентов
|
// Отслеживаем предыдущее количество студентов
|
||||||
const prevStudentCountRef = useRef(0)
|
const prevStudentCountRef = useRef(0)
|
||||||
|
// Отслеживаем предыдущие реакции для определения новых
|
||||||
|
const prevReactionsRef = useRef<Record<string, Reaction[]>>({})
|
||||||
|
// Храним актуальные реакции студентов
|
||||||
|
const [studentReactions, setStudentReactions] = useState<Record<string, Reaction[]>>({})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isFetching,
|
isFetching,
|
||||||
@ -121,6 +125,44 @@ const LessonDetail = () => {
|
|||||||
}
|
}
|
||||||
}, [accessCode])
|
}, [accessCode])
|
||||||
|
|
||||||
|
// Эффект для обработки новых реакций
|
||||||
|
useEffect(() => {
|
||||||
|
if (accessCode?.body?.lesson?.reactions) {
|
||||||
|
const reactions = accessCode.body.lesson.reactions;
|
||||||
|
|
||||||
|
// Группируем реакции по sub (идентификатору студента)
|
||||||
|
const groupedReactions: Record<string, Reaction[]> = {};
|
||||||
|
|
||||||
|
reactions.forEach(reaction => {
|
||||||
|
if (!groupedReactions[reaction.sub]) {
|
||||||
|
groupedReactions[reaction.sub] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем только новые реакции
|
||||||
|
const isNewReaction = !prevReactionsRef.current[reaction.sub]?.some(
|
||||||
|
r => r._id === reaction._id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNewReaction) {
|
||||||
|
groupedReactions[reaction.sub].push(reaction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем отображаемые реакции
|
||||||
|
setStudentReactions(groupedReactions);
|
||||||
|
|
||||||
|
// Обновляем предыдущие реакции
|
||||||
|
prevReactionsRef.current = { ...groupedReactions };
|
||||||
|
|
||||||
|
// Сбрасываем отображаемые реакции через некоторое время
|
||||||
|
const clearReactionsTimeout = setTimeout(() => {
|
||||||
|
setStudentReactions({});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearTimeout(clearReactionsTimeout);
|
||||||
|
}
|
||||||
|
}, [accessCode?.body?.lesson?.reactions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (manualAddRqst.isSuccess) {
|
if (manualAddRqst.isSuccess) {
|
||||||
refetch()
|
refetch()
|
||||||
@ -348,6 +390,7 @@ const LessonDetail = () => {
|
|||||||
present={student.present}
|
present={student.present}
|
||||||
recentlyPresent={student.recentlyPresent}
|
recentlyPresent={student.recentlyPresent}
|
||||||
onAddUser={(user: User) => manualAdd({ lessonId, user })}
|
onAddUser={(user: User) => manualAdd({ lessonId, user })}
|
||||||
|
reactions={studentReactions[student.sub] || []}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
@ -17,17 +17,31 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Flex,
|
Flex,
|
||||||
useColorMode,
|
useColorMode,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
HStack,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { UserCard } from '../components/user-card'
|
import { UserCard } from '../components/user-card'
|
||||||
import { StudentListView } from './style'
|
import { StudentListView } from './style'
|
||||||
import { useSetBreadcrumbs } from '../components'
|
import { useSetBreadcrumbs } from '../components'
|
||||||
|
|
||||||
|
// Reaction emojis with their string values
|
||||||
|
const REACTIONS = [
|
||||||
|
{ emoji: '👍', value: 'thumbs_up' },
|
||||||
|
{ emoji: '❤️', value: 'heart' },
|
||||||
|
{ emoji: '😂', value: 'laugh' },
|
||||||
|
{ emoji: '😮', value: 'wow' },
|
||||||
|
{ emoji: '👏', value: 'clap' },
|
||||||
|
]
|
||||||
|
|
||||||
const UserPage = () => {
|
const UserPage = () => {
|
||||||
const { lessonId, accessId } = useParams()
|
const { lessonId, accessId } = useParams()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { colorMode } = useColorMode()
|
const { colorMode } = useColorMode()
|
||||||
const acc = api.useGetAccessQuery({ accessCode: accessId })
|
const acc = api.useGetAccessQuery({ accessCode: accessId })
|
||||||
const [animatedStudents, setAnimatedStudents] = useState([])
|
const [animatedStudents, setAnimatedStudents] = useState([])
|
||||||
|
const [sendReaction] = api.useSendReactionMutation()
|
||||||
|
const [activeReaction, setActiveReaction] = useState(null)
|
||||||
|
|
||||||
const ls = api.useLessonByIdQuery(lessonId, {
|
const ls = api.useLessonByIdQuery(lessonId, {
|
||||||
pollingInterval: 1000,
|
pollingInterval: 1000,
|
||||||
@ -85,6 +99,19 @@ const UserPage = () => {
|
|||||||
}
|
}
|
||||||
}, [animatedStudents])
|
}, [animatedStudents])
|
||||||
|
|
||||||
|
// Обработчик отправки реакции
|
||||||
|
const handleReaction = (reaction) => {
|
||||||
|
if (lessonId) {
|
||||||
|
sendReaction({ lessonId, reaction })
|
||||||
|
setActiveReaction(reaction)
|
||||||
|
|
||||||
|
// Сбрасываем активную реакцию через 1 секунду
|
||||||
|
setTimeout(() => {
|
||||||
|
setActiveReaction(null)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (acc.isLoading) {
|
if (acc.isLoading) {
|
||||||
return (
|
return (
|
||||||
<Container maxW="container.xl">
|
<Container maxW="container.xl">
|
||||||
@ -168,6 +195,49 @@ const UserPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Реакции на занятие */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
mb={6}
|
||||||
|
p={5}
|
||||||
|
borderRadius="xl"
|
||||||
|
bg={colorMode === "light" ? "gray.50" : "gray.700"}
|
||||||
|
boxShadow="md"
|
||||||
|
>
|
||||||
|
<Text mb={3} fontWeight="medium">{t('journal.pl.lesson.reactions')}</Text>
|
||||||
|
|
||||||
|
<HStack spacing={3} justify="center">
|
||||||
|
{REACTIONS.map((reaction) => (
|
||||||
|
<Tooltip key={reaction.value} label={t(`journal.pl.reactions.${reaction.value}`)} placement="top">
|
||||||
|
<IconButton
|
||||||
|
aria-label={t(`journal.pl.reactions.${reaction.value}`)}
|
||||||
|
icon={<Text fontSize="24px">{reaction.emoji}</Text>}
|
||||||
|
size="lg"
|
||||||
|
variant={activeReaction === reaction.value ? "solid" : "outline"}
|
||||||
|
colorScheme={activeReaction === reaction.value ? "blue" : "gray"}
|
||||||
|
onClick={() => handleReaction(reaction.value)}
|
||||||
|
transition="all 0.2s"
|
||||||
|
_hover={{ transform: "scale(1.1)" }}
|
||||||
|
sx={{
|
||||||
|
animation: activeReaction === reaction.value
|
||||||
|
? "pulse 0.5s ease-in-out" : "none",
|
||||||
|
"@keyframes pulse": {
|
||||||
|
"0%": { transform: "scale(1)" },
|
||||||
|
"50%": { transform: "scale(1.2)" },
|
||||||
|
"100%": { transform: "scale(1)" }
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
@ -207,6 +277,7 @@ const UserPage = () => {
|
|||||||
student={student}
|
student={student}
|
||||||
present={true}
|
present={true}
|
||||||
recentlyPresent={student.isNew}
|
recentlyPresent={student.isNew}
|
||||||
|
reactions={ls.data?.body?.reactions?.filter(r => r.sub === student.sub) || []}
|
||||||
/>
|
/>
|
||||||
</motion.li>
|
</motion.li>
|
||||||
))}
|
))}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const path = require('node:path')
|
const path = require('node:path')
|
||||||
|
const mockGenerator = require('./mock-generator')
|
||||||
|
|
||||||
const timer =
|
const timer =
|
||||||
(time = 1000) =>
|
(time = 1000) =>
|
||||||
@ -56,19 +57,21 @@ router.post('/lesson', (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.post('/lesson/access-code', (req, res) => {
|
router.post('/lesson/access-code', (req, res) => {
|
||||||
const answer = fs.readFileSync(
|
// Generate random students and reactions dynamically
|
||||||
path.resolve(__dirname, '../mocks/lessons/access-code/create/success.json'),
|
const dynamicData = mockGenerator.generateDynamicAccessCodeResponse();
|
||||||
)
|
res.send(dynamicData);
|
||||||
// res.send(require('../mocks/lessons/access-code/create/success.json'))
|
|
||||||
res.send(answer)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/lesson/access-code/:accessCode', (req, res) => {
|
router.get('/lesson/access-code/:accessCode', (req, res) => {
|
||||||
res.status(400).send(require('../mocks/lessons/access-code/get/error.json'))
|
// Generate dynamic data for the access code lookup
|
||||||
|
const dynamicData = mockGenerator.generateDynamicAccessLookupResponse(req.params.accessCode);
|
||||||
|
res.send(dynamicData);
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/lesson/:lessonId', (req, res) => {
|
router.get('/lesson/:lessonId', (req, res) => {
|
||||||
res.send(require('../mocks/lessons/byid/success.json'))
|
// Generate dynamic lesson data using the same helpers
|
||||||
|
const dynamicData = mockGenerator.generateDynamicLessonResponse(req.params.lessonId);
|
||||||
|
res.send(dynamicData);
|
||||||
})
|
})
|
||||||
|
|
||||||
router.delete('/lesson/:lessonId', (req, res) => {
|
router.delete('/lesson/:lessonId', (req, res) => {
|
||||||
@ -79,4 +82,291 @@ router.put('/lesson', (req, res) => {
|
|||||||
res.send({ success: true, body: req.body })
|
res.send({ success: true, body: req.body })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/lesson/reaction/:lessonId', (req, res) => {
|
||||||
|
// Simulate processing a new reaction
|
||||||
|
const { reaction } = req.body;
|
||||||
|
const lessonId = req.params.lessonId;
|
||||||
|
|
||||||
|
// Log the reaction for debugging
|
||||||
|
console.log(`Received reaction "${reaction}" for lesson ${lessonId}`);
|
||||||
|
|
||||||
|
// Return success response
|
||||||
|
res.send({
|
||||||
|
success: true,
|
||||||
|
body: {
|
||||||
|
_id: `r-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
||||||
|
reaction,
|
||||||
|
lessonId,
|
||||||
|
created: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
||||||
|
// Database of potential students
|
||||||
|
const potentialStudents = [
|
||||||
|
{
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Мария Капитанова",
|
||||||
|
"preferred_username": "maryaKapitan@gmail.com",
|
||||||
|
"given_name": "Мария",
|
||||||
|
"family_name": "Капитанова",
|
||||||
|
"email": "maryaKapitan@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJgIjjOFD2YUSyRF5kH4jaysE6X5p-kq0Cg0CFncfMi=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Евгения Жужова",
|
||||||
|
"preferred_username": "zhuzhova@gmail.com",
|
||||||
|
"given_name": "Евгения",
|
||||||
|
"family_name": "Жужова",
|
||||||
|
"email": "zhuzhova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJUtJBAVBm642AxoGpMDDMV8CPu3MEoLjU3hmO7oisG=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "a723b8c2-f1d7-4620-9c35-1d48c821afb7",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Иван Петров",
|
||||||
|
"preferred_username": "ivan.petrov@gmail.com",
|
||||||
|
"given_name": "Иван",
|
||||||
|
"family_name": "Петров",
|
||||||
|
"email": "ivan.petrov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJKHmMLFXY1s0Lkj_KKf9ZEsHl-rW6FnDs4vPHUl2aF=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "e4f9d328-7b2e-49c1-b5e8-12f78c54a63d",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Алексей Смирнов",
|
||||||
|
"preferred_username": "alexey.smirnov@gmail.com",
|
||||||
|
"given_name": "Алексей",
|
||||||
|
"family_name": "Смирнов",
|
||||||
|
"email": "alexey.smirnov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocK9Nfj_jT4DLjG5hVQWS2bz8_QTZ3cHVJ6K8mD8aqWr=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "b9d7e1f5-6a3c-47d0-9bce-3c54e28a0ef2",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Ольга Иванова",
|
||||||
|
"preferred_username": "olga.ivanova@gmail.com",
|
||||||
|
"given_name": "Ольга",
|
||||||
|
"family_name": "Иванова",
|
||||||
|
"email": "olga.ivanova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocI48DY7C2ZXbMvHrEjKmY6w9JdF5PLKwEDgTR9x1jY2=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "c5e8d4f3-2b1a-4c9d-8e7f-6a5b4c3d2e1f",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Дмитрий Кузнецов",
|
||||||
|
"preferred_username": "dmitry.kuznetsov@gmail.com",
|
||||||
|
"given_name": "Дмитрий",
|
||||||
|
"family_name": "Кузнецов",
|
||||||
|
"email": "dmitry.kuznetsov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLqZD7KjXy3B1P2VsRn6Z9tY8XMhCJ6F5gK7sD1qB3t=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "d6f9e8d7-3c2b-4a1d-9e8f-7a6b5c4d3e2f",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Анна Соколова",
|
||||||
|
"preferred_username": "anna.sokolova@gmail.com",
|
||||||
|
"given_name": "Анна",
|
||||||
|
"family_name": "Соколова",
|
||||||
|
"email": "anna.sokolova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocK3dN5mYwLjE1qFvX9pZ8rY1hJ5L2mN3oP6gR7tUb4s=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "e7f8g9h0-4d3c-2b1a-0f9e-8d7c6b5a4e3d",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Сергей Новиков",
|
||||||
|
"preferred_username": "sergey.novikov@gmail.com",
|
||||||
|
"given_name": "Сергей",
|
||||||
|
"family_name": "Новиков",
|
||||||
|
"email": "sergey.novikov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocI7P2dF3vQ5wR9jH6tN8bZ1cM4kD6yL2jN5oR8tYb5r=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "f8g9h0i1-5e4d-3c2b-1a0f-9e8d7c6b5a4e",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Екатерина Морозова",
|
||||||
|
"preferred_username": "ekaterina.morozova@gmail.com",
|
||||||
|
"given_name": "Екатерина",
|
||||||
|
"family_name": "Морозова",
|
||||||
|
"email": "ekaterina.morozova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJ6N5oR7sD8tUb4rY1hJ5L2mN3oP6gR7tUb4s9pZ8=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "g9h0i1j2-6f5e-4d3c-2b1a-0f9e8d7c6b5a",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Андрей Волков",
|
||||||
|
"preferred_username": "andrey.volkov@gmail.com",
|
||||||
|
"given_name": "Андрей",
|
||||||
|
"family_name": "Волков",
|
||||||
|
"email": "andrey.volkov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocK4e3d2c1b0a9f8e7d6c5b4a3e2d1c0b9a8f7e6d5=s96-c"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Available reaction types
|
||||||
|
const reactionTypes = ['thumbs_up', 'heart', 'laugh', 'wow', 'clap'];
|
||||||
|
|
||||||
|
// Function to generate a random integer between min and max (inclusive)
|
||||||
|
function getRandomInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate a random subset of students
|
||||||
|
function getRandomStudents() {
|
||||||
|
const totalStudents = potentialStudents.length;
|
||||||
|
const count = getRandomInt(1, Math.min(8, totalStudents));
|
||||||
|
|
||||||
|
// Shuffle array and take a subset
|
||||||
|
const shuffled = [...potentialStudents].sort(() => 0.5 - Math.random());
|
||||||
|
return shuffled.slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate a random reaction
|
||||||
|
function generateReaction(studentSub, index) {
|
||||||
|
const reactionType = reactionTypes[getRandomInt(0, reactionTypes.length - 1)];
|
||||||
|
|
||||||
|
return {
|
||||||
|
"_id": `r-${Date.now()}-${index}`,
|
||||||
|
"sub": studentSub,
|
||||||
|
"reaction": reactionType,
|
||||||
|
"created": new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate random reactions for each student
|
||||||
|
function generateReactions(students) {
|
||||||
|
const reactions = [];
|
||||||
|
let reactionIndex = 0;
|
||||||
|
|
||||||
|
students.forEach(student => {
|
||||||
|
// Small chance (20%) of a "reaction burst" - multiple reactions in rapid succession
|
||||||
|
const hasBurst = Math.random() < 0.2;
|
||||||
|
|
||||||
|
if (hasBurst) {
|
||||||
|
// Generate a burst of 2-5 rapid reactions
|
||||||
|
const burstCount = getRandomInt(2, 5);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < burstCount; i++) {
|
||||||
|
// Reactions spaced 0.5-2 seconds apart
|
||||||
|
const timeOffset = i * getRandomInt(500, 2000);
|
||||||
|
const reactionTime = new Date(now - timeOffset);
|
||||||
|
|
||||||
|
reactions.push({
|
||||||
|
"_id": `r-burst-${now}-${i}-${reactionIndex++}`,
|
||||||
|
"sub": student.sub,
|
||||||
|
"reaction": reactionTypes[getRandomInt(0, reactionTypes.length - 1)],
|
||||||
|
"created": reactionTime.toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Each student may have 0-3 random reactions
|
||||||
|
const reactionCount = getRandomInt(0, 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < reactionCount; i++) {
|
||||||
|
// Space out regular reactions by 5-30 seconds
|
||||||
|
const timeOffset = getRandomInt(5000, 30000);
|
||||||
|
const reactionTime = new Date(Date.now() - timeOffset);
|
||||||
|
|
||||||
|
reactions.push({
|
||||||
|
"_id": `r-${Date.now()}-${reactionIndex++}`,
|
||||||
|
"sub": student.sub,
|
||||||
|
"reaction": reactionTypes[getRandomInt(0, reactionTypes.length - 1)],
|
||||||
|
"created": reactionTime.toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort reactions by creation time (newest first)
|
||||||
|
return reactions.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate the entire dynamic response
|
||||||
|
function generateDynamicAccessCodeResponse() {
|
||||||
|
// Base template from the static file
|
||||||
|
const baseTemplate = {
|
||||||
|
"success": true,
|
||||||
|
"body": {
|
||||||
|
"expires": new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour from now
|
||||||
|
"lesson": {
|
||||||
|
"_id": "65df996c584b172772d69706",
|
||||||
|
"name": "ВВОДНАЯ ПО JS.ПРИМЕНЕНИЕ И СПОСОБЫ ПОДКЛЮЧЕНИЯ НА СТРАНИЦЕ. LET, CONST. БАЗОВЫЕ ТИПЫ ДАННЫХ, ПРИВЕДЕНИЕ ТИПОВ. ПЕРЕМЕННЫЕ, ОБЛАСТЬ ВИДИМОСТИ ПЕРЕМЕННЫХ",
|
||||||
|
"date": new Date().toISOString(),
|
||||||
|
"created": new Date().toISOString(),
|
||||||
|
"__v": 0
|
||||||
|
},
|
||||||
|
"_id": `access-${Date.now()}`,
|
||||||
|
"created": new Date().toISOString(),
|
||||||
|
"__v": 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate random students
|
||||||
|
const students = getRandomStudents();
|
||||||
|
baseTemplate.body.lesson.students = students;
|
||||||
|
|
||||||
|
// Generate random reactions for those students
|
||||||
|
baseTemplate.body.lesson.reactions = generateReactions(students);
|
||||||
|
|
||||||
|
return baseTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate a dynamic lesson response
|
||||||
|
function generateDynamicLessonResponse(lessonId) {
|
||||||
|
// Base template for lesson response
|
||||||
|
const baseTemplate = {
|
||||||
|
"success": true,
|
||||||
|
"body": {
|
||||||
|
"_id": lessonId || "65df996c584b172772d69706",
|
||||||
|
"name": "ВВОДНАЯ ПО JS.ПРИМЕНЕНИЕ И СПОСОБЫ ПОДКЛЮЧЕНИЯ НА СТРАНИЦЕ. LET, CONST. БАЗОВЫЕ ТИПЫ ДАННЫХ, ПРИВЕДЕНИЕ ТИПОВ. ПЕРЕМЕННЫЕ, ОБЛАСТЬ ВИДИМОСТИ ПЕРЕМЕННЫХ",
|
||||||
|
"date": new Date().toISOString(),
|
||||||
|
"created": new Date().toISOString(),
|
||||||
|
"__v": 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate random students
|
||||||
|
const students = getRandomStudents();
|
||||||
|
baseTemplate.body.students = students;
|
||||||
|
|
||||||
|
// Generate random reactions for those students
|
||||||
|
baseTemplate.body.reactions = generateReactions(students);
|
||||||
|
|
||||||
|
return baseTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate a dynamic access code lookup response
|
||||||
|
function generateDynamicAccessLookupResponse(accessCode) {
|
||||||
|
// Generate a lesson with students and reactions
|
||||||
|
const lessonData = generateDynamicLessonResponse();
|
||||||
|
|
||||||
|
// Create a mock user
|
||||||
|
const mockUser = {
|
||||||
|
sub: `user-${Date.now()}`,
|
||||||
|
email_verified: true,
|
||||||
|
name: "Текущий Пользователь",
|
||||||
|
preferred_username: "current.user@example.com",
|
||||||
|
email: "current.user@example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combine into the expected format
|
||||||
|
return {
|
||||||
|
"success": true,
|
||||||
|
"body": {
|
||||||
|
"user": mockUser,
|
||||||
|
"accessCode": {
|
||||||
|
"expires": new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
||||||
|
"lesson": lessonData.body,
|
||||||
|
"_id": accessCode || `access-${Date.now()}`,
|
||||||
|
"created": new Date().toISOString(),
|
||||||
|
"__v": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
264
stubs/api/mock-generator.js
Normal file
264
stubs/api/mock-generator.js
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
// Database of potential students
|
||||||
|
const potentialStudents = [
|
||||||
|
{
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Мария Капитанова",
|
||||||
|
"preferred_username": "maryaKapitan@gmail.com",
|
||||||
|
"given_name": "Мария",
|
||||||
|
"family_name": "Капитанова",
|
||||||
|
"email": "maryaKapitan@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJgIjjOFD2YUSyRF5kH4jaysE6X5p-kq0Cg0CFncfMi=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Евгения Жужова",
|
||||||
|
"preferred_username": "zhuzhova@gmail.com",
|
||||||
|
"given_name": "Евгения",
|
||||||
|
"family_name": "Жужова",
|
||||||
|
"email": "zhuzhova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJUtJBAVBm642AxoGpMDDMV8CPu3MEoLjU3hmO7oisG=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "a723b8c2-f1d7-4620-9c35-1d48c821afb7",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Иван Петров",
|
||||||
|
"preferred_username": "ivan.petrov@gmail.com",
|
||||||
|
"given_name": "Иван",
|
||||||
|
"family_name": "Петров",
|
||||||
|
"email": "ivan.petrov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJKHmMLFXY1s0Lkj_KKf9ZEsHl-rW6FnDs4vPHUl2aF=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "e4f9d328-7b2e-49c1-b5e8-12f78c54a63d",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Алексей Смирнов",
|
||||||
|
"preferred_username": "alexey.smirnov@gmail.com",
|
||||||
|
"given_name": "Алексей",
|
||||||
|
"family_name": "Смирнов",
|
||||||
|
"email": "alexey.smirnov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocK9Nfj_jT4DLjG5hVQWS2bz8_QTZ3cHVJ6K8mD8aqWr=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "b9d7e1f5-6a3c-47d0-9bce-3c54e28a0ef2",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Ольга Иванова",
|
||||||
|
"preferred_username": "olga.ivanova@gmail.com",
|
||||||
|
"given_name": "Ольга",
|
||||||
|
"family_name": "Иванова",
|
||||||
|
"email": "olga.ivanova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocI48DY7C2ZXbMvHrEjKmY6w9JdF5PLKwEDgTR9x1jY2=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "c5e8d4f3-2b1a-4c9d-8e7f-6a5b4c3d2e1f",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Дмитрий Кузнецов",
|
||||||
|
"preferred_username": "dmitry.kuznetsov@gmail.com",
|
||||||
|
"given_name": "Дмитрий",
|
||||||
|
"family_name": "Кузнецов",
|
||||||
|
"email": "dmitry.kuznetsov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLqZD7KjXy3B1P2VsRn6Z9tY8XMhCJ6F5gK7sD1qB3t=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "d6f9e8d7-3c2b-4a1d-9e8f-7a6b5c4d3e2f",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Анна Соколова",
|
||||||
|
"preferred_username": "anna.sokolova@gmail.com",
|
||||||
|
"given_name": "Анна",
|
||||||
|
"family_name": "Соколова",
|
||||||
|
"email": "anna.sokolova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocK3dN5mYwLjE1qFvX9pZ8rY1hJ5L2mN3oP6gR7tUb4s=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "e7f8g9h0-4d3c-2b1a-0f9e-8d7c6b5a4e3d",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Сергей Новиков",
|
||||||
|
"preferred_username": "sergey.novikov@gmail.com",
|
||||||
|
"given_name": "Сергей",
|
||||||
|
"family_name": "Новиков",
|
||||||
|
"email": "sergey.novikov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocI7P2dF3vQ5wR9jH6tN8bZ1cM4kD6yL2jN5oR8tYb5r=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "f8g9h0i1-5e4d-3c2b-1a0f-9e8d7c6b5a4e",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Екатерина Морозова",
|
||||||
|
"preferred_username": "ekaterina.morozova@gmail.com",
|
||||||
|
"given_name": "Екатерина",
|
||||||
|
"family_name": "Морозова",
|
||||||
|
"email": "ekaterina.morozova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJ6N5oR7sD8tUb4rY1hJ5L2mN3oP6gR7tUb4s9pZ8=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "g9h0i1j2-6f5e-4d3c-2b1a-0f9e8d7c6b5a",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Андрей Волков",
|
||||||
|
"preferred_username": "andrey.volkov@gmail.com",
|
||||||
|
"given_name": "Андрей",
|
||||||
|
"family_name": "Волков",
|
||||||
|
"email": "andrey.volkov@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocK4e3d2c1b0a9f8e7d6c5b4a3e2d1c0b9a8f7e6d5=s96-c"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Available reaction types
|
||||||
|
const reactionTypes = ['thumbs_up', 'heart', 'laugh', 'wow', 'clap'];
|
||||||
|
|
||||||
|
// Function to generate a random integer between min and max (inclusive)
|
||||||
|
function getRandomInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate a random subset of students
|
||||||
|
function getRandomStudents() {
|
||||||
|
const totalStudents = potentialStudents.length;
|
||||||
|
const count = getRandomInt(1, Math.min(8, totalStudents));
|
||||||
|
|
||||||
|
// Shuffle array and take a subset
|
||||||
|
const shuffled = [...potentialStudents].sort(() => 0.5 - Math.random());
|
||||||
|
return shuffled.slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate random reactions for each student
|
||||||
|
function generateReactions(students) {
|
||||||
|
const reactions = [];
|
||||||
|
let reactionIndex = 0;
|
||||||
|
|
||||||
|
students.forEach(student => {
|
||||||
|
// Small chance (20%) of a "reaction burst" - multiple reactions in rapid succession
|
||||||
|
const hasBurst = Math.random() < 0.2;
|
||||||
|
|
||||||
|
if (hasBurst) {
|
||||||
|
// Generate a burst of 2-5 rapid reactions
|
||||||
|
const burstCount = getRandomInt(2, 5);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < burstCount; i++) {
|
||||||
|
// Reactions spaced 0.5-2 seconds apart
|
||||||
|
const timeOffset = i * getRandomInt(500, 2000);
|
||||||
|
const reactionTime = new Date(now - timeOffset);
|
||||||
|
|
||||||
|
reactions.push({
|
||||||
|
"_id": `r-burst-${now}-${i}-${reactionIndex++}`,
|
||||||
|
"sub": student.sub,
|
||||||
|
"reaction": reactionTypes[getRandomInt(0, reactionTypes.length - 1)],
|
||||||
|
"created": reactionTime.toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Each student may have 0-3 random reactions
|
||||||
|
const reactionCount = getRandomInt(0, 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < reactionCount; i++) {
|
||||||
|
// Space out regular reactions by 5-30 seconds
|
||||||
|
const timeOffset = getRandomInt(5000, 30000);
|
||||||
|
const reactionTime = new Date(Date.now() - timeOffset);
|
||||||
|
|
||||||
|
reactions.push({
|
||||||
|
"_id": `r-${Date.now()}-${reactionIndex++}`,
|
||||||
|
"sub": student.sub,
|
||||||
|
"reaction": reactionTypes[getRandomInt(0, reactionTypes.length - 1)],
|
||||||
|
"created": reactionTime.toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort reactions by creation time (newest first)
|
||||||
|
return reactions.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate the entire dynamic response
|
||||||
|
function generateDynamicAccessCodeResponse() {
|
||||||
|
// Base template from the static file
|
||||||
|
const baseTemplate = {
|
||||||
|
"success": true,
|
||||||
|
"body": {
|
||||||
|
"expires": new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour from now
|
||||||
|
"lesson": {
|
||||||
|
"_id": "65df996c584b172772d69706",
|
||||||
|
"name": "ВВОДНАЯ ПО JS.ПРИМЕНЕНИЕ И СПОСОБЫ ПОДКЛЮЧЕНИЯ НА СТРАНИЦЕ. LET, CONST. БАЗОВЫЕ ТИПЫ ДАННЫХ, ПРИВЕДЕНИЕ ТИПОВ. ПЕРЕМЕННЫЕ, ОБЛАСТЬ ВИДИМОСТИ ПЕРЕМЕННЫХ",
|
||||||
|
"date": new Date().toISOString(),
|
||||||
|
"created": new Date().toISOString(),
|
||||||
|
"__v": 0
|
||||||
|
},
|
||||||
|
"_id": `access-${Date.now()}`,
|
||||||
|
"created": new Date().toISOString(),
|
||||||
|
"__v": 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate random students
|
||||||
|
const students = getRandomStudents();
|
||||||
|
baseTemplate.body.lesson.students = students;
|
||||||
|
|
||||||
|
// Generate random reactions for those students
|
||||||
|
baseTemplate.body.lesson.reactions = generateReactions(students);
|
||||||
|
|
||||||
|
return baseTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate a dynamic lesson response
|
||||||
|
function generateDynamicLessonResponse(lessonId) {
|
||||||
|
// Base template for lesson response
|
||||||
|
const baseTemplate = {
|
||||||
|
"success": true,
|
||||||
|
"body": {
|
||||||
|
"_id": lessonId || "65df996c584b172772d69706",
|
||||||
|
"name": "ВВОДНАЯ ПО JS.ПРИМЕНЕНИЕ И СПОСОБЫ ПОДКЛЮЧЕНИЯ НА СТРАНИЦЕ. LET, CONST. БАЗОВЫЕ ТИПЫ ДАННЫХ, ПРИВЕДЕНИЕ ТИПОВ. ПЕРЕМЕННЫЕ, ОБЛАСТЬ ВИДИМОСТИ ПЕРЕМЕННЫХ",
|
||||||
|
"date": new Date().toISOString(),
|
||||||
|
"created": new Date().toISOString(),
|
||||||
|
"__v": 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate random students
|
||||||
|
const students = getRandomStudents();
|
||||||
|
baseTemplate.body.students = students;
|
||||||
|
|
||||||
|
// Generate random reactions for those students
|
||||||
|
baseTemplate.body.reactions = generateReactions(students);
|
||||||
|
|
||||||
|
return baseTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate a dynamic access code lookup response
|
||||||
|
function generateDynamicAccessLookupResponse(accessCode) {
|
||||||
|
// Generate a lesson with students and reactions
|
||||||
|
const lessonData = generateDynamicLessonResponse();
|
||||||
|
|
||||||
|
// Create a mock user
|
||||||
|
const mockUser = {
|
||||||
|
sub: `user-${Date.now()}`,
|
||||||
|
email_verified: true,
|
||||||
|
name: "Текущий Пользователь",
|
||||||
|
preferred_username: "current.user@example.com",
|
||||||
|
email: "current.user@example.com"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combine into the expected format
|
||||||
|
return {
|
||||||
|
"success": true,
|
||||||
|
"body": {
|
||||||
|
"user": mockUser,
|
||||||
|
"accessCode": {
|
||||||
|
"expires": new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
||||||
|
"lesson": lessonData.body,
|
||||||
|
"_id": accessCode || `access-${Date.now()}`,
|
||||||
|
"created": new Date().toISOString(),
|
||||||
|
"__v": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export all the necessary functions
|
||||||
|
module.exports = {
|
||||||
|
getRandomStudents,
|
||||||
|
generateReactions,
|
||||||
|
generateDynamicAccessCodeResponse,
|
||||||
|
generateDynamicLessonResponse,
|
||||||
|
generateDynamicAccessLookupResponse,
|
||||||
|
reactionTypes
|
||||||
|
};
|
@ -27,6 +27,68 @@
|
|||||||
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJUtJBAVBm642AxoGpMDDMV8CPu3MEoLjU3hmO7oisG=s96-c"
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJUtJBAVBm642AxoGpMDDMV8CPu3MEoLjU3hmO7oisG=s96-c"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"reactions": [
|
||||||
|
{
|
||||||
|
"_id": "r1d73f22-c9ba-422a-b572-c59e515a2901",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "thumbs_up"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r2d73f22-c9ba-422a-b572-c59e515a2902",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "heart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r3d73f22-c9ba-422a-b572-c59e515a2903",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "clap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r4d73f22-c9ba-422a-b572-c59e515a2904",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "laugh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r5d73f22-c9ba-422a-b572-c59e515a2905",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "wow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r6d73f22-c9ba-422a-b572-c59e515a2906",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "thumbs_up"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r7d73f22-c9ba-422a-b572-c59e515a2907",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "heart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r8d73f22-c9ba-422a-b572-c59e515a2908",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "clap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r9d73f22-c9ba-422a-b572-c59e515a2909",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "laugh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r10d73f22-c9ba-422a-b572-c59e515a2910",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "wow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r11d73f22-c9ba-422a-b572-c59e515a2911",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "laugh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r12d73f22-c9ba-422a-b572-c59e515a2912",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "heart"
|
||||||
|
}
|
||||||
|
],
|
||||||
"date": "2024-02-28T20:37:00.057Z",
|
"date": "2024-02-28T20:37:00.057Z",
|
||||||
"created": "2024-02-28T20:37:00.057Z",
|
"created": "2024-02-28T20:37:00.057Z",
|
||||||
"__v": 0
|
"__v": 0
|
||||||
|
100
stubs/mocks/lessons/access-code/create/with-rapid-reactions.json
Normal file
100
stubs/mocks/lessons/access-code/create/with-rapid-reactions.json
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"body": {
|
||||||
|
"expires": "2024-03-01T07:52:16.374Z",
|
||||||
|
"lesson": {
|
||||||
|
"_id": "65df996c584b172772d69706",
|
||||||
|
"name": "ВВОДНАЯ ПО JS.ПРИМЕНЕНИЕ И СПОСОБЫ ПОДКЛЮЧЕНИЯ НА СТРАНИЦЕ. LET, CONST. БАЗОВЫЕ ТИПЫ ДАННЫХ, ПРИВЕДЕНИЕ ТИПОВ. ПЕРЕМЕННЫЕ, ОБЛАСТЬ ВИДИМОСТИ ПЕРЕМЕННЫХ",
|
||||||
|
"students": [
|
||||||
|
{
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Мария Капитанова",
|
||||||
|
"preferred_username": "maryaKapitan@gmail.com",
|
||||||
|
"given_name": "Мария",
|
||||||
|
"family_name": "Капитанова",
|
||||||
|
"email": "maryaKapitan@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJgIjjOFD2YUSyRF5kH4jaysE6X5p-kq0Cg0CFncfMi=s96-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"email_verified": true,
|
||||||
|
"name": "Евгения Жужова",
|
||||||
|
"preferred_username": "zhuzhova@gmail.com",
|
||||||
|
"given_name": "Евгения",
|
||||||
|
"family_name": "Жужова",
|
||||||
|
"email": "zhuzhova@gmail.com",
|
||||||
|
"picture": "https://lh3.googleusercontent.com/a/ACg8ocJUtJBAVBm642AxoGpMDDMV8CPu3MEoLjU3hmO7oisG=s96-c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reactions": [
|
||||||
|
{
|
||||||
|
"_id": "r1-rapid-001",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "thumbs_up",
|
||||||
|
"created": "2024-03-08T10:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r1-rapid-002",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "heart",
|
||||||
|
"created": "2024-03-08T10:00:01.500Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r1-rapid-003",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "laugh",
|
||||||
|
"created": "2024-03-08T10:00:02.800Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r1-rapid-004",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "wow",
|
||||||
|
"created": "2024-03-08T10:00:04.200Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r1-rapid-005",
|
||||||
|
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",
|
||||||
|
"reaction": "clap",
|
||||||
|
"created": "2024-03-08T10:00:05.500Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r2-rapid-001",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "thumbs_up",
|
||||||
|
"created": "2024-03-08T10:01:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r2-rapid-002",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "heart",
|
||||||
|
"created": "2024-03-08T10:01:01.200Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r2-rapid-003",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "wow",
|
||||||
|
"created": "2024-03-08T10:01:02.300Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r2-rapid-004",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "laugh",
|
||||||
|
"created": "2024-03-08T10:01:04.100Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_id": "r2-rapid-005",
|
||||||
|
"sub": "8555885b-715c-4dee-a7c5-9563a6a05211",
|
||||||
|
"reaction": "clap",
|
||||||
|
"created": "2024-03-08T10:01:05.300Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"date": "2024-02-28T20:37:00.057Z",
|
||||||
|
"created": "2024-02-28T20:37:00.057Z",
|
||||||
|
"__v": 0
|
||||||
|
},
|
||||||
|
"_id": "65e18926584b172772d69722",
|
||||||
|
"created": "2024-03-01T07:52:06.375Z",
|
||||||
|
"__v": 0
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user