diff --git a/api/config.js b/api/config.js new file mode 100644 index 000000000..5614c886c --- /dev/null +++ b/api/config.js @@ -0,0 +1 @@ +export const BASE_URL = "https://bootcamp-api.codeit.kr/api"; diff --git a/components/EmailInput.tsx b/components/EmailInput.tsx index fad90f919..8461ff54d 100644 --- a/components/EmailInput.tsx +++ b/components/EmailInput.tsx @@ -1,8 +1,8 @@ -// components/EmailInput.tsx - import React, { useState, useEffect } from "react"; import classNames from "classnames/bind"; -import styles from "@/styles/signin.module.scss"; +import styles from "@/styles/sign-in.module.scss"; + +import { isEmailValid } from "@/utils/util"; interface EmailInputProps { value: string; @@ -10,13 +10,11 @@ interface EmailInputProps { error?: string; } -export const emailCheck = (email: string) => { - const emailForm = - /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i; - return emailForm.test(email); -}; - -export function EmailInput({ value, onChange, error }: EmailInputProps) { +export default function EmailInput({ + value, + onChange, + error, +}: EmailInputProps) { const cx = classNames.bind(styles); const [isFocused, setIsFocused] = useState(false); const [emailErrorText, setEmailErrorText] = useState(""); @@ -33,7 +31,7 @@ export function EmailInput({ value, onChange, error }: EmailInputProps) { const handleBlur = () => { setIsFocused(false); if (value) { - if (!emailCheck(value)) { + if (!isEmailValid(value)) { setEmailErrorText("올바른 이메일 주소가 아닙니다."); } else { setEmailErrorText(""); @@ -51,9 +49,7 @@ export function EmailInput({ value, onChange, error }: EmailInputProps) { return (
- + + + + + 추가하기 + + + ); +} + +export default AddLinkBar; diff --git a/components/Folder/Button.tsx b/components/Folder/Button.tsx new file mode 100644 index 000000000..2d218caa3 --- /dev/null +++ b/components/Folder/Button.tsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import styled from "styled-components"; + +interface FolderData { + id: number; + name: string; +} + +interface StyledFolderButtonProps { + active: boolean; +} + +const StyledFolderButton = styled.button` + background-color: #ffffff; + width: auto; + height: 36px; + border-radius: 5px; + border: 1px solid #6d6afe; + font-size: 16px; + font-weight: 400; + margin-top: 6px; + margin-right: 10px; + text-align: center; + + &:hover { + cursor: pointer; + } + + ${({ active }) => + active && + ` + background-color: #6D6AFE; + color: #ffffff; + `} +`; + +interface ButtonProps { + folderData: FolderData[]; + selectedFolderId: number | null; + onFolderClick: (buttonId: number | null) => void; +} + +function Button({ folderData, selectedFolderId, onFolderClick }: ButtonProps) { + const [activeButton, setActiveButton] = useState(null); + + const handleButtonClick = (buttonId: number | null) => { + setActiveButton(buttonId); + onFolderClick(buttonId); + }; + + return ( + <> + handleButtonClick(null)} + active={activeButton === null} + > + 전체 + + {folderData.map((data) => ( + handleButtonClick(data.id)} + active={activeButton === data.id} + > + {data.name} + + ))} + + ); +} +export default Button; diff --git a/components/Folder/Card.tsx b/components/Folder/Card.tsx new file mode 100644 index 000000000..2c643a4ae --- /dev/null +++ b/components/Folder/Card.tsx @@ -0,0 +1,96 @@ +import styled from "styled-components"; +import { formatDate, generateTimeText } from "@/utils/util"; +import Link from "next/link"; + +const StyledCard = styled.div` + max-width: 340px; + display: flex; + flex-direction: column; + align-items: flex-start; + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; + border-radius: 10px; + + @media (max-width: 1124px) { + max-width: 340px; + } +`; + +const StyledLink = styled(Link)` + display: block; + width: 100%; +`; + +const CardImg = styled.img` + width: 100%; + height: 200px; + object-fit: cover; + border-top-left-radius: 10px; + border-top-right-radius: 10px; +`; + +const CardTextSection = styled.section` + width: 100%; + height: 136px; + padding: 10px 20px; + display: grid; + grid-template-rows: 1fr 2fr 1fr; + grid-template-columns: 1fr; + grid-template-areas: + "time" + "description" + "date"; +`; + +const CardDescription = styled.p` + margin: 0; + + width: 100%; + height: 50px; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + grid-area: description; +`; + +const TimeText = styled.p` + margin: 0; + grid-area: time; +`; + +const DateText = styled.p` + margin: 0; + + grid-area: date; +`; +export interface LinkData { + id: number; + created_at: string; + updated_at: string; + url: string; + title: string; + description: string; + image_source: string; + folder_id: number; +} + +function Card({ linkData }: { linkData: LinkData }) { + return ( + + + + + + {generateTimeText(linkData.created_at)} + {linkData.description} + {formatDate(linkData.created_at)} + + + ); +} + +export default Card; diff --git a/components/Folder/CardList.tsx b/components/Folder/CardList.tsx new file mode 100644 index 000000000..d50f8d6bd --- /dev/null +++ b/components/Folder/CardList.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; +import Card, { LinkData } from "./Card"; +import { BASE_URL } from "@/api/config"; +import styled from "styled-components"; + +const StyledCardContainer = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + column-gap: 20px; + row-gap: 25px; + margin: 0 auto; + + @media (max-width: 1124px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: 767px) { + grid-template-columns: repeat(1, 1fr); + } +`; + +interface CardListProps { + selectedFolderId: number | null; +} + +function CardList({ selectedFolderId }: CardListProps) { + const [linkData, setLinkData] = useState([]); + + useEffect(() => { + const fetchLinkData = async () => { + const accessToken = localStorage.getItem("accessToken"); + + if (accessToken) { + try { + let url = `${BASE_URL}/links`; + if (selectedFolderId !== null) { + url += `?folderId=${selectedFolderId}`; + console.log("Id :", selectedFolderId); + } + const response = await axios.get(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (selectedFolderId === null) { + setLinkData(response.data.data.folder); + } else { + setLinkData(response.data.data); + } + console.log("response : ", response.data.data); + } catch (error) { + console.error("에러 메세지 : ", error); + } + } + }; + + fetchLinkData(); + }, [selectedFolderId]); + + return ( + + {linkData.length > 0 ? ( + linkData.map((data) => ) + ) : ( +
저장된 링크가 없습니다.
+ )} +
+ ); +} + +export default CardList; diff --git a/components/Folder/FolderList.tsx b/components/Folder/FolderList.tsx new file mode 100644 index 000000000..a1b9ca019 --- /dev/null +++ b/components/Folder/FolderList.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import axios from "axios"; +import styled from "styled-components"; + +import { BASE_URL } from "@/api/config"; +import Button from "./Button"; + +const ActiveFolderName = styled.p` + text-align: left; + width: 100%; + position: relative; + left: 50px; + font-size: 24px; + font-weight: 700; +`; + +interface FolderListProps { + selectedFolderId: number | null; + onFolderClick: (folderId: number | null) => void; +} +interface FolderData { + id: number; + created_at: string; + name: string; + user_id: number; + favorite: boolean; + link: { + count: number; + }; +} + +function FolderList({ selectedFolderId, onFolderClick }: FolderListProps) { + const [folderData, setFolderData] = useState([]); + + useEffect(() => { + const fetchFolderData = async () => { + const accessToken = localStorage.getItem("accessToken"); + + if (accessToken) { + try { + const response = await axios.get(`${BASE_URL}/folders`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + setFolderData(response.data.data.folder); + } catch (error) { + console.error("folderList 에러 : ", error); + } + } + }; + + fetchFolderData(); + }, []); + + return ( +
+ {folderData.length > 0 ? ( + <> +
+ ); +} + +export default FolderList; diff --git a/components/Folder/Header.tsx b/components/Folder/Header.tsx new file mode 100644 index 000000000..32037f6b8 --- /dev/null +++ b/components/Folder/Header.tsx @@ -0,0 +1,21 @@ +import styled from "styled-components"; + +import Nav from "../Header-Nav"; +import AddLinkBar from "./AddLinkBar"; + +const StyledHeader = styled.div` + background-color: #f0f6ff; + width: 100%; + height: 300px; +`; + +function FolderHeader() { + return ( + +