196 lines
8.3 KiB
Svelte
196 lines
8.3 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import AnalyticsConsent from '../src/lib/components/AnalyticsConsent.svelte';
|
|
import HomePage from '../src/routes/+page.svelte';
|
|
import CardsPage from '../src/routes/cards/+page.svelte';
|
|
import AnalysisPage from '../src/routes/analysis/+page.svelte';
|
|
import RatingPage from '../src/routes/rating/+page.svelte';
|
|
import CombinationPage from '../src/routes/combination/+page.svelte';
|
|
import HoloWorkPage from '../src/routes/tools/holo-work/+page.svelte';
|
|
import BoardAwakeningPage from '../src/routes/tools/board-awakening/+page.svelte';
|
|
import ToolsPage from '../src/routes/tools/+page.svelte';
|
|
import ComparePage from '../src/routes/compare/+page.svelte';
|
|
import EventPage from '../src/routes/event/+page.svelte';
|
|
import DataPage from '../src/routes/data/+page.svelte';
|
|
import DataCardsPage from '../src/routes/data/cards/+page.svelte';
|
|
import DataBoardsPage from '../src/routes/data/boards/+page.svelte';
|
|
import UpdatesPage from '../src/routes/updates/+page.svelte';
|
|
import { setOfflinePage } from './page-store';
|
|
import { events } from '../src/lib/data/events';
|
|
import { activeEventAt } from '../src/lib/engine/event-optimization';
|
|
|
|
const routes = new Map([
|
|
['/', HomePage],
|
|
['/cards', CardsPage],
|
|
['/analysis', AnalysisPage],
|
|
['/rating', RatingPage],
|
|
['/combination', CombinationPage],
|
|
['/tools', ToolsPage],
|
|
['/compare', ComparePage],
|
|
['/tools/random', ToolsPage],
|
|
['/tools/holo-work', HoloWorkPage],
|
|
['/tools/board-awakening', BoardAwakeningPage],
|
|
['/random', ToolsPage],
|
|
['/event', EventPage],
|
|
['/data', DataPage],
|
|
['/data/cards', DataCardsPage],
|
|
['/data/boards', DataBoardsPage],
|
|
['/updates', UpdatesPage]
|
|
]);
|
|
|
|
function pathFromHash() {
|
|
const path = window.location.hash.replace(/^#/, '') || '/';
|
|
if (path === '/tools/compare') return '/compare';
|
|
return routes.has(path) ? path : '/';
|
|
}
|
|
|
|
let currentPath = pathFromHash();
|
|
let CurrentPage = routes.get(currentPath) ?? HomePage;
|
|
let eventNow = Date.now();
|
|
let menuOpen = false;
|
|
$: if (currentPath) menuOpen = false;
|
|
$: showEventTab = Boolean(activeEventAt(events, eventNow));
|
|
setOfflinePage(currentPath);
|
|
|
|
function syncRoute() {
|
|
currentPath = pathFromHash();
|
|
CurrentPage = routes.get(currentPath) ?? HomePage;
|
|
setOfflinePage(currentPath);
|
|
window.scrollTo({ top: 0 });
|
|
}
|
|
|
|
onMount(() => {
|
|
const eventTimer = window.setInterval(() => eventNow = Date.now(), 1_000);
|
|
const rewriteInternalLinks = () => {
|
|
for (const anchor of document.querySelectorAll<HTMLAnchorElement>('a[href^="/"]')) {
|
|
anchor.href = `#${anchor.getAttribute('href')}`;
|
|
}
|
|
};
|
|
const handleClick = (event: MouseEvent) => {
|
|
const target = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[href]') : null;
|
|
const href = target?.getAttribute('href');
|
|
if (!href?.startsWith('/') || target?.target === '_blank') return;
|
|
event.preventDefault();
|
|
window.location.hash = `#${href}`;
|
|
};
|
|
rewriteInternalLinks();
|
|
const linkObserver = new MutationObserver(rewriteInternalLinks);
|
|
linkObserver.observe(document.body, { childList: true, subtree: true });
|
|
window.addEventListener('hashchange', syncRoute);
|
|
document.addEventListener('click', handleClick);
|
|
return () => {
|
|
window.removeEventListener('hashchange', syncRoute);
|
|
document.removeEventListener('click', handleClick);
|
|
linkObserver.disconnect();
|
|
window.clearInterval(eventTimer);
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<header class="site-nav">
|
|
<div class="nav-inner">
|
|
<a class="brand" href="#/" aria-label="홀로도리 계산기 홈">홀로도리 계산기</a>
|
|
<button class="menu-toggle" type="button" aria-controls="primary-navigation" aria-expanded={menuOpen} on:click={() => menuOpen = !menuOpen}>{menuOpen ? '메뉴 닫기' : '전체 메뉴'}</button>
|
|
<nav id="primary-navigation" class:open={menuOpen} aria-label="주요 메뉴">
|
|
<a href="#/cards" aria-current={currentPath.startsWith('/cards') ? 'page' : undefined}>카드 목록</a>
|
|
<a href="#/compare" aria-current={currentPath.startsWith('/compare') ? 'page' : undefined}>편성 비교</a>
|
|
<a href="#/analysis" aria-current={currentPath.startsWith('/analysis') ? 'page' : undefined}>배치 분석</a>
|
|
<a href="#/rating" aria-current={currentPath.startsWith('/rating') ? 'page' : undefined}>리더 레이팅</a>
|
|
<a href="#/combination" aria-current={currentPath.startsWith('/combination') ? 'page' : undefined}>조합 탐색</a>
|
|
{#if showEventTab}<a href="#/event" aria-current={currentPath.startsWith('/event') ? 'page' : undefined}>이벤트</a>{/if}
|
|
<a href="#/data" aria-current={currentPath.startsWith('/data') ? 'page' : undefined}>사용자 데이터</a>
|
|
<a href="#/updates" aria-current={currentPath.startsWith('/updates') ? 'page' : undefined}>업데이트 내역</a>
|
|
<a href="#/tools" aria-current={currentPath.startsWith('/tools') || currentPath === '/random' ? 'page' : undefined}>도구</a>
|
|
<a
|
|
class="feedback-link"
|
|
href="https://docs.google.com/forms/d/1z9c0c1crbCO6avXaYwrHZjhw2ib3zgAmW4ijJDTo4DM"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
aria-label="버그 제보 Google 폼을 새 탭에서 열기"
|
|
>버그 제보 <span aria-hidden="true">↗</span></a>
|
|
<a
|
|
class="feedback-link"
|
|
href="https://docs.google.com/forms/d/16NcTst7Rpa9XzqYIfPa4t9JF2X0CMfYzEbZrT8PLqco"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
aria-label="기능 건의 Google 폼을 새 탭에서 열기"
|
|
>기능 건의 <span aria-hidden="true">↗</span></a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<svelte:component this={CurrentPage} />
|
|
|
|
<AnalyticsConsent pagePath={currentPath} />
|
|
|
|
<style>
|
|
:global(*) { box-sizing: border-box; }
|
|
:global(body) { margin: 0; background: #101319; color: #e8ebf1; font-family: Pretendard, "Noto Sans KR", sans-serif; }
|
|
|
|
.site-nav {
|
|
position: sticky;
|
|
z-index: 20;
|
|
top: 0;
|
|
border-bottom: 1px solid rgb(255 255 255 / 8%);
|
|
background: rgb(16 19 25 / 88%);
|
|
backdrop-filter: blur(14px);
|
|
}
|
|
|
|
.nav-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
width: min(var(--page-max-width), calc(100% - var(--page-gutter)));
|
|
min-height: 62px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.brand {
|
|
color: #f3f5f8;
|
|
font-size: 16px;
|
|
font-weight: 850;
|
|
letter-spacing: -.03em;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.brand { flex-shrink: 0; }
|
|
.menu-toggle { display: none; }
|
|
nav { display: flex; min-width: 0; flex: 1; align-items: center; justify-content: safe flex-end; gap: 4px; overflow-x: auto; scrollbar-width: thin; }
|
|
nav::-webkit-scrollbar { display: none; }
|
|
|
|
nav a {
|
|
flex: 0 0 auto;
|
|
padding: 8px 12px;
|
|
border-radius: 9px;
|
|
color: #929cad;
|
|
font-size: 13px;
|
|
font-weight: 750;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
nav a:hover { color: #e2e6ed; background: rgb(255 255 255 / 5%); }
|
|
nav a[aria-current='page'] { color: #f1a1c3; background: rgb(239 139 182 / 12%); }
|
|
.feedback-link { border: 1px solid #9f5877; color: #edb8cf; background: linear-gradient(135deg, #482c39, #38232e); box-shadow: 0 0 12px rgb(175 91 128 / 10%); }
|
|
.feedback-link:hover { color: #f6d5e3; background: linear-gradient(135deg, #583444, #472a37); box-shadow: 0 0 16px rgb(175 91 128 / 18%); }
|
|
a:focus-visible { outline: 3px solid rgb(239 139 182 / 28%); outline-offset: 2px; }
|
|
|
|
@media (max-width: 1100px) {
|
|
.nav-inner { flex-wrap: wrap; padding-block: 5px; }
|
|
.menu-toggle { display: block; min-height: 44px; padding: 8px 14px; border: 1px solid #694256; border-radius: 9px; color: #f1a1c3; background: #29202a; font: inherit; font-size: 14px; cursor: pointer; }
|
|
.menu-toggle:focus-visible { outline: 3px solid #ef8bb6; outline-offset: 2px; }
|
|
nav { display: none; flex: 0 0 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: 65dvh; overflow-y: auto; padding: 4px 2px 10px; }
|
|
nav.open { display: grid; }
|
|
nav a { display: flex; align-items: center; min-height: 44px; white-space: normal; }
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
:global(:root) { --page-gutter: 24px; }
|
|
.nav-inner { width: calc(100% - 24px); min-height: 54px; }
|
|
.brand { font-size: 13px; }
|
|
nav { gap: 2px; }
|
|
nav a { padding: 7px 7px; font-size: 12px; }
|
|
}
|
|
</style>
|