diff --git a/.gitignore b/.gitignore index bfe35795a..4ba33ce39 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,4 @@ yarn-error.log* next-env.d.ts # spell -cspell.json - -# signin -src/pages/signin.tsx \ No newline at end of file +cspell.json \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f1f8aee93..55bad7357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "next": "13.5.6", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.48.2", "react-tiny-popover": "^8.0.4", "styled-components": "^6.1.1", "timeago": "^1.6.7", @@ -3093,6 +3094,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.48.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.48.2.tgz", + "integrity": "sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 57faa8c6b..4bc18dddd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "next": "13.5.6", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.48.2", "react-tiny-popover": "^8.0.4", "styled-components": "^6.1.1", "timeago": "^1.6.7", diff --git a/public/assets/images/screenshot/hero.png b/public/assets/images/screenshot/hero.png new file mode 100644 index 000000000..11dc528fb Binary files /dev/null and b/public/assets/images/screenshot/hero.png differ diff --git a/public/assets/images/screenshot/image1.png b/public/assets/images/screenshot/image1.png new file mode 100644 index 000000000..f6355852b Binary files /dev/null and b/public/assets/images/screenshot/image1.png differ diff --git a/public/assets/images/screenshot/image2.png b/public/assets/images/screenshot/image2.png new file mode 100644 index 000000000..b9a3c674a Binary files /dev/null and b/public/assets/images/screenshot/image2.png differ diff --git a/public/assets/images/screenshot/image3.png b/public/assets/images/screenshot/image3.png new file mode 100644 index 000000000..986a39679 Binary files /dev/null and b/public/assets/images/screenshot/image3.png differ diff --git a/public/assets/images/screenshot/image4.png b/public/assets/images/screenshot/image4.png new file mode 100644 index 000000000..762bdc960 Binary files /dev/null and b/public/assets/images/screenshot/image4.png differ diff --git a/src/api/fetch.ts b/src/api/apiRequest.ts similarity index 53% rename from src/api/fetch.ts rename to src/api/apiRequest.ts index fa4b6ed4d..fa13136a7 100644 --- a/src/api/fetch.ts +++ b/src/api/apiRequest.ts @@ -1,12 +1,12 @@ import axios, { AxiosRequestConfig } from 'axios'; const api = axios.create({ - baseURL: 'https://bootcamp-api.codeit.kr/api/', + baseURL: process.env.NEXT_PUBLIC_BASE_URL, }); -const fetch = async (options: AxiosRequestConfig) => { +const apiRequest = async (options: AxiosRequestConfig) => { const response = await api({ ...options }); return response; }; -export default fetch; +export default apiRequest; diff --git a/src/components/AddLinkForm/AddLinkForm.tsx b/src/components/AddLinkForm/AddLinkForm.tsx index 1fd9b190d..d7945ed83 100644 --- a/src/components/AddLinkForm/AddLinkForm.tsx +++ b/src/components/AddLinkForm/AddLinkForm.tsx @@ -7,7 +7,7 @@ interface Props { isScrolled: boolean; } -function AddLinkForm({ isScrolled }: Props) { +const AddLinkForm = ({ isScrolled }: Props) => { const [value, setValue] = useState(''); const [showFooter, setShowFooter] = useState(false); const footerRef = useContext(FooterRefContext); @@ -18,11 +18,7 @@ function AddLinkForm({ isScrolled }: Props) { useEffect(() => { const observer = new IntersectionObserver(([entry]) => { - if (entry.isIntersecting) { - setShowFooter(true); - } else { - setShowFooter(false); - } + setShowFooter(entry.isIntersecting); }); const currentRef = footerRef?.current; @@ -53,6 +49,6 @@ function AddLinkForm({ isScrolled }: Props) { ); -} +}; export default AddLinkForm; diff --git a/src/components/CardList/Card/Card.tsx b/src/components/CardList/Card/Card.tsx index c4cf57809..56002fca5 100644 --- a/src/components/CardList/Card/Card.tsx +++ b/src/components/CardList/Card/Card.tsx @@ -1,7 +1,7 @@ import * as S from './Card.style'; import { formatDate, formatTimeDiff } from '@utils/format'; import { Popover } from 'react-tiny-popover'; -import { MouseEvent, useState } from 'react'; +import { MouseEvent, useCallback, useState } from 'react'; import Modal from '@components/Modal'; import ModalDeleteLink from '@components/Modal/ModalDeleteLink'; import ModalAddLink from '@components/Modal/ModalAddLink'; @@ -15,7 +15,7 @@ interface Props { item: Data; } -function Card({ folders, item }: Props) { +const Card = ({ folders, item }: Props) => { const [popoverIsOpen, setPopoverIsOpen] = useState(false); const [modalDeleteLinkIsOpen, setModalDeleteLinkIsOpen] = useState(false); const [modalAddLinkIsOpen, setModalAddLinkIsOpen] = useState(false); @@ -24,6 +24,35 @@ function Card({ folders, item }: Props) { const timeDiff = formatTimeDiff(created_at); const date = formatDate(created_at); + const handleOpenPopover = useCallback((e: MouseEvent) => { + e.preventDefault(); + setPopoverIsOpen(!popoverIsOpen); + }, []); + + const handleClosePopover = useCallback(() => { + setPopoverIsOpen(false); + }, []); + + const handleOpenModalDeleteLink = useCallback((e: MouseEvent) => { + e.preventDefault(); + setModalDeleteLinkIsOpen(true); + }, []); + + const handleCloseModalDeleteLink = useCallback((e: MouseEvent) => { + e.preventDefault(); + setModalDeleteLinkIsOpen(false); + }, []); + + const handleOpenModalAddLink = useCallback((e: MouseEvent) => { + e.preventDefault(); + setModalAddLinkIsOpen(true); + }, []); + + const handleCloseModalAddLink = useCallback((e: MouseEvent) => { + e.preventDefault(); + setModalAddLinkIsOpen(false); + }, []); + return ( @@ -40,48 +69,28 @@ function Card({ folders, item }: Props) { setPopoverIsOpen(false)} + onClickOutside={handleClosePopover} content={ - { - e.preventDefault(); - setModalDeleteLinkIsOpen(true); - }}> + 삭제하기 - {modalDeleteLinkIsOpen && ( - { - e.preventDefault(); - setModalDeleteLinkIsOpen(false); - }}> + {modalDeleteLinkIsOpen ? ( + - )} - { - e.preventDefault(); - setModalAddLinkIsOpen(true); - }}> + ) : null} + 폴더에 추가 - {modalAddLinkIsOpen && ( - { - e.preventDefault(); - setModalAddLinkIsOpen(false); - }}> + {modalAddLinkIsOpen ? ( + - )} + ) : null} }> - { - e.preventDefault(); - setPopoverIsOpen(!popoverIsOpen); - }}> + 케밥 버튼 @@ -91,6 +100,6 @@ function Card({ folders, item }: Props) { ); -} +}; export default Card; diff --git a/src/components/CardList/CardList.tsx b/src/components/CardList/CardList.tsx index 8bf34eeec..3fb0f755a 100644 --- a/src/components/CardList/CardList.tsx +++ b/src/components/CardList/CardList.tsx @@ -10,7 +10,7 @@ interface Props { searchKeyword: string; } -function CardList({ folders, items, searchKeyword }: Props) { +const CardList = ({ folders, items, searchKeyword }: Props) => { const lowerCaseKeyword = searchKeyword.toLowerCase(); const filteredItems = useMemo( @@ -23,23 +23,19 @@ function CardList({ folders, items, searchKeyword }: Props) { [items, lowerCaseKeyword] ); + if (!filteredItems?.length) { + return 저장된 링크가 없습니다; + } + return ( - <> - {!filteredItems?.length ? ( - 저장된 링크가 없습니다 - ) : ( - filteredItems && ( - - {filteredItems.map((item) => ( - - - - ))} - - ) - )} - + + {filteredItems.map((item) => ( + + + + ))} + ); -} +}; export default CardList; diff --git a/src/components/CurrentFolderInfo/CurrentFolderInfo.tsx b/src/components/CurrentFolderInfo/CurrentFolderInfo.tsx index 9d6498afd..6735dfde3 100644 --- a/src/components/CurrentFolderInfo/CurrentFolderInfo.tsx +++ b/src/components/CurrentFolderInfo/CurrentFolderInfo.tsx @@ -10,7 +10,7 @@ interface Props { selectedId: string | undefined; } -function CurrentFolderInfo({ selectedName, selectedId }: Props) { +const CurrentFolderInfo = ({ selectedName, selectedId }: Props) => { const [modalShareIsOpen, setModalShareIsOpen] = useState(false); const [modalEditIsOpen, setModalEditIsOpen] = useState(false); const [modalDeleteIsOpen, setModalDeleteIsOpen] = useState(false); @@ -23,32 +23,32 @@ function CurrentFolderInfo({ selectedName, selectedId }: Props) { 공유 아이콘 공유 - {modalShareIsOpen && ( + {modalShareIsOpen ? ( setModalShareIsOpen(false)}> - )} + ) : null} - {modalEditIsOpen && ( + {modalEditIsOpen ? ( setModalEditIsOpen(false)}> - )} + ) : null} - {modalDeleteIsOpen && ( + {modalDeleteIsOpen ? ( setModalDeleteIsOpen(false)}> - )} + ) : null} ); -} +}; export default CurrentFolderInfo; diff --git a/src/components/FolderInfo/FolderInfo.tsx b/src/components/FolderInfo/FolderInfo.tsx index 2a8cc6db2..81c0271a5 100644 --- a/src/components/FolderInfo/FolderInfo.tsx +++ b/src/components/FolderInfo/FolderInfo.tsx @@ -5,11 +5,9 @@ interface Props { folder: Folder; } -function FolderInfo({ folder }: Props) { - const folderName = folder?.name; - const owner = folder?.owner; - const ownerName = owner?.name; - const ownerProfileImg = owner?.profileImageSource; +const FolderInfo = ({ folder }: Props) => { + const { name: folderName } = folder; + const { name: ownerName, profileImageSource: ownerProfileImg } = folder.owner; return ( @@ -18,6 +16,6 @@ function FolderInfo({ folder }: Props) { {folderName} ); -} +}; export default FolderInfo; diff --git a/src/components/FolderList/FolderList.tsx b/src/components/FolderList/FolderList.tsx index 47cf0c3a0..d3d684c09 100644 --- a/src/components/FolderList/FolderList.tsx +++ b/src/components/FolderList/FolderList.tsx @@ -10,35 +10,35 @@ interface Props { folders: Folder[]; } -function FolderList({ folders }: Props) { +const FolderList = ({ folders }: Props) => { const [selectedId, setSelectedId] = useState(''); const [selectedName, setSelectedName] = useState('전체'); - const [modalIsOpen, setModalIsOpen] = useState(false); + const [modalAddFolderIsOpen, setModalAddFolderIsOpen] = useState(false); - const handleAllClick = () => { + const handleSelectEntire = () => { setSelectedId(''); setSelectedName('전체'); }; - const handleClick = (id: number, name: string) => { + const handleSelectFolder = (id: number, name: string) => { setSelectedId(String(id)); setSelectedName(name); }; return ( <> - {folders && ( + {folders ? ( - + 전체 {folders?.map((folder) => ( handleClick(folder.id, folder.name)} + onClick={() => handleSelectFolder(folder.id, folder.name)} id={String(folder.id)} selected={selectedId ? +selectedId === folder.id : false}> {folder.name} @@ -46,7 +46,7 @@ function FolderList({ folders }: Props) { ))} - setModalIsOpen(true)}> + setModalAddFolderIsOpen(true)}> 폴더 추가 추가 아이콘 - {modalIsOpen && ( - setModalIsOpen(false)}> + {modalAddFolderIsOpen ? ( + setModalAddFolderIsOpen(false)}> - )} + ) : null} - )} + ) : null} ); -} +}; export default FolderList; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index bdbe40c62..52ecc471c 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import * as S from './Footer.style'; -export default function Footer() { +const Footer = () => { return ( <> @@ -53,4 +53,6 @@ export default function Footer() { ); -} +}; + +export default Footer; diff --git a/src/components/Input/Input.style.ts b/src/components/Input/Input.style.ts index c6f935973..9501460a1 100644 --- a/src/components/Input/Input.style.ts +++ b/src/components/Input/Input.style.ts @@ -25,12 +25,11 @@ export const Input = styled.input<{ $error: boolean; $passwordType: boolean }>` ${({ $error }) => ($error ? `${COLORS.RED}` : `${COLORS.GRAY_20}`)}; background: ${COLORS.WHITE}; font-size: 1.6rem; - color: ${COLORS.GRAY_60}; + color: ${COLORS.GRAY_100}; line-height: 2.4rem; &:focus { border-color: ${COLORS.PRIMARY}; - color: ${COLORS.GRAY_100}; } `; diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 8311ce889..bde8969c0 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -1,26 +1,21 @@ +import { useFormContext } from 'react-hook-form'; +import { useState } from 'react'; import * as S from './Input.style'; -import { FocusEventHandler, useState } from 'react'; interface Props { id: string; passwordType?: boolean; placeholder?: string; - onBlur?: FocusEventHandler; - errorMessage?: string; - hasError?: boolean; } -export default function Input({ - id, - passwordType = false, - placeholder, - onBlur, - errorMessage, - hasError = false, -}: Props) { +const Input = ({ id, passwordType = false, placeholder }: Props) => { const [showPassword, setShowPassword] = useState(false); + const { + register, + formState: { errors }, + } = useFormContext(); - const togglePasswordVisibility = () => { + const handleTogglePasswordVisibility = () => { setShowPassword(!showPassword); }; @@ -29,21 +24,25 @@ export default function Input({ - {passwordType && ( + {passwordType ? ( - )} + ) : null} - {hasError && {errorMessage}} + {errors[id] ? ( + {String(errors[id]?.message)} + ) : null} ); -} +}; + +export default Input; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index d0472ef87..31fac856a 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -2,18 +2,21 @@ import Nav from '@components/Nav'; import Footer from '@components/Footer'; import FooterRefContext from '@contexts/FooterRefContext'; import { ReactNode, useRef } from 'react'; +import { SampleUserProfile } from '@pages/shared'; +import { UserProfile } from '@pages/folder'; interface Props { + profile?: SampleUserProfile | UserProfile; children?: ReactNode; } -const Layout = ({ children }: Props) => { +const Layout = ({ profile, children }: Props) => { const footerRef = useRef(null); return ( <> -