Skip to content

Commit

Permalink
[#563] [모임 생성] 모임 가입문제 퍼널 (#568)
Browse files Browse the repository at this point in the history
* feat: 모임 가입문제 유무 라디오 카드 구현

- tailwind config에 check content 추가

* feat: 모임 가입문제 스텝 구현

- 모임 가입문제 유무 fieldset
- 모임 가입문제,정답 fieldset

* feat: 모임 가입문제 스텝 스토리 작성

* refactor: JoinTypeFieldset 합성 컴포넌트로 리팩토링

* refactor: PropsWithChildren 타입 사용하도록 수정

* refactor: FormValue export명 수정
  • Loading branch information
gxxrxn committed Jun 17, 2024
1 parent 94365ee commit 4d6a14f
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 0 deletions.
46 changes: 46 additions & 0 deletions src/stories/bookGroup/create/steps/SelectJoinTypeStep.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Meta, StoryObj } from '@storybook/react';
import { FormProvider, useForm } from 'react-hook-form';
import { appLayoutMeta } from '@/stories/meta';

import {
SelectJoinTypeStep,
SelectJoinTypeStepFormValues,
} from '@/v1/bookGroup/create/steps/SelectJoinTypeStep';

const meta: Meta<typeof SelectJoinTypeStep> = {
title: 'bookGroup/create/steps/SelectJoinTypeStep',
component: SelectJoinTypeStep,
...appLayoutMeta,
};

export default meta;

type Story = StoryObj<typeof SelectJoinTypeStep>;

const RenderSelectJoinTypeStep = () => {
const methods = useForm<SelectJoinTypeStepFormValues>({
defaultValues: {
hasJoinPasswd: 'false',
},
mode: 'all',
});

const onSubmit = () => {
const { hasJoinPasswd, joinPasswd, joinQuestion } = methods.getValues();
alert(
`가입 문제 유무: ${hasJoinPasswd}\n가입 문제: ${joinQuestion}\n정답: ${joinPasswd}`
);
};

return (
<FormProvider {...methods}>
<form>
<SelectJoinTypeStep onSubmit={onSubmit} />
</form>
</FormProvider>
);
};

export const Default: Story = {
render: () => <RenderSelectJoinTypeStep />,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useFormContext } from 'react-hook-form';

import BottomActionButton from '@/v1/base/BottomActionButton';

import { JoinPasswordFieldset, JoinTypeFieldset } from './fields';

interface MoveFunnelStepProps {
onPrevStep?: () => void;
onNextStep?: () => void;
onSubmit?: () => void;
}

export type JoinTypeStepFormValues = {
hasJoinPasswd: 'true' | 'false';
joinQuestion?: string;
joinPasswd?: string;
};

export type JoinTypeStepFieldName = keyof JoinTypeStepFormValues;
export type JoinTypeStepFieldProp = { name: JoinTypeStepFieldName };

const SelectJoinTypeStep = ({ onSubmit }: MoveFunnelStepProps) => {
const { handleSubmit } = useFormContext<JoinTypeStepFormValues>();

return (
<article>
<h2 className="mb-[3rem] text-lg font-bold">가입은 어떻게 받을까요?</h2>

<section className="flex flex-col gap-[2rem]">
<JoinTypeFieldset>
<JoinTypeFieldset.RadioCardField name="hasJoinPasswd" />
</JoinTypeFieldset>

<JoinPasswordFieldset joinTypeFieldName="hasJoinPasswd">
<JoinPasswordFieldset.QuestionField name="joinQuestion" />
<JoinPasswordFieldset.AnswerField name="joinPasswd" />
</JoinPasswordFieldset>
</section>

<BottomActionButton
type="submit"
onClick={onSubmit && handleSubmit(onSubmit)}
>
독서모임 만들기
</BottomActionButton>
</article>
);
};

export default SelectJoinTypeStep;
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { PropsWithChildren } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';

import ErrorMessage from '@/v1/base/ErrorMessage';
import Input from '@/v1/base/Input';
import InputLength from '@/v1/base/InputLength';

import {
JoinTypeStepFieldName,
JoinTypeStepFormValues,
JoinTypeStepFieldProp,
} from '../SelectJoinTypeStep';

type JoinPasswordFieldsetProps = {
joinTypeFieldName: JoinTypeStepFieldName;
};

const JoinPasswordFieldset = ({
joinTypeFieldName,
children,
}: PropsWithChildren<JoinPasswordFieldsetProps>) => {
const { control } = useFormContext<JoinTypeStepFormValues>();
const hasJoinPassword = useWatch({ control, name: joinTypeFieldName });

const shouldRender = hasJoinPassword === 'true';

return (
<>
{shouldRender && (
<fieldset className="flex flex-col gap-[1.5rem]">{children}</fieldset>
)}
</>
);
};

const JoinQuestionField = ({ name }: JoinTypeStepFieldProp) => {
const {
register,
control,
formState: { errors },
} = useFormContext<JoinTypeStepFormValues>();

const joinQuestion = useWatch({ control, name });

const questionLength = joinQuestion?.length;
const error = errors[name];

return (
<label className="flex flex-col gap-[0.5rem]">
<p>가입 문제</p>
<Input
placeholder="모임에 가입하기 위한 적절한 문제를 작성해주세요"
{...register(name, {
required: '1 ~ 30글자의 가입 문제가 필요해요',
maxLength: {
value: 30,
message: '1 ~ 30글자의 가입 문제를 작성해주세요',
},
})}
error={!!error}
/>
<div className="flex flex-row-reverse justify-between">
<InputLength
currentLength={questionLength}
isError={!!error}
maxLength={30}
/>
<ErrorMessage>{error?.message}</ErrorMessage>
</div>
</label>
);
};

const JoinAnswerField = ({ name }: JoinTypeStepFieldProp) => {
const {
register,
control,
formState: { errors },
} = useFormContext<JoinTypeStepFormValues>();

const joinPasswd = useWatch({ control, name });

const passwordLength = joinPasswd?.length;
const error = errors[name];

return (
<label className="flex flex-col gap-[0.5rem]">
<p>정답</p>
<Input
placeholder="띄어쓰기 없이 정답을 작성해주세요"
{...register(name, {
required: '띄어쓰기 없이 10글자 이하의 정답이 필요해요',
maxLength: {
value: 10,
message: '띄어쓰기 없이 10글자 이하의 정답을 작성해주세요,',
},
pattern: {
value: /^[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9]+$/,
message: '띄어쓰기 없이 한글, 영어, 숫자만 입력할 수 있어요',
},
})}
error={!!error}
/>
<div className="flex flex-row-reverse justify-between">
<InputLength
currentLength={passwordLength}
isError={!!error}
maxLength={10}
/>
<ErrorMessage>{error?.message}</ErrorMessage>
</div>
</label>
);
};

JoinPasswordFieldset.QuestionField = JoinQuestionField;
JoinPasswordFieldset.AnswerField = JoinAnswerField;

export default JoinPasswordFieldset;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useFormContext } from 'react-hook-form';

import {
JoinTypeStepFormValues,
JoinTypeStepFieldProp,
} from '../SelectJoinTypeStep';

import JoinTypeRadioCard from './JoinTypeRadioCard';

const JoinTypeFieldset = ({ children }: { children?: React.ReactNode }) => {
return <fieldset className="flex flex-col gap-[1rem]">{children}</fieldset>;
};

const RadioCardField = ({ name }: JoinTypeStepFieldProp) => {
const { register } = useFormContext<JoinTypeStepFormValues>();

return (
<>
<JoinTypeRadioCard
{...register(name)}
id="no-password"
value="false"
label="문제 없이 가입할 수 있어요"
/>
<JoinTypeRadioCard
{...register(name)}
id="has-password"
value="true"
label="문제를 맞춰야 모임에 가입할 수 있어요"
/>
</>
);
};

JoinTypeFieldset.RadioCardField = RadioCardField;

export default JoinTypeFieldset;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { forwardRef, InputHTMLAttributes, Ref } from 'react';

const BASE_CLASSES =
'flex h-[8rem] w-full cursor-pointer items-center justify-between rounded-[0.5rem] border-[0.1rem] bg-white px-[2.5rem] text-black-600 text-md';

const JoinTypeRadioCard = (
{
id,
value,
label,
...props
}: Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'> & {
label?: string;
},
ref: Ref<HTMLInputElement>
) => {
const inputId = id || 'radio-card';

return (
<div>
<input
type="radio"
id={inputId}
value={value}
className="peer hidden"
ref={ref}
{...props}
/>
<label
className={`${BASE_CLASSES} after:h-[2.4rem] peer-checked:border-main-900 peer-checked:bg-main-900/[0.05] peer-checked:text-[#FF8B00] peer-checked:after:content-check`}
htmlFor={inputId}
>
<p>{label}</p>
</label>
</div>
);
};

export default forwardRef(JoinTypeRadioCard);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as JoinTypeFieldset } from './JoinTypeFieldset';
export { default as JoinPasswordFieldset } from './JoinPasswordFieldset';
2 changes: 2 additions & 0 deletions src/v1/bookGroup/create/steps/SelectJoinTypeStep/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { JoinTypeStepFormValues as SelectJoinTypeStepFormValues } from './SelectJoinTypeStep';
export { default as SelectJoinTypeStep } from './SelectJoinTypeStep';
1 change: 1 addition & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ module.exports = {
},
content: {
search: 'url("/icons/search.svg")',
check: 'url("/icons/check.svg")',
},
},
},
Expand Down

0 comments on commit 4d6a14f

Please sign in to comment.