add world map @ home page
This commit is contained in:
parent
595e984b12
commit
21ff12f6b8
1361
package-lock.json
generated
1361
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -15,6 +15,9 @@
|
|||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/topojson-client": "^3.1.5",
|
||||
"@types/topojson-server": "^3.0.4",
|
||||
"nodemon": "^3.1.10",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
|
@ -23,6 +26,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"world-map": "^0.0.9"
|
||||
"topojson-client": "^3.1.0",
|
||||
"topojson-server": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="stylesheet" href="./app.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
|
114
src/lib/components/WorldMap.svelte
Normal file
114
src/lib/components/WorldMap.svelte
Normal file
|
@ -0,0 +1,114 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import * as d3 from 'd3';
|
||||
import { feature } from 'topojson-client';
|
||||
import { Colors } from '../constants/Colors';
|
||||
import '../../app.css';
|
||||
|
||||
let mapContainer: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
const width = mapContainer.clientWidth;
|
||||
const height = mapContainer.clientHeight;
|
||||
|
||||
// Create SVG
|
||||
const svg = d3.select(mapContainer)
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('viewBox', `0 0 ${width} ${height}`) // make a coordinate from (0, 0) to (width, height)
|
||||
.attr('preserveAspectRatio', 'xMidYMid meet') as d3.Selection<SVGSVGElement, unknown, null, undefined>; // center the map
|
||||
|
||||
// Add a group for all map elements that will be transformed
|
||||
const g = svg.append('g');
|
||||
|
||||
// Create projection
|
||||
const projection = d3.geoMercator()
|
||||
.scale(width / (2 * Math.PI))
|
||||
.translate([width / 2, height / 1.6]); // position the map, horizontally centered but is slighty upward
|
||||
|
||||
const path = d3.geoPath().projection(projection);
|
||||
|
||||
// Tokyo coordinates [longitude, latitude]
|
||||
const tokyo: [number, number] = [139.6917, 35.6895];
|
||||
|
||||
const initMap = async () => {
|
||||
try {
|
||||
// Load world map data
|
||||
const response = await fetch('https://unpkg.com/world-atlas@2/countries-110m.json');
|
||||
const world = await response.json();
|
||||
|
||||
// Convert TopoJSON to GeoJSON
|
||||
const countries = feature(world, world.objects.countries) as any;
|
||||
|
||||
// Draw the map
|
||||
g.append('g')
|
||||
.selectAll('path')
|
||||
.data(countries.features)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('d', path as any)
|
||||
.attr('class', 'country')
|
||||
.attr('fill', Colors.gray.light200)
|
||||
.attr('stroke', Colors.gray.light50)
|
||||
.attr('stroke-width', '0.5');
|
||||
|
||||
// Add Tokyo marker
|
||||
g.append('circle')
|
||||
.attr('cx', projection(tokyo)![0])
|
||||
.attr('cy', projection(tokyo)![1])
|
||||
.attr('r', 5)
|
||||
.attr('class', 'marker')
|
||||
.attr('fill', Colors.planner.med400);
|
||||
|
||||
// Add zoom behavior
|
||||
const zoom = d3.zoom<SVGSVGElement, unknown>()
|
||||
.scaleExtent([1, 8])
|
||||
.on('zoom', (event) => {
|
||||
g.attr('transform', event.transform);
|
||||
});
|
||||
|
||||
svg.call(zoom)
|
||||
.call(zoom.transform, d3.zoomIdentity);
|
||||
} catch (error) {
|
||||
console.error('Error loading map:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initMap();
|
||||
|
||||
return () => {
|
||||
d3.select(mapContainer).selectAll('*').remove();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={mapContainer} class="map-wrapper"></div>
|
||||
|
||||
<style>
|
||||
.map-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--gray-50);
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
:global(.country) {
|
||||
transition: fill 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.country:hover) {
|
||||
fill: #a1cdd2;
|
||||
}
|
||||
|
||||
:global(.marker) {
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
:global(.marker:hover) {
|
||||
r: 8;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
// import WorldMap from '$lib/components/WorldMap.svelte';
|
||||
import WorldMap from '$lib/components/WorldMap.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import '../app.css';
|
||||
|
||||
let title = "Travel App";
|
||||
let activeTab = "Planner";
|
||||
|
@ -27,13 +28,15 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="profile">
|
||||
<button class="profile-btn">👤</button>
|
||||
<button class="profile-btn">
|
||||
<img src="/user.png" alt="" class="profile-pic"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="map-container">
|
||||
<!-- <WorldMap /> -->
|
||||
<WorldMap />
|
||||
</div>
|
||||
|
||||
<div class="bottom-bar">
|
||||
|
@ -50,7 +53,7 @@
|
|||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #F0F0F0;
|
||||
background-color: var(--gray-50);
|
||||
font-family: 'Inter';
|
||||
}
|
||||
|
||||
|
@ -71,7 +74,7 @@
|
|||
.right-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.menu {
|
||||
|
@ -85,7 +88,7 @@
|
|||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 1rem;
|
||||
color: #999;
|
||||
color:var(--gray-400);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
|
@ -101,7 +104,9 @@
|
|||
.profile-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
opacity: 0.3;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
|
@ -109,13 +114,18 @@
|
|||
}
|
||||
|
||||
.profile-btn:hover {
|
||||
background-color: #f5f5f5;
|
||||
background-color: var(--gray-100);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background-color: #F0F0F0;
|
||||
background-color: var(--gray-50);
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
|
||||
|
@ -124,10 +134,9 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
margin-top: 10px;
|
||||
background-color: white;
|
||||
border-radius: 20px 20px 0 0;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.past-trips {
|
||||
|
@ -148,11 +157,11 @@
|
|||
.hint {
|
||||
margin: 0.2rem 0 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
|
||||
.new-trip-btn {
|
||||
background-color: #38C1D0;
|
||||
background-color: var(--planner-300);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
|
|
BIN
static/user.png
Normal file
BIN
static/user.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Loading…
Reference in New Issue
Block a user