27 Commits

Author SHA1 Message Date
Primakov Alexandr Alexandrovich
81533c3342 3.6.7 2025-01-08 18:33:10 +03:00
Primakov Alexandr Alexandrovich
462ba85fe8 check exam link to render 2025-01-08 18:33:02 +03:00
Primakov Alexandr Alexandrovich
d0f7dfb87d login required back
All checks were successful
platform/bro-js/journal.pl/pipeline/head This commit looks good
2025-01-07 18:09:54 +03:00
Primakov Alexandr Alexandrovich
a133cea95c 3.6.6 2025-01-07 18:04:01 +03:00
Primakov Alexandr Alexandrovich
4704b404f9 update ts config 2025-01-07 17:00:37 +03:00
Primakov Alexandr Alexandrovich
55d23f1e47 twik a bit 2025-01-07 16:57:37 +03:00
Primakov Alexandr Alexandrovich
6b07fef62f up kc js version 2025-01-07 16:49:55 +03:00
Primakov Alexandr Alexandrovich
3242576a12 check sso 2 debug 2025-01-07 16:42:04 +03:00
Primakov Alexandr Alexandrovich
a7168231a1 kc params from env 2025-01-07 16:20:20 +03:00
Primakov Alexandr Alexandrovich
cc7f3d3371 3.6.5
All checks were successful
platform/bro-js/journal.pl/pipeline/head This commit looks good
2024-12-21 16:19:32 +03:00
Primakov Alexandr Alexandrovich
d20cb7257b 3.6.4 2024-12-21 16:16:40 +03:00
Primakov Alexandr Alexandrovich
ab9e5f6d19 3.6.3
All checks were successful
platform/bro-js/journal.pl/pipeline/head This commit looks good
2024-12-15 20:35:48 +03:00
Primakov Alexandr Alexandrovich
71d2f59750 teachers in Attendance 2024-12-15 20:35:39 +03:00
Primakov Alexandr Alexandrovich
bdd53ca15b 3.6.2 2024-12-15 17:20:20 +03:00
Primakov Alexandr Alexandrovich
789d2ed6ca manual add for teacher only 2024-12-15 17:20:16 +03:00
Primakov Alexandr Alexandrovich
d4b7d0616e 3.6.1 2024-12-15 17:14:49 +03:00
Primakov Alexandr Alexandrovich
b5bd2e02d7 not create access token if not teacher 2024-12-15 17:03:14 +03:00
Primakov Alexandr Alexandrovich
428b06f920 3.6.0
All checks were successful
platform/bro-js/journal.pl/pipeline/head This commit looks good
2024-12-12 22:59:07 +03:00
Primakov Alexandr Alexandrovich
7d6f2a4ca0 no JSON stringify in attendance 2024-12-12 16:00:49 +03:00
Primakov Alexandr Alexandrovich
2fe7600ef3 fix 2024-12-12 15:20:55 +03:00
Primakov Alexandr Alexandrovich
985b8ef315 stringify 2024-12-12 12:30:56 +03:00
Primakov Alexandr Alexandrovich
956fdec7f5 unknown name if no name 2024-12-12 12:12:19 +03:00
Primakov Alexandr Alexandrovich
d44a511a3d try get student name even better 2024-12-12 12:09:25 +03:00
Primakov Alexandr Alexandrovich
0aebb87210 update get student name in attendance 2024-12-12 11:17:38 +03:00
Primakov Alexandr Alexandrovich
9509f12d73 3.5.1
All checks were successful
platform/bro-js/journal.pl/pipeline/head This commit looks good
2024-11-06 13:04:00 +03:00
Primakov Alexandr Alexandrovich
021031ced7 short lesson name 2024-11-06 13:03:55 +03:00
Primakov Alexandr Alexandrovich
22a9199d9d no attendance link if no nav element 2024-11-06 12:59:24 +03:00
15 changed files with 2070 additions and 3833 deletions

24
@types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,24 @@
declare const IS_PROD: string
declare const KC_URL: string
declare const KC_REALM: string
declare const KC_CLIENT_ID: string
declare module '*.svg' {
const value: string
export default value
}
declare module '*.jpg' {
const value: string
export default value
}
declare module '*.png' {
const value: string
export default value
}
declare const __webpack_public_path__: string

View File

@@ -1,3 +1,5 @@
const webpack = require('webpack');
const pkg = require('./package') const pkg = require('./package')
module.exports = { module.exports = {
@@ -6,6 +8,13 @@ module.exports = {
output: { output: {
publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/`, publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/`,
}, },
plugins: [
new webpack.DefinePlugin({
KC_URL: process.env.KC_URL || '"https://kc.bro-js.ru"',
KC_REALM: process.env.KC_REALM || '"bro-js"',
KC_CLIENT_ID: process.env.KC_CLIENT_ID || '"microfrontend-admin"',
}),
],
}, },
navigations: { navigations: {
'journal.main': '/journal.pl', 'journal.main': '/journal.pl',

6
index.d.ts vendored
View File

@@ -1,6 +0,0 @@
declare module '*.svg' {
const src: string;
export default src;
}
declare const __webpack_public_path__: string;

View File

@@ -1,12 +0,0 @@
import type { ConfigFile } from '@rtk-query/codegen-openapi'
const config: ConfigFile = {
schemaFile: 'https://platform.bro-js.ru/jrnl-bh/documentation/json',
apiFile: './src/__data__/api/api.ts',
apiImport: 'api',
outputFile: './src/__data__/api/jrnl.ts',
exportName: 'jrnlApi',
hooks: true,
}
export default config

5650
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "journal.pl", "name": "journal.pl",
"version": "3.5.0", "version": "3.6.7",
"description": "bro-js platform journal ui repo", "description": "bro-js platform journal ui repo",
"main": "./src/index.tsx", "main": "./src/index.tsx",
"scripts": { "scripts": {
@@ -19,14 +19,13 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@rtk-query/codegen-openapi": "^1.2.0",
"@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0", "@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1" "eslint-plugin-react": "^7.34.1"
}, },
"dependencies": { "dependencies": {
"@brojs/cli": "^0.0.4-beta.0", "@brojs/cli": "^1.8.4",
"@chakra-ui/icons": "^2.1.1", "@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.2", "@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
@@ -38,7 +37,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"express": "^4.19.2", "express": "^4.19.2",
"js-sha256": "^0.11.0", "js-sha256": "^0.11.0",
"keycloak-js": "^23.0.7", "keycloak-js": "^26.0.7",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"react": "^18.3.1", "react": "^18.3.1",

View File

@@ -1,9 +1,9 @@
import Keycloak from 'keycloak-js'; import Keycloak from 'keycloak-js'
export const keycloak = new Keycloak({ export const keycloak = new Keycloak({
url: 'https://kc.bro-js.ru', url: KC_URL,
realm: 'bro-js', realm: KC_REALM,
clientId: 'journal' clientId: KC_CLIENT_ID,
}); });
window.keycloak = keycloak; (window as any).kc = keycloak

View File

@@ -53,10 +53,17 @@ export interface Lesson {
id: string; id: string;
name: string; name: string;
students: User[]; students: User[];
teachers: Teacher[];
date: string; date: string;
created: string; created: string;
} }
interface Teacher {
sub: string;
email_verified: boolean;
preferred_username: string;
}
export interface AccessCode { export interface AccessCode {
expires: string; expires: string;
lesson: Lesson; lesson: Lesson;

View File

@@ -14,23 +14,30 @@ export default (props) => <App {...props} />;
let rootElement: ReactDOM.Root let rootElement: ReactDOM.Root
export const mount = async (Сomponent, element = document.getElementById('app')) => { export const mount = async (Component, element = document.getElementById('app')) => {
let user = null; let user = null;
try { try {
await keycloak.init({ onLoad: "login-required" }); await keycloak.init({ onLoad: 'login-required' })
user = { ...(await keycloak.loadUserInfo()), ...keycloak.tokenParsed };
const userInfo = await keycloak.loadUserInfo()
if (userInfo && keycloak.tokenParsed) {
user = { ...userInfo, ...keycloak.tokenParsed }
} else {
console.error('No userInfo or tokenParsed', userInfo, keycloak.tokenParsed)
}
} catch (error) { } catch (error) {
console.error("Failed to initialize adapter:", error); console.error('Failed to initialize adapter:', error)
keycloak.login(); keycloak.login()
} }
const store = createStore({ user }); const store = createStore({ user });
rootElement = ReactDOM.createRoot(element); rootElement = ReactDOM.createRoot(element);
rootElement.render(<Сomponent store={store} />); rootElement.render(<Component store={store} />);
if(module.hot) { if(module.hot) {
module.hot.accept('./app', ()=> { module.hot.accept('./app', ()=> {
rootElement.render(<Сomponent store={store} />); rootElement.render(<Component store={store} />);
}) })
} }
}; };

View File

@@ -1,11 +1,10 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import styled from '@emotion/styled' import { Box, Heading, Tooltip, Text } from '@chakra-ui/react'
import dayjs from 'dayjs'
import { api } from '../../__data__/api/api' import { api } from '../../__data__/api/api'
import { PageLoader } from '../../components/page-loader/page-loader' import { PageLoader } from '../../components/page-loader/page-loader'
import { Box, Container, Heading } from '@chakra-ui/react'
import dayjs from 'dayjs'
export const Attendance = () => { export const Attendance = () => {
const { courseId } = useParams() const { courseId } = useParams()
@@ -22,26 +21,38 @@ export const Attendance = () => {
if (!attendance) return null if (!attendance) return null
const studentsMap = new Map() const studentsMap = new Map()
const teachersMap = new Map()
attendance.forEach((lesson) => { attendance.forEach((lesson) => {
lesson.teachers?.map((teacher: any) => {
teachersMap.set(teacher.sub, { id: teacher.sub, ...teacher, value: teacher.value || (teacher.family_name && teacher.given_name
? `${teacher.family_name} ${teacher.given_name}`
: teacher.name || teacher.email || teacher.preferred_username || teacher.family_name || teacher.given_name), })
})
lesson.students.forEach((student) => { lesson.students.forEach((student) => {
const current = studentsMap.get(student.sub) || {}
studentsMap.set(student.sub, { studentsMap.set(student.sub, {
...student, ...student,
value: id: student.sub,
student.family_name && student.given_name value: current.value || (student.family_name && student.given_name
? `${student.family_name} ${student.given_name}` ? `${student.family_name} ${student.given_name}`
: student.name || student.email, : student.name || student.email || student.preferred_username || student.family_name || student.given_name),
}) })
}) })
}) })
const compare = Intl.Collator('ru').compare const compare = Intl.Collator('ru').compare
const students = [...studentsMap.values()] const students = [...studentsMap.values()]
const taechers = [...teachersMap.values()]
students.sort(({ family_name: name }, { family_name: nname }) => students.sort(({ family_name: name }, { family_name: nname }) =>
compare(name, nname), compare(name, nname),
) )
return { return {
students, students,
taechers,
} }
}, [attendance]) }, [attendance])
@@ -58,25 +69,52 @@ export const Attendance = () => {
<table> <table>
<thead> <thead>
<tr> <tr>
{data.taechers.map(teacher => (
<th id={teacher.id} key={teacher.id}>{teacher.value}</th>
))}
<th>Дата</th> <th>Дата</th>
<th>Название занятия</th> <th>Название занятия</th>
{data.students.map((student) => ( {data.students.map((student) => (
<th key={student.sub}>{student.name}</th> <th id={student.id || student.sub} key={student.sub}>{student.name || student.value || 'Имя не определено'}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{attendance.map((lesson, index) => ( {attendance.map((lesson, index) => (
<tr key={lesson.name}> <tr key={lesson.name}>
{data?.taechers?.map((teacher) => {
const wasThere = Boolean(lesson.teachers) &&
lesson?.teachers?.findIndex((u) => u.sub === teacher.sub) !== -1
return (
<td
style={{
textAlign: 'center',
backgroundColor: wasThere ? '#8ef78a' : '#e09797',
}}
key={teacher.sub}
>
{wasThere ? '+' : '-'}
</td>
)
})}
<td>{dayjs(lesson.date).format('DD.MM.YYYY')}</td> <td>{dayjs(lesson.date).format('DD.MM.YYYY')}</td>
<td>{lesson.name}</td> <td>{<ShortText text={lesson.name} />}</td>
{data.students.map((st) => { {data.students.map((st) => {
const wasThere = const wasThere =
lesson.students.findIndex((u) => u.sub === st.sub) !== -1 lesson.students.findIndex((u) => u.sub === st.sub) !== -1
return <td style={{ return (
textAlign: 'center', <td
backgroundColor: wasThere ? '#8ef78a' : '#e09797', style={{
}} key={st.sub}>{wasThere ? '+' : '-'}</td> textAlign: 'center',
backgroundColor: wasThere ? '#8ef78a' : '#e09797',
}}
key={st.sub}
>
{wasThere ? '+' : '-'}
</td>
)
})} })}
</tr> </tr>
))} ))}
@@ -86,3 +124,17 @@ export const Attendance = () => {
</Box> </Box>
) )
} }
const ShortText = ({ text }: { text: string }) => {
const needShortText = text.length > 20
if (needShortText) {
return (
<Tooltip label="На страницу с лекциями" fontSize="12px" top="16px">
<Text>{text.slice(0, 20)}...</Text>
</Tooltip>
)
}
return text
}

View File

@@ -57,21 +57,27 @@ export const CourseCard = ({ course }: { course: Course }) => {
<CourseDetails populatedCourse={populatedCourse.data} /> <CourseDetails populatedCourse={populatedCourse.data} />
)} )}
<Tooltip label="На страницу с лекциями" fontSize="12px" top="16px"> {getNavigationsValue('link.journal.attendance') && (
<Button <Tooltip
leftIcon={<LinkIcon />} label="На страницу с лекциями"
as={ConnectedLink} fontSize="12px"
variant="outline" top="16px"
colorScheme="blue"
to={generatePath(
`${getNavigationsValue('journal.main')}${getNavigationsValue('link.journal.attendance')}`,
{ courseId: course.id },
)}
> >
<Box mt={3}></Box> <Button
Посещаемость leftIcon={<LinkIcon />}
</Button> as={ConnectedLink}
</Tooltip> variant="outline"
colorScheme="blue"
to={generatePath(
`${getNavigationsValue('journal.main')}${getNavigationsValue('link.journal.attendance')}`,
{ courseId: course.id },
)}
>
<Box mt={3}></Box>
Посещаемость
</Button>
</Tooltip>
)}
</Stack> </Stack>
</CardBody> </CardBody>
)} )}

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Link as ConnectedLink } from 'react-router-dom' import { Link as ConnectedLink } from 'react-router-dom'
import { getNavigationsValue, getHistory } from '@brojs/cli' import { getNavigationValue, getHistory } from '@brojs/cli'
import { Stack, Heading, Link, Button, Tooltip, Box } from '@chakra-ui/react' import { Stack, Heading, Link, Button, Tooltip, Box } from '@chakra-ui/react'
import { useAppSelector } from '../../__data__/store' import { useAppSelector } from '../../__data__/store'
@@ -27,23 +27,23 @@ export const CourseDetails = ({ populatedCourse }: CourseDetailsProps) => {
{isTeacher(user) && ( {isTeacher(user) && (
<Heading as="h3" mt={4} mb={3} size="lg"> <Heading as="h3" mt={4} mb={3} size="lg">
Экзамен: {exam?.name}{' '} Экзамен: {exam?.name}{' '}
{exam && ( {exam && getNavigationValue('exam.main') && getNavigationValue('link.exam.details') && (
<Tooltip label="Начать экзамен" fontSize="12px" top="16px"> <Tooltip label="Начать экзамен" fontSize="12px" top="16px">
<Button <Button
leftIcon={<LinkIcon />} leftIcon={<LinkIcon />}
as={'a'} as={'a'}
colorScheme="blue" colorScheme="blue"
href={ href={
getNavigationsValue('exam.main') + getNavigationValue('exam.main') +
getNavigationsValue('link.exam.details') getNavigationValue('link.exam.details')
.replace(':courseId', populatedCourse.id) .replace(':courseId', populatedCourse.id)
.replace(':examId', exam.id) .replace(':examId', exam.id)
} }
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
history.push( history.push(
getNavigationsValue('exam.main') + getNavigationValue('exam.main') +
getNavigationsValue('link.exam.details') getNavigationValue('link.exam.details')
.replace(':courseId', populatedCourse.id) .replace(':courseId', populatedCourse.id)
.replace(':examId', exam.id), .replace(':examId', exam.id),
) )

View File

@@ -24,6 +24,8 @@ import {
StudentList, StudentList,
BreadcrumbsWrapper, BreadcrumbsWrapper,
} from './style' } from './style'
import { useAppSelector } from '../__data__/store'
import { isTeacher } from '../utils/user'
export function getGravatarURL(email, user) { export function getGravatarURL(email, user) {
if (!email) return void 0 if (!email) return void 0
@@ -37,6 +39,8 @@ export function getGravatarURL(email, user) {
const LessonDetail = () => { const LessonDetail = () => {
const { lessonId, courseId } = useParams() const { lessonId, courseId } = useParams()
const canvRef = useRef(null) const canvRef = useRef(null)
const user = useAppSelector((s) => s.user)
const { const {
isFetching, isFetching,
data: accessCode, data: accessCode,
@@ -45,6 +49,7 @@ const LessonDetail = () => {
} = api.useCreateAccessCodeQuery( } = api.useCreateAccessCodeQuery(
{ lessonId }, { lessonId },
{ {
skip: !isTeacher(user),
pollingInterval: pollingInterval:
Number(getConfigValue('journal.polling-interval')) || 3000, Number(getConfigValue('journal.polling-interval')) || 3000,
skipPollingIfUnfocused: true, skipPollingIfUnfocused: true,
@@ -144,7 +149,7 @@ const LessonDetail = () => {
<QRCanvas ref={canvRef} /> <QRCanvas ref={canvRef} />
</a> </a>
<StudentList> <StudentList>
{studentsArr.map((student) => ( {isTeacher(user) && studentsArr.map((student) => (
<UserCard <UserCard
wrapperAS="li" wrapperAS="li"
key={student.sub} key={student.sub}

View File

@@ -4,6 +4,13 @@
{ {
"_id": "65e2e5fbec37fec650f28489", "_id": "65e2e5fbec37fec650f28489",
"name": "ВВЕДЕНИЕ В ВЕБ-РАЗРАБОТКУ. ИНСТРУМЕНТАРИЙ, ОБЗОР ВЕБ-ТЕХНОЛОГИЙ", "name": "ВВЕДЕНИЕ В ВЕБ-РАЗРАБОТКУ. ИНСТРУМЕНТАРИЙ, ОБЗОР ВЕБ-ТЕХНОЛОГИЙ",
"teachers": [
{
"sub": "f62905b1-e223-40ca-910f-c8d84c6137c1",
"email_verified": true,
"preferred_username": "primakov"
}
],
"students": [ "students": [
{ {
"sub": "fcde3f22-d9ba-412a-a572-c59e515a290f", "sub": "fcde3f22-d9ba-412a-a572-c59e515a290f",

View File

@@ -1,25 +1,26 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": ["dom", "es2017"],
"dom",
"es2017"
],
"outDir": "./dist/", "outDir": "./dist/",
"sourceMap": true, "sourceMap": true,
"esModuleInterop": true, "esModuleInterop": true,
"noImplicitAny": false, "noImplicitAny": false,
"module": "esnext", "module": "esnext",
"moduleResolution": "node",
"target": "es6", "target": "es6",
"jsx": "react", "jsx": "react",
"typeRoots": ["node_modules/@types", "src/typings"], "typeRoots": ["node_modules/@types", "./@types"],
"types" : ["webpack-env", "node"], "types": ["webpack-env", "node"],
"resolveJsonModule": true "resolveJsonModule": true,
"moduleResolution": "Bundler",
"skipLibCheck": true,
}, },
"types": [
"@types/*"
],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"**/*.test.ts", "**/*.test.ts",
"**/*.test.tsx", "**/*.test.tsx",
"node_modules/@types/jest" "node_modules/@types/jest"
] ]
} }