Skip to content

Commit

Permalink
feat(heroes): implement heroes radar (#2280)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ThorsAngerVaNeT authored Sep 27, 2023
1 parent 39e6af1 commit e5de929
Show file tree
Hide file tree
Showing 20 changed files with 775 additions and 31 deletions.
167 changes: 167 additions & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HeroesRadarBadgeDto>}
* @memberof HeroRadarDto
*/
'badges': Array<HeroesRadarBadgeDto>;
}
/**
*
* @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<HeroRadarDto>}
* @memberof HeroesRadarDto
*/
'content': Array<HeroRadarDto>;
/**
*
* @type {PaginationMetaDto}
* @memberof HeroesRadarDto
*/
'pagination': PaginationMetaDto;
}
/**
*
* @export
Expand Down Expand Up @@ -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<RequestArgs> => {
// 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};
Expand Down Expand Up @@ -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<HeroesRadarDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getHeroesRadar(current, pageSize, courseId, notActivist, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};

Expand Down Expand Up @@ -11950,6 +12091,18 @@ export const GratitudesApiFactory = function (configuration?: Configuration, bas
getBadges(courseId: number, options?: any): AxiosPromise<Array<BadgeDto>> {
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<HeroesRadarDto> {
return localVarFp.getHeroesRadar(current, pageSize, courseId, notActivist, options).then((request) => request(axios, basePath));
},
};
};

Expand Down Expand Up @@ -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));
}
}


Expand Down
13 changes: 5 additions & 8 deletions client/src/components/Forms/Heroes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<IGratitudeGetResponse[]>([]);
const [heroesCount, setHeroesCount] = useState(initialPage);
const [currentPage, setCurrentPage] = useState(initialPage);
Expand Down Expand Up @@ -122,7 +119,7 @@ export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void
<Card style={{ position: 'relative', background: 'none' }}>
<div
className="badge-bg"
style={{ backgroundImage: `url(/static/svg/badges/${(heroesBadges as any)[feedback.badgeId].url})` }}
style={{ backgroundImage: `url(/static/svg/badges/${heroesBadges[feedback.badgeId].url})` }}
/>
<div className="badge-note" style={{ marginBottom: 48 }}>
<Paragraph style={{ margin: 0 }}>
Expand All @@ -137,7 +134,7 @@ export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void
<div className="flex-center" style={{ marginBottom: 48 }}>
<div className="badge">
<Avatar
src={`/static/svg/badges/${(heroesBadges as any)[feedback.badgeId].url}`}
src={`/static/svg/badges/${heroesBadges[feedback.badgeId].url}`}
alt={`${feedback.badgeId} badge`}
size={128}
/>
Expand Down
17 changes: 17 additions & 0 deletions client/src/components/Heroes/HeroesCountBadge.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ margin: 5, display: 'inline-block' }}>
<Badge count={count}>
<Tooltip title={heroesBadges[id].name}>
<Avatar src={`/static/svg/badges/${heroesBadges[id].url}`} alt={`${id} badge`} size={48} />
</Tooltip>
</Badge>
</div>
);
}

export default HeroesCountBadge;
95 changes: 95 additions & 0 deletions client/src/components/Heroes/HeroesRadarTab.tsx
Original file line number Diff line number Diff line change
@@ -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<IPaginationInfo>;

export type LayoutType = Parameters<typeof Form>[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<HeroesRadarDto>({
content: [],
pagination: { current: initialPage, pageSize: initialPageSize, itemCount: 0, total: 0, totalPages: 0 },
});
const [form] = Form.useForm();
const [formData, setFormData] = useState<HeroesRadarFormProps>(form.getFieldsValue());
const [formLayout, setFormLayout] = useState<LayoutType>('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<HeroRadarDto>['onChange'] = async ({ current, pageSize }) => {
try {
setLoading(true);
await getHeroes({ current, pageSize, ...formData });
} finally {
setLoading(false);
}
};

return (
<>
<Form layout={formLayout} form={form} onFinish={handleSubmit} style={{ marginBottom: 24 }}>
<Form.Item name={'courseId'} label="Courses" style={{ minWidth: 260, marginBottom: 16 }}>
<Select options={courses.map(({ id, name }) => ({ value: id, label: name }))} />
</Form.Item>
<Form.Item name={'notActivist'} valuePropName="checked" style={{ marginBottom: 16 }}>
<Checkbox>Show only not activists</Checkbox>
</Form.Item>
<Space align="start" size={20}>
<Button size="middle" type="primary" htmlType="submit">
Filter
</Button>
<Button size="middle" type="primary" onClick={onClear}>
Clear
</Button>
</Space>
</Form>
<HeroesRadarTable heroes={heroes} onChange={handleChange} setFormLayout={setFormLayout} />
</>
);
}

export default HeroesRadarTab;
Loading

0 comments on commit e5de929

Please sign in to comment.