Skip to content

Commit

Permalink
Merge pull request #59 from softeerbootcamp4th/TASK-135
Browse files Browse the repository at this point in the history
[Feature][Task-135] 로그인 모달 구현 및 API 연동 & 로그인이 선행되어야 하는 기능을 위해 공통 Protected 로직 구현 (HOC) & 그 외 다수의 관련된 에러 해결
  • Loading branch information
nim-od authored Aug 15, 2024
2 parents 0a35369 + e3c81e3 commit ec3c281
Show file tree
Hide file tree
Showing 22 changed files with 332 additions and 165 deletions.
2 changes: 1 addition & 1 deletion packages/user/src/components/event/chatting/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function Chat({ type, team, sender, content }: ChatProps) {
case 'm':
default:
return (
<Message sender={sender} team={team} isMyMessage={me?.id === sender}>
<Message sender={sender} team={team} isMyMessage={me?.id === sender.toString()}>
{content}
</Message>
);
Expand Down
34 changes: 19 additions & 15 deletions packages/user/src/components/event/chatting/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { ChatList } from '@softeer/common/components';
import { memo } from 'react';
import ChatInput from 'src/components/event/chatting/inputArea/input/index.tsx';
import { UseChatSocketReturnType } from 'src/hooks/socket/useChatSocket.ts';
import Chat from './Chat.tsx';
import ChatInputArea from './inputArea/index.tsx';

/** 실시간 기대평 섹션 */

export default function RealTimeChatting({ onSendMessage, messages }: UseChatSocketReturnType) {
return (
<section className="container flex max-w-[1200px] snap-start flex-col items-center pb-[115px] pt-[50px]">
<h6 className="text-heading-10 mb-[25px] font-medium">기대평을 남겨보세요!</h6>
<ChatInputArea onSend={onSendMessage} />
<div className="h-[1000px] w-full overflow-y-auto rounded-[10px] bg-neutral-800 py-10">
<ChatList>
{messages.map((message) => (
<Chat key={message.id} {...message} />
))}
</ChatList>
</div>
</section>
);
}
const RealTimeChatting = memo(({ onSendMessage, messages }: UseChatSocketReturnType) => (
<section className="container flex max-w-[1200px] snap-start flex-col items-center pb-[115px] pt-[50px]">
<h6 className="text-heading-10 mb-[25px] font-medium">기대평을 남겨보세요!</h6>
<ChatInputArea>
<ChatInput onSend={onSendMessage} />
</ChatInputArea>
<div className="h-[1000px] w-full overflow-y-auto rounded-[10px] bg-neutral-800 py-10">
<ChatList>
{messages.map((message) => (
<Chat key={message.id} {...message} />
))}
</ChatList>
</div>
</section>
));

export default RealTimeChatting;
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import ChatInput from './input/index.tsx';
import { PropsWithChildren } from 'react';

interface ChatInputAreaProps {
onSend: (message: string) => void;
}
export default function ChatInputArea({ onSend }: ChatInputAreaProps) {
export default function ChatInputArea({ children }: PropsWithChildren) {
return (
<div className="mb-[54px] flex flex-col items-center gap-3">
{/* Todo: 비속어 작성 횟수 불러오기 */}
<p className="text-detail-2 text-[#FF3C76]">
비속어 혹은 부적절한 기대평을 5회 이상 작성할 경우, 댓글 작성이 제한됩니다.
</p>
<ChatInput onSend={onSend} />
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useRef } from 'react';
import { memo, useCallback, useRef } from 'react';
import OutlinedButton from 'src/components/common/OutlinedButton.tsx';
import LoginModal from 'src/components/shared/modal/login/index.tsx';
import withAuth from 'src/components/shared/withAuthHOC.tsx';
import useAuth from 'src/hooks/useAuth.tsx';
import { useToast } from 'src/hooks/useToast.ts';
import Input from './Input.tsx';

const ProtectedWrapper = memo(withAuth(() => <OutlinedButton>보내기</OutlinedButton>));

const DISABLED_CHATTING_TOAST_DESCRIPTION = '로그인 후 채팅에 참여할 수 있습니다!';
interface ChatInputProps {
onSend: (message: string) => void;
Expand All @@ -15,7 +17,7 @@ export default function ChatInput({ onSend }: ChatInputProps) {

const inputRef = useRef<HTMLInputElement>(null);

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
const handleSubmit = useCallback((event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

const disabledChatting = !isAuthenticated;
Expand All @@ -29,16 +31,14 @@ export default function ChatInput({ onSend }: ChatInputProps) {
onSend(inputRef.current.value);
inputRef.current.value = '';
}
}
}, [isAuthenticated]);

return (
<form className="flex items-center gap-4" onSubmit={handleSubmit}>
<Input ref={inputRef} name="input" required />
{isAuthenticated ? (
<OutlinedButton type="submit">보내기</OutlinedButton>
) : (
<LoginModal openTrigger={<OutlinedButton>로그인하고 채팅 보내기</OutlinedButton>} />
)}
<ProtectedWrapper
unauthenticatedDisplay={<OutlinedButton>로그인하고 채팅 보내기</OutlinedButton>}
/>
</form>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,23 @@ export interface ChargeButtonData {
percentage: number;
}

const ControlButton = memo(
({ onCharge, onFullyCharged, type, data }: ControlButtonProps) => {
const { rank, percentage } = data;
const { progress, clickCount, handleClick } = useGaugeProgress({
percentage,
onCharge,
onFullyCharged,
});
const ControlButton = memo(({ onCharge, onFullyCharged, type, data }: ControlButtonProps) => {
const { rank, percentage } = data;
const { progress, clickCount, handleClick } = useGaugeProgress({
percentage,
onCharge,
onFullyCharged,
});

return (
<ControllButtonWrapper rank={rank}>
<Gauge percent={progress} />
<ChargeButtonWrapper onClick={handleClick} disabled={clickCount === MAX_CLICK} type={type}>
<ChargeButtonContent type={type} {...data} />
</ChargeButtonWrapper>
</ControllButtonWrapper>
);
},
);
return (
<ControllButtonWrapper rank={rank}>
<Gauge percent={progress} />
<ChargeButtonWrapper onClick={handleClick} disabled={clickCount === MAX_CLICK} type={type}>
<ChargeButtonContent type={type} {...data} />
</ChargeButtonWrapper>
</ControllButtonWrapper>
);
});

export default ControlButton;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Category } from '@softeer/common/types';
import { useState } from 'react';
import { memo, useState } from 'react';
import TriggerButtonWrapper from 'src/components/common/TriggerButtonWrapper.tsx';
import TeamSelectModal from 'src/components/shared/modal/teamSelectModal/index.tsx';
import TeamSelectModal, { type TeamSelectModalProps } from 'src/components/shared/modal/teamSelectModal/index.tsx';
import ShareCountTeamCard from 'src/components/shared/ShareCountTeamCard.tsx';
import withAuth from 'src/components/shared/withAuthHOC.tsx';
import useAuth from 'src/hooks/useAuth.tsx';
import UnassignedCard from './UnassignedCard.tsx';

const ProtectedTeamSelectModal = memo(withAuth<TeamSelectModalProps>(TeamSelectModal));

export default function RacingCard() {
const { user } = useAuth();
const [type, setType] = useState<Category | undefined>(user?.type);
Expand All @@ -16,13 +19,14 @@ export default function RacingCard() {
{type ? (
<ShareCountTeamCard type={type} size="racing" />
) : (
<TeamSelectModal
<ProtectedTeamSelectModal
openTrigger={
<TriggerButtonWrapper>
<UnassignedCard />
</TriggerButtonWrapper>
}
onClose={() => setType(user?.type)}
unauthenticatedDisplay={<UnassignedCard />}
/>
)}
</div>
Expand Down
44 changes: 18 additions & 26 deletions packages/user/src/components/layout/fcfsController/index.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,40 @@
import { lazy, Suspense, useEffect, useState } from 'react';
import TriggerButtonWrapper from 'src/components/common/TriggerButtonWrapper.tsx';
import LoginModal from 'src/components/shared/modal/login/index.tsx';
import useAuth from 'src/hooks/useAuth.tsx';
import withAuth from 'src/components/shared/withAuthHOC.tsx';
import TriggerButtonLike from './TriggerButtonLike.tsx';

const FCFSModal = lazy(() => import('src/components/shared/modal/fcfs/index.tsx'));

const EVENT_OPEN_HOUR = 15;
const EVENT_OPEN_MINUTE = 15;

const ProtectedFCFSButton = withAuth(() => (
<FCFSModal
openTrigger={
<TriggerButtonWrapper>
<TriggerButtonLike>
선착순 퀴즈 <p className="text-heading-8 text-foreground font-extrabold">OPEN</p>
</TriggerButtonLike>
</TriggerButtonWrapper>
}
/>
));

export default function FCFSFloatingButtonController() {
const { isAuthenticated } = useAuth();
const shouldLoadComponent = useEventActivation(EVENT_OPEN_HOUR, EVENT_OPEN_MINUTE);

// 설정한 시각 이전에는 버튼 노출하지 않음
if (!shouldLoadComponent) {
return null;
}

// 로그인하지 않은 유저에게는 로그인 모달 트리거 버튼 노출
if (!isAuthenticated) {
return (
<LoginModal
openTrigger={
<TriggerButtonWrapper>
<TriggerButtonLike>
<p className="text-heading-8 text-foreground font-bold">로그인</p>하고 퀴즈 풀기
</TriggerButtonLike>
</TriggerButtonWrapper>
}
/>
);
}

// 로그인한 유저에게는 선착순 퀴즈 모달 트리거 버튼 노출
return (
<Suspense>
<FCFSModal
openTrigger={
<TriggerButtonWrapper>
<TriggerButtonLike>
선착순 퀴즈 <p className="text-heading-8 text-foreground font-extrabold">OPEN</p>
</TriggerButtonLike>
</TriggerButtonWrapper>
<ProtectedFCFSButton
unauthenticatedDisplay={
<TriggerButtonLike>
<p className="text-heading-8 text-foreground font-bold">로그인</p>하고 퀴즈 풀기
</TriggerButtonLike>
}
/>
</Suspense>
Expand Down
26 changes: 11 additions & 15 deletions packages/user/src/components/layout/header/user/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import UserIcon from 'src/assets/icons/user.svg?react';
import useAuth from 'src/hooks/useAuth.tsx';
import withAuth from 'src/components/shared/withAuthHOC.tsx';
import SpeechBubble from './SpeechBubble.tsx';

export default function User() {
const { user } = useAuth();
const UserGreeting = withAuth(() => <p className="text-detail-1">김보민님 반갑습니다</p>);

// TODO: 로그인, 로그아웃
// TODO: 로그아웃
export default function User() {
return (
<div className="flex items-center gap-2">
{user ? (
<p className="text-detail-1">김보민님 반갑습니다</p>
) : (
<>
<UserGreeting
unauthenticatedDisplay={
<div className="flex items-center gap-2">
<SpeechBubble />
<button type="button" aria-label="user-icon" className="p-[10px]">
<UserIcon />
</button>
</>
)}
</div>
<UserIcon />
</div>
}
/>
);
}
4 changes: 2 additions & 2 deletions packages/user/src/components/shared/modal/InfoStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { PropsWithChildren } from 'react';

export default function InfoStep({ children }: PropsWithChildren) {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-10 p-[20px]">
<img src="/images/fcfs/modal.png" alt="modal" className="h-full object-contain" />
<div className="flex h-full flex-col items-center justify-center gap-10 p-[20px]">
<img src="/images/fcfs/modal.png" alt="modal" className="max-w-[500px] object-contain" />
<div className="text-heading-12 flex flex-col items-center gap-4 font-medium">{children}</div>
</div>
);
Expand Down
34 changes: 31 additions & 3 deletions packages/user/src/components/shared/modal/fcfs/QuizStep.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import { useEffect } from 'react';
import Chip from 'src/components/common/Chip.tsx';
import OptionButton from 'src/components/common/OptionButton.tsx';
import useGetFCFSQuiz from 'src/hooks/query/useGetFCFSQuiz.ts';
import useSubmitFCFSQuiz, { SubmitFCFSQuizResponse } from 'src/hooks/query/useSubmitFCFSQuiz.ts';

export type ResultStepType = ReturnType<typeof getResultStepFromStatus>;
interface QuizStepProps {
onSelect: (answer: number) => void;
onStepChange: (step: ResultStepType | 'pending') => void;
}

export default function QuizStep({ onSelect }: QuizStepProps) {
export default function QuizStep({ onStepChange }: QuizStepProps) {
const {
quiz: { question, choices },
} = useGetFCFSQuiz();

const { isPending, mutate: submitAnswer } = useSubmitFCFSQuiz();

useEffect(() => {
if (isPending) onStepChange('pending');
}, [isPending]);

const handleSubmit = (answer: number) =>
submitAnswer(
{ answer },
{ onSuccess: (response) => onStepChange(getResultStepFromStatus(response)) },
);

return (
<div className="flex h-full w-full max-w-[400px] flex-col justify-between gap-9 sm:max-w-[500px] md:max-w-[650px] lg:max-w-[800px]">
<div className="flex flex-col items-center gap-9">
Expand All @@ -19,11 +34,24 @@ export default function QuizStep({ onSelect }: QuizStepProps) {
</div>
<div className="grid w-full grid-cols-1 gap-5 md:grid-cols-2">
{choices.map(({ num, text }) => (
<OptionButton key={num} onClick={() => onSelect(num)}>
<OptionButton key={num} onClick={() => handleSubmit(num)}>
{text}
</OptionButton>
))}
</div>
</div>
);
}

function getResultStepFromStatus({ status }: SubmitFCFSQuizResponse) {
switch (status) {
case 'END':
return 'end';
case 'PARTICIPATED':
return 'already-done';
case 'WRONG':
return 'wrong-answer';
default:
return 'correct-answer';
}
}
5 changes: 3 additions & 2 deletions packages/user/src/components/shared/modal/fcfs/ResultStep.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import type { ResultStepType } from 'src/components/shared/modal/fcfs/index.tsx';
import type { ResultStepType } from './QuizStep.tsx';

interface ResultStepProps {
step: ResultStepType;
Expand All @@ -8,9 +8,10 @@ interface ResultStepProps {
export default function ResultStep({ step }: ResultStepProps) {
const imageUrl = IMAGE_URLS[step];
const { title, subTitle, details } = DESCRIPTIONS[step];

return (
<div className="flex h-full w-full flex-col items-center justify-center p-[20px]">
<img src={imageUrl} alt="modal" className="h-[230px] object-contain" />
<img src={imageUrl} alt={`${step} 캐스퍼 캐릭터`} className="h-[230px] object-contain" />
<p className="text-heading-7 mb-9 font-bold">{title}</p>
<p className="text-body-1 mb-4 whitespace-pre-line text-center font-medium">{subTitle}</p>
<caption className="text-body-4 text-neutral-100">{details}</caption>
Expand Down
Loading

0 comments on commit ec3c281

Please sign in to comment.