Skip to content
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

feat: 사이드바 기능 구현 및 애니메이션 구현 #66

Merged
merged 19 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions src/app/(sidebar)/(my-info)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,32 @@ import { motion } from 'framer-motion';
import { InfoCardSkeleton } from './components/InfoCardSkeleton';
import { AsyncBoundaryWithQuery } from '@/lib';
import { Onboarding } from './containers/Onboarding/Onboarding';
import { useSearchParams } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';

const getType = (typeParam: string | null) => {
if (typeParam && INFO_TYPES.includes(typeParam as InfoType)) {
return typeParam as InfoType;
}

return '경험_정리';
};

export default function MyInfo() {
const router = useRouter();
const searchParams = useSearchParams();

const [showHeader, setShowHeader] = useState(false);
const headerRef = useRef<HTMLDivElement>(null);

const [currentCardType, setCurrentCardType] = useState<InfoType>('경험_정리');
const typeParam = searchParams.get('type');
const currentCardType = getType(typeParam);

const { data: cardCount } = useGetCardTypeCount();

const handleTypeChange = (type: InfoType) => {
router.replace(`?type=${type}`);
};

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

const params = useSearchParams();
Expand Down Expand Up @@ -61,7 +77,7 @@ export default function MyInfo() {
key={type}
checked={type === currentCardType}
className={type === currentCardType ? 'text-neutral-30' : ''}
onClick={() => setCurrentCardType(type)}>
onClick={() => handleTypeChange(type)}>
{type.replace('_', ' ')}
</Dropdown.CheckedItem>
))}
Expand All @@ -75,7 +91,7 @@ export default function MyInfo() {
<TouchButton
key={type}
className="flex gap-[6px] items-center cursor-pointer"
onClick={() => setCurrentCardType(type)}>
onClick={() => handleTypeChange(type)}>
<div
className={cn(
'text-[18px] text-neutral-10 font-semibold',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const getRecruitById = (id: string) => {

export const useGetRecruitById = (id: string) => {
return useSuspenseQuery({
queryKey: [GET_RECRUIT_BY_ID],
queryKey: [GET_RECRUIT_BY_ID, id],
queryFn: async () => {
const res = await getRecruitById(id);

Expand Down
2 changes: 2 additions & 0 deletions src/app/(sidebar)/my-recruit/api/useDeleteRecruit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { http } from '@/apis/http';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { GET_PROGRESSING_RECRUITS_KEY } from '@/app/(sidebar)/my-recruit/api/useGetProgressingRecruits';
import { GET_ALL_RECRUITS_KEY } from '@/app/(sidebar)/my-recruit/api/useGetAllRecruits';
import { GET_RECRUIT_TITLES_KEY } from './useGetRecruitTitles';

export const DELETE_RECRUIT_KEY = 'delete-recruit';

Expand All @@ -18,6 +19,7 @@ export const useDeleteRecruit = () => {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [GET_PROGRESSING_RECRUITS_KEY] });
queryClient.invalidateQueries({ queryKey: [GET_ALL_RECRUITS_KEY] });
queryClient.invalidateQueries({ queryKey: [GET_RECRUIT_TITLES_KEY] });
},
});

Expand Down
26 changes: 26 additions & 0 deletions src/app/(sidebar)/my-recruit/api/useGetRecruitTitles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { http } from '@/apis/http';
import { useQuery } from '@tanstack/react-query';

interface RecruitTitleType {
id: number;
title: string;
}

type Response = RecruitTitleType[];

export const GET_RECRUIT_TITLES_KEY = 'recruit-titles';

function getRecruiteTitles() {
return http.get<Response>({ url: '/recruits/titles' });
}

export function useGetRecruitTitles() {
return useQuery({
queryKey: [GET_RECRUIT_TITLES_KEY],
queryFn: async () => {
const res = await getRecruiteTitles();

return res.data;
},
});
}
2 changes: 2 additions & 0 deletions src/app/(sidebar)/my-recruit/api/usePostRecruit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { http } from '@/apis/http';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { GET_PROGRESSING_RECRUITS_KEY } from '@/app/(sidebar)/my-recruit/api/useGetProgressingRecruits';
import { GET_ALL_RECRUITS_KEY } from '@/app/(sidebar)/my-recruit/api/useGetAllRecruits';
import { GET_RECRUIT_TITLES_KEY } from './useGetRecruitTitles';

export interface Request {
title: string;
Expand All @@ -25,6 +26,7 @@ export function usePostRecruit() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [GET_PROGRESSING_RECRUITS_KEY] });
queryClient.invalidateQueries({ queryKey: [GET_ALL_RECRUITS_KEY] });
queryClient.invalidateQueries({ queryKey: [GET_RECRUIT_TITLES_KEY] });
},
});

Expand Down
40 changes: 14 additions & 26 deletions src/components/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,37 @@
export function Logo() {
return (
<svg width="42" height="18" viewBox="0 0 42 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.43441 16.071L5.42343 12.4775L1.39249 13.5772L3.43441 16.071Z" fill="#0CC47F" />
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.61446 31.8391L12.8291 25.9752L6.07856 27.5573L9.61446 31.8391Z" fill="#20E79D" />
<path
d="M8.20584 5.31051L7.91584 3.64064L9.8988 1.75699L13.0741 2.90586L14.4516 4.38111L13.6354 6.18728L15.3317 6.09142L16.6988 8.32846L16.1945 11.5653L14.2046 12.4462L12.3386 11.8763L11.8024 13.9501L10.1266 15.116L6.75294 13.8613L6.75097 11.3615L4.42429 10.8235L3.52081 8.9727L4.10093 5.24913L6.34823 4.47444L8.20584 5.31051Z"
fill="#0CC47F"
d="M19.1236 13.599L18.7897 10.6982L22.3864 7.65402L27.7346 9.94356L29.9582 12.617L28.3762 15.6431L31.3033 15.6456L33.4339 19.6284L32.247 25.1461L28.7373 26.4648L25.5839 25.3003L24.457 28.8144L21.4595 30.6544L15.7805 28.1632L16.0239 23.8634L12.075 22.7083L10.7037 19.4356L12.0691 13.0882L16.011 11.9776L19.1236 13.599Z"
fill="#20E79D"
/>
<path
opacity="0.9"
fillRule="evenodd"
clipRule="evenodd"
d="M13.0036 10.1666L11.1957 9.34769L11.438 8.64078L13.3344 9.10947L13.0036 10.1666Z"
fill-rule="evenodd"
clip-rule="evenodd"
d="M26.8956 22.4264L23.8667 20.8394L24.3534 19.6475L27.5689 20.6408L26.8956 22.4264Z"
fill="#007D4F"
/>
<path
opacity="0.9"
d="M10.2031 7.46296L10.5432 5.62869L11.6003 5.90878L10.9235 7.6614L10.2031 7.46296Z"
d="M22.3473 17.4978L23.1133 14.3763L24.9039 14.9625L23.5669 17.9102L22.3473 17.4978Z"
fill="#007D4F"
/>
<path
opacity="0.9"
fillRule="evenodd"
clipRule="evenodd"
d="M8.83031 8.70624L6.84901 8.3798L7.06104 7.48553L8.95683 8.03165L8.83031 8.70624Z"
fill-rule="evenodd"
clip-rule="evenodd"
d="M19.862 19.5017L16.4863 18.7446L16.9393 17.2274L20.1462 18.3539L19.862 19.5017Z"
fill="#007D4F"
/>
<path
opacity="0.9"
fillRule="evenodd"
clipRule="evenodd"
d="M10.3933 9.70135L10.14 11.4766L9.14099 11.3758L9.66329 9.5415L10.3933 9.70135Z"
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.4521 21.3671L21.8411 24.3956L20.1328 24.1235L21.2122 21.0201L22.4521 21.3671Z"
fill="#007D4F"
/>
<path
d="M31.2208 5.62317V3.75693H36.8853V5.62317L35.5765 9.53033L33.5597 8.92262L34.6325 5.62317H31.2208Z"
fill="#00A467"
/>
<path d="M40.1037 3.75693H38.1298V9.53033H40.1037V7.62037H41.2838V5.62317H40.1037V3.75693Z" fill="#00A467" />
<path d="M31.5857 11.8744V9.96441H40.1037V13.958H38.1298V11.8744H31.5857Z" fill="#00A467" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20.4014 3.61185V10.0369H23.8956V11.5309H19.9814V13.4904H29.8072V11.5309H25.913V10.0369H29.4078V3.61185H27.4307V5.08679H25.913V3.61185H23.8956V5.08679H22.3983V3.61185H20.4014ZM22.385 6.91295V8.14636H23.8832V6.91295H22.385ZM25.8437 6.91295V8.14636H27.3658V6.91295H25.8437Z"
fill="#00A467"
/>
</svg>
);
}
8 changes: 7 additions & 1 deletion src/container/Sidebar/Collapsible/CollapsibleTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ export function CollapsibleTrigger({ children }: Props) {
const { collapsed, onCollapsedChange } = useCollapsibleContext();

return (
<button aria-expanded={!collapsed} onClick={() => onCollapsedChange(!collapsed)}>
<button
aria-expanded={!collapsed}
onClick={(e) => {
e.stopPropagation();
onCollapsedChange(!collapsed);
}}
className="overflow-hidden">
{children}
</button>
);
Expand Down
97 changes: 81 additions & 16 deletions src/container/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,63 @@ import { MY_INFO_PATH, MY_RECRUIT_PATH } from '@/route';
import { Icon } from '@/system/components';
import { cn } from '@/utils';
import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react';
import { PropsWithChildren, useState } from 'react';
import { Collapsible } from './Collapsible/Collapsible';
import { deleteCookie } from 'cookies-next';

const SIDEBAR_CLASSNAME = {
expanded: 'w-[220px]',
shrinked: 'w-[72px]',
} as const;
import { INFO_TYPES, InfoType } from '@/types';
import { TouchButton } from '@/components/TouchButton';
import { Spacing } from '@/system/utils/Spacing';
import { useGetCardTypeCount } from '@/app/(sidebar)/(my-info)/apis/useGetCardTypeCount';
import { useGetRecruitTitles } from '@/app/(sidebar)/my-recruit/api/useGetRecruitTitles';
import { If } from '@/system/utils/If';
import { motion } from 'framer-motion';

export function Sidebar() {
const router = useRouter();
const pathname = usePathname();
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = useState(true);
const [myInfoCollapsed, setMyInfoCollapsed] = useState(true);
const [myJDCollapsed, setMyJDCollapsed] = useState(true);

const { data: typeCounts } = useGetCardTypeCount();
const { data: recruiteTitles } = useGetRecruitTitles();

const logout = () => {
deleteCookie('accessToken');
deleteCookie('refreshToken');
router.push('/login');
};

const handleInfoTypeClick = (type: InfoType) => {
const targetPath = `${MY_INFO_PATH}?type=${type}`;

if (pathname === MY_INFO_PATH) {
router.replace(targetPath);
return;
}

router.push(targetPath);
};
const isRecruitPage = pathname.includes(MY_RECRUIT_PATH);

return (
<nav
className={`z-[10000] relative flex flex-col px-[16px] py-[32px] h-screen bg-black ${SIDEBAR_CLASSNAME[expanded ? 'expanded' : 'shrinked']}`}>
<div className="relative mb-[32px]">
<Logo />
<motion.nav
variants={{
expanded: { width: '220px' },
shrinked: { width: '72px' },
}}
animate={expanded ? 'expanded' : 'shrinked'}
className={`z-[10000] relative shrink-0 flex flex-col px-[16px] py-[32px] h-screen bg-black`}>
<div className="flex relative mb-[32px]">
<TouchButton onClick={() => router.push(MY_INFO_PATH)}>
<Logo />
</TouchButton>
<button
aria-label={expanded ? '사이드바 축소' : '사이드바 확장'}
className={cn('absolute top-[50%] translate-y-[-50%]', expanded ? 'right-0' : 'right-[-62px]')}
className={cn(
'absolute top-[50%] translate-y-[-50%] p-6 rounded-6',
expanded ? 'right-0 hover:bg-neutral-80' : 'right-[-68px] border hover:bg-neutral-3',
)}
onClick={() => setExpanded(!expanded)}>
<Icon name="division" color={expanded ? '#5A5C63' : '#AEB0B6'} />
</button>
Expand All @@ -64,8 +89,15 @@ export function Sidebar() {
onClick={() => router.push(MY_INFO_PATH)}
/>
<Collapsible.Content>
{/* FIXME: */}
<div style={{ color: 'white' }}>준비중이에요!</div>
<Spacing size={14} />
<div className="flex flex-col">
{INFO_TYPES.map((type) => (
<CollapsibleItemButton key={type} onClick={() => handleInfoTypeClick(type)}>
<div className="truncate">{type.replaceAll('_', ' ')}</div>
<div className="px-4 truncate">{typeCounts?.[type] || 0}</div>
</CollapsibleItemButton>
))}
</div>
</Collapsible.Content>
</Collapsible>
<Collapsible collapsed={expanded ? myJDCollapsed : true} onCollapsedChange={setMyJDCollapsed}>
Expand All @@ -84,7 +116,30 @@ export function Sidebar() {
onClick={() => router.push(MY_RECRUIT_PATH)}
/>
<Collapsible.Content>
<div style={{ color: 'white' }}>준비중이에요!</div>
<If condition={recruiteTitles?.length === 0}>
<Spacing size={20} />
<div className="text-caption1 font-regular text-neutral-30 text-center">
현재 지원중인 공고가 없습니다.
</div>
</If>
<If condition={recruiteTitles?.length !== 0}>
<Spacing size={14} />
<div className="flex flex-col">
{recruiteTitles?.map((data) => (
<CollapsibleItemButton key={data.id} onClick={() => router.push(`${MY_RECRUIT_PATH}/${data.id}`)}>
<div className="px-6 truncate">{data.title}</div>
</CollapsibleItemButton>
))}
</div>
<Spacing size={32} />
<div className="flex justify-center">
<TouchButton
className="bg-neutral-80 text-neutral-30 px-8 py-6 rounded-6 text-caption1 font-regular"
onClick={() => router.push(MY_RECRUIT_PATH)}>
모든 공고 보기
</TouchButton>
</div>
</If>
</Collapsible.Content>
</Collapsible>
</div>
Expand All @@ -99,6 +154,16 @@ export function Sidebar() {
onClick={logout}
/>
</div>
</nav>
</motion.nav>
);
}

function CollapsibleItemButton({ onClick, children }: PropsWithChildren<{ onClick: () => void }>) {
return (
<TouchButton
onClick={onClick}
className="flex justify-between text-neutral-10 py-6 hover:bg-neutral-80 mx-[-16px] px-[22px] text-label2 font-regular">
{children}
</TouchButton>
);
}
2 changes: 1 addition & 1 deletion src/container/Sidebar/SidebarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function SidebarButton({
<div className="relative flex justify-start gap-[12px]">
<Icon name={iconName} color={selected ? SELECTED_COLOR : DEFAULT_COLOR} />
<If condition={expanded}>
<Text typography="body1" color={selected ? SELECTED_COLOR : DEFAULT_COLOR}>
<Text typography="body1" color={selected ? SELECTED_COLOR : DEFAULT_COLOR} className="truncate">
{expandedText}
</Text>
</If>
Expand Down
Loading