Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
]
},
"dependencies": {
"@rehookify/datepicker": "^6.6.8",
"@tanstack/react-form": "^1.27.0",
"@tanstack/react-query": "^5.90.3",
"@tanstack/react-query-devtools": "^5.90.2",
Expand Down
120 changes: 91 additions & 29 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions src/app/post-meetup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const PostMeetupPage = () => {
defaultValues: {
title: '',
address: '',
date: '',
dateAndTime: {
date: '',
time: '',
},
cap: 0,
images: {},
detail: '',
Expand All @@ -40,7 +43,7 @@ const PostMeetupPage = () => {
<section className='px-4'>
<form.Field children={(field) => <MeetupTitleField field={field} />} name='title' />
<form.Field children={(field) => <MeetupAddressField field={field} />} name='address' />
<form.Field children={(field) => <MeetupDateField field={field} />} name='date' />
<form.Field children={(field) => <MeetupDateField field={field} />} name='dateAndTime' />
<form.Field children={(field) => <MeetupCapField field={field} />} name='cap' />
<form.Field children={(field) => <MeetupImagesField field={field} />} name='images' />
<form.Field children={(field) => <MeetupDetailField field={field} />} name='detail' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const MeetupAddressField = ({ field }: Props) => {
type='text'
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onClick={() => console.log('address clicked!')}
/>
</div>
);
Expand Down
40 changes: 22 additions & 18 deletions src/components/pages/post-meetup/fields/date-field/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,39 @@
import { AnyFieldApi } from '@tanstack/react-form';

import { Icon } from '@/components/icon';
import { Input, Label } from '@/components/ui';
import { DatePickerModal } from '@/components/pages/post-meetup/modals/date-picker-modal';
import { Label } from '@/components/ui';
import { useModal } from '@/components/ui';

interface Props {
field: AnyFieldApi;
}

export const MeetupDateField = ({ field }: Props) => {
const { open } = useModal();

const value = field.state.value.date + field.state.value.time;

return (
<div className='mt-6 flex w-full flex-col gap-1'>
<Label htmlFor='post-meetup-date' required>
모임 날짜
</Label>
<Input
id='post-meetup-date'
className='bg-mono-white focus:border-mint-500 rounded-2xl border border-gray-300'
frontIcon={
<Icon
id='calendar-1'
width={20}
className='pointer-events-none absolute top-0 left-4 flex h-full items-center text-gray-500'
height={20}
/>
}
placeholder='날짜와 시간을 선택해주세요'
required
type='text'
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
<button
className='bg-mono-white focus:border-mint-500 relative cursor-pointer rounded-2xl border border-gray-300 p-4 pl-11 focus:outline-none'
type='button'
onClick={() => open(<DatePickerModal field={field} />)}
>
<Icon
id='calendar-1'
width={20}
className='pointer-events-none absolute top-0 left-4 flex h-full items-center text-gray-500'
height={20}
/>
<p className='text-left text-gray-500'>
{value.trim() ? value : '날짜와 시간을 선택해주세요'}
</p>
</button>
</div>
);
};
35 changes: 28 additions & 7 deletions src/components/pages/post-meetup/fields/tags-field/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import { useState } from 'react';

import { AnyFieldApi } from '@tanstack/react-form';

import { Icon } from '@/components/icon';
Expand All @@ -10,6 +12,18 @@ interface Props {
}

export const MeetupTagsField = ({ field }: Props) => {
const [inputValue, setInputValue] = useState('');

const onEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.code !== 'Enter' && e.code !== 'NumpadEnter') return;

const hasDupe = field.state.value.includes(inputValue);

if (!hasDupe && inputValue.trim()) field.pushValue(inputValue);

setInputValue('');
};

return (
<div className='flex w-full flex-col gap-1'>
<Label htmlFor='post-meetup-tags'>태그</Label>
Expand All @@ -26,14 +40,21 @@ export const MeetupTagsField = ({ field }: Props) => {
}
placeholder='입력 후 Enter'
type='text'
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={onEnter}
/>
<ul className='mt-0.5 flex flex-wrap px-2'>
<li className='bg-mint-100 text-mint-700 flex-center gap-0.5 rounded-full px-2 py-0.5'>
<p className='text-text-xs-medium'>#태그</p>
<Icon id='small-x-1' width={7} height={7} />
</li>
<ul className='mt-0.5 flex flex-wrap gap-x-1 gap-y-1.5 px-2'>
{field.state.value.map((tag: string, idx: number) => (
<li
key={tag}
className='bg-mint-100 text-mint-700 flex-center cursor-pointer gap-0.5 rounded-full px-2 py-0.5'
onClick={() => field.removeValue(idx)}
>
<p className='text-text-xs-medium'>#{tag}</p>
<Icon id='small-x-2' width={10} height={10} />
</li>
))}
</ul>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useEffect, useState } from 'react';

import { useDatePicker } from '@rehookify/datepicker';
import clsx from 'clsx';

import { Icon } from '@/components/icon';

interface Props {
currentTab: 'date' | 'time';
handleDateChange: (date: string) => void;
}

export const DatePicker = ({ currentTab, handleDateChange }: Props) => {
const nowDate = new Date();
const [selectedDates, onDatesChange] = useState<Date[]>([nowDate]);

useEffect(() => {
handleDateChange(selectedDates.toString());
}, [selectedDates]);

const {
data: { weekDays, calendars },
propGetters: { dayButton, addOffset, subtractOffset },
} = useDatePicker({
selectedDates,
onDatesChange,
locale: {
day: 'numeric',
weekday: 'short',
monthName: 'numeric',
},
dates: {
minDate: nowDate,
maxDate: new Date(nowDate.getFullYear() + 1, 12, 0),
},
calendar: {
mode: 'fluid',
},
});

const { year, month, days } = calendars[0];

return (
<section className='mt-4 select-none'>
<div className='flex justify-end gap-4'>
<button {...subtractOffset({ months: 1 })} aria-label='Previous month'>
<Icon id='chevron-left-1' className='text-gray-600' />
</button>
<button {...addOffset({ months: 1 })} aria-label='Next month'>
<Icon id='chevron-right-1' className='text-gray-600' />
</button>
</div>

<div className='mt-2 border-b-1 border-gray-200 pb-3'>
<ul className='grid grid-cols-7 text-gray-800'>
{weekDays.map((day) => (
<li key={`${month}-${day}`} className='flex-center size-10'>
{day}
</li>
))}
</ul>
<ul className='text-text-sm-regular grid grid-cols-7 grid-rows-5 gap-y-1 text-gray-800'>
{days.map((d) => (
<li key={d.$date.toDateString()}>
<button
{...dayButton(d)}
className={clsx(
'flex-center size-10',
d.selected && 'bg-mint-500 text-text-sm-medium rounded-full text-white',
!d.inCurrentMonth && 'text-gray-600',
)}
>
{d.day}
</button>
</li>
))}
</ul>
</div>

<div className='text-text-md-semibold flex-center mt-3 gap-2.5 text-gray-700'>
<span>{year}년</span>
<span className={currentTab === 'date' ? 'text-mint-600' : ''}>
{month}월 {selectedDates[0].getDate()}일
</span>
<span className={currentTab === 'time' ? 'text-mint-600' : ''}>12:20</span>
<span>AM</span>
<span>PM</span>
</div>
</section>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

import { useState } from 'react';

import { AnyFieldApi } from '@tanstack/react-form';
import clsx from 'clsx';

import { Icon } from '@/components/icon';
import { DatePicker } from '@/components/pages/post-meetup/modals/date-picker-modal/date-picker';
import { ModalContent, ModalTitle, useModal } from '@/components/ui';

interface Props {
field: AnyFieldApi;
}

export const DatePickerModal = ({ field }: Props) => {
const [tabMenu, setTabMenu] = useState<'date' | 'time'>('date');
const { close } = useModal();

return (
<ModalContent>
<ModalTitle>모달임</ModalTitle>
<section className=''>
<nav className='flex'>
{DATE_MODAL_TAB_MENU.map(({ name, iconId }) => (
<button
key={name}
className={clsx(
'flex-center h-12 grow-1 border-b-2 border-gray-200',
tabMenu === name && 'border-mint-500',
)}
type='button'
onClick={() => setTabMenu(name)}
>
<Icon
id={iconId}
className={clsx('text-gray-500', tabMenu === name && 'text-mint-500')}
/>
</button>
))}
</nav>

<DatePicker
currentTab={tabMenu}
handleDateChange={(date: string) => field.handleChange({ ...field.state.value, date })}
/>

<div className='flex-center mt-6 gap-2'>
<button
className='text-text-md-semibold grow-1 rounded-2xl border-1 border-gray-400 bg-white py-3.25 text-gray-600'
type='button'
onClick={close}
>
취소
</button>
<button className='text-text-md-bold bg-mint-400 grow-1 rounded-2xl py-3.5 text-white disabled:bg-gray-500'>
확인
</button>
</div>
</section>
</ModalContent>
);
};

const DATE_MODAL_TAB_MENU = [
{ name: 'date', iconId: 'calendar-1' },
{ name: 'time', iconId: 'clock' },
] as const;
2 changes: 1 addition & 1 deletion src/components/pages/post-meetup/post-button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from '@/components/ui';

export const MeetupSubmitButton = () => {
return (
<div className='mt-6 border-t-1 border-gray-200 px-4 py-3'>
<div className='mt-6 border-t-1 border-gray-200 bg-white px-4 py-3'>
<Button type='submit'>모임 생성</Button>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ body {
background: var(--background);
color: var(--foreground);
}

button {
cursor: pointer;
}