Skip to content

Commit

Permalink
Merge pull request #10 from JECT-Study/feature/design-system
Browse files Browse the repository at this point in the history
[TASK-46, 47] style: Pagination, FilterDropdown 구현
  • Loading branch information
dahyeo-n authored Dec 23, 2024
2 parents 9161958 + f18e373 commit d863078
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 26 deletions.
6 changes: 2 additions & 4 deletions src/components/Button/CategoryButton.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { CategoryButtonProps } from '@/types';

import { FC } from 'react';

const CategoryButton: FC<CategoryButtonProps> = ({
const CategoryButton = ({
backgroundColor = 'bg-gray-800',
textColor = 'text-gray-300',
textSize,
children,

onClick,
ariaLabel,
}) => {
}: CategoryButtonProps) => {
return (
<button
onClick={onClick}
Expand Down
6 changes: 3 additions & 3 deletions src/components/Button/FollowButton.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
'use client';

import { FC, useState } from 'react';
import { useState } from 'react';

import { FollowButtonProps } from '@/types/buttons/FollowButtonProps';

import Icon from '../Icon/Icon';

const FollowButton: FC<FollowButtonProps> = ({
const FollowButton = ({
backgroundColor = 'bg-gray-900',
textColor = 'text-white',
textSize = 'button-s',

onClick,
ariaLabel,
}) => {
}: FollowButtonProps) => {
const [isFollowing, setIsFollowing] = useState(false);

const toggleFollow = () => {
Expand Down
6 changes: 2 additions & 4 deletions src/components/Button/SquareButtonL.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { SquareButtonLProps } from '@/types';

import { FC } from 'react';

const SquareButtonL: FC<SquareButtonLProps> = ({
const SquareButtonL = ({
backgroundColor = 'bg-gray-800',
textColor = 'text-white',
textSize,
Expand All @@ -15,7 +13,7 @@ const SquareButtonL: FC<SquareButtonLProps> = ({
icon,
iconPosition,
type = 'button',
}) => {
}: SquareButtonLProps) => {
return (
<button
type={type}
Expand Down
83 changes: 83 additions & 0 deletions src/components/FilterDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client';

import { useEffect, useRef, useState } from 'react';

import Icon from '../Icon/Icon';

interface FilterDropdownProps {
options: string[];
selected: string;
onChange: (value: string) => void;
}

const FilterDropdown = ({
options,
selected,
onChange,
}: FilterDropdownProps) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

const handleOptionSelect = (option: string) => {
onChange(option);
setIsDropdownOpen(false);
};

return (
<div className='body2 relative inline-block text-left' ref={dropdownRef}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
aria-label={`Currently selected filter: ${selected}`}
aria-expanded={isDropdownOpen}
className='flex items-center justify-between w-[149px] h-[44px] px-[23px] py-[10px]
text-white bg-gray-900 rounded-[5px] gap-[36px] shadow-sm
hover:bg-gray-800 focus:outline-none'
>
{selected}
<span className='text-gray-300'>
{isDropdownOpen ? (
<Icon name='ChevronUp' size='m' />
) : (
<Icon name='ChevronDown' size='m' />
)}
</span>
</button>

{isDropdownOpen && (
<div
className='w-full mt-3 bg-gray-900 rounded-[5px] shadow-lg'
role='menu'
>
<ul className='py-1'>
{options.map((option, index) => (
<li
key={index}
onClick={() => handleOptionSelect(option)}
aria-current={option === selected ? 'true' : undefined}
className={`block w-[149px] h-[44px] px-[23px] py-[10px] text-gray-400 cursor-pointer hover:bg-gray-800`}
>
{option}
</li>
))}
</ul>
</div>
)}
</div>
);
};

export default FilterDropdown;
9 changes: 2 additions & 7 deletions src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentType, FC } from 'react';
import { ComponentType } from 'react';

import iconSizes, { IconSize } from './iconSizes';
import { iconsNames } from './iconsNames';
Expand All @@ -10,12 +10,7 @@ interface IconProps {
onClick?: () => void;
}

const Icon: FC<IconProps> = ({
name,
size = 'm',
className = '',
onClick,
}: IconProps) => {
const Icon = ({ name, size = 'm', className = '', onClick }: IconProps) => {
const Component = iconsNames[name] as ComponentType<{
className?: string;
onClick?: () => void;
Expand Down
9 changes: 6 additions & 3 deletions src/components/Icon/icons/Close.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { FC } from 'react';
import { ReactNode } from 'react';

const Close: FC<{ className?: string; onClick?: () => void }> = ({
const Close = ({
className,
onClick,
}) => {
}: {
className?: string;
onClick?: () => void;
}): ReactNode => {
return (
<svg
className={className}
Expand Down
7 changes: 4 additions & 3 deletions src/components/Icon/icons/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FC } from 'react';

const Menu: FC<{ className?: string; onClick?: () => void }> = ({
const Menu = ({
className,
onClick,
}: {
className?: string;
onClick?: () => void;
}) => {
return (
<svg
Expand Down
10 changes: 8 additions & 2 deletions src/components/Icon/iconsNames.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC } from 'react';
import { ReactNode } from 'react';

import AlertCircle from './icons/AlertCircle';
import AlternateShare from './icons/AlternateShare';
Expand Down Expand Up @@ -39,7 +39,13 @@ import UploadShare from './icons/UploadShare';

export const iconsNames: Record<
string,
FC<{ className?: string; onClick?: () => void }>
({
className,
onClick,
}: {
className?: string;
onClick?: () => void;
}) => ReactNode
> = {
AlertCircle,
AlternateShare,
Expand Down
1 change: 1 addition & 0 deletions src/components/Layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const AppShell = ({ children }: { children: ReactNode }) => {
<NextUIProvider>
<NextThemesProvider
attribute='class'
defaultTheme='dark'
value={{
dark: 'custom-dark',
}}
Expand Down
107 changes: 107 additions & 0 deletions src/components/Pagination/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Icon from '../Icon/Icon';

interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}

const Pagination = ({
currentPage,
totalPages,
onPageChange,
}: PaginationProps) => {
const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;

const calculateVisiblePages = (
currentPage: number,
totalPages: number,
maxVisiblePages: number,
): number[] => {
const startPage = Math.max(
1,
currentPage - Math.floor(maxVisiblePages / 2),
);
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
const adjustedStartPage = Math.max(1, endPage - maxVisiblePages + 1);

return Array.from(
{ length: endPage - adjustedStartPage + 1 },
(_, i) => adjustedStartPage + i,
);
};

const visiblePages = calculateVisiblePages(currentPage, totalPages, 5);

return (
<div className='flex items-center justify-center gap-6'>
<div className='flex gap-1' id='first-previous-buttons'>
<button
onClick={() => onPageChange(1)}
disabled={isFirstPage}
aria-label='Go to first page'
className={`p-2 rounded ${
isFirstPage ? 'text-gray-600 cursor-not-allowed' : 'text-gray-300'
}`}
>
<Icon name='ChevronDoubleLeft' size='s' />
</button>

<button
onClick={() => onPageChange(currentPage - 1)}
disabled={isFirstPage}
aria-label='Go to previous page'
className={`p-2 rounded ${
isFirstPage ? 'text-gray-600 cursor-not-allowed' : 'text-gray-300'
}`}
>
<Icon name='ChevronLeft' size='s' />
</button>
</div>

<div className='flex gap-1' id='number-buttons'>
{visiblePages.map((page) => (
<button
key={page}
onClick={() => onPageChange(page)}
aria-current={page === currentPage ? 'page' : undefined}
className={`button-s p-3 w-[46px] rounded-full transition-colors ${
page === currentPage
? 'bg-main text-white'
: 'text-gray-600 hover:bg-gray-900'
}`}
>
{page}
</button>
))}
</div>

<div className='flex gap-1' id='next-last-buttons'>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={isLastPage}
aria-label='Go to next page'
className={`p-2 rounded ${
isLastPage ? 'text-gray-600 cursor-not-allowed' : 'text-gray-300'
}`}
>
<Icon name='ChevronRight' size='s' />
</button>

<button
onClick={() => onPageChange(totalPages)}
disabled={isLastPage}
aria-label='Go to last page'
className={`p-2 rounded ${
isLastPage ? 'text-gray-600 cursor-not-allowed' : 'text-gray-300'
}`}
>
<Icon name='ChevronDoubleRight' size='s' />
</button>
</div>
</div>
);
};

export default Pagination;

0 comments on commit d863078

Please sign in to comment.