Skip to content

Commit

Permalink
feat: 내 정보 페이지 스크롤 케이스 & 스켈레톤 구현 (#41)
Browse files Browse the repository at this point in the history
* 카드 추가 모달 구현

* type 명 변경

* feat: 카드 조회 api 연동

* CustomDialog

* 카드 추가하기 기능 구현

* 카드삭제 기능 구현

* 터치버튼

* http

* 쿼리키 분리

* 카드 추가 후 에디터로 이동

* ui 일부 수정

* 제목 색상 변경

* delete

* scroll

* button

* skeleton

* api 변경사항 반영
  • Loading branch information
woo-jk authored Aug 26, 2024
1 parent e12a6af commit 2fc81e7
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 67 deletions.
4 changes: 2 additions & 2 deletions src/app/(sidebar)/(my-info)/apis/useGetInfoCardList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { useSuspenseQuery } from '@tanstack/react-query';
import { http } from '../../../../apis/http';
import { InfoCardType, InfoType } from '@/types/info';

Expand All @@ -11,7 +11,7 @@ const getInfoCardList = (cardType: InfoType) => {
};

export const useGetInfoCardList = (cardType: InfoType) => {
return useQuery({
return useSuspenseQuery({
queryKey: [GET_INFO_CARD_LIST, cardType],
queryFn: async () => {
const res = await getInfoCardList(cardType);
Expand Down
1 change: 1 addition & 0 deletions src/app/(sidebar)/(my-info)/apis/usePostCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const postCard = (cardType: InfoType, tagIdList: number[]) => {
return http.post<PostCardResponse>({
url: `/card`,
data: {
cardTypeValueGroup: '내_정보',
cardTypeValueList: [cardType],
tagIdList,
},
Expand Down
48 changes: 6 additions & 42 deletions src/app/(sidebar)/(my-info)/components/InfoCardList.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,18 @@
'use client';

import { useState } from 'react';
import { cn } from '@/utils/tailwind-util';
import { Button, Icon } from '@/system/components';
import { AddInfoCardDialog } from './AddInfoCardDialog';
import { INFO_TYPES, InfoType } from '@/types/info';
import { InfoType } from '@/types/info';
import { useGetInfoCardList } from '../apis/useGetInfoCardList';
import { useGetCardTypeCount } from '../apis/useGetCardTypeCount';
import { InfoCard } from '@/components/InfoCard';
import { TouchButton } from '@/components/TouchButton';

export function InfoCardList() {
const [currentCardType, setCurrentCardType] = useState<InfoType>('경험_정리');
interface InfoCardListProps {
cardType: InfoType;
}

const { data: infoCardList } = useGetInfoCardList(currentCardType);
const { data: cardCount } = useGetCardTypeCount();
export function InfoCardList({ cardType }: InfoCardListProps) {
const { data: infoCardList } = useGetInfoCardList(cardType);

return (
<section>
<div className="mb-[28px] flex justify-between">
<div className="flex gap-[24px]">
{INFO_TYPES.map((type) => (
<TouchButton
key={type}
className="flex gap-[6px] items-center cursor-pointer"
onClick={() => setCurrentCardType(type)}>
<div
className={cn(
'text-[18px] text-neutral-10 font-semibold',
currentCardType === type && 'text-neutral-80',
)}>
{type.replaceAll('_', ' ')}
</div>
<div
className={cn(
'px-[8px] py-[2px] bg-neutral-10 rounded-[6px] text-neutral-1 text-[14px] font-semibold',
currentCardType === type && 'bg-neutral-80',
)}>
{cardCount?.[type] || 0}
</div>
</TouchButton>
))}
</div>
<AddInfoCardDialog>
<TouchButton className="flex items-center gap-[4px] bg-neutral-95 py-[8px] px-[16px] rounded-[6px]">
<Icon name="add" color="#08F29B" />
<div className="text-white text-[14px] font-semibold">카드 추가</div>
</TouchButton>
</AddInfoCardDialog>
</div>
{infoCardList?.length ? (
<ul className="grid grid-cols-[repeat(auto-fill,minmax(343px,1fr))] gap-[16px]">
{infoCardList?.map((info) => (
Expand Down
35 changes: 35 additions & 0 deletions src/app/(sidebar)/(my-info)/components/InfoCardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { motion } from 'framer-motion';
import { match } from 'ts-pattern';

interface InfoCardSkeletonProps {
count: number;
}

export function InfoCardSkeleton({ count }: InfoCardSkeletonProps) {
return (
<motion.div
variants={{
show: {
transition: {
staggerChildren: 0.1,
},
},
}}
initial="hide"
animate="show"
className="grid grid-cols-[repeat(auto-fill,minmax(343px,1fr))] gap-[16px]">
{Array(count)
.fill(0)
.map((_, index) => (
<motion.div
key={index}
variants={{
hide: { opacity: 0.1, scale: 1 },
show: { opacity: 1, scale: 0.98, transition: { repeat: Infinity, repeatType: 'reverse', duration: 0.5 } },
}}
className="h-[140px] p-[24px] rounded-[16px] bg-neutral-3"
/>
))}
</motion.div>
);
}
117 changes: 108 additions & 9 deletions src/app/(sidebar)/(my-info)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,118 @@
'use client';

import { Icon } from '@/system/components';
import { Dropdown, Icon } from '@/system/components';
import { InfoCardList } from './components/InfoCardList';
import { Suspense, useEffect, useRef, useState } from 'react';
import { AddInfoCardDialog } from './components/AddInfoCardDialog';
import { TouchButton } from '@/components/TouchButton';
import { INFO_TYPES, InfoType } from '@/types';
import { useScroll } from '@/hooks/useScroll';
import { cn } from '@/utils/tailwind-util';
import { useGetCardTypeCount } from './apis/useGetCardTypeCount';
import { If } from '@/system/utils/If';
import { motion } from 'framer-motion';
import { InfoCardSkeleton } from './components/InfoCardSkeleton';
import { AsyncBoundaryWithQuery } from '@/lib';

export default function MyInfo() {
const [showHeader, setShowHeader] = useState(false);
const headerRef = useRef<HTMLDivElement>(null);

const [currentCardType, setCurrentCardType] = useState<InfoType>('경험_정리');

const { data: cardCount } = useGetCardTypeCount();

useScroll(headerRef, (y) => setShowHeader(y > 192));

return (
<div className="max-w-[1700px] py-[64px] px-[80px] mx-auto bg-neutral-1">
<div className="mb-[72px] flex justify-between">
<h1 className="text-[28px] font-bold">내 정보</h1>
<button className="flex gap-[24px] p-[16px] border rounded-[8px] border-neutral-5 bg-white">
<div className="text-[16px] font-semibold">김뽀각님의 기본정보</div>
<Icon name="down" color="#878A93" />
</button>
<div ref={headerRef} className="max-h-[100vh] w-full overflow-auto">
<div className="mx-auto max-w-[1700px] py-[64px] px-[80px] bg-neutral-1">
<div className="mb-[48px] flex justify-between">
<h1 className="text-[28px] font-bold">내 정보</h1>
</div>
<div className="sticky top-0 bg-neutral-1">
<div className="flex justify-between py-[24px]">
<If condition={showHeader}>
<div className="flex gap-12 items-center">
<motion.h1 initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-heading1 font-bold">
내 정보
</motion.h1>
<Dropdown>
<Dropdown.Trigger>
<TouchButton layout>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center gap-4 rounded-6 border bg-white px-12 py-6">
<span className="text-label1 font-semibold text-neutral-95">
{currentCardType.replaceAll('_', ' ')}
</span>
<Dropdown.TriggerArrow />
</motion.div>
</TouchButton>
</Dropdown.Trigger>
<Dropdown.Content align="end">
{INFO_TYPES.map((type) => (
<Dropdown.CheckedItem
key={type}
checked={type === currentCardType}
className={type === currentCardType ? 'text-neutral-30' : ''}
onClick={() => setCurrentCardType(type)}>
{type.replace('_', ' ')}
</Dropdown.CheckedItem>
))}
</Dropdown.Content>
</Dropdown>
</div>
</If>
<If condition={!showHeader}>
<div className="flex gap-[24px]">
{INFO_TYPES.map((type) => (
<TouchButton
key={type}
className="flex gap-[6px] items-center cursor-pointer"
onClick={() => setCurrentCardType(type)}>
<div
className={cn(
'text-[18px] text-neutral-10 font-semibold',
currentCardType === type && 'text-neutral-80',
)}>
{type.replaceAll('_', ' ')}
</div>
<div
className={cn(
'px-[8px] py-[2px] bg-neutral-10 rounded-[6px] text-neutral-1 text-[14px] font-semibold',
currentCardType === type && 'bg-neutral-80',
)}>
{cardCount?.[type] || 0}
</div>
</TouchButton>
))}
</div>
</If>
<AddInfoCardDialog>
<TouchButton layout>
<motion.div
initial={{ padding: '8px 16px' }}
variants={{ longPadding: { padding: '8px 16px' }, shortPadding: { padding: '8px 8px' } }}
animate={showHeader ? 'shortPadding' : 'longPadding'}
className="bg-neutral-95 flex items-center gap-[4px] rounded-[6px]">
<Icon name="add" size={20} color="#20E79D" />
{!showHeader && <span className="text-label1 text-white font-semibold">카드 추가</span>}
</motion.div>
</TouchButton>
</AddInfoCardDialog>
</div>
<If condition={showHeader}>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mx-[-80px] bg-neutral-5 h-[1px]" />
</If>
</div>
<AsyncBoundaryWithQuery
errorFallback={<InfoCardSkeleton count={4} />}
pendingFallback={<InfoCardSkeleton count={4} />}>
<InfoCardList cardType={currentCardType} />
</AsyncBoundaryWithQuery>
</div>
<InfoCardList />
</div>
);
}
2 changes: 1 addition & 1 deletion src/app/(sidebar)/my-recruit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export default function MyRecruit() {
variants={{ longPadding: { padding: '8px 16px' }, shortPadding: { padding: '8px 8px' } }}
animate={isSticky ? 'shortPadding' : 'longPadding'}
className="bg-neutral-95 flex items-center gap-[4px] rounded-[6px]">
<Icon name="add" size={24} color="#20E79D" />
<Icon name="add" size={20} color="#20E79D" />
{!isSticky && <span className="text-label1 text-white font-semibold">새 공고</span>}
</motion.div>
</TouchButton>
Expand Down
10 changes: 0 additions & 10 deletions src/components/InfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from '@/system/components/DropdownMenu/DropdownMenu';
import { InfoCardType, TAG_TYPE_COLOR } from '@/types/info';
import { color } from '@/system/token/color';
import { If } from '@/system/utils/If';
import { useDeleteCard } from '@/app/(sidebar)/(my-info)/apis/useDeleteCard';
import Link from 'next/link';
import { MouseEventHandler } from 'react';
Expand Down Expand Up @@ -63,15 +62,6 @@ export function InfoCard({ id, title, updatedDate, tagList }: InfoCardProps) {
))}
</div>
</div>
<If condition={tagList != null}>
<div className="flex gap-[8px]">
{tagList?.map(({ id, type, name }) => (
<Tag key={id} color={TAG_TYPE_COLOR[type]}>
{name}
</Tag>
))}
</div>
</If>
</Link>
);
}
6 changes: 3 additions & 3 deletions src/system/components/Icon/SVG/Add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function Add({ size, color }: IconBaseType) {
return (
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" xmlns="http://www.w3.org/2000/svg">
<mask
id="mask0_1054_1585"
id="mask0_3051_19736"
style={{ maskType: 'alpha' }}
maskUnits="userSpaceOnUse"
x="0"
Expand All @@ -13,8 +13,8 @@ export function Add({ size, color }: IconBaseType) {
height={size}>
<rect width={size} height={size} fill="#D9D9D9" />
</mask>
<g mask="url(#mask0_1054_1585)">
<path d="M11.25 12.75H5.5V11.25H11.25V5.5H12.75V11.25H18.5V12.75H12.75V18.5H11.25V12.75Z" fill={color} />
<g mask="url(#mask0_3051_19736)">
<path d="M9.99967 3.3335V16.6668M16.6663 10.0002H3.33301" stroke={color} stroke-width="1.5" />
</g>
</svg>
);
Expand Down

0 comments on commit 2fc81e7

Please sign in to comment.