diff --git a/index.html b/index.html index f828b81..4720b4c 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + xquare infra
diff --git a/package.json b/package.json index d7b459f..9f2aef4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", + "@iconify/react": "^4.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.3", diff --git a/src/assets/Eye.svg b/src/assets/Eye.svg new file mode 100644 index 0000000..0ee5751 --- /dev/null +++ b/src/assets/Eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/EyeClose.svg b/src/assets/EyeClose.svg new file mode 100644 index 0000000..32707b4 --- /dev/null +++ b/src/assets/EyeClose.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Main/firstContainer.tsx b/src/components/Main/firstContainer.tsx index 8cf0cb1..179e8ef 100644 --- a/src/components/Main/firstContainer.tsx +++ b/src/components/Main/firstContainer.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import MainImg from '@/assets/Main.svg'; -import { Button } from '../common/button'; +import { Button } from '../common/Button'; export const FirstContainer = () => { return ( diff --git a/src/components/Team/Tag.tsx b/src/components/Team/Tag.tsx new file mode 100644 index 0000000..a7291ae --- /dev/null +++ b/src/components/Team/Tag.tsx @@ -0,0 +1,56 @@ +import { theme } from '@/style/theme'; +import styled from '@emotion/styled'; + +type TagType = 'club' | 'team' | 'alone' | 'etc'; + +export const Tag = ({ tag }: { tag: TagType }) => { + const tagText = (): string => { + switch (tag) { + case 'club': + return '동아리'; + case 'team': + return '팀 프로젝트'; + case 'alone': + return '개인 프로젝트'; + default: + return '기타'; + } + }; + + return {tagText()}; +}; + +const Wrapper = styled.div<{ tag: TagType }>` + padding: 0 10px; + height: 24px; + font-size: 12px; + background-color: purple; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50px; + color: ${({ tag }) => { + switch (tag) { + case 'club': + return `${theme.color.mainDark2} !important`; + case 'team': + return `#0C288A !important`; + case 'alone': + return `#876900 !important`; + default: + return `${theme.color.gray6} !important`; + } + }}; + background-color: ${({ tag }) => { + switch (tag) { + case 'club': + return `${theme.color.mainLight2} !important`; + case 'team': + return `#ECF5FF !important`; + case 'alone': + return `#FFFBDB !important`; + default: + return `${theme.color.gray2} !important`; + } + }}; +`; diff --git a/src/components/Team/TeamContainer.tsx b/src/components/Team/TeamContainer.tsx new file mode 100644 index 0000000..7642a8c --- /dev/null +++ b/src/components/Team/TeamContainer.tsx @@ -0,0 +1,65 @@ +import { theme } from '@/style/theme'; +import styled from '@emotion/styled'; +import { Tag } from './Tag'; + +type TagType = 'club' | 'team' | 'alone' | 'etc'; + +type TeamType = { + name: string; + admin: string; + deploy: string[]; + tag: TagType; +}; + +export const TeamContainer = ({ name, admin, deploy, tag }: TeamType) => { + return ( + +
+ {name} + +
+
관리자: {admin}
+
+ 배포:  + {deploy.map((element, index) => { + switch (index) { + case 0: + return ( + <> + {element} + {deploy.length > 1 && ', '} + + ); + case 1: + return <>{element} ; + case 2: + return <>{deploy.length > 2 ? <>등 {deploy.length - 2}개 : <>}; + default: + return null; + } + })} +
+
+ ); +}; + +const Wrapper = styled.div` + width: 1120px; + height: 134px; + padding: 20px 40px; + border-radius: 6px; + border: 1px ${theme.color.gray5} solid; + display: flex; + flex-direction: column; + justify-content: space-between; + font-size: 20px; + font-weight: 400; + color: ${theme.color.gray6}; + & :nth-child(1) { + display: flex; + gap: 6px; + align-items: center; + color: ${theme.color.gray8}; + font-weight: 500; + } +`; diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx new file mode 100644 index 0000000..ac93afb --- /dev/null +++ b/src/components/common/AutoComplete.tsx @@ -0,0 +1,48 @@ +import styled from '@emotion/styled'; +import { theme } from '@/style/theme'; +import { css } from '@emotion/react'; + +export const AutoComplete = ({ props }: { props?: string[] }) => { + return ( + <> + {props && props.length >= 1 && ( + + {props.map((element, index) => { + return ( + + {element} + + ); + })} + + )} + + ); +}; + +const Wrapper = styled.div` + width: 134px; + padding: 0 12px; + border-radius: 4px; + border: 1px ${theme.color.gray5} solid; + background-color: ${theme.color.gray1}; +`; + +const Box = styled.div<{ isLast: string }>` + width: 100%; + font-size: 12px; + font-weight: 400; + color: ${theme.color.gray7}; + height: 46px; + display: flex; + justify-content: center; + align-items: center; + ${({ isLast }) => { + return ( + isLast === 'false' && + css` + border-bottom: 1px ${theme.color.gray5} solid; + ` + ); + }}; +`; diff --git a/src/components/common/Input.tsx b/src/components/common/Input.tsx new file mode 100644 index 0000000..4874748 --- /dev/null +++ b/src/components/common/Input.tsx @@ -0,0 +1,81 @@ +import React, { InputHTMLAttributes, useState } from 'react'; +import styled from '@emotion/styled'; +import EyeImg from '@/assets/Eye.svg'; +import EyeCloseImg from '@/assets/EyeClose.svg'; +import { theme } from '@/style/theme'; + +interface InputType extends InputHTMLAttributes { + width: number; + onChange?: (e: React.ChangeEvent) => void; + label?: string; +} + +export const Input = ({ + width, + label, + onChange = () => { + console.log('change!'); + }, + ...props +}: InputType) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + {label && } + + {props.type === 'password' && ( + { + setIsOpen(!isOpen); + }} + > + + + )} + + ); +}; + +const Wrapper = styled.div<{ width: number; label?: string }>` + width: ${({ width }) => `${width}px`}; + height: ${({ label }) => (!!label ? '74px' : '46px')}; + position: relative; +`; + +const Label = styled.label` + width: 100%; + height: 22px; + cursor: default; + font-size: 14px; + font-weight: 400; + color: ${theme.color.gray6}; + display: flex; + align-items: center; + margin-bottom: 6px; + margin-left: 6px; +`; + +const InputBox = styled.input` + border-radius: 4px; + width: 100%; + padding: 0 16px; + height: 46px; + background-color: ${theme.color.gray1}; + border: 1px solid ${theme.color.gray5}; + cursor: pointer; + font-size: 16px; + font-weight: 400; + &::placeholder { + color: ${theme.color.gray5}; + } +`; + +const ViewInput = styled.div` + width: 0px; + height: 0px; + position: absolute; + right: 40px; + bottom: 34px; + cursor: pointer; +`; diff --git a/src/components/common/XButton.tsx b/src/components/common/XButton.tsx index e69de29..854dde0 100644 --- a/src/components/common/XButton.tsx +++ b/src/components/common/XButton.tsx @@ -0,0 +1,63 @@ +import { ReactNode } from 'react'; +import styled from '@emotion/styled'; +import { theme } from '@/style/theme'; +import { css } from '@emotion/react'; + +type ButtonStyleType = 'solid' | 'ghost'; + +type ButtonPropsType = { + buttonStyle: ButtonStyleType; + width: number; + height: number; + children: ReactNode; + onClick: () => void; +}; + +export const XButton = ({ buttonStyle, width, height, children, onClick }: ButtonPropsType) => { + return ( + { + onClick(); + }} + > + {children} + + ); +}; + +const Wrapper = styled.div>` + width: ${({ width }) => width + 'px'}; + height: ${({ height }) => height + 'px'}; + background-color: ${({ buttonStyle }) => (buttonStyle === 'solid' ? theme.color.main : theme.color.gray1)}; + color: ${({ buttonStyle }) => (buttonStyle === 'solid' ? theme.color.gray1 : theme.color.mainDark1)}; + ${({ buttonStyle }) => + buttonStyle === 'ghost' && + css` + border: 1px solid ${theme.color.mainDark1}; + `}; + border-radius: 4px; + font-size: 14px; + font-weight: 700; + display: flex; + justify-content: center; + gap: 6px; + align-items: center; + cursor: pointer; + transition: 0.2s linear; + &:hover { + ${({ buttonStyle }) => + buttonStyle === 'ghost' && + css` + color: ${theme.color.gray1}; + `}; + background-color: ${({ buttonStyle }) => + buttonStyle === 'solid' ? theme.color.mainLight1 : theme.color.mainDark1}; + } + &:active { + transition: 0.05s ease-in-out; + background-color: ${({ buttonStyle }) => (buttonStyle === 'solid' ? theme.color.mainDark1 : theme.color.mainDark2)}; + } +`; diff --git a/src/components/common/button.tsx b/src/components/common/button.tsx index ef53b16..a00a3a5 100644 --- a/src/components/common/button.tsx +++ b/src/components/common/button.tsx @@ -40,7 +40,7 @@ export const Button = ({ ); }; -const Wrapper = styled.div>` +const Wrapper = styled.div>` cursor: pointer; width: ${({ width }) => width + 'px'}; height: ${({ height }) => height + 'px'}; diff --git a/src/components/common/header/index.tsx b/src/components/common/header/index.tsx index b69d387..ba0394e 100644 --- a/src/components/common/header/index.tsx +++ b/src/components/common/header/index.tsx @@ -1,10 +1,14 @@ import styled from '@emotion/styled'; import LogoImg from '@/assets/Logo.svg'; import { useEffect, useState } from 'react'; -import { Button } from '../button'; +import { Button } from '../Button'; +import { useLocation } from 'react-router-dom'; +import { css } from '@emotion/react'; export const Header = () => { const [scroll, setScroll] = useState(0); + const { pathname } = useLocation(); + const _pathname: string = pathname.substring(1); useEffect(() => { const handleScroll = () => { @@ -20,7 +24,7 @@ export const Header = () => { return ( <> - + Xquare Infra @@ -42,13 +46,13 @@ export const Header = () => { 무료로 시작하기 - + ); }; -const Wrapper = styled.div<{ scroll: number }>` +const Wrapper = styled.div<{ scroll: number; pathname: string }>` position: fixed; transition: 0.25s ease-in-out; top: 0; @@ -59,7 +63,13 @@ const Wrapper = styled.div<{ scroll: number }>` justify-content: space-between; padding: 0 90px 0 90px; align-items: center; + background-color: ${({ pathname }) => pathname !== '' && 'white'}; color: ${({ scroll }) => (scroll === 0 ? 'white' : scroll >= 408 ? 'rgba(0,0,0,0)' : 'black')}; + ${({ pathname }) => + pathname !== '' && + css` + border-bottom: 1px #dddddd solid; + `}; z-index: 999; `; diff --git a/src/components/common/sidebar/index.tsx b/src/components/common/sidebar/index.tsx new file mode 100644 index 0000000..e50dd6c --- /dev/null +++ b/src/components/common/sidebar/index.tsx @@ -0,0 +1,126 @@ +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { Icon } from '@iconify/react'; + +export const Sidebar = () => { + const [isOpen, seIsOpen] = useState(false); + + return ( + + + a + b + + { + seIsOpen(!isOpen); + }} + > +
+
+ +
+ 사이드바 축소 +
+
+
+ ); +}; + +const Wrapper = styled.div<{ isOpen: boolean }>` + width: ${({ isOpen }) => (isOpen ? '260px' : '80px')}; + left: 0; + transition: 0.4s ease-in-out; + height: calc(100vh - 80px); + background-color: white; + border-right: 1px #dddddd solid; + position: absolute; + z-index: 10; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 30px 0 30px 0; +`; + +const Container = styled.div` + display: flex; + flex-direction: column; +`; + +const Menu = styled.div<{ isOpen: boolean }>` + cursor: pointer; + width: ${({ isOpen }) => (isOpen ? '240px' : '60px')}; + padding: 10px; + height: 60px; + border-radius: 8px; + display: flex; + align-items: center; + overflow: hidden; + transition: + background-color 0.1s linear, + width 0.4s ease-in-out; + &:hover { + background-color: #f0e6ff; + } + > div { + width: 240px; + height: 40px; + display: flex; + align-items: center; + > div { + width: 40px; + height: 40px; + flex: 0; + } + > span { + font-size: 20px; + width: 180px; + transition: 0.4s ease-in-out; + overflow: hidden; + word-break: keep-all; + white-space: nowrap; + text-align: center; + } + } +`; + +const BottomMenu = styled.div<{ isOpen: boolean }>` + cursor: pointer; + width: ${({ isOpen }) => (isOpen ? '240px' : '60px')}; + padding: 10px; + height: 60px; + border-radius: 8px; + display: flex; + align-items: center; + overflow: hidden; + transition: + background-color 0.1s linear, + width 0.4s ease-in-out; + &:hover { + background-color: #f0e6ff; + } + > div { + width: 240px; + height: 40px; + display: flex; + align-items: center; + > div { + width: 40px; + height: 40px; + flex: 0; + transition: 0.4s ease-in-out; + ${({ isOpen }) => isOpen && `transform: rotate(-180deg)`}; + } + > span { + font-size: 20px; + width: 180px; + transition: 0.4s ease-in-out; + overflow: hidden; + word-break: keep-all; + white-space: nowrap; + text-align: center; + } + } +`; diff --git a/src/pages/Main.tsx b/src/pages/Main.tsx index acd2b41..5961137 100644 --- a/src/pages/Main.tsx +++ b/src/pages/Main.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; -import { FirstContainer } from '@/components/Main/firstContainer'; -import { SecondContainer } from '@/components/Main/secondContainer'; -import { ThirdContainer } from '@/components/Main/thirdContainer'; -import { Button } from '@/components/common/button'; +import { FirstContainer } from '@/components/Main/FirstContainer'; +import { SecondContainer } from '@/components/Main/SecondContainer'; +import { ThirdContainer } from '@/components/Main/ThirdContainer'; +import { Button } from '@/components/common/Button'; export const Main = () => { return ( diff --git a/src/pages/Team/Create.tsx b/src/pages/Team/Create.tsx new file mode 100644 index 0000000..a46d05a --- /dev/null +++ b/src/pages/Team/Create.tsx @@ -0,0 +1,56 @@ +import styled from '@emotion/styled'; +import { Sidebar } from '@/components/common/sidebar'; +import { Input } from '@/components/common/Input'; + +export const TeamCreate = () => { + return ( + + + + + 팀 생성 + +
+ + +
+
+
+ ); +}; + +const Wrapper = styled.div` + margin-top: 80px; + margin-left: 60px; + width: 100%; + display: flex; +`; + +const Container = styled.div` + width: 100%; + height: calc(100vh - 80px); + display: flex; + flex-direction: column; + align-items: center; +`; + +const TitleContainer = styled.div` + width: 1120px; + margin-top: 80px; +`; + +const Title = styled.div` + font-size: 30px; + font-weight: 600; + color: #202020; +`; + +const Form = styled.div` + margin-top: 56px; + width: 1120px; + display: flex; + flex-direction: column; + align-items: start; + justify-content: start; + gap: 24px; +`; diff --git a/src/pages/Team/index.tsx b/src/pages/Team/index.tsx index b5a4922..276be44 100644 --- a/src/pages/Team/index.tsx +++ b/src/pages/Team/index.tsx @@ -1,10 +1,160 @@ import styled from '@emotion/styled'; +import { Sidebar } from '@/components/common/sidebar'; +import { XButton } from '@/components/common/XButton'; +import { Icon } from '@iconify/react'; +import { theme } from '@/style/theme'; +import { TeamContainer } from '@/components/Team/TeamContainer'; + +type TagType = 'club' | 'team' | 'alone' | 'etc'; + +type TeamType = { + name: string; + admin: string; + deploy: string[]; + tag: TagType; +}; + +const dummy: TeamType[] = [ + { + name: '에일리언즈 (Team-aliens)', + admin: '김은빈', + deploy: ['DMS-Backend', 'DMS', 'DMS-admin', 'DMS-admin-front', 'DMS-auth'], + tag: 'team', + }, + { + name: '에일리언즈 (Team-aliens)', + admin: '김은빈', + deploy: ['DMS-Backend', 'DMS', 'DMS-admin', 'DMS-admin-front', 'DMS-auth'], + tag: 'team', + }, + { + name: '에일리언즈 (Team-aliens)', + admin: '김은빈', + deploy: ['DMS-Backend', 'DMS', 'DMS-admin', 'DMS-admin-front', 'DMS-auth'], + tag: 'team', + }, +]; export const Team = () => { - return hello; + return ( + + + + + + 프로젝트를 개발하고 관리할 팀을 정의해보세요. + + +
+ + +
+ { + console.log('click!!'); + }} + > + 팀 등록 + +
+ + {dummy.length > 0 ? ( + dummy.map((element, index) => { + return ( + + ); + }) + ) : ( + 아직 생성하거나 속한 팀이 없습니다! + )} + +
+
+ ); }; const Wrapper = styled.div` margin-top: 80px; + margin-left: 60px; width: 100%; + display: flex; +`; + +const Container = styled.div` + width: 100%; + height: calc(100vh - 80px); + display: flex; + flex-direction: column; + align-items: center; +`; + +const TitleContainer = styled.div` + width: 1120px; + margin-top: 80px; +`; + +const Title = styled.div` + font-size: 30px; + font-weight: 600; + color: #202020; +`; + +const Describtion = styled.div` + font-size: 24px; + font-weight: 100; + color: #202020; +`; + +const UtilContainer = styled.div` + width: 1120px; + height: 50px; + margin: 30px 0 30px 0; + display: flex; + justify-content: space-between; + & > div:nth-child(1) { + & > svg { + position: relative; + right: 24px; + top: 6px; + cursor: pointer; + } + } +`; + +const SearchBar = styled.input` + width: 312px; + height: 50px; + border-bottom: 1px ${theme.color.gray5} solid; + font-size: 14px; + & ::placeholder { + color: ${theme.color.gray2}; + } +`; + +const TipBox = styled.div` + width: 1120px; + height: 120px; + color: ${theme.color.gray5}; + border: 1px ${theme.color.gray5} solid; + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; +`; + +const ContainerWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: start; + gap: 14px; `; diff --git a/src/router/router.tsx b/src/router/router.tsx index 880f667..5f62b61 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -2,6 +2,7 @@ import { createBrowserRouter } from 'react-router-dom'; import { Layout } from './layout'; import { Main } from '../pages/Main'; import { Team } from '@/pages/Team'; +import { TeamCreate } from '@/pages/Team/Create'; export const Router = createBrowserRouter([ { @@ -9,7 +10,13 @@ export const Router = createBrowserRouter([ element: , children: [ { path: '', element:
}, - { path: 'team', children: [{ index: true, element: }] }, + { + path: 'team', + children: [ + { index: true, element: }, + { path: 'create', element: }, + ], + }, ], }, ]); diff --git a/src/style/GlobalStyle.tsx b/src/style/GlobalStyle.tsx index 3fe1906..f60857d 100644 --- a/src/style/GlobalStyle.tsx +++ b/src/style/GlobalStyle.tsx @@ -29,6 +29,13 @@ const style = css` src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff'); } + @font-face { + font-family: 'Pretendard'; + font-weight: 100; + font-style: normal; + src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Thin.woff') format('woff'); + } + * { margin: 0; padding: 0; @@ -39,6 +46,10 @@ const style = css` font-style: normal; font-family: 'Pretendard', sans-serif; } + + body { + overflow-x: hidden; + } `; export const GlobalStyle = () => { diff --git a/src/style/theme.ts b/src/style/theme.ts new file mode 100644 index 0000000..1a492c4 --- /dev/null +++ b/src/style/theme.ts @@ -0,0 +1,33 @@ +const color = { + //main + main: '#9650FA', + mainDark1: '#6D1BE0', + mainDark2: '#420399', + mainLight1: '#B885FF', + mainLight2: '#F0E6FF', + + //info + infoLight: '#D6FFD6', + infoDark1: '#35BF3B', + infoDark2: '#096407', + + //gray + gray1: '#FFFFFF', + gray2: '#F9F9F9', + gray3: '#EEEEEE', + gray4: '#DDDDDD', + gray5: '#999999', + gray6: '#555555', + gray7: '#343434', + gray8: '#121212', + gray9: '#000000', + + //error + errorLight: '#FFD3D3', + errorDark1: '#C23535', + errorDark2: '#852424', +} as const; + +export const theme = { + color, +} as const; diff --git a/tsconfig.json b/tsconfig.json index 29a43f6..0546665 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "allowSyntheticDefaultImports": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true }, "include": ["src", "src/custom.d.ts"] -} \ No newline at end of file +}