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] 채팅방 컴포넌트 기본 퍼블리싱 #98

Merged
merged 8 commits into from
Nov 14, 2024
4 changes: 4 additions & 0 deletions frontend/src/assets/icons/check.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/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions frontend/src/assets/icons/out.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions frontend/src/assets/icons/question.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/icons/send.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions frontend/src/assets/icons/speech-bubble.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions frontend/src/assets/icons/three-point.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions frontend/src/components/chat/ChatHeader.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styled-components에서 as라는 것을 처음봤는데 좋네요!

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import styled from 'styled-components';
import ThreePointIcon from '@assets/icons/three-point.svg';
import OutIcon from '@assets/icons/out.svg';

interface ChatHeaderProps {
outBtnHandler?: () => void;
}

export const ChatHeader = ({ outBtnHandler }: ChatHeaderProps) => {
return (
<ChatHeaderContainer>
<HeaderBtn onClick={outBtnHandler}>
<StyledIcon as={OutIcon} />
</HeaderBtn>
<h2>채팅</h2>
<HeaderBtn>
<StyledIcon as={ThreePointIcon} />
</HeaderBtn>
</ChatHeaderContainer>
);
};
export default ChatHeader;

const ChatHeaderContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 15px;
border-top: 1px solid ${({ theme }) => theme.tokenColors['surface-alt']};
color: ${({ theme }) => theme.tokenColors['color-white']};
${({ theme }) => theme.tokenTypographys['display-bold20']};
`;

const HeaderBtn = styled.button`
display: flex;
color: ${({ theme }) => theme.tokenColors['text-bold']};
cursor: pointer;
`;

const StyledIcon = styled.svg`
width: 25px;
height: 25px;
cursor: pointer;
`;
108 changes: 108 additions & 0 deletions frontend/src/components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import styled, { css } from 'styled-components';
import SpeechBubbleIcon from '@assets/icons/speech-bubble.svg';
import QuestionIcon from '@assets/icons/question.svg';
import SendIcon from '@assets/icons/send.svg';
import { useRef, useEffect, useState } from 'react';

interface ChatInputProps {
type: 'normal' | 'question';
}

export const ChatInput = ({ type }: ChatInputProps) => {
const [hasInput, setHasInput] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement | null>(null);

useEffect(() => {
const handleResize = () => {
if (textareaRef.current) {
textareaRef.current.style.height = '14px';
textareaRef.current.style.height = `${textareaRef.current.scrollHeight - 5}px`;
}
};

Comment on lines +17 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRef로 채팅창 리사이징 하는 코드 좋네요 :)
여유 있으실 때 매직넘버는 상수화 따로 분리하면 좋을 것 같아요!

'textareaRef.current' 자주 쓰이는것 같아서 변수로 따로 빼서 사용하는 것도 적절해 보입니다.

추후에 useAutoSize나 useChatInput 같이 리사이징이과 인풋 관련 로직의 관심사를 분리하는 방향으로 가면 좋을 것 같습니다!

if (textareaRef.current) {
textareaRef.current.addEventListener('input', handleResize);
handleResize();
}

return () => {
if (textareaRef.current) {
textareaRef.current.removeEventListener('input', handleResize);

Check warning on line 31 in frontend/src/components/chat/ChatInput.tsx

View workflow job for this annotation

GitHub Actions / build (frontend)

The ref value 'textareaRef.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'textareaRef.current' to a variable inside the effect, and use that variable in the cleanup function
}
};
}, []);

const handleBlur = () => {
if (textareaRef.current) {
setHasInput(textareaRef.current.value.length > 0);
setIsFocused(false);
}
};

const handleFocus = () => {
setIsFocused(true);
};

return (
<ChatInputWrapper $hasInput={hasInput} $isFocused={isFocused}>
<InputBtn aria-label={type}>
{type === 'normal' ? <StyledIcon as={SpeechBubbleIcon} /> : <StyledIcon as={QuestionIcon} />}
</InputBtn>
<ChatInputArea
ref={textareaRef}
placeholder={`${type === 'normal' ? '채팅' : '질문'}을 입력해주세요`}
onBlur={handleBlur}
onFocus={handleFocus}
/>
<InputBtn aria-label="전송">
<StyledIcon as={SendIcon} />
</InputBtn>
</ChatInputWrapper>
);
};
export default ChatInput;

const ChatInputWrapper = styled.div<{ $hasInput: boolean; $isFocused: boolean }>`
min-height: 20px;
display: flex;
padding: 5px 10px;
gap: 10px;
border: 3px solid ${({ theme }) => theme.tokenColors['text-weak']};
border-radius: 7px;
background-color: transparent;

${({ $isFocused, $hasInput }) =>
!$hasInput &&
!$isFocused &&
css`
border: 3px solid transparent;
background-color: #373a3f;
`}
`;

const ChatInputArea = styled.textarea`
width: 100%;
max-height: 40px;
overflow-y: auto;
resize: none;
border: none;
outline: none;
color: ${({ theme }) => theme.tokenColors['text-strong']};
${({ theme }) => theme.tokenTypographys['display-medium16']}
background-color: transparent;
`;

const InputBtn = styled.button`
display: flex;
height: 25px;
align-items: center;
color: ${({ theme }) => theme.tokenColors['text-strong']};
cursor: pointer;
`;

const StyledIcon = styled.svg`
width: 20px;
height: 20px;
cursor: pointer;
`;
75 changes: 75 additions & 0 deletions frontend/src/components/chat/ChatList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import styled from 'styled-components';
import QuestionCard from './QuestionCard';
import { useEffect, useRef } from 'react';

const sampleData = [
{ user: '고양이', message: 'ㅇㅅㅇ', type: 'normal' },
{ user: '강아지', message: 'ㅎㅇㅎㅇ', type: 'normal' },
{
user: '오리',
message:
'가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하가나다라마바사아자차카타파하',
type: 'normal'
}
];

function getRandomBrightColor(): string {
const hue = Math.floor(Math.random() * 360);
const saturation = Math.floor(Math.random() * 50) + 50;
const lightness = Math.floor(Math.random() * 30) + 50;

return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}

export const ChatList = () => {
const chatListRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
if (chatListRef.current) {
chatListRef.current.scrollTop = chatListRef.current.scrollHeight;
}
}, []);

return (
<ChatListWrapper ref={chatListRef}>
{[...Array(6)].map((_, i) =>
sampleData.map((chat, index) => (
<ChatItemWrapper key={`${i}-${index}`}>
{chat.type === 'normal' ? (
<NormalChat $pointColor={getRandomBrightColor()}>
<span className="text_point">{chat.user}</span>
<span>{chat.message}</span>
</NormalChat>
) : (
<QuestionCard type="client" user={chat.user} message={chat.message} />
)}
</ChatItemWrapper>
))
)}
</ChatListWrapper>
);
};
export default ChatList;

const ChatListWrapper = styled.div`
max-height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 50px 20px 0 20px;
scrollbar-width: none;
`;

const ChatItemWrapper = styled.div`
margin-top: auto;
padding: 5px 0;
`;

const NormalChat = styled.div<{ $pointColor: string }>`
${({ theme }) => theme.tokenTypographys['display-medium14']};
color: ${({ theme }) => theme.tokenColors['color-white']};
.text_point {
color: ${({ $pointColor }) => $pointColor};
margin-right: 5px;
}
`;
55 changes: 55 additions & 0 deletions frontend/src/components/chat/ChatQuestionSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useState } from 'react';
import styled from 'styled-components';
import QuestionCard from './QuestionCard';

export const ChatQuestionSection = () => {
const [expanded, setExpanded] = useState(false);

const toggleSection = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수의 동작이 하나여서 아래 예시 처럼 작성하면 가독성이 더 좋아질 것 같아요!

<ToggleBtn onClick={() => setExpanded((prev) => !prev)} />

추후 함수의 역할이 더 많아진다면 지금처럼 함수로 분리해도 좋은 것 같습니다

setExpanded((prev) => !prev);
};

return (
<SectionContainer>
<QuestionCard
type="host"
user="부스트"
message="설명해주셨던 내용 중에 yarn-berry를 설정했던 방법이 인상깊었는데 거기서 생겼던 오류가 있나요?"
/>
{expanded && <QuestionCard type="host" user="라이부" message="다른 질문들" />}
<SwipeBtn onClick={toggleSection} />
</SectionContainer>
);
};
export default ChatQuestionSection;

const SectionContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
min-height: 95px;
padding: 13px 20px 0px 20px;
gap: 10px;
border-top: 1px solid ${({ theme }) => theme.tokenColors['surface-alt']};
border-bottom: 1px solid ${({ theme }) => theme.tokenColors['surface-alt']};
overflow: hidden;
`;

const SwipeBtn = styled.button`
position: relative;
width: 100%;
height: 25px;
cursor: pointer;

&::before {
content: '';
position: absolute;
top: 35%;
left: 50%;
background-color: ${({ theme }) => theme.tokenColors['text-weak']};
border-radius: 2px;
height: 5px;
width: 50px;
transform: translate(-50%, -50%);
}
`;
45 changes: 45 additions & 0 deletions frontend/src/components/chat/ChatRoom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import styled from 'styled-components';
import ChatHeader from './ChatHeader';
import ChatInput from './ChatInput';
import ChatList from './ChatList';
import ChatQuestionSection from './ChatQuestionSection';

export const ChatRoom = () => {
return (
<ChatRoomContainer>
<ChatHeader />

<ChatQuestionSection />

<ChatListContainer>
<ChatList />
</ChatListContainer>

<ChatInputContainer>
<ChatInput type="normal" />
</ChatInputContainer>
</ChatRoomContainer>
);
};
export default ChatRoom;

const ChatRoomContainer = styled.aside`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aside로 잡아주셔서 좋네요 :)

display: flex;
flex-direction: column;
height: 100%;
width: 380px;
border-left: 1px solid ${({ theme }) => theme.tokenColors['surface-alt']};
background: ${({ theme }) => theme.tokenColors['surface-default']};
`;

const ChatListContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: end;
overflow-y: auto;
`;

const ChatInputContainer = styled.div`
padding: 10px 20px;
`;
Loading
Loading