diff --git a/apps/pyconkr-admin/src/main.tsx b/apps/pyconkr-admin/src/main.tsx index a2055cf..6545c53 100644 --- a/apps/pyconkr-admin/src/main.tsx +++ b/apps/pyconkr-admin/src/main.tsx @@ -32,6 +32,7 @@ const queryClient = new QueryClient({ const CommonOptions: Common.Contexts.ContextOptions = { debug: true, + language: "ko", baseUrl: ".", frontendDomain: import.meta.env.VITE_PYCONKR_FRONTEND_DOMAIN, backendApiDomain: import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN, diff --git a/apps/pyconkr/src/App.tsx b/apps/pyconkr/src/App.tsx index f02b9f1..1471c9c 100644 --- a/apps/pyconkr/src/App.tsx +++ b/apps/pyconkr/src/App.tsx @@ -1,36 +1,48 @@ import * as Common from "@frontend/common"; -import React from "react"; -import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; +import * as React from "react"; +import { Route, Routes, useLocation } from "react-router-dom"; +import * as R from "remeda"; -import MainLayout from "./components/layout"; -import { Test } from "./components/pages/test"; -import { IS_DEBUG_ENV } from "./consts/index.ts"; -import { SponsorProvider } from "./contexts/SponsorContext"; +import MainLayout from "./components/layout/index.tsx"; +import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx"; +import { Test } from "./components/pages/test.tsx"; +import { IS_DEBUG_ENV } from "./consts"; +import { useAppContext } from "./contexts/app_context"; +import BackendAPISchemas from "../../../packages/common/src/schemas/backendAPI"; -// 스폰서를 표시할 페이지 경로 설정 -const SPONSOR_VISIBLE_PATHS = ["/"]; +export const App: React.FC = () => { + const backendAPIClient = Common.Hooks.BackendAPI.useBackendClient(); + const { data: flatSiteMap } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendAPIClient); + const siteMapNode = Common.Utils.buildNestedSiteMap(flatSiteMap)?.[""]; -const AppContent = () => { const location = useLocation(); - const shouldShowSponsor = SPONSOR_VISIBLE_PATHS.includes(location.pathname); + const { setAppContext, language } = useAppContext(); - return ( - - - }> - {IS_DEBUG_ENV && } />} - } /> - } /> - - - - ); -}; + React.useEffect(() => { + (async () => { + const currentRouteCodes = ["", ...location.pathname.split("/").filter((code) => !R.isEmpty(code))]; + const currentSiteMapDepth: (BackendAPISchemas.NestedSiteMapSchema | undefined)[] = [siteMapNode]; + + for (const routeCode of currentRouteCodes.splice(1)) { + currentSiteMapDepth.push(currentSiteMapDepth.at(-1)?.children[routeCode]); + if (R.isNullish(currentSiteMapDepth.at(-1))) { + console.warn(`Route not found in site map: ${routeCode}`); + break; + } + } + + setAppContext((ps) => ({ ...ps, siteMapNode, currentSiteMapDepth })); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location, language, flatSiteMap]); -export const App: React.FC = () => { return ( - - - + + }> + {IS_DEBUG_ENV && } />} + } /> + } /> + + ); }; diff --git a/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx b/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx index 1a6fdaf..464b7af 100644 --- a/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx +++ b/apps/pyconkr/src/components/layout/BreadCrumb/index.tsx @@ -1,56 +1,71 @@ -import styled from "@emotion/styled"; -import { useEffect, useState } from "react"; +import { Stack, styled } from "@mui/material"; +import * as React from "react"; +import { Link } from "react-router-dom"; +import * as R from "remeda"; -export default function BreadCrumb() { - const [breadcrumbInfo, setBreadcrumbInfo] = useState({ - paths: [{ text: "홈", href: "/" }], - title: "파이콘 한국 행동강령(CoC)", - }); +import BackendAPISchemas from "../../../../../../packages/common/src/schemas/backendAPI"; - useEffect(() => { - const mockPathInfo = { - paths: [ - { text: "홈", href: "/" }, - { text: "파이콘 한국", href: "/about" }, - ], - title: "파이콘 한국 행동강령(CoC)", - }; - setBreadcrumbInfo(mockPathInfo); - }, []); +type BreadCrumbPropType = { + title: string; + parentSiteMaps: (BackendAPISchemas.NestedSiteMapSchema | undefined)[]; +}; +export const BreadCrumb: React.FC = ({ title, parentSiteMaps }) => { + let route = "/"; return ( - - {breadcrumbInfo.paths.map((item, index) => ( - - {index > 0 && >} - {item.text} - - ))} + + {parentSiteMaps + .slice(1, -1) + .filter((routeInfo) => R.isNonNullish(routeInfo)) + .map(({ route_code, name }, index) => { + route += `${route_code}/`; + return ( + + {index > 0 && >} + + + ); + })} - {breadcrumbInfo.title} + {title} ); -} +}; -const BreadCrumbContainer = styled.div` - width: 100%; - padding: 14px 117px; - background-color: rgba(255, 255, 255, 0.7); - background-image: linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.45)); - box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - gap: 5px; -`; +const BreadCrumbContainer = styled(Stack)(({ theme }) => ({ + position: "fixed", + + top: "3.625rem", + width: "100%", + height: "4.5rem", + background: "linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.45))", + boxShadow: "0 1px 10px rgba(0, 0, 0, 0.1)", + backdropFilter: "blur(10px)", + + gap: "0.25rem", + justifyContent: "center", + alignItems: "flex-start", + + zIndex: theme.zIndex.appBar - 1, + + paddingRight: "8rem", + paddingLeft: "8rem", + + [theme.breakpoints.down("lg")]: { + paddingRight: "2rem", + paddingLeft: "2rem", + }, + [theme.breakpoints.down("sm")]: { + paddingRight: "1rem", + paddingLeft: "1rem", + }, +})); -const BreadcrumbPathContainer = styled.div` +const BreadcrumbPathContainer = styled(Stack)` font-size: 9.75px; font-weight: 300; color: #000000; - display: flex; - align-items: center; - gap: 0; a { color: #000000; @@ -67,7 +82,7 @@ const BreadcrumbPathContainer = styled.div` } `; -const PageTitle = styled.h1` +const PageTitle = styled("h1")` font-size: 27px; font-weight: 600; color: #000000; diff --git a/apps/pyconkr/src/components/layout/Header/index.tsx b/apps/pyconkr/src/components/layout/Header/index.tsx index d9aab15..b8301fd 100644 --- a/apps/pyconkr/src/components/layout/Header/index.tsx +++ b/apps/pyconkr/src/components/layout/Header/index.tsx @@ -1,57 +1,287 @@ -import styled from "@emotion/styled"; import * as Common from "@frontend/common"; -import { useNavigate } from "react-router-dom"; +import { ArrowForwardIos } from "@mui/icons-material"; +import { Box, Button, CircularProgress, Divider, Stack, styled, SxProps, Theme, Typography } from "@mui/material"; +import { MUIStyledCommonProps } from "@mui/system"; +import * as React from "react"; +import { Link } from "react-router-dom"; +import * as R from "remeda"; -import BreadCrumb from "../BreadCrumb"; +import BackendAPISchemas from "../../../../../../packages/common/src/schemas/backendAPI"; +import { useAppContext } from "../../../contexts/app_context"; import LanguageSelector from "../LanguageSelector"; import LoginButton from "../LoginButton"; -import Nav from "../Nav"; + +type MenuType = BackendAPISchemas.NestedSiteMapSchema; +type MenuOrUndefinedType = MenuType | undefined; + +type NavigationStateType = { + depth1?: MenuType; + depth2?: MenuType; + depth3?: MenuType; +}; + +const HeaderHeight: React.CSSProperties["height"] = "3.625rem"; +const BreadCrumbHeight: React.CSSProperties["height"] = "4.5rem"; const Header: React.FC = () => { - const navigate = useNavigate(); + const { title, language, siteMapNode, currentSiteMapDepth, shouldShowTitleBanner } = useAppContext(); + const [navState, setNavState] = React.useState({}); + + const resetDepths = () => setNavState({}); + const setDepth1 = (depth1: MenuOrUndefinedType) => setNavState({ depth1 }); + const setDepth2 = (depth2: MenuOrUndefinedType) => setNavState((ps) => ({ ...ps, depth2, depth3: undefined })); + const setDepth3 = (depth3: MenuOrUndefinedType) => setNavState((ps) => ({ ...ps, depth3 })); + + const getDepth2Route = (nextRoute?: string) => (navState.depth1?.route_code || "") + `/${nextRoute || ""}`; + const getDepth3Route = (nextRoute?: string) => getDepth2Route(navState.depth2?.route_code) + `/${nextRoute || ""}`; + + React.useEffect(resetDepths, [language]); + + let breadCrumbRoute = ""; + let breadCrumbArray = currentSiteMapDepth.slice(1, -1); + if (R.isEmpty(breadCrumbArray)) breadCrumbArray = currentSiteMapDepth.slice(0, -1); + + const headerContainerStyle: SxProps = shouldShowTitleBanner + ? {} + : { + backgroundColor: "transparent", + [":hover"]: { backgroundColor: (theme) => theme.palette.primary.light }, + }; return ( - <> - - navigate("/")}> - - -