diff --git a/apps/pyconkr-2026/index.html b/apps/pyconkr-2026/index.html index 6d1a4d2..b921dcb 100644 --- a/apps/pyconkr-2026/index.html +++ b/apps/pyconkr-2026/index.html @@ -1,18 +1,18 @@ - + - - - + + + - - - + + + - - + + @@ -22,7 +22,9 @@ - + shrink-to-fit=no" + /> diff --git a/apps/pyconkr-2026/package.json b/apps/pyconkr-2026/package.json index bbec73a..0351ea9 100644 --- a/apps/pyconkr-2026/package.json +++ b/apps/pyconkr-2026/package.json @@ -1,13 +1,13 @@ { - "name": "@apps/pyconkr-2026", - "dependencies": { - "@frontend/common": "workspace:*", - "@frontend/shop": "workspace:*" - }, - "devDependencies": { - "vite": "^6.3.5", - "vite-plugin-mdx": "^3.6.1", - "vite-plugin-mkcert": "^1.17.8", - "vite-plugin-svgr": "^4.3.0" - } + "name": "@apps/pyconkr-2026", + "dependencies": { + "@frontend/common": "workspace:*", + "@frontend/shop": "workspace:*" + }, + "devDependencies": { + "vite": "^6.3.5", + "vite-plugin-mdx": "^3.6.1", + "vite-plugin-mkcert": "^1.17.8", + "vite-plugin-svgr": "^4.3.0" + } } diff --git a/apps/pyconkr-2026/src/App.tsx b/apps/pyconkr-2026/src/App.tsx index de9e00b..051811e 100644 --- a/apps/pyconkr-2026/src/App.tsx +++ b/apps/pyconkr-2026/src/App.tsx @@ -1,10 +1,10 @@ import { useBackendClient, useFlattenSiteMapQuery, useSponsorQuery } from "@frontend/common/src/hooks/useAPI"; +import * as BackendAPISchemas from "@frontend/common/src/schemas/backendAPI"; import { buildNestedSiteMap } from "@frontend/common/src/utils"; import * as React from "react"; import { Route, Routes, useLocation } from "react-router-dom"; import * as R from "remeda"; -import * as BackendAPISchemas from "@frontend/common/src/schemas/backendAPI"; import MainLayout from "./components/layout/index.tsx"; import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx"; import { PresentationDetailPage } from "./components/pages/presentation_detail.tsx"; diff --git a/apps/pyconkr-2026/src/assets/pyconkr2026_main_cover_image.png b/apps/pyconkr-2026/src/assets/pyconkr2026_main_cover_image.png new file mode 100644 index 0000000..97f37df Binary files /dev/null and b/apps/pyconkr-2026/src/assets/pyconkr2026_main_cover_image.png differ diff --git a/apps/pyconkr-2026/src/components/layout/Footer/Mobile/MobileFooter.tsx b/apps/pyconkr-2026/src/components/layout/Footer/Mobile/MobileFooter.tsx new file mode 100644 index 0000000..2eb9368 --- /dev/null +++ b/apps/pyconkr-2026/src/components/layout/Footer/Mobile/MobileFooter.tsx @@ -0,0 +1,167 @@ +import styled from "@emotion/styled"; +import { useEmail } from "@frontend/common/src/hooks/useEmail"; +import { Article, Email, Facebook, GitHub, Instagram, LinkedIn, X, YouTube } from "@mui/icons-material"; +import * as React from "react"; + +import FlickrIcon from "@apps/pyconkr-2026/assets/thirdparty/flickr.svg?react"; + +import { useAppContext } from "../../../../contexts/app_context"; + +interface IconItem { + icon: React.FC<{ width?: number; height?: number }>; + alt: string; + href: string; +} + +const defaultIcons: IconItem[] = [ + { icon: Facebook, alt: "facebook", href: "https://www.facebook.com/pyconkorea/" }, + { icon: YouTube, alt: "YouTube", href: "https://www.youtube.com/c/PyConKRtube" }, + { icon: X, alt: "X", href: "https://x.com/PyConKR" }, + { icon: GitHub, alt: "github", href: "https://github.com/pythonkr" }, + { icon: Instagram, alt: "Instagram", href: "https://www.instagram.com/pycon_korea/" }, + { icon: LinkedIn, alt: "LinkedIn", href: "https://www.linkedin.com/company/pyconkorea/" }, + { icon: Article, alt: "blog", href: "https://blog.pycon.kr/" }, + { icon: FlickrIcon, alt: "Flickr", href: "https://www.flickr.com/photos/126829363@N08/" }, +]; + +export default function MobileFooter() { + const { sendEmail } = useEmail(); + const { language } = useAppContext(); + + const title = language === "ko" ? "파이콘 한국 2026" : "PyCon Korea 2026"; + const committeeTitle = + language === "ko" + ? "파이콘 한국 2026은 파이콘 한국 준비위원회가 만들고 있습니다" + : "PyCon Korea 2026 is organized by the PyCon Korea Organizing Committee"; + const djangoTitle = language === "ko" ? "파이썬 웹 프레임워크 Django로 만들었습니다" : "Built with the Django web framework for Python"; + + const links = [ + { + text: language === "ko" ? "파이콘 한국 행동 강령(CoC)" : "PyCon Korea Code of Conduct", + href: "https://pythonkr.github.io/pycon-code-of-conduct/ko/coc/a_intent_and_purpose.html", + }, + { text: language === "ko" ? "서비스 이용 약관" : "Terms of Service", href: "/about/terms-of-service" }, + { text: language === "ko" ? "개인 정보 처리 방침" : "Privacy Policy", href: "/about/privacy-policy" }, + ]; + + return ( + + + +
+ +
+ +
+ +
+
+ + {links.map((link, index) => ( + + {link.text} + {index < links.length - 1 && |} + + ))} + + + + + {defaultIcons.map((icon) => ( + + + ))} + +
+
+ ); +} + +const FooterContainer = styled.footer` + background: linear-gradient( + to bottom, + ${({ theme }) => theme.palette.background.default} 0%, + ${({ theme }) => theme.palette.background.paper} 25%, + ${({ theme }) => theme.palette.primary.dark} 75%, + ${({ theme }) => theme.palette.primary.main} 100% + ); + color: ${({ theme }) => theme.palette.text.primary}; + font-size: 0.75rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + max-height: 16rem; + padding: 5rem 0 1rem 0; +`; + +const FooterContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.75rem; +`; + +const FooterBoldText = styled.text` + font-weight: 600; +`; + +const FooterNormalText = styled.text` + font-weight: 400; +`; + +const FooterSlogan = styled.div` + text-align: center; +`; + +const FooterLinkSlogan = styled.div` + display: flex; + gap: 0.3rem; +`; + +const FooterLinks = styled.div` + display: flex; + align-items: center; + gap: 0.3rem; +`; + +const FooterIcons = styled.div` + display: flex; + align-items: center; + gap: 9px; +`; + +const Link = styled.a` + color: ${({ theme }) => theme.palette.text.primary}; + text-decoration: none; + &:hover { + text-decoration: underline; + } +`; + +const Separator = styled.span` + color: ${({ theme }) => theme.palette.text.primary}; + opacity: 0.5; + margin: 0.05rem 0; +`; + +const IconLink = styled.a` + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: ${({ theme }) => theme.palette.text.primary}; + + &:hover { + opacity: 0.8; + } + + img { + width: 20px; + height: 20px; + } +`; diff --git a/apps/pyconkr-2026/src/components/layout/Footer/index.tsx b/apps/pyconkr-2026/src/components/layout/Footer/index.tsx index 7f947fb..a010177 100644 --- a/apps/pyconkr-2026/src/components/layout/Footer/index.tsx +++ b/apps/pyconkr-2026/src/components/layout/Footer/index.tsx @@ -1,12 +1,13 @@ import styled from "@emotion/styled"; import { useEmail } from "@frontend/common/src/hooks/useEmail"; import { Article, Email, Facebook, GitHub, Instagram, LinkedIn, OpenInNew, X, YouTube } from "@mui/icons-material"; -import { Button } from "@mui/material"; +import { Button, useMediaQuery, useTheme } from "@mui/material"; import * as React from "react"; import FlickrIcon from "@apps/pyconkr-2026/assets/thirdparty/flickr.svg?react"; import { useAppContext } from "../../../contexts/app_context"; +import MobileFooter from "./Mobile/MobileFooter"; interface IconItem { icon: React.FC<{ width?: number; height?: number }>; @@ -49,9 +50,13 @@ const Bar: React.FC = () =>
; + const corpPasamoStr = language === "ko" ? "사단법인 파이썬사용자모임" : "Python Korea"; const corpAddressStr = language === "ko" ? "서울특별시 강남구 강남대로84길 24-4" : "24-4, Gangnam-daero 84-gil, Gangnam-gu, Seoul, Republic of Korea"; @@ -137,8 +142,14 @@ export default function Footer() { } const FooterContainer = styled.footer` - background-color: ${({ theme }) => theme.palette.primary.main}; - color: ${({ theme }) => theme.palette.common.white}; + background: linear-gradient( + to bottom, + ${({ theme }) => theme.palette.background.default} 0%, + ${({ theme }) => theme.palette.background.paper} 25%, + ${({ theme }) => theme.palette.primary.dark} 75%, + ${({ theme }) => theme.palette.primary.main} 100% + ); + color: ${({ theme }) => theme.palette.text.primary}; font-size: 0.75rem; display: flex; flex-direction: column; diff --git a/apps/pyconkr-2026/src/components/layout/Header/Mobile/HamburgerButton.tsx b/apps/pyconkr-2026/src/components/layout/Header/Mobile/HamburgerButton.tsx new file mode 100644 index 0000000..18f90b5 --- /dev/null +++ b/apps/pyconkr-2026/src/components/layout/Header/Mobile/HamburgerButton.tsx @@ -0,0 +1,45 @@ +import { IconButton, styled } from "@mui/material"; +import * as React from "react"; + +interface HamburgerButtonProps { + isOpen: boolean; + onClick: () => void; +} + +export const HamburgerButton: React.FC = ({ isOpen, onClick }) => { + return ( + + + + + + + + ); +}; + +const StyledIconButton = styled(IconButton)({ + padding: 0, + width: 26, + height: 18, + color: "#ededde", +}); + +const HamburgerIcon = styled("div")<{ isOpen: boolean }>(({ isOpen }) => ({ + width: 26, + height: 18, + position: "relative", + cursor: "pointer", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + + "& span": { + display: "block", + height: isOpen ? 3 : 2, + width: "100%", + backgroundColor: "#ededde", + borderRadius: 1, + transition: "height 0.3s ease", + }, +})); diff --git a/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileHeader.tsx b/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileHeader.tsx new file mode 100644 index 0000000..98c5f1c --- /dev/null +++ b/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileHeader.tsx @@ -0,0 +1,58 @@ +import { Components } from "@frontend/common"; +import { Box, Stack, styled, Typography } from "@mui/material"; +import * as React from "react"; +import { Link } from "react-router-dom"; + +import { HamburgerButton } from "./HamburgerButton"; +import { MobileNavigation } from "./MobileNavigation"; +import LanguageSelector from "../../LanguageSelector"; +import { useAppContext } from "../../../../contexts/app_context"; + +export const MobileHeader: React.FC = () => { + const { siteMapNode } = useAppContext(); + const [isOpen, setIsOpen] = React.useState(false); + + return ( + <> + + + setIsOpen(!isOpen)} /> + + + + + 파이콘 한국 2026 + + + + + + + + setIsOpen(false)} siteMapNode={siteMapNode} /> + + ); +}; + +const MobileHeaderContainer = styled("header")<{ isOpen: boolean }>(({ theme, isOpen }) => ({ + display: isOpen ? "none" : "flex", + alignItems: "center", + justifyContent: "space-between", + + position: "sticky", + top: 0, + left: 0, + right: 0, + width: "100%", + height: 60, + padding: "15px 20px", + + backgroundColor: "rgba(18, 9, 30, 0.9)", + backdropFilter: "blur(10px)", + WebkitBackdropFilter: "blur(10px)", + borderBottom: "1px solid rgba(237, 94, 189, 0.2)", + + zIndex: theme.zIndex.appBar, +})); + +const LeftContent = styled(Box)({ display: "flex", alignItems: "center", gap: 17 }); diff --git a/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileNavigation.tsx b/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileNavigation.tsx new file mode 100644 index 0000000..7848de4 --- /dev/null +++ b/apps/pyconkr-2026/src/components/layout/Header/Mobile/MobileNavigation.tsx @@ -0,0 +1,264 @@ +import { Components } from "@frontend/common"; +import * as BackendAPISchemas from "@frontend/common/src/schemas/backendAPI"; +import { ArrowBack, ArrowForward } from "@mui/icons-material"; +import { Box, Button, Chip, Drawer, IconButton, Stack, styled } from "@mui/material"; +import * as React from "react"; +import { Link } from "react-router-dom"; +import * as R from "remeda"; + +import { HamburgerButton } from "./HamburgerButton"; +import LanguageSelector from "../../LanguageSelector"; + +type MenuType = BackendAPISchemas.NestedSiteMapSchema; + +interface MobileNavigationProps { + isOpen: boolean; + onClose: () => void; + siteMapNode?: MenuType; +} + +type NavigationLevel = "depth1" | "depth2" | "depth3"; + +interface NavigationState { + level: NavigationLevel; + depth1?: MenuType; + depth2?: MenuType; + breadcrumbs: { name: string; level: NavigationLevel }[]; +} + +export const MobileNavigation: React.FC = ({ isOpen, onClose, siteMapNode }) => { + const [navState, setNavState] = React.useState({ level: "depth1", breadcrumbs: [] }); + + const resetNavigation = () => setNavState({ level: "depth1", breadcrumbs: [] }); + + const navigateToDepth2 = (depth1: MenuType) => { + setNavState({ level: "depth2", depth1, breadcrumbs: [{ name: depth1.name, level: "depth1" }] }); + }; + + const navigateToDepth3 = (depth2: MenuType) => { + setNavState((prev) => ({ + ...prev, + level: "depth3", + depth2, + breadcrumbs: [...prev.breadcrumbs, { name: depth2.name, level: "depth2" }], + })); + }; + + const goBack = () => { + if (navState.level === "depth3") { + setNavState((prev) => ({ ...prev, level: "depth2", depth2: undefined, breadcrumbs: prev.breadcrumbs.slice(0, -1) })); + } else if (navState.level === "depth2") { + resetNavigation(); + } + }; + + const handleClose = () => { + onClose(); + resetNavigation(); + }; + + const renderDepth1Menu = () => { + if (!siteMapNode) return null; + return ( + + {Object.values(siteMapNode.children) + .filter((s) => !s.hide) + .map((menu) => ( + + {!R.isEmpty(menu.children) && Object.values(menu.children).some((c) => !c.hide) ? ( + navigateToDepth2(menu)}>{menu.name} + ) : ( + + {menu.name} + + )} + {!R.isEmpty(menu.children) && Object.values(menu.children).some((c) => !c.hide) && ( + navigateToDepth2(menu)}> + + + )} + + ))} + + ); + }; + + const renderDepth2Menu = () => { + if (!navState.depth1) return null; + return ( + + + + + + {navState.depth1.name} + + + + {Object.values(navState.depth1.children) + .filter((s) => !s.hide) + .map((menu) => ( + + + + + {!R.isEmpty(menu.children) && Object.values(menu.children).some((c) => !c.hide) && ( + navigateToDepth3(menu)}> + + + )} + + ))} + + + ); + }; + + const renderDepth3Menu = () => { + if (!navState.depth2) return null; + return ( + + + + + + {navState.depth2.name} + + + + {Object.values(navState.depth2.children) + .filter((s) => !s.hide) + .map((menu) => ( + + + + ))} + + + ); + }; + + return ( + + + + + + + + 파이콘 한국 2026 + + + + + + {navState.level === "depth1" && renderDepth1Menu()} + {navState.level === "depth2" && renderDepth2Menu()} + {navState.level === "depth3" && renderDepth3Menu()} + + + + + + + + ); +}; + +const StyledDrawer = styled(Drawer)({ + "& .MuiDrawer-paper": { + width: "70vw", + background: "rgba(18, 9, 30, 0.97)", + backdropFilter: "blur(12px)", + WebkitBackdropFilter: "blur(12px)", + color: "#ededde", + borderTopRightRadius: 15, + borderBottomRightRadius: 15, + borderRight: "1px solid rgba(237, 94, 189, 0.2)", + }, +}); + +const DrawerContent = styled(Box)({ height: "100%", display: "flex", flexDirection: "column" }); + +const NavigationHeader = styled(Box)({ + display: "flex", + alignItems: "center", + padding: "23px 23px 10px 23px", + gap: 17, +}); + +const HeaderTitle = styled("span")({ + color: "#ededde", + fontSize: "16px", + fontWeight: 600, +}); + +const NavigationContent = styled(Box)({ flex: 1, overflow: "auto" }); + +const MenuContainer = styled(Stack)({ padding: "20px 0", gap: "25px" }); + +const MenuItem = styled(Box)({ display: "flex", alignItems: "center", padding: "0 23px", gap: 23 }); + +const MenuLink = styled(Link)({ + color: "#ededde", + textDecoration: "none", + fontSize: "20px", + fontWeight: 600, +}); + +const MenuButton = styled(Button)({ + color: "#ededde", + textTransform: "none", + fontSize: "20px", + fontWeight: 600, + padding: 0, + minWidth: "auto", + justifyContent: "flex-start", +}); + +const MenuArrowButton = styled(IconButton)({ color: "#ededde", padding: 8 }); + +const NavigationMenuSection = styled(Box)({ padding: "20px 0" }); + +const DepthHeader = styled(Box)({ display: "flex", alignItems: "center", padding: "0 23px 12px", gap: 8 }); + +const BackButton = styled(Button)({ + color: "#ed5ebd", + textTransform: "none", + padding: "0 15px 0 0", + minWidth: "auto", +}); + +const DepthTitle = styled("span")({ color: "#ededde", fontSize: "18px", fontWeight: 700 }); + +const DepthDivider = styled("div")({ + margin: "0 23px 16px", + height: 3, + width: "3rem", + borderRadius: 2, + backgroundColor: "#f5c73d", +}); + +const DepthMenuList = styled(Stack)({ padding: "0 23px", gap: "12px" }); + +const Depth2MenuItem = styled(Box)({ display: "flex", alignItems: "center", gap: 8 }); + +const DepthMenuGrid = styled(Box)({ padding: "0 23px", display: "flex", flexWrap: "wrap", gap: "10px" }); + +const MenuChip = styled(Chip)({ + backgroundColor: "rgba(237, 94, 189, 0.15)", + color: "#ededde", + height: 40, + borderRadius: 15, + padding: "10px 4px", + fontSize: "15px", + fontWeight: 600, + border: "1px solid rgba(237, 94, 189, 0.3)", + "&:hover": { backgroundColor: "rgba(237, 94, 189, 0.3)" }, + "& .MuiChip-label": { padding: "0 10px" }, +}); diff --git a/apps/pyconkr-2026/src/components/layout/Header/index.tsx b/apps/pyconkr-2026/src/components/layout/Header/index.tsx index 38e8fd7..b9da072 100644 --- a/apps/pyconkr-2026/src/components/layout/Header/index.tsx +++ b/apps/pyconkr-2026/src/components/layout/Header/index.tsx @@ -1,6 +1,6 @@ import { Components } from "@frontend/common"; import { ArrowForwardIos } from "@mui/icons-material"; -import { Box, Button, CircularProgress, Divider, Stack, styled, SxProps, Theme, Typography } from "@mui/material"; +import { Box, Button, CircularProgress, Divider, Stack, styled, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"; import { MUIStyledCommonProps } from "@mui/system"; import * as React from "react"; import { Link } from "react-router-dom"; @@ -8,10 +8,8 @@ import * as R from "remeda"; import { NestedSiteMapSchema } from "../../../../../../packages/common/src/schemas/backendAPI"; import { useAppContext } from "../../../contexts/app_context"; -import { CartBadgeButton } from "../CartBadgeButton"; import LanguageSelector from "../LanguageSelector"; -import { SignInButton } from "../SignInButton"; -// import { ScanCodeIconButton } from "../UserScanCodeButton"; +import { MobileHeader } from "./Mobile/MobileHeader"; type MenuType = NestedSiteMapSchema; type MenuOrUndefinedType = MenuType | undefined; @@ -25,8 +23,10 @@ type NavigationStateType = { const HeaderHeight: React.CSSProperties["height"] = "3.625rem"; const BreadCrumbHeight: React.CSSProperties["height"] = "4.5rem"; -const Header: React.FC = () => { +export default function Header() { const { title, language, siteMapNode, currentSiteMapDepth, shouldShowTitleBanner } = useAppContext(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); const [navState, setNavState] = React.useState({}); const resetDepths = () => setNavState({}); @@ -39,114 +39,110 @@ const Header: React.FC = () => { React.useEffect(resetDepths, [language]); + if (isMobile) return ; + 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 }, - }; + const headerStyle: SxProps = shouldShowTitleBanner ? {} : { backgroundColor: "transparent" }; return ( - + - - - + + + + + PyCon Korea 2026 + - {siteMapNode ? ( - <> - - {Object.values(siteMapNode.children) - .filter((s) => !s.hide) - .map((r) => ( - - - - ))} - - {navState.depth1 && ( - - - - {navState.depth1.name} - - - - - {Object.values(navState.depth1.children) - .filter((s) => !s.hide) - .map((r) => ( - setDepth2(r)} - // 하위 depth가 있는 경우, 하위 depth를 선택할 수 있도록 유지하기 위해 depth2도 유지합니다. - onMouseLeave={() => R.isEmpty(navState.depth2?.children ?? {}) && setDepth2(undefined)} - target={R.isString(r.external_link) ? "_blank" : undefined} - rel={R.isString(r.external_link) ? "noopener noreferrer" : undefined} - to={r.external_link || getDepth2Route(r.route_code)} - /> - ))} - - - {navState.depth2 && !R.isEmpty(navState.depth2.children) && ( - <> - {!R.isEmpty(navState.depth2.children) && } - - - {Object.values(navState.depth2.children) - .filter((s) => !s.hide) - .map((r) => ( - setDepth3(r)} - onMouseLeave={() => setDepth3(undefined)} - target={R.isString(r.external_link) ? "_blank" : undefined} - rel={R.isString(r.external_link) ? "noopener noreferrer" : undefined} - to={r.external_link || getDepth3Route(r?.route_code)} - /> - ))} - - - )} - - - - )} - + {siteMapNode ? ( + + {Object.values(siteMapNode.children) + .filter((s) => !s.hide) + .map((r) => ( + + setDepth1(r)} isActive={navState.depth1?.id === r.id}> + {r.name} + + + ))} + ) : ( - + )} - - - - - {/* */} - - + + + + + {navState.depth1 && ( + + + + {navState.depth1.name} + + + + + {Object.values(navState.depth1.children) + .filter((s) => !s.hide) + .map((r) => ( + setDepth2(r)} + onMouseLeave={() => R.isEmpty(navState.depth2?.children ?? {}) && setDepth2(undefined)} + target={R.isString(r.external_link) ? "_blank" : undefined} + rel={R.isString(r.external_link) ? "noopener noreferrer" : undefined} + to={r.external_link || getDepth2Route(r.route_code)} + > + {r.name} + + ))} + + + {navState.depth2 && !R.isEmpty(navState.depth2.children) && ( + <> + + + {Object.values(navState.depth2.children) + .filter((s) => !s.hide) + .map((r) => ( + setDepth3(r)} + onMouseLeave={() => setDepth3(undefined)} + target={R.isString(r.external_link) ? "_blank" : undefined} + rel={R.isString(r.external_link) ? "noopener noreferrer" : undefined} + to={r.external_link || getDepth3Route(r?.route_code)} + > + {r.name} + + ))} + + + )} + + + + )} + {shouldShowTitleBanner && ( <> @@ -157,151 +153,133 @@ const Header: React.FC = () => { breadCrumbRoute += `${route_code}/`; return ( - {index > 0 && } + {index > 0 && } ); })} - + {title} - {/* Spacer for fixed header */} )} ); -}; +} -const ResponsivePaddingDefinition = ({ theme }: MUIStyledCommonProps) => ({ +const ResponsivePadding = ({ theme }: MUIStyledCommonProps) => ({ paddingRight: theme!.spacing(16), paddingLeft: theme!.spacing(16), - - [theme!.breakpoints.down("lg")]: { - paddingRight: theme!.spacing(4), - paddingLeft: theme!.spacing(4), - }, - [theme!.breakpoints.down("sm")]: { - paddingRight: theme!.spacing(2), - paddingLeft: theme!.spacing(2), - }, + [theme!.breakpoints.down("lg")]: { paddingRight: theme!.spacing(4), paddingLeft: theme!.spacing(4) }, + [theme!.breakpoints.down("sm")]: { paddingRight: theme!.spacing(2), paddingLeft: theme!.spacing(2) }, }); const HeaderContainer = styled("header")(({ theme }) => ({ position: "fixed", - display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", - width: "100%", - minWidth: "100%", - maxWidth: "100%", height: HeaderHeight, - - backgroundColor: theme.palette.primary.light, - color: theme.palette.primary.dark, - + backgroundColor: "rgba(18, 9, 30, 0.85)", + backdropFilter: "blur(12px)", + WebkitBackdropFilter: "blur(12px)", + borderBottom: "1px solid rgba(237, 94, 189, 0.2)", + color: "#ededde", fontWeight: 500, - zIndex: theme.zIndex.appBar, transition: "background-color 0.3s ease-in-out", + "& .header-title-text": { + opacity: 0, + transition: "opacity 0.2s ease", + }, + "&:hover .header-title-text": { + opacity: 1, + }, + ...ResponsivePadding({ theme }), +})); - ...ResponsivePaddingDefinition({ theme }), +const NavButton = styled(Button)<{ isActive?: boolean }>(({ isActive }) => ({ + color: isActive ? "#ed5ebd" : "#ededde", + minWidth: 0, + textTransform: "none", + fontSize: "0.9rem", + fontWeight: isActive ? 700 : 400, + transition: "color 0.2s ease", + "&:hover": { color: "#ed5ebd", backgroundColor: "transparent" }, })); -const NavOuterContainer = styled(Stack)(({ theme }) => ({ - width: "100vw", +const NavSideElementContainer = styled(Stack)({ flexGrow: 1, flexBasis: 0 }); +const NavDropdownOuter = styled(Stack)(({ theme }) => ({ + width: "100vw", position: "fixed", left: 0, top: HeaderHeight, - zIndex: theme.zIndex.appBar + 1, - - backgroundColor: "rgba(255, 255, 255, 0.7)", - boxShadow: "0 5px 5px 0px rgba(0, 0, 0, 0.1)", - backdropFilter: "blur(10px)", - - fontSize: "0.875rem", - color: theme.palette.primary.dark, + backgroundColor: "rgba(18, 9, 30, 0.95)", + boxShadow: "0 5px 20px rgba(0, 0, 0, 0.4)", + backdropFilter: "blur(12px)", + WebkitBackdropFilter: "blur(12px)", + borderBottom: "1px solid rgba(237, 94, 189, 0.2)", })); -const NavInnerContainer = styled(Stack)(({ theme }) => ({ +const NavDropdownInner = styled(Stack)(({ theme }) => ({ width: "100%", minHeight: "10rem", overflowY: "auto", gap: "1rem", - - backgroundColor: "rgba(182, 216, 215, 0.05)", - paddingTop: "1.5rem", paddingBottom: "2rem", - - ...ResponsivePaddingDefinition({ theme }), + color: "#ededde", + ...ResponsivePadding({ theme }), })); -const NavSideElementContainer = styled(Stack)({ - flexGrow: 1, - flexBasis: 0, +const HighlightDivider = styled(Divider)({ + width: "3rem", + borderBottom: "4px solid #f5c73d", + borderColor: "#f5c73d", }); -const Depth1to2Divider = styled(Divider)(({ theme }) => ({ - width: "3.375rem", - borderBottom: `4px solid ${theme.palette.highlight.main}`, -})); - -const Depth2Item = styled(Link)(({ theme }) => ({ +const Depth2Item = styled(Link)({ + color: "#ededde", fontWeight: 300, textDecoration: "none", width: "fit-content", borderBottom: "2px solid transparent", + transition: "color 0.15s ease", + "&.active": { fontWeight: 700, borderBottom: "2px solid #ed5ebd", color: "#ed5ebd" }, + "&:hover": { color: "#ed5ebd" }, +}); - "&.active": { - fontWeight: 700, - borderBottom: `2px solid ${theme.palette.primary.dark}`, - }, -})); - -const Depth2to3Divider = styled(Divider)(({ theme }) => ({ borderColor: theme.palette.primary.light })); +const Depth2to3Divider = styled(Divider)({ borderColor: "rgba(237, 94, 189, 0.3)" }); const Depth3Item = styled(Depth2Item)({ fontSize: "0.75rem" }); const BreadCrumbContainer = styled(Stack)(({ theme }) => ({ position: "fixed", - top: HeaderHeight, width: "100%", height: BreadCrumbHeight, - 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)", + background: "rgba(18, 9, 30, 0.8)", + boxShadow: "0 1px 10px rgba(0, 0, 0, 0.3)", backdropFilter: "blur(10px)", - + WebkitBackdropFilter: "blur(10px)", + borderBottom: "1px solid rgba(237, 94, 189, 0.15)", gap: "0.25rem", justifyContent: "center", alignItems: "flex-start", - zIndex: theme.zIndex.appBar - 1, - - ...ResponsivePaddingDefinition({ theme }), - + ...ResponsivePadding({ theme }), "& a": { - color: "#000000", + color: "#f5c73d", fontWeight: 300, fontSize: "0.75rem", textDecoration: "none", - - "&:hover": { - textDecoration: "underline", - }, - }, - "& svg": { - color: "rgba(0, 0, 0, 0.5)", - fontSize: "0.75rem", + "&:hover": { textDecoration: "underline" }, }, })); - -export default Header; diff --git a/apps/pyconkr-2026/src/components/layout/index.tsx b/apps/pyconkr-2026/src/components/layout/index.tsx index ee0da09..f86462c 100644 --- a/apps/pyconkr-2026/src/components/layout/index.tsx +++ b/apps/pyconkr-2026/src/components/layout/index.tsx @@ -2,10 +2,10 @@ import styled from "@emotion/styled"; import { Stack } from "@mui/material"; import { Outlet } from "react-router-dom"; +import { useAppContext } from "../../contexts/app_context"; import Footer from "./Footer"; import Header from "./Header"; import { Sponsor } from "./Sponsor"; -import { useAppContext } from "../../contexts/app_context"; export default function MainLayout() { const { shouldShowSponsorBanner } = useAppContext(); diff --git a/apps/pyconkr-2026/src/consts/mdx_components.ts b/apps/pyconkr-2026/src/consts/mdx_components.ts index 403ea8d..974feee 100644 --- a/apps/pyconkr-2026/src/consts/mdx_components.ts +++ b/apps/pyconkr-2026/src/consts/mdx_components.ts @@ -7,9 +7,9 @@ import * as React from "react"; import PyCon2025HostLogoBig from "../../../../packages/common/src/assets/pyconkr2025_hostlogo_big.png"; import PyCon2025HostLogoSmall from "../../../../packages/common/src/assets/pyconkr2025_hostlogo_small.png"; +import PyCon2025Logo from "../../../../packages/common/src/assets/pyconkr2025_logo.png"; import PyCon2025MobileLogoImage from "../../../../packages/common/src/assets/pyconkr2025_main_cover_image.png"; import PyCon2025MobileLogoTitle from "../../../../packages/common/src/assets/pyconkr2025_main_cover_title.png"; -import PyCon2025Logo from "../../../../packages/common/src/assets/pyconkr2025_logo.png"; const MUIMDXComponents: MDXComponents = { Mui__material__Accordion: mui.Accordion, diff --git a/apps/pyconkr-2026/src/styles/globalStyles.ts b/apps/pyconkr-2026/src/styles/globalStyles.ts index 0f60b97..e21d4c9 100644 --- a/apps/pyconkr-2026/src/styles/globalStyles.ts +++ b/apps/pyconkr-2026/src/styles/globalStyles.ts @@ -7,108 +7,109 @@ export const muiTheme = createTheme({ 'Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif', }, palette: { + mode: "dark", primary: { - main: "#259299", - light: "#B6D8D7", - dark: "#126D7F", - nonFocus: "#7AB2B3", + main: "#ed5ebd", + light: "#f5a0dc", + dark: "#c93da0", + nonFocus: "#c97baa", }, secondary: { - main: "#259299", - light: "#B6D8D7", - dark: "#126D7F", + main: "#ed5ebd", + light: "#f5a0dc", + dark: "#c93da0", }, highlight: { - main: "#E17101", - light: "#EE8D74", - dark: "#C66900", - contrastText: "#FFFFFF", + main: "#f5c73d", + light: "#f9d86d", + dark: "#d4a818", + contrastText: "#12091e", }, mobileHeader: { main: { - background: "rgba(182, 216, 215, 0.1)", - text: "#FFFFFF", - activeLanguage: "#888888", + background: "rgba(237, 94, 189, 0.1)", + text: "#ededde", + activeLanguage: "#888880", }, sub: { - background: "#B6D8D7", - text: "rgba(18, 109, 127, 0.6)", - activeLanguage: "#126D7F", + background: "#1e1230", + text: "rgba(237, 94, 189, 0.6)", + activeLanguage: "#ed5ebd", }, }, mobileNavigation: { main: { background: - "linear-gradient(0deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5)), linear-gradient(0deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15))", - text: "#FFFFFF", + "linear-gradient(0deg, rgba(18, 9, 30, 0.85), rgba(18, 9, 30, 0.85)), linear-gradient(0deg, rgba(237, 94, 189, 0.08), rgba(237, 94, 189, 0.08))", + text: "#ededde", chip: { - background: "rgba(212, 212, 212, 0.5)", - hover: "rgba(212, 212, 212, 0.7)", + background: "rgba(237, 94, 189, 0.15)", + hover: "rgba(237, 94, 189, 0.25)", }, - divider: "rgba(255, 255, 255, 0.3)", + divider: "rgba(237, 94, 189, 0.2)", languageToggle: { background: "transparent", active: { - background: "rgba(255, 255, 255, 0.7)", - hover: "rgba(255, 255, 255, 0.8)", + background: "rgba(237, 94, 189, 0.3)", + hover: "rgba(237, 94, 189, 0.4)", }, inactive: { - hover: "rgba(255, 255, 255, 0.1)", + hover: "rgba(237, 94, 189, 0.1)", }, }, }, sub: { - background: "#B6D8D7", - text: "rgba(18, 109, 127, 0.9)", + background: "#1e1230", + text: "rgba(237, 94, 189, 0.9)", chip: { - background: "rgba(18, 109, 127, 0.2)", - hover: "rgba(18, 109, 127, 0.3)", + background: "rgba(237, 94, 189, 0.15)", + hover: "rgba(237, 94, 189, 0.25)", }, - divider: "rgba(18, 109, 127, 0.3)", + divider: "rgba(237, 94, 189, 0.2)", languageToggle: { - background: "rgba(255, 255, 255, 0.1)", + background: "rgba(237, 94, 189, 0.05)", active: { - background: "rgba(255, 255, 255, 0.9)", - hover: "rgba(255, 255, 255, 1)", + background: "rgba(237, 94, 189, 0.3)", + hover: "rgba(237, 94, 189, 0.4)", }, inactive: { - hover: "rgba(255, 255, 255, 0.3)", + hover: "rgba(237, 94, 189, 0.1)", }, }, }, }, text: { - primary: "#000000", - secondary: "#666666", - disabled: "#999999", + primary: "#ededde", + secondary: "#b8b8a8", + disabled: "#888880", }, background: { - default: "#FFFFFF", - paper: "#FFFFFF", + default: "#12091e", + paper: "#1e1230", }, common: { black: "#000000", - white: "#FFFFFF", + white: "#ffffff", }, error: { - main: "#d32f2f", - light: "#ef5350", - dark: "#c62828", + main: "#f44336", + light: "#e57373", + dark: "#d32f2f", }, warning: { - main: "#ed6c02", - light: "#ff9800", - dark: "#e65100", + main: "#f5c73d", + light: "#f9d86d", + dark: "#d4a818", }, info: { - main: "#0288d1", - light: "#03a9f4", - dark: "#01579b", + main: "#29b6f6", + light: "#4fc3f7", + dark: "#0288d1", }, success: { - main: "#2e7d32", - light: "#4caf50", - dark: "#1b5e20", + main: "#66bb6a", + light: "#81c784", + dark: "#388e3c", }, }, }); diff --git a/apps/pyconkr-2026/tsconfig.json b/apps/pyconkr-2026/tsconfig.json index 06ac591..e9694fd 100644 --- a/apps/pyconkr-2026/tsconfig.json +++ b/apps/pyconkr-2026/tsconfig.json @@ -25,8 +25,8 @@ /* Paths */ "baseUrl": ".", "paths": { - "@apps/pyconkr-2026/*": ["apps/pyconkr-2026/src/*"], + "@apps/pyconkr-2026/*": ["apps/pyconkr-2026/src/*"] } }, - "include": ["src", "vite.config.mts", "vite-env.d.ts", "../../types"], + "include": ["src", "vite.config.mts", "vite-env.d.ts", "../../types"] } diff --git a/packages/common/src/components/mdx_components/mobile_cover.tsx b/packages/common/src/components/mdx_components/mobile_cover.tsx index cb37c6d..badd579 100644 --- a/packages/common/src/components/mdx_components/mobile_cover.tsx +++ b/packages/common/src/components/mdx_components/mobile_cover.tsx @@ -7,6 +7,7 @@ import * as Hooks from "../../hooks"; type MobileCoverProps = { coverImageSrc: string; coverTitleSrc: string; + coverImageObjectFit?: React.CSSProperties["objectFit"]; buttonTextKo?: string; buttonTextEn?: string; }; @@ -14,6 +15,7 @@ type MobileCoverProps = { export const MobileCover: React.FC = ({ coverImageSrc, coverTitleSrc, + coverImageObjectFit = "cover", buttonTextKo = "티켓 구매하기", buttonTextEn = "Buy Ticket", }) => { @@ -22,8 +24,8 @@ export const MobileCover: React.FC = ({ return ( - - Mobile Cover Image + + Mobile Cover Image Mobile Cover Title