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.tryAgainLater": "An error occurred while generating lesson suggestions. Please try again later.",
|
||||
"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.startExam": "Start exam",
|
||||
|
@ -86,6 +86,16 @@
|
||||
"journal.pl.lesson.aiGenerationError": "Ошибка генерации рекомендаций ИИ",
|
||||
"journal.pl.lesson.tryAgainLater": "Произошла ошибка при генерации рекомендаций для занятий. Пожалуйста, попробуйте позже.",
|
||||
"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.startExam": "Начать экзамен",
|
||||
|
@ -122,6 +122,15 @@ export const api = createApi({
|
||||
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>({
|
||||
query: (courseId) => `/course/${courseId}`,
|
||||
transformResponse: (response: BaseResponse<PopulatedCourse>) => response.body,
|
||||
|
@ -49,10 +49,17 @@ export type BaseResponse<Data> = {
|
||||
body: Data;
|
||||
};
|
||||
|
||||
export interface Reaction {
|
||||
_id: string;
|
||||
sub: string;
|
||||
reaction: string;
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
id: string;
|
||||
_id: string;
|
||||
name: string;
|
||||
reactions: Reaction[];
|
||||
students: User[];
|
||||
teachers: Teacher[];
|
||||
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;
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
@ -98,6 +98,13 @@ export const Wrapper = styled.div<{ warn?: boolean; width?: string | number }>`
|
||||
`
|
||||
: ''}
|
||||
|
||||
${({ position }) =>
|
||||
position
|
||||
? css`
|
||||
position: ${position};
|
||||
`
|
||||
: ''}
|
||||
|
||||
${(props) =>
|
||||
props.warn
|
||||
? css`
|
||||
|
@ -1,13 +1,24 @@
|
||||
import React from 'react'
|
||||
import { sha256 } from 'js-sha256'
|
||||
import { useState } from 'react'
|
||||
import { Box, useColorMode } from '@chakra-ui/react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Box, useColorMode, Text } from '@chakra-ui/react'
|
||||
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'
|
||||
|
||||
// Map of reaction types to emojis
|
||||
const REACTION_EMOJIS = {
|
||||
thumbs_up: '👍',
|
||||
heart: '❤️',
|
||||
laugh: '😂',
|
||||
wow: '😮',
|
||||
clap: '👏'
|
||||
}
|
||||
|
||||
export function getGravatarURL(email, user) {
|
||||
if (!email) return void 0
|
||||
const address = String(email).trim().toLowerCase()
|
||||
@ -22,7 +33,8 @@ export const UserCard = ({
|
||||
onAddUser = undefined,
|
||||
wrapperAS = 'div',
|
||||
width,
|
||||
recentlyPresent = false
|
||||
recentlyPresent = false,
|
||||
reactions = []
|
||||
}: {
|
||||
student: User
|
||||
present: boolean
|
||||
@ -30,9 +42,50 @@ export const UserCard = ({
|
||||
onAddUser?: (user: User) => void
|
||||
wrapperAS?: React.ElementType<any, keyof React.JSX.IntrinsicElements>;
|
||||
recentlyPresent?: boolean
|
||||
reactions?: Reaction[]
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const { t } = useTranslation();
|
||||
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 (
|
||||
<Wrapper
|
||||
@ -40,6 +93,7 @@ export const UserCard = ({
|
||||
as={wrapperAS}
|
||||
width={width}
|
||||
className={!present ? 'warn' : recentlyPresent ? 'recent' : ''}
|
||||
position="relative"
|
||||
>
|
||||
<Avatar
|
||||
src={imageError ? getGravatarURL(student.email, null) : (student.picture || getGravatarURL(student.email, null))}
|
||||
@ -58,10 +112,52 @@ export const UserCard = ({
|
||||
)}
|
||||
</NameOverlay>
|
||||
{onAddUser && !present && (
|
||||
<AddMissedButton onClick={() => onAddUser(student)} aria-label="Отметить присутствие">
|
||||
<AddMissedButton onClick={() => onAddUser(student)} aria-label={t('journal.pl.common.add')}>
|
||||
<AddIcon boxSize={3} />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { api } from '../__data__/api/api'
|
||||
import { User } from '../__data__/model'
|
||||
import { User, Reaction } from '../__data__/model'
|
||||
import { UserCard } from '../components/user-card'
|
||||
import { formatDate } from '../utils/dayjs-config'
|
||||
import { useSetBreadcrumbs } from '../components'
|
||||
@ -71,6 +71,10 @@ const LessonDetail = () => {
|
||||
const [isPulsing, setIsPulsing] = useState(false)
|
||||
// Отслеживаем предыдущее количество студентов
|
||||
const prevStudentCountRef = useRef(0)
|
||||
// Отслеживаем предыдущие реакции для определения новых
|
||||
const prevReactionsRef = useRef<Record<string, Reaction[]>>({})
|
||||
// Храним актуальные реакции студентов
|
||||
const [studentReactions, setStudentReactions] = useState<Record<string, Reaction[]>>({})
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
@ -121,6 +125,44 @@ const LessonDetail = () => {
|
||||
}
|
||||
}, [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(() => {
|
||||
if (manualAddRqst.isSuccess) {
|
||||
refetch()
|
||||
@ -348,6 +390,7 @@ const LessonDetail = () => {
|
||||
present={student.present}
|
||||
recentlyPresent={student.recentlyPresent}
|
||||
onAddUser={(user: User) => manualAdd({ lessonId, user })}
|
||||
reactions={studentReactions[student.sub] || []}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
@ -17,17 +17,31 @@ import {
|
||||
Badge,
|
||||
Flex,
|
||||
useColorMode,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { UserCard } from '../components/user-card'
|
||||
import { StudentListView } from './style'
|
||||
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 { lessonId, accessId } = useParams()
|
||||
const { t } = useTranslation()
|
||||
const { colorMode } = useColorMode()
|
||||
const acc = api.useGetAccessQuery({ accessCode: accessId })
|
||||
const [animatedStudents, setAnimatedStudents] = useState([])
|
||||
const [sendReaction] = api.useSendReactionMutation()
|
||||
const [activeReaction, setActiveReaction] = useState(null)
|
||||
|
||||
const ls = api.useLessonByIdQuery(lessonId, {
|
||||
pollingInterval: 1000,
|
||||
@ -85,6 +99,19 @@ const UserPage = () => {
|
||||
}
|
||||
}, [animatedStudents])
|
||||
|
||||
// Обработчик отправки реакции
|
||||
const handleReaction = (reaction) => {
|
||||
if (lessonId) {
|
||||
sendReaction({ lessonId, reaction })
|
||||
setActiveReaction(reaction)
|
||||
|
||||
// Сбрасываем активную реакцию через 1 секунду
|
||||
setTimeout(() => {
|
||||
setActiveReaction(null)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
if (acc.isLoading) {
|
||||
return (
|
||||
<Container maxW="container.xl">
|
||||
@ -168,6 +195,49 @@ const UserPage = () => {
|
||||
</Box>
|
||||
</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
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@ -207,6 +277,7 @@ const UserPage = () => {
|
||||
student={student}
|
||||
present={true}
|
||||
recentlyPresent={student.isNew}
|
||||
reactions={ls.data?.body?.reactions?.filter(r => r.sub === student.sub) || []}
|
||||
/>
|
||||
</motion.li>
|
||||
))}
|
||||
|
@ -1,6 +1,7 @@
|
||||
const router = require('express').Router()
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const mockGenerator = require('./mock-generator')
|
||||
|
||||
const timer =
|
||||
(time = 1000) =>
|
||||
@ -56,19 +57,21 @@ router.post('/lesson', (req, res) => {
|
||||
})
|
||||
|
||||
router.post('/lesson/access-code', (req, res) => {
|
||||
const answer = fs.readFileSync(
|
||||
path.resolve(__dirname, '../mocks/lessons/access-code/create/success.json'),
|
||||
)
|
||||
// res.send(require('../mocks/lessons/access-code/create/success.json'))
|
||||
res.send(answer)
|
||||
// Generate random students and reactions dynamically
|
||||
const dynamicData = mockGenerator.generateDynamicAccessCodeResponse();
|
||||
res.send(dynamicData);
|
||||
})
|
||||
|
||||
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) => {
|
||||
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) => {
|
||||
@ -79,4 +82,291 @@ router.put('/lesson', (req, res) => {
|
||||
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
|
||||
|
||||
// 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"
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"created": "2024-02-28T20:37:00.057Z",
|
||||
"__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