Skip to content

Commit

Permalink
템플릿 공개 범위 설정(�visibility) 기능 구현 (#787)
Browse files Browse the repository at this point in the history
* feat(components): Toggle 컴포넌트

* feat(images): PrivateIcon, PublicIcon 추가

* refactor(mocks): templateList mock data에 "visibility" 추가

* feat(src): visibility 기능 추가

* feat(src): visibility가 private인 경우 템플릿 카드, 상세 페이지에서 privateIcon 보이도록 설정

* refactor(TemplateCard): 사용하지 않는 스타일드 컴포넌트 제거

* refactor(src): PRIVATE 상수화

* refactor(Toggle): Toggle.style 에서 불필요한 calc() 제거

* refactor(src): ICON_SIZE 상수 선언 및 적용

* refactor(components): CategoryDropdown 스타일 변경

* refactor(Toggle): Toggle 스타일 변경 및 showOptions 생성

* refactor(pages): 템플릿 생성 및 수정 페이지 Toggle 변경사항 반영
  • Loading branch information
Hain-tain authored Oct 18, 2024
1 parent 1b2a3b3 commit ce7380a
Show file tree
Hide file tree
Showing 36 changed files with 392 additions and 93 deletions.
10 changes: 9 additions & 1 deletion frontend/src/assets/images/Hamburger.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { ICON_SIZE } from '@/style/styleConstants';

interface Props {
menuOpen?: boolean;
onClick?: () => void;
}

const HamburgerIcon = ({ onClick }: Props) => (
<div onClick={onClick} style={{ cursor: 'pointer', padding: '0.5rem' }}>
<svg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<svg
width={ICON_SIZE.LARGE}
height={ICON_SIZE.LARGE}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M5 17H19M5 12H19M5 7H19' stroke='black' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' />
</svg>
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/assets/images/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export { default as ClockIcon } from './clock.svg';
export { default as BooksIcon } from './books.svg';
export { default as CheckCircleIcon } from './checkCircle.svg';
export { default as LikeIcon } from './like';
export { default as PrivateIcon } from './private.svg';
export { default as PublicIcon } from './public.svg';

// Logo
export { default as CodeZapLogo } from './codezapLogo.svg';
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/assets/images/private.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/images/public.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import styled from '@emotion/styled';

export const CategoryDropdownContainer = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
`;
6 changes: 4 additions & 2 deletions frontend/src/components/CategoryDropdown/CategoryDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useInputWithValidate } from '@/hooks';
import { validateCategoryName } from '@/service/validates';
import { Category } from '@/types';

import * as S from './CategoryDropdown.style';

interface Props {
options: Category[];
isOpen: boolean;
Expand Down Expand Up @@ -53,7 +55,7 @@ const CategoryDropdown = ({
};

return (
<>
<S.CategoryDropdownContainer>
<CategoryGuide isOpen={isOpen} categoryErrorMessage={categoryInputErrorMessage} />
<Dropdown
options={options}
Expand All @@ -72,7 +74,7 @@ const CategoryDropdown = ({
/>
}
/>
</>
</S.CategoryDropdownContainer>
);
};

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CategoryGuide/CategoryGuide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const CategoryGuide = ({ isOpen, categoryErrorMessage }: Props) => {
const isError = categoryErrorMessage !== '';

return (
<Guide isOpen={isOpen} css={{ marginTop: '0.5rem', marginBottom: '-0.5rem' }} aria-hidden={!isOpen}>
<Guide isOpen={isOpen} aria-hidden={!isOpen}>
{isError ? (
<Text.Small color={theme.color.light.analogous_primary_300}>{categoryErrorMessage}</Text.Small>
) : (
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChevronIcon } from '@/assets/images';
import { ICON_SIZE } from '@/style/styleConstants';

import { theme } from '../../style/theme';
import Text from '../Text/Text';
Expand Down Expand Up @@ -65,8 +66,8 @@ const SelectedButton = <T,>({ toggleDropdown, getOptionLabel, currentValue, isOp
<S.SelectedButton onClick={toggleDropdown}>
<Text.Small color={theme.color.light.black}>{getOptionLabel(currentValue)}</Text.Small>
<ChevronIcon
width={16}
height={16}
width={ICON_SIZE.SMALL}
height={ICON_SIZE.SMALL}
aria-label='정렬기준 펼침'
css={{ transition: 'transform 0.3s ease', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
/>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/LikeButton/LikeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LikeIcon } from '@/assets/images';
import { Text } from '@/components';
import { ICON_SIZE } from '@/style/styleConstants';
import { theme } from '@/style/theme';
import { formatWithK } from '@/utils';

Expand All @@ -13,7 +14,7 @@ interface Props {

const LikeButton = ({ likesCount, isLiked, onLikeButtonClick }: Props) => (
<S.LikeButtonContainer isLiked={isLiked} onClick={onLikeButtonClick}>
<LikeIcon state={isLiked ? 'like' : 'unlike'} size={20} />
<LikeIcon state={isLiked ? 'like' : 'unlike'} size={ICON_SIZE.MEDIUM_LARGE} />
<Text.Medium color={theme.color.light.secondary_800}>{formatWithK(likesCount)}</Text.Medium>
</S.LikeButtonContainer>
);
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/LikeCounter/LikeCounter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LikeIcon } from '@/assets/images';
import { Text } from '@/components';
import { ICON_SIZE } from '@/style/styleConstants';
import { theme } from '@/style/theme';
import { formatWithK } from '@/utils';

Expand All @@ -12,7 +13,7 @@ interface Props {

const LikeCounter = ({ likesCount, isLiked }: Props) => (
<S.LikeCounterContainer>
<LikeIcon state={isLiked ? 'like' : 'unClickable'} size={14} />
<LikeIcon state={isLiked ? 'like' : 'unClickable'} size={ICON_SIZE.X_SMALL} />
<Text.Small color={theme.color.light.secondary_800}>{formatWithK(likesCount)}</Text.Small>
</S.LikeCounterContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useRef } from 'react';

import { TrashcanIcon } from '@/assets/images';
import { SourceCode } from '@/components';
import { ICON_SIZE } from '@/style/styleConstants';
import { getLanguageByFilename } from '@/utils';

import * as S from './SourceCodeEditor.style';
Expand Down Expand Up @@ -67,7 +68,7 @@ const SourceCodeEditor = ({
handleContentChange={handleContentChange}
/>
<S.DeleteButton size='small' variant='text' onClick={handleDeleteSourceCode}>
<TrashcanIcon width={24} height={24} aria-label='템플릿 삭제' />
<TrashcanIcon width={ICON_SIZE.LARGE} height={ICON_SIZE.LARGE} aria-label='템플릿 삭제' />
</S.DeleteButton>
</S.SourceCodeEditorContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { Meta, StoryObj } from '@storybook/react';

import { templates } from '@/mocks/templateList.json';
import { TemplateListItem } from '@/types';

import TemplateCard from './TemplateCard';

const meta: Meta<typeof TemplateCard> = {
title: 'TemplateCard',
component: TemplateCard,
args: {
template: templates[0],
template: templates[0] as TemplateListItem,
},
};

Expand Down
6 changes: 0 additions & 6 deletions frontend/src/components/TemplateCard/TemplateCard.style.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { EditorView } from '@uiw/react-codemirror';

import { theme } from '@/style/theme';

Expand Down Expand Up @@ -88,8 +87,3 @@ export const NoWrapTextWrapper = styled.div`
export const BlankDescription = styled.div`
height: 1rem;
`;

export const CustomCodeMirrorTheme = EditorView.theme({
'.cm-activeLine': { backgroundColor: `transparent !important` },
'.cm-activeLineGutter': { backgroundColor: `transparent !important` },
});
12 changes: 8 additions & 4 deletions frontend/src/components/TemplateCard/TemplateCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ClockIcon, PersonIcon } from '@/assets/images';
import { ClockIcon, PersonIcon, PrivateIcon } from '@/assets/images';
import { Button, Flex, LikeCounter, TagButton, Text, SourceCodeViewer } from '@/components';
import { useToggle } from '@/hooks';
import { VISIBILITY_PRIVATE } from '@/service/constants';
import { ICON_SIZE } from '@/style/styleConstants';
import { theme } from '@/style/theme';
import type { Tag, TemplateListItem } from '@/types';
import { formatRelativeTime } from '@/utils/formatRelativeTime';
Expand All @@ -12,8 +14,9 @@ interface Props {
}

const TemplateCard = ({ template }: Props) => {
const { title, description, thumbnail, tags, modifiedAt, member } = template;
const { title, description, thumbnail, tags, modifiedAt, member, visibility } = template;
const [showAllTagList, toggleShowAllTagList] = useToggle();
const isPrivate = visibility === VISIBILITY_PRIVATE;

const blockMovingToDetailPage = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.MouseEvent<HTMLDivElement, MouseEvent>,
Expand All @@ -34,8 +37,9 @@ const TemplateCard = ({ template }: Props) => {
<Flex width='100%' direction='column' gap='1rem'>
<Flex width='100%' justify='space-between' gap='1rem'>
<Flex gap='0.75rem' flex='1' style={{ minWidth: '0' }}>
{isPrivate && <PrivateIcon width={ICON_SIZE.X_SMALL} color={theme.color.light.secondary_800} />}
<Flex align='center' gap='0.25rem' style={{ minWidth: '0' }}>
<PersonIcon width={14} />
<PersonIcon width={ICON_SIZE.X_SMALL} />
<S.EllipsisTextWrapper style={{ width: '100%' }}>
<Text.Small
color={theme.mode === 'dark' ? theme.color.dark.primary_300 : theme.color.light.primary_500}
Expand All @@ -45,7 +49,7 @@ const TemplateCard = ({ template }: Props) => {
</S.EllipsisTextWrapper>
</Flex>
<Flex align='center' gap='0.25rem'>
<ClockIcon width={14} />
<ClockIcon width={ICON_SIZE.X_SMALL} />
<S.NoWrapTextWrapper>
<Text.Small color={theme.color.light.primary_500}>{formatRelativeTime(modifiedAt)}</Text.Small>
</S.NoWrapTextWrapper>
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/components/Toggle/Toggle.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';

import Toggle from './Toggle';

const meta: Meta<typeof Toggle> = {
title: 'Toggle',
component: Toggle,
};

export default meta;

type Story = StoryObj<typeof Toggle>;

export const Default: Story = {
args: {
options: ['private', 'public'],
selectedOption: 'private',
},

render: (args) => {
const [selectedOption, setSelectedOption] = useState(args.selectedOption);

const handleToggle = (option: string) => {
setSelectedOption(option);
};

return <Toggle options={args.options} selectedOption={selectedOption} switchOption={handleToggle} />;
},
};
55 changes: 55 additions & 0 deletions frontend/src/components/Toggle/Toggle.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import styled from '@emotion/styled';

import { theme } from '@/style/theme';

export const ToggleContainer = styled.div`
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
max-width: 12rem;
height: 2.375rem; /* Button medium size와 동일 */
background-color: ${theme.color.light.secondary_100};
border-radius: 20px;
`;

export const ToggleOption = styled.div<{ selected: boolean }>`
z-index: 1;
display: flex;
flex: 1;
gap: 1px;
align-items: center;
justify-content: center;
padding: 0 1rem;
color: ${({ selected }) => (selected ? theme.color.light.white : theme.color.light.secondary_500)};
transition: color 0.3s ease;
`;

export const ToggleSlider = styled.div<{
isRight: boolean;
optionSliderColor: [string | undefined, string | undefined];
}>`
position: absolute;
top: 2px;
left: 2px;
transform: ${({ isRight }) => (isRight ? 'translateX(calc(100% - 4px))' : 'translateX(0)')};
width: 50%;
height: calc(100% - 4px);
background-color: ${({ isRight, optionSliderColor: [leftColor, rightColor] }) =>
isRight ? (rightColor ?? theme.color.light.secondary_500) : (leftColor ?? theme.color.light.secondary_500)};
border-radius: 18px;
transition:
transform 0.3s ease,
background-color 0.3s ease;
`;
48 changes: 48 additions & 0 deletions frontend/src/components/Toggle/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ReactNode } from 'react';

import * as S from './Toggle.style';

type ToggleOption<T extends string> = T;

export interface ToggleProps<T extends string> {
showOptions?: boolean;
options: [ToggleOption<T>, ToggleOption<T>];
optionSliderColor?: [string | undefined, string | undefined];
optionAdornments?: [ReactNode, ReactNode];
selectedOption: ToggleOption<T>;
switchOption: (option: ToggleOption<T>) => void;
}

const Toggle = <T extends string>({
showOptions = true,
options,
optionAdornments = [undefined, undefined],
optionSliderColor = [undefined, undefined],
selectedOption,
switchOption,
}: ToggleProps<T>) => {
const [leftOption, rightOption] = options;
const [leftOptionAdornment, rightOptionAdornment] = optionAdornments;

const handleToggle = () => {
const newOption = selectedOption === leftOption ? rightOption : leftOption;

switchOption(newOption);
};

return (
<S.ToggleContainer onClick={handleToggle}>
<S.ToggleSlider isRight={selectedOption === rightOption} optionSliderColor={optionSliderColor} />
<S.ToggleOption selected={selectedOption === leftOption}>
{leftOptionAdornment ?? ''}
{showOptions && leftOption}
</S.ToggleOption>
<S.ToggleOption selected={selectedOption === rightOption}>
{showOptions && rightOption}
{rightOptionAdornment ?? ''}
</S.ToggleOption>
</S.ToggleContainer>
);
};

export default Toggle;
1 change: 1 addition & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { default as NewCategoryInput } from './NewCategoryInput/NewCategoryInput
export { default as NoSearchResults } from './NoSearchResults/NoSearchResults';
export { default as Textarea } from './Textarea/Textarea';
export { default as ContactUs } from './ContactUs/ContactUs';
export { default as Toggle } from './Toggle/Toggle';
export { default as ScrollTopButton } from './ScrollTopButton/ScrollTopButton';

// Skeleton UI
Expand Down
Loading

0 comments on commit ce7380a

Please sign in to comment.