Skip to content

Commit

Permalink
feat: 사이드바 디자인 변경
Browse files Browse the repository at this point in the history
  • Loading branch information
lurgi committed Oct 9, 2024
1 parent 3a25b0d commit 8a48712
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 75 deletions.
128 changes: 103 additions & 25 deletions frontend/src/components/dashboard/DashboardSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,123 @@
import Logo from '@assets/images/logo.svg';
import Accordion from '@components/_common/molecules/Accordion';
import { routes } from '@router/path';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';

import type { RecruitmentStatusObject } from '@utils/compareTime';

import { Fragment } from 'react/jsx-runtime';
import { HiChevronDoubleLeft, HiOutlineHome } from 'react-icons/hi2';
import { HiOutlineMenu } from 'react-icons/hi';
import { GrDocumentTime, GrDocumentUser, GrDocumentVerified } from 'react-icons/gr';

import IconButton from '@components/_common/atoms/IconButton';
import LogoutButton from './LogoutButton';

import S from './style';

interface Option {
text: string;
isSelected: boolean;
applyFormId: string;
dashboardId: string;
status: RecruitmentStatusObject;
}

interface SidebarStyle {
isSidebarOpen: boolean;
onClickSidebarToggle: () => void;
}

interface DashboardSidebarProps {
sidebarStyle: SidebarStyle;
options: Option[];
}

export default function DashboardSidebar({ options }: DashboardSidebarProps) {
export default function DashboardSidebar({ sidebarStyle, options }: DashboardSidebarProps) {
const pendingPosts = options.filter(({ status }) => status.isPending);
const onGoingPosts = options.filter(({ status }) => status.isOngoing);
const closedPosts = options.filter(({ status }) => status.isClosed);

const sidebarContentList = [
{ title: '모집 예정 공고 목록', posts: pendingPosts },
{ title: '진행 중 공고 목록', posts: onGoingPosts },
{ title: '마감 된 공고 목록', posts: closedPosts },
];
const location = useLocation();

return (
<S.Container>
<Link to={routes.dashboard.list()}>
<S.Logo
src={Logo}
alt="크루루 로고"
/>
</Link>

<S.Contents>
<Accordion title={<Link to={routes.dashboard.list()}>공고</Link>}>
{options.map(({ text, isSelected, applyFormId, dashboardId }, index) => (
// eslint-disable-next-line react/no-array-index-key
<Accordion.ListItem key={index}>
<S.LinkContainer isSelected={isSelected}>
<Link to={routes.dashboard.post({ dashboardId, applyFormId })}>{text}</Link>
</S.LinkContainer>
</Accordion.ListItem>
))}
</Accordion>
</S.Contents>

<LogoutButton />
<S.Container isSidebarOpen={sidebarStyle.isSidebarOpen}>
<S.SidebarHeader>
{sidebarStyle.isSidebarOpen && (
<Link to={routes.dashboard.list()}>
<S.Logo
src={Logo}
alt="크루루 로고"
/>
</Link>
)}

<IconButton
size="sm"
outline={false}
onClick={sidebarStyle.onClickSidebarToggle}
>
{sidebarStyle.isSidebarOpen ? <HiChevronDoubleLeft /> : <HiOutlineMenu />}
</IconButton>
</S.SidebarHeader>

<nav>
<S.Contents>
<S.SidebarItem>
<Link to={routes.dashboard.list()}>
<S.SidebarItemLink isSelected={location.pathname === routes.dashboard.list()}>
<HiOutlineHome
size={24}
strokeWidth={2.4}
/>
{sidebarStyle.isSidebarOpen && <S.SidebarItemText>모집 공고</S.SidebarItemText>}
</S.SidebarItemLink>
</Link>
</S.SidebarItem>

<S.Divider />

{sidebarContentList.map(({ title, posts }) => {
if (posts.length === 0) return;
return (
<Fragment key={title}>
<S.ContentSubTitle>{sidebarStyle.isSidebarOpen ? title : <S.Circle />}</S.ContentSubTitle>
{posts.map(({ text, isSelected, applyFormId, dashboardId, status }) => (
<S.SidebarItem key={applyFormId}>
<Link to={routes.dashboard.post({ dashboardId, applyFormId })}>
<S.SidebarItemLink isSelected={isSelected}>
{status.isPending ? (
<GrDocumentTime
size={24}
strokeWidth={2}
/>
) : status.isOngoing ? (
<GrDocumentUser
size={24}
strokeWidth={2}
/>
) : status.isClosed ? (
<GrDocumentVerified
size={24}
strokeWidth={2}
/>
) : null}
{sidebarStyle.isSidebarOpen && <S.SidebarItemText>{text}</S.SidebarItemText>}
</S.SidebarItemLink>
</Link>
</S.SidebarItem>
))}
</Fragment>
);
})}
</S.Contents>
</nav>

{sidebarStyle.isSidebarOpen && <LogoutButton />}
</S.Container>
);
}
77 changes: 58 additions & 19 deletions frontend/src/components/dashboard/DashboardSidebar/style.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,96 @@
import styled from '@emotion/styled';

const Container = styled.div`
const Container = styled.div<{ isSidebarOpen: boolean }>`
position: relative;
width: 28rem;
width: ${({ isSidebarOpen }) => (isSidebarOpen ? '276px' : '56px')};
height: 100%;
border-right: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
padding: 3.6rem 1.6rem;
background-color: ${({ theme }) => theme.baseColors.grayscale[50]};
border-radius: 1.6rem 0 0 1.6rem;
display: flex;
flex-direction: column;
gap: 3.2rem;
gap: 4rem;
`;

const SidebarHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
height: 2.4rem;
`;

const Logo = styled.img`
height: 2.4rem;
`;

const Contents = styled.nav`
const Contents = styled.ul`
display: flex;
flex-direction: column;
gap: 1.4rem;
gap: 2rem;
`;

const LinkContainer = styled.div<{ isSelected: boolean }>`
${({ theme }) => theme.typography.common.block}
color: ${({ theme, isSelected }) => (isSelected ? theme.colors.brand.primary : theme.colors.text.default)};
margin-bottom: 0;
const SidebarItem = styled.li`
list-style: none;
`;

const SidebarItemLink = styled.div<{ isSelected: boolean }>`
display: flex;
align-items: flex-start;
align-items: center;
gap: 1rem;
color: ${({ theme }) => theme.baseColors.grayscale[900]};
opacity: ${({ isSelected }) => (isSelected ? 0.99 : 0.4)};
transition: opacity 0.2s ease;
& > button > a {
text-align: start;
&:hover {
opacity: 0.99;
}
`;

const SidebarItemText = styled.p`
width: 21rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
${({ theme }) => theme.typography.common.largeAccent}
`;

const Divider = styled.div`
border-bottom: 0.15rem solid ${({ theme }) => theme.baseColors.grayscale[400]};
`;

const ContentSubTitle = styled.p`
height: 2rem;
color: ${({ theme }) => theme.baseColors.grayscale[500]};
${({ theme }) => theme.typography.common.default}
`;

const Circle = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
&::before {
content: '•';
display: block;
width: 1rem;
aspect-ratio: 1/1;
margin: 0 0.8rem;
scale: 1.3;
}
`;

const S = {
Container,
SidebarHeader,
Logo,
Contents,
LinkContainer,
SidebarItem,
SidebarItemLink,
SidebarItemText,
Divider,
ContentSubTitle,
Circle,
};

export default S;
2 changes: 1 addition & 1 deletion frontend/src/pages/Dashboard/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { hiddenStyles, hideScrollBar, visibleStyles } from '@styles/utils';

const AppContainer = styled.div`
padding: 3.2rem 1.6rem;
height: 100vh;
height: 100%;
`;

const Header = styled.div`
Expand Down
41 changes: 15 additions & 26 deletions frontend/src/pages/DashboardLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import { useRef, useState } from 'react';
import { useState } from 'react';

import DashboardSidebar from '@components/dashboard/DashboardSidebar';
import IconButton from '@components/_common/atoms/IconButton';
import useGetDashboards from '@hooks/useGetDashboards';
import { HiChevronDoubleLeft, HiOutlineMenu } from 'react-icons/hi';

import { Outlet, useParams } from 'react-router-dom';
import { getTimeStatus } from '@utils/compareTime';
import S from './style';

export default function DashboardLayout() {
const { applyFormId: currentPostId } = useParams();
const { data, isLoading } = useGetDashboards();
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const sidebarRef = useRef<HTMLDivElement>(null);

if (isLoading) return <div>Loading...</div>;
if (!data) return <div>something wrong</div>;

const titleList = data.dashboards.map(({ title, dashboardId, applyFormId }) => ({
const applyFormList = data?.dashboards.map(({ title, dashboardId, applyFormId, startDate, endDate }) => ({
text: title,
isSelected: !!currentPostId && currentPostId === applyFormId,
status: getTimeStatus({ startDate, endDate }),
applyFormId,
dashboardId,
}));
Expand All @@ -31,25 +27,18 @@ export default function DashboardLayout() {

return (
<S.Layout>
<S.SidebarContainer>
{isSidebarOpen && (
<S.Sidebar ref={sidebarRef}>
<DashboardSidebar options={titleList} />
</S.Sidebar>
<S.Sidebar>
{isLoading ? (
<div>Loading...</div> // TODO: Suspense로 리팩토링
) : !applyFormList ? (
<div>something wrong</div> // TODO: ErrorBoundary로 리팩터링
) : (
<DashboardSidebar
sidebarStyle={{ isSidebarOpen, onClickSidebarToggle: handleToggleSidebar }}
options={applyFormList}
/>
)}

<S.SidebarController isSidebarOpen={isSidebarOpen}>
<S.ToggleButton>
<IconButton
size="sm"
outline={false}
onClick={handleToggleSidebar}
>
{isSidebarOpen ? <HiChevronDoubleLeft /> : <HiOutlineMenu />}
</IconButton>
</S.ToggleButton>
</S.SidebarController>
</S.SidebarContainer>
</S.Sidebar>

<S.MainContainer isSidebarOpen={isSidebarOpen}>
<Outlet />
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/pages/DashboardLayout/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import styled from '@emotion/styled';

interface SidebarStyleProps {
isSidebarOpen: boolean;
sidebarWidth?: number;
}

const Layout = styled.div`
height: 100vh;
width: 100vw;
display: flex;
background-color: ${({ theme }) => theme.baseColors.grayscale[50]};
overflow-y: hidden;
`;

const SidebarContainer = styled.div`
Expand All @@ -30,9 +33,7 @@ const ToggleButton = styled.div`
`;

const MainContainer = styled.div<SidebarStyleProps>`
flex: 1;
height: 100vh;
width: ${({ isSidebarOpen }) => (isSidebarOpen ? 'calc(100% - 276px)' : 'calc(100% - 56px)')};
padding-left: ${({ isSidebarOpen }) => isSidebarOpen && '1rem'};
`;

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/utils/compareTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface GetRecruitmentStatusProps {
endDate: string;
}

interface RecruitmentStatusObject {
export interface RecruitmentStatusObject {
status: RecruitmentStatusType;
isPending: boolean;
isOngoing: boolean;
Expand Down

0 comments on commit 8a48712

Please sign in to comment.