diff --git a/components/Button.module.scss b/components/Button.module.scss new file mode 100644 index 000000000..ce675d0bf --- /dev/null +++ b/components/Button.module.scss @@ -0,0 +1,49 @@ +@import '@/styles/mixin.scss'; + +%button-base { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 1.6rem; + color: var(--color-white); + text-align: center; + cursor: pointer; + border-radius: 0.8rem; + transition: opacity 300ms ease-in-out; + + &:disabled { + cursor: not-allowed; + background-color: rgba(0, 0, 0, 0.2); + } + + &:enabled:hover { + opacity: 0.9; + } +} + +.primary { + @extend %button-base; + background-image: linear-gradient( + 91deg, + var(--color-primary) 0.12%, + #6ae3fe 101.84% + ); +} + +.large { + height: 3.7rem; + font-size: 1.4rem; + font-weight: 600; + + @include tablet { + height: 5.4rem; + font-size: 1.8rem; + font-weight: 600; + } +} + +.small { + height: 3.7rem; + font-size: 1.4rem; + font-weight: 600; +} diff --git a/components/Button.tsx b/components/Button.tsx new file mode 100644 index 000000000..26dbdf09a --- /dev/null +++ b/components/Button.tsx @@ -0,0 +1,31 @@ +import { ReactNode } from 'react'; +import { MouseEvent } from 'react'; +import styles from '@/components/Button.module.scss'; +import classNames from 'classnames/bind'; +interface ButtonProps { + children: ReactNode; + className?: string; + variant: 'primary'; + size: 'large' | 'small'; + onClick?: (e: MouseEvent) => void; +} +export default function Button({ + children, + className = '', + variant = 'primary', + onClick, + size = 'large', + ...props +}: ButtonProps) { + const cx = classNames.bind(styles); + + return ( + + ); +} diff --git a/components/Container.module.scss b/components/Container.module.scss new file mode 100644 index 000000000..ecee0a004 --- /dev/null +++ b/components/Container.module.scss @@ -0,0 +1,23 @@ +.container { + margin: 0 auto; + width: 100%; + max-width: 120rem; +} + +.header.container { + display: flex; + justify-content: space-between; + margin: 0 auto; + width: 100%; + max-width: 192rem; +} + +.signup.container { + width: 40rem; + margin: 23.8rem auto 0; + @media screen and (max-width: 768px) { + width: 100%; + margin: 12rem auto; + padding: 0 3.2rem; + } +} diff --git a/components/Container.tsx b/components/Container.tsx new file mode 100644 index 000000000..9abb70e9f --- /dev/null +++ b/components/Container.tsx @@ -0,0 +1,13 @@ +import styles from './Container.module.scss'; + +interface ContainerProps { + className?: string; + page?: string; + children?: React.ReactNode; +} + +export default function Container({ page, children }: ContainerProps) { + const classNames = `${styles.container} ${page ? styles[page] : ''}`; + + return
{children}
; +} diff --git a/components/Input.module.scss b/components/Input.module.scss new file mode 100644 index 000000000..c098ccdf4 --- /dev/null +++ b/components/Input.module.scss @@ -0,0 +1,48 @@ +%input-text { + font-size: 1.6rem; + font-weight: 400; + color: var(--color-gray100); +} + +.container { + position: relative; + + input { + width: 100%; + height: 6rem; + padding: 0 1.5rem; + border-radius: 0.8rem; + background-color: var(--color-white); + border: 1px solid var(--color-gray20); + @extend %input-text; + + &[type='password'] { + padding-right: 4.6rem; + } + + &::placeholder { + @extend %input-text; + } + + &:focus { + border: 1px solid var(--color-primary); + } + } +} + +.toggle-eye { + position: absolute; + top: 50%; + right: 0.3rem; + transform: translateY(-50%); + width: 4rem; + height: 4rem; + background: url('../public/assets/icons/eye-on.svg') no-repeat center/ 1.6rem + 1.6rem; + cursor: pointer; +} + +.toggle-eye.active { + background: url('../public/assets/icons/eye-off.svg') no-repeat center/ 1.6rem + 1.6rem; +} \ No newline at end of file diff --git a/components/Input.tsx b/components/Input.tsx new file mode 100644 index 000000000..aa3887371 --- /dev/null +++ b/components/Input.tsx @@ -0,0 +1,44 @@ +import styles from '@/components/Input.module.scss'; +import classNames from 'classnames/bind'; +import { useState, useRef, InputHTMLAttributes, RefObject } from 'react'; + +const cx = classNames.bind(styles); +interface InputRefProps { + inputRef: RefObject; +} + +const ToggleEye = ({ inputRef }: InputRefProps) => { + const [isActive, setActive] = useState(false); + + const handleClick = () => { + setActive(!isActive); + if (inputRef.current) { + inputRef.current.type = isActive ? 'text' : 'password'; + } + }; + + const label = isActive ? '비밀번호 숨기기' : '비밀번호 보이기'; + + const className = cx('toggle-eye', { active: isActive }); + + return ( + + ); +}; + +interface InputProps extends InputHTMLAttributes { + className?: string; +} + +export default function Input({ className = '', ...props }: InputProps) { + const inputRef = useRef(null); + + return ( +
+ + {props.type === 'password' && } +
+ ); +} diff --git a/package-lock.json b/package-lock.json index 8b4150360..e5cb32163 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "fe-weekly-mission", "version": "0.1.0", "dependencies": { + "axios": "^1.6.5", + "classnames": "^2.5.1", "next": "13.5.6", "react": "^18", "react-dom": "^18" @@ -719,6 +721,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -740,6 +747,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -894,6 +911,11 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -917,6 +939,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1003,6 +1036,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1707,6 +1748,25 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -1716,6 +1776,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2595,6 +2668,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2991,6 +3083,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index f51b65277..fc818bcad 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "axios": "^1.6.5", + "classnames": "^2.5.1", "next": "13.5.6", "react": "^18", "react-dom": "^18" diff --git a/pages/index.tsx b/pages/index.tsx index 2de1e67fe..ea2540a9b 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,19 +1,31 @@ -import ButtonLink from '@/components/ButtonLink'; -import SearchForm from '@/components/SearchForm'; -import buttonLinkStyles from '@/components/ButtonLink.module.scss'; -import searchFormStyles from '@/components/SearchForm.module.scss'; +import Button from '@/components/Button'; +import Input from '@/components/Input'; +import styles from '@/styles/Home.module.scss'; export default function Home() { return ( <> - alert('버튼 클릭')} > 로그인 - - - + + + + ); } diff --git a/pages/signup.tsx b/pages/signup.tsx index 03c0f537f..f8fb32b39 100644 --- a/pages/signup.tsx +++ b/pages/signup.tsx @@ -1,3 +1,122 @@ +import Link from 'next/link'; +import Image from 'next/image'; +import Container from '@/components/Container'; +import Button from '@/components/Button'; +import Input from '@/components/Input'; +import styles from '@/styles/SignupPage.module.scss'; +import logoImg from '@/public/assets/images/logo.svg'; +import googleImg from '@/public/assets/images/google.png'; +import kakaoImg from '@/public/assets/images/kakao.png'; +import classNames from 'classnames/bind'; +import { useState, ChangeEvent, FormEvent } from 'react'; +import { useRouter } from 'next/router'; + +const cx = classNames.bind(styles); + export default function SignUpPage() { - return
signup 페이지
; + const [values, setValues] = useState({ + name: '', + email: '', + password: '', + passwordRepeat: '', + }); + + const router = useRouter(); + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + + setValues((prevValues) => ({ + ...prevValues, + [name]: value, + })); + }; + + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + + try { + console.log('회원가입 성공'); + router.push('/folder'); + } catch (error) { + console.error('Error:', error); + } + } + + return ( + +
+

회원가입 페이지

+ + 홈으로 연결된 Linkbrary 로고 + + +
+ 이미 회원이신가요? + + 로그인 하기 + +
+
+ +
+
+
+ + + 이메일 입력해주세요 +
+
+ + + 비밀번호를 입력해주세요 +
+
+ + + 비밀번호를 입력해주세요 +
+
+ +
+ +
+

다른 방식으로 가입하기

+
+ + 구글 + + + 카카오톡 + +
+
+
+ ); } diff --git a/public/assets/icons/eye-off.svg b/public/assets/icons/eye-off.svg new file mode 100644 index 000000000..0670c9ca7 --- /dev/null +++ b/public/assets/icons/eye-off.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/assets/icons/eye-on.svg b/public/assets/icons/eye-on.svg new file mode 100644 index 000000000..5165ab3e9 --- /dev/null +++ b/public/assets/icons/eye-on.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/assets/images/google.png b/public/assets/images/google.png new file mode 100644 index 000000000..c59796fbc Binary files /dev/null and b/public/assets/images/google.png differ diff --git a/public/assets/images/kakao.png b/public/assets/images/kakao.png new file mode 100644 index 000000000..94f7e7518 Binary files /dev/null and b/public/assets/images/kakao.png differ diff --git a/public/assets/images/logo.svg b/public/assets/images/logo.svg new file mode 100644 index 000000000..6536950b1 --- /dev/null +++ b/public/assets/images/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/styles/Home.module.scss b/styles/Home.module.scss index e69de29bb..f043f39cc 100644 --- a/styles/Home.module.scss +++ b/styles/Home.module.scss @@ -0,0 +1,6 @@ +.button { + width: 200px; // 테스트용 +} +.input { + width: 200px; // 테스트용 +} diff --git a/styles/SignupPage.module.scss b/styles/SignupPage.module.scss new file mode 100644 index 000000000..450ff9d63 --- /dev/null +++ b/styles/SignupPage.module.scss @@ -0,0 +1,89 @@ +.header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 3rem; + + .logo { + display: block; + width: 21.2rem; + height: 3.8rem; + margin-bottom: 1.6rem; + } + + .text { + color: #000; + font-size: 1.6rem; + font-weight: 400; + text-align: center; + + .link { + margin-left: 0.8rem; + color: var(--primary-color); + } + } +} + +.form { + margin-bottom: 3.2rem; + + .input-group { + display: flex; + flex-direction: column; + gap: 2.4rem; + margin-bottom: 3rem; + } + + label { + display: block; + margin-bottom: 1.2rem; + font-size: 1.4rem; + } + + .error-message { + display: none; + margin-top: 0.6rem; + font-size: 1.4rem; + color: var(--color-red); + + &.active { + display: block; + } + } + + .button { + width: 100%; + } +} + +.footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.2rem 2.4rem; + border: 1px solid #ccd5e3; + border-radius: 0.8rem; + background-color: var(--color-gray10); + + .title { + font-size: 1.4rem; + font-weight: 400; + } + + .link-group { + display: flex; + align-items: center; + column-gap: 1.6rem; + + .link { + display: block; + width: 4.2rem; + height: 4.2rem; + + img { + width: 100%; + height: 100%; + } + } + } +} diff --git a/styles/colors.scss b/styles/colors.scss new file mode 100644 index 000000000..8d40bebfc --- /dev/null +++ b/styles/colors.scss @@ -0,0 +1,18 @@ +:root { + --color-primary: #6d6afe; + --color-red: #ff5b56; + --color-black: #111322; + --color-white: #ffffff; + + --color-gray100: #373740; + --color-gray60: #9fa6b2; + --color-gray20: #ccd5e3; + --color-gray10: #e7effb; + --color-gray-light: #f5f5f5; + + --color-light-blue: #f0f6ff; + + --color-text-gray: #676767; + --color-text-content-gray: #666666; + --color-text-content-black: #333333; +} diff --git a/styles/globals.scss b/styles/globals.scss index 6fd9615ea..2b962efd6 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -1,78 +1,15 @@ -/* Reset CSS */ -* { - margin: 0; - word-break: keep-all; - box-sizing: border-box; -} - -html, -body { - font-family: 'Pretendard', sans-serif; - font-size: 62.5%; -} - -body { - font-size: 1.6rem; -} - -a { - color: inherit; - text-decoration: none; -} - -ul { - padding: 0; -} - -button, -input, -select, -textarea { - border: 0; - background-color: transparent; - &:focus { - outline: none; - box-shadow: none; - } -} - -/* Global CSS */ -:root { - --primary-color: #6d6afe; - --red-color: #ff5b56; - --white-color: #ffffff; - --black-color: #000000; - --gray-color-100: #373740; - --gray-color-60: #9fa6b2; - --gray-color-20: #ccd5e3; - --gray-color-10: #e7effb; - --gray-color-bg: #f0f6ff; - --blue-color-bg: #edf7ff; -} - -.container { - display: flex; - justify-content: space-between; - margin: 0 auto; - width: 100%; - max-width: 192rem; -} - -/* Responsive Global CSS */ -@media screen and (max-width: 768px) { - .sm-hidden { - display: none; - } -} - -@media screen and (max-width: 1200px) { - .md-hidden { - display: none; - } -} - -@media screen and (min-width: 1200px) { - .lg-hidden { - display: none; - } +@import './reset.scss'; +@import './colors.scss'; +@import './mixin.scss'; + +.visually-hidden { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; } diff --git a/styles/mixin.scss b/styles/mixin.scss new file mode 100644 index 000000000..721cfd6b9 --- /dev/null +++ b/styles/mixin.scss @@ -0,0 +1,24 @@ +@mixin desktop { + @media (min-width: 1200px) { + @content; + } +} + +@mixin tablet { + @media (min-width: 768px) { + @content; + } +} + +@mixin ellipsis($line: 1) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap !important; + + @if $line > 1 { + display: -webkit-box; + -webkit-line-clamp: $line; + white-space: initial !important; + -webkit-box-orient: vertical; + } +} diff --git a/styles/reset.scss b/styles/reset.scss new file mode 100644 index 000000000..38db843ce --- /dev/null +++ b/styles/reset.scss @@ -0,0 +1,40 @@ +@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css'); + +/* Reset CSS */ +* { + margin: 0; + word-break: keep-all; + box-sizing: border-box; +} + +html, +body { + font-family: 'Pretendard', sans-serif; + font-size: 62.5%; +} + +body { + font-size: 1.6rem; +} + +a { + color: inherit; + text-decoration: none; +} + +ul { + padding: 0; +} + +button, +input, +select, +textarea { + border: 0; + background-color: transparent; + font-family: 'Pretendard', sans-serif; + &:focus { + outline: none; + box-shadow: none; + } +}