Skip to content

Commit

Permalink
Merge pull request #61 from Nico1eKim/feat/setting-design
Browse files Browse the repository at this point in the history
💄 design: setting 페이지들 디자인 완료
  • Loading branch information
gw-lim authored Feb 6, 2024
2 parents 513fe3c + 93da8a6 commit 5ec180c
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 131 deletions.
64 changes: 27 additions & 37 deletions app/(route)/(header)/setting/favorite/page.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,39 @@
"use client";

import { MOCK } from "app/_constants/mock";
import classNames from "classnames";
import { useEffect, useState } from "react";
import BottomButton from "@/components/button/BottomButton";
import useInfScroll from "@/hooks/useInfScroll";

const INITIAL = ["정우"];
import { 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 [favorite, setFavorite] = useState<string[]>(INITIAL);
const handleClick = (name: string) => () => {
if (favorite.includes(name)) {
setFavorite((prev) => prev.filter((item) => item !== name));
return;
}
setFavorite((prev) => [...prev, name]);
};

const { isVisible, infRef } = useInfScroll();
const [cursorId, setCursorId] = useState(12);

useEffect(() => {
if (cursorId > MOCK.length) return;
if (isVisible) {
setCursorId((prev) => prev + 6);
}
}, [isVisible]);
const { modal, openModal, closeModal } = useModal();
const { control } = useForm({ defaultValues: { search: "" } });

return (
<div className="flex flex-col gap-24 overflow-auto py-24">
<button className="w-max text-14 font-600 text-gray-500 underline underline-offset-2">{"찾으시는 아티스트가 없나요? >"}</button>
<div className="grid h-5/6 snap-y snap-mandatory grid-cols-3 gap-20 self-start overflow-auto px-16">
{MOCK.slice(0, cursorId).map((entity) => (
<button onClick={handleClick(entity.name)} key={entity.name} className="flex w-max snap-start flex-col items-center gap-8">
<div className={classNames("h-80 w-80 rounded-full bg-gray-300", { "border-[1px] border-solid border-black": favorite.includes(entity.name) })} />
{/* <Image src={entity.profileImage} alt={`${entity.name} 사진`} /> */}
<p className="text-16 font-600">{entity.name}</p>
<>
<div className="flex flex-col gap-24 px-20 py-36">
<section className="flex flex-col gap-12">
<h2 className="text-20 font-700 text-gray-900">좋아하는 아티스트를 알려주세요!</h2>
<button onClick={() => openModal("noArtist")} className="w-188 text-14 text-gray-500 underline underline-offset-2">
찾으시는 아티스트가 없으신가요?
</button>
))}
<div ref={infRef} />
</section>
<MyArtistList data={MOCK} />
</div>
{/* <button className={classNames("rounded-sm px-16 py-12 text-16", { "bg-black text-white": favorite.length }, { "bg-gray-300 text-black": !favorite.length })}>변경하기</button> */}
<BottomButton isDisabled={!favorite.length}>변경 내용 저장</BottomButton>
</div>
{modal === "noArtist" && (
<InputModal
title="아티스트 등록 요청"
label=""
btnText="요청하기"
handleBtnClick={() => openModal("confirm")}
closeModal={closeModal}
{...{ placeholder: "찾으시는 아티스트를 알려주세요.", rules: { required: "내용을 입력하세요." }, control: control }}
/>
)}
{modal === "confirm" && <AlertModal closeModal={closeModal}>등록 요청이 제출되었습니다.</AlertModal>}
</>
);
};
export default FavoritePage;
14 changes: 0 additions & 14 deletions app/(route)/(header)/setting/layout.tsx

This file was deleted.

103 changes: 52 additions & 51 deletions app/(route)/(header)/setting/password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,60 +26,61 @@ const PasswordPage = () => {
console.log(currentPw, newPw);
};

const isFormValid = formState.isValid;

return (
<form ref={formSection} onSubmit={handleSubmit(handlePwChange)} className="flex flex-grow flex-col justify-between py-60">
<div className="flex flex-col gap-24">
<InputText
name="currentPw"
type="password"
control={control}
rules={{
required: ERROR_MESSAGES.password.passwordField,
validate: {
matchPassword: (value: string) => {
//React-Query로 현재비밀번호 확인하면 됨.
const passwordValue = "iwant18080";
return passwordValue === value || ERROR_MESSAGES.passwordCh.passwordChField;
},
<form ref={formSection} onSubmit={handleSubmit(handlePwChange)} className="flex flex-col gap-20 px-20 py-36">
<InputText
name="currentPw"
type="password"
control={control}
rules={{
required: ERROR_MESSAGES.password.passwordField,
validate: {
matchPassword: (value: string) => {
//React-Query로 현재비밀번호 확인하면 됨.
const passwordValue = "iwant18080";
return passwordValue === value || ERROR_MESSAGES.passwordCh.passwordChField;
},
}}
onKeyDown={handleEnterNext}
>
현재 비밀번호
</InputText>
<InputText
name="newPw"
type="password"
control={control}
rules={{
required: ERROR_MESSAGES.password.passwordField,
pattern: { value: REG_EXP.CHECK_PASSWORD, message: ERROR_MESSAGES.password.passwordPattern },
deps: ["newPwCheck"],
}}
onKeyDown={handleEnterNext}
>
새 비밀번호
</InputText>
<InputText
name="newPwCheck"
type="password"
control={control}
rules={{
required: ERROR_MESSAGES.password.passwordField,
validate: {
matchPassword: (value) => {
const passwordValue = getValues("newPw");
return passwordValue === value || ERROR_MESSAGES.passwordCh.passwordChField;
},
},
}}
onKeyDown={handleEnterNext}
>
현재 비밀번호
</InputText>
<InputText
name="newPw"
type="password"
control={control}
rules={{
required: ERROR_MESSAGES.password.passwordField,
pattern: { value: REG_EXP.CHECK_PASSWORD, message: ERROR_MESSAGES.password.passwordPattern },
deps: ["newPwCheck"],
}}
onKeyDown={handleEnterNext}
hint="숫자, 영문을 조합하여 8자리 이상"
>
새 비밀번호
</InputText>
<InputText
name="newPwCheck"
type="password"
control={control}
rules={{
required: ERROR_MESSAGES.password.passwordField,
validate: {
matchPassword: (value) => {
const passwordValue = getValues("newPw");
return passwordValue === value || ERROR_MESSAGES.passwordCh.passwordChField;
},
}}
onKeyDown={handleEnterNext}
>
새 비밀번호 확인
</InputText>
</div>
{/* <button className="rounded-sm bg-black px-16 py-12 text-16 text-white">변경하기</button> */}
<BottomButton>변경 내용 저장</BottomButton>
},
}}
onKeyDown={handleEnterNext}
placeholder="다시 입력해주세요."
>
새 비밀번호 확인
</InputText>
<BottomButton isDisabled={!isFormValid}>변경 내용 저장</BottomButton>
</form>
);
};
Expand Down
34 changes: 17 additions & 17 deletions app/(route)/(header)/setting/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Image from "next/image";
import { useEffect, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import BottomButton from "@/components/button/BottomButton";
import InputFile from "@/components/input/InputFile";
import InputProfileImg from "@/components/input/InputProfileImg";
import InputText from "@/components/input/InputText";
import { ERROR_MESSAGES, REG_EXP } from "@/utils/signupValidation";

Expand Down Expand Up @@ -50,26 +50,26 @@ const ProfilePage = () => {
};

return (
<form onSubmit={handleSubmit(handleProfileSubmit)} className="flex flex-col gap-24 py-60">
<div className="relative flex flex-col">
<InputFile control={control} name="profileImage">
프로필 사진
</InputFile>
<Image
src={thumbnail || "/icon/no-profile.svg"}
width={100}
height={100}
alt="설정할 프로필 이미지"
className="pointer-events-none absolute left-1/2 top-1/2 -mt-[0.2rem] h-100 -translate-x-1/2 -translate-y-1/2 rounded-full object-cover"
/>
<button type="button" onClick={() => setThumbnail("")} className="w-max self-center text-14 underline">
기본 이미지로 설정하기
</button>
<form onSubmit={handleSubmit(handleProfileSubmit)} className="flex flex-col gap-20 px-20 py-36">
<div className="flex h-172 flex-col gap-8">
<InputProfileImg control={control} name="profileImage" hasProfile={!!thumbnail}>
<Image
src={thumbnail || "/icon/no-profile.svg"}
width={100}
height={100}
alt="설정할 프로필 이미지"
className="pointer-events-none -mt-[0.2rem] h-100 rounded-full object-cover"
/>
</InputProfileImg>
{thumbnail && (
<button type="button" onClick={() => setThumbnail("")} className="w-max self-center text-14 font-600 text-gray-400 underline">
기본 이미지로 변경
</button>
)}
</div>
<InputText name="nickname" control={control} maxLength={10} rules={nicknameRules}>
닉네임
</InputText>
{/* <button className={`rounded-sm bg-black px-16 py-12 text-16 text-white`}>변경하기</button> */}
<BottomButton>변경 내용 저장</BottomButton>
</form>
);
Expand Down
48 changes: 48 additions & 0 deletions app/_components/MyArtistList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useState } from "react";
import { ArtistType } from "../_types";
import ArtistCard from "./ArtistCard";
import ChipButton from "./chip/ChipButton";
import SearchInput from "./input/SearchInput";

interface Props {
data: ArtistType[];
}

const MyArtistList = ({ data }: Props) => {
const [keyword, setKeyword] = useState("");
const [selected, setSelected] = useState<string[]>([]);

const handleArtistClick = (name: string) => {
if (selected.includes(name)) {
setSelected((prevSelected) => prevSelected.filter((item) => item !== name));
} else {
setSelected((prevSelected) => [...prevSelected, name]);
}
};

return (
<div className="flex flex-col gap-24 ">
<section className="flex flex-col gap-16">
<SearchInput setKeyword={setKeyword} placeholder="최애의 행사를 찾아보세요!" />
<div className="flex w-full gap-12 overflow-auto">
{selected.map((artist) => (
<ChipButton label={artist} key={artist} onDelete={() => handleArtistClick(artist)} />
))}
</div>
</section>
<section className="m-auto w-320">
<ul className="grid grid-cols-3 gap-x-16 gap-y-20 px-8">
{data.map((artist, index) => (
<li key={index}>
<ArtistCard onClick={() => handleArtistClick(artist.name)} isChecked={selected.includes(artist.name)} profileImage={artist.profileImage}>
{artist.name}
</ArtistCard>
</li>
))}
</ul>
</section>
</div>
);
};

export default MyArtistList;
5 changes: 4 additions & 1 deletion app/_components/chip/ChipButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ const ChipButton = ({ label, selected: initial = false, onClick, onDelete }: Pro
}

return (
<button onClick={handleClick} className={`flex-center w-max gap-4 rounded-lg px-12 py-4 ${selected ? "bg-gray-900 text-white-black" : "bg-gray-50 text-gray-700"}`}>
<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"}`}
>
<p className="text-14 font-500">{label}</p>
{!!onDelete && <CloseIcon onClick={handleDelete} alt="태그 삭제" width={16} height={16} stroke={selected ? "#FFF" : "#A0A5B1"} />}
</button>
Expand Down
65 changes: 65 additions & 0 deletions app/_components/input/InputProfileImg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import classNames from "classnames";
import { ChangeEvent, KeyboardEvent, ReactNode } from "react";
import { FieldPath, FieldValues, UseControllerProps, useController } from "react-hook-form";
import EditIcon from "@/public/icon/pencil.svg";

interface Props {
children?: ReactNode;
hasProfile?: boolean;
}

type Function = <TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>(
prop: UseControllerProps<TFieldValues, TName> & Props,
) => ReactNode;

const InputProfileImg: Function = ({ children, hasProfile, ...props }) => {
const { field, fieldState } = useController(props);

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
field.onChange({ target: { value: e.target.files, name: field.name } });
};

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
const target = e.target as HTMLElement;
const input = target.nextElementSibling as HTMLElement;
if (input) {
input.click();
}
}
};

return (
<div className="flex-center flex-col gap-8">
<h2 className="w-full text-16 text-gray-900">프로필 사진</h2>
<div className="relative flex flex-col gap-8">
{children}
<label>
{hasProfile ? (
<div
onKeyDown={handleKeyDown}
tabIndex={0}
className="flex-center absolute right-0 top-0 h-28 w-28 cursor-pointer rounded-md border border-white-black bg-main-pink-500"
>
<EditIcon />
</div>
) : (
<div
onKeyDown={handleKeyDown}
tabIndex={0}
className="flex-center h-32 w-100 cursor-pointer rounded-sm border border-gray-400 bg-gray-50 px-16 text-14 font-600 text-gray-700"
>
이미지 등록
</div>
)}
<input type="file" name={field.name} ref={field.ref} multiple onChange={handleChange} className="hidden" accept="image/*" />
</label>
</div>
{fieldState.error && (
<p className={classNames(`font-normal mt-4 h-8 text-12`, { "text-red-500": fieldState.error, "text-gray-400": !fieldState.error })}>{fieldState?.error?.message}</p>
)}
</div>
);
};
export default InputProfileImg;
2 changes: 1 addition & 1 deletion app/_components/input/InputText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const InputText: Function = ({
{(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 mt-4 h-12 text-12 ${fieldState.error ? "text-red" : "text-gray-400"}`}>{fieldState?.error?.message || hint}</p>
<p className={`font-normal h-18 mt-4 text-12 ${fieldState.error ? "text-red" : "text-gray-500"}`}>{fieldState?.error?.message || hint}</p>
</div>
)}
</div>
Expand Down
Loading

0 comments on commit 5ec180c

Please sign in to comment.