Skip to content

Commit

Permalink
implement challenges pagination in dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
HarlonGarcia committed Feb 3, 2025
1 parent b99dd7a commit 58985f0
Show file tree
Hide file tree
Showing 13 changed files with 293 additions and 109 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-headless-pagination": "^1.1.6",
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.0",
"react-icons": "^5.3.0",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions src/components/ChallengeSlider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ export const Slider = ({ challenges }: SliderProps) => {
};

export const ChallengeSlider = ({ category }: ChallengeSliderProps) => {
const { data: allChallenges = [] } = useChallenges();
const {
data: { items = [] } = {},
} = useChallenges();

const challenges = useMemo(
() => allChallenges.filter((challenge) => challenge.category?.id === category.id),
[allChallenges, category.id],
() => items.filter((challenge) => challenge.category?.id === category.id),
[items, category.id],
);

if (0 === challenges.length) {
Expand Down
70 changes: 70 additions & 0 deletions src/components/Pagination/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
Pagination as HeadlessPagination,
PrevButton,
NextButton,
PageButton,
IPaginationProps,
} from 'react-headless-pagination';

import { FiArrowLeft, FiArrowRight } from 'react-icons/fi';
import { twMerge } from 'tailwind-merge';
import { IPagination } from 'types';

interface PaginationProps extends Partial<IPagination>, Partial<IPaginationProps> {
onPageChange: (page: number) => void;
className?: string;
}

export const Pagination = ({
page = 1,
total = 1,
size = 1,
className,
onPageChange,
...args
}: PaginationProps) => {
const totalPages = Math.ceil(total / size);

return (
<HeadlessPagination
{...args}
currentPage={page}
totalPages={totalPages}
setCurrentPage={onPageChange}
edgePageCount={2}
middlePagesSiblingCount={1}
className={twMerge('flex items-center h-10 text-sm select-none w-fit font-fira',
className
)}
truncableText='...'
truncableClassName='w-10 px-0.5 text-center'
>
<PrevButton
as={<button />}
className={twMerge('flex items-center mr-2 text-pink-700 transition-all duration-300 ease-in-out',
page !== 0 ? 'cursor-pointer hover:text-pink-100' : 'opacity-40'
)}
>
<FiArrowLeft size={20} className='mr-3' />
</PrevButton>

<nav className="flex justify-center flex-grow">
<ul className="flex items-center">
<PageButton
activeClassName="bg-purple-800 text-pink-100 hover:bg-purple-800/85"
inactiveClassName="text-pink-900"
className='flex items-center justify-center transition-all duration-300 ease-in-out outline-none cursor-pointer w-9 h-9 focus:font-bold hover:text-pink-700 focus:text-pink-700 rounded-xl xl:text-lg'
/>
</ul>
</nav>

<NextButton
className={twMerge('flex items-center mr-2 text-pink-700 transition-all duration-300 ease-in-out',
page !== totalPages - 1 ? 'cursor-pointer hover:text-pink-100' : 'opacity-40'
)}
>
<FiArrowRight size={20} className='ml-3' />
</NextButton>
</HeadlessPagination>
);
};
2 changes: 1 addition & 1 deletion src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function Sidebar(props: SidebarProps) {
isVisible ? 'fixed' : 'hidden'
);

const toggleClasses = twMerge('fixed top-0 right-0 flex justify-center w-16 h-full py-5',
const toggleClasses = twMerge('fixed top-0 right-0 flex justify-center w-16 h-full py-5 outline-none',
'before:content-[""] before:absolute before:inset-0 before:w-16 before:bg-purple-800 before:translation-all before:duration-300 before:ease-in-out',
isVisible ? 'before:-translate-x-0 before:h-32' : 'before:translate-x-0'
);
Expand Down
139 changes: 41 additions & 98 deletions src/pages/Dashboard/partials/Challenges/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import { ChangeEvent, useContext, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';

import imagePlaceholder from 'assets/images/card-image-placeholder-2.png'
import { DeleteChallengeDialog } from 'components/Dialog/DeleteChallengeDialog';
import { Pagination } from 'components/Pagination';
import { Select } from 'components/Select';
import { AuthContext } from 'contexts/AuthContext';
import { challengesOrderBy } from 'enums/challengeOrderBy';
import { challengeStatuses } from 'enums/challengeStatus';
import { FaArrowUpAZ, FaArrowDownAZ } from "react-icons/fa6";
import { GrStatusGoodSmall } from "react-icons/gr";
import { IoMdList, IoMdTrash } from "react-icons/io";
import { IoMdList } from "react-icons/io";
import { LuPlus } from "react-icons/lu";
import { MdEdit, MdGridView } from "react-icons/md";
import { MdGridView } from "react-icons/md";
import { useCategories } from 'services/category';
import { useChallenges, useDeleteChallenge } from 'services/challenge';
import { IGetChallengeParams } from 'services/challenge/types';
import { useTechnologies } from 'services/technology';
import { ChallengeStatusEnum, IChallenge } from 'types';
import { getBase64Image } from 'utils';
import { NONE } from 'utils/constants';
import { DEFAULT_ITEMS_PAGE, NONE } from 'utils/constants';

import { Grid } from './partials/Grid';
import { List } from './partials/List';
import * as S from './styles';

interface Filters {
Expand All @@ -38,30 +38,23 @@ export default function Challenges() {
const [isAscOrder, setIsAscOrder] = useState(false);
const [isGrid, setIsGrid] = useState(false);
const [isModalOpened, setIsModalOpened] = useState(false);
const [page, setPage] = useState(0);
const [selectedChallenge, setSelectedChallenge] = useState<IChallenge | null>(null);

const {
data: challenges = [],
data: { items = [], pagination } = {},
} = useChallenges({
...filters,
page,
size: DEFAULT_ITEMS_PAGE,
authorId: user?.id,
order: isAscOrder ? 'asc' : 'desc',
});

const { data: allCategories = [] } = useCategories();
const { data: allTechnologies = [] } = useTechnologies();
const { mutate: deleteChallenge } = useDeleteChallenge();

const {
data: categoriesItems = [],
} = useCategories();

const {
data: technologiesItems = [],
} = useTechnologies();

const toggleAlphabeticalOrder = () => {
setIsAscOrder((prevState) => !prevState);
};

const handleFilterChange = (key: string) => (event: ChangeEvent<HTMLSelectElement>) => {
const value = event.target.value;

Expand All @@ -77,14 +70,16 @@ export default function Challenges() {
onChange: handleFilterChange(id),
});

const getChallengeImage = (image?: string) => {
return image ? getBase64Image(image) : imagePlaceholder;
}

const handleChallengeAction = (challenge: IChallenge | null, visible = true) => {
setIsModalOpened(visible);
setSelectedChallenge(challenge);
};

const handlePageChange = (newPage: number) => {
setPage(newPage);
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });

};

const handleDeleteChange = () => {
if (selectedChallenge) {
Expand All @@ -95,20 +90,20 @@ export default function Challenges() {
}

const categories = useMemo(() => {
return categoriesItems.map(({ id, name }) => ({
return allCategories.map(({ id, name }) => ({
key: id,
value: id,
label: name,
}))
}, [categoriesItems]);
}, [allCategories]);

const technologies = useMemo(() => {
return technologiesItems.map(({ id, name }) => ({
return allTechnologies.map(({ id, name }) => ({
key: id,
value: id,
label: name,
}))
}, [technologiesItems]);
}, [allTechnologies]);

const statuses = Object.values(challengeStatuses).map(({ id, label }) => ({
key: id,
Expand Down Expand Up @@ -175,7 +170,7 @@ export default function Challenges() {
/>
</S.Filters>
<S.Actions>
<S.Order onClick={toggleAlphabeticalOrder}>
<S.Order onClick={() => setIsAscOrder((prev) => !prev)}>
{isAscOrder ? <FaArrowUpAZ /> : <FaArrowDownAZ />}
</S.Order>
<S.Toggle active={isGrid ? 'grid' : 'list'}>
Expand All @@ -188,78 +183,26 @@ export default function Challenges() {
</S.Toggle>
</S.Actions>
</S.ChallengesHeader>
{!isGrid ? (
<S.List>
{challenges.map((challenge, index) => (
<S.ListItem key={challenge.id}>
<S.Title>
<small>{(index + 1).toString().padStart(2, '0')}</small>
<strong>{challenge.title}</strong>
</S.Title>
<div className={'flex items-center gap-8'}>
<div className={'hidden items-center gap-2 lg:flex'}>
<span
style={{ color: challengeStatuses[challenge.status].color }}
className={'flex items-center gap-2 py-2 px-3 bg-purple-900/50 text-xl font-semibold rounded-lg'}
>
<GrStatusGoodSmall size={10} />
<small>
{challengeStatuses[challenge.status].label}
</small>
</span>
<span className={'py-2 px-3 bg-pink-700 font-semibold rounded-lg text-purple-700'}>
{challenge.category?.name}
</span>
<span className={'py-2 px-3 bg-green-800 font-semibold rounded-lg text-purple-700'}>
{challenge.technologies?.map((technology) => technology.name).join(' / ')}
</span>
</div>
<S.ChallengeActions>
<S.Action type='edit'>
<MdEdit />
</S.Action>
<S.Action type='delete' onClick={() => handleChallengeAction(challenge)}>
<IoMdTrash />
</S.Action>
</S.ChallengeActions>
</div>
</S.ListItem>
))}
</S.List>
{isGrid ? (
<Grid
items={items}
onEdit={handleChallengeAction}
onDelete={handleChallengeAction}
/>
) : (
<S.Grid>
{challenges.map(({
id,
title,
status,
category,
technologies,
image,
}) => (
<S.GridItem key={id}>
<img src={getChallengeImage(image?.file)} alt="" />
<div className={'grid-item-info'}>
<strong>
<span>{title}</span>
<MdEdit />
</strong>
<div className={'hidden grid-item-info-badges'}>
<span style={{ color: challengeStatuses[status].color }}>
<GrStatusGoodSmall />
{challengeStatuses[status].label}
</span>
<span>
{category?.name}
</span>
<span className={'grid-item-info-badges-techs'}>
{technologies.map((technology) => technology.name).join(' / ')}
</span>
</div>
</div>
</S.GridItem>
))}
</S.Grid>
<List
items={items}
onEdit={handleChallengeAction}
onDelete={handleChallengeAction}
/>
)}
<Pagination
page={page}
total={pagination?.total}
size={pagination?.size}
onPageChange={handlePageChange}
className='mx-auto mt-8'
/>
</div>
</>
);
Expand Down
Loading

0 comments on commit 58985f0

Please # to comment.