Skip to content

Commit

Permalink
💄 add create challenge UI
Browse files Browse the repository at this point in the history
  • Loading branch information
wook-hyung committed Dec 16, 2023
1 parent 48bbe84 commit 1690fb7
Show file tree
Hide file tree
Showing 11 changed files with 693 additions and 10 deletions.
105 changes: 105 additions & 0 deletions app/(route)/challenge/_components/ChallengeFormDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use client'

import { useState } from 'react'

import * as DialogPrimitive from '@radix-ui/react-dialog'

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

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

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

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

export default function ChallengeFormDialog() {
const [selectedCategory, setSelectedCategory] = useState<Category>('선택안함')

return (
<Dialog>
<DialogTrigger className='absolute bottom-16 right-8'>
<button>
<AddIcon />
</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'>
<DialogClose className='absolute right-2 top-1'>
<BlackCloseIcon />
</DialogClose>

<DialogPrimitive.Title className='text-center text-xl font-extrabold text-[#482BD9]'>
챌린지 추가
</DialogPrimitive.Title>

<div className='flex flex-col gap-3'>
<div className='flex flex-col gap-2'>
<ChallengeForm>
<ChallengeForm.Title title='챌린지 이름' />
<ChallengeForm.Input currentLength={0} maxLength={12} placeholder='챌린지 이름을 작성해볼까요?' />
</ChallengeForm>

<div className='flex gap-1'>
{CATEGORIES.map((category) => {
return (
<button
key={category}
className={cn('h-8 rounded bg-[#A6A6A6] px-2 text-center text-xs font-semibold', {
'bg-[#84D5D7]': category === selectedCategory,
})}
onClick={() => {
setSelectedCategory(category)
}}
>
{category}
</button>
)
})}
</div>
<div className='flex items-center gap-2'>
<AlertIcon />
<span className='text-[10px] text-[#FF4B2B]'>챌린지 이름은 챌린지 생성 이후 변경이 어려워요</span>
</div>
</div>
<ChallengeForm>
<ChallengeForm.Title title='인증 방식' />
<ChallengeForm.Input currentLength={0} maxLength={8} placeholder='어떤 사물을 찍어서 인증할까요?' />
</ChallengeForm>
<ChallengeForm>
<ChallengeForm.Title title='보상' />
<ChallengeForm.Input currentLength={0} maxLength={15} placeholder='어떤 보상으로 설정할까요?' />
</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' />
<div className='flex flex-col'>
<span className='text-[10px] text-[#140A29]'>
인증 사진을 챌린지 상대가 승인 및 반려할 수 있습니다.
</span>
<span className='text-[10px] text-[#140A29]'>24시간 이상 미승인시 자동 승인됩니다.</span>
</div>
</div>
<div className='flex items-center gap-1'>
<div className='h-4 w-4 rounded-full bg-[#A6A6A6] p-1' />
<span className='text-[10px] text-[#140A29]'>인증 사진이 승인된 건에 한해서는 취소가 불가능합니다.</span>
</div>
</div>
<button
className={cn(
'mx-auto min-h-[60px] w-[240px] rounded-lg bg-[#482BD9] text-center',
'disabled:bg-[#A6A6A6]'
)}
>
함께할 친구 초대하기
</button>
</div>
</DialogContent>
</Dialog>
)
}
29 changes: 29 additions & 0 deletions app/(route)/challenge/_components/challenge-form-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default function ChallengeForm({ children }: { children: React.ReactNode }) {
return <label className='flex flex-col gap-1'>{children}</label>
}

function Title({ title }: { title: string }) {
return <span className='text-sm font-semibold text-[#140A29]'>{title}</span>
}

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

function Input({ currentLength, maxLength, ...props }: ChallengeFormInputProps) {
return (
<div className='flex justify-between gap-2 rounded-lg border border-[#140A29] bg-white p-3'>
<input
className='flex-1 appearance-none text-xs text-[#595959] placeholder:text-xs placeholder:text-[#A6A6A6] focus:ring-0'
{...props}
/>
<span className='text-[10px] text-[#595959]'>
{currentLength}/{maxLength}
</span>
</div>
)
}

ChallengeForm.Title = Title
ChallengeForm.Input = Input
2 changes: 1 addition & 1 deletion app/(route)/challenge/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
}}
/>
<Header.Title>챌린지</Header.Title>
<div />
<div className='w-9' />
</Header>
{children}
<BottomNavigation selected='challenge' />
Expand Down
32 changes: 28 additions & 4 deletions app/(route)/challenge/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
'use client'

import { useState } from 'react'

import { cn } from '@/app/_styles/utils'

import ChallengeCard from './_components/ChallengeCard'
import ChallengeFormDialog from './_components/ChallengeFormDialog'

export default function ChallengePage() {
const [tab, setTab] = useState<'ongoing' | 'completed'>('ongoing')

return (
<div className='flex max-h-full flex-col gap-4 px-6 pt-[88px]'>
<div className='relative flex max-h-full flex-col gap-4 px-6 pt-[88px]'>
<div className='flex w-full'>
<div className='flex h-12 w-1/2 items-center justify-center border-b border-white font-semibold'>진행중</div>
<div className='flex h-12 w-1/2 items-center justify-center border-b border-[#595959] font-semibold text-[#595959]'>
<button
className={cn('flex h-12 w-1/2 items-center justify-center border-b border-white font-semibold', {
'border-b-[#595959] text-[#595959]': tab !== 'ongoing',
})}
onClick={() => {
setTab('ongoing')
}}
>
진행중
</button>
<button
className={cn('flex h-12 w-1/2 items-center justify-center border-b border-[white] font-semibold', {
'border-b-[#595959] text-[#595959]': tab !== 'completed',
})}
onClick={() => {
setTab('completed')
}}
>
완료
</div>
</button>
</div>
<div className='flex flex-col gap-3 overflow-y-auto pb-20'>
{new Array(10).fill(0).map((_, index) => {
return <ChallengeCard key={index} />
})}
</div>
<ChallengeFormDialog />
</div>
)
}
40 changes: 40 additions & 0 deletions app/_components/icons/AddIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export default function AddIcon() {
return (
<svg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 64 64' fill='none'>
<g filter='url(#filter0_d_210_4970)'>
<circle cx='32' cy='28' r='28' fill='#482BD9' />
<path
fillRule='evenodd'
clipRule='evenodd'
d='M31.9994 16C31.0527 16 30.2852 16.7675 30.2852 17.7143V26.2857H21.7143C20.7675 26.2857 20 27.0532 20 28C20 28.9468 20.7675 29.7143 21.7143 29.7143H30.2852V38.2857C30.2852 39.2325 31.0527 40 31.9994 40C32.9462 40 33.7137 39.2325 33.7137 38.2857V29.7143H42.2857C43.2325 29.7143 44 28.9468 44 28C44 27.0532 43.2325 26.2857 42.2857 26.2857H33.7137V17.7143C33.7137 16.7675 32.9462 16 31.9994 16Z'
fill='white'
/>
</g>
<defs>
<filter
id='filter0_d_210_4970'
x='0'
y='0'
width='64'
height='64'
filterUnits='userSpaceOnUse'
colorInterpolationFilters='sRGB'
>
<feFlood floodOpacity='0' result='BackgroundImageFix' />
<feColorMatrix
in='SourceAlpha'
type='matrix'
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
result='hardAlpha'
/>
<feOffset dy='4' />
<feGaussianBlur stdDeviation='2' />
<feComposite in2='hardAlpha' operator='out' />
<feColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0' />
<feBlend mode='normal' in2='BackgroundImageFix' result='effect1_dropShadow_210_4970' />
<feBlend mode='normal' in='SourceGraphic' in2='effect1_dropShadow_210_4970' result='shape' />
</filter>
</defs>
</svg>
)
}
9 changes: 9 additions & 0 deletions app/_components/icons/AlertIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function AlertIcon() {
return (
<svg xmlns='http://www.w3.org/2000/svg' width='17' height='17' viewBox='0 0 17 17' fill='none'>
<circle cx='8.5' cy='8.5' r='8' fill='#FF4B2B' />
<rect x='7.5' y='4.5' width='2' height='5' rx='1' fill='white' />
<circle cx='8.5' cy='11.5' r='1' fill='white' />
</svg>
)
}
12 changes: 12 additions & 0 deletions app/_components/icons/BlackCloseIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function BlackCloseIcon() {
return (
<svg xmlns='http://www.w3.org/2000/svg' width='41' height='40' viewBox='0 0 41 40' fill='none'>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M12.096 13.9543L17.996 19.8543L12.096 25.8377C11.7856 26.1499 11.6113 26.5723 11.6113 27.0127C11.6113 27.453 11.7856 27.8754 12.096 28.1877C12.2517 28.3421 12.4364 28.4643 12.6395 28.5473C12.8425 28.6302 13.06 28.6723 13.2793 28.671C13.4987 28.6723 13.7161 28.6302 13.9192 28.5473C14.1222 28.4643 14.3069 28.3421 14.4626 28.1877L20.1506 22.4327L25.8385 28.1877C25.9942 28.3421 26.1789 28.4643 26.382 28.5473C26.585 28.6302 26.8025 28.6723 27.0218 28.671C27.2412 28.6723 27.4586 28.6302 27.6617 28.5473C27.8647 28.4643 28.0494 28.3421 28.2052 28.1877C28.5156 27.8754 28.6898 27.453 28.6898 27.0127C28.6898 26.5723 28.5156 26.1499 28.2052 25.8377L22.3052 19.8543L28.2052 13.9543C28.5156 13.6421 28.6898 13.2196 28.6898 12.7793C28.6898 12.339 28.5156 11.9166 28.2052 11.6043C28.0502 11.4481 27.8659 11.3241 27.6628 11.2395C27.4597 11.1549 27.2418 11.1113 27.0218 11.1113C26.8018 11.1113 26.584 11.1549 26.3809 11.2395C26.1778 11.3241 25.9934 11.4481 25.8385 11.6043L20.1506 17.2922L14.4626 11.6043C14.3077 11.4481 14.1234 11.3241 13.9203 11.2395C13.7172 11.1549 13.4993 11.1113 13.2793 11.1113C13.0593 11.1113 12.8415 11.1549 12.6384 11.2395C12.4353 11.3241 12.2509 11.4481 12.096 11.6043C11.7856 11.9166 11.6113 12.339 11.6113 12.7793C11.6113 13.2196 11.7856 13.6421 12.096 13.9543Z'
fill='#140A29'
/>
</svg>
)
}
24 changes: 24 additions & 0 deletions app/_components/shared/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'

import * as DialogPrimitive from '@radix-ui/react-dialog'

export const DialogContent = React.forwardRef<
HTMLDivElement,
{
children: React.ReactNode
props?: React.ForwardRefExoticComponent<DialogPrimitive.DialogContentProps & React.RefAttributes<HTMLDivElement>>
}
>(({ children, ...props }, forwardedRef) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className='DialogOverlay' />
<DialogPrimitive.Content className='DialogContent' {...props} ref={forwardedRef}>
{children}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
))

DialogContent.displayName = 'Dialog'

export const Dialog = DialogPrimitive.Root
export const DialogTrigger = DialogPrimitive.Trigger
export const DialogClose = DialogPrimitive.Close
54 changes: 54 additions & 0 deletions app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,57 @@ html {
body {
height: 100%;
}

/* reset */
button,
fieldset,
input {
all: unset;
}

.DialogOverlay {
background-color: rgb(0, 0, 0, 0.5);
position: fixed;
inset: 0;
animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
z-index: 50;
}

.DialogContent {
z-index: 50;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90vw;
max-width: 450px;
max-height: 85vh;
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
}

.DialogContent:focus {
outline: none;
}

@keyframes overlayShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes contentShow {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
]
},
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@tanstack/react-query": "^5.13.4",
"@tanstack/react-query-devtools": "^5.13.5",
"axios": "^1.6.2",
Expand Down
Loading

0 comments on commit 1690fb7

Please sign in to comment.