Skip to content

Commit

Permalink
Feature #40: pagination 컴포넌트 구현하기 (#43)
Browse files Browse the repository at this point in the history
* feat(#36): 레이아웃 적용 및 헤더 컴포넌트 구현하기

- 로그인 여부에 따른 헤더 컴포넌트 구현하기
- 디렉토리 기반 라우팅으로 레이아웃 적용하기

* feat(#40): 페이지네이션 간단히 구현하기

- shadcn 페이지네이션 컴포넌트 적용하기
- 3개 이상 양끝 페이지와 차이가 나면 ...으로 표시하기

* feat(#40): 페이지네이션 위젯을 카드 리스트 위젯 안으로 넣기 및 페이지 선택 시 스크롤 상단으로 이동하기

- 추가로 현재 페이지 번호를 또 클릭했을 때는 페이지네이션 동작 안하도록하기

* fix(#40): 불필요한 console.log 제거하기
  • Loading branch information
naarang authored Nov 13, 2024
1 parent 5cfc46e commit 50dbe7c
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 28 deletions.
2 changes: 1 addition & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"react-icons": "^5.3.0"
"react-icons": "^5.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
57 changes: 57 additions & 0 deletions apps/frontend/src/feature/Pagination/usePagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState } from 'react';

interface UsePaginationProps {
totalPages: number;
initialPage?: number;
onChangePage?: (page: number) => void;
}

export function usePagination({ totalPages, initialPage = 1, onChangePage }: UsePaginationProps) {
const [currentPage, setCurrentPage] = useState(initialPage);

const onClickPage = (page: number) => {
setCurrentPage(page);
onChangePage?.(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
};

const onClickPrevious = () => {
if (currentPage > 1) onClickPage(currentPage - 1);
};

const onClickNext = () => {
if (currentPage < totalPages) onClickPage(currentPage + 1);
};

// "첫 페이지 ... 현재 페이지와 앞뒤 1페이지 ... 마지막 페이지 "로 구성되도록 구현함
const getPaginationItems = () => {
const items: (number | 'ellipsis')[] = [];
const showStartEllipsis = currentPage > 4;
const showEndEllipsis = currentPage < totalPages - 3;

items.push(1);

if (showStartEllipsis) items.push('ellipsis');

const startPage = showStartEllipsis ? Math.max(2, currentPage - 1) : 2;
const endPage = showEndEllipsis ? Math.min(totalPages - 1, currentPage + 1) : totalPages - 1;

for (let page = startPage; page <= endPage; page++) {
items.push(page);
}

if (showEndEllipsis) items.push('ellipsis');

if (totalPages > 1) items.push(totalPages);

return items;
};

return {
currentPage,
onClickPage,
onClickPrevious,
onClickNext,
getPaginationItems
};
}
49 changes: 27 additions & 22 deletions apps/frontend/src/widget/LotusList/LotusCardList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Lotus } from '@/feature/Lotus';
import { LotusType } from '@/feature/Lotus/type';
import { Pagination } from '@/widget/Pagination';

const LotusDummyData: LotusType = {
link: 'https://example.com',
Expand All @@ -14,29 +15,33 @@ const LotusDummyData: LotusType = {
}
};

// TODO: 나중에 Props로 size 받아서 재사용하기
export function LotusCardList() {
return (
<div className="w-full grid grid-cols-3 gap-[2rem]">
{new Array(10).fill(0).map((_, index) => (
<Lotus lotus={LotusDummyData} key={index}>
<Lotus.Link className="max-w-[24rem] p-5 border-2 border-[#E2E8F0] rounded-[0.75rem]">
<Lotus.Title className="text-[#1C1D22]" />
<Lotus.Author className="text-[rgba(28,29,34,0.5)]" />
<div className="w-full flex justify-between items-end">
<Lotus.CreateDate className="text-xs font-bold text-[#888DA7] bg-[rgba(136,141,167,0.1)] px-[1rem] py-[0.5rem] rounded-3xl" />
<Lotus.Logo />
</div>
{LotusDummyData?.tags?.length ? (
<>
<div className="mt-[1rem] w-full border-b-2 border-[#E2E8F0]" />
<Lotus.TagList className="pt-[1rem] min-h-[2rem]" variant={'default'} />
</>
) : (
<></>
)}
</Lotus.Link>
</Lotus>
))}
</div>
<>
<div className="w-full grid grid-cols-3 gap-[2rem]">
{new Array(10).fill(0).map((_, index) => (
<Lotus lotus={LotusDummyData} key={index}>
<Lotus.Link className="max-w-[24rem] p-5 border-2 border-[#E2E8F0] rounded-[0.75rem]">
<Lotus.Title className="text-[#1C1D22]" />
<Lotus.Author className="text-[rgba(28,29,34,0.5)]" />
<div className="w-full flex justify-between items-end">
<Lotus.CreateDate className="text-xs font-bold text-[#888DA7] bg-[rgba(136,141,167,0.1)] px-[1rem] py-[0.5rem] rounded-3xl" />
<Lotus.Logo />
</div>
{LotusDummyData?.tags?.length ? (
<>
<div className="mt-[1rem] w-full border-b-2 border-[#E2E8F0]" />
<Lotus.TagList className="pt-[1rem] min-h-[2rem]" variant={'default'} />
</>
) : (
<></>
)}
</Lotus.Link>
</Lotus>
))}
</div>
<Pagination totalPages={10} onChangePage={(page) => console.log(page)} />
</>
);
}
48 changes: 48 additions & 0 deletions apps/frontend/src/widget/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Pagination as PaginationBox,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious
} from '@froxy/design/components';
import { usePagination } from '@/feature/Pagination/usePagination';

interface PaginationProps {
totalPages: number;
initialPage?: number;
onChangePage?: (page: number) => void;
}

export function Pagination({ totalPages, initialPage = 1, onChangePage }: PaginationProps) {
const { currentPage, onClickPage, onClickPrevious, onClickNext, getPaginationItems } = usePagination({
totalPages,
initialPage,
onChangePage
});

return (
<PaginationBox>
<PaginationContent>
<PaginationItem>
<PaginationPrevious onClick={onClickPrevious} />
</PaginationItem>
{getPaginationItems().map((item, index) => (
<PaginationItem key={index}>
{typeof item === 'number' ? (
<PaginationLink onClick={() => item !== currentPage && onClickPage(item)} isActive={item === currentPage}>
{item}
</PaginationLink>
) : (
<PaginationEllipsis />
)}
</PaginationItem>
))}
<PaginationItem>
<PaginationNext onClick={onClickNext} />
</PaginationItem>
</PaginationContent>
</PaginationBox>
);
}
1 change: 1 addition & 0 deletions packages/design/src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './ui/badge';
export * from './ui/button';
export * from './ui/carousel';
export * from './ui/input';
export * from './ui/pagination';
export * from './ui/typography';
export * from './ui/tabs';
export * from './Slot';
80 changes: 80 additions & 0 deletions packages/design/src/components/ui/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react';
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
import { ButtonProps, buttonVariants } from './button';
import { cn } from '@/lib/utils';

const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
<nav
role="navigation"
aria-label="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
{...props}
/>
);
Pagination.displayName = 'Pagination';

const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn('flex flex-row items-center gap-1', className)} {...props} />
)
);
PaginationContent.displayName = 'PaginationContent';

const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(({ className, ...props }, ref) => (
<li ref={ref} className={cn('', className)} {...props} />
));
PaginationItem.displayName = 'PaginationItem';

type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, 'size'> &
React.ComponentProps<'a'>;

const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? 'page' : undefined}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
size
}),
className
)}
{...props}
/>
);
PaginationLink.displayName = 'PaginationLink';

const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to previous page" size="default" className={cn('gap-1 pl-2.5', className)} {...props}>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = 'PaginationPrevious';

const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to next page" size="default" className={cn('gap-1 pr-2.5', className)} {...props}>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';

const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
<span aria-hidden className={cn('flex h-9 w-9 items-center justify-center', className)} {...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = 'PaginationEllipsis';

export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious
};
Loading

0 comments on commit 50dbe7c

Please sign in to comment.