Skip to content

Commit

Permalink
Merge pull request #2122 from ever-co/feat/pagination
Browse files Browse the repository at this point in the history
Feat: Pagination and Infinity Scroll
  • Loading branch information
evereq authored Jan 26, 2024
2 parents cf3d384 + 71a17e4 commit 87222fd
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 124 deletions.
13 changes: 7 additions & 6 deletions apps/web/app/hooks/useInfinityFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ import React from 'react';
export const getPartData = ({ offset = 0, limit = 10, arr = [] }: { offset?: number; limit?: number; arr: any[] }) =>
arr.slice(0, offset * limit + limit);

export const useInfinityScrolling = (arr: any) => {
export const useInfinityScrolling = <T>(arr: T[], lim?: number) => {
const limit = lim ?? 10;
const [offset, setOffset] = React.useState(0);
const [data, setData] = React.useState<any[]>(arr);
const [data, setData] = React.useState<T[]>(arr);

const getSomeTasks = React.useCallback(
(offset: number) => {
setData(getPartData({ arr, limit: 10, offset }));
setData(getPartData({ arr, limit, offset }));
},
[arr]
[arr, limit]
);

const nextOffset = React.useCallback(() => {
setOffset((prev) => prev + 1);
setData((prev) => getPartData({ arr: prev, limit: 10, offset }));
}, [offset]);
setData((prev) => getPartData({ arr: prev, limit, offset }));
}, [limit, offset]);

React.useEffect(() => {
console.log({ offset });
Expand Down
55 changes: 47 additions & 8 deletions apps/web/components/shared/Observer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,61 @@
import React from 'react';

export const ObserverComponent = ({ isLast, getNextData }: { isLast: boolean; getNextData: () => any }) => {
export const ObserverComponent = ({
isLast,
getNextData
}: {
root?: JSX.Element;
isLast: boolean;
getNextData: () => any;
}) => {
const cardRef = React.useRef<HTMLDivElement>();

React.useEffect(() => {
if (!cardRef?.current) return;

const observer = new IntersectionObserver(([entry]) => {
if (isLast && entry.isIntersecting) {
// fetch with new Entry
console.log('IN OBSERVER');
getNextData();
observer.unobserve(entry.target);
const observer = new IntersectionObserver(
([entry]) => {
if (isLast && entry.isIntersecting) {
// fetch with new Entry
console.log('IN OBSERVER');
getNextData();
observer.unobserve(entry.target);
}
},
{
threshold: 1.0
}
});
);

observer.observe(cardRef.current);

return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
cardRef.current && observer.unobserve(cardRef.current);
};
}, [isLast, getNextData]);
// @ts-expect-error
return <div ref={cardRef} className="-z-10 h-2 bg-transparent " />;
};

export const useElementOnScreen = (options: IntersectionObserverInit | undefined) => {
const containerRef = React.useRef<HTMLDivElement>();
const [isVisible, setIsVisible] = React.useState();

const cbFunction = (entries: any[]) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};

React.useEffect(() => {
const observer = new IntersectionObserver(cbFunction, options);
if (containerRef.current) observer.observe(containerRef.current);

return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, [containerRef, options]);

return [isVisible, containerRef];
};
226 changes: 116 additions & 110 deletions apps/web/lib/features/task/task-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { TaskItem } from './task-item';
import { TaskLabels } from './task-labels';
import { ActiveTaskPropertiesDropdown, ActiveTaskSizesDropdown, ActiveTaskStatusDropdown } from './task-status';
import { useTranslations } from 'next-intl';
import { useInfinityScrolling } from '@app/hooks/useInfinityFetch';
import { ObserverComponent } from '@components/shared/Observer';

type Props = {
task?: Nullable<ITeamTask>;
Expand Down Expand Up @@ -419,6 +421,7 @@ function TaskCard({
const { taskLabels: taskLabelsData } = useTaskLabels();

const { taskStatus, taskPriority, taskSize, taskLabels, taskDescription } = datas;
const { nextOffset, data } = useInfinityScrolling(updatedTaskList ?? [], 5);

useEffect(() => {
if (datas.editMode) {
Expand All @@ -437,139 +440,142 @@ function TaskCard({
shadow="custom"
className={clsxm(
'rounded-xl md:px-4 md:py-4',
'overflow-auto',
'overflow-hidden',
!cardWithoutShadow && ['shadow-xlcard'],
fullWidth ? ['w-full'] : ['md:w-[500px]'],
fullHeight ? 'h-full' : 'max-h-96'
)}
>
{inputField}
{/* Create team button */}
<div className="flex flex-col gap-y-2">
{datas.hasCreateForm && (
<div>
<InputField
placeholder="Description"
onChange={(e) => {
if (taskDescription) {
taskDescription.current = e.target.value;
}
}}
className={'dark:bg-[#1B1D22]'}
/>

<div className="flex justify-start gap-2">
<ActiveTaskStatusDropdown
className="lg:min-w-[170px]"
taskStatusClassName="h-7 text-xs"
onValueChange={(v) => {
if (v && taskStatus) {
taskStatus.current = v;
<div className="h-2/5">
{/* Create team button */}
<div className="flex flex-col gap-y-2">
{datas.hasCreateForm && (
<div>
<InputField
placeholder="Description"
onChange={(e) => {
if (taskDescription) {
taskDescription.current = e.target.value;
}
}}
defaultValue={taskStatus?.current as ITaskStatus}
task={null}
className={'dark:bg-[#1B1D22]'}
/>

<ActiveTaskPropertiesDropdown
className="lg:min-w-[170px]"
taskStatusClassName="h-7 text-xs"
onValueChange={(v) => {
if (v && taskPriority) {
taskPriority.current = v;
}
}}
defaultValue={taskPriority?.current as ITaskPriority}
task={null}
/>
<div className="flex justify-start gap-2">
<ActiveTaskStatusDropdown
className="lg:min-w-[170px]"
taskStatusClassName="h-7 text-xs"
onValueChange={(v) => {
if (v && taskStatus) {
taskStatus.current = v;
}
}}
defaultValue={taskStatus?.current as ITaskStatus}
task={null}
/>

<ActiveTaskSizesDropdown
className="lg:min-w-[170px]"
taskStatusClassName="h-7 text-xs"
onValueChange={(v) => {
if (v && taskSize) {
taskSize.current = v;
}
}}
defaultValue={taskSize?.current as ITaskSize}
task={null}
/>
<ActiveTaskPropertiesDropdown
className="lg:min-w-[170px]"
taskStatusClassName="h-7 text-xs"
onValueChange={(v) => {
if (v && taskPriority) {
taskPriority.current = v;
}
}}
defaultValue={taskPriority?.current as ITaskPriority}
task={null}
/>

<TaskLabels
className="lg:min-w-[170px] text-xs"
forDetails={false}
taskStatusClassName="dark:bg-[#1B1D22] dark:border dark:border-[#FFFFFF33] h-7 text-xs"
onValueChange={(_: any, values: string[] | undefined) => {
taskLabelsData.filter((tag) => (tag.name ? values?.includes(tag.name) : false));
<ActiveTaskSizesDropdown
className="lg:min-w-[170px]"
taskStatusClassName="h-7 text-xs"
onValueChange={(v) => {
if (v && taskSize) {
taskSize.current = v;
}
}}
defaultValue={taskSize?.current as ITaskSize}
task={null}
/>

if (taskLabels && values?.length) {
taskLabels.current = taskLabelsData.filter((tag) =>
<TaskLabels
className="lg:min-w-[170px] text-xs"
forDetails={false}
taskStatusClassName="dark:bg-[#1B1D22] dark:border dark:border-[#FFFFFF33] h-7 text-xs"
onValueChange={(_: any, values: string[] | undefined) => {
taskLabelsData.filter((tag) =>
tag.name ? values?.includes(tag.name) : false
);
}
}}
task={datas.inputTask}
/>

if (taskLabels && values?.length) {
taskLabels.current = taskLabelsData.filter((tag) =>
tag.name ? values?.includes(tag.name) : false
);
}
}}
task={datas.inputTask}
/>
</div>
</div>
</div>
)}
)}

<Tooltip
enabled={!datas.user?.isEmailVerified}
label={t('common.VERIFY_ACCOUNT_MSG')}
placement="top-start"
className="inline-block"
>
<Button
variant="outline"
disabled={!datas.hasCreateForm || datas.createLoading || !datas.user?.isEmailVerified}
loading={datas.createLoading}
className="font-normal text-sm rounded-xl min-w-[240px] max-w-[240px] inline-flex"
onClick={handleTaskCreation}
<Tooltip
enabled={!datas.user?.isEmailVerified}
label={t('common.VERIFY_ACCOUNT_MSG')}
placement="top-start"
className="inline-block"
>
{!datas.createLoading && <PlusIcon className="w-[16px] h-[16px]" />}
{t('common.CREATE_TASK')}
</Button>
</Tooltip>
</div>

{/* Task filter buttons */}
<div className="flex mt-4 space-x-3">
<OutlineBadge
className="py-2 text-xs cursor-pointer input-border"
onClick={() => datas.setFilter && datas.setFilter('open')}
>
<div className={clsxm('w-4 h-4 rounded-full opacity-50 bg-green-300')} />
<span
className={clsxm(
datas.filter === 'open' && ['text-primary dark:text-primary-light font-semibold']
)}
<Button
variant="outline"
disabled={!datas.hasCreateForm || datas.createLoading || !datas.user?.isEmailVerified}
loading={datas.createLoading}
className="font-normal text-sm rounded-xl min-w-[240px] max-w-[240px] inline-flex"
onClick={handleTaskCreation}
>
{!datas.createLoading && <PlusIcon className="w-[16px] h-[16px]" />}
{t('common.CREATE_TASK')}
</Button>
</Tooltip>
</div>

{/* Task filter buttons */}
<div className="flex mt-4 space-x-3">
<OutlineBadge
className="py-2 text-xs cursor-pointer input-border"
onClick={() => datas.setFilter && datas.setFilter('open')}
>
{datas.openTaskCount || 0} {t('common.OPEN')}
</span>
</OutlineBadge>

<OutlineBadge
className="py-2 text-xs cursor-pointer input-border"
onClick={() => datas.setFilter && datas.setFilter('closed')}
>
<TickCircleIcon className="opacity-50" />
<span
className={clsxm(
datas.filter === 'closed' && ['text-primary dark:text-primary-light font-semibold']
)}
<div className={clsxm('w-4 h-4 rounded-full opacity-50 bg-green-300')} />
<span
className={clsxm(
datas.filter === 'open' && ['text-primary dark:text-primary-light font-semibold']
)}
>
{datas.openTaskCount || 0} {t('common.OPEN')}
</span>
</OutlineBadge>

<OutlineBadge
className="py-2 text-xs cursor-pointer input-border"
onClick={() => datas.setFilter && datas.setFilter('closed')}
>
{datas.closedTaskCount || 0} {t('common.CLOSED')}
</span>
</OutlineBadge>
<TickCircleIcon className="opacity-50" />
<span
className={clsxm(
datas.filter === 'closed' && ['text-primary dark:text-primary-light font-semibold']
)}
>
{datas.closedTaskCount || 0} {t('common.CLOSED')}
</span>
</OutlineBadge>
</div>
</div>

<Divider className="mt-4" />

{/* Task list */}
<ul className="my-6">
<ul className="py-6 max-h-56 overflow-scroll">
{forParentChildRelationship &&
updatedTaskList?.map((task, i) => {
data?.map((task, i) => {
const last = (datas.filteredTasks?.length || 0) - 1 === i;
const active = datas.inputTask === task;

Expand All @@ -581,7 +587,7 @@ function TaskCard({
onClick={onItemClick}
className="cursor-pointer"
/>

<ObserverComponent isLast={i === data.length - 1} getNextData={nextOffset} />
{!last && <Divider className="my-5" />}
</li>
);
Expand All @@ -606,7 +612,7 @@ function TaskCard({
})}

{(forParentChildRelationship && updatedTaskList && updatedTaskList.length === 0) ||
(!forParentChildRelationship && datas?.filteredTasks && datas.filteredTasks.length === 0 && (
(!forParentChildRelationship && datas.filteredTasks && datas.filteredTasks.length === 0 && (
<div className="text-center">{t('common.NO_TASKS')}</div>
))}
</ul>
Expand Down

0 comments on commit 87222fd

Please sign in to comment.