-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] BDYFE-189 카드 리스트 공통 컴포넌트 구현 #182
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
Changes from all commits
126487e
94852bc
23ef350
5d775bd
6b9ed37
df7e119
7e4f173
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| 'use client'; | ||
|
|
||
| import { cn } from '@/lib/cn'; | ||
| import { BookmarkIcon } from '@/shared/components/icons'; | ||
|
|
||
| interface BookmarkButtonProps { | ||
| isBookmarked: boolean; | ||
| onClick: () => void; | ||
| className?: string; | ||
| } | ||
|
|
||
| export const BookmarkButton = ({ | ||
| isBookmarked, | ||
| onClick, | ||
| className, | ||
| }: BookmarkButtonProps) => { | ||
| return ( | ||
| <button | ||
| type="button" | ||
| aria-label={isBookmarked ? '북마크 해제' : '북마크 추가'} | ||
| aria-pressed={isBookmarked} | ||
| className={cn( | ||
| 'focus-visible:outline-mint-300 flex shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid', | ||
| isBookmarked ? 'text-mint-300' : 'text-gray-200', | ||
| className, | ||
| )} | ||
| onClick={(event) => { | ||
| event.stopPropagation(); | ||
| onClick(); | ||
| }} | ||
| > | ||
| <BookmarkIcon className={cn('size-6', isBookmarked && 'fill-current')} /> | ||
| </button> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { cn } from '@/lib/cn'; | ||
| import { Skeleton } from '@/shared/components/ui/skeleton/skeleton'; | ||
|
|
||
| const SKELETON_IMAGE_KEYS = [1, 2, 3, 4] as const; | ||
|
|
||
| interface CardListSkeletonProps { | ||
| className?: string; | ||
| } | ||
|
|
||
| export const CardListSkeleton = ({ className }: CardListSkeletonProps) => { | ||
| return ( | ||
| <div | ||
| aria-hidden | ||
| className={cn( | ||
| 'flex w-full min-w-0 flex-col items-start gap-3 overflow-clip', | ||
| className, | ||
| )} | ||
| > | ||
| <div className="flex w-52 flex-col items-start gap-1"> | ||
| <Skeleton className="my-0.5 h-5 w-full rounded-[3px]" /> | ||
| <Skeleton className="my-0.5 h-3.5 w-3/4 rounded-[3px]" /> | ||
| </div> | ||
|
|
||
| <div className="flex items-start gap-2"> | ||
| {SKELETON_IMAGE_KEYS.map((imageKey) => ( | ||
| <Skeleton key={imageKey} className="size-25 shrink-0 rounded-lg" /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| 'use client'; | ||
|
|
||
| import Link from 'next/link'; | ||
|
|
||
| import { cn } from '@/lib/cn'; | ||
| import { BookmarkButton } from '@/shared/components/ui/bookmark-button/bookmark-button'; | ||
| import { CommonImage } from '@/shared/components/ui/common-image/common-image'; | ||
|
|
||
| interface CardListImage { | ||
| src: string; | ||
| alt: string; | ||
| } | ||
|
|
||
| interface CardListProps { | ||
| title: string; | ||
| description: string; | ||
| images: CardListImage[]; | ||
| isBookmarked: boolean; | ||
| onBookmarkClick: () => void; | ||
| href?: string; | ||
| className?: string; | ||
| } | ||
|
|
||
| export const CardList = ({ | ||
| title, | ||
| description, | ||
| images, | ||
| isBookmarked, | ||
| onBookmarkClick, | ||
| href, | ||
| className, | ||
| }: CardListProps) => { | ||
| const information = ( | ||
| <> | ||
| <h3 className="text-body-sb-16 w-full truncate text-gray-800">{title}</h3> | ||
| <p className="text-caption-m-12 w-full truncate text-gray-500"> | ||
| {description} | ||
| </p> | ||
| </> | ||
| ); | ||
|
|
||
| return ( | ||
| <article | ||
| className={cn( | ||
| 'flex w-full min-w-0 flex-col items-start gap-3', | ||
| className, | ||
| )} | ||
| > | ||
| <div className="flex w-full items-center gap-8"> | ||
| {href ? ( | ||
| <Link | ||
| href={href} | ||
| className="focus-visible:outline-mint-300 flex min-w-0 flex-1 flex-col items-start gap-1 rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid" | ||
| > | ||
| {information} | ||
| </Link> | ||
| ) : ( | ||
| <div className="flex min-w-0 flex-1 flex-col items-start gap-1"> | ||
| {information} | ||
| </div> | ||
| )} | ||
|
|
||
| <BookmarkButton | ||
| isBookmarked={isBookmarked} | ||
| className="h-6 w-6.25" | ||
| onClick={onBookmarkClick} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
1)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 생각하지 못했었는데 좋은 의견인 것 같아요! 어떤 개선 방향이 좋을지 고민해봤는데
꼼꼼히 봐주시고 좋은 의견 주셔서 감사합니다~!! |
||
| /> | ||
| </div> | ||
|
|
||
| <div | ||
| role="group" | ||
| aria-label={`${title} 이미지 목록`} | ||
| tabIndex={0} | ||
| className="focus-visible:outline-mint-300 flex w-full scrollbar-none items-center gap-2 overflow-x-auto focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid [&::-webkit-scrollbar]:hidden" | ||
| > | ||
| {images.map(({ src, alt }) => ( | ||
| <CommonImage | ||
| key={src} | ||
| src={src} | ||
| alt={alt} | ||
| width={100} | ||
| height={100} | ||
| radius="rounded-xl" | ||
| className="size-25" | ||
| /> | ||
| ))} | ||
| </div> | ||
| </article> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { CardList } from './card-list'; | ||
| export { CardListSkeleton } from './card-list-skeleton'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { cn } from '@/lib/cn'; | ||
|
|
||
| interface SkeletonProps { | ||
| className?: string; | ||
| } | ||
|
|
||
| export const Skeleton = ({ className }: SkeletonProps) => { | ||
| return ( | ||
| <div | ||
| aria-hidden | ||
| className={cn('animate-skeleton-wave bg-gray-200/20', className)} | ||
| /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| @keyframes skeleton-gradient-wave { | ||
| to { | ||
| transform: translateX(100%); | ||
| } | ||
| } | ||
|
|
||
| @utility animate-skeleton-wave { | ||
| position: relative; | ||
| overflow: hidden; | ||
|
|
||
| &::after { | ||
| position: absolute; | ||
| inset: 0; | ||
| background: linear-gradient( | ||
| 100deg, | ||
| transparent 20%, | ||
| color-mix(in srgb, var(--color-white) 55%, transparent) 50%, | ||
| transparent 80% | ||
| ); | ||
| content: ''; | ||
| transform: translateX(-100%); | ||
| animation: skeleton-gradient-wave 1.7s linear infinite; | ||
| } | ||
| } | ||
|
|
||
| @media (prefers-reduced-motion: reduce) { | ||
| .animate-skeleton-wave::after { | ||
| animation: none; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.