Skip to content

Commit

Permalink
feat-fe: 공통 컴포넌트 SideBar 구현 (#271)
Browse files Browse the repository at this point in the history
  • Loading branch information
lurgi authored Aug 6, 2024
1 parent 6a31b08 commit 7f4ebc1
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 3 deletions.
51 changes: 51 additions & 0 deletions frontend/src/components/common/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Meta, StoryObj } from '@storybook/react';
import Accordion from './index';
import S from './style';

const meta: Meta<typeof Accordion> = {
title: 'Common/Accordion',
component: Accordion,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Accordion 컴포넌트는 제목을 클릭하여 내용을 확장하거나 축소할 수 있는 컴포넌트입니다.',
},
},
},
tags: ['autodocs'],
argTypes: {
title: {
description: 'Accordion의 제목입니다.',
control: { type: 'text' },
table: {
type: { summary: 'string' },
},
},
children: {
description: 'Accordion의 내부 요소입니다.',
control: { type: 'text' },
table: {
type: { summary: 'React.ReactNode' },
},
},
},
args: {
title: '공고',
},
};

export default meta;
type Story = StoryObj<typeof Accordion>;

export const Default: Story = {
render: (args) => (
<div style={{ width: '200px' }}>
<Accordion {...args}>
<S.ListItem>프론트엔드 7기 모집</S.ListItem>
<S.ListItem>백엔드 7기 모집</S.ListItem>
<S.ListItem>안드로이드 7기 모집</S.ListItem>
</Accordion>
</div>
),
};
39 changes: 39 additions & 0 deletions frontend/src/components/common/Accordion/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import { HiOutlineClipboardList } from 'react-icons/hi';

import S from './style';
import ChevronButton from '../ChevronButton';

interface AccordionProps {
title: string;
children: React.ReactNode;
}

function Accordion({ title, children }: AccordionProps) {
// TODO: 현재 아코디언의 오픈값을 True로 설정합니다. 추후에 아코디언이 추가될 경우 변경이 필요합니다.
const [isOpen] = useState(true);

const toggleAccordion = () => {
// setIsOpen(!isOpen);
};

return (
<S.Container>
<S.Header onClick={toggleAccordion}>
<S.Title>
<HiOutlineClipboardList />
<S.TitleText>{title}</S.TitleText>
</S.Title>
<ChevronButton
size="sm"
direction={isOpen ? 'down' : 'up'}
/>
</S.Header>
{isOpen && <S.List>{children}</S.List>}
</S.Container>
);
}

Accordion.ListItem = S.ListItem;

export default Accordion;
56 changes: 56 additions & 0 deletions frontend/src/components/common/Accordion/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import styled from '@emotion/styled';

const Container = styled.div`
display: flex;
flex-direction: column;
gap: 0.8rem;
`;

const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 0.4rem;
width: 100%;
/* cursor: pointer; */ // TODO: 추후에 아코디언이 추가될 경우 변경이 필요합니다.
border-bottom: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
`;

const Title = styled.div`
display: flex;
align-items: center;
gap: 0.4rem;
${({ theme }) => theme.typography.heading[500]}
width: 80%;
`;

const TitleText = styled.span`
overflow: hidden;
text-overflow: ellipsis;
width: 80%;
white-space: nowrap;
`;

const List = styled.ul`
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 0 0 0 0.8rem;
`;

const ListItem = styled.li`
${({ theme }) => theme.typography.common.block}
margin-bottom: 0;
`;

const S = {
Container,
Header,
Title,
TitleText,
List,
ListItem,
};

export default S;
4 changes: 2 additions & 2 deletions frontend/src/components/common/ToggleSwitch/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ const Switch = styled.div<StyleProps>`
aspect-ratio: 5/3;
background-color: ${({ isChecked, theme }) =>
isChecked ? theme.baseColors.purplescale[600] : theme.baseColors.grayscale[100]};
isChecked ? theme.colors.brand.primary : theme.baseColors.grayscale[100]};
outline: 0.2rem solid
${({ isChecked, theme }) => (isChecked ? theme.baseColors.purplescale[600] : theme.baseColors.grayscale[700])};
${({ isChecked, theme }) => (isChecked ? theme.colors.brand.primary : theme.baseColors.grayscale[700])};
border-radius: 99rem;
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Meta, StoryObj } from '@storybook/react';
import DashboardSidebar from './index';

const meta: Meta<typeof DashboardSidebar> = {
title: 'Components/Dashboard/Sidebar',
component: DashboardSidebar,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'DashboardSidebar 컴포넌트는 로고와 여러 개의 Accordion으로 구성된 사이드바입니다.',
},
},
},
tags: ['autodocs'],
decorators: [
(Child) => (
<div
style={{
height: '1000px',
width: '400px',
backgroundColor: 'gray',
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
}}
>
<Child />
</div>
),
],
};

export default meta;
type Story = StoryObj<typeof DashboardSidebar>;

export const Default: Story = {};
28 changes: 28 additions & 0 deletions frontend/src/components/dashboard/DashboardSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Accordion from '@components/common/Accordion';
import S from './style';

export default function DashboardSidebar() {
// TODO: URL Param과 같은 방법으로 현재 공고가 Selected 인지 확인할 수 있도록 합니다.

const options = [
{ text: '프론트엔드 7기 모집', isSelected: true },
{ text: '백엔드 7기 모집', isSelected: false },
{ text: '안드로이드 7기 모집', isSelected: false },
];

return (
<S.Container>
<S.Logo>ㅋㄹㄹ</S.Logo>
<S.Contents>
<Accordion title="공고">
{options.map(({ text, isSelected }, index) => (
// eslint-disable-next-line react/no-array-index-key
<Accordion.ListItem key={index}>
<S.NavButton isSelected={isSelected}>{text}</S.NavButton>
</Accordion.ListItem>
))}
</Accordion>
</S.Contents>
</S.Container>
);
}
44 changes: 44 additions & 0 deletions frontend/src/components/dashboard/DashboardSidebar/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import styled from '@emotion/styled';

const Container = styled.div`
width: 300px;
border-right: 1px solid ${({ theme }) => theme.baseColors.grayscale[400]};
padding: 3.6rem 1.6rem;
height: 100%;
background-color: ${({ theme }) => theme.baseColors.grayscale[50]};
border-radius: 1.6rem 0 0 1.6rem;
`;

const Logo = styled.div`
${({ theme }) => theme.typography.heading[700]}
margin-bottom: 3.2rem;
`;

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

const NavButton = styled.button<{ isSelected: boolean }>`
${({ theme }) => theme.typography.common.block}
color: ${({ theme, isSelected }) => (isSelected ? theme.colors.brand.primary : theme.colors.text.default)};
margin-bottom: 0;
&::before {
content: '•';
width: 1rem;
aspect-ratio: 1/1;
margin: 0 0.8rem;
}
`;

const S = {
Container,
Logo,
Contents,
NavButton,
};

export default S;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Meta, StoryObj } from '@storybook/react';
import RecruitmentSidebar from './index';

const meta: Meta<typeof RecruitmentSidebar> = {
title: 'Components/Recruitment/RecruitmentSidebar',
component: RecruitmentSidebar,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'RecruitmentSidebar 컴포넌트는 모집 공고를 작성하고 게시하는 과정을 안내하는 사이드바입니다.',
},
},
},
tags: ['autodocs'],
decorators: [
(Child) => (
<div
style={{
height: '1000px',
width: '400px',
backgroundColor: 'gray',
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
}}
>
<Child />
</div>
),
],
};

export default meta;
type Story = StoryObj<typeof RecruitmentSidebar>;

export const Default: Story = {};
44 changes: 44 additions & 0 deletions frontend/src/components/recruitment/RecruitmentSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import S from './style';

interface StepProps {
stepNumber: number;
label: string;
isSelected: boolean;
}
function Step({ stepNumber, label, isSelected }: StepProps) {
return (
<S.StepContainer isSelected={isSelected}>
<S.StepNumber isSelected={isSelected}>{stepNumber}</S.StepNumber>
<S.StepLabel isSelected={isSelected}>{label}</S.StepLabel>
</S.StepContainer>
);
}

export default function RecruitmentSidebar() {
// TODO: isSelected에 해당하는 값을 넣어야 합니다.
const options = [
{ text: '공고 작성', isSelected: true },
{ text: '지원서 작성', isSelected: false },
{ text: '공고 게시', isSelected: false },
];

return (
<S.Container>
<S.SidebarHeader>
<S.Title>공고 생성</S.Title>
<S.Description>모집 공고를 작성하고 인터넷에 게시하세요.</S.Description>
</S.SidebarHeader>
<S.SidebarContents>
{options.map(({ text, isSelected }, index) => (
<Step
// eslint-disable-next-line react/no-array-index-key
key={index}
stepNumber={index + 1}
label={text}
isSelected={isSelected}
/>
))}
</S.SidebarContents>
</S.Container>
);
}
Loading

0 comments on commit 7f4ebc1

Please sign in to comment.