Skip to content

Commit

Permalink
✨ add create challenge feature
Browse files Browse the repository at this point in the history
  • Loading branch information
wook-hyung committed Dec 16, 2023
1 parent 4941b50 commit 44009bc
Show file tree
Hide file tree
Showing 17 changed files with 311 additions and 34 deletions.
3 changes: 0 additions & 3 deletions app/(route)/(index)/not-found.tsx

This file was deleted.

175 changes: 158 additions & 17 deletions app/(route)/challenge/_components/ChallengeFormDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,79 @@
import { useState } from 'react'

import * as DialogPrimitive from '@radix-ui/react-dialog'
import { type SubmitHandler, useForm, Controller } from 'react-hook-form'

import AddIcon from '@/app/_components/icons/AddIcon'
import AlertIcon from '@/app/_components/icons/AlertIcon'
import BlackCloseIcon from '@/app/_components/icons/BlackCloseIcon'
import CheckBoxIcon from '@/app/_components/icons/CheckBoxIcon'
import { useUserContext } from '@/app/_components/providers/UserProvider'
import { Dialog, DialogClose, DialogContent, DialogTrigger } from '@/app/_components/shared/dialog'
import { cn } from '@/app/_styles/utils'

import { share } from '../_utils'
import { useCreateChallengeMutation } from '../queries'

import ChallengeForm from './challenge-form-input'

type Category = '자기계발' | '생활습관' | '공부' | '운동' | '기타' | '선택안함'
type Category = '자기계발' | '생활습관' | '공부' | '운동' | '기타'

const CATEGORIES: Category[] = ['자기계발', '생활습관', '공부', '운동', '기타']

const categoryObject = {
자기계발: 'SELF_DEVELOPMENT',
생활습관: 'LIFE_STYLES',
공부: 'STUDY',
운동: 'WORKOUT',
기타: 'ETC',
} as const

interface Inputs {
challengeName: string
authenticationMethod: string
reward: string
}

export default function ChallengeFormDialog() {
const [selectedCategory, setSelectedCategory] = useState<Category>('선택안함')
const [selectedCategory, setSelectedCategory] = useState<Category | '선택안함'>('선택안함')
const [isChecked, setIsChecked] = useState({
check1: false,
check2: false,
})

const createChallengeMutation = useCreateChallengeMutation()
const { user } = useUserContext()

const { handleSubmit, watch, control } = useForm<Inputs>({
mode: 'onChange',
defaultValues: {
challengeName: '',
authenticationMethod: '',
reward: '',
},
})

const onSubmit: SubmitHandler<Inputs> = ({ challengeName, authenticationMethod, reward }) => {
createChallengeMutation.mutate(
{
name: challengeName,
authenticationMethod,
reward,
category: categoryObject[selectedCategory as Category],
},
{
onSuccess: ({ data: { challengeId } }) => {
console.log('challengeId', challengeId)
share.kakao({
title: `${user.nickname}님이 초대장을 보냈어요!`,
description: '1:1 목표 매칭 서비스',
imageUrl: 'https://dodals3.s3.ap-northeast-2.amazonaws.com/asset/dodaldodal_square.png',
link: `https://dodaldodal-frontend-vercel.app/challenge/${challengeId}`,
})
},
}
)
}

return (
<Dialog>
Expand All @@ -29,7 +85,10 @@ export default function ChallengeFormDialog() {
</button>
</DialogTrigger>
<DialogContent>
<div className='relative flex max-h-[85vh] w-[324px] flex-col gap-7 overflow-y-auto rounded-xl bg-[#D9D9D9] px-6 pb-6 pt-10'>
<form
className='relative flex max-h-[85vh] w-[324px] flex-col gap-7 overflow-y-auto rounded-xl bg-[#D9D9D9] px-6 pb-6 pt-10'
onSubmit={handleSubmit(onSubmit)}
>
<DialogClose className='absolute right-2 top-1'>
<BlackCloseIcon />
</DialogClose>
Expand All @@ -42,14 +101,30 @@ export default function ChallengeFormDialog() {
<div className='flex flex-col gap-2'>
<ChallengeForm>
<ChallengeForm.Title title='챌린지 이름' />
<ChallengeForm.Input currentLength={0} maxLength={12} placeholder='챌린지 이름을 작성해볼까요?' />
<Controller
control={control}
name='challengeName'
render={({ field: { onChange, value } }) => (
<ChallengeForm.Input
currentLength={watch('challengeName')?.length ?? 0}
limitLength={12}
placeholder='챌린지 이름을 작성해볼까요?'
value={value}
onChange={(e) => {
onChange(e.target.value.slice(0, 12))
}}
maxLength={12}
/>
)}
/>
</ChallengeForm>

<div className='flex gap-1'>
{CATEGORIES.map((category) => {
return (
<button
key={category}
type='button'
className={cn('h-8 rounded bg-[#A6A6A6] px-2 text-center text-xs font-semibold', {
'bg-[#84D5D7]': category === selectedCategory,
})}
Expand All @@ -70,18 +145,66 @@ export default function ChallengeFormDialog() {
<ChallengeForm>
<ChallengeForm.Title title='인증 방식' />
<div className='flex w-full items-center gap-4'>
<ChallengeForm.Input currentLength={0} maxLength={8} placeholder='어떤 사물을 찍어서 인증할까요?' />
<Controller
control={control}
name='authenticationMethod'
render={({ field: { onChange, value } }) => (
<ChallengeForm.Input
currentLength={watch('authenticationMethod')?.length ?? 0}
limitLength={8}
placeholder='어떤 사물을 찍어서 인증할까요?'
value={value}
onChange={(e) => {
onChange(e.target.value.slice(0, 8))
}}
maxLength={8}
/>
)}
/>

<span className='whitespace-nowrap text-sm font-semibold text-[#140A29]'>찍기</span>
</div>
</ChallengeForm>
<ChallengeForm>
<ChallengeForm.Title title='보상' />
<ChallengeForm.Input currentLength={0} maxLength={15} placeholder='어떤 보상으로 설정할까요?' />
<Controller
control={control}
name='reward'
render={({ field: { onChange, value } }) => (
<ChallengeForm.Input
currentLength={watch('reward')?.length ?? 0}
limitLength={15}
placeholder='어떤 보상으로 설정할까요?'
value={value}
onChange={(e) => {
onChange(e.target.value.slice(0, 15))
}}
maxLength={15}
/>
)}
/>
</ChallengeForm>
</div>
<div className='flex flex-col gap-2'>
<div className='flex items-center gap-1'>
<div className='h-4 w-4 rounded-full bg-[#A6A6A6] p-1' />
{isChecked.check1 ? (
<button
type='button'
onClick={() => {
setIsChecked((prev) => ({ ...prev, check1: !prev.check1 }))
}}
>
<CheckBoxIcon />
</button>
) : (
<button
type='button'
className='h-4 w-[17px] rounded-full bg-[#A6A6A6] p-1'
onClick={() => {
setIsChecked((prev) => ({ ...prev, check1: !prev.check1 }))
}}
/>
)}
<div className='flex flex-col'>
<span className='text-[10px] text-[#140A29]'>
인증 사진을 챌린지 상대가 승인 및 반려할 수 있습니다.
Expand All @@ -90,7 +213,24 @@ export default function ChallengeFormDialog() {
</div>
</div>
<div className='flex items-center gap-1'>
<div className='h-4 w-4 rounded-full bg-[#A6A6A6] p-1' />
{isChecked.check2 ? (
<button
type='button'
onClick={() => {
setIsChecked((prev) => ({ ...prev, check2: !prev.check2 }))
}}
>
<CheckBoxIcon />
</button>
) : (
<button
className='h-4 w-[17px] rounded-full bg-[#A6A6A6] p-1'
type='button'
onClick={() => {
setIsChecked((prev) => ({ ...prev, check2: !prev.check2 }))
}}
/>
)}
<span className='text-[10px] text-[#140A29]'>인증 사진이 승인된 건에 한해서는 취소가 불가능합니다.</span>
</div>
</div>
Expand All @@ -99,18 +239,19 @@ export default function ChallengeFormDialog() {
'mx-auto min-h-[60px] w-[240px] rounded-lg bg-[#482BD9] text-center',
'disabled:bg-[#A6A6A6]'
)}
onClick={() => {
share.kakao({
title: '불주먹123님이 초대장을 보냈어요!',
description: '1:1 목표 매칭 서비스',
imageUrl: 'https://dodals3.s3.ap-northeast-2.amazonaws.com/asset/dodaldodal_square.png',
link: 'https://dodaldodal-frontend-vercel.app',
})
}}
type='submit'
disabled={
watch('challengeName')?.length < 1 ||
watch('authenticationMethod')?.length < 1 ||
watch('reward')?.length < 1 ||
selectedCategory === '선택안함' ||
!isChecked.check1 ||
!isChecked.check2
}
>
함께할 친구 초대하기
</button>
</div>
</form>
</DialogContent>
</Dialog>
)
Expand Down
6 changes: 3 additions & 3 deletions app/(route)/challenge/_components/challenge-form-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ function Title({ title }: ChallengeFormTitleProps) {

interface ChallengeFormInputProps extends React.HTMLProps<HTMLInputElement> {
currentLength: number
maxLength: number
limitLength: number
}

function Input({ currentLength, maxLength, ...props }: ChallengeFormInputProps) {
function Input({ currentLength, limitLength, ...props }: ChallengeFormInputProps) {
return (
<div className='flex w-full justify-between gap-2 rounded-lg border border-[#140A29] bg-white p-3'>
<input
className='flex-1 appearance-none border-none p-0 text-xs text-[#595959] placeholder:text-xs placeholder:text-[#A6A6A6] focus:ring-0'
{...props}
/>
<span className='text-[10px] text-[#595959]'>
{currentLength}/{maxLength}
{currentLength}/{limitLength}
</span>
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion app/(route)/challenge/_utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export const share = {
}) => {
const { Kakao } = window
if (!Kakao.isInitialized()) {
console.log('init')
Kakao.init('f0146584008c5fad855abb3cfad073d7')
}

Kakao.Share.sendDefault({
objectType: 'feed',
content: {
Expand Down
6 changes: 3 additions & 3 deletions app/(route)/challenge/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useRouter } from 'next/navigation'
import Script from 'next/script'

import BottomNavigation from '@/app/_components/shared/bottom-navigation'
import Header from '@/app/_components/shared/header'
Expand All @@ -21,12 +22,11 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</Header>
{children}
<BottomNavigation selected='challenge' />
<script
<Script
src='https://t1.kakaocdn.net/kakao_js_sdk/2.5.0/kakao.min.js'
integrity='sha384-kYPsUbBPlktXsY6/oNHSUDZoTX6+YI51f63jCPEIPFP09ttByAdxd2mEjKuhdqn4'
crossOrigin='anonymous'
defer
></script>
></Script>
</main>
)
}
9 changes: 9 additions & 0 deletions app/(route)/challenge/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useMutation } from '@tanstack/react-query'

import { createChallenge } from '@/app/_service/challenge'

export const useCreateChallengeMutation = () => {
return useMutation({
mutationFn: createChallenge,
})
}
11 changes: 11 additions & 0 deletions app/_components/icons/CheckBoxIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function CheckBoxIcon() {
return (
<svg xmlns='http://www.w3.org/2000/svg' width='17' height='16' viewBox='0 0 17 16' fill='none'>
<circle cx='8.5' cy='8' r='8' fill='#482BD9' />
<path
d='M12.3167 4.84014C12.2638 4.78512 12.2009 4.74145 12.1315 4.71165C12.0621 4.68185 11.9877 4.6665 11.9126 4.6665C11.8374 4.6665 11.763 4.68185 11.6937 4.71165C11.6243 4.74145 11.5613 4.78512 11.5084 4.84014L7.2676 9.2191L5.48588 7.37594C5.43093 7.32121 5.36607 7.27818 5.295 7.2493C5.22393 7.22041 5.14804 7.20625 5.07166 7.20761C4.99527 7.20897 4.9199 7.22584 4.84984 7.25724C4.77978 7.28864 4.7164 7.33396 4.66333 7.39062C4.61025 7.44728 4.56852 7.51416 4.54051 7.58745C4.5125 7.66074 4.49877 7.739 4.50009 7.81776C4.50141 7.89652 4.51776 7.97425 4.54821 8.04649C4.57866 8.11874 4.62261 8.18409 4.67756 8.23882L6.86344 10.4929C6.91635 10.5479 6.97931 10.5916 7.04868 10.6214C7.11805 10.6512 7.19245 10.6665 7.2676 10.6665C7.34274 10.6665 7.41714 10.6512 7.48651 10.6214C7.55588 10.5916 7.61884 10.5479 7.67175 10.4929L12.3167 5.70301C12.3745 5.64805 12.4206 5.58133 12.4522 5.50708C12.4837 5.43283 12.5 5.35264 12.5 5.27157C12.5 5.19051 12.4837 5.11032 12.4522 5.03607C12.4206 4.96182 12.3745 4.8951 12.3167 4.84014Z'
fill='white'
/>
</svg>
)
}
5 changes: 4 additions & 1 deletion app/_components/providers/CoreProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { useAxiosInterceptor } from '@/app/_hooks/useAxiosInterceptor'

import AxiosProvider from './AxiosInterceptorProvider'
import QueryClientProvider from './QueryClientProvider'
import { UserContextProvider } from './UserProvider'

export default function CoreProvider({ children }: { children: React.ReactNode }) {
useAxiosInterceptor()

return (
<AxiosProvider>
<QueryClientProvider>{children}</QueryClientProvider>
<QueryClientProvider>
<UserContextProvider>{children}</UserContextProvider>
</QueryClientProvider>
</AxiosProvider>
)
}
Loading

0 comments on commit 44009bc

Please sign in to comment.