Skip to content

Commit

Permalink
Merge pull request #335 from boostcampwm2023/feature/setting-page
Browse files Browse the repository at this point in the history
feat: 세팅 페이지, 프로젝트 제목, 주제 수정 기능 구현
  • Loading branch information
surinkwon authored Sep 18, 2024
2 parents 4c7783b + b96ef81 commit c9aa172
Show file tree
Hide file tree
Showing 20 changed files with 579 additions and 20 deletions.
3 changes: 2 additions & 1 deletion frontend/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import UnfinishedStoryPage from "./pages/backlog/UnfinishedStoryPage";
import BacklogPage from "./pages/backlog/BacklogPage";
import FinishedStoryPage from "./pages/backlog/FinishedStoryPage";
import EpicPage from "./pages/backlog/EpicPage";
import SettingPage from "./pages/setting/SettingPage";

type RouteType = "PRIVATE" | "PUBLIC";

Expand Down Expand Up @@ -96,7 +97,7 @@ const router = createBrowserRouter([
element: <BacklogPage />,
},
{ path: ROUTER_URL.SPRINT, element: <div>sprint Page</div> },
{ path: ROUTER_URL.SETTINGS, element: <div>setting Page</div> },
{ path: ROUTER_URL.SETTINGS, element: <SettingPage /> },
],
},
]),
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/backlog/BacklogHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const BacklogHeader = () => {
return (
<header className="flex items-baseline justify-between">
<div className="flex items-baseline gap-2">
<h1 className="text-m text-dark-green">{TAB_TITLE[lastPath]} 백로그</h1>
<h1 className="font-bold text-m text-middle-green">
{TAB_TITLE[lastPath]} 백로그
</h1>
<p className="">우선순위 내림차순</p>
</div>
<div className="flex gap-1">
Expand Down
20 changes: 14 additions & 6 deletions frontend/src/components/landing/member/LandingMember.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,20 @@ const LandingMember = ({ projectTitle }: LandingMemberProps) => {
/>
<div className="flex justify-between text-white">
<p className="text-xs font-bold">| 함께하는 사람들</p>
<button
className="text-xxs hover:underline"
onClick={handleInviteButtonClick}
>
초대링크 복사
</button>
{myInfo.role === "LEADER" && (
<div className="flex gap-1">
<button
className="text-xxs hover:underline"
onClick={handleInviteButtonClick}
>
초대링크 복사
</button>
<span>|</span>
<button className="text-xxs hover:underline" onClick={() => {}}>
링크 변경
</button>
</div>
)}
</div>
{memberList.map((memberData: LandingMemberDTO) => (
<UserBlock {...memberData} key={memberData.id} />
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/setting/InformationInput.tsx
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 frontend/src/components/setting/InformationSettingSection.tsx
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;
32 changes: 32 additions & 0 deletions frontend/src/components/setting/MemberBlock.tsx
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;
26 changes: 26 additions & 0 deletions frontend/src/components/setting/MemberSettingSection.tsx
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;
97 changes: 97 additions & 0 deletions frontend/src/components/setting/ProjectDeleteModal.tsx
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;
Loading

0 comments on commit c9aa172

Please sign in to comment.