Skip to content

Commit f6390d9

Browse files
authored
Merge pull request #252 from kw-coms/codex/move-activity-links-to-nav
[codex] Move activity links into global navigation
2 parents 550b5db + 1f8482f commit f6390d9

2 files changed

Lines changed: 70 additions & 40 deletions

File tree

src/App.jsx

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,6 @@ const showcaseItems = [
111111
]
112112

113113
const activityHubItems = [
114-
{
115-
title: 'Activity log',
116-
body: '관리자가 등록한 실제 활동 기록과 사진을 회원에게 보여줍니다.',
117-
route: '/activities',
118-
cta: '활동 기록 보기',
119-
},
120-
{
121-
title: 'Monthly calendar',
122-
body: '정기 회의, 세미나, 발표, 모집 마감을 월별 일정으로 확인합니다.',
123-
route: '/activities',
124-
cta: '일정 보기',
125-
},
126114
{
127115
title: '공지사항',
128116
body: '모집, 세미나, 운영 안내를 빠르게 확인합니다.',
@@ -508,6 +496,27 @@ const navExtraItems = [
508496
{ id: 'community', label: 'Community', path: '/community', auth: true },
509497
]
510498

499+
const activitySectionNavItems = [
500+
{
501+
id: 'activity-log',
502+
label: 'Activity log',
503+
hint: '활동 기록',
504+
path: '/activities',
505+
hash: '#activity-log',
506+
icon: Sparkles,
507+
accent: 'text-rose-400',
508+
},
509+
{
510+
id: 'monthly-calendar',
511+
label: 'Monthly calendar',
512+
hint: '월별 일정',
513+
path: '/activities',
514+
hash: '#monthly-calendar',
515+
icon: CalendarDays,
516+
accent: 'text-sky-500',
517+
},
518+
]
519+
511520
function getTabRoute(id) {
512521
return id === 'recruit' ? '/recruit' : `/${id}`
513522
}
@@ -729,6 +738,12 @@ function GlobalNavigation() {
729738
const activeKey = getActiveNavKey(location.pathname)
730739
const mobileTabs = tabs.slice(0, 3)
731740
const showMobileTabs = mobileTabs.some((tab) => tab.id === activeKey)
741+
const primaryNavItems = [
742+
...tabs.slice(0, 2).map((tab) => ({ ...tab, path: getTabRoute(tab.id) })),
743+
...activitySectionNavItems,
744+
...tabs.slice(2).map((tab) => ({ ...tab, path: getTabRoute(tab.id) })),
745+
]
746+
const desktopNavItems = [...primaryNavItems, ...navExtraItems]
732747

733748
useEffect(() => {
734749
if (!appsOpen) return undefined
@@ -747,6 +762,15 @@ function GlobalNavigation() {
747762
}
748763
}, [appsOpen])
749764

765+
useEffect(() => {
766+
if (!location.hash) return undefined
767+
const sectionId = decodeURIComponent(location.hash.slice(1))
768+
const frame = window.requestAnimationFrame(() => {
769+
document.getElementById(sectionId)?.scrollIntoView({ block: 'start' })
770+
})
771+
return () => window.cancelAnimationFrame(frame)
772+
}, [location.hash, location.pathname])
773+
750774
const goPageTop = (to, options) => {
751775
scrollToTopInstant()
752776
navigate(to, options)
@@ -763,6 +787,15 @@ function GlobalNavigation() {
763787
closeAndGo(to)
764788
}
765789

790+
const goNavItem = (item) => {
791+
const target = `${item.path}${item.hash || ''}`
792+
if (item.auth) {
793+
goProtected(target)
794+
return
795+
}
796+
goPageTop(target)
797+
}
798+
766799
const handleLogout = async () => {
767800
await logout()
768801
setMobileMenuOpen(false)
@@ -790,21 +823,12 @@ function GlobalNavigation() {
790823
<span className="whitespace-nowrap text-sm font-bold text-[var(--app-text)]">KW COM&apos;s</span>
791824
</button>
792825

793-
<nav className="pointer-events-auto absolute left-1/2 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 items-center gap-6 md:flex">
794-
{tabs.map((tab) => {
795-
const route = getTabRoute(tab.id)
796-
return (
797-
<button key={tab.id} type="button" onClick={() => goPageTop(route)} className={navClass(tab.id)}>
798-
{tab.label}
799-
<span className={`absolute -bottom-4 left-1/2 h-0.5 w-4 -translate-x-1/2 rounded-full bg-[var(--app-accent)] transition ${activeKey === tab.id ? 'opacity-100' : 'opacity-0'}`} />
800-
</button>
801-
)
802-
})}
803-
{navExtraItems.map((item) => (
826+
<nav className="pointer-events-auto absolute left-1/2 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 items-center gap-3 lg:gap-5 md:flex">
827+
{desktopNavItems.map((item) => (
804828
<button
805829
key={item.id}
806830
type="button"
807-
onClick={() => (item.auth ? goProtected(item.path) : goPageTop(item.path))}
831+
onClick={() => goNavItem(item)}
808832
disabled={item.auth && authLoading}
809833
className={`${navClass(item.id)} disabled:cursor-wait disabled:opacity-60`}
810834
>
@@ -902,16 +926,13 @@ function GlobalNavigation() {
902926
className="apple-mobile-menu-panel mx-auto md:hidden"
903927
>
904928
<div className="flex flex-col divide-y divide-[var(--app-hairline)]">
905-
{tabs.map((tab) => {
906-
const route = getTabRoute(tab.id)
907-
return (
908-
<button key={tab.id} type="button" onClick={() => closeAndGo(route)} className="apple-mobile-menu-item">
909-
<tab.icon size={15} className={tab.accent} />
910-
<span>{tab.label}</span>
911-
<span className="ml-auto text-xs text-[var(--app-muted)]">{tab.hint}</span>
912-
</button>
913-
)
914-
})}
929+
{primaryNavItems.map((item) => (
930+
<button key={item.id} type="button" onClick={() => closeAndGo(`${item.path}${item.hash || ''}`)} className="apple-mobile-menu-item">
931+
<item.icon size={15} className={item.accent} />
932+
<span>{item.label}</span>
933+
<span className="ml-auto text-xs text-[var(--app-muted)]">{item.hint}</span>
934+
</button>
935+
))}
915936
<button type="button" onClick={() => closeAndGo('/notices')} className="apple-mobile-menu-item">
916937
<Megaphone size={15} className="text-cyan-500" />
917938
<span>Notices</span>
@@ -1543,7 +1564,7 @@ function ActivityLogSection({ compact = false }) {
15431564
const isLocked = !authLoading && !user
15441565

15451566
return (
1546-
<section className={`activity-proof-section ${compact ? 'activity-proof-section-compact' : ''} bg-white px-5 py-12 sm:py-16`}>
1567+
<section id="activity-log" className={`activity-proof-section ${compact ? 'activity-proof-section-compact' : ''} scroll-mt-24 bg-white px-5 py-12 sm:py-16`}>
15471568
<div className="mx-auto max-w-7xl">
15481569
<div className="grid gap-8 lg:grid-cols-[0.62fr_1fr] lg:items-end">
15491570
<div>
@@ -1710,7 +1731,7 @@ function ClubCalendarSection({ compact = false }) {
17101731
}
17111732

17121733
return (
1713-
<section className={`club-calendar-section ${compact ? 'club-calendar-section-compact' : ''} bg-[#f5f5f7] px-5 py-12 sm:py-16`}>
1734+
<section id="monthly-calendar" className={`club-calendar-section ${compact ? 'club-calendar-section-compact' : ''} scroll-mt-24 bg-[#f5f5f7] px-5 py-12 sm:py-16`}>
17141735
<div className="mx-auto max-w-7xl">
17151736
<div className="flex flex-wrap items-end justify-between gap-4">
17161737
<div>
@@ -2303,7 +2324,7 @@ function HomeView() {
23032324
</div>
23042325
<button type="button" onClick={() => goPageTop('/notices')} className={ghostActionBtnClass}>최근 공지 보기</button>
23052326
</div>
2306-
<div className="mt-8 grid gap-3 md:grid-cols-2 xl:grid-cols-5">
2327+
<div className="mt-8 grid gap-3 md:grid-cols-3">
23072328
{activityHubItems.map((item, index) => (
23082329
<button
23092330
key={item.title}

tests/e2e/app-smoke.spec.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -764,15 +764,24 @@ test('admin tracks recruit applications from overview to status update', async (
764764
await expect(page.locator('article').filter({ hasText: '김지원' }).locator('span').filter({ hasText: '검토중' })).toBeVisible()
765765
})
766766

767-
test('activity hub points members back to notices community and archive', async ({ page }) => {
767+
test('top navigation exposes activity log and monthly calendar outside the home hub', async ({ page }) => {
768768
await page.goto('/')
769769

770+
const desktopNav = page.locator('header nav').first()
771+
await expect(desktopNav.getByRole('button', { name: 'Activity log' })).toBeVisible()
772+
await expect(desktopNav.getByRole('button', { name: 'Monthly calendar' })).toBeVisible()
773+
770774
await expect(page.getByRole('heading', { name: '활동 허브' })).toBeVisible()
771-
await expect(page.getByRole('button', { name: /Activity log/ })).toBeVisible()
772-
await expect(page.getByRole('button', { name: /Monthly calendar/ })).toBeVisible()
775+
await expect(page.locator('section').filter({ has: page.getByRole('heading', { name: '활동 허브' }) }).getByRole('button', { name: /Activity log/ })).toHaveCount(0)
776+
await expect(page.locator('section').filter({ has: page.getByRole('heading', { name: '활동 허브' }) }).getByRole('button', { name: /Monthly calendar/ })).toHaveCount(0)
773777
await expect(page.getByRole('heading', { name: '공지사항' })).toBeVisible()
774778
await expect(page.getByRole('heading', { name: '커뮤니티' })).toBeVisible()
775779
await expect(page.getByRole('heading', { name: '자료실' })).toBeVisible()
780+
781+
await page.setViewportSize({ width: 390, height: 844 })
782+
await page.getByLabel('메뉴 열기').click()
783+
await expect(page.getByRole('menu').getByRole('button', { name: /Activity log/ })).toBeVisible()
784+
await expect(page.getByRole('menu').getByRole('button', { name: /Monthly calendar/ })).toBeVisible()
776785
})
777786

778787
test('activity records and calendar are locked for guests without dummy content', async ({ page }) => {

0 commit comments

Comments
 (0)