Skip to content

Commit

Permalink
Merge pull request #68 from han-kimm/feat/calendar
Browse files Browse the repository at this point in the history
✨ feat: 캘린더에 표시되는 행사 기간 UI 비즈니스 로직 수정
  • Loading branch information
naya-h2 authored Feb 7, 2024
2 parents 96d4774 + 20a06e4 commit 8e4e40c
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 120 deletions.
111 changes: 65 additions & 46 deletions app/(route)/(bottom-nav)/mypage/_components/tab/MyCalendarTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,40 @@ import NextIcon from "@/public/icon/arrow-left_lg.svg";
import PrevIcon from "@/public/icon/arrow-right_lg.svg";
import { ScheduleDataProps } from "../../page";

const MyCalendarTab = ({ scheduleData }: { scheduleData: ScheduleDataProps[] }) => {
const EventTab = ({ scheduleData }: { scheduleData: ScheduleDataProps[] }) => {
const [data, setData] = useState(scheduleData);
const [calendarStyle, setCalendarStyle] = useState("");
let lastDay: (ScheduleDataProps | "blank")[] = [];

const tileContent = ({ date }: { date: Date }) => {
const eventsForDate = data.filter((event) => new Date(event.startDate).getTime() <= date.getTime() && new Date(event.endDate).getTime() >= date.getTime());

if (eventsForDate.length > 0) {
const sortedEvents: ScheduleDataProps[] = sortEvents(eventsForDate);
let today: (ScheduleDataProps | "blank")[] = sortEvents(eventsForDate);

for (const idx in today) {
const lastDayItem = lastDay[idx];
const todayItem = today[idx];
if (!lastDayItem) {
lastDay[idx] = todayItem;
continue;
}
if (lastDayItem === todayItem) {
continue;
}
today.splice(Number(idx), 0, "blank");
}

if (date.getDay() === 1) {
today = today.filter((item) => item !== "blank");
}
lastDay = today;

return (
<div>
{sortedEvents.map((event) => {
{today.map((event, idx) => {
if (event === "blank") {
return <span key={idx + event} className={`h-4 rounded-sm`} />;
}
let type;
if (event.startDate === event.endDate) {
type = SHAPE_TYPE.oneDay;
Expand All @@ -32,7 +53,7 @@ const MyCalendarTab = ({ scheduleData }: { scheduleData: ScheduleDataProps[] })
} else {
type = SHAPE_TYPE.middleDay;
}
return <span key={event.id} className={`h-4 rounded-sm ${type} ${COLOR_TYPE[(event.id + 1) % 6]}`} />;
return <span key={event.id} className={`h-4 rounded-sm ${type} ${COLOR_TYPE[event.id % 6]}`} />;
})}
</div>
);
Expand Down Expand Up @@ -97,54 +118,52 @@ const MyCalendarTab = ({ scheduleData }: { scheduleData: ScheduleDataProps[] })
};

return (
<>
<div className="flex flex-col items-center justify-stretch gap-20 px-20 pb-16 pt-72">
<style>{calendarStyle}</style>
{calendarStyle !== "" && (
<Calendar
locale="ko"
onChange={handleClickToday}
value={selectedDate}
tileContent={tileContent}
nextLabel={<PrevIcon width={16} height={16} viewBox="0 0 24 24" stroke="#A2A5AA" />}
prevLabel={<NextIcon width={16} height={16} viewBox="0 0 24 24" stroke="#A2A5AA" />}
next2Label={null}
prev2Label={null}
formatDay={(locale, date) => date.getDate().toString()}
formatShortWeekday={(locale, date) => {
const shortWeekdays = ["S", "M", "T", "W", "T", "F", "S"];
return shortWeekdays[date.getDay()];
}}
/>
)}
<div className="w-full">
<div className="flex w-full gap-12">
<ChipButton label="예정" onClick={() => handleChipClick("예정")} selected={currentLabel === "예정"} />
<ChipButton label="진행중" onClick={() => handleChipClick("진행중")} selected={currentLabel === "진행중"} />
<ChipButton label="종료" onClick={() => handleChipClick("종료")} selected={currentLabel === "종료"} />
</div>
<ul>
{data
.filter((event) => !selectedDate || (new Date(event.startDate).getTime() <= selectedDate.getTime() && new Date(event.endDate).getTime() >= selectedDate.getTime()))
.map((event) => (
<HorizontalEventCard key={event.id} data={event} hasHeart onHeartClick={handleHeartClick} />
))}
</ul>
<div className="flex flex-col gap-16 px-20 py-16">
<style>{calendarStyle}</style>
{calendarStyle !== "" && (
<Calendar
locale="ko"
onChange={handleClickToday}
value={selectedDate}
tileContent={tileContent}
nextLabel={<PrevIcon onClick={() => (lastDay = [])} width={64} height={16} viewBox="0 0 24 24" stroke="#A2A5AA" />}
prevLabel={<NextIcon onClick={() => (lastDay = [])} width={64} height={16} viewBox="0 0 24 24" stroke="#A2A5AA" />}
next2Label={null}
prev2Label={null}
formatDay={(locale, date) => date.getDate().toString()}
formatShortWeekday={(locale, date) => {
const shortWeekdays = ["S", "M", "T", "W", "T", "F", "S"];
return shortWeekdays[date.getDay()];
}}
/>
)}
<div>
<div className="flex gap-12">
<ChipButton label="예정" onClick={() => handleChipClick("예정")} selected={currentLabel === "예정"} />
<ChipButton label="진행중" onClick={() => handleChipClick("진행중")} selected={currentLabel === "진행중"} />
<ChipButton label="종료" onClick={() => handleChipClick("종료")} selected={currentLabel === "종료"} />
</div>
<ul>
{data
.filter((event) => !selectedDate || (new Date(event.startDate).getTime() <= selectedDate.getTime() && new Date(event.endDate).getTime() >= selectedDate.getTime()))
.map((event) => (
<HorizontalEventCard key={event.id} data={event} hasHeart onHeartClick={handleHeartClick} />
))}
</ul>
</div>
</>
</div>
);
};

export default MyCalendarTab;
export default EventTab;

const COLOR_TYPE: Record<number, string> = {
1: `bg-sub-pink`,
2: `bg-sub-yellow`,
3: `bg-sub-skyblue`,
4: `bg-sub-blue`,
5: `bg-sub-purple`,
6: `bg-sub-red`,
0: `bg-sub-pink`,
1: `bg-sub-yellow`,
2: `bg-sub-skyblue`,
3: `bg-sub-blue`,
4: `bg-sub-purple`,
5: `bg-sub-red`,
};

const SHAPE_TYPE = {
Expand Down
15 changes: 10 additions & 5 deletions app/(route)/(header)/setting/favorite/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
"use client";

import { MOCK } from "app/_constants/mock";
import { useForm } from "react-hook-form";
import { SubmitHandler, useForm } from "react-hook-form";
import MyArtistList from "@/components/MyArtistList";
import AlertModal from "@/components/modal/AlertModal";
import InputModal from "@/components/modal/InputModal";
import { useModal } from "@/hooks/useModal";

const FavoritePage = () => {
const { modal, openModal, closeModal } = useModal();
const { control } = useForm({ defaultValues: { search: "" } });
const { control, handleSubmit, setValue } = useForm({ defaultValues: { request: "" } });

const onSubmit: SubmitHandler<{ request: string }> = ({ request }) => {
if (request) {
openModal("confirm");
setValue("request", "");
}
};
return (
<>
<div className="flex flex-col gap-24 px-20 py-36">
Expand All @@ -25,11 +31,10 @@ const FavoritePage = () => {
{modal === "noArtist" && (
<InputModal
title="아티스트 등록 요청"
label=""
btnText="요청하기"
handleBtnClick={() => openModal("confirm")}
handleBtnClick={handleSubmit(onSubmit)}
closeModal={closeModal}
{...{ placeholder: "찾으시는 아티스트를 알려주세요.", rules: { required: "내용을 입력하세요." }, control: control }}
{...{ name: "request", placeholder: "찾으시는 아티스트를 알려주세요.", rules: { required: "내용을 입력하세요." }, control, noButton: true }}
/>
)}
{modal === "confirm" && <AlertModal closeModal={closeModal}>등록 요청이 제출되었습니다.</AlertModal>}
Expand Down
2 changes: 1 addition & 1 deletion app/_components/MyArtistList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const MyArtistList = ({ data }: Props) => {
<SearchInput setKeyword={setKeyword} placeholder="최애의 행사를 찾아보세요!" />
<div className="flex w-full gap-12 overflow-auto">
{selected.map((artist) => (
<ChipButton label={artist} key={artist} onDelete={() => handleArtistClick(artist)} />
<ChipButton label={artist} key={artist} onClick={() => handleArtistClick(artist)} canDelete />
))}
</div>
</section>
Expand Down
42 changes: 9 additions & 33 deletions app/_components/chip/ChipButton.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,24 @@
"use client";

import { MouseEvent, useState } from "react";
import { ButtonHTMLAttributes, Ref, RefObject, forwardRef } from "react";
import CloseIcon from "@/public/icon/close.svg";

type Handler = "onClick" | "onDelete";
type MappedHandler = {
[key in Handler]?: (e?: MouseEvent) => void;
};

interface Props extends MappedHandler {
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
label: string;
selected?: boolean;
canDelete?: boolean;
}

const ChipButton = ({ label, selected: initial = false, onClick, onDelete }: Props) => {
const [selected, setSelected] = useState(initial);
const [isDelete, setIsDelete] = useState(false);
const handleClick = (e: MouseEvent) => {
e.preventDefault();
setSelected((prev) => !prev);
if (onClick) {
onClick(e);
}
};

const handleDelete = (e: MouseEvent) => {
setIsDelete(true);
if (onDelete) {
onDelete(e);
}
};

if (isDelete) {
return null;
}

const ChipButton = forwardRef(({ label, selected, canDelete, ...rest }: Props, ref: Ref<HTMLButtonElement>) => {
return (
<button
onClick={handleClick}
className={`flex-center w-max flex-shrink-0 gap-4 rounded-lg px-12 py-4 ${selected ? "bg-gray-900 text-white-black" : "bg-gray-50 text-gray-700"}`}
ref={ref}
{...rest}
className={`flex-center w-max flex-shrink-0 gap-4 rounded-lg px-12 py-4 ${canDelete && "border border-main-pink-300 bg-sub-pink-bg text-main-pink-white"} ${selected ? "bg-gray-900 text-white-black" : "bg-gray-50 text-gray-700"}`}
>
<p className="text-14 font-500">{label}</p>
{!!onDelete && <CloseIcon onClick={handleDelete} alt="태그 삭제" width={16} height={16} stroke={selected ? "#FFF" : "#A0A5B1"} />}
{canDelete && <CloseIcon alt="태그 삭제" width={16} height={16} stroke="#FF50AA" />}
</button>
);
};
});
export default ChipButton;
74 changes: 43 additions & 31 deletions app/_components/input/InputText.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import classNames from "classnames";
import Image from "next/image";
import { KeyboardEvent, ReactNode, useState } from "react";
import { InputHTMLAttributes, KeyboardEvent, ReactNode, useCallback, useMemo, useState } from "react";
import { FieldPath, FieldValues, UseControllerProps, useController } from "react-hook-form";

interface Prop {
interface Prop extends InputHTMLAttributes<HTMLInputElement> {
children?: ReactNode;
type?: "text" | "password";
horizontal?: boolean;
placeholder?: string;
autoComplete?: string;
hint?: string;
maxLength?: number;
hidden?: boolean;
readOnly?: boolean;
required?: boolean;
disabled?: boolean;
onKeyDown?: (e: KeyboardEvent) => void;
onClick?: () => void;
isEdit?: boolean;
noButton?: boolean;
}

type Function = <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>(
Expand All @@ -31,14 +25,14 @@ const InputText: Function = ({
placeholder,
autoComplete,
hint,
maxLength,
hidden,
required,
readOnly,
disabled,
onClick,
onKeyDown,
isEdit,
noButton,
...control
}) => {
const { field, fieldState } = useController(control);
Expand All @@ -52,20 +46,53 @@ const InputText: Function = ({
field.onChange("");
};

return (
<div className={`${horizontal && "flex gap-28"}`}>
{children && (
const Label = useCallback(() => {
if (children) {
return (
<label htmlFor={field.name} className={`text-16 ${horizontal && "mt-20"}`}>
{children}
<span className="ml-4 text-red">{required && "*"}</span>
</label>
)}
);
}
}, []);

const Button = useCallback(() => {
if (noButton || hidden) {
return null;
}
if (initialType === "password") {
return (
<button onClick={handlePasswordShow} onKeyDown={onKeyDown} type="button" className="flex-center absolute right-16 top-20 h-24 w-24">
{<Image src={type === "password" ? "/icon/closed-eyes_black.svg" : "/icon/opened-eyes_black.svg"} alt="비밀번호 아이콘" width={16} height={16} />}
</button>
);
}
return (
<button onClick={handleDelete} onKeyDown={onKeyDown} type="button" className="absolute right-16 top-24 h-16 w-16">
<Image src="/icon/x_gray.svg" alt="초기화 버튼" width={16} height={16} />
</button>
);
}, []);

const ErrorField = useCallback(() => {
return (
<div className="mt-4 flex h-12">
{(!!fieldState.error || hint) && <p className={`font-normal text-12 ${fieldState.error ? "text-red" : "text-gray-500"}`}>{fieldState?.error?.message || hint}</p>}
</div>
);
}, [fieldState.error]);

return (
<div className={`${horizontal && "flex gap-28"}`}>
<Label />
<div className={`relative ${horizontal && "flex-1"}`}>
<input
id={field.name}
type={type}
placeholder={placeholder ?? "입력해주세요."}
autoComplete={autoComplete ?? "off"}
hidden={hidden ?? false}
readOnly={readOnly ?? false}
disabled={disabled ?? false}
onClick={onClick}
Expand All @@ -74,26 +101,11 @@ const InputText: Function = ({
className={classNames(
"focus:border-1 mt-8 h-48 w-full rounded-sm bg-gray-50 px-16 py-12 pr-36 text-16 text-gray-900 placeholder:text-gray-400 focus:outline focus:outline-1 focus:outline-blue",
{ "outline outline-1 outline-red": fieldState.error },
{ hidden: hidden ?? false },
{ "outline outline-1 outline-blue": isEdit },
)}
/>
{initialType === "password" && (
<button onClick={handlePasswordShow} onKeyDown={onKeyDown} type="button" className="flex-center absolute right-16 top-20 h-24 w-24">
{<Image src={type === "password" ? "/icon/closed-eyes_black.svg" : "/icon/opened-eyes_black.svg"} alt="비밀번호 아이콘" width={16} height={16} />}
</button>
)}
{initialType !== "password" && !hidden && (
<button onClick={handleDelete} onKeyDown={onKeyDown} type="button" className="absolute right-16 top-24 h-16 w-16">
<Image src="/icon/x_gray.svg" alt="초기화 버튼" width={16} height={16} />
</button>
)}
{(maxLength || fieldState.error || hint) && (
<div className="flex gap-8">
{maxLength ? <span className={classNames("mt-4 h-8", { "text-red": field.value.length > maxLength })}>{`(${field.value.length}/${maxLength})`}</span> : null}
<p className={`font-normal h-18 mt-4 text-12 ${fieldState.error ? "text-red" : "text-gray-500"}`}>{fieldState?.error?.message || hint}</p>
</div>
)}
<Button />
<ErrorField />
</div>
</div>
);
Expand Down
Loading

0 comments on commit 8e4e40c

Please sign in to comment.