diff --git a/.gitignore b/.gitignore index 8f322f0d8..45c1abce8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ yarn-error.log* # local env files .env*.local +.env # vercel .vercel diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..ef1860b8a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["Linkbrary"] +} diff --git a/@types/import-env.d.ts b/@types/import-env.d.ts new file mode 100644 index 000000000..57671709a --- /dev/null +++ b/@types/import-env.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface ProcessEnv { + NEXT_PUBLIC_BASE_URL: string; + } +} diff --git a/Portal.tsx b/Portal.tsx index fb6fcb585..9b1c597d3 100644 --- a/Portal.tsx +++ b/Portal.tsx @@ -2,23 +2,13 @@ import React, { ReactElement, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; const ModalPortal = ({ children }: { children: ReactElement }) => { - const [mounted, setMounted] = useState(false); const [portalElement, setPortalElement] = useState(null); useEffect(() => { setPortalElement(document.getElementById('modal')); }, []); - useEffect(() => { - setMounted(true); - return () => setMounted(false); - }, []); - - return ( - <> - {mounted && portalElement ? createPortal(children, portalElement) : null} - - ); + return <>{portalElement ? createPortal(children, portalElement) : null}; }; export default ModalPortal; diff --git a/pages/api/api.ts b/api/api.ts similarity index 71% rename from pages/api/api.ts rename to api/api.ts index 9ec76c152..66e7d4f7b 100644 --- a/pages/api/api.ts +++ b/api/api.ts @@ -1,4 +1,17 @@ -import axios from '../../instance/instance'; +import axios from '../instance/instance'; + +axios.interceptors.request.use( + (config) => { + const accessToken = localStorage.getItem('token'); + config.headers['Authorization'] = accessToken; + + return config; + }, + (error) => { + console.log(error); + return Promise.reject(error); + } +); export async function getSampleUser() { try { @@ -49,7 +62,17 @@ export async function getFolder(id: string) { } } -export async function getFolderList(id: string, folderId: number) { +export async function getFolderData(folderId: string) { + try { + const { data } = await axios.get(`/folders/${folderId}`); + return data.data; + } catch (error) { + console.error('Error fetching folder:', error); + throw error; + } +} + +export async function getFolderList(id: string, folderId: string) { if (folderId) { try { const query = `/${id}/links?folderId=${folderId}`; @@ -85,6 +108,16 @@ export async function getUser(accessToken: string) { } } +export async function getUserData(id: string) { + try { + const { data } = await axios.get(`/users/${id}`); + return data.data; + } catch (error) { + console.error('Error fetching user:', error); + throw error; + } +} + export async function postSignIn(id: string, password: string) { try { const { data } = await axios.post('/sign-in', { @@ -92,7 +125,6 @@ export async function postSignIn(id: string, password: string) { password: password, }); localStorage.setItem('token', data.data.accessToken); - window.location.href = '/'; return data; } catch (error) { console.error('Error fetching sign-in:', error); @@ -120,7 +152,6 @@ export async function postSignUp(id: string, password: string) { }); localStorage.setItem('token', data.data.accessToken); alert('회원가입이 완료되었습니다!'); - window.location.href = '/'; return data; } catch (error) { console.error('Error fetching sign-in:', error); @@ -130,53 +161,31 @@ export async function postSignUp(id: string, password: string) { export async function postFolder(name: string) { try { - const token = localStorage.getItem('token'); - const { data } = await axios.post( - '/folders', - { - name: name, - }, - { - headers: { - Authorization: token, - }, - } - ); - return data; + const { data } = await axios.post('/folders', { + name: name, + }); + return data.data; } catch (error) { console.error('Error fetching post folder:', error); } } -export async function deleteFolder(folderId: number) { +export async function deleteFolder(folderId: string) { try { const token = localStorage.getItem('token'); - const { data } = await axios.delete(`/folders/${folderId}`, { - headers: { - Authorization: token, - }, - }); + const { data } = await axios.delete(`/folders/${folderId}`); return data; } catch (error) { console.error('Error fetching post folder:', error); } } -export async function postLink(folderId: number, url: string) { +export async function postLink(folderId: string, url: string) { try { - const token = localStorage.getItem('token'); - const { data } = await axios.post( - '/links', - { - url: url, - folderId: folderId, - }, - { - headers: { - Authorization: token, - }, - } - ); + const { data } = await axios.post('/links', { + url: url, + folderId: folderId, + }); return data; } catch (error) { alert('url과 폴더를 지정해주세요!'); @@ -186,14 +195,21 @@ export async function postLink(folderId: number, url: string) { export async function deleteLink(linkId: number) { try { - const token = localStorage.getItem('token'); - const { data } = await axios.delete(`/links/${linkId}`, { - headers: { - Authorization: token, - }, - }); + const { data } = await axios.delete(`/links/${linkId}`); return data; } catch (error) { console.error('Error fetching post folder:', error); } } + +export async function putFolder(folderId: string, name: string) { + try { + const { data } = await axios.put(`/folders/${folderId}`, { + name: name, + }); + return data; + } catch (error) { + alert('이름 수정에 실패했습니다!'); + console.error('Error fetching put folder:', error); + } +} diff --git a/components/Button/Button.styled.ts b/components/Button/Button.styled.ts index f63a1270d..f0d73cb89 100644 --- a/components/Button/Button.styled.ts +++ b/components/Button/Button.styled.ts @@ -25,6 +25,7 @@ export const Cta = styled.button` line-height: normal; width: ${({ size }) => buttonSize[size]}rem; position: relative; + white-space: nowrap; &:hover { opacity: 0.8; @@ -32,5 +33,7 @@ export const Cta = styled.button` @media (max-width: 768px) { font-size: 1.4rem; + padding: 1rem 1.6rem; + width: ${({ size }) => (size === 'lg' ? '100%' : `${buttonSize[size]}rem`)}; } `; diff --git a/components/Card/Card.styled.ts b/components/Card/Card.styled.ts index 25faf06e2..84466ef83 100644 --- a/components/Card/Card.styled.ts +++ b/components/Card/Card.styled.ts @@ -1,38 +1,10 @@ import styled from 'styled-components'; -export const EmptyImg = styled.div` - height: 100%; - background-color: var(--EmptyArea); - border-radius: 1.5rem 1.5rem 0 0; - display: flex; - justify-content: center; - flex-direction: column; - align-items: center; - - img { - opacity: 0.2; - width: 13.3rem; - height: 2.4rem; - } -`; - -export const ItemImg = styled.div<{ image: string }>` - height: 100%; - background-image: url(${(props) => props.image}); - border-radius: 1.5rem 1.5rem 0 0; - background-size: cover; - background-position: center; - - &:hover { - background-size: 150%; - } -`; - export const ItemCard = styled.div` width: 34rem; - height: 33.4rem; display: flex; flex-direction: column; + align-items: center; box-shadow: 0px 5px 25px 0px rgba(0, 0, 0, 0.08); border-radius: 1.5rem; text-decoration: none; @@ -49,13 +21,53 @@ export const ItemCard = styled.div` } `; -export const StarIcon = styled.img` +export const StarIcon = styled.div` width: 3.4rem; height: 3rem; flex-shrink: 0; position: absolute; top: 1.5rem; right: 1.5rem; + z-index: 1; +`; + +export const EmptyImg = styled.div` + height: 100%; + width: 100%; + background-color: var(--EmptyArea); + border-radius: 1.5rem 1.5rem 0 0; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + + img { + opacity: 0.2; + } +`; + +export const ItemImg = styled.div` + position: relative; + transition: 0.3s ease; + width: 100%; + height: 100%; + img { + object-fit: cover; + } + + &:hover { + width: 170%; + } +`; + +export const ImageArea = styled.div` + border-radius: 1.5rem 1.5rem 0 0; + height: 23.5rem; + width: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: center; `; export const ItemInfo = styled.div` @@ -63,7 +75,7 @@ export const ItemInfo = styled.div` flex-direction: column; padding: 1.5rem 2rem; width: 100%; - height: 13.5rem; + height: 10.5rem; gap: 1rem; position: relative; `; diff --git a/components/Card/Card.tsx b/components/Card/Card.tsx index ec2731f4a..9dff3197a 100644 --- a/components/Card/Card.tsx +++ b/components/Card/Card.tsx @@ -5,67 +5,103 @@ import KebabMenu from '../KebabMenu/KebabMenu'; import { LinkData } from '../../hooks/useGetFolder'; import Image from 'next/image'; import Link from 'next/link'; +import logo from '@/public/logo.svg'; function Card({ item, setUrl, + onSelect, + setLinkId, }: { item: LinkData; - setUrl: Dispatch>; + setUrl?: Dispatch>; + onSelect?: { + id: string; + name: string; + }; + setLinkId?: Dispatch>; }) { const [createdAt, setCreatedAt] = useState({ time: 0, result: '' }); const [fullDate, setFullDate] = useState(''); - const { image_source } = item; const [kebabView, setKebabView] = useState(false); const [like, setLike] = useState(false); - const { url, description, id } = item; + const { url, description, id, image_source, title } = item; + const [imageUrl, setImageUrl] = useState(image_source); const createdText = `${createdAt.time} ${createdAt.result} ago`; + const handleKebab = (e: React.MouseEvent) => { + setKebabView(!kebabView); + if (setLinkId) { + setLinkId(id); + } + e.preventDefault(); + }; + useEffect(() => { const nowDate = new Date(); - const createdate = new Date(item.created_at); - const date = (Number(nowDate) - Number(createdate)) / 1000; + const createDate = new Date(item.created_at); + const date = (Number(nowDate) - Number(createDate)) / 1000; setCreatedAt(calculateDate(date)); - setFullDate(changeDate(createdate)); + setFullDate(changeDate(createDate)); }, [item]); return ( - - - { - setLike(!like); - e.preventDefault(); - }} - /> - {image_source ? ( - - ) : ( - - 빈 이미지 - - )} - - + + + { - setKebabView(!kebabView); + setLike(!like); e.preventDefault(); }} - /> - {createdText} - - {description ? description : url} - - {fullDate} - - {kebabView && } - - + > + 별 이미지 + + + {imageUrl ? ( + + 카드 이미지 setImageUrl('')} + /> + + ) : ( + + 빈 이미지 + + )} + + + {onSelect && onSelect.name && ( + + )} + {createdText} + {title ? title : description} + {fullDate} + + {kebabView && ( + + )} + + + ); } diff --git a/components/ContentsContainer.tsx b/components/ContentsContainer.tsx index fd8b487b1..609c5b6a8 100644 --- a/components/ContentsContainer.tsx +++ b/components/ContentsContainer.tsx @@ -2,7 +2,6 @@ import { ReactNode } from 'react'; import styled from 'styled-components'; const Container = styled.div<{ $empty: number }>` - min-height: 40rem; gap: 2rem; display: grid; grid-template-columns: ${(props) => diff --git a/components/FolderButton/FolderButton.tsx b/components/FolderButton/FolderButton.tsx index cc0c2bbcb..a9135b884 100644 --- a/components/FolderButton/FolderButton.tsx +++ b/components/FolderButton/FolderButton.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { Dispatch, SetStateAction } from 'react'; import * as S from './FolderButton.styled'; import { Folder } from '@/hooks/useGetFolderList'; +import Link from 'next/link'; function FolderButton({ item, @@ -13,9 +14,7 @@ function FolderButton({ isSelected: string; handleMenuClick: (index: number) => void; index: number; - setOnSelect: React.Dispatch< - React.SetStateAction<{ id: number; name: string }> - >; + setOnSelect: Dispatch>; }) { const changeFolder = () => { setOnSelect({ id: item.id, name: item.name }); @@ -23,9 +22,11 @@ function FolderButton({ }; return ( - - {item.name} - + + + {item.name} + + ); } diff --git a/components/FolderButtonContainer/FolderButtonContainer.tsx b/components/FolderButtonContainer/FolderButtonContainer.tsx index dcb0a95d9..f7454e177 100644 --- a/components/FolderButtonContainer/FolderButtonContainer.tsx +++ b/components/FolderButtonContainer/FolderButtonContainer.tsx @@ -1,19 +1,18 @@ -import { useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import * as S from './FolderButtonContainer.styled'; import FolderButton from '../FolderButton/FolderButton'; import { useModal } from '../../contexts/ModalContext'; import { Folders } from '../../hooks/useGetFolderList'; import ModalPortal from '@/Portal'; import AddFolderModal from '../Modal/AddFolderModal/AddFolderModal'; +import Link from 'next/link'; function FolderButtonContainer({ link, setOnSelect, }: { link: Folders; - setOnSelect: React.Dispatch< - React.SetStateAction<{ id: number; name: string }> - >; + setOnSelect: Dispatch>; }) { const [linkSelected, setLinkSelected] = useState([]); const [totalBtn, setTotalBtn] = useState(true); @@ -29,19 +28,21 @@ function FolderButtonContainer({ const handleClickTotalButton = () => { const totalArr: string[] = new Array(link.length).fill('white'); setLinkSelected(totalArr); - setOnSelect({ id: 0, name: '' }); + setOnSelect({ id: '', name: '' }); setTotalBtn(true); }; return ( - - 전체 - + + + 전체 + + {link ? link.map((item, index: number) => ( {modalState.addFolder && ( - + )} diff --git a/components/FolderModalContainer/FolderModals.tsx b/components/FolderModalContainer/FolderModals.tsx index 31548cb44..d699c7b52 100644 --- a/components/FolderModalContainer/FolderModals.tsx +++ b/components/FolderModalContainer/FolderModals.tsx @@ -1,5 +1,5 @@ import Image from 'next/image'; -import { ReactNode } from 'react'; +import { Dispatch, ReactNode, SetStateAction } from 'react'; import * as S from './FolderModals.styled'; import { useModal } from '@/contexts/ModalContext'; import ModalPortal from '@/Portal'; @@ -26,8 +26,21 @@ function FolderIcon({ ); } -function FolderModals({ id, name }: { id: number; name: string }) { - const { modalState, openModal, closeModal } = useModal(); +function FolderModals({ + id, + name, + setOnSelect, +}: { + id: string; + name: string; + setOnSelect: Dispatch< + SetStateAction<{ + id: string; + name: string; + }> + >; +}) { + const { modalState, openModal } = useModal(); return ( @@ -42,17 +55,21 @@ function FolderModals({ id, name }: { id: number; name: string }) { {modalState.share && ( - + )} {modalState.edit && ( - + )} {modalState.delete && ( - + )} diff --git a/components/Input/Input.styled.ts b/components/Input/Input.styled.ts index ceb6baed5..df0368a89 100644 --- a/components/Input/Input.styled.ts +++ b/components/Input/Input.styled.ts @@ -26,6 +26,15 @@ export const InputModal = styled.div<{ &:focus { border: 1px solid var(--Primary); } + + @media (max-width: 768px) { + width: 100%; + + input { + font-size: 1.4rem; + width: 100%; + } + } `; export const TextArea = styled.div` diff --git a/components/Input/Input.tsx b/components/Input/Input.tsx index 595cf78af..33ad42574 100644 --- a/components/Input/Input.tsx +++ b/components/Input/Input.tsx @@ -11,7 +11,6 @@ export interface InputProps { type: string; size: 'sm' | 'md' | 'lg'; field: ControllerRenderProps; - id: string; error: FieldError | undefined; } diff --git a/components/KebabMenu/KebabMenu.styled.ts b/components/KebabMenu/KebabMenu.styled.ts index ee8d9ea24..2b251c57d 100644 --- a/components/KebabMenu/KebabMenu.styled.ts +++ b/components/KebabMenu/KebabMenu.styled.ts @@ -2,7 +2,7 @@ import styled from 'styled-components'; export const ModalBody = styled.div` position: absolute; - right: -5rem; + right: -1rem; bottom: 1rem; width: 10rem; box-shadow: 0px 2px 8px 0px rgba(51, 50, 54, 0.1); @@ -11,7 +11,6 @@ export const ModalBody = styled.div` align-items: center; justify-content: center; background: #fff; - z-index: 10; `; export const ModalButton = styled.div` diff --git a/components/KebabMenu/KebabMenu.tsx b/components/KebabMenu/KebabMenu.tsx index f0b162db8..424f9b585 100644 --- a/components/KebabMenu/KebabMenu.tsx +++ b/components/KebabMenu/KebabMenu.tsx @@ -1,25 +1,28 @@ -import React, { Dispatch, SetStateAction, useRef } from 'react'; +import React, { Dispatch, SetStateAction, useEffect, useRef } from 'react'; import { useModal } from '@/contexts/ModalContext'; import * as S from './KebabMenu.styled'; -import ModalPortal from '@/Portal'; -import DeleteLinkModal from '../Modal/DeleteLinkModal/DeleteLinkModal'; function KebabMenu({ url, - id, setUrl, + setKebabView, + kebabView, }: { url: string; id: number; - setUrl: Dispatch>; + setUrl?: Dispatch>; + setKebabView: Dispatch>; + kebabView: boolean; }) { const kebabRef = useRef(null); - const { modalState, openModal } = useModal(); + const { openModal } = useModal(); const handleAddKebab = (e: React.MouseEvent) => { e.preventDefault(); openModal('add'); - setUrl(url); + if (setUrl) { + setUrl(url); + } }; const handleDeleteKebab = ( @@ -29,17 +32,29 @@ function KebabMenu({ openModal('deleteLink'); }; + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if ( + kebabView && + kebabRef.current && + !kebabRef.current.contains(e.target as Node) + ) { + setKebabView(!kebabView); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [setKebabView, kebabView]); + return ( <> 삭제하기 폴더에 추가 - {modalState.deleteLink && ( - - - - )} ); } diff --git a/components/Loading/Loading.styled.ts b/components/Loading/Loading.styled.ts new file mode 100644 index 000000000..49e768bd7 --- /dev/null +++ b/components/Loading/Loading.styled.ts @@ -0,0 +1,31 @@ +import styled, { keyframes } from 'styled-components'; + +const rotation = keyframes` + from{ + transform: rotate(0deg) + } + to{ + transform: rotate(360deg) + } +`; + +export const LoadingBody = styled.div` + position: fixed; + background-color: #fff; + opacity: 0.9; + left: 50%; + transform: translate(-50%, 0); + width: 100%; + height: 100vh; + margin: 0 auto; + z-index: 30; + display: flex; + justify-content: center; + align-items: center; +`; + +export const LoadingImage = styled.div` + animation: ${rotation} 2s linear infinite; + width: 100px; + height: 100px; +`; diff --git a/components/Loading/Loading.tsx b/components/Loading/Loading.tsx new file mode 100644 index 000000000..8685e1fa3 --- /dev/null +++ b/components/Loading/Loading.tsx @@ -0,0 +1,14 @@ +import Image from 'next/image'; +import * as S from './Loading.styled'; + +function Loading() { + return ( + + + 로딩 아이콘 + + + ); +} + +export default Loading; diff --git a/components/MainSectionCard/MainSectionCard.styled.ts b/components/MainSectionCard/MainSectionCard.styled.ts index 202cc3db1..eae36b05f 100644 --- a/components/MainSectionCard/MainSectionCard.styled.ts +++ b/components/MainSectionCard/MainSectionCard.styled.ts @@ -27,7 +27,6 @@ export const SectionCard = styled.div` 'description'; row-gap: 0.5rem; column-gap: 2.6rem; - justify-content: center; padding: 4rem 3.2rem; } `; @@ -61,6 +60,7 @@ export const Description = styled.p` margin: 0; grid-area: description; align-self: flex-start; + display: inline-block; @media (max-width: 768px) { font-size: 1.6rem; @@ -68,7 +68,18 @@ export const Description = styled.p` } `; -export const SectionImage = styled.img` +export const SectionImage = styled.div` width: 100%; + height: 45rem; grid-area: image; + position: relative; + + @media (max-width: 1199px) { + height: 31.5rem; + } + + @media (max-width: 768px) { + aspect-ratio: 1.2/1; + height: 100%; + } `; diff --git a/components/MainSectionCard/MainSectionCard.tsx b/components/MainSectionCard/MainSectionCard.tsx index a22d99321..a031fd695 100644 --- a/components/MainSectionCard/MainSectionCard.tsx +++ b/components/MainSectionCard/MainSectionCard.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import * as S from './MainSectionCard.styled'; +import Image from 'next/image'; interface SectionItem { title: string; @@ -15,7 +16,9 @@ function MainSectionCard({ item }: { item: SectionItem }) { {title} {description} - + + 색션 이미지 + ); } diff --git a/components/Modal/AddFolderModal/AddFolderModal.tsx b/components/Modal/AddFolderModal/AddFolderModal.tsx index 5f754dc1c..f92ad9d82 100644 --- a/components/Modal/AddFolderModal/AddFolderModal.tsx +++ b/components/Modal/AddFolderModal/AddFolderModal.tsx @@ -1,39 +1,53 @@ import * as S from '../EditModal/EditModal.styled'; import BaseModal from '../BaseModal/BaseModal'; -import useValidate from '@/hooks/useValidate'; import Input from '@/components/Input/Input'; -import { ChangeEvent, useState } from 'react'; -import { postFolder } from '@/pages/api/api'; +import { postFolder } from '@/api/api'; +import { Controller, useForm } from 'react-hook-form'; +import { Dispatch, SetStateAction } from 'react'; +import { useModal } from '@/contexts/ModalContext'; import { useRouter } from 'next/router'; -function AddFolderModal() { - const { checkText, textError } = useValidate(); - const [title, setTitle] = useState(''); +function AddFolderModal({ + setOnSelect, +}: { + setOnSelect: Dispatch< + SetStateAction<{ + id: string; + name: string; + }> + >; +}) { + const { handleSubmit, control } = useForm(); + const { closeModal } = useModal(); const router = useRouter(); - const addFolder = async (e: ChangeEvent) => { - e.preventDefault(); - await postFolder(title); - router.reload(); + const addFolder = async (data: any) => { + const result = await postFolder(data.folder); + router.push(`/folder/${result[0].id}`); + closeModal('addFolder'); }; return ( - + 폴더 추가 - { - checkText(e.target.value); - setTitle(e.target.value); + ( + + )} /> - - {textError && {textError}} - 추가하기 diff --git a/components/Modal/AddModal/AddModal.tsx b/components/Modal/AddModal/AddModal.tsx index fbf1224c9..dd39daf16 100644 --- a/components/Modal/AddModal/AddModal.tsx +++ b/components/Modal/AddModal/AddModal.tsx @@ -4,7 +4,7 @@ import BaseModal from '../BaseModal/BaseModal'; import { Folder, Folders } from '../../../hooks/useGetFolderList'; import { Button } from '../../Button/Button'; import Image from 'next/image'; -import { postLink } from '@/pages/api/api'; +import { postLink } from '@/api/api'; import { useRouter } from 'next/router'; import { useModal } from '@/contexts/ModalContext'; @@ -18,8 +18,8 @@ function FolderButton({ item: Folder; $isSelected: string; index: number; - handleMenuClick: (index: number, folderId: number) => void; - folderId: number; + handleMenuClick: (index: number, folderId: string) => void; + folderId: string; }) { return ( ([]); - const [selectedId, setSelectedId] = useState(0); + const [selectedId, setSelectedId] = useState(''); const { openModal, closeModal } = useModal(); const router = useRouter(); - const handleMenuClick = (index: number, folderId: number) => { + const handleMenuClick = (index: number, folderId: string) => { const booleanArr: string[] = new Array(link.length).fill('none'); booleanArr[index] = 'select'; setFolderSelected(booleanArr); diff --git a/components/Modal/BaseModal/BaseModal.styled.ts b/components/Modal/BaseModal/BaseModal.styled.ts index 052c48201..6ce876e0f 100644 --- a/components/Modal/BaseModal/BaseModal.styled.ts +++ b/components/Modal/BaseModal/BaseModal.styled.ts @@ -35,5 +35,5 @@ export const CloseIcon = styled.img` right: 1.6rem; width: 2.4rem; height: 2.4rem; - z-index: 10; + z-index: 20; `; diff --git a/components/Modal/DeleteLinkModal/DeleteLinkModal.tsx b/components/Modal/DeleteLinkModal/DeleteLinkModal.tsx index bb24f6c04..5c6428fe4 100644 --- a/components/Modal/DeleteLinkModal/DeleteLinkModal.tsx +++ b/components/Modal/DeleteLinkModal/DeleteLinkModal.tsx @@ -1,7 +1,7 @@ import React from 'react'; import * as S from '../DeleteModal/DeleteModal.styled'; import BaseModal from '../BaseModal/BaseModal'; -import { deleteLink } from '@/pages/api/api'; +import { deleteLink } from '@/api/api'; import { useRouter } from 'next/router'; function DeleteLinkModal({ id }: { id: number }) { @@ -17,7 +17,9 @@ function DeleteLinkModal({ id }: { id: number }) { return ( - 링크 삭제 + + 링크 삭제 + 삭제하기 diff --git a/components/Modal/DeleteModal/DeleteModal.styled.ts b/components/Modal/DeleteModal/DeleteModal.styled.ts index f5bb1d5b9..4c01edad7 100644 --- a/components/Modal/DeleteModal/DeleteModal.styled.ts +++ b/components/Modal/DeleteModal/DeleteModal.styled.ts @@ -1,6 +1,13 @@ import { Cta } from '@/components/Button/Button.styled'; import styled from 'styled-components'; +export const Header = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.8rem; +`; + export const Title = styled.p` font-size: 2rem; font-style: normal; diff --git a/components/Modal/DeleteModal/DeleteModal.tsx b/components/Modal/DeleteModal/DeleteModal.tsx index 5418aba93..77692afce 100644 --- a/components/Modal/DeleteModal/DeleteModal.tsx +++ b/components/Modal/DeleteModal/DeleteModal.tsx @@ -1,22 +1,30 @@ -import React from 'react'; +import React, { Dispatch, SetStateAction } from 'react'; import * as S from './DeleteModal.styled'; import BaseModal from '../BaseModal/BaseModal'; -import { deleteFolder } from '@/pages/api/api'; -import { useRouter } from 'next/router'; +import { deleteFolder } from '@/api/api'; +import { useModal } from '@/contexts/ModalContext'; function DeleteModal({ folderName, folderId, + setOnSelect, }: { folderName: string; - folderId: number; + folderId: string; + setOnSelect: Dispatch< + SetStateAction<{ + id: string; + name: string; + }> + >; }) { - const router = useRouter(); + const { closeModal } = useModal(); const isDeleteModal = async (e: React.MouseEvent) => { e.preventDefault(); await deleteFolder(folderId); - router.reload(); + window.location.href = '/folder'; + closeModal('delete'); }; return ( diff --git a/components/Modal/EditModal/EditModal.tsx b/components/Modal/EditModal/EditModal.tsx index af6926f34..f8558114c 100644 --- a/components/Modal/EditModal/EditModal.tsx +++ b/components/Modal/EditModal/EditModal.tsx @@ -1,26 +1,40 @@ import * as S from './EditModal.styled'; import BaseModal from '../BaseModal/BaseModal'; -import useValidate from '@/hooks/useValidate'; import { Button } from '@/components/Button/Button'; import Input from '@/components/Input/Input'; +import { Controller, useForm } from 'react-hook-form'; +import { putFolder } from '@/api/api'; +import { useRouter } from 'next/router'; -function EditModal() { - const { checkText, textError } = useValidate(); +function EditModal({ folderId }: { folderId: string }) { + const { handleSubmit, control } = useForm(); + const router = useRouter(); + + const editFolder = async (data: any) => { + await putFolder(folderId, data.edit); + router.reload(); + }; return ( - + 폴더이름 변경 - checkText(e.target.value)} - size="sm" + ( + + )} /> - - {textError && {textError}} - diff --git a/components/Modal/ShareModal/ShareModal.styled.ts b/components/Modal/ShareModal/ShareModal.styled.ts index 77b305b59..a24c7b833 100644 --- a/components/Modal/ShareModal/ShareModal.styled.ts +++ b/components/Modal/ShareModal/ShareModal.styled.ts @@ -35,6 +35,7 @@ export const ShareButtonBody = styled.div<{ $color: string }>` flex-direction: column; align-items: center; gap: 1rem; + cursor: pointer; p { color: var(--Linkbrary-gray100); @@ -56,3 +57,8 @@ export const ShareButtonBody = styled.div<{ $color: string }>` background-color: ${(props) => props.$color}; } `; + +export const Toast = styled.div` + position: absolute; + bottom: -4rem; +`; diff --git a/components/Modal/ShareModal/ShareModal.tsx b/components/Modal/ShareModal/ShareModal.tsx index 9f0cfe8f5..d0ff179c8 100644 --- a/components/Modal/ShareModal/ShareModal.tsx +++ b/components/Modal/ShareModal/ShareModal.tsx @@ -1,9 +1,59 @@ -import React from 'react'; +import React, { useContext, useState } from 'react'; import * as S from './ShareModal.styled'; import BaseModal from '../BaseModal/BaseModal'; import Image from 'next/image'; +import Toast from '@/components/Toast/Toast'; +import { UserContext } from '@/contexts/UserContext'; + +function ShareModal({ + folderName, + folderId, +}: { + folderName: string; + folderId: string; +}) { + const id = useContext(UserContext); + const [toast, setToast] = useState(false); + + const shareLink = async () => { + await navigator.clipboard.writeText( + `${process.env.NEXT_PUBLIC_BASE_URL}/shared/${folderId}` + ); + setToast(true); + }; + + const shareKakao = () => { + const { Kakao } = window; + Kakao.Share.sendDefault({ + objectType: 'feed', + content: { + title: 'Linkbrary', + description: '나만의 폴더를 만들고 링크를 저장해보세요!', + imageUrl: '', + link: { + mobileWebUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/shared/${folderId}?userId=${id}`, + }, + }, + buttons: [ + { + title: '링크 추가하러 가기', + link: { + mobileWebUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/shared/${folderId}?userId=${id}`, + }, + }, + ], + }); + }; + + const shareFacebook = () => { + const title = '페이스북 공유하기'; + window.open( + `https://www.facebook.com/sharer.php?u=${process.env.NEXT_PUBLIC_BASE_URL}/shared/${folderId}?userId=${id}`, + title, + 'toolbar=0,status=0,width=655,height=520' + ); + }; -function ShareModal({ folderName }: { folderName: string }) { return ( @@ -11,7 +61,7 @@ function ShareModal({ folderName }: { folderName: string }) { {folderName} - +

카카오톡

- +

페이스북

- + 링크 아이콘

링크 복사

+ {toast && }
); } diff --git a/components/Nav/Nav.styled.ts b/components/Nav/Nav.styled.ts index ab9c6794a..c56cafc29 100644 --- a/components/Nav/Nav.styled.ts +++ b/components/Nav/Nav.styled.ts @@ -4,11 +4,7 @@ export const NavModal = styled.div` display: flex; justify-content: space-between; align-items: center; -`; - -export const NavLogo = styled.img` - width: 13.3rem; - height: 2.4rem; + height: 3rem; `; export const UserProfile = styled.div` @@ -24,10 +20,12 @@ export const ProfileBody = styled.div` gap: 0.8rem; `; -export const UserPicture = styled.img` +export const UserPicture = styled.div` + position: relative; width: 2.8rem; height: 2.8rem; border-radius: 1.4rem; + overflow: hidden; `; export const NavBar = styled.div` diff --git a/components/Nav/Nav.tsx b/components/Nav/Nav.tsx index 52cda6e14..189610b81 100644 --- a/components/Nav/Nav.tsx +++ b/components/Nav/Nav.tsx @@ -3,6 +3,7 @@ import * as S from './Nav.styled'; import { User } from '../../hooks/useGetUser'; import Link from 'next/link'; import { Dispatch, useState } from 'react'; +import Image from 'next/image'; function NavUser({ user, @@ -20,7 +21,9 @@ function NavUser({ return ( - + + userPicture +

setToggle(!toggle)}>{user.email}

{toggle && ( @@ -38,7 +41,12 @@ function Nav({ user }: { user: User }) { - + 네이게이션 로고 {user.id ? ( diff --git a/components/NotFound/NotFound.styled.ts b/components/NotFound/NotFound.styled.ts new file mode 100644 index 000000000..bb6508a99 --- /dev/null +++ b/components/NotFound/NotFound.styled.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +export const Body = styled.div` + height: 70vh; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 2rem; + + p { + font-size: 3rem; + font-weight: 600; + } +`; diff --git a/components/NotFound/NotFound.tsx b/components/NotFound/NotFound.tsx new file mode 100644 index 000000000..9d178044b --- /dev/null +++ b/components/NotFound/NotFound.tsx @@ -0,0 +1,14 @@ +import { Button } from '@/components/Button/Button'; +import * as S from './NotFound.styled'; +import Link from 'next/link'; + +function NotFound() { + return ( + +

존재하지 않는 폴더입니다!

+

다른 폴더를 선택해주세요!

+
+ ); +} + +export default NotFound; diff --git a/components/SearchBar/SearchBar.tsx b/components/SearchBar/SearchBar.tsx index 88d628eb7..08e12d7af 100644 --- a/components/SearchBar/SearchBar.tsx +++ b/components/SearchBar/SearchBar.tsx @@ -11,6 +11,10 @@ function SearchModal({ setSearchKeyWord }: PropsType) { const searchLink = (e: ChangeEvent) => { e.preventDefault(); + if (text.length >= 30) { + alert('30자 이하로 검색가능합니다!'); + return; + } setSearchKeyWord(text); }; diff --git a/components/Toast/Toast.styled.ts b/components/Toast/Toast.styled.ts new file mode 100644 index 000000000..c9aa63ce6 --- /dev/null +++ b/components/Toast/Toast.styled.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +export const ToastBody = styled.div` + position: absolute; + bottom: -6rem; + left: 50%; + transform: translate(-50%, 0); + padding: 1.2rem 2rem; + justify-content: center; + align-items: center; + border-radius: 0.8rem; + background: var(--Linkbrary-gray100); + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); + color: #fff; + p { + color: var(--Grayscale-10); + font-family: Pretendard; + font-size: 1.4rem; + font-style: normal; + font-weight: 500; + line-height: 18px; + } +`; diff --git a/components/Toast/Toast.tsx b/components/Toast/Toast.tsx new file mode 100644 index 000000000..fbba3f586 --- /dev/null +++ b/components/Toast/Toast.tsx @@ -0,0 +1,26 @@ +import { Dispatch, SetStateAction, useEffect } from 'react'; +import * as S from './Toast.styled'; + +interface ToastProps { + setToast: Dispatch>; + text: string; +} + +function Toast({ setToast, text }: ToastProps) { + useEffect(() => { + const timer = setTimeout(() => { + setToast(false); + }, 3000); + return () => { + clearTimeout(timer); + }; + }, [setToast]); + + return ( + +

{text}

+
+ ); +} + +export default Toast; diff --git a/hooks/useGetFolder.ts b/hooks/useGetFolder.ts index 43ac65b16..ed814adab 100644 --- a/hooks/useGetFolder.ts +++ b/hooks/useGetFolder.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { getFolderList } from '../pages/api/api'; +import { getFolderList } from '../api/api'; export type LinkData = { id: number; @@ -14,53 +14,41 @@ export type LinkData = { export interface Links extends Array {} -function useGetFolder(id: string, searchKeyword: string, folderId: number) { +function useGetFolder(id: string, searchKeyword: string, folderId: string) { const [linkList, setLinkList] = useState([]); const [loading, setLoading] = useState(false); const search = (list: Links) => { - let arr: Links = []; - for (let i = 0; i < list.length; i++) { - if (list[i].title) { - if (list[i].title.includes(searchKeyword)) { - arr = [...arr, list[i]]; - continue; - } - } - if (list[i].description) { - if (list[i].description.includes(searchKeyword)) { - arr = [...arr, list[i]]; - continue; - } - } - if (list[i].url) { - if (list[i].url.includes(searchKeyword)) { - arr = [...arr, list[i]]; - } - } + if (list) { + const searchLinks = list.filter( + (link) => + link.url?.includes(searchKeyword) || + link.title?.includes(searchKeyword) || + link.description?.includes(searchKeyword) + ); + setLinkList(searchLinks); } - return arr; }; useEffect(() => { - if (id) { - try { - setLoading(true); - const loadFolder = async () => { - const list = await getFolderList(id, folderId); - if (searchKeyword) { - const searchList = search(list); - setLinkList(searchList); - } else { - setLinkList(list); - } - }; - loadFolder(); - setLoading(false); - } catch (error) { - console.error(error); - setLoading(false); - } + if (!id) { + return; + } + try { + setLoading(true); + const loadFolder = async () => { + const list = await getFolderList(id, folderId); + if (searchKeyword) { + search(list); + setLoading(false); + } else { + setLinkList(list); + setLoading(false); + } + }; + loadFolder(); + } catch (error) { + console.error(error); } }, [folderId, id, searchKeyword]); diff --git a/hooks/useGetFolderList.ts b/hooks/useGetFolderList.ts index 460572b3f..a6e1ece18 100644 --- a/hooks/useGetFolderList.ts +++ b/hooks/useGetFolderList.ts @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react'; -import { getFolder } from '../pages/api/api'; +import { getFolder } from '../api/api'; type Like = { count: number; }; export interface Folder { - id: number; + id: string; created_at: Date; name: string; user_id: number; @@ -16,21 +16,30 @@ export interface Folder { export interface Folders extends Array {} -function useGetFolderList(userId: string) { +function useGetFolderList(userId: string, folderId?: string) { const [link, setLink] = useState([]); + const [linkLoading, setLinkLoading] = useState(false); useEffect(() => { - if (userId) { + if (!userId) { + return; + } + try { + setLinkLoading(true); const loadFolderList = async () => { const links = await getFolder(userId); setLink(links.data); + setLinkLoading(false); }; loadFolderList(); + } catch (error) { + console.error(); } - }, [userId]); + }, [userId, folderId]); return { link, + linkLoading, }; } export default useGetFolderList; diff --git a/hooks/useGetUser.ts b/hooks/useGetUser.ts index ac53759a1..0a332db0c 100644 --- a/hooks/useGetUser.ts +++ b/hooks/useGetUser.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { getUser } from '../pages/api/api'; +import { getUser } from '../api/api'; export interface User { id: string; diff --git a/next.config.js b/next.config.js index 18744343d..16c70f3a6 100644 --- a/next.config.js +++ b/next.config.js @@ -4,8 +4,19 @@ module.exports = { compiler: { styledComponents: true, }, - assetPrefix: '.', experimental: { forceSwcTransforms: true, }, + images: { + remotePatterns: [ + { + protocol: 'http', + hostname: '**', + }, + { + protocol: 'https', + hostname: '**', + }, + ], + }, }; diff --git a/package-lock.json b/package-lock.json index 623d4f649..1c8b08f39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.51.4", + "sharp": "^0.33.4", "styled-components": "^6.1.11" }, "devDependencies": { @@ -392,6 +393,15 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -689,6 +699,437 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", + "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", + "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", + "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", + "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", + "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", + "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", + "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", + "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", + "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", + "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", + "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", + "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.31", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", + "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", + "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", + "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", + "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.1.1" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", + "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", + "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1614,6 +2055,18 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1626,8 +2079,32 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1837,6 +2314,14 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4426,6 +4911,56 @@ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, + "node_modules/sharp": { + "version": "0.33.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", + "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.0" + }, + "engines": { + "libvips": ">=8.15.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.4", + "@img/sharp-darwin-x64": "0.33.4", + "@img/sharp-libvips-darwin-arm64": "1.0.2", + "@img/sharp-libvips-darwin-x64": "1.0.2", + "@img/sharp-libvips-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-arm64": "1.0.2", + "@img/sharp-libvips-linux-s390x": "1.0.2", + "@img/sharp-libvips-linux-x64": "1.0.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", + "@img/sharp-libvips-linuxmusl-x64": "1.0.2", + "@img/sharp-linux-arm": "0.33.4", + "@img/sharp-linux-arm64": "0.33.4", + "@img/sharp-linux-s390x": "0.33.4", + "@img/sharp-linux-x64": "0.33.4", + "@img/sharp-linuxmusl-arm64": "0.33.4", + "@img/sharp-linuxmusl-x64": "0.33.4", + "@img/sharp-wasm32": "0.33.4", + "@img/sharp-win32-ia32": "0.33.4", + "@img/sharp-win32-x64": "0.33.4" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4465,6 +5000,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 255881070..062d87763 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.51.4", + "sharp": "^0.33.4", "styled-components": "^6.1.11" }, "devDependencies": { diff --git a/pages/_app.tsx b/pages/_app.tsx index b734455dd..8d55b5780 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -6,9 +6,15 @@ import '@/styles/globals.css'; import type { AppProps } from 'next/app'; import Head from 'next/head'; import { useEffect, useState } from 'react'; -import { getUser } from './api/api'; +import { getUser } from '../api/api'; import { User } from '@/hooks/useGetUser'; +declare global { + interface Window { + Kakao: any; + } +} + export default function App({ Component, pageProps }: AppProps) { const [user, setUser] = useState({ id: '', @@ -30,6 +36,10 @@ export default function App({ Component, pageProps }: AppProps) { } }, []); + useEffect(() => { + window.Kakao.init(process.env.NEXT_PUBLIC_KAKAO_KEY); + }, []); + return ( <> diff --git a/pages/_document.tsx b/pages/_document.tsx index d03f88815..0c1f3d53f 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,14 +1,54 @@ import { Html, Head, Main, NextScript } from 'next/document'; +import Document, { DocumentContext } from 'next/document'; +import { ServerStyleSheet } from 'styled-components'; -export default function Document() { - return ( - - - -
- - - - - ); +class MyDocument extends Document { + static async getInitialProps(ctx: DocumentContext) { + const sheet = new ServerStyleSheet(); + const originalRenderPage = ctx.renderPage; + + try { + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: (App) => (props) => + sheet.collectStyles(), + }); + + const initialProps = await Document.getInitialProps(ctx); + + return { + ...initialProps, + styles: [ + <> + {initialProps.styles} + {sheet.getStyleElement()} + , + ], + }; + } catch (error) { + throw error; + } finally { + sheet.seal(); + } + } + + render() { + return ( + + + + + +
+ + + + + ); + } } + +export default MyDocument; diff --git a/pages/folder.tsx b/pages/folder.tsx deleted file mode 100644 index 36d26a5ac..000000000 --- a/pages/folder.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useContext } from 'react'; -import { useEffect, useRef, useState } from 'react'; -import * as S from '../styles/folder.styled'; -import SearchBar from '../components/SearchBar/SearchBar'; -import ContentsContainer from '../components/ContentsContainer'; -import Card from '../components/Card/Card'; -import useGetFolder from '../hooks/useGetFolder'; -import useGetFolderList from '../hooks/useGetFolderList'; -import FolderButtonContainer from '../components/FolderButtonContainer/FolderButtonContainer'; -import { ContextValue, useModal } from '../contexts/ModalContext'; -import AddModal from '../components/Modal/AddModal/AddModal'; -import ModalPortal from '../Portal'; -import { UserContext } from '@/contexts/UserContext'; -import FolderModals from '@/components/FolderModalContainer/FolderModals'; - -function Folder() { - const id = useContext(UserContext); - const [onSelect, setOnSelect] = useState({ - id: 0, - name: '', - }); - const [searchKeyword, setSearchKeyWord] = useState(''); - const { linkList, loading } = useGetFolder(id, searchKeyword, onSelect.id); - const { link } = useGetFolderList(id); - const [toggleInput, setToggleInput] = useState(true); - const { modalState, openModal }: ContextValue = useModal(); - const obsRef = useRef(null); - const inputRef = useRef(null); - const [url, setUrl] = useState(''); - - const handleObserver = (entries: IntersectionObserverEntry[]) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - setToggleInput(false); - return; - } else { - setToggleInput(true); - return; - } - }); - }; - - useEffect(() => { - const observer = new IntersectionObserver(handleObserver); - if (!loading && obsRef.current) observer.observe(obsRef.current); - return () => { - observer.disconnect(); - }; - }, [loading]); - - return ( - <> -
- - - - - - { - openModal('add'); - if (inputRef.current) { - setUrl(inputRef.current.value); - } - }} - > - 추가하기 - - - - - - - {searchKeyword && ( - -

{searchKeyword}

으로 검색한 결과입니다. -
- )} - - - {onSelect.name ? onSelect.name : '전체'} - {onSelect.name && ( - - )} - - - {linkList.length > 0 ? ( - linkList.map((item) => ( - - )) - ) : ( - 저장된 링크가 없습니다. - )} - - {modalState.add && inputRef.current && ( - - - - )} -
- - ); -} - -export default Folder; diff --git a/pages/folder/[[...folderId]].tsx b/pages/folder/[[...folderId]].tsx new file mode 100644 index 000000000..c748094bb --- /dev/null +++ b/pages/folder/[[...folderId]].tsx @@ -0,0 +1,157 @@ +import { useContext } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import * as S from '../../styles/folder.styled'; +import SearchBar from '../../components/SearchBar/SearchBar'; +import ContentsContainer from '../../components/ContentsContainer'; +import Card from '../../components/Card/Card'; +import useGetFolder from '../../hooks/useGetFolder'; +import useGetFolderList from '../../hooks/useGetFolderList'; +import FolderButtonContainer from '../../components/FolderButtonContainer/FolderButtonContainer'; +import { useModal } from '../../contexts/ModalContext'; +import AddModal from '../../components/Modal/AddModal/AddModal'; +import ModalPortal from '../../Portal'; +import { UserContext } from '@/contexts/UserContext'; +import FolderModals from '@/components/FolderModalContainer/FolderModals'; +import { useRouter } from 'next/router'; +import Image from 'next/image'; +import NotFound from '@/components/NotFound/NotFound'; +import DeleteLinkModal from '@/components/Modal/DeleteLinkModal/DeleteLinkModal'; +import Loading from '@/components/Loading/Loading'; + +function Folder() { + const id = useContext(UserContext); + const [onSelect, setOnSelect] = useState({ + id: '', + name: '', + }); + const [searchKeyword, setSearchKeyWord] = useState(''); + const [url, setUrl] = useState(''); + const [toggleInput, setToggleInput] = useState(true); + const [wrongFolder, setWrongFolder] = useState(false); + const [linkId, setLinkId] = useState(0); + const router = useRouter(); + const folderId = router.query.folderId as string; + const { linkList, loading } = useGetFolder(id, searchKeyword, folderId); + const { link, linkLoading } = useGetFolderList(id, folderId); + const { modalState, openModal } = useModal(); + const obsRef = useRef(null); + const inputRef = useRef(null); + + const handleObserver = (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setToggleInput(false); + return; + } else { + setToggleInput(true); + return; + } + }); + }; + + useEffect(() => { + const access = localStorage.getItem('token'); + if (!access) { + router.replace('/'); + return; + } + const observer = new IntersectionObserver(handleObserver); + if (!loading && obsRef.current) { + observer.observe(obsRef.current); + } + return () => { + observer.disconnect(); + }; + }, [loading, folderId, router]); + + useEffect(() => { + for (let i = 0; i < link.length; i++) { + setWrongFolder(true); + if (link[i].id == folderId) { + setOnSelect({ id: folderId, name: link[i].name }); + setWrongFolder(false); + break; + } + } + }, [link, folderId]); + + return ( + <> + {linkLoading || loading ? : null} +
+ + + + + 링크 아이콘 + + + { + openModal('add'); + if (inputRef.current) { + setUrl(inputRef.current.value); + } + }} + > + 추가하기 + + + + + + + {searchKeyword && ( + +

{searchKeyword}

로 검색한 결과입니다. +
+ )} + + {folderId && wrongFolder ? ( + + ) : ( + <> + +

{folderId ? onSelect.name : '전체'}

+ {folderId && ( + + )} +
+ + {linkList.length > 0 ? ( + linkList.map((item) => ( + + )) + ) : ( + 저장된 링크가 없습니다. + )} + + + )} + {modalState.add && inputRef.current && ( + + + + )} + {modalState.deleteLink && ( + + + + )} +
+ + ); +} + +export default Folder; diff --git a/pages/index.tsx b/pages/index.tsx index e588fb25b..e9e2eb4bb 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -5,6 +5,7 @@ import { sectionDescription } from '../util/sectionDescription'; import { Button } from '../components/Button/Button'; import Link from 'next/link'; import { UserContext } from '@/contexts/UserContext'; +import Image from 'next/image'; function Main() { const [sectionList, setSectionList] = useState([]); @@ -29,7 +30,9 @@ function Main() { - + + 헤더 이미지 + diff --git a/pages/shared.tsx b/pages/shared.tsx deleted file mode 100644 index 232ca09cf..000000000 --- a/pages/shared.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect, useState } from 'react'; -import * as S from '../styles/shared.styled'; -import { getSampleFolder } from './api/api'; -import Card from '../components/Card/Card'; -import SearchBar from '../components/SearchBar/SearchBar'; - -type FolderName = { - name: string; -}; - -type Owner = { - name: string; - profileImageSource: string; -}; - -type Link = { - id: number; - created_at: Date; - url: string; - title: string; - description: string; - image_source: string; -}; - -interface Links extends Array {} - -interface SampleFolder { - id: number; - name: FolderName; - owner: Owner; - links: Links; -} - -type Folder = { - folder: SampleFolder; -}; - -function Shared() { - const [folderInfo, setFolderInfo] = useState({ - name: '', - }); - const [linkList, setLinkList] = useState([]); - const [folderOwner, setFolderOwner] = useState({ - profileImageSource: '', - name: '', - }); - - const infoLoad = async () => { - const result: Folder = await getSampleFolder(); - const { folder } = result; - setFolderInfo(folder.name); - setFolderOwner(folder.owner); - setLinkList(folder.links); - }; - - useEffect(() => { - infoLoad(); - }, []); - - return ( - - - - {folderOwner.name} - {folderInfo.name} - - - - ): void { - throw new Error('Function not implemented.'); - }} - /> - - {linkList.map((item) => ( - {}} /> - ))} - - - - ); -} - -export default Shared; diff --git a/pages/shared/[[...folderId]].tsx b/pages/shared/[[...folderId]].tsx new file mode 100644 index 000000000..1f02d450b --- /dev/null +++ b/pages/shared/[[...folderId]].tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from 'react'; +import * as S from '../../styles/shared.styled'; +import { getFolderData, getUserData } from '../../api/api'; +import Card from '../../components/Card/Card'; +import SearchBar from '../../components/SearchBar/SearchBar'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import useGetFolder from '@/hooks/useGetFolder'; +import ContentsContainer from '@/components/ContentsContainer'; +import Loading from '@/components/Loading/Loading'; + +function Shared() { + const [searchKeyword, setSearchKeyWord] = useState(''); + const [owner, setOwner] = useState({ + id: '', + created_at: new Date(), + name: '', + image_source: '', + email: '', + auth_id: '', + }); + const [folderName, setFolderName] = useState(''); + const router = useRouter(); + const folderId = router.query.folderId as string; + const [userId, setUserId] = useState(''); + const { linkList, loading } = useGetFolder(userId, searchKeyword, folderId); + + useEffect(() => { + const loadOwnerFolderData = async () => { + const folder = await getFolderData(folderId); + if (folder) { + setFolderName(folder[0].name); + setUserId(folder[0].user_id); + } + }; + + const loadOwnerData = async () => { + const user = await getUserData(userId); + if (user) { + setOwner(user[0]); + } + }; + if (folderId) { + loadOwnerFolderData(); + } + if (userId) { + loadOwnerData(); + } + }, [folderId, userId]); + + return ( + <> + {loading && } + + owner 이미지 + {owner.name} + {folderName} + + + + {searchKeyword && ( + +

{searchKeyword}

로 검색한 결과입니다. +
+ )} + + {linkList.length > 0 ? ( + linkList.map((item) => ) + ) : ( + 저장된 링크가 없습니다. + )} + +
+ + ); +} + +export default Shared; diff --git a/pages/signin.tsx b/pages/signin.tsx index b3b436c7f..c4444ef7d 100644 --- a/pages/signin.tsx +++ b/pages/signin.tsx @@ -4,7 +4,7 @@ import Input from '@/components/Input/Input'; import Link from 'next/link'; import Image from 'next/image'; import { Button } from '@/components/Button/Button'; -import { postSignIn } from './api/api'; +import { postSignIn } from '../api/api'; import { Controller, useForm } from 'react-hook-form'; import { emailPattern } from '@/util/util'; @@ -13,7 +13,10 @@ function SignIn() { const { handleSubmit, control } = useForm(); const formAction = async (data: any) => { - await postSignIn(data.id, data.password); + const result = await postSignIn(data.id, data.password); + if (result) { + window.location.href = '/'; + } }; const hiddenText = () => { @@ -51,7 +54,6 @@ function SignIn() { }} render={({ field, fieldState: { error } }) => ( ( { const result = await postCheckEmail(data.id); if (result) { - await postSignUp(data.id, data.password); + const signUp = await postSignUp(data.id, data.password); + if (signUp) { + window.location.href = '/'; + } } }; @@ -53,7 +56,6 @@ function SignUp() { }} render={({ field, fieldState: { error } }) => ( ( ( + + + diff --git a/styles/folder.styled.ts b/styles/folder.styled.ts index 6cd89eb11..c22080a83 100644 --- a/styles/folder.styled.ts +++ b/styles/folder.styled.ts @@ -15,7 +15,7 @@ export const AddButton = styled(Cta)` `; export const EmptyFolder = styled.div` - height: 30rem; + height: 50vh; font-size: 1.6rem; margin: 0 auto; display: flex; @@ -39,9 +39,7 @@ export const AddLinkInput = styled.input` } `; -export const LinkIcon = styled.img` - width: 2rem; - height: 2rem; +export const LinkIcon = styled.div` position: absolute; left: 2rem; @media (max-width: 768px) { @@ -116,11 +114,14 @@ export const FolderModalContainer = styled.div` width: 100%; display: flex; justify-content: space-between; - font-family: Pretendard; - font-weight: 600; - font-size: 2.4rem; margin: 2.4rem auto; + p { + font-family: Pretendard; + font-weight: 600; + font-size: 2.4rem; + } + @media (max-width: 768px) { flex-direction: column; justify-content: center; diff --git a/styles/index.styled.ts b/styles/index.styled.ts index 700938e55..fa37d04f8 100644 --- a/styles/index.styled.ts +++ b/styles/index.styled.ts @@ -34,7 +34,12 @@ export const Header__contents = styled.div` width: 100%; @media (max-width: 768px) { - gap: 1.5rem; + gap: 2.4rem; + + a { + width: 100%; + max-width: 40rem; + } } `; @@ -61,25 +66,42 @@ export const Slogan_gradient = styled.span` `; export const Header__image = styled.div` - position: relative; - padding: 5rem 4rem 0 4rem; width: 120rem; - aspect-ratio: 2; + height: 59rem; + padding: 5rem 4.1rem 0; overflow: hidden; @media (max-width: 1199px) { - width: 70rem; + width: 69.8rem; + height: 34.3rem; + padding: 2.9rem 2.4rem 0; } @media (max-width: 768px) { width: 100%; + height: 100%; + aspect-ratio: 2 / 1; } `; -export const HeaderImage = styled.img` +export const HeaderImage = styled.div` + position: relative; + height: 65.9rem; width: 100%; - border-radius: 2.5rem; - box-shadow: 0px 0.4rem 2.5rem 0px rgba(0, 0, 0, 0.08); + + img { + border-radius: 2.5rem; + box-shadow: 0px 0.4rem 2.5rem 0px rgba(0, 0, 0, 0.08); + } + + @media (max-width: 1199px) { + height: 38rem; + } + + @media (max-width: 768px) { + aspect-ratio: 1.7 / 1; + height: auto; + } `; export const Main__contents = styled.div` @@ -117,7 +139,6 @@ export const Main__contents = styled.div` 'description'; row-gap: 0.5rem; column-gap: 2.6rem; - justify-content: center; padding: 4rem 3.2rem; } } diff --git a/styles/reset.css b/styles/reset.css index 5d0ebf24b..b6a7e2f42 100644 --- a/styles/reset.css +++ b/styles/reset.css @@ -143,7 +143,3 @@ button { a { text-decoration: none; } - -::-webkit-scrollbar { - display: none; -} diff --git a/styles/shared.styled.ts b/styles/shared.styled.ts index 0d5d860d2..8af73435f 100644 --- a/styles/shared.styled.ts +++ b/styles/shared.styled.ts @@ -1,13 +1,5 @@ import styled from 'styled-components'; -export const Shared = styled.div` - margin: 0 auto; - display: flex; - flex-direction: column; - align-items: center; - font-family: Pretendard; -`; - export const OwnerProfile = styled.div` background-color: var(--Background); width: 100%; @@ -18,11 +10,6 @@ export const OwnerProfile = styled.div` padding: 2rem 0 6rem 0; `; -export const OwnerProfileImage = styled.img` - width: 6rem; - height: 6rem; -`; - export const OwnerName = styled.p` font-family: Pretendard; font-size: 1.6rem; @@ -30,6 +17,7 @@ export const OwnerName = styled.p` font-weight: 400; line-height: 2.4rem; margin-top: 1.2rem; + height: 2.4rem; `; export const FolderName = styled.p` @@ -42,13 +30,16 @@ export const FolderName = styled.p` font-weight: 600; line-height: normal; margin-top: 2rem; + height: 5.5rem; `; export const SharedContent = styled.div` - margin: 0 auto; display: flex; - align-items: center; flex-direction: column; + justify-content: center; + align-items: center; + margin: 0 auto; + max-width: 106rem; @media (max-width: 1199px) { padding: 0 3.2rem; @@ -70,3 +61,31 @@ export const Container = styled.div` grid-template-columns: 1fr; } `; + +export const EmptyFolder = styled.div` + height: 50vh; + font-size: 1.6rem; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 8rem 0; +`; + +export const SearchResult = styled.div` + display: flex; + width: 100%; + margin-bottom: 4rem; + color: var(--Linkbrary-gray60); + font-family: Pretendard; + font-size: 3.2rem; + font-style: normal; + font-weight: 600; + line-height: normal; + letter-spacing: -0.2px; + + p { + color: var(--Linkbrary-gray100); + } +`; diff --git a/styles/signin.styled.ts b/styles/signin.styled.ts index b871f8ec3..80fb45775 100644 --- a/styles/signin.styled.ts +++ b/styles/signin.styled.ts @@ -112,6 +112,11 @@ export const SnsLogin = styled.div` font-style: normal; font-weight: 400; line-height: normal; + + @media (max-width: 768px) { + width: 100%; + max-width: 40rem; + } `; export const SnsIcons = styled.div` @@ -154,4 +159,9 @@ export const SignForm = styled.form` display: flex; flex-direction: column; justify-content: flex-start; + + @media (max-width: 768px) { + width: 100%; + max-width: 40rem; + } `;