diff --git a/src/App.tsx b/src/App.tsx index 2ec9ece..bb91b7f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,59 +1,14 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import Layout from "./components/Layout"; +import routerInfo from "./shared/routing/routerInfo.tsx"; -import Layout from "./components/Layout/index.tsx"; -import Main from "./pages/Main/index.tsx"; -import Artist from "./pages/Artist/index.tsx"; -import Booth from "./pages/Booth/index.tsx"; -import BoothNFoodList from "./pages/BoothNFoodList/index.tsx"; -import Foodtruck from "./pages/Foodtruck/index.tsx"; -import Makers from "./pages/Makers/index.tsx"; -import Map from "./pages/Map/index.tsx"; -import Notice from "./pages/Notice/index.tsx"; -import QnA from "./pages/QnA/index.tsx"; -import Timetable from "./pages/Timetable/index.tsx"; +const RouterPath = routerInfo.map((info) => { + return { + path: info.path, + element: info.element, + }; +}); -const RouterPath = [ - { - path: "", - element:
, - }, - { - path: "artist/:id", - element: , - }, - { - path: "booth/:id", - element: , - }, - { - path: "booth_foodtruck_list", - element: , - }, - { - path: "foodtruck/:id", - element: , - }, - { - path: "makers", - element: , - }, - { - path: "map", - element: , - }, - { - path: "notice", - element: , - }, - { - path: "QnA", - element: , - }, - { - path: "timetable", - element: , - }, -]; const router = createBrowserRouter([ { path: "/", diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..08c652d --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + diff --git a/src/assets/menu_buton.json b/src/assets/menu_buton.json new file mode 100644 index 0000000..cf5bf59 --- /dev/null +++ b/src/assets/menu_buton.json @@ -0,0 +1 @@ +{"v":"5.4.4","fr":60,"ip":0,"op":140,"w":100,"h":100,"nm":"Menu","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Buttom","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[0],"e":[-45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[-45],"e":[-45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":85,"s":[-45],"e":[0]},{"t":105}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[51,70,0],"e":[51,78,0],"to":[0,1.333,0],"ti":[0,3.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":23,"s":[51,78,0],"e":[51,50,0],"to":[0,-3.333,0],"ti":[0,4.667,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":36,"s":[51,50,0],"e":[51,50,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":110,"s":[51,50,0],"e":[51,78,0],"to":[0,4.667,0],"ti":[0,-3.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":123,"s":[51,78,0],"e":[51,70,0],"to":[0,3.333,0],"ti":[0,1.333,0]},{"t":136}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[31,4],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Buttom","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":331,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Center","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[0],"e":[45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[45],"e":[45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":85,"s":[45],"e":[0]},{"t":105}],"ix":10},"p":{"a":0,"k":[51,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[31,4],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Center","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":331,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Upper","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[0],"e":[-45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[-45],"e":[-45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":85,"s":[-45],"e":[0]},{"t":105}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[51,30,0],"e":[51,22,0],"to":[0,-1.333,0],"ti":[0,-3.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":23,"s":[51,22,0],"e":[51,50,0],"to":[0,3.333,0],"ti":[0,-4.667,0]},{"i":{"x":0.831,"y":0.831},"o":{"x":0.165,"y":0.165},"t":36,"s":[51,50,0],"e":[51,50,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":110,"s":[51,50,0],"e":[51,22,0],"to":[0,-4.667,0],"ti":[0,3.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":123,"s":[51,22,0],"e":[51,30,0],"to":[0,-3.333,0],"ti":[0,-1.333,0]},{"t":136}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[31,4],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[200,200],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Upper","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":331,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/src/components/Header/.css.ts b/src/components/Header/.css.ts new file mode 100644 index 0000000..e2b1e15 --- /dev/null +++ b/src/components/Header/.css.ts @@ -0,0 +1,122 @@ +import { style } from "@vanilla-extract/css"; +import { vars } from "../../shared/styles/vars.css"; + +const mobileBreakpoint = "1000px"; + +export const headerStyles = style({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "10px 20px", + position: "relative", +}); + +export const logoStyles = style({ + width: "4em", + height: "auto", + fill: vars.color.green2, + ":hover": { + fill: vars.color.green1, + }, +}); + +export const currentPageStyles = style({ + color: "white", + fontSize: "16px", + fontWeight: "bold", +}); + +export const toggleBtnStyles = style({ + background: "none", + border: "none", + color: "white", + fontSize: "24px", + cursor: "pointer", + display: "block", + height: "1.5em", + "@media": { + [`(min-width: ${mobileBreakpoint})`]: { + display: "none", + }, + }, +}); + +const menuTransition = "transform 0.3s ease-out, opacity 0.3s ease-out"; + +export const menuStyles = style({ + position: "absolute", + top: "100%", + left: 0, + right: 0, + padding: "20px", + boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)", + transform: "translateX(-100%)", + opacity: 0, + pointerEvents: "none", + transition: menuTransition, + selectors: { + "&.active": { + transform: "translateX(0)", + opacity: 1, + pointerEvents: "auto", + backdropFilter: "blur(5px)", + }, + }, + "@media": { + [`(min-width: ${mobileBreakpoint})`]: { + position: "static", + display: "flex", + flexDirection: "row", + boxShadow: "none", + padding: 0, + transform: "none", + opacity: 1, + pointerEvents: "auto", + transition: "none", + }, + }, +}); + +export const menuListStyles = style({ + listStyle: "none", + padding: 0, + margin: 0, + display: "flex", + flexDirection: "column", + alignItems: "flex-end", + "@media": { + [`(min-width: ${mobileBreakpoint})`]: { + flexDirection: "row", + alignItems: "center", + }, + }, +}); + +export const menuItemStyles = style({ + margin: "16px", + fontSize: "large", + "@media": { + [`(min-width: ${mobileBreakpoint})`]: { + margin: "0 0 0 20px", + }, + }, +}); + +export const menuItemLinkStyles = style({ + display: "inline-block", + padding: "10px 15px", + color: "white", + textDecoration: "none", + fontSize: "16px", + borderRadius: "20px", + backgroundColor: "rgba(255, 255, 255, 0.1)", + transition: "background-color 0.3s ease", + ":hover": { + backgroundColor: "rgba(255, 255, 255, 0.2)", + }, +}); + +export const highlightStyles = style({ + fontWeight: "bold", + backgroundColor: "rgba(255, 255, 255, 0.3)", +}); diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx new file mode 100644 index 0000000..9ca1da3 --- /dev/null +++ b/src/components/Header/index.tsx @@ -0,0 +1,116 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Link, matchPath, useLocation } from "react-router-dom"; +import routerInfo from "../../shared/routing/routerInfo.tsx"; +import Logo from "../../assets/logo.svg?react"; +import Menu from "../../assets/menu_buton.json"; +import { + currentPageStyles, + headerStyles, + highlightStyles, + logoStyles, + menuItemLinkStyles, + menuItemStyles, + menuListStyles, + menuStyles, + toggleBtnStyles, +} from "./.css.ts"; +import { routerInfoType } from "../../shared/types/routing.ts"; +import Lottie, { LottieRefCurrentProps } from "lottie-react"; + +/** + * 현재 경로인지 확인하는 함수 + * @param path 비교할 경로 + * @param pathname 현재 경로(useLocation().pathname) + * @return boolean + */ +const isCurrentPath = (path: string, pathname: string): boolean => + !!matchPath({ path, end: true }, pathname); + +/** + * Header 컴포넌트 + * @component + * @returns {React.ReactElement} Header 컴포넌트 요소 + */ +export const Header: React.FC = () => { + const [isActive, setIsActive] = useState(false); + const [currentPage, setCurrentPage] = useState("/"); + const location = useLocation(); + + // lottie 애니메이션을 제어하기 위한 ref | 참고: https://lottiereact.com/#calling-the-methods + const lottieRef = useRef(null); + + /** + * 토글 버튼 클릭 이벤트 핸들러 + * @function handleToggle + * @return {void} + */ + const handleToggle = useCallback(() => { + setIsActive((prevState) => { + const newState = !prevState; + if (lottieRef.current) { + if (newState) { + lottieRef.current.playSegments([0, 60], true); + } else { + lottieRef.current.playSegments([60, 120], true); + } + } + return newState; + }); + }, []); + + /** + * 컴포넌트가 마운트될 때 현재 페이지를 설정하고, isActive를 false로 초기화함 + */ + useEffect(() => { + const currentRoute = routerInfo.find((route) => isCurrentPath(route.path, location.pathname)); + setCurrentPage(currentRoute?.korean ?? "/"); + setIsActive(false); + lottieRef.current?.setSpeed(1.5); + }, [location.pathname]); + + return ( +
+ {/*로고 부분*/} + + + + + {/*현재 페이지 출력 부분*/} + {currentPage} + + {/*토글 버튼 (lottie)*/} + + + {/*페이지 메뉴*/} + +
+ ); +}; + +export default Header; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 3c8f64a..78fb21f 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,11 +1,13 @@ import { Outlet } from "react-router-dom"; import "../../shared/styles/globalStyle.css"; import backgroundImg from "../../assets/background.png"; +import Header from "../Header"; // TODO: 모든 페이지에 공통으로 들어가는 레이아웃을 작성합니다. 필요 없으면 삭제해주세요 export default function Layout() { return (
+
background
diff --git a/src/shared/routing/routerInfo.tsx b/src/shared/routing/routerInfo.tsx new file mode 100644 index 0000000..d5c5516 --- /dev/null +++ b/src/shared/routing/routerInfo.tsx @@ -0,0 +1,86 @@ +import { routerInfoType } from "../types/routing.ts"; +import Main from "../../pages/Main"; +import Artist from "../../pages/Artist"; +import Booth from "../../pages/Booth"; +import BoothNFoodList from "../../pages/BoothNFoodList"; +import Foodtruck from "../../pages/Foodtruck"; +import Makers from "../../pages/Makers"; +import Map from "../../pages/Map"; +import Notice from "../../pages/Notice"; +import QnA from "../../pages/QnA"; +import Timetable from "../../pages/Timetable"; + +export const routerInfo: routerInfoType[] = [ + { + path: "/", + element:
, + english: "Main", + korean: "메인", + expose: true, + }, + { + path: "artist/:id", + element: , + english: "Artist", + korean: "아티스트", + expose: false, + }, + { + path: "booth/:id", + element: , + english: "Booth", + korean: "부스", + expose: false, + }, + { + path: "booth_foodtruck_list", + element: , + english: "Booth & Foodtruck List", + korean: "부스 & 푸드트럭 리스트", + expose: true, + }, + { + path: "foodtruck/:id", + element: , + english: "Foodtruck", + korean: "푸드트럭", + expose: false, + }, + { + path: "makers", + element: , + english: "Makers", + korean: "메이커스", + expose: false, + }, + { + path: "map", + element: , + english: "Map", + korean: "지도", + expose: true, + }, + { + path: "notice", + element: , + english: "Notice", + korean: "공지사항", + expose: true, + }, + { + path: "QnA", + element: , + english: "QnA", + korean: "QnA", + expose: true, + }, + { + path: "timetable", + element: , + english: "Timetable", + korean: "타임테이블", + expose: true, + }, +]; + +export default routerInfo; diff --git a/src/shared/types/routing.ts b/src/shared/types/routing.ts new file mode 100644 index 0000000..7392783 --- /dev/null +++ b/src/shared/types/routing.ts @@ -0,0 +1,8 @@ +// 라우팅 정보 객체 타입 +export interface routerInfoType { + path: string; + element: React.ReactElement; + english: string; + korean: string; + expose: boolean; +}