feat: improve combination search and offline build
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from '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 CombinationPage from '../src/routes/combination/+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 { setOfflinePage } from './page-store';
|
||||
|
||||
const routes = new Map([
|
||||
['/', HomePage],
|
||||
['/cards', CardsPage],
|
||||
['/analysis', AnalysisPage],
|
||||
['/combination', CombinationPage],
|
||||
['/data', DataPage],
|
||||
['/data/cards', DataCardsPage],
|
||||
['/data/boards', DataBoardsPage]
|
||||
]);
|
||||
|
||||
function pathFromHash() {
|
||||
const path = window.location.hash.replace(/^#/, '') || '/';
|
||||
return routes.has(path) ? path : '/';
|
||||
}
|
||||
|
||||
let currentPath = pathFromHash();
|
||||
let CurrentPage = routes.get(currentPath) ?? HomePage;
|
||||
setOfflinePage(currentPath);
|
||||
|
||||
function syncRoute() {
|
||||
currentPath = pathFromHash();
|
||||
CurrentPage = routes.get(currentPath) ?? HomePage;
|
||||
setOfflinePage(currentPath);
|
||||
window.scrollTo({ top: 0 });
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
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();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<header class="site-nav">
|
||||
<div class="nav-inner">
|
||||
<a class="brand" href="#/" aria-label="홀로도리 계산기 홈">홀로도리 계산기</a>
|
||||
<nav aria-label="주요 메뉴">
|
||||
<a href="#/cards" aria-current={currentPath.startsWith('/cards') ? 'page' : undefined}>카드 목록</a>
|
||||
<a href="#/analysis" aria-current={currentPath.startsWith('/analysis') ? 'page' : undefined}>배치 분석</a>
|
||||
<a href="#/combination" aria-current={currentPath.startsWith('/combination') ? 'page' : undefined}>조합 탐색</a>
|
||||
<a href="#/data" aria-current={currentPath.startsWith('/data') ? 'page' : undefined}>사용자 데이터</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<svelte:component this={CurrentPage} />
|
||||
|
||||
<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;
|
||||
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;
|
||||
}
|
||||
|
||||
nav { display: flex; align-items: center; gap: 4px; }
|
||||
nav a {
|
||||
padding: 8px 12px;
|
||||
border-radius: 9px;
|
||||
color: #929cad;
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
text-decoration: none;
|
||||
}
|
||||
nav a:hover { color: #e2e6ed; background: rgb(255 255 255 / 5%); }
|
||||
nav a[aria-current='page'] { color: #f1a1c3; background: rgb(239 139 182 / 12%); }
|
||||
a:focus-visible { outline: 3px solid rgb(239 139 182 / 28%); outline-offset: 2px; }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
:global(:root) { --page-gutter: 24px; }
|
||||
.nav-inner { width: calc(100% - 24px); min-height: 54px; }
|
||||
.brand { font-size: 15px; }
|
||||
nav a { padding: 7px 9px; font-size: 14px; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ExactSongChart } from '$lib/engine/index.ts';
|
||||
|
||||
const chartSources = import.meta.glob('../static/data/charts/*.json', {
|
||||
eager: true,
|
||||
query: '?raw',
|
||||
import: 'default'
|
||||
}) as Record<string, string>;
|
||||
|
||||
const sourceByFilename = new Map(
|
||||
Object.entries(chartSources).map(([path, source]) => [path.split('/').at(-1) ?? path, source])
|
||||
);
|
||||
const parsedCharts = new Map<string, ExactSongChart>();
|
||||
|
||||
export async function loadExactSongChart(path: string): Promise<ExactSongChart> {
|
||||
const filename = path.split('/').at(-1) ?? path;
|
||||
const cached = parsedCharts.get(filename);
|
||||
if (cached) return cached;
|
||||
|
||||
const source = sourceByFilename.get(filename);
|
||||
if (!source) throw new Error(`포함된 채보 데이터를 찾지 못했습니다: ${filename}`);
|
||||
const chart = JSON.parse(source) as ExactSongChart;
|
||||
parsedCharts.set(filename, chart);
|
||||
return chart;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<title>홀로도리 계산기</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { mount } from 'svelte';
|
||||
import App from './OfflineApp.svelte';
|
||||
import '../src/lib/styles/layout-tokens.css';
|
||||
|
||||
mount(App, {
|
||||
target: document.getElementById('app')!
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
function pageValue(pathname: string) {
|
||||
return { url: new URL(pathname, 'https://offline.holodori.local') };
|
||||
}
|
||||
|
||||
export const page = writable(pageValue('/'));
|
||||
|
||||
export function setOfflinePage(pathname: string) {
|
||||
page.set(pageValue(pathname));
|
||||
}
|
||||
Reference in New Issue
Block a user