From 9e1c2c9504914152fb78968ab4879b31e72c4db1 Mon Sep 17 00:00:00 2001 From: primakov Date: Fri, 1 Mar 2024 11:43:31 +0300 Subject: [PATCH] mvp --- .prettierrc.json | 7 + src/__data__/api/api.ts | 58 ++++--- src/__data__/model.ts | 22 ++- src/dashboard.tsx | 2 +- src/pages/Journal.tsx | 153 ++++++++---------- src/pages/Lesson.tsx | 61 ++++--- src/pages/UserPage.tsx | 55 +++---- src/pages/style.ts | 151 ++++++++++------- stubs/api/index.js | 26 ++- .../lessons/access-code/create/success.json | 32 ++++ .../lessons/access-code/get/success.json | 33 ++++ stubs/mocks/lessons/byid/success.json | 26 +++ 12 files changed, 400 insertions(+), 226 deletions(-) create mode 100644 .prettierrc.json create mode 100644 stubs/mocks/lessons/access-code/create/success.json create mode 100644 stubs/mocks/lessons/access-code/get/success.json create mode 100644 stubs/mocks/lessons/byid/success.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..bcf40f7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "jsxSingleQuote": false +} diff --git a/src/__data__/api/api.ts b/src/__data__/api/api.ts index 8d99b36..0d13c54 100644 --- a/src/__data__/api/api.ts +++ b/src/__data__/api/api.ts @@ -2,7 +2,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; import { getConfigValue } from "@ijl/cli"; import { keycloak } from "../kc"; -import { BaseResponse, Lesson } from "../model"; +import { AccessCode, BaseResponse, Lesson, UserData } from "../model"; export const api = createApi({ reducerPath: "auth", @@ -15,37 +15,35 @@ export const api = createApi({ headers.set('Authorization', `Bearer ${keycloak.token}`) } }), + tagTypes: ['Lesson'], endpoints: (builder) => ({ lessonList: builder.query, void>({ - query: () => '/lesson/list' + query: () => '/lesson/list', + providesTags: ['Lesson'] + }), + createLesson: builder.mutation, Pick>({ + query: ({ name }) => ({ + url: '/lesson', + method: 'POST', + body: {name }, + }), + invalidatesTags: ['Lesson'] + }), + lessonById: builder.query, string>({ + query: (lessonId: string) => `/api/lesson/${lessonId}` + }), + createAccessCode: builder.query, { lessonId: string }>({ + query: ({ lessonId }) => ({ + url: '/lesson/access-code', + method: 'POST', + body: { lessonId }, + }) + }), + getAccess: builder.query, { accessCode: string }>({ + query: ({ accessCode }) => ({ + url: `/lesson/access-code/${accessCode}`, + method: 'GET', + }) }) - // signIn: builder.mutation({ - // query: ({ login, password }) => ({ - // url: URLs.queryApi.login, - // method: 'POST', - // body: { login, password }, - // }), - // }), - // recoverPassword: builder.mutation<{ error?: string }, { email: string }>({ - // query: ({ email }) => ({ - // url: URLs.queryApi.revoverPassword, - // method: 'POST', - // body: { email } - // }) - // }), - // recoverPasswordConfirm: builder.mutation<{ error?: string }, { code: string }>({ - // query: ({ code }) => ({ - // url: URLs.queryApi.revoverPasswordConfirm, - // method: 'POST', - // body: { code } - // }) - // }), - // recoverPasswordNewPassword: builder.mutation<{ error?: string }, { newPassword: string }>({ - // query: ({ newPassword }) => ({ - // url: URLs.queryApi.revoverPasswordNew, - // method: 'POST', - // body: { newPassword } - // }) - // }) }), }); diff --git a/src/__data__/model.ts b/src/__data__/model.ts index 3b5a1b7..827b336 100644 --- a/src/__data__/model.ts +++ b/src/__data__/model.ts @@ -52,7 +52,27 @@ export type BaseResponse = { export interface Lesson { _id: string; name: string; - students: any[]; + students: Student[]; date: string; created: string; } + +export interface AccessCode { + expires: string; + lesson: Lesson; + _id: string; + created: string; + __v: number; +} + +export interface Student { + sub: string; + email_verified: boolean; + gravatar: string; + name: string; + groups: string[]; + preferred_username: string; + given_name: string; + family_name: string; + email: string; +} \ No newline at end of file diff --git a/src/dashboard.tsx b/src/dashboard.tsx index 79a05b2..659600c 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -21,7 +21,7 @@ export const Dashboard = ({ store }) => ( } /> } /> - } /> + } /> } /> diff --git a/src/pages/Journal.tsx b/src/pages/Journal.tsx index 8f19f04..e5423e3 100644 --- a/src/pages/Journal.tsx +++ b/src/pages/Journal.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import dayjs from "dayjs"; -import { Link } from "react-router-dom"; -import { getConfigValue } from "@ijl/cli"; +import React, { useCallback, useEffect, useRef, useState } from 'react' +import dayjs from 'dayjs' +import { Link } from 'react-router-dom' +import { getConfigValue } from '@ijl/cli' import { ArrowImg, @@ -12,105 +12,88 @@ import { StartWrapper, LessonItem, Lessonname, -} from "./style"; + Papper, + ErrorSpan, + Cross, + AddButton, +} from './style' -import arrow from "../assets/36-arrow-right.svg"; -import { keycloak } from "../__data__/kc"; -import { useAppSelector } from "../__data__/store"; +import arrow from '../assets/36-arrow-right.svg' +import { keycloak } from '../__data__/kc' +import { useAppSelector } from '../__data__/store' import { api } from '../__data__/api/api' -import { isTeacher } from "../utils/user"; +import { isTeacher } from '../utils/user' export const Journal = () => { - const [lessons, setLessons] = useState(null); - const user = useAppSelector((s) => s.user); - const { data, isLoading, error } = api.useLessonListQuery(); + const user = useAppSelector((s) => s.user) + const { data, isLoading, error } = api.useLessonListQuery() + const [createLesson, crLQuery] = api.useCreateLessonMutation() + const [value, setValue] = useState('') + const [showForm, setShowForm] = useState(false) - useEffect(() => { - const check = async () => { - if (keycloak.authenticated) { - keycloak; - const rq = await fetch(`${getConfigValue("journal.back.url")}/check`, { - headers: { - accept: "application/json", - authorization: `Bearer ${keycloak.token}`, - }, - }); - const data = await rq.json(); - - console.log("check", data); - } else { - keycloak.onAuthSuccess = check; - } - }; - - check(); - }, []); - - const [answer, setAnswer] = useState(null); - - const send = async () => { - if (keycloak.authenticated) { - keycloak; - const rq = await fetch(`${getConfigValue("journal.back.url")}/test`, { - headers: { - accept: "application/json", - authorization: `Bearer ${keycloak.token}`, - }, - }); - const data = await rq.json(); - setAnswer(data); - } else { - setAnswer({ message: "Пользователь не авторизован" }); - } - }; - - const [value, setValue] = useState(""); const handleChange = useCallback( (event) => { - setValue(event.target.value.toUpperCase()); + setValue(event.target.value.toUpperCase()) }, - [setValue] - ); - const inputRef = useRef(null); - + [setValue], + ) const handleSubmit = useCallback( (event) => { - event.preventDefault(); - - // const socket = getSocket(); - // socket.emit("create-lesson", { lessonName: value }); - setValue(""); + event.preventDefault() + createLesson({ name: value }) }, - [value] - ); + [value], + ) + + useEffect(() => { + if (crLQuery.isSuccess) { + setValue('') + } + }, [crLQuery.isSuccess]) return ( {isTeacher(user) && ( -
- - Название новой лекции: - - - - - -
+ <> + {showForm ? ( + + setShowForm(false)}>X +
+ + + Название новой лекции: + + + + + + + {crLQuery.error && ( + {crLQuery.error.error} + )} +
+
+ ) : ( + setShowForm(true)}>Добавить + )} + )}
    {data?.body?.map((lesson) => ( - + {lesson.name} - {dayjs(lesson.date).format("DD MMMM YYYYг.")} - + {dayjs(lesson.date).format('DD MMMM YYYYг.')} + Участников - {lesson.students.length} @@ -118,5 +101,5 @@ export const Journal = () => { ))}
- ); -}; + ) +} diff --git a/src/pages/Lesson.tsx b/src/pages/Lesson.tsx index a1b3dc3..ed813e1 100644 --- a/src/pages/Lesson.tsx +++ b/src/pages/Lesson.tsx @@ -1,37 +1,56 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { useParams } from 'react-router-dom'; -import dayjs from 'dayjs'; -import QRCode from 'qrcode'; +import React, { useEffect, useState, useRef } from 'react' +import { useParams } from 'react-router-dom' +import dayjs from 'dayjs' +import QRCode from 'qrcode' -import { MainWrapper, StartWrapper, QRCanvas, LessonItem, Lessonname } from './style'; +import { + MainWrapper, + StartWrapper, + QRCanvas, + LessonItem, + Lessonname, +} from './style' +import { api } from '../__data__/api/api' export const Lesson = () => { - const { lessonId } = useParams(); - const canvRef = useRef(null); - const [lesson, setLesson] = useState(null); + const { lessonId } = useParams() + const canvRef = useRef(null) + const [lesson, setLesson] = useState(null) + const { isFetching, isLoading, data: accessCode, error, isSuccess } = + api.useCreateAccessCodeQuery( + { lessonId }, + { + pollingInterval: 3000, + skipPollingIfUnfocused: true, + }, + ) useEffect(() => { - // socket.on('lessons', data => { - // setLesson(data.find(lesson => lesson.id === lessonId)); - // }) - - QRCode.toCanvas(canvRef.current, `${location.origin}/journal/u/${lessonId}` , function (error) { - if (error) console.error(error) - console.log('success!'); - }) - }, []); + if (!isFetching && isSuccess) { + console.log(`${location.origin}/journal/u/${lessonId}/${accessCode.body._id}`) + QRCode.toCanvas( + canvRef.current, + `${location.origin}/journal/u/${lessonId}/${accessCode.body._id}`, + { width: 600 }, + function (error) { + if (error) console.error(error) + console.log('success!') + }, + ) + } + }, [isFetching, isSuccess]) return ( -

Lesson - {lesson?.name}

- {dayjs(lesson?.ts).format('DD MMMM YYYYг.')} +

Тема занятия - {accessCode?.body?.lesson?.name}

+ {dayjs(accessCode?.body?.lesson?.date).format('DD MMMM YYYYг.')} Отмечено - {accessCode?.body?.lesson?.students?.length} человек
    - {lesson?.padavans?.map((padavan, index) => ( + {accessCode?.body?.lesson?.students?.map((student, index) => ( - {padavan.name} + {student.preferred_username || student.name} ))}
diff --git a/src/pages/UserPage.tsx b/src/pages/UserPage.tsx index 2ca9133..8dc3767 100644 --- a/src/pages/UserPage.tsx +++ b/src/pages/UserPage.tsx @@ -1,13 +1,22 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { ArrowImg, IconButton, InputElement, InputLabel, InputWrapper, MainWrapper, StartWrapper } from './style'; +import { ArrowImg, IconButton, InputElement, InputLabel, InputWrapper, LessonItem, Lessonname, MainWrapper, StartWrapper } from './style'; import arrow from '../assets/36-arrow-right.svg'; +import { api } from '../__data__/api/api'; +import dayjs from 'dayjs'; export const UserPage = () => { const [socketId, setSocketId] = useState(null); const [error, setError] = useState(null); - const { lessonId } = useParams(); + const { lessonId, accessId } = useParams(); + const acc = api.useGetAccessQuery({ accessCode: accessId }) + + const ls = api.useLessonByIdQuery(lessonId, { + pollingInterval: 1000, + skipPollingIfUnfocused: true + }); + useEffect(() => { // socket.on('connect', () => { // const id = localStorage.getItem('socketId'); @@ -30,39 +39,23 @@ export const UserPage = () => { }, []); const [value, setValue] = useState(localStorage.getItem('name') || ''); - const handleChange = useCallback(event => { - setValue(event.target.value.toUpperCase()) - }, [setValue]); - - const handleSubmit = useCallback((event) => { - event.preventDefault(); - - localStorage.setItem('name', value) - // socket.emit('add', { socketId: localStorage.getItem('socketId') || socketId, name: value, lessonid: lessonId }); - }, [value]) return ( -
- - - Как вас зовут: - - - - - - -
+ {acc.isLoading &&

Отправляем запрос

} + {acc.isSuccess &&

Успешно

} + +

Тема занятия - {ls.data?.body?.name}

+ {dayjs(ls.data?.body?.date).format('DD MMMM YYYYг.')} + +
    + {ls.data?.body?.students?.map((student, index) => ( + + {student.preferred_username || student.name} + + ))} +
) diff --git a/src/pages/style.ts b/src/pages/style.ts index a3ef198..b44e618 100644 --- a/src/pages/style.ts +++ b/src/pages/style.ts @@ -1,50 +1,51 @@ -import styled from '@emotion/styled'; -import { css, keyframes } from '@emotion/react'; +import styled from '@emotion/styled' +import { css, keyframes } from '@emotion/react' export const MainWrapper = styled.main` - display: flex; - justify-content: center; - align-items: center; - height: 100%; -`; + display: flex; + justify-content: center; + align-items: center; + height: 100%; +` export const InputWrapper = styled.div` - position: relative; - padding: 12px; - display: flex; - align-items: center; + position: relative; + padding: 12px; + display: flex; + align-items: center; - @media screen and (max-width: 600px) { - flex-direction: column; - } -`; + @media screen and (max-width: 600px) { + flex-direction: column; + } +` export const InputLabel = styled.label` - position: absolute; - top: -8px; - left: 24px; - z-index: 2; -`; + position: absolute; + top: -8px; + left: 24px; + z-index: 2; +` export const InputElement = styled.input` - border: 1px solid #ccc; - padding: 12px; - font-size: 24px; - border-radius: 8px; - color: #117623; - max-width: 90vw; -`; + border: 1px solid #ccc; + padding: 12px; + font-size: 24px; + border-radius: 8px; + color: #117623; + max-width: 90vw; + box-shadow: inset 7px 8px 20px 8px #4990db12; +` export const ArrowImg = styled.img` - width: 48px; - height: 48px; -`; + width: 48px; + height: 48px; +` export const IconButton = styled.button` - border: none; - background-color: rgba(0, 0, 0, 0); - display: flex; - align-items: center; - height: 100%; -`; + border: none; + background-color: rgba(0, 0, 0, 0); + display: flex; + align-items: center; + height: 100%; +` const reveal = keyframes` 0% { @@ -54,25 +55,33 @@ const reveal = keyframes` 100% { transform: scale(1); } -`; +` export const StartWrapper = styled.div` - animation: ${reveal} 1s ease forwards; - /* box-shadow: 0 -2px 5px rgba(255,255,255,0.05), 0 2px 5px rgba(255,255,255,0.1); */ - width: 650px; - height: calc(100vh - 300px); - /* margin: 60px auto; */ - position: relative; -`; + animation: ${reveal} 1s ease forwards; + /* box-shadow: 0 -2px 5px rgba(255,255,255,0.05), 0 2px 5px rgba(255,255,255,0.1); */ + width: 650px; + height: calc(100vh - 300px); + /* margin: 60px auto; */ + position: relative; +` export const Svg = styled.svg` - position: absolute; - top: 50%; - left: 50%; - transform-origin: 50% 50%; - transform: translate(-50%, -50%); - /* stroke-dasharray: 600; */ -`; + position: absolute; + top: 50%; + left: 50%; + transform-origin: 50% 50%; + transform: translate(-50%, -50%); + /* stroke-dasharray: 600; */ +` + +export const Papper = styled.div` + position: relative; + background-color: #ffffff; + border-radius: 12px; + padding: 32px 16px 16px; + box-shadow: 2px 2px 6px #0000005c; +` export const LessonItem = styled.li` list-style: none; @@ -81,13 +90,47 @@ export const LessonItem = styled.li` border-radius: 12px; box-shadow: 2px 2px 6px #0000005c; margin-bottom: 12px; -`; +` export const Lessonname = styled.span` display: inline-box; margin-right: 12px; -`; +` export const QRCanvas = styled.canvas` display: block; -`; +` + +export const ErrorSpan = styled.span` + color: #f9e2e2; + display: block; + padding: 16px; + background-color: #d32f0b; + border-radius: 11px; +` + +export const Cross = styled.button` + position: absolute; + right: 19px; + top: 14px; + font-size: 24px; + padding: 7px; + cursor: pointer; + background-color: #fff; + border: none; + + :hover { + background-color: #d7181812; + border-radius: 20px; + } +` + +export const AddButton = styled.button` + background-color: transparent; + border: none; + cursor: pointer; + + :hover { + box-shadow: 3px 2px 5px #00000038; + } +` diff --git a/stubs/api/index.js b/stubs/api/index.js index 0ac3927..9bcc5f3 100644 --- a/stubs/api/index.js +++ b/stubs/api/index.js @@ -1,6 +1,8 @@ -const router = require('express').Router(); +const router = require('express').Router() +const fs = require('node:fs') +const path = require('node:path') -router.get('/check', function (req, res){ +router.get('/check', function (req, res) { res.send({ ok: true }) }) @@ -8,4 +10,22 @@ router.get('/lesson/list', (req, res) => { res.send(require('../mocks/lessons/list/success.json')) }) -module.exports = router; +router.post('/lesson', (req, res) => { + res.send(require('../mocks/lessons/create/success.json')) +}) + +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) +}) + +router.get('/lesson/access-code/:accessCode', (req, res) => { + res.send(require('../mocks/lessons/access-code/get/success.json')) +}) + +router.get('/api/lesson/:lessonId', (req, res) => { + res.send(require('../mocks/lessons/byid/success.json')) +}) + +module.exports = router diff --git a/stubs/mocks/lessons/access-code/create/success.json b/stubs/mocks/lessons/access-code/create/success.json new file mode 100644 index 0000000..3dedeaf --- /dev/null +++ b/stubs/mocks/lessons/access-code/create/success.json @@ -0,0 +1,32 @@ +{ + "success": true, + "body": { + "expires": "2024-03-01T07:52:16.374Z", + "lesson": { + "_id": "65df996c584b172772d69706", + "name": "Проверочное занятие", + "students": [ + { + "sub": "f62905b1-e223-40ca-910f-c8d84c6137c1", + "email_verified": true, + "gravatar": "true", + "name": "Александр Примаков", + "groups": [ + "/inno-staff", + "/microfrontend-admin-user" + ], + "preferred_username": "primakov", + "given_name": "Александр", + "family_name": "Примаков", + "email": "primakovpro@gmail.com" + } + ], + "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 + } +} \ No newline at end of file diff --git a/stubs/mocks/lessons/access-code/get/success.json b/stubs/mocks/lessons/access-code/get/success.json new file mode 100644 index 0000000..174474d --- /dev/null +++ b/stubs/mocks/lessons/access-code/get/success.json @@ -0,0 +1,33 @@ +{ + "success": true, + "body": { + "user": { + "sub": "f62905b1-e223-40ca-910f-c8d84c6137c1", + "email_verified": true, + "gravatar": "true", + "name": "Александр Примаков", + "groups": [ + "/inno-staff", + "/microfrontend-admin-user" + ], + "preferred_username": "primakov", + "given_name": "Александр", + "family_name": "Примаков", + "email": "primakovpro@gmail.com" + }, + "accessCode": { + "_id": "65e1891f584b172772d6971b", + "expires": "2024-03-01T07:52:09.233Z", + "lesson": { + "_id": "65df996c584b172772d69706", + "name": "Проверочное занятие", + "students": [], + "date": "2024-02-28T20:37:00.057Z", + "created": "2024-02-28T20:37:00.057Z", + "__v": 0 + }, + "created": "2024-03-01T07:51:59.234Z", + "__v": 0 + } + } +} \ No newline at end of file diff --git a/stubs/mocks/lessons/byid/success.json b/stubs/mocks/lessons/byid/success.json new file mode 100644 index 0000000..3f5d5f4 --- /dev/null +++ b/stubs/mocks/lessons/byid/success.json @@ -0,0 +1,26 @@ +{ + "success": true, + "body": { + "_id": "65df996c584b172772d69706", + "name": "Проверочное занятие", + "students": [ + { + "sub": "f62905b1-e223-40ca-910f-c8d84c6137c1", + "email_verified": true, + "gravatar": "true", + "name": "Александр Примаков", + "groups": [ + "/inno-staff", + "/microfrontend-admin-user" + ], + "preferred_username": "primakov", + "given_name": "Александр", + "family_name": "Примаков", + "email": "primakovpro@gmail.com" + } + ], + "date": "2024-02-28T20:37:00.057Z", + "created": "2024-02-28T20:37:00.057Z", + "__v": 0 + } +} \ No newline at end of file