Skip to content

Commit b632deb

Browse files
authored
Merge pull request #259 from boostcampwm2023/feat/landing/memberUI
feat : Landing Page - ProjectUI, SprintUI, MemberUI 구현
2 parents de58f47 + e65c476 commit b632deb

40 files changed

+510
-55
lines changed

frontend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"msw": "^1.1.0",
4141
"postcss": "^8.4.35",
4242
"react-test-renderer": "^18.2.0",
43+
"tailwind-scrollbar": "^3.1.0",
4344
"tailwindcss": "^3.4.1",
4445
"ts-jest": "^29.1.2",
4546
"typescript": "^5.2.2",

frontend/src/AppRouter.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import GlobalErrorBoundary from "./GlobalErrorBoundary";
1717
import PrivateRoute from "./components/common/route/PrivateRoute";
1818
import PublicRoute from "./components/common/route/PublicRoute";
1919
import MainPage from "./pages/main/MainPage";
20+
import LandingPage from "./pages/landing/LandingPage";
2021

2122
type RouteType = "PRIVATE" | "PUBLIC";
2223

@@ -72,7 +73,7 @@ const router = createBrowserRouter([
7273
path: ROUTER_URL.MAIN,
7374
element: <MainPage />,
7475
children: [
75-
{ index: true, element: <div>landing page</div> },
76+
{ index: true, element: <LandingPage /> },
7677
{ path: ROUTER_URL.BACKLOG, element: <div>backlog Page</div> },
7778
{ path: ROUTER_URL.SPRINT, element: <div>sprint Page</div> },
7879
{ path: ROUTER_URL.SETTINGS, element: <div>setting Page</div> },

frontend/src/apis/api/loginAPI.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
GithubOauthUrlDTO,
77
RefreshDTO,
88
TempIdTokenResponse,
9-
} from "../../types/authDTO";
9+
} from "../../types/DTO/authDTO";
1010
import { useNavigate } from "react-router-dom";
1111
import { authAPI, setAccessToken } from "../utils/authAPI";
1212

frontend/src/apis/api/signupAPI.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { API_URL } from "../../constants/path";
22
import { baseAPI } from "../utils";
3-
import { SignupDTO } from "../../types/authDTO";
3+
import { SignupDTO } from "../../types/DTO/authDTO";
44
import { setAccessToken } from "../utils/authAPI";
55

66
interface SignupParams extends SignupDTO {
@@ -25,12 +25,7 @@ export const getGithubUsername = async (tempIdToken: string) => {
2525
}
2626
};
2727

28-
export const postSignup = async ({
29-
tempIdToken,
30-
username,
31-
position,
32-
techStack,
33-
}: SignupParams) => {
28+
export const postSignup = async ({ tempIdToken, username, position, techStack }: SignupParams) => {
3429
const response = await baseAPI(API_URL.SIGN_UP, {
3530
method: "post",
3631
data: { username, position, techStack },

frontend/src/apis/utils/authAPI.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios";
22
import { API_URL, BASE_URL } from "../../constants/path";
3-
import { RefreshDTO } from "../../types/authDTO";
3+
import { RefreshDTO } from "../../types/DTO/authDTO";
44

55
let accessToken: string | undefined;
66

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const ProfileImage = ({ imageUrl, pxSize }: { imageUrl: string; pxSize: number }) => {
2+
return (
3+
<div
4+
className="ml-[0.46875rem] rounded-full overflow-hidden"
5+
style={{ height: pxSize, width: pxSize }}
6+
>
7+
<img src={imageUrl} alt="프로필 이미지 사진" />
8+
</div>
9+
);
10+
};
11+
12+
export default ProfileImage;

frontend/src/components/common/route/PrivateRoute.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const PrivateRoute = (): React.ReactElement => {
1616
showBoundary(result);
1717
}
1818
});
19-
}, [checkAuthentication]);
19+
});
2020

2121
return loadingState ? <RouteLoading /> : <Outlet />;
2222
};

frontend/src/components/common/route/PublicRoute.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const PublicRoute = () => {
1818
setAuthenticated(false);
1919
}
2020
});
21-
}, [checkAuthentication]);
21+
});
2222

2323
return loadingState ? (
2424
<RouteLoading />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { DEFAULT_MEMBER } from "../../constants/projects";
2+
import useDropdown from "../../hooks/common/dropdown/useDropdown";
3+
import { LandingMemberDTO } from "../../types/DTO/landingDTO";
4+
import UserBlock from "./UserBlock";
5+
6+
const LandingMember = ({ member }: { member: LandingMemberDTO[] }) => {
7+
const { Dropdown, selectedOption } = useDropdown({
8+
placeholder: "내 상태",
9+
options: ["접속 중", "부재 중", "자리비움"],
10+
defaultOption: "접속 중",
11+
});
12+
13+
const { imageUrl, username } = JSON.parse(
14+
window.localStorage.getItem("member") ?? DEFAULT_MEMBER
15+
);
16+
17+
const getUserState = (state: string): "on" | "off" | "away" => {
18+
switch (state) {
19+
case "접속 중":
20+
return "on";
21+
case "부재 중":
22+
return "off";
23+
default:
24+
return "away";
25+
}
26+
};
27+
28+
return (
29+
<div className="w-full shadow-box rounded-lg bg-gradient-to-tr to-light-green-linear-from from-light-green py-6 px-6 overflow-y-scroll scrollbar-thin scrollbar-thumb-light-green scrollbar-track-transparent scrollbar-thumb-rounded-full">
30+
<div className="flex flex-col gap-3">
31+
<div className="flex justify-between">
32+
<p className="text-white text-xs font-bold">| 내 상태</p>
33+
<div className="h-fit" />
34+
<Dropdown
35+
buttonClassName="flex justify-between items-center w-[6rem] h-6 pl-5 text-white text-xxxs bg-middle-green pr-3 rounded-md"
36+
containerClassName="w-[6rem] bg-white rounded-b-lg overflow-hidden"
37+
itemClassName="w-full text-xxxs text-center font-semibold py-2 hover:bg-middle-green hover:text-white hover:font-semibold"
38+
iconSize="w-[12px] h-[12px]"
39+
/>
40+
</div>
41+
<UserBlock
42+
{...{ imageUrl, username, status: getUserState(selectedOption) }}
43+
/>
44+
<div className="flex justify-between text-white">
45+
<p className="text-xs font-bold">| 함께하는 사람들</p>
46+
<button className="text-xxs hover:underline">초대링크 복사</button>
47+
</div>
48+
{member.map((memberData: LandingMemberDTO) => {
49+
return <UserBlock {...memberData} />;
50+
})}
51+
</div>
52+
</div>
53+
);
54+
};
55+
56+
export default LandingMember;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { LandingProjectDTO } from "../../types/DTO/landingDTO";
2+
import formatDate from "../../utils/formatDate";
3+
import LandingProjectLink from "./LandingProjectLink";
4+
5+
interface LandingProjectProps {
6+
project: LandingProjectDTO;
7+
projectId: string;
8+
}
9+
10+
const LandingProject = ({ project, projectId }: LandingProjectProps) => {
11+
return (
12+
<div className="w-full p-6 flex flex-col justify-between shadow-box rounded-lg">
13+
<div className="flex justify-between items-baseline text-middle-green font-bold">
14+
<p className="text-xl">| {project.title}</p>
15+
<p className="text-xs">{formatDate(project.createdAt)}</p>
16+
</div>
17+
<div className="text-xs">{project.subject}</div>
18+
<div className="flex justify-between">
19+
<LandingProjectLink projectId={projectId} type="BACKLOG" />
20+
<LandingProjectLink projectId={projectId} type="SPRINT" />
21+
<LandingProjectLink projectId={projectId} type="SETTINGS" />
22+
</div>
23+
</div>
24+
);
25+
};
26+
27+
export default LandingProject;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Link } from "react-router-dom";
2+
import { LANDING_PROJECT_LINK } from "../../constants/landing";
3+
import { LINK_URL } from "../../constants/path";
4+
5+
interface LandingProjectLinkProps {
6+
projectId: string;
7+
type: "BACKLOG" | "SPRINT" | "SETTINGS";
8+
}
9+
10+
const LandingProjectLink = ({ projectId, type }: LandingProjectLinkProps) => {
11+
const { color, text, Icon } = LANDING_PROJECT_LINK[type];
12+
return (
13+
<Link
14+
to={LINK_URL.BACKLOG(projectId)}
15+
className={`w-[8.75rem] h-[5rem] rounded-lg flex justify-center gap-2 items-center ${color} hover:shadow-button`}
16+
>
17+
<Icon height={36} width={36} fill="#FFFFFF" />
18+
<div className="flex flex-col items-center gap-0 text-white text-[1rem] font-semibold">
19+
<p>{text}</p>
20+
<p>바로가기</p>
21+
</div>
22+
</Link>
23+
);
24+
};
25+
26+
export default LandingProjectLink;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { LandingSprintDTO } from "../../types/DTO/landingDTO";
2+
import diffBetweenDate from "../../utils/diffBetweenDate";
3+
import formatDate from "../../utils/formatDate";
4+
import LandingSprintBar from "./LandingSprintBar";
5+
6+
const LandingSprint = ({ sprint }: { sprint: LandingSprintDTO | null }) => {
7+
return (
8+
<div className="w-full shadow-box rounded-lg p-6 flex flex-col justify-between">
9+
<p className="text-l text-middle-green font-bold">| 스프린트 정보</p>
10+
{sprint ? (
11+
<>
12+
<LandingSprintBar
13+
start={formatDate(sprint.startDate).slice(2)}
14+
end={formatDate(sprint.endDate).slice(2)}
15+
displayNum={diffBetweenDate(sprint.endDate, new Date().toISOString())}
16+
percent={Number(
17+
(
18+
diffBetweenDate(new Date().toISOString(), sprint.startDate) /
19+
diffBetweenDate(sprint.endDate, sprint.startDate)
20+
).toFixed(2)
21+
)}
22+
type="SPRINT"
23+
/>
24+
<LandingSprintBar
25+
start={`${sprint.completedTask} Task`}
26+
end={`${sprint.totalTask} Task`}
27+
displayNum={Math.ceil((100 * sprint.completedTask) / sprint.totalTask)}
28+
percent={Number((sprint.completedTask / sprint.totalTask).toFixed(2))}
29+
type="TOTAL"
30+
/>
31+
<LandingSprintBar
32+
start={`${sprint.myCompletedTask} Task`}
33+
end={`${sprint.myTotalTask} Task`}
34+
displayNum={Math.ceil((100 * sprint.myCompletedTask) / sprint.myTotalTask)}
35+
percent={Number((sprint.myCompletedTask / sprint.myTotalTask).toFixed(2))}
36+
type="PERSONAL"
37+
/>
38+
</>
39+
) : (
40+
<div className="h-full flex justify-center items-center">스프린트 정보가 없습니다</div>
41+
)}
42+
</div>
43+
);
44+
};
45+
46+
export default LandingSprint;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { LANDING_SPRINT_BAR } from "../../constants/landing";
2+
3+
interface LandingSprintBarProps {
4+
start: string;
5+
end: string;
6+
displayNum: number;
7+
percent: number;
8+
type: "SPRINT" | "TOTAL" | "PERSONAL";
9+
}
10+
11+
const LandingSprintBar = ({ start, end, displayNum, percent, type }: LandingSprintBarProps) => {
12+
const { color, text, bgColor, display } = LANDING_SPRINT_BAR[type];
13+
return (
14+
<div className={`flex flex-col ${color}`}>
15+
<p className="text-base font-bold">{text}</p>
16+
<div className="flex justify-between">
17+
<p className="text-m font-bold">{display(displayNum)}</p>
18+
<div className="w-[13.125rem] flex flex-col">
19+
<div className="flex justify-between text-black text-base font-bold">
20+
<p>{start}</p>
21+
<p>{end}</p>
22+
</div>
23+
<div className="w-full h-3 flex bg-light-gray rounded-full">
24+
<div
25+
className={`${bgColor} rounded-full`}
26+
style={{ width: percent <= 1 ? 210 * percent : 210 }}
27+
/>
28+
</div>
29+
</div>
30+
</div>
31+
</div>
32+
);
33+
};
34+
35+
export default LandingSprintBar;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { USER_STATE_DISPLAY } from "../../constants/landing";
2+
import ProfileImage from "../common/ProfileImage";
3+
4+
interface UserBlockProps {
5+
imageUrl: string;
6+
username: string;
7+
status: "on" | "off" | "away";
8+
}
9+
10+
const UserStateDisplay = ({ status }: { status: "on" | "off" | "away" }) => {
11+
const { bgColor, text } = USER_STATE_DISPLAY[status];
12+
return (
13+
<div className="flex gap-2 items-center w-[4.0625rem]">
14+
<div className={`w-3 h-3 rounded-full ${bgColor}`} />
15+
<p className="text-xxxs font-semibold">{text}</p>
16+
</div>
17+
);
18+
};
19+
20+
const UserBlock = ({ imageUrl, username, status }: UserBlockProps) => {
21+
return (
22+
<div className="w-full flex justify-between items-center bg-white rounded-lg p-3 shadow-box">
23+
<ProfileImage imageUrl={imageUrl} pxSize={40} />
24+
<p className="text-xs font-bold text-middle-green">{username}</p>
25+
<UserStateDisplay status={status} />
26+
</div>
27+
);
28+
};
29+
30+
export default UserBlock;

frontend/src/components/main/PageIcon.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SVGReactElement } from "../../types/common";
1+
import { SVGReactElement } from "../../types/common/common";
22
import { Link } from "react-router-dom";
33
import ChevronRightIcon from "../../assets/icons/chevron-right.svg?react";
44

frontend/src/components/main/PageLinkIcons.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import BacklogIcon from "../../assets/icons/backlog.svg?react";
44
import SprintIcon from "../../assets/icons/sprint.svg?react";
55
import SettingIcon from "../../assets/icons/settings.svg?react";
66
import { LINK_URL } from "../../constants/path";
7-
import { ProjectSidebarProps } from "../../types/main";
7+
import { ProjectSidebarProps } from "../../types/common/main";
88

99
const PageLinkIcons = ({ pathname, projectId }: ProjectSidebarProps) => {
1010
return (

frontend/src/components/main/ProjectSidebar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ProjectSidebarProps } from "../../types/main";
1+
import { ProjectSidebarProps } from "../../types/common/main";
22
import PageLinkIcons from "./PageLinkIcons";
33
import UtilIcons from "./UtilIcons";
44

frontend/src/components/main/UtilIcons.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { LINK_URL, ROUTER_URL } from "../../constants/path";
22
import { DEFAULT_MEMBER } from "../../constants/projects";
3-
import { ProjectSidebarProps } from "../../types/main";
3+
import { ProjectSidebarProps } from "../../types/common/main";
44
import PageIcon from "./PageIcon";
55
import PageOutIcon from "../../assets/icons/pageout.svg?react";
66
import NotificationIcon from "../../assets/icons/notifications.svg?react";
77
import MemberIcon from "../../assets/icons/member.svg?react";
8+
import ProfileImage from "../common/ProfileImage";
89

910
const UtilIcons = ({ pathname, projectId }: ProjectSidebarProps) => {
1011
const { imageUrl } = JSON.parse(window.localStorage.getItem("member") ?? DEFAULT_MEMBER);
@@ -24,9 +25,7 @@ const UtilIcons = ({ pathname, projectId }: ProjectSidebarProps) => {
2425
to={ROUTER_URL.PROJECTS}
2526
pageName="내 프로젝트"
2627
/>
27-
<div className="w-10 h-10 ml-[0.46875rem] rounded-full overflow-hidden">
28-
<img src={imageUrl} alt="프로필 이미지 사진" />
29-
</div>
28+
<ProfileImage imageUrl={imageUrl} pxSize={40} />
3029
</div>
3130
);
3231
};

frontend/src/components/projects/ProjectCard.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ProjectDTO } from "../../types/projectDTO";
1+
import { ProjectDTO } from "../../types/DTO/projectDTO";
22
import formatDate from "../../utils/formatDate";
33

44
interface ProjectCardProps {

frontend/src/components/projects/ProjectCreateInput.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useRef, useState } from "react";
22
import NextStepButton from "../common/NextStepButton";
33
import { PROJECT_NAME_INPUT_ID } from "../../constants/projects";
4-
import { Step } from "../../types/common";
4+
import { Step } from "../../types/common/common";
55

66
interface ProjectCreateInputProps {
77
elementId: string;

frontend/src/components/projects/ProjectCreateMainSection.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
PROJECT_NAME_INPUT_ID,
66
PROJECT_SUBJECT_INPUT_ID,
77
} from "../../constants/projects";
8-
import { Step } from "../../types/common";
8+
import { Step } from "../../types/common/common";
99
import usePostCreateProject from "../../hooks/queries/project/usePostCreateProject";
1010

1111
interface ProjectCreateMainSectionProps {

frontend/src/components/projects/ProjectCreateSideBar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Step } from "../../types/common";
1+
import { Step } from "../../types/common/common";
22

33
interface ProjectCreateSideBarProps {
44
currentStep: Step;

0 commit comments

Comments
 (0)