Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow filtering for analytics page (frontend) #178

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion apps/frontend/src/components/analytics/analytics.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,22 @@ import { StarsTableComponent } from '@gitroom/frontend/components/analytics/star
import useSWR from 'swr';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import {
DateRange,
DateRangePicker,
} from '../launches/helpers/date.range.picker';
import {
getDateRangeQuery,
getDateRangeUrl,
} from '../launches/helpers/date.query';

export const AnalyticsComponent: FC = () => {
const fetch = useFetch();

const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();
const dateRange: DateRange = getDateRangeQuery(searchParams);
const load = useCallback(async (path: string) => {
return await (await fetch(path)).json();
}, []);
Expand All @@ -27,9 +39,17 @@ export const AnalyticsComponent: FC = () => {
return <LoadingComponent />;
}

const changeDateRange = (newDateRange: DateRange) => {
const dateRangeUrl = getDateRangeUrl(searchParams, newDateRange);
router.replace(`${pathname}?${dateRangeUrl}`);
};

return (
<div className="flex gap-[24px] flex-1">
<div className="flex flex-col gap-[24px] flex-1">
<div className="flex justify-end">
<DateRangePicker onChange={changeDateRange} dateRange={dateRange} />
</div>
<StarsAndForks list={analytics} trending={trending} />
<StarsTableComponent />
</div>
Expand Down
46 changes: 37 additions & 9 deletions apps/frontend/src/components/analytics/stars.table.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
const { name, param } = props;
const router = useRouter();
const searchParams = useSearchParams();
const startDate = searchParams.get('startDate');
const endDate = searchParams.get('endDate');

const state = useMemo(() => {
const newName = searchParams.get('key');
Expand All @@ -35,18 +37,28 @@

const changeStateUrl = useCallback(
(newState: string) => {
const query =
newState === 'none' ? `` : `?key=${param}&state=${newState}`;
router.replace(`/analytics${query}`);
const params = new URLSearchParams();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to use URLSearchParams to make it easier to manage the dynamic params

if (startDate) {
params.set('startDate', startDate);
}
if (endDate) {
params.set('endDate', endDate);
}

if (newState !== 'none') {
params.set('key', param);
params.set('state', newState);
}
router.replace(`/analytics?${params.toString()}`);
},
[state, param]
[state, param, startDate, endDate]

Check warning

Code scanning / ESLint

verifies the list of dependencies for Hooks like useEffect and similar Warning

React Hook useCallback has a missing dependency: 'router'. Either include it or remove the dependency array.
);

const changeState = useCallback(() => {
changeStateUrl(
state === 'none' ? 'desc' : state === 'desc' ? 'asc' : 'none'
);
}, [state, param]);
}, [state, param, startDate, endDate]);

Check warning

Code scanning / ESLint

verifies the list of dependencies for Hooks like useEffect and similar Warning

React Hook useCallback has a missing dependency: 'changeStateUrl'. Either include it or remove the dependency array.

return (
<div
Expand Down Expand Up @@ -96,6 +108,10 @@
const page = +(searchParams.get('page') || 1);
const key = searchParams.get('key');
const state = searchParams.get('state');

const startDate = searchParams.get('startDate');
const endDate = searchParams.get('endDate');

const [loading, setLoading] = useState(false);
const [, startTransition] = useTransition();

Expand Down Expand Up @@ -148,11 +164,23 @@

const changePage = useCallback(
(type: 'increase' | 'decrease') => () => {
const newPage = type === 'increase' ? page + 1 : page - 1;
const keyAndState = key && state ? `&key=${key}&state=${state}` : '';
router.replace(`/analytics?page=${newPage}${keyAndState}`);
const params = new URLSearchParams();
const pageValue = type === 'increase' ? page + 1 : page - 1;
params.set('page', pageValue.toString());
if (key && state) {
params.set('key', key);
params.set('state', state);
}

if (startDate) {
params.set('startDate', startDate);
}
if (endDate) {
params.set('endDate', endDate);
}
router.replace(`/analytics?${params.toString()}`);
},
[page, key, state]
[page, key, state, startDate, endDate]

Check warning

Code scanning / ESLint

verifies the list of dependencies for Hooks like useEffect and similar Warning

React Hook useCallback has a missing dependency: 'router'. Either include it or remove the dependency array.
);

return (
Expand Down
39 changes: 39 additions & 0 deletions apps/frontend/src/components/launches/helpers/date.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import dayjs from 'dayjs';
import { ReadonlyURLSearchParams } from 'next/navigation';
import { DateRange } from './date.range.picker';
import customParseFormat from 'dayjs/plugin/customParseFormat';

const DATE_QUERY_FORMAT = 'YYYY-MM-DD';

export const getDateRangeQuery = (searchParams: ReadonlyURLSearchParams) => {
const startDateParams = searchParams.get('startDate');
const endDateParams = searchParams.get('endDate');
dayjs.extend(customParseFormat);
const dateRange: DateRange = {
startDate: startDateParams
? dayjs(startDateParams, DATE_QUERY_FORMAT)
: undefined,
endDate: endDateParams
? dayjs(endDateParams, DATE_QUERY_FORMAT)
: undefined,
};
return dateRange;
};
export const getDateRangeUrl = (
searchParams: ReadonlyURLSearchParams,
dateRange: DateRange
) => {
const params = new URLSearchParams(searchParams);
if (dateRange.startDate) {
params.set('startDate', dateRange.startDate.format(DATE_QUERY_FORMAT));
} else {
params.delete('startDate');
}

if (dateRange.endDate) {
params.set('endDate', dateRange.endDate.format(DATE_QUERY_FORMAT));
} else {
params.delete('endDate');
}
return params.toString();
};
129 changes: 129 additions & 0 deletions apps/frontend/src/components/launches/helpers/date.range.picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { FC, useCallback, useState } from 'react';
import dayjs from 'dayjs';
import { Calendar, RangeCalendar, TimeInput } from '@mantine/dates';

Check warning

Code scanning / ESLint

Disallow unused variables Warning

'Calendar' is defined but never used.

Check warning

Code scanning / ESLint

Disallow unused variables Warning

'TimeInput' is defined but never used.
import { useClickOutside } from '@mantine/hooks';
import { Button } from '@gitroom/react/form/button';

export type DateRange = {
startDate?: dayjs.Dayjs;
endDate?: dayjs.Dayjs;
};

export const DateRangePicker: FC<{
dateRange: DateRange;
onChange: (dateRange: DateRange) => void;
}> = (props) => {
const { dateRange, onChange } = props;
const [open, setOpen] = useState(false);
const startDate: Date | null = dateRange.startDate
? dateRange.startDate.toDate()
: null;
const endDate: Date | null = dateRange.endDate
? dateRange.endDate.toDate()
: null;

const rangeCalendarValue: [Date | null, Date | null] = [startDate, endDate];

const isSameDay = dateRange?.startDate?.isSame(dateRange?.endDate);
const changeShow = useCallback(() => {
setOpen((prev) => !prev);
}, []);

const ref = useClickOutside<HTMLDivElement>(() => {
setOpen(false);
});

const dateDisplay = () => {
if (isSameDay) {
return dateRange?.startDate?.format('DD MMM YYYY');
}
return `${dateRange?.startDate?.format(
'DD MMM YYYY'
)} - ${dateRange?.endDate?.format('DD MMM YYYY')}`;
};

const changeDateRange = useCallback(
(newDateRange: [Date | null, Date | null]) => {
if (newDateRange[0] && newDateRange[1]) {
const result = {
startDate: dayjs(newDateRange[0]),
endDate: dayjs(newDateRange[1]),
};
onChange(result);
}
},
[onChange]
);

return (
<div
className="flex gap-[8px] items-center relative px-[16px] select-none"
onClick={changeShow}
ref={ref}
>
{dateRange?.startDate && dateRange?.endDate ? (
<>
<div className="cursor-pointer">{dateDisplay()}</div>
<div className="cursor-pointer">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="21"
viewBox="0 0 20 21"
fill="none"
>
<path
d="M16.25 3H14.375V2.375C14.375 2.20924 14.3092 2.05027 14.1919 1.93306C14.0747 1.81585 13.9158 1.75 13.75 1.75C13.5842 1.75 13.4253 1.81585 13.3081 1.93306C13.1908 2.05027 13.125 2.20924 13.125 2.375V3H6.875V2.375C6.875 2.20924 6.80915 2.05027 6.69194 1.93306C6.57473 1.81585 6.41576 1.75 6.25 1.75C6.08424 1.75 5.92527 1.81585 5.80806 1.93306C5.69085 2.05027 5.625 2.20924 5.625 2.375V3H3.75C3.41848 3 3.10054 3.1317 2.86612 3.36612C2.6317 3.60054 2.5 3.91848 2.5 4.25V16.75C2.5 17.0815 2.6317 17.3995 2.86612 17.6339C3.10054 17.8683 3.41848 18 3.75 18H16.25C16.5815 18 16.8995 17.8683 17.1339 17.6339C17.3683 17.3995 17.5 17.0815 17.5 16.75V4.25C17.5 3.91848 17.3683 3.60054 17.1339 3.36612C16.8995 3.1317 16.5815 3 16.25 3ZM16.25 6.75H3.75V4.25H5.625V4.875C5.625 5.04076 5.69085 5.19973 5.80806 5.31694C5.92527 5.43415 6.08424 5.5 6.25 5.5C6.41576 5.5 6.57473 5.43415 6.69194 5.31694C6.80915 5.19973 6.875 5.04076 6.875 4.875V4.25H13.125V4.875C13.125 5.04076 13.1908 5.19973 13.3081 5.31694C13.4253 5.43415 13.5842 5.5 13.75 5.5C13.9158 5.5 14.0747 5.43415 14.1919 5.31694C14.3092 5.19973 14.375 5.04076 14.375 4.875V4.25H16.25V6.75Z"
fill="#B69DEC"
/>
</svg>
</div>
</>
) : (
<Button>Filter by Date</Button>
)}
{open && (
<div
onClick={(e) => e.stopPropagation()}
className="animate-normalFadeDown absolute top-[100%] mt-[16px] right-0 bg-sixth border border-tableBorder text-white rounded-[16px] z-[300] p-[16px] flex flex-col"
>
<RangeCalendar
onChange={changeDateRange}
value={rangeCalendarValue}
dayClassName={(date, modifiers) => {
if (modifiers.selectedInRange) {
return '!text-white !bg-seventh !outline-none';
}
if (modifiers.inRange) {
return '!bg-seventh !text-white';
}
if (modifiers.weekend) {
return '!text-[#B69DEC]';
}

if (modifiers.outside) {
return '!text-gray';
}

return '!text-white';
}}
classNames={{
day: 'hover:bg-seventh',
calendarHeaderControl: 'text-white hover:bg-third',
calendarHeaderLevel: 'text-white hover:bg-third', // cell: 'child:!text-white'
}}
styles={{
// kind of a hack to make sure that we are able to style when a range is selected but not finished
day: {
'&[data-in-range], &[data-in-range]:hover': {
backgroundColor: `rgb(114, 54, 241)`,
color: 'white',
},
},
}}
/>
</div>
)}
</div>
);
};