Skip to content

Commit

Permalink
feat : reaction 컴포넌트 구현 (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
jymaeng1234 authored Nov 5, 2024
1 parent d8f1a5d commit a8fd6db
Show file tree
Hide file tree
Showing 22 changed files with 924 additions and 4 deletions.
11 changes: 11 additions & 0 deletions public/reaction_plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,26 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import ToastContainer from '@/features/Toast/ui/ToastContainer';
import { RouterProvider } from 'react-router-dom';
import router from './router';
import ReactionSelector from '@/widgets/reaction-selector/ui/ReactionSelector';

const queryClient = new QueryClient();

const App: React.FC = () => {
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1pbmpvb24iLCJlbWFpbCI6ImFubmF3YTZAbmF2ZXIuY29tLmNvbSIsImlhdCI6MTczMDc3MjU3MiwiZXhwIjoxNzMwNzgzMzcyfQ.jOVHX1RsVVr0hYB9bF9E3CRL3cQzYb9bYr9b35AYFg0';

return (
<QueryClientProvider client={queryClient}>
<GlobalStyles />
<ToastContainer />
<RouterProvider router={router} />
{/* <RouterProvider router={router} /> */}
<ReactionSelector
diaryId={36}
userEmail="annawa6@naver.com.com"
isHorizontal
isAddBtnVisible
token={token}
/>
</QueryClientProvider>
);
};
Expand Down
7 changes: 7 additions & 0 deletions src/entities/ReactionButtonContainer/model/reaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Emotions } from '@/shared/model/EmotionEnum';

export interface Reaction {
emotion: Emotions;
reactionCnt: number;
isClicked: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';

import ReactionButtonContainer from './ReactionButtonContainer';
import { Emotions } from '../../../shared/model/EmotionEnum';

const meta: Meta<typeof ReactionButtonContainer> = {
component: ReactionButtonContainer,
title: 'entities/ReactionButtonContainer',
tags: ['autodocs'],
argTypes: {
isHorizontal: {
description: '버튼 배열 방식: true일 경우 가로 배열, false일 경우 세로 배열입니다.',
},
isAddBtnVisible: {
description: '이모티콘 추가 버튼을 추가하세요 (기본 : false)',
},
reactions: {
description: '감정 버튼 목록입니다. 각 버튼의 감정, 반응 수, 클릭 상태를 포함합니다.',
},
},
};

export default meta;

type Story = StoryObj<typeof ReactionButtonContainer>;

export const Default: Story = {
args: {
isHorizontal: true,
isAddBtnVisible: true,
reactions: [
{ emotion: Emotions.Happy, reactionCnt: 10, isClicked: false },
{ emotion: Emotions.Angry, reactionCnt: 5, isClicked: false },
{ emotion: Emotions.Annoyed, reactionCnt: 8, isClicked: false },
{ emotion: Emotions.Awkward, reactionCnt: 10, isClicked: false },
{ emotion: Emotions.Blank, reactionCnt: 5, isClicked: false },
{ emotion: Emotions.Comfortable, reactionCnt: 8, isClicked: false },
{ emotion: Emotions.Confident, reactionCnt: 10, isClicked: false },
{ emotion: Emotions.Depressed, reactionCnt: 5, isClicked: false },
{ emotion: Emotions.Disappointed, reactionCnt: 8, isClicked: false },
{ emotion: Emotions.Embarrassed, reactionCnt: 10, isClicked: false },
{ emotion: Emotions.Excited, reactionCnt: 5, isClicked: false },
{ emotion: Emotions.Fun, reactionCnt: 8, isClicked: false },
{ emotion: Emotions.Grateful, reactionCnt: 10, isClicked: false },
{ emotion: Emotions.Lonely, reactionCnt: 5, isClicked: false },
{ emotion: Emotions.Lovely, reactionCnt: 8, isClicked: false },
{ emotion: Emotions.Not_sure, reactionCnt: 10, isClicked: false },
{ emotion: Emotions.Sad, reactionCnt: 8, isClicked: false },
],
onReactionUpdate: (emotion: Emotions, count: number) => {
console.log(`Emotion: ${emotion}, Updated Count: ${count}`);
},
},

play: ({ args }) => {
const logReactionUpdate = (emotion: Emotions, count: number) => {
console.log(`Updated ${emotion}: ${count}`);
};

args.onReactionUpdate = logReactionUpdate;
},
};

export const Vertical: Story = {
args: {
isHorizontal: false,
isAddBtnVisible: true,
reactions: [
{ emotion: Emotions.Angry, reactionCnt: 3, isClicked: false },
{ emotion: Emotions.Surprised, reactionCnt: 12, isClicked: false },
],
onReactionUpdate: (emotion: Emotions, count: number) => {
console.log(`Emotion: ${emotion}, Updated Count: ${count}`);
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import theme from '@/app/styles/theme';
import styled from 'styled-components';

export const StyledReactionContainer = styled.div`
width: 40%;
`;

export const StyledReactionBtnContainer = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 100%;
& > button {
margin: 5px;
}
`;

export const StyledEmotionContainer = styled.div`
width: 50%;
position: absolute;
left: 50%;
transform: translateX(-50%);
z-index: 998;
background: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
padding: 10px;
border-radius: 8px;
`;

export const StyledCloseButton = styled.button`
background-color: transparent;
border: none;
cursor: pointer;
font-size: 16px;
position: absolute;
top: 10px;
right: 10px;
z-index: 999;
&:hover {
color: ${theme.colors.orange_primary};
}
`;
131 changes: 131 additions & 0 deletions src/entities/ReactionButtonContainer/ui/ReactionButtonContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { useEffect, useState } from 'react';
import {
StyledEmotionContainer,
StyledReactionContainer,
StyledCloseButton,
StyledReactionBtnContainer
} from './ReactionButtonContainer.styled';
import { Emotions } from '../../../shared/model/EmotionEnum';
import ReactionButton from '../../../shared/ReactionButton/ui/ReactionButton';
import ReactionAddButton from '../../../shared/ReactionAddButton/ui/ReactionAddButton';
import useModal from '@/shared/hooks/useModal';
import EmotionList from '@/shared/EmotionButtonList/ui/EmotionButtonList';
import { Reaction } from '../model/reaction';

interface ReactionListProps {
reactions: Reaction[];
isHorizontal: boolean;
isAddBtnVisible: boolean;
onReactionUpdate: (
emotion: Emotions,
count: number,
isAlreadyClicked: boolean
) => void;
onSelectedEmotionsChange: (selectedEmotions: Emotions[]) => void;
}

const ReactionButtonContainer: React.FC<ReactionListProps> = ({
reactions = [],
isHorizontal,
isAddBtnVisible = false,
onReactionUpdate,
onSelectedEmotionsChange
}) => {
const [clickedEmotions, setClickedEmotions] = useState<Emotions[]>([]);
const [updatedReactions, setUpdatedReactions] =
useState<Reaction[]>(reactions);
const [isModalOpen, setIsModalOpen] = useState(false);

const { openModal, ModalComponent } = useModal();

useEffect(() => {
const initialClickedEmotions = reactions
.filter((reaction) => reaction.isClicked)
.map((reaction) => reaction.emotion);
setClickedEmotions(initialClickedEmotions);
}, [reactions]);

const handleClick = (emotion: Emotions) => {
setClickedEmotions((prev) => {
const isAlreadyClicked = prev.includes(emotion);
const updatedCount = updatedReactions.map((reaction) => {
if (reaction.emotion === emotion) {
const newCount = isAlreadyClicked
? reaction.reactionCnt - 1
: reaction.reactionCnt + 1;

onReactionUpdate(emotion, newCount, isAlreadyClicked);

return {
...reaction,
reactionCnt: newCount
};
}
return reaction;
});

setUpdatedReactions(updatedCount);

return isAlreadyClicked
? prev.filter((e) => e !== emotion)
: [...prev, emotion];
});
};

const toggleModal = () => {
setIsModalOpen((prev) => !prev);
};

const handleOnClickAddButton = () => {
toggleModal();
};

const onClickTest = (selectedEmotions: Emotions[]) => {
onSelectedEmotionsChange(selectedEmotions);
};

const initialSelectedEmotions = reactions
.filter((reaction) => reaction.isClicked)
.map((reaction) => reaction.emotion);

return (
<StyledReactionContainer>
<StyledReactionBtnContainer>
{updatedReactions.map(({ emotion, reactionCnt }) => (
<ReactionButton
key={emotion}
emotion={emotion}
reactionCnt={reactionCnt}
isHorizontal={isHorizontal}
isClicked={clickedEmotions.includes(emotion)}
onClick={handleClick}
/>
))}

{isAddBtnVisible && (
<ReactionAddButton
isHorizontal={isHorizontal}
isClicked={false}
onClick={handleOnClickAddButton}
/>
)}
</StyledReactionBtnContainer>

{isModalOpen && (
<StyledEmotionContainer>
<StyledCloseButton onClick={toggleModal}>
x
</StyledCloseButton>
<EmotionList
isPrimary={false}
maxSelections={22}
initialSelectedEmotions={initialSelectedEmotions}
onSelectionChange={onClickTest}
/>
</StyledEmotionContainer>
)}
</StyledReactionContainer>
);
};

export default ReactionButtonContainer;
17 changes: 17 additions & 0 deletions src/shared/ReactionAddButton/ui/ReactionAddButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';

import ReactionAddButton from './ReactionAddButton';

const meta: Meta<typeof ReactionAddButton> = {
component: ReactionAddButton,
title: 'shared/ReactionAddButton',
tags: ['autodocs'],
argTypes: {},
};
export default meta;

type Story = StoryObj<typeof ReactionAddButton>;

export const Default: Story = {
args: {},
};
31 changes: 31 additions & 0 deletions src/shared/ReactionAddButton/ui/ReactionAddButton.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import theme from '@/app/styles/theme';
import styled from 'styled-components';

interface reactionAddBtnProps {
isHorizontal: boolean;
clicked: boolean;
}

export const styledReactionAddBtn = styled.button<reactionAddBtnProps>`
background-color: ${({ clicked }) =>
clicked ? theme.colors.orange_selected : theme.colors.white_bg};
border: 1px solid
${({ clicked }) =>
clicked ? theme.colors.orange_primary : theme.colors.gray_normal};
border-radius: ${({ isHorizontal }) => (isHorizontal ? '30px' : '20px')};
cursor: pointer;
transition:
background-color 0.2s,
border-color 0.2s;
&:hover {
background-color: ${({ clicked }) =>
clicked
? theme.colors.orange_selected
: theme.colors.orange_selected};
border-color: ${({ clicked }) =>
clicked
? theme.colors.orange_primary
: theme.colors.orange_primary};
}
`;
34 changes: 34 additions & 0 deletions src/shared/ReactionAddButton/ui/ReactionAddButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { StyledReactionButton } from '../../ReactionButton/ui/ReactionButton.styled';
import React from 'react';
import { StyledEmotionIcon } from '../../EmotionIcon/ui/EmotionIcon.styled';

interface ReactionAddButtonProps {
isClicked: boolean;
isHorizontal: boolean;
onClick: () => void;
}

/** 일기 글에 대한 반응을 추가하기 위한 버튼입니다. */
const ReactionAddButton = ({
isClicked,
isHorizontal,
onClick
}: ReactionAddButtonProps) => {
const handleClick = () => {
onClick();
};

return (
<StyledReactionButton
isHorizontal={isHorizontal}
clicked={isClicked}
onClick={handleClick}
>
<StyledEmotionIcon width="80%" height="80%">
<img src="./reaction_plus.svg" alt="이미지 추가" />
</StyledEmotionIcon>
</StyledReactionButton>
);
};

export default ReactionAddButton;
Loading

0 comments on commit a8fd6db

Please sign in to comment.