Skip to content

Commit

Permalink
Merge branch 'master' into rs-app-2147
Browse files Browse the repository at this point in the history
  • Loading branch information
Alphajax committed Oct 27, 2023
2 parents c3b9fc3 + 39080a1 commit 0dbe212
Show file tree
Hide file tree
Showing 53 changed files with 1,358 additions and 460 deletions.
247 changes: 238 additions & 9 deletions client/src/api/api.ts

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions client/src/components/Forms/Heroes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +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 { Course } from 'services/models';
import { getFullName } from 'domain/user';
import { useActiveCourseContext } from 'modules/Course/contexts';

const { Text, Link, Paragraph } = Typography;
const { useBreakpoint } = Grid;
Expand All @@ -22,7 +22,9 @@ export const fields = {
courseId: 'courseId',
} as const;

export const HeroesForm = ({ setLoading, courses }: { setLoading: (arg: boolean) => void; courses: Course[] }) => {
export const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void }) => {
const { courses } = useActiveCourseContext();

const [heroesData, setHeroesData] = useState<IGratitudeGetResponse[]>([]);
const [heroesCount, setHeroesCount] = useState(initialPage);
const [currentPage, setCurrentPage] = useState(initialPage);
Expand Down
69 changes: 41 additions & 28 deletions client/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useContext, useMemo } from 'react';
import { useContext, useMemo } from 'react';
import { Button, Dropdown, Menu, Space } from 'antd';
import type { MenuProps } from 'antd';
import {
EyeOutlined,
LogoutOutlined,
Expand All @@ -14,17 +15,20 @@ import { SolidarityUkraine } from './SolidarityUkraine';
import { SessionContext } from 'modules/Course/contexts';
import { getNavigationItems } from 'modules/Home/data/links';
import { useActiveCourseContext } from 'modules/Course/contexts/ActiveCourseContext';
import css from 'styled-jsx/css';

type Props = {
showCourseName?: boolean;
title?: string;
};

type MenuItem = Required<MenuProps>['items'][number];

const MENU_ITEMS = [
{
link: '/profile',
icon: <EyeOutlined />,
title: 'View',
title: 'Profile',
},
{
link: '/profile/notifications',
Expand All @@ -51,26 +55,29 @@ const MENU_ITEMS = [

export function Header({ title, showCourseName }: Props) {
const { asPath: currentRoute } = useRouter();
const menuActiveItemStyle = { backgroundColor: '#e0f2ff' };

const session = useContext(SessionContext);
const { course } = useActiveCourseContext();
const courseLinks = useMemo(() => getNavigationItems(session, course ?? null), [course]);

const menu = (
<Menu>
{MENU_ITEMS.map(({ link, icon, title, target }, id, arr) => (
<Fragment key={id}>
{id === arr.length - 1 ? <Menu.Divider /> : null}
<Menu.Item key={id} style={currentRoute === link ? menuActiveItemStyle : undefined}>
<Button type="link" target={target} href={link} style={{ textAlign: 'left', width: '100%' }}>
{icon} {title}
</Button>
</Menu.Item>
</Fragment>
))}
</Menu>
);
const menuItems = useMemo((): MenuProps['items'] => {
const items = MENU_ITEMS.map(({ title, link, target, icon }) => {
const isActive = currentRoute === link;

return {
key: title,
label: (
<Button type="link" target={target} href={link} className={isActive ? 'menu-item-active' : undefined}>
{icon} {title}
</Button>
),
};
});

const lastItem = items.pop() as MenuItem;

return [...items, { type: 'divider' }, lastItem];
}, [currentRoute]);

return (
<Space
Expand Down Expand Up @@ -104,25 +111,31 @@ export function Header({ title, showCourseName }: Props) {
</div>
<div className="profile">
{session.githubId && (
<Dropdown overlay={menu} trigger={['click']}>
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
<Button type="link">
<GithubAvatar githubId={session?.githubId} size={32} />
</Button>
</Dropdown>
)}
</div>
<style jsx>{`
@media all and (max-width: 768px) {
.title {
width: 100%;
order: 3;
text-align: center;
margin-top: 16px;
}
}
`}</style>
<style jsx>{styles}</style>
</nav>
<Menu selectedKeys={[currentRoute]} mode="horizontal" items={courseLinks} />
</Space>
);
}

const styles = css`
:global(li:has(.menu-item-active)) {
background-color: #e0f2ff;
}
@media all and (max-width: 768px) {
.title {
width: 100%;
order: 3;
text-align: center;
margin-top: 16px;
}
}
`;
26 changes: 23 additions & 3 deletions client/src/components/Heroes/HeroesCountBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import { Badge, Tooltip, Avatar } from 'antd';
import { HeroesRadarBadgeDto } from 'api';
import heroesBadges from 'configs/heroes-badges';
import dayjs from 'dayjs';

function HeroesCountBadge({ badge: { id, count } }: { badge: HeroesRadarBadgeDto }) {
type HeroesCountBadgeProps = {
badge: Omit<HeroesRadarBadgeDto, 'id' | 'comment' | 'date'> &
Partial<Pick<HeroesRadarBadgeDto, 'comment' | 'date'> & { count: number }>;
};

function HeroesCountBadge({ badge: { badgeId, count = 0, comment, date } }: HeroesCountBadgeProps) {
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
title={
<>
{heroesBadges[badgeId].name}
{comment && date && (
<>
<br />
{comment}
<br />
{dayjs(date).format('YYYY-MM-DD HH:mm')}
</>
)}
</>
}
>
<Avatar src={`/static/svg/badges/${heroesBadges[badgeId].url}`} alt={`${badgeId} badge`} size={48} />
</Tooltip>
</Badge>
</div>
Expand Down
119 changes: 98 additions & 21 deletions client/src/components/Heroes/HeroesRadarTab.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Button, Checkbox, Form, Select, Space, TableProps } from 'antd';
import { Button, Checkbox, DatePicker, Form, Select, Space, TableProps, Row } from 'antd';
import { FileExcelOutlined } from '@ant-design/icons';
import HeroesRadarTable from './HeroesRadarTable';
import { HeroesRadarDto, GratitudesApi, HeroRadarDto } from 'api';
import { HeroesRadarDto, GratitudesApi, HeroRadarDto, CountryDto } from 'api';
import { IPaginationInfo } from 'common/types/pagination';
import { useState, useEffect, useCallback } from 'react';
import { Course } from 'services/models';
import { useState, useEffect, useCallback, useContext } from 'react';
import { onlyDefined } from 'utils/onlyDefined';
import dayjs from 'dayjs';
import type { TimeRangePickerProps } from 'antd';
import type { Dayjs } from 'dayjs';
import { SessionContext, useActiveCourseContext } from 'modules/Course/contexts';

export type HeroesRadarFormProps = {
courseId?: number;
notActivist?: boolean;
countryName?: string;
dates?: (Dayjs | null)[];
};

type GetHeroesProps = HeroesRadarFormProps & Partial<IPaginationInfo>;
Expand All @@ -19,25 +25,57 @@ const initialPage = 1;
const initialPageSize = 20;
const initialQueryParams = { current: initialPage, pageSize: initialPageSize };

function HeroesRadarTab({ setLoading, courses }: { setLoading: (arg: boolean) => void; courses: Course[] }) {
const { RangePicker } = DatePicker;

const currentDayjs = dayjs();
const rangePresets: TimeRangePickerProps['presets'] = [
{ label: 'Last 7 Days', value: [currentDayjs.add(-7, 'd'), currentDayjs] },
{ label: 'Last 14 Days', value: [currentDayjs.add(-14, 'd'), currentDayjs] },
{ label: 'Last 30 Days', value: [currentDayjs.add(-30, 'd'), currentDayjs] },
{ label: 'Last 90 Days', value: [currentDayjs.add(-90, 'd'), currentDayjs] },
];

function HeroesRadarTab({ setLoading }: { setLoading: (arg: boolean) => void }) {
const { courses } = useActiveCourseContext();

const [heroes, setHeroes] = useState<HeroesRadarDto>({
content: [],
pagination: { current: initialPage, pageSize: initialPageSize, itemCount: 0, total: 0, totalPages: 0 },
});

const [countries, setCountries] = useState<CountryDto[]>([]);
const [form] = Form.useForm();
const [formData, setFormData] = useState<HeroesRadarFormProps>(form.getFieldsValue());
const [formLayout, setFormLayout] = useState<LayoutType>('inline');
const { isAdmin } = useContext(SessionContext);

const gratitudeApi = new GratitudesApi();

const getCountries = async () => {
const { data } = await gratitudeApi.getHeroesCountries();
setCountries(data);
};

const getHeroes = async ({
current = initialPage,
pageSize = initialPageSize,
courseId,
notActivist,
countryName,
dates,
}: GetHeroesProps) => {
try {
setLoading(true);
const { data } = await gratitudeApi.getHeroesRadar(current, pageSize, courseId, notActivist);
const [startDate, endDate] = dates?.map(date => date?.format('YYYY-MM-DD')) ?? [];
const { data } = await gratitudeApi.getHeroesRadar(
current,
pageSize,
courseId,
notActivist,
countryName,
startDate,
endDate,
);
setHeroes(data);
} finally {
setLoading(false);
Expand All @@ -46,6 +84,7 @@ function HeroesRadarTab({ setLoading, courses }: { setLoading: (arg: boolean) =>

useEffect(() => {
getHeroes(initialQueryParams);
isAdmin && getCountries();
}, []);

const handleSubmit = useCallback(async (formData: HeroesRadarFormProps) => {
Expand All @@ -69,24 +108,62 @@ function HeroesRadarTab({ setLoading, courses }: { setLoading: (arg: boolean) =>
}
};

const exportToCsv = () => {
const data = onlyDefined(formData);
const formParams = Object.entries(data).reduce((acc: string[][], [key, value]) => {
if (key === 'dates' && Array.isArray(value)) {
const [startDate, endDate] = value.map(date => date?.format('YYYY-MM-DD'));
return [...acc, ['startDate', `${startDate}`], ['endDate', `${endDate}`]];
}
return [...acc, [key, `${value}`]];
}, []);

const params = new URLSearchParams([['current', '1'], ['pageSize', `${heroes.pagination.total}`], ...formParams]);
window.location.href = `/api/v2/gratitudes/heroes/radar/csv?${params}`;
};

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
<Row style={{ marginBottom: 24 }} justify="space-between">
<Form layout={formLayout} form={form} onFinish={handleSubmit}>
<Form.Item name={'courseId'} label="Courses" style={{ minWidth: 260, marginBottom: 16 }}>
<Select
placeholder="Select course"
showSearch
optionFilterProp="label"
options={courses.map(({ id, name }) => ({ value: id, label: name }))}
/>
</Form.Item>
{isAdmin && (
<Form.Item name={'countryName'} label="Countries" style={{ minWidth: 260, marginBottom: 16 }}>
<Select
placeholder="Select country"
showSearch
options={countries.map(({ countryName }) => ({ value: countryName, label: countryName }))}
/>
</Form.Item>
)}
<Form.Item name={'dates'} label="Dates" style={{ minWidth: 260, marginBottom: 16 }}>
<RangePicker presets={rangePresets} />
</Form.Item>
<Form.Item name={'notActivist'} valuePropName="checked" style={{ marginBottom: 16 }}>
<Checkbox>Show only not activists</Checkbox>
</Form.Item>
<Space align="start" size={20} style={{ marginBottom: 16 }}>
<Button type="primary" htmlType="submit">
Filter
</Button>
<Button type="primary" onClick={onClear}>
Clear
</Button>
</Space>
</Form>
{isAdmin && (
<Button icon={<FileExcelOutlined />} style={{ marginRight: 8 }} onClick={exportToCsv}>
Export CSV
</Button>
</Space>
</Form>
)}
</Row>
<HeroesRadarTable heroes={heroes} onChange={handleChange} setFormLayout={setFormLayout} />
</>
);
Expand Down
12 changes: 7 additions & 5 deletions client/src/components/Heroes/HeroesRadarTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Table, TableProps } from 'antd';
import { Table, TableProps, Tag } from 'antd';
import { ColumnType } from 'antd/lib/table';
import { HeroRadarDto, HeroesRadarBadgeDto, HeroesRadarDto } from 'api';
import { GithubAvatar } from 'components/GithubAvatar';
Expand Down Expand Up @@ -71,11 +71,13 @@ const initColumns: ColumnType<HeroRadarDto>[] = [
key: 'badges',
width: Object.keys(heroesBadges).length * (BADGE_SIZE + BADGE_SUM_HORIZONTAL_MARGIN),
responsive: ['xxl', 'xl', 'lg', 'md', 'sm'],
render: (value: HeroesRadarBadgeDto[], { githubId }: HeroRadarDto) => (
render: (value: HeroesRadarBadgeDto[], { total }: HeroRadarDto) => (
<>
{value.map(badge => (
<HeroesCountBadge key={`${githubId}-${badge.id}`} badge={badge} />
))}
{value.map(({ id, badgeId, comment, date }) => {
return <HeroesCountBadge key={id} badge={{ badgeId, comment, date }} />;
})}

{total > 20 && <Tag>+{total - 20} More</Tag>}
</>
),
},
Expand Down
Loading

0 comments on commit 0dbe212

Please sign in to comment.