add app layout
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>map-journal</title>
|
||||
<title>Map Journal</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -1,24 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 963 KiB |
BIN
public/profile.jpg
Normal file
BIN
public/profile.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -1,5 +1,19 @@
|
||||
<script>
|
||||
import Layout from './lib/Layout.svelte';
|
||||
import WorldMap from './lib/WorldMap.svelte';
|
||||
import Timeline from './lib/Timeline.svelte';
|
||||
|
||||
let screen = $state('worldmap');
|
||||
|
||||
function onNavigate(s) {
|
||||
screen = s;
|
||||
}
|
||||
</script>
|
||||
|
||||
<WorldMap />
|
||||
<Layout {screen} {onNavigate}>
|
||||
{#if screen === 'worldmap'}
|
||||
<WorldMap />
|
||||
{:else}
|
||||
<Timeline />
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
16
src/lib/Footer.svelte
Normal file
16
src/lib/Footer.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<footer class="footer">
|
||||
© 2026 Tomas Horsky & Haeri Kim
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.footer {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 24px;
|
||||
background: #dce8f0;
|
||||
font: 14px/1.5 sans-serif;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
29
src/lib/Layout.svelte
Normal file
29
src/lib/Layout.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import TopBar from './TopBar.svelte';
|
||||
import Footer from './Footer.svelte';
|
||||
|
||||
let { screen, onNavigate, children } = $props();
|
||||
</script>
|
||||
|
||||
<div class="layout">
|
||||
<TopBar {screen} {onNavigate} />
|
||||
<main class="main">
|
||||
{@render children()}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.layout {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
15
src/lib/Timeline.svelte
Normal file
15
src/lib/Timeline.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<div class="placeholder">
|
||||
<p>Timeline — coming soon</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: 16px/1.4 sans-serif;
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
109
src/lib/TopBar.svelte
Normal file
109
src/lib/TopBar.svelte
Normal file
@@ -0,0 +1,109 @@
|
||||
<script>
|
||||
let { screen, onNavigate } = $props();
|
||||
</script>
|
||||
|
||||
<div class="topbar">
|
||||
<div class="left">
|
||||
<img src="/logo.png" alt="Logo" class="logo" />
|
||||
<span class="app-name">Map Journal</span>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="segmented">
|
||||
<div
|
||||
class="slider"
|
||||
style="transform: translateX({screen === 'worldmap' ? 0 : 100}%);"
|
||||
></div>
|
||||
<button onclick={() => onNavigate('worldmap')}>Worldmap</button>
|
||||
<button onclick={() => onNavigate('timeline')}>Timeline</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<img src="/profile.jpg" alt="Profile" class="avatar" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.topbar {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
background: #dce8f0;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font: 700 20px/1.2 sans-serif;
|
||||
color: #1f2937;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.segmented {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
border-radius: 999px;
|
||||
padding: 4px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: calc(50% - 4px);
|
||||
height: calc(100% - 8px);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
transition: transform 0.25s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.segmented button {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font: 500 16px/1.4 sans-serif;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -4,21 +4,27 @@
|
||||
import { feature } from 'topojson-client';
|
||||
import worldData from 'world-atlas/countries-110m.json';
|
||||
|
||||
let container;
|
||||
let frameEl;
|
||||
|
||||
function fitProjection(proj, w, h) {
|
||||
proj.fitSize([w, h], { type: 'Sphere' });
|
||||
const s = proj.scale() * 1.5;
|
||||
proj.scale(s).translate([w / 2, h * 0.70]);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
const width = frameEl.clientWidth;
|
||||
const height = frameEl.clientHeight;
|
||||
|
||||
const projection = d3.geoMercator()
|
||||
.fitSize([width, height], { type: 'Sphere' });
|
||||
const projection = d3.geoMercator();
|
||||
fitProjection(projection, width, height);
|
||||
|
||||
const path = d3.geoPath().projection(projection);
|
||||
|
||||
const countries = feature(worldData, worldData.objects.countries)
|
||||
.features.filter(f => f.id !== '010');
|
||||
|
||||
const svg = d3.select(container)
|
||||
const svg = d3.select(frameEl)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
@@ -26,25 +32,21 @@
|
||||
const g = svg.append('g');
|
||||
const selected = new Set();
|
||||
|
||||
const tooltip = d3.select(container)
|
||||
const tooltip = d3.select(frameEl)
|
||||
.append('div')
|
||||
.attr('class', 'tooltip')
|
||||
.style('display', 'none');
|
||||
|
||||
function color(d) {
|
||||
return selected.has(d.id) ? '#4ade80' : '#e0e0e0';
|
||||
}
|
||||
|
||||
function updateFill(sel) {
|
||||
sel.attr('fill', d => selected.has(d.id) ? '#4ade80' : '#e0e0e0');
|
||||
sel.attr('fill', d => selected.has(d.id) ? '#22c55e' : '#ffffff');
|
||||
}
|
||||
|
||||
g.selectAll('path')
|
||||
.data(countries)
|
||||
.join('path')
|
||||
.attr('d', path)
|
||||
.attr('fill', color)
|
||||
.attr('stroke', '#999')
|
||||
.attr('fill', '#ffffff')
|
||||
.attr('stroke', '#d4d4d4')
|
||||
.attr('stroke-width', 0.5)
|
||||
.on('click', (event, d) => {
|
||||
if (selected.has(d.id)) {
|
||||
@@ -55,15 +57,15 @@
|
||||
updateFill(d3.select(event.currentTarget));
|
||||
})
|
||||
.on('mouseenter', (event, d) => {
|
||||
d3.select(event.currentTarget).attr('fill', selected.has(d.id) ? '#6ee7a0' : '#d0d0d0');
|
||||
d3.select(event.currentTarget).attr('fill', selected.has(d.id) ? '#16a34a' : '#f0f6fa');
|
||||
tooltip.style('display', 'block').text(d.properties.name);
|
||||
})
|
||||
.on('mousemove', (event) => {
|
||||
const [x, y] = d3.pointer(event, container);
|
||||
const [x, y] = d3.pointer(event, frameEl);
|
||||
tooltip.style('left', (x + 10) + 'px').style('top', (y - 28) + 'px');
|
||||
})
|
||||
.on('mouseleave', (event, d) => {
|
||||
d3.select(event.currentTarget).attr('fill', selected.has(d.id) ? '#4ade80' : '#e0e0e0');
|
||||
d3.select(event.currentTarget).attr('fill', selected.has(d.id) ? '#22c55e' : '#ffffff');
|
||||
tooltip.style('display', 'none');
|
||||
});
|
||||
|
||||
@@ -79,14 +81,14 @@
|
||||
for (const entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
svg.attr('width', width).attr('height', height);
|
||||
projection.fitSize([width, height], { type: 'Sphere' });
|
||||
const paths = svg.selectAll('path');
|
||||
paths.attr('d', path);
|
||||
updateFill(paths);
|
||||
fitProjection(projection, width, height);
|
||||
const countryPaths = g.selectAll('path');
|
||||
countryPaths.attr('d', path);
|
||||
updateFill(countryPaths);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
observer.observe(frameEl);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
@@ -95,32 +97,44 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="map-container"></div>
|
||||
<div class="wrapper">
|
||||
<div bind:this={frameEl} class="map-frame"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.map-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.map-container :global(svg) {
|
||||
.map-frame {
|
||||
width: min(100%, calc((100vh - 96px) * 16 / 9));
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #dce8f0;
|
||||
}
|
||||
|
||||
.map-frame :global(svg) {
|
||||
display: block;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.map-container :global(svg:active) {
|
||||
.map-frame :global(svg:active) {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.map-container :global(svg path) {
|
||||
.map-frame :global(svg path) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.map-container :global(.tooltip) {
|
||||
.map-frame :global(.tooltip) {
|
||||
position: absolute;
|
||||
padding: 4px 10px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
background: #1f2937;
|
||||
color: #fff;
|
||||
font: 14px/1.4 sans-serif;
|
||||
border-radius: 4px;
|
||||
|
||||
Reference in New Issue
Block a user