-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #335 from boostcampwm2023/feature/setting-page
feat: 세팅 페이지, 프로젝트 제목, 주제 수정 기능 구현
- Loading branch information
Showing
20 changed files
with
579 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { ChangeEvent } from "react"; | ||
|
||
interface InformationInputProps { | ||
label: string; | ||
inputId: string; | ||
inputValue: string; | ||
errorMessage: string; | ||
onChange: (event: ChangeEvent<HTMLInputElement>) => void; | ||
} | ||
|
||
const InformationInput = ({ | ||
label, | ||
inputId, | ||
inputValue, | ||
errorMessage, | ||
onChange, | ||
}: InformationInputProps) => ( | ||
<div className="flex w-full gap-6 mb-5"> | ||
<label | ||
className="min-w-[6.125rem] text-xs font-semibold text-middle-green" | ||
htmlFor={inputId} | ||
> | ||
{label} | ||
</label> | ||
<div> | ||
<input | ||
className="w-[60.3rem] mb-1 h-10 border-[2px] border-text-gray rounded-lg focus:outline-middle-green px-1 hover:cursor-pointer" | ||
type="text" | ||
id={inputId} | ||
autoComplete="off" | ||
value={inputValue} | ||
onChange={onChange} | ||
/> | ||
<p className="text-xxxs text-error-red">{errorMessage}</p> | ||
</div> | ||
</div> | ||
); | ||
|
||
export default InformationInput; |
116 changes: 116 additions & 0 deletions
116
frontend/src/components/setting/InformationSettingSection.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { ChangeEvent, useEffect, useMemo, useState } from "react"; | ||
import InformationInput from "./InformationInput"; | ||
import { useOutletContext } from "react-router-dom"; | ||
import { Socket } from "socket.io-client"; | ||
import useSettingProjectInfoSocket from "../../hooks/pages/setting/useSettingProjectInfoSocket"; | ||
|
||
interface InformationSettingSectionProps { | ||
title: string; | ||
subject: string; | ||
} | ||
|
||
const InformationSettingSection = ({ | ||
title, | ||
subject, | ||
}: InformationSettingSectionProps) => { | ||
const { socket }: { socket: Socket } = useOutletContext(); | ||
const { emitProjectInfoUpdateEvent } = useSettingProjectInfoSocket(socket); | ||
const [titleValue, setTitleValue] = useState(title); | ||
const [titleErrorMessage, setTitleErrorMessage] = useState(""); | ||
const [subjectValue, setSubjectValue] = useState(subject); | ||
const [subjectErrorMessage, setSubjectErrorMessage] = useState(""); | ||
const submitActivated = useMemo( | ||
() => | ||
!( | ||
!titleValue || | ||
!subjectValue || | ||
(titleValue === title && subjectValue === subject) | ||
), | ||
[title, subject, titleValue, subjectValue] | ||
); | ||
|
||
const handleTitleChange = (event: ChangeEvent<HTMLInputElement>) => { | ||
const { value } = event.target; | ||
|
||
setTitleValue(value); | ||
|
||
if (!value.trim()) { | ||
setTitleErrorMessage("프로젝트 이름을 입력해주세요."); | ||
return; | ||
} | ||
|
||
if (value.length > 255) { | ||
setTitleErrorMessage("프로젝트 이름이 너무 깁니다."); | ||
return; | ||
} | ||
|
||
setTitleErrorMessage(""); | ||
}; | ||
|
||
const handleSubjectChange = (event: ChangeEvent<HTMLInputElement>) => { | ||
const { value } = event.target; | ||
|
||
setSubjectValue(value); | ||
|
||
if (!value.trim()) { | ||
setSubjectErrorMessage("프로젝트 주제를 입력해주세요."); | ||
return; | ||
} | ||
|
||
if (value.length > 255) { | ||
setSubjectErrorMessage("프로젝트 주제가 너무 깁니다."); | ||
return; | ||
} | ||
|
||
setSubjectErrorMessage(""); | ||
}; | ||
|
||
const handleSubmit = () => { | ||
emitProjectInfoUpdateEvent({ title: titleValue, subject: subjectValue }); | ||
}; | ||
|
||
useEffect(() => { | ||
setTitleValue(title); | ||
setSubjectValue(subject); | ||
}, [title, subject]); | ||
|
||
return ( | ||
<> | ||
<div className="w-full mb-2"> | ||
<p className="font-bold text-m text-middle-green">프로젝트 설정</p> | ||
</div> | ||
<div className="flex flex-col w-full"> | ||
<InformationInput | ||
label="프로젝트 이름" | ||
inputId="title" | ||
inputValue={titleValue} | ||
errorMessage={titleErrorMessage} | ||
onChange={handleTitleChange} | ||
/> | ||
<InformationInput | ||
label="프로젝트 주제" | ||
inputId="subject" | ||
inputValue={subjectValue} | ||
errorMessage={subjectErrorMessage} | ||
onChange={handleSubjectChange} | ||
/> | ||
<div className="self-end relative w-[4.5rem] h-10"> | ||
{!submitActivated && ( | ||
<div className="absolute w-full h-full rounded-lg opacity-50 bg-slate-500"></div> | ||
)} | ||
|
||
<button | ||
className="w-full h-full text-xs text-white rounded-lg bg-middle-green" | ||
type="button" | ||
disabled={!submitActivated} | ||
onClick={handleSubmit} | ||
> | ||
저장 | ||
</button> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
}; | ||
|
||
export default InformationSettingSection; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import useMemberStore from "../../stores/useMemberStore"; | ||
import { LandingMemberDTO } from "../../types/DTO/landingDTO"; | ||
|
||
interface MemberBlockProps extends LandingMemberDTO {} | ||
|
||
const MemberBlock = ({ username, imageUrl, role }: MemberBlockProps) => { | ||
const myRole = useMemberStore((state) => state.myInfo.role); | ||
|
||
return ( | ||
<div className="flex w-full gap-3"> | ||
<div className="w-[16.25rem] flex gap-3 items-center"> | ||
<img className="w-8 h-8 rounded-full" src={imageUrl} alt={username} /> | ||
<p className="">{username}</p> | ||
</div> | ||
<div className="w-[18.75rem]"> | ||
<p className="">{role}</p> | ||
</div> | ||
<div className="w-[30rem]"> | ||
{myRole === "LEADER" && ( | ||
<button | ||
className="px-2 py-1 text-white rounded w-fit bg-error-red text-xxs" | ||
type="button" | ||
> | ||
프로젝트에서 제거 | ||
</button> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default MemberBlock; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { LandingMemberDTO } from "../../types/DTO/landingDTO"; | ||
import MemberBlock from "./MemberBlock"; | ||
|
||
interface MemberSettingSectionProps { | ||
memberList: LandingMemberDTO[]; | ||
} | ||
|
||
const MemberSettingSection = ({ memberList }: MemberSettingSectionProps) => ( | ||
<div className="mb-5"> | ||
<div className="mb-2"> | ||
<p className="font-bold text-m text-middle-green">멤버 관리</p> | ||
</div> | ||
<div className="flex flex-col gap-2 h-[18.52rem]"> | ||
<div className="flex w-full gap-3 border-b-[2px] text-[1rem] text-dark-gray"> | ||
<p className="w-[16.25rem]">닉네임</p> | ||
<p className="w-[18.75rem]">역할</p> | ||
<p className="w-[30rem]">작업</p> | ||
</div> | ||
<div className="flex flex-col gap-3 overflow-y-auto scrollbar-thin"> | ||
{...memberList.map((member) => <MemberBlock {...member} />)} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
|
||
export default MemberSettingSection; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { ChangeEvent, MouseEventHandler, useState } from "react"; | ||
import useSettingProjectSocket from "../../hooks/pages/setting/useSettingProjectSocket"; | ||
import Closed from "../../assets/icons/closed.svg?react"; | ||
import { Socket } from "socket.io-client"; | ||
|
||
interface ProjectDeleteModalProps { | ||
projectTitle: string; | ||
socket: Socket; | ||
close: () => void; | ||
} | ||
|
||
const ProjectDeleteModal = ({ | ||
projectTitle, | ||
socket, | ||
close, | ||
}: ProjectDeleteModalProps) => { | ||
const [inputValue, setInputValue] = useState(""); | ||
const [confirmed, setConfirmed] = useState(false); | ||
|
||
const { emitProjectDeleteEvent } = useSettingProjectSocket(socket); | ||
|
||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => { | ||
const { value } = event.target; | ||
|
||
setInputValue(value); | ||
|
||
if (value === projectTitle) { | ||
setConfirmed(true); | ||
} else { | ||
setConfirmed(false); | ||
} | ||
}; | ||
|
||
const handleCloseClick: MouseEventHandler< | ||
HTMLButtonElement | HTMLDivElement | ||
> = ({ target, currentTarget }: React.MouseEvent) => { | ||
if (target !== currentTarget) { | ||
return; | ||
} | ||
close(); | ||
}; | ||
|
||
const handleDeleteButtonClick = () => { | ||
if (confirmed) { | ||
emitProjectDeleteEvent(); | ||
} | ||
}; | ||
|
||
return ( | ||
<div | ||
className="fixed top-0 left-0 z-50 flex items-center justify-center w-full h-full bg-black bg-opacity-30" | ||
onClick={handleCloseClick} | ||
> | ||
<div className="px-6 py-7 bg-white rounded-lg w-[23.75rem] h-fit"> | ||
<div className="flex justify-between w-full mb-2"> | ||
<p className="font-bold text-m">|프로젝트 삭제</p> | ||
<button type="button" onClick={close}> | ||
<Closed width={32} height={32} stroke="black" /> | ||
</button> | ||
</div> | ||
<p className="mb-10 text-lg">프로젝트 삭제 후 되돌릴 수 없습니다.</p> | ||
<div className="flex flex-col gap-2"> | ||
<p className="text-sm font-bold select-none"> | ||
삭제하시려면 "{projectTitle}"을(를) 입력해주세요. | ||
</p> | ||
<input | ||
className="w-full px-1 text-sm border rounded border-error-red outline-error-red" | ||
type="text" | ||
value={inputValue} | ||
onChange={handleInputChange} | ||
/> | ||
<div | ||
className={`${ | ||
!confirmed ? "hover:cursor-not-allowed" : "" | ||
} h-7 relative`} | ||
> | ||
{!confirmed && ( | ||
<div className="absolute w-full h-full bg-black rounded opacity-40"></div> | ||
)} | ||
<button | ||
className={`w-full h-full text-sm text-center text-white rounded ${ | ||
!confirmed ? "bg-gray-300" : "bg-error-red" | ||
}`} | ||
type="button" | ||
disabled={!confirmed} | ||
onClick={handleDeleteButtonClick} | ||
> | ||
프로젝트 삭제 | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ProjectDeleteModal; |
Oops, something went wrong.