- {!activeCourse && }
+ {!course && }
{approvedCourse && (
@@ -88,7 +94,7 @@ export function HomePage() {
{hasRegistryBanner && }
-
+
diff --git a/client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx b/client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx
index 1b7c26a693..e0b83464b5 100644
--- a/client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx
+++ b/client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx
@@ -4,6 +4,7 @@ import { Course } from 'services/models';
import { CourseInfo, Session } from 'components/withSession';
import { INSTRUCTIONS_TEXT } from '../Instructions';
import { MentorDashboardProps } from 'pages/course/mentor/dashboard';
+import { SessionContext } from 'modules/Course/contexts';
jest.mock('modules/Mentor/hooks/useMentorDashboard', () => ({
useMentorDashboard: jest.fn().mockReturnValue([[], false]),
@@ -13,19 +14,6 @@ jest.mock('next/router', () => ({
}));
const PROPS_MOCK: MentorDashboardProps = {
- session: {
- id: 1,
- isActivist: false,
- isAdmin: true,
- isHirer: false,
- githubId: 'github-id',
- courses: {
- '400': {
- mentorId: 1,
- roles: ['mentor'],
- } as CourseInfo,
- },
- } as Session,
course: {
id: 400,
} as Course,
@@ -36,7 +24,27 @@ const PROPS_MOCK: MentorDashboardProps = {
describe('MentorDashboard', () => {
it('should render instructions when mentor has no students for this course', () => {
- render();
+ render(
+
+
+ ,
+ );
const instructionsTitle = screen.getByText(INSTRUCTIONS_TEXT.title);
@@ -44,7 +52,27 @@ describe('MentorDashboard', () => {
});
it('should render empty table when mentor has students for this course', () => {
- render();
+ render(
+
+
+ ,
+ );
const emptyTable = screen.getByText(/No Data/i);
From 39e6af1e1256b26580366b85f45df71b487fb0c1 Mon Sep 17 00:00:00 2001
From: Valery <57412523+valerydluski@users.noreply.github.com>
Date: Wed, 27 Sep 2023 15:28:27 +0200
Subject: [PATCH 2/3] fix: update mobile schedule view (#2306)
* fix: update mobile schedule view
* remove old code
* fix: update header
* fix: prettier
---
client/src/components/Header.tsx | 35 ++++++-------------
.../MobileItemCard/MobileItemCard.tsx | 13 +++----
.../Schedule/pages/SchedulePage/index.tsx | 28 ++++++++-------
3 files changed, 33 insertions(+), 43 deletions(-)
diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx
index 5f9ea46a6c..32cef51f16 100644
--- a/client/src/components/Header.tsx
+++ b/client/src/components/Header.tsx
@@ -1,7 +1,7 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useContext, useMemo } from 'react';
-import { Button, Dropdown, Menu, Space, Tooltip } from 'antd';
+import { Button, Dropdown, Menu, Space } from 'antd';
import {
EyeOutlined,
LogoutOutlined,
@@ -36,6 +36,12 @@ const MENU_ITEMS = [
icon: ,
title: 'My CV',
},
+ {
+ link: 'https://docs.app.rs.school',
+ icon: ,
+ title: 'Help',
+ target: '_blank',
+ },
{
link: '/api/v2/auth/github/logout',
icon: ,
@@ -53,11 +59,11 @@ export function Header({ title, showCourseName }: Props) {
const menu = (
diff --git a/client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx b/client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx
index 9f864c78c7..509b454bf3 100644
--- a/client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx
+++ b/client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx
@@ -3,21 +3,22 @@ import { Row, Col, Typography } from 'antd';
import { SwapRightOutlined } from '@ant-design/icons';
import { coloredDateRenderer } from 'components/Table';
import { renderTagWithStyle, statusRenderer } from '../TableView/renderers';
+import Link from 'next/link';
const { Title } = Typography;
export const MobileItemCard = ({ item }: { item: CourseScheduleItemDto }) => {
return (
-
-
+
+
-
- {item.name}
-
+
+ {item.name}
+
{renderTagWithStyle(item.tag)}
-
+
{statusRenderer(item.status)}
{coloredDateRenderer('UTC', 'MMM D HH:mm', 'start', 'Recommended date for studying')(item.startDate, item)}
diff --git a/client/src/modules/Schedule/pages/SchedulePage/index.tsx b/client/src/modules/Schedule/pages/SchedulePage/index.tsx
index ca9da59913..19fa634acc 100644
--- a/client/src/modules/Schedule/pages/SchedulePage/index.tsx
+++ b/client/src/modules/Schedule/pages/SchedulePage/index.tsx
@@ -87,19 +87,21 @@ export function SchedulePage() {
<>
- setCopyModal({})}
- isCourseManager={isManager}
- courseId={course.id}
- courseAlias={course.alias}
- settings={settings}
- calendarToken={cipher}
- tags={eventTags}
- refreshData={refreshData}
- mobileView={mobileView}
- />
+ {!mobileView && (
+ setCopyModal({})}
+ isCourseManager={isManager}
+ courseId={course.id}
+ courseAlias={course.alias}
+ settings={settings}
+ calendarToken={cipher}
+ tags={eventTags}
+ refreshData={refreshData}
+ mobileView={mobileView}
+ />
+ )}
From e5de929166d864874a5e093a6050d9e8bd356227 Mon Sep 17 00:00:00 2001
From: Vadzim Antonau
Date: Wed, 27 Sep 2023 21:03:55 +0300
Subject: [PATCH 3/3] feat(heroes): implement heroes radar (#2280)
* feat(gratitudes): add heroes/radar endpoint to controller and service
* feat(gratitudes): add first and last name to heroes-radar response
* feat(gratitudes): add HeroesRadarQueryDto to correctly validate optional query param
* feat(gratitudes): add ApiPropertyOptional to HeroesRadarQueryDto
* feat(api): update openapi
* feat(heroes): move heroes route to folder
* feat(heroes): add blank heroes radar page
* feat(gratitudes): add dtos to have response types in client api
* feat(api): update openapi with dtos
* fix(gratitudes): fields naming returned from DB
* feat(heroes): move getFullName function to utils from component
* feat(heroes-radar): add data fetching and basic test layout
* feat(client): add HeroesRadarCard component
* feat(client): add HeroesCountBadge component
* refactor(client): update PublicFeedbackCard with HeroesCountBade
* feat(client): add GithubAvatar to HeroesRadarCard
* feat(client): add link to profile and total badges count at the bottom HeroesRadarCard
* refactor(client): move HeroesCountBadge and HeroesRadarCard to components
* feat(client): add course selection from to heroes radar and logic for it
* feat(nestjs): add pagination to Heroes Radar, create and update dtos
* feat(nestjs): update openapi
* feat(client): update Heroes Radar for new API with pagination
* feat(client): add Pagination to Heroes Radar
* feat(client): replace Mansory with Row in Heroes Radar
* feat(client): replace HeroesRadarCard with HeroesRadarTable
* feat(client): process pagination with courseId in Heroes Radar
* feat(client): add badges column width, get rid of some magic numbers
* refactor(heroes-radar): remove console.log
* fix(heroes-radar): used typeorm methods for pagination, optimize user info query
* feat(heroes-radar): add notActivist query, refactor getHeroesRadar service method
* feat(heroes-radar): update openapi
* feat(heroes-radar): add notActivist checkbox to form
* feat(heroes-radar): merge courseId and notActivist to one state
* feat(heroes-radar): move name definition to dto from client
* fix(heroes-radar): formatting
* fix(heroes-radar): misspelling in variable name
* refactor(profile): move missing key for HeroesCountBadge in PublicFeedbackCard
* fix(heroes): remove redundant indent
* fix(heroes-radar): setting parameter in sub query
* chore(setup): add feedback (gratitudes) seeds
* fix(heroes-radar): add try-finally in getHeroes
* refactor(heroes-radar): replace string concatenation with template string
* refactor(heroes-radar): get rid of redundant prefix in HeroesRadarBadgeDto
* refactor(heroes): add typing to heroesBadges, remove any type assertion
* refactor: move getFullName helper function to domain/user
* fix(heroes-radar): remove unused deps in useCallback hooks
* refactor(heroes-radar): fix formatting
* feat(heroes-radar): move rank logic to backend, make ranking like in score page
* refactor(gratitudes): replace with explicit conversion to number
* refactor(heroes-rardar): replace div with Space component
* refactor(heroes-radar): move onChange logic to parent component
* refactor(heroes-radar): rename property items to heroes, add HeroesRadar interface
* feat(heroes-radar): use pagination meta to calculate rank
* feat(heroes-radar): add align start to Space
* feat(heroes-radar): add additional ordering by github
* feat(heroes-radar): remove equal total equal rank logic
* refactor(heroes-radar): change submit button label
* refactor(heroes): make tabs at Heroes page, move Heroes Radar to HeroesRadarTab
* fix(heroes-radar): wrong import path
* feat(heroes): replace deprecated TabPane
* refactor(heroes): move tabs to separate variable
* refactor(heroes): remove commented code
* refactor(heroes-radar): replace Select.Option, remove margin left for clear button
* refactor(profile): add badgesCount typing, replace keys with entries
* refactor(heroes-radar): remove redundant Promise.all
* feat(heroes-radar): get rid of redundant makeRequest
---
client/src/api/api.ts | 167 ++++++++++++++++++
client/src/components/Forms/Heroes/index.tsx | 13 +-
.../components/Heroes/HeroesCountBadge.tsx | 17 ++
.../src/components/Heroes/HeroesRadarTab.tsx | 95 ++++++++++
.../components/Heroes/HeroesRadarTable.tsx | 123 +++++++++++++
.../components/Profile/PublicFeedbackCard.tsx | 21 +--
.../Profile/PublicFeedbackModal.tsx | 2 +-
client/src/configs/heroes-badges.ts | 4 +-
client/src/domain/user.ts | 4 +
.../Score/components/ScoreTable/index.tsx | 2 +-
client/src/pages/heroes.tsx | 19 +-
nestjs/src/gratitudes/dto/hero-radar.dto.ts | 37 ++++
.../gratitudes/dto/heroes-radar-badge.dto.ts | 20 +++
.../gratitudes/dto/heroes-radar-query.dto.ts | 34 ++++
nestjs/src/gratitudes/dto/heroes-radar.dto.ts | 30 ++++
nestjs/src/gratitudes/dto/index.ts | 1 +
.../src/gratitudes/gratitudes.controller.ts | 14 +-
nestjs/src/gratitudes/gratitudes.service.ts | 61 ++++++-
nestjs/src/spec.json | 43 +++++
setup/backup-local.sql | 99 +++++++++++
20 files changed, 775 insertions(+), 31 deletions(-)
create mode 100644 client/src/components/Heroes/HeroesCountBadge.tsx
create mode 100644 client/src/components/Heroes/HeroesRadarTab.tsx
create mode 100644 client/src/components/Heroes/HeroesRadarTable.tsx
create mode 100644 nestjs/src/gratitudes/dto/hero-radar.dto.ts
create mode 100644 nestjs/src/gratitudes/dto/heroes-radar-badge.dto.ts
create mode 100644 nestjs/src/gratitudes/dto/heroes-radar-query.dto.ts
create mode 100644 nestjs/src/gratitudes/dto/heroes-radar.dto.ts
diff --git a/client/src/api/api.ts b/client/src/api/api.ts
index ccdbd6a86c..2c901dcdd5 100644
--- a/client/src/api/api.ts
+++ b/client/src/api/api.ts
@@ -3005,6 +3005,81 @@ export const GratitudeDtoBadgeIdEnum = {
export type GratitudeDtoBadgeIdEnum = typeof GratitudeDtoBadgeIdEnum[keyof typeof GratitudeDtoBadgeIdEnum];
+/**
+ *
+ * @export
+ * @interface HeroRadarDto
+ */
+export interface HeroRadarDto {
+ /**
+ *
+ * @type {string}
+ * @memberof HeroRadarDto
+ */
+ 'githubId': string;
+ /**
+ *
+ * @type {string}
+ * @memberof HeroRadarDto
+ */
+ 'name': string;
+ /**
+ *
+ * @type {number}
+ * @memberof HeroRadarDto
+ */
+ 'rank': number;
+ /**
+ *
+ * @type {number}
+ * @memberof HeroRadarDto
+ */
+ 'total': number;
+ /**
+ *
+ * @type {Array}
+ * @memberof HeroRadarDto
+ */
+ 'badges': Array;
+}
+/**
+ *
+ * @export
+ * @interface HeroesRadarBadgeDto
+ */
+export interface HeroesRadarBadgeDto {
+ /**
+ *
+ * @type {string}
+ * @memberof HeroesRadarBadgeDto
+ */
+ 'id': string;
+ /**
+ *
+ * @type {number}
+ * @memberof HeroesRadarBadgeDto
+ */
+ 'count': number;
+}
+/**
+ *
+ * @export
+ * @interface HeroesRadarDto
+ */
+export interface HeroesRadarDto {
+ /**
+ *
+ * @type {Array}
+ * @memberof HeroesRadarDto
+ */
+ 'content': Array;
+ /**
+ *
+ * @type {PaginationMetaDto}
+ * @memberof HeroesRadarDto
+ */
+ 'pagination': PaginationMetaDto;
+}
/**
*
* @export
@@ -11883,6 +11958,59 @@ export const GratitudesApiAxiosParamCreator = function (configuration?: Configur
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @param {number} current
+ * @param {number} pageSize
+ * @param {number} [courseId]
+ * @param {boolean} [notActivist]
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getHeroesRadar: async (current: number, pageSize: number, courseId?: number, notActivist?: boolean, options: AxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'current' is not null or undefined
+ assertParamExists('getHeroesRadar', 'current', current)
+ // verify required parameter 'pageSize' is not null or undefined
+ assertParamExists('getHeroesRadar', 'pageSize', pageSize)
+ const localVarPath = `/gratitudes/heroes/radar`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ if (courseId !== undefined) {
+ localVarQueryParameter['courseId'] = courseId;
+ }
+
+ if (notActivist !== undefined) {
+ localVarQueryParameter['notActivist'] = notActivist;
+ }
+
+ if (current !== undefined) {
+ localVarQueryParameter['current'] = current;
+ }
+
+ if (pageSize !== undefined) {
+ localVarQueryParameter['pageSize'] = pageSize;
+ }
+
+
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -11922,6 +12050,19 @@ export const GratitudesApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getBadges(courseId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
+ /**
+ *
+ * @param {number} current
+ * @param {number} pageSize
+ * @param {number} [courseId]
+ * @param {boolean} [notActivist]
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.getHeroesRadar(current, pageSize, courseId, notActivist, options);
+ return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+ },
}
};
@@ -11950,6 +12091,18 @@ export const GratitudesApiFactory = function (configuration?: Configuration, bas
getBadges(courseId: number, options?: any): AxiosPromise> {
return localVarFp.getBadges(courseId, options).then((request) => request(axios, basePath));
},
+ /**
+ *
+ * @param {number} current
+ * @param {number} pageSize
+ * @param {number} [courseId]
+ * @param {boolean} [notActivist]
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, options?: any): AxiosPromise {
+ return localVarFp.getHeroesRadar(current, pageSize, courseId, notActivist, options).then((request) => request(axios, basePath));
+ },
};
};
@@ -11981,6 +12134,20 @@ export class GratitudesApi extends BaseAPI {
public getBadges(courseId: number, options?: AxiosRequestConfig) {
return GratitudesApiFp(this.configuration).getBadges(courseId, options).then((request) => request(this.axios, this.basePath));
}
+
+ /**
+ *
+ * @param {number} current
+ * @param {number} pageSize
+ * @param {number} [courseId]
+ * @param {boolean} [notActivist]
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof GratitudesApi
+ */
+ public getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, options?: AxiosRequestConfig) {
+ return GratitudesApiFp(this.configuration).getHeroesRadar(current, pageSize, courseId, notActivist, options).then((request) => request(this.axios, this.basePath));
+ }
}
diff --git a/client/src/components/Forms/Heroes/index.tsx b/client/src/components/Forms/Heroes/index.tsx
index 8d61753853..64baed09b6 100644
--- a/client/src/components/Forms/Heroes/index.tsx
+++ b/client/src/components/Forms/Heroes/index.tsx
@@ -7,7 +7,8 @@ import { IGratitudeGetRequest, IGratitudeGetResponse, HeroesFormData } from 'com
import heroesBadges from 'configs/heroes-badges';
import { GratitudeService } from 'services/gratitude';
import { onlyDefined } from 'utils/onlyDefined';
-import { useActiveCourseContext } from 'modules/Course/contexts';
+import { Course } from 'services/models';
+import { getFullName } from 'domain/user';
const { Text, Link, Paragraph } = Typography;
const { useBreakpoint } = Grid;
@@ -21,11 +22,7 @@ export const fields = {
courseId: 'courseId',
} as const;
-const getFullName = (user: { firstName: string | null; lastName: string | null; githubId: string }) =>
- user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : `${user.githubId}`;
-
-export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void }) => {
- const { courses } = useActiveCourseContext();
+export const HeroesForm = ({ setLoading, courses }: { setLoading: (arg: boolean) => void; courses: Course[] }) => {
const [heroesData, setHeroesData] = useState([]);
const [heroesCount, setHeroesCount] = useState(initialPage);
const [currentPage, setCurrentPage] = useState(initialPage);
@@ -122,7 +119,7 @@ export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void
@@ -137,7 +134,7 @@ export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void
diff --git a/client/src/components/Heroes/HeroesCountBadge.tsx b/client/src/components/Heroes/HeroesCountBadge.tsx
new file mode 100644
index 0000000000..548af7c4ed
--- /dev/null
+++ b/client/src/components/Heroes/HeroesCountBadge.tsx
@@ -0,0 +1,17 @@
+import { Badge, Tooltip, Avatar } from 'antd';
+import { HeroesRadarBadgeDto } from 'api';
+import heroesBadges from 'configs/heroes-badges';
+
+function HeroesCountBadge({ badge: { id, count } }: { badge: HeroesRadarBadgeDto }) {
+ return (
+
+ );
+}
+
+export default HeroesCountBadge;
diff --git a/client/src/components/Heroes/HeroesRadarTab.tsx b/client/src/components/Heroes/HeroesRadarTab.tsx
new file mode 100644
index 0000000000..c382f82219
--- /dev/null
+++ b/client/src/components/Heroes/HeroesRadarTab.tsx
@@ -0,0 +1,95 @@
+import { Button, Checkbox, Form, Select, Space, TableProps } from 'antd';
+import HeroesRadarTable from './HeroesRadarTable';
+import { HeroesRadarDto, GratitudesApi, HeroRadarDto } from 'api';
+import { IPaginationInfo } from 'common/types/pagination';
+import { useState, useEffect, useCallback } from 'react';
+import { Course } from 'services/models';
+import { onlyDefined } from 'utils/onlyDefined';
+
+export type HeroesRadarFormProps = {
+ courseId?: number;
+ notActivist?: boolean;
+};
+
+type GetHeroesProps = HeroesRadarFormProps & Partial
;
+
+export type LayoutType = Parameters[0]['layout'];
+
+const initialPage = 1;
+const initialPageSize = 20;
+const initialQueryParams = { current: initialPage, pageSize: initialPageSize };
+
+function HeroesRadarTab({ setLoading, courses }: { setLoading: (arg: boolean) => void; courses: Course[] }) {
+ const [heroes, setHeroes] = useState({
+ content: [],
+ pagination: { current: initialPage, pageSize: initialPageSize, itemCount: 0, total: 0, totalPages: 0 },
+ });
+ const [form] = Form.useForm();
+ const [formData, setFormData] = useState(form.getFieldsValue());
+ const [formLayout, setFormLayout] = useState('inline');
+ const gratitudeApi = new GratitudesApi();
+
+ const getHeroes = async ({
+ current = initialPage,
+ pageSize = initialPageSize,
+ courseId,
+ notActivist,
+ }: GetHeroesProps) => {
+ try {
+ setLoading(true);
+ const { data } = await gratitudeApi.getHeroesRadar(current, pageSize, courseId, notActivist);
+ setHeroes(data);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ getHeroes(initialQueryParams);
+ }, []);
+
+ const handleSubmit = useCallback(async (formData: HeroesRadarFormProps) => {
+ const data = onlyDefined(formData);
+ setFormData(data);
+ await getHeroes(data);
+ }, []);
+
+ const onClear = useCallback(async () => {
+ form.resetFields();
+ setFormData(form.getFieldsValue());
+ await getHeroes(initialQueryParams);
+ }, []);
+
+ const handleChange: TableProps['onChange'] = async ({ current, pageSize }) => {
+ try {
+ setLoading(true);
+ await getHeroes({ current, pageSize, ...formData });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+ Show only not activists
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default HeroesRadarTab;
diff --git a/client/src/components/Heroes/HeroesRadarTable.tsx b/client/src/components/Heroes/HeroesRadarTable.tsx
new file mode 100644
index 0000000000..10c81b4ac0
--- /dev/null
+++ b/client/src/components/Heroes/HeroesRadarTable.tsx
@@ -0,0 +1,123 @@
+import { Table, TableProps } from 'antd';
+import { ColumnType } from 'antd/lib/table';
+import { HeroRadarDto, HeroesRadarBadgeDto, HeroesRadarDto } from 'api';
+import { GithubAvatar } from 'components/GithubAvatar';
+import Link from 'next/link';
+import HeroesCountBadge from './HeroesCountBadge';
+import useWindowDimensions from 'utils/useWindowDimensions';
+import { useState, useEffect } from 'react';
+import type { LayoutType } from './HeroesRadarTab';
+import { getTableWidth } from 'modules/Score/components/ScoreTable';
+import heroesBadges from 'configs/heroes-badges';
+
+interface HeroesRadarTableProps {
+ heroes: HeroesRadarDto;
+ onChange: TableProps['onChange'];
+ setFormLayout: (layout: LayoutType) => void;
+}
+
+const BADGE_SIZE = 48;
+const BADGE_SUM_HORIZONTAL_MARGIN = 2 * 5;
+const XS_BREAKPOINT_IN_PX = 575;
+
+const initColumns: ColumnType[] = [
+ {
+ title: '#',
+ fixed: 'left',
+ dataIndex: 'rank',
+ key: 'rank',
+ width: 50,
+ defaultSortOrder: 'ascend',
+ sorter: (a, b) => a.rank - b.rank,
+ render: (value: number) => (value >= 999999 ? 'New' : value),
+ },
+ {
+ title: 'Github',
+ fixed: 'left',
+ key: 'githubId',
+ dataIndex: 'githubId',
+ width: 150,
+ render: (value: string) => (
+
+ ),
+ },
+ {
+ title: 'Name',
+ dataIndex: 'name',
+ key: 'name',
+ width: 150,
+ render: (value: string, record: HeroRadarDto) => (
+
+ {value}
+
+ ),
+ },
+ {
+ title: 'Total badges count',
+ dataIndex: 'total',
+ key: 'total',
+ width: 80,
+ render: (value: number) => {value},
+ },
+ {
+ title: 'Badges',
+ dataIndex: 'badges',
+ key: 'badges',
+ width: Object.keys(heroesBadges).length * (BADGE_SIZE + BADGE_SUM_HORIZONTAL_MARGIN),
+ responsive: ['xxl', 'xl', 'lg', 'md', 'sm'],
+ render: (value: HeroesRadarBadgeDto[], { githubId }: HeroRadarDto) => (
+ <>
+ {value.map(badge => (
+
+ ))}
+ >
+ ),
+ },
+];
+
+function HeroesRadarTable({ heroes, onChange, setFormLayout }: HeroesRadarTableProps) {
+ const { width } = useWindowDimensions();
+ const [fixedColumn, setFixedColumn] = useState(true);
+ const [columns, setColumns] = useState(initColumns);
+
+ useEffect(() => {
+ if (width < XS_BREAKPOINT_IN_PX) {
+ setFixedColumn(false);
+ setFormLayout('vertical');
+ return;
+ }
+
+ setFixedColumn(true);
+ setFormLayout('inline');
+ }, [width]);
+
+ useEffect(() => {
+ setColumns(prevColumns => {
+ const githubColumn = prevColumns.find(el => el.key === 'githubId');
+ if (githubColumn) {
+ githubColumn.fixed = fixedColumn ? 'left' : false;
+ }
+
+ return prevColumns;
+ });
+ }, [fixedColumn]);
+
+ return (
+ `Total ${total} students` }}
+ onChange={onChange}
+ rowKey="githubId"
+ scroll={{ x: getTableWidth(columns.length), y: 'calc(95vh - 290px)' }}
+ dataSource={heroes.content}
+ columns={columns}
+ />
+ );
+}
+
+export default HeroesRadarTable;
diff --git a/client/src/components/Profile/PublicFeedbackCard.tsx b/client/src/components/Profile/PublicFeedbackCard.tsx
index 3c6af75f53..5d816cb7af 100644
--- a/client/src/components/Profile/PublicFeedbackCard.tsx
+++ b/client/src/components/Profile/PublicFeedbackCard.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import isEqual from 'lodash/isEqual';
-import { Typography, Tooltip, Avatar, Badge } from 'antd';
+import { Typography, Tooltip } from 'antd';
import { Comment } from '@ant-design/compatible';
import FullscreenOutlined from '@ant-design/icons/FullscreenOutlined';
import MessageOutlined from '@ant-design/icons/MessageOutlined';
@@ -11,6 +11,7 @@ import { PublicFeedback } from 'common/models/profile';
import { GithubAvatar } from 'components/GithubAvatar';
import dayjs from 'dayjs';
import relative from 'dayjs/plugin/relativeTime';
+import HeroesCountBadge from 'components/Heroes/HeroesCountBadge';
dayjs.extend(relative);
@@ -43,7 +44,7 @@ class PublicFeedbackCard extends React.Component {
private countBadges = () => {
const receivedBadges = this.props.data;
- const badgesCount: any = {};
+ const badgesCount: Record = {};
receivedBadges.forEach(({ badgeId }) => {
if (badgeId) {
@@ -86,18 +87,8 @@ class PublicFeedbackCard extends React.Component {
Total badges: {badges.length}
- {Object.keys(badgesCount).map(badgeId => (
-
+ {Object.entries(badgesCount).map(([id, count]) => (
+
))}
@@ -112,7 +103,7 @@ class PublicFeedbackCard extends React.Component
{
<>
{badgeId ? (
- {(heroesBadges as any)[badgeId].name}
+ {heroesBadges[badgeId].name}
) : (
''
diff --git a/client/src/components/Profile/PublicFeedbackModal.tsx b/client/src/components/Profile/PublicFeedbackModal.tsx
index 5b8cae39c2..fcd43f91c1 100644
--- a/client/src/components/Profile/PublicFeedbackModal.tsx
+++ b/client/src/components/Profile/PublicFeedbackModal.tsx
@@ -39,7 +39,7 @@ class PublicFeedbackModal extends React.PureComponent {
<>
{badgeId ? (
- {(heroesBadges as any)[badgeId].name}
+ {heroesBadges[badgeId].name}
) : (
''
diff --git a/client/src/configs/heroes-badges.ts b/client/src/configs/heroes-badges.ts
index 4b51e28137..e2cc36ece0 100644
--- a/client/src/configs/heroes-badges.ts
+++ b/client/src/configs/heroes-badges.ts
@@ -1,4 +1,4 @@
-export default {
+const heroesBadges: Record = {
Congratulations: {
name: 'Congratulations',
pictureId: 23,
@@ -60,3 +60,5 @@ export default {
url: 'JuryTeam.svg',
},
};
+
+export default heroesBadges;
diff --git a/client/src/domain/user.ts b/client/src/domain/user.ts
index 6e1e379b28..bf33121a69 100644
--- a/client/src/domain/user.ts
+++ b/client/src/domain/user.ts
@@ -81,3 +81,7 @@ export function isTaskOwner(session: Session, courseId: number) {
export function isHirer(session: Session) {
return Boolean(session.isHirer);
}
+
+export function getFullName(user: { firstName: string | null; lastName: string | null; githubId: string }) {
+ return user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : `${user.githubId}`;
+}
diff --git a/client/src/modules/Score/components/ScoreTable/index.tsx b/client/src/modules/Score/components/ScoreTable/index.tsx
index 3af48cdcf3..9d7df04635 100644
--- a/client/src/modules/Score/components/ScoreTable/index.tsx
+++ b/client/src/modules/Score/components/ScoreTable/index.tsx
@@ -215,7 +215,7 @@ export function ScoreTable(props: Props) {
);
}
-function getTableWidth(columnsCount: number) {
+export function getTableWidth(columnsCount: number) {
const columnWidth = 90;
// where 800 is approximate sum of basic columns (GitHub, Name, etc.)
const tableWidth = columnsCount * columnWidth;
diff --git a/client/src/pages/heroes.tsx b/client/src/pages/heroes.tsx
index 145c8c009d..8bf4a18733 100644
--- a/client/src/pages/heroes.tsx
+++ b/client/src/pages/heroes.tsx
@@ -2,14 +2,31 @@ import { PageLayout } from 'components/PageLayout';
import { HeroesForm } from '../components/Forms/Heroes';
import { useState } from 'react';
import { ActiveCourseProvider, SessionProvider } from 'modules/Course/contexts';
+import { Tabs } from 'antd';
+import HeroesRadarTab from 'components/Heroes/HeroesRadarTab';
+import { Course } from 'services/models';
+import { CoursesService } from 'services/courses';
+import { useAsync } from 'react-use';
function Page() {
const [loading, setLoading] = useState(false);
+ const [courses, setCourses] = useState([]);
+
+ const tabs = [
+ { label: 'Gratitudes', key: '1', children: },
+ { label: 'Heroes Radar', key: '2', children: },
+ ];
+
+ useAsync(async () => {
+ const courses = await new CoursesService().getCourses();
+ setCourses(courses);
+ }, []);
+
return (
-
+
diff --git a/nestjs/src/gratitudes/dto/hero-radar.dto.ts b/nestjs/src/gratitudes/dto/hero-radar.dto.ts
new file mode 100644
index 0000000000..5b2b9805e2
--- /dev/null
+++ b/nestjs/src/gratitudes/dto/hero-radar.dto.ts
@@ -0,0 +1,37 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { HeroesRadarBadge, HeroesRadarBadgeDto } from './heroes-radar-badge.dto';
+import { PersonDto } from 'src/core/dto';
+
+export interface HeroRadar {
+ githubId: string;
+ firstName: string;
+ lastName: string;
+ rank: number;
+ total: number;
+ badges: HeroesRadarBadge[];
+}
+
+export class HeroRadarDto {
+ constructor(hero: HeroRadar) {
+ this.githubId = hero.githubId;
+ this.name = PersonDto.getName(hero);
+ this.rank = hero.rank;
+ this.total = hero.total;
+ this.badges = hero.badges.map(badge => new HeroesRadarBadgeDto(badge));
+ }
+
+ @ApiProperty()
+ public githubId: string;
+
+ @ApiProperty()
+ public name: string;
+
+ @ApiProperty()
+ public rank: number;
+
+ @ApiProperty()
+ public total: number;
+
+ @ApiProperty({ type: [HeroesRadarBadgeDto] })
+ badges: HeroesRadarBadgeDto[];
+}
diff --git a/nestjs/src/gratitudes/dto/heroes-radar-badge.dto.ts b/nestjs/src/gratitudes/dto/heroes-radar-badge.dto.ts
new file mode 100644
index 0000000000..b15d4fa1e1
--- /dev/null
+++ b/nestjs/src/gratitudes/dto/heroes-radar-badge.dto.ts
@@ -0,0 +1,20 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Badge } from './badge.dto';
+
+export interface HeroesRadarBadge {
+ id: Badge;
+ count: number;
+}
+
+export class HeroesRadarBadgeDto {
+ constructor(badge: HeroesRadarBadge) {
+ this.id = badge.id;
+ this.count = badge.count;
+ }
+
+ @ApiProperty()
+ public id: Badge;
+
+ @ApiProperty()
+ public count: number;
+}
diff --git a/nestjs/src/gratitudes/dto/heroes-radar-query.dto.ts b/nestjs/src/gratitudes/dto/heroes-radar-query.dto.ts
new file mode 100644
index 0000000000..3c66450c95
--- /dev/null
+++ b/nestjs/src/gratitudes/dto/heroes-radar-query.dto.ts
@@ -0,0 +1,34 @@
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
+import { Transform, Type } from 'class-transformer';
+import { IsBoolean, IsInt, IsOptional } from 'class-validator';
+
+export class HeroesRadarQueryDto {
+ @ApiPropertyOptional()
+ @IsOptional()
+ @IsInt()
+ @Type(() => Number)
+ courseId?: number;
+
+ @ApiPropertyOptional()
+ @Transform(
+ ({ value }: { value: string }) => {
+ const newValue = value.toLowerCase();
+
+ return newValue === 'true' || newValue === '1';
+ },
+ { toClassOnly: true },
+ )
+ @IsOptional()
+ @IsBoolean()
+ notActivist?: boolean;
+
+ @ApiProperty()
+ @IsInt()
+ @Type(() => Number)
+ public current: number;
+
+ @ApiProperty()
+ @IsInt()
+ @Type(() => Number)
+ public pageSize: number;
+}
diff --git a/nestjs/src/gratitudes/dto/heroes-radar.dto.ts b/nestjs/src/gratitudes/dto/heroes-radar.dto.ts
new file mode 100644
index 0000000000..e2960cca78
--- /dev/null
+++ b/nestjs/src/gratitudes/dto/heroes-radar.dto.ts
@@ -0,0 +1,30 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { HeroRadar, HeroRadarDto } from './hero-radar.dto';
+import { PaginationMeta } from 'src/core/paginate';
+import { PaginationMetaDto } from 'src/core/paginate/dto/Paginate.dto';
+
+interface HeroesRadar {
+ heroes: HeroRadar[];
+ meta: PaginationMeta;
+}
+
+const calculateRank = ({ heroes, meta }: HeroesRadar): HeroRadar[] => {
+ const rankedHeroes = heroes.map((hero, index) => {
+ const rank = index + 1 + meta.pageSize * (meta.current - 1);
+ return { ...hero, rank };
+ });
+ return rankedHeroes;
+};
+
+export class HeroesRadarDto {
+ constructor(heroesRadar: HeroesRadar) {
+ this.content = calculateRank(heroesRadar).map(hero => new HeroRadarDto(hero));
+ this.pagination = new PaginationMetaDto(heroesRadar.meta);
+ }
+
+ @ApiProperty({ type: [HeroRadarDto] })
+ public content: HeroRadarDto[];
+
+ @ApiProperty({ type: PaginationMetaDto })
+ pagination: PaginationMetaDto;
+}
diff --git a/nestjs/src/gratitudes/dto/index.ts b/nestjs/src/gratitudes/dto/index.ts
index 60bbd6f351..0abfb86147 100644
--- a/nestjs/src/gratitudes/dto/index.ts
+++ b/nestjs/src/gratitudes/dto/index.ts
@@ -1,3 +1,4 @@
export * from './create-gratitude.dto';
export * from './gratitude.dto';
export * from './badge.dto';
+export * from './heroes-radar-query.dto';
diff --git a/nestjs/src/gratitudes/gratitudes.controller.ts b/nestjs/src/gratitudes/gratitudes.controller.ts
index 8e67ed957a..6d9318d4fe 100644
--- a/nestjs/src/gratitudes/gratitudes.controller.ts
+++ b/nestjs/src/gratitudes/gratitudes.controller.ts
@@ -1,8 +1,9 @@
-import { Body, Controller, Get, Param, ParseIntPipe, Post, Req, UseGuards } from '@nestjs/common';
+import { Body, Controller, Get, Param, ParseIntPipe, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CurrentRequest, DefaultGuard } from '../auth';
-import { BadgeDto, CreateGratitudeDto, GratitudeDto } from './dto';
+import { BadgeDto, CreateGratitudeDto, GratitudeDto, HeroesRadarQueryDto } from './dto';
import { GratitudesService } from './gratitudes.service';
+import { HeroesRadarDto } from './dto/heroes-radar.dto';
@Controller('gratitudes')
@ApiTags('gratitudes')
@@ -24,4 +25,13 @@ export class GratitudesController {
const badges = this.service.getBadges(req.user, courseId);
return badges.map(badge => new BadgeDto(badge));
}
+
+ @Get('/heroes/radar')
+ @ApiOperation({ operationId: 'getHeroesRadar' })
+ @ApiOkResponse({ type: HeroesRadarDto })
+ public async getHeroesRadar(@Query() query: HeroesRadarQueryDto) {
+ const heroes = await this.service.getHeroesRadar(query);
+
+ return new HeroesRadarDto(heroes);
+ }
}
diff --git a/nestjs/src/gratitudes/gratitudes.service.ts b/nestjs/src/gratitudes/gratitudes.service.ts
index 46b8e6d0ed..c97181280f 100644
--- a/nestjs/src/gratitudes/gratitudes.service.ts
+++ b/nestjs/src/gratitudes/gratitudes.service.ts
@@ -2,9 +2,9 @@ import { Feedback } from '@entities/feedback';
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthUser, CourseRole } from 'src/auth';
-import { Repository } from 'typeorm';
+import { Repository, DataSource } from 'typeorm';
import { DiscordService } from './discord.service';
-import { Badge, CreateGratitudeDto } from './dto';
+import { Badge, CreateGratitudeDto, HeroesRadarQueryDto } from './dto';
@Injectable()
export class GratitudesService {
@@ -14,6 +14,7 @@ export class GratitudesService {
private discordService: DiscordService,
@InjectRepository(Feedback)
private repository: Repository,
+ private dataSource: DataSource,
) {}
public async create(authUser: AuthUser, data: CreateGratitudeDto) {
@@ -51,6 +52,62 @@ export class GratitudesService {
});
}
+ public async getHeroesRadar({ courseId, current: page = 1, pageSize = 20, notActivist }: HeroesRadarQueryDto) {
+ const countSubQuery = this.repository.createQueryBuilder('feedback');
+ const countQuery = this.dataSource.createQueryBuilder();
+
+ const heroesSubQuery = this.repository.createQueryBuilder('feedback');
+ const heroesQuery = this.dataSource.createQueryBuilder();
+
+ if (notActivist) {
+ [countSubQuery, heroesQuery].forEach(query => query.having(`not bool_or("badgeId" = 'RS_activist')`));
+ }
+
+ if (courseId) {
+ [heroesSubQuery, countSubQuery].forEach(query => query.where('feedback."courseId" = :courseId', { courseId }));
+ }
+
+ countSubQuery.select(`jsonb_agg(json_build_object('badgeId', "badgeId"))`).groupBy('"toUserId"');
+ countQuery
+ .select('COUNT(*) as count')
+ .from(`(${countSubQuery.getQuery()})`, 'badges')
+ .setParameters(countSubQuery.getParameters());
+
+ heroesSubQuery
+ .select(['"feedback"."badgeId"', '"feedback"."toUserId"', 'COUNT(*) as "badgeCount"'])
+ .groupBy('"badgeId"')
+ .addGroupBy('"toUserId"');
+
+ heroesQuery
+ .select([
+ '"user"."githubId"',
+ '"user"."firstName"',
+ '"user"."lastName"',
+ 'sum("badgeCount") as total',
+ `jsonb_agg(json_build_object('id', "badgeId", 'count', "badgeCount")) as badges`,
+ ])
+ .from(`(${heroesSubQuery.getQuery()})`, 'badges')
+ .leftJoin('user', 'user', 'badges."toUserId" = "user"."id"')
+ .groupBy('"githubId"')
+ .addGroupBy('"firstName"')
+ .addGroupBy('"lastName"')
+ .orderBy('total', 'DESC')
+ .addOrderBy('"githubId"', 'ASC')
+ .limit(pageSize)
+ .offset((page - 1) * pageSize)
+ .setParameters(heroesSubQuery.getParameters());
+
+ const { count } = await countQuery.getRawOne();
+ const total = Number(count);
+ const heroes = await heroesQuery.getRawMany();
+ const totalPages = Math.ceil(total / pageSize);
+
+ return {
+ heroes,
+ meta: { itemCount: heroes.length, total, current: page, pageSize, totalPages },
+ };
+ }
+
private async postUserFeedback(data: Feedback) {
const feedback = await this.createFeedback(data);
await this.postToDiscord(feedback);
diff --git a/nestjs/src/spec.json b/nestjs/src/spec.json
index cc9d299cf1..4d21ec4317 100644
--- a/nestjs/src/spec.json
+++ b/nestjs/src/spec.json
@@ -2077,6 +2077,25 @@
"tags": ["gratitudes"]
}
},
+ "/gratitudes/heroes/radar": {
+ "get": {
+ "operationId": "getHeroesRadar",
+ "summary": "",
+ "parameters": [
+ { "name": "courseId", "required": false, "in": "query", "schema": { "type": "number" } },
+ { "name": "notActivist", "required": false, "in": "query", "schema": { "type": "boolean" } },
+ { "name": "current", "required": true, "in": "query", "schema": { "type": "number" } },
+ { "name": "pageSize", "required": true, "in": "query", "schema": { "type": "number" } }
+ ],
+ "responses": {
+ "200": {
+ "description": "",
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HeroesRadarDto" } } }
+ }
+ },
+ "tags": ["gratitudes"]
+ }
+ },
"/events": {
"get": {
"operationId": "getEvents",
@@ -4326,6 +4345,30 @@
},
"required": ["name", "id"]
},
+ "HeroesRadarBadgeDto": {
+ "type": "object",
+ "properties": { "id": { "type": "string" }, "count": { "type": "number" } },
+ "required": ["id", "count"]
+ },
+ "HeroRadarDto": {
+ "type": "object",
+ "properties": {
+ "githubId": { "type": "string" },
+ "name": { "type": "string" },
+ "rank": { "type": "number" },
+ "total": { "type": "number" },
+ "badges": { "type": "array", "items": { "$ref": "#/components/schemas/HeroesRadarBadgeDto" } }
+ },
+ "required": ["githubId", "name", "rank", "total", "badges"]
+ },
+ "HeroesRadarDto": {
+ "type": "object",
+ "properties": {
+ "content": { "type": "array", "items": { "$ref": "#/components/schemas/HeroRadarDto" } },
+ "pagination": { "$ref": "#/components/schemas/PaginationMetaDto" }
+ },
+ "required": ["content", "pagination"]
+ },
"EventDto": {
"type": "object",
"properties": {
diff --git a/setup/backup-local.sql b/setup/backup-local.sql
index 5de14d635c..80b80651f4 100644
--- a/setup/backup-local.sql
+++ b/setup/backup-local.sql
@@ -2966,6 +2966,105 @@ COPY public.event (id, "createdDate", "updatedDate", name, "descriptionUrl", des
--
COPY public.feedback (id, "createdDate", "updatedDate", "badgeId", "fromUserId", "toUserId", "courseId", comment) FROM stdin;
+616 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 3493 677 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+617 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 677 587 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+618 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 2444 2595 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+619 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 4428 10130 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+620 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 606 677 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+621 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 2595 10130 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+622 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 2693 11563 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+623 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 2480 6776 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+624 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Thank_you 606 4428 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+625 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 1090 4428 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+626 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 4428 2480 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+627 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 2103 587 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+628 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 1090 2098 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+629 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 677 10130 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+630 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 3961 2098 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+631 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Expert_help 2480 2480 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+632 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 606 2612 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+633 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Thank_you 2084 587 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+634 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 1328 677 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+635 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 2084 4476 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+636 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 2549 2595 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+637 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Hero 587 2089 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+638 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 677 11569 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+639 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 RS_activist 4428 2098 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+640 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 RS_activist 2480 7485 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+641 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 587 2595 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+642 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 1090 10031 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+643 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 6776 677 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+644 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 7485 10031 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+645 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 7485 2444 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+646 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Hero 2595 2084 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+647 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 10130 2693 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+648 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Hero 11569 11569 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+649 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Hero 2103 10130 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+650 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Expert_help 3961 2084 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+651 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 5481 1090 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+652 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 3961 677 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+653 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Thank_you 5481 2115 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+654 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 2089 2693 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+655 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 2595 1090 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+656 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 2612 4428 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+657 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 RS_activist 2595 11569 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+658 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 3961 2693 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+659 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Good_job 11569 677 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+660 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 4749 2693 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+661 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 RS_activist 2115 2115 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+662 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Thank_you 2444 11563 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+663 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 2089 2103 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+664 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 2084 11569 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+665 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 587 606 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+666 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 2480 677 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+667 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 587 2032 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+668 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 RS_activist 10031 2084 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+669 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 606 2612 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+670 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 11569 2549 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+671 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Thank_you 4749 2098 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+672 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 2549 11563 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+673 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 1090 2480 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+674 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 7485 677 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+675 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 RS_activist 7485 2277 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+676 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 RS_activist 3961 1090 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+677 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 7485 1090 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+678 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 2480 5481 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+679 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 3961 10130 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+680 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 2595 5481 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+681 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Expert_help 2277 2612 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+682 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 11569 2032 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+683 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 4749 677 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+684 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 RS_activist 1328 2480 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+685 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 4428 2612 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+686 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Hero 4749 4476 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+687 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 2480 10130 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+688 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 2115 4428 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+689 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Good_job 4749 2103 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+690 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 2089 2693 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+691 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Great_speaker 2693 5481 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+692 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 4749 677 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+693 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 2549 606 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+694 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Expert_help 4476 1090 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+695 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 2103 11569 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+696 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 5481 1090 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+697 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 4476 2612 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+698 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Good_job 4749 2595 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+699 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 2115 11563 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+700 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 2693 677 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+701 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 11569 2084 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+702 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Jury_Team 2444 1090 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+703 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Good_job 1328 606 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+704 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Top_performer 4749 2549 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+705 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 10031 3961 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+706 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 2693 2098 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+707 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 2084 2549 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+708 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Congratulations 1328 4428 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+709 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Thank_you 4428 10130 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+710 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Thank_you 2089 4428 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+711 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Job_Offer 4476 4428 13 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+712 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Helping_hand 587 7485 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+713 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Thank_you 2103 606 11 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
+714 2023-08-26 14:16:06.484519 2023-08-26 14:16:06.484519 Outstanding_work 2693 3961 23 Pariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.
\.