Skip to content

Commit ae8f263

Browse files
committed
feat: Nav 마크업
1 parent 3632149 commit ae8f263

File tree

3 files changed

+215
-59
lines changed

3 files changed

+215
-59
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import styled from "@emotion/styled";
2+
import { useEffect, useState } from "react";
3+
4+
const menus = [
5+
{
6+
text: "파이콘 한국",
7+
subMenu: [
8+
{ text: "파이콘 한국 소개", href: "/about" },
9+
{ text: "파이콘 한국 2025", href: "/2025" },
10+
{ text: "파이콘 한국 행동강령(CoC)", href: "/coc" },
11+
{ text: "파이썬 사용자 모임", href: "/user-group" },
12+
{
13+
text: "역대 파이콘 행사",
14+
href: "/past-events",
15+
subMenu: [
16+
{ text: "2025", href: "/2025" },
17+
{ text: "2024", href: "/2024" },
18+
{ text: "2023", href: "/2023" },
19+
{ text: "2022", href: "/2022" },
20+
{ text: "2021", href: "/2021" },
21+
{ text: "2020", href: "/2020" },
22+
],
23+
},
24+
{ text: "파이콘 한국 건강 관련 안내", href: "/health" },
25+
],
26+
},
27+
{
28+
text: "프로그램",
29+
subMenu: [
30+
{ text: "튜토리얼", href: "/tutorial" },
31+
{ text: "스프린트", href: "/sprint" },
32+
{ text: "포스터 세션", href: "/poster" },
33+
],
34+
},
35+
{
36+
text: "세션",
37+
subMenu: [
38+
{ text: "세션 목록", href: "/sessions" },
39+
{ text: "세션 시간표", href: "/schedule" },
40+
],
41+
},
42+
{
43+
text: "구매",
44+
subMenu: [
45+
{ text: "티켓 구매", href: "/tickets" },
46+
{ text: "굿즈 구매", href: "/goods" },
47+
{ text: "결제 내역", href: "/payments" },
48+
],
49+
},
50+
{
51+
text: "후원하기",
52+
subMenu: [
53+
{ text: "후원사 안내", href: "/sponsors" },
54+
{ text: "개인 후원자", href: "/individual-sponsors" },
55+
],
56+
},
57+
];
58+
59+
function findBreadcrumbInfo(path: string) {
60+
if (path === "/" || path === "") {
61+
return {
62+
paths: [{ text: "홈", href: "/" }],
63+
title: "홈",
64+
};
65+
}
66+
67+
const normalizedPath = path.replace(/^\/|\/$/g, "");
68+
69+
let breadcrumbPaths = [{ text: "홈", href: "/" }];
70+
let pageTitle = "";
71+
72+
for (const menu of menus) {
73+
for (const subMenu of menu.subMenu) {
74+
const subMenuPath = subMenu.href.replace(/^\/|\/$/g, "");
75+
76+
if (subMenuPath === normalizedPath) {
77+
breadcrumbPaths.push({ text: menu.text, href: subMenu.href });
78+
pageTitle = subMenu.text;
79+
return { paths: breadcrumbPaths, title: pageTitle };
80+
}
81+
82+
if (subMenu.subMenu) {
83+
for (const thirdMenu of subMenu.subMenu) {
84+
const thirdMenuPath = thirdMenu.href.replace(/^\/|\/$/g, "");
85+
86+
if (thirdMenuPath === normalizedPath) {
87+
breadcrumbPaths.push({ text: menu.text, href: subMenu.href });
88+
breadcrumbPaths.push({ text: subMenu.text, href: subMenu.href });
89+
pageTitle = thirdMenu.text;
90+
return { paths: breadcrumbPaths, title: pageTitle };
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
return {
98+
paths: [{ text: "홈", href: "/" }],
99+
title: normalizedPath.charAt(0).toUpperCase() + normalizedPath.slice(1),
100+
};
101+
}
102+
103+
export default function BreadCrumb() {
104+
const [breadcrumbInfo, setBreadcrumbInfo] = useState({
105+
paths: [{ text: "홈", href: "/" }],
106+
title: "파이콘 한국 행동강령(CoC)",
107+
});
108+
109+
useEffect(() => {
110+
const mockPathInfo = {
111+
paths: [
112+
{ text: "홈", href: "/" },
113+
{ text: "파이콘 한국", href: "/about" },
114+
],
115+
title: "파이콘 한국 행동강령(CoC)",
116+
};
117+
setBreadcrumbInfo(mockPathInfo);
118+
}, []);
119+
120+
return (
121+
<BreadCrumbContainer>
122+
<BreadcrumbPathContainer>
123+
{breadcrumbInfo.paths.map((item, index) => (
124+
<span key={index}>
125+
{index > 0 && <span className="separator">&gt;</span>}
126+
<a href={item.href}>{item.text}</a>
127+
</span>
128+
))}
129+
</BreadcrumbPathContainer>
130+
<PageTitle>{breadcrumbInfo.title}</PageTitle>
131+
</BreadCrumbContainer>
132+
);
133+
}
134+
135+
const BreadCrumbContainer = styled.div`
136+
width: 100%;
137+
padding: 14px 117px;
138+
background-color: rgba(255, 255, 255, 0.7);
139+
background-image: linear-gradient(
140+
rgba(255, 255, 255, 0.7),
141+
rgba(255, 255, 255, 0.45)
142+
);
143+
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
144+
display: flex;
145+
flex-direction: column;
146+
gap: 5px;
147+
`;
148+
149+
const BreadcrumbPathContainer = styled.div`
150+
font-size: 9.75px;
151+
font-weight: 300;
152+
color: #000000;
153+
display: flex;
154+
align-items: center;
155+
gap: 0;
156+
157+
a {
158+
color: #000000;
159+
text-decoration: none;
160+
161+
&:hover {
162+
text-decoration: underline;
163+
}
164+
}
165+
166+
.separator {
167+
color: #4e869d;
168+
margin: 0 5px;
169+
}
170+
`;
171+
172+
const PageTitle = styled.h1`
173+
font-size: 27px;
174+
font-weight: 600;
175+
color: #000000;
176+
margin: 0;
177+
`;

apps/pyconkr/src/components/layout/Header/index.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,27 @@ import { useNavigate } from "react-router-dom";
55
import LanguageSelector from "../LanguageSelector";
66
import LoginButton from "../LoginButton";
77
import Nav from "../Nav";
8+
import BreadCrumb from "../BreadCrumb";
89

910
interface HeaderProps {}
1011

1112
export default function Header({}: HeaderProps) {
1213
const navigate = useNavigate();
1314

1415
return (
15-
<HeaderContainer>
16-
<HeaderLogo onClick={() => navigate("/")}>
17-
<Common.Components.PythonKorea style={{ width: 40, height: 40 }} />
18-
</HeaderLogo>
19-
<Nav />
20-
<HeaderLeft>
21-
<LanguageSelector />
22-
<LoginButton />
23-
</HeaderLeft>
24-
</HeaderContainer>
16+
<>
17+
<HeaderContainer>
18+
<HeaderLogo onClick={() => navigate("/")}>
19+
<Common.Components.PythonKorea style={{ width: 40, height: 40 }} />
20+
</HeaderLogo>
21+
<Nav />
22+
<HeaderLeft>
23+
<LanguageSelector />
24+
<LoginButton />
25+
</HeaderLeft>
26+
</HeaderContainer>
27+
<BreadCrumb />
28+
</>
2529
);
2630
}
2731

apps/pyconkr/src/components/layout/Nav/index.tsx

Lines changed: 24 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ const menus = [
2121
{ text: "2022", href: "/2022" },
2222
{ text: "2021", href: "/2021" },
2323
{ text: "2020", href: "/2020" },
24-
{ text: "2019", href: "/2019" },
25-
{ text: "2018", href: "/2018" },
26-
{ text: "2017", href: "/2017" },
27-
{ text: "2016", href: "/2016" },
28-
{ text: "2015", href: "/2015" },
29-
{ text: "2014", href: "/2014" },
3024
],
3125
},
3226
{ text: "파이콘 한국 건강 관련 안내", href: "/health" },
@@ -78,30 +72,20 @@ export default function Nav() {
7872
const [isSubMenuHovered, setIsSubMenuHovered] = useState(false);
7973
const [hoveredSubItem, setHoveredSubItem] = useState<string | null>(null);
8074
const lastActiveMenuRef = useRef<string | null>(null);
81-
const [forceOpenMenu, setForceOpenMenu] = useState(false);
8275

8376
useEffect(() => {
8477
if (hoveredMenu || focusedMenu) {
8578
lastActiveMenuRef.current = hoveredMenu || focusedMenu;
8679
}
8780
}, [hoveredMenu, focusedMenu]);
8881

89-
const showSubmenu =
90-
forceOpenMenu || !!hoveredMenu || !!focusedMenu || isSubMenuHovered;
82+
const showSubmenu = !!hoveredMenu || !!focusedMenu || isSubMenuHovered;
9183
const activeMenu =
9284
hoveredMenu ||
9385
focusedMenu ||
94-
(isSubMenuHovered ? lastActiveMenuRef.current : null) ||
95-
(forceOpenMenu ? "파이콘 한국" : null);
86+
(isSubMenuHovered ? lastActiveMenuRef.current : null);
9687
const currentMenu = menus.find((menu) => menu.text === activeMenu);
9788

98-
const toggleTestMode = () => {
99-
setForceOpenMenu(!forceOpenMenu);
100-
if (!forceOpenMenu && !hoveredSubItem) {
101-
setHoveredSubItem("역대 파이콘 행사");
102-
}
103-
};
104-
10589
const hasActiveThirdLevel = useMemo(() => {
10690
if (!hoveredSubItem || !currentMenu) return false;
10791
const activeSubItem = currentMenu.subMenu.find(
@@ -112,10 +96,6 @@ export default function Nav() {
11296

11397
return (
11498
<>
115-
<TestButton onClick={toggleTestMode}>
116-
{forceOpenMenu ? "테스트 모드 끄기" : "테스트 모드 켜기"}
117-
</TestButton>
118-
11999
<NavMainContainer>
120100
<HeaderNav>
121101
{menus.map((menu) => (
@@ -140,7 +120,10 @@ export default function Nav() {
140120
{showSubmenu && currentMenu && (
141121
<NavSubContainer
142122
onMouseEnter={() => setIsSubMenuHovered(true)}
143-
onMouseLeave={() => !forceOpenMenu && setIsSubMenuHovered(false)}
123+
onMouseLeave={() => {
124+
setIsSubMenuHovered(false);
125+
setHoveredSubItem(null);
126+
}}
144127
>
145128
<SubMenuWrapper>
146129
<CategoryTitle>{currentMenu.text}</CategoryTitle>
@@ -152,7 +135,6 @@ export default function Nav() {
152135
<SecondLevelItem
153136
key={subItem.text}
154137
onMouseEnter={() => setHoveredSubItem(subItem.text)}
155-
onMouseLeave={() => setHoveredSubItem(null)}
156138
className={
157139
hoveredSubItem === subItem.text ? "active" : ""
158140
}
@@ -170,7 +152,13 @@ export default function Nav() {
170152
<>
171153
<ThirdLevelDivider />
172154

173-
<ThirdLevelSection>
155+
<ThirdLevelSection
156+
onMouseEnter={() => {
157+
if (hoveredSubItem) {
158+
setHoveredSubItem(hoveredSubItem);
159+
}
160+
}}
161+
>
174162
{currentMenu.subMenu.map((subItem) => {
175163
const hasThirdLevel =
176164
subItem.subMenu && subItem.subMenu.length > 0;
@@ -212,8 +200,12 @@ const NavSubContainer = styled.div`
212200
width: 100vw;
213201
height: auto;
214202
min-height: 150px;
215-
background-color: white;
216-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
203+
background-color: rgba(255, 255, 255, 0.7);
204+
background-image: linear-gradient(
205+
rgba(255, 255, 255, 0.7),
206+
rgba(255, 255, 255, 0.45)
207+
);
208+
box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
217209
position: fixed;
218210
left: 0;
219211
top: 60px;
@@ -369,18 +361,18 @@ const SecondLevelItem = styled.li`
369361
const ThirdLevelDivider = styled.div`
370362
width: 1px;
371363
height: auto;
372-
background-color: #b6d8d7;
364+
background-color: ${({ theme }) => theme.palette.primary.light};
373365
margin: 0;
374366
flex-shrink: 0;
375367
align-self: stretch;
376368
`;
377369

378370
const ThirdLevelSection = styled.div`
379-
height: auto;
371+
height: 100%;
380372
display: flex;
381373
flex-direction: column;
374+
justify-content: center;
382375
min-width: 220px;
383-
width: 220px;
384376
padding-left: 30px;
385377
`;
386378

@@ -401,36 +393,19 @@ const ThirdLevelList = styled.ul`
401393
const ThirdLevelItem = styled.li`
402394
width: auto;
403395
text-align: right;
396+
min-width: 40px;
404397
405398
a {
406399
color: ${({ theme }) => theme.palette.primary.dark};
407400
text-decoration: none;
408401
font-size: 10px;
409-
font-weight: 400;
410402
display: inline-block;
411403
white-space: nowrap;
404+
font-variation-settings: "wght" 400;
412405
413406
&:hover,
414407
&:focus {
415408
font-weight: 700;
416409
}
417410
}
418411
`;
419-
420-
const TestButton = styled.button`
421-
position: fixed;
422-
top: 10px;
423-
right: 30px;
424-
z-index: 2000;
425-
background: #259299;
426-
color: white;
427-
border: none;
428-
padding: 8px 12px;
429-
border-radius: 4px;
430-
cursor: pointer;
431-
font-size: 14px;
432-
433-
&:hover {
434-
background: #126d7f;
435-
}
436-
`;

0 commit comments

Comments
 (0)