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/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.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",
|
"nodemon": "^3.1.10",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
@ -23,6 +26,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3": "^7.9.0",
|
"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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</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>
|
<script>
|
||||||
// import WorldMap from '$lib/components/WorldMap.svelte';
|
import WorldMap from '$lib/components/WorldMap.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
let title = "Travel App";
|
let title = "Travel App";
|
||||||
let activeTab = "Planner";
|
let activeTab = "Planner";
|
||||||
|
@ -27,13 +28,15 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile">
|
<div class="profile">
|
||||||
<button class="profile-btn">👤</button>
|
<button class="profile-btn">
|
||||||
|
<img src="/user.png" alt="" class="profile-pic"/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="map-container">
|
<div class="map-container">
|
||||||
<!-- <WorldMap /> -->
|
<WorldMap />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom-bar">
|
<div class="bottom-bar">
|
||||||
|
@ -50,7 +53,7 @@
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: #F0F0F0;
|
background-color: var(--gray-50);
|
||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +74,7 @@
|
||||||
.right-nav {
|
.right-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
|
@ -85,7 +88,7 @@
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
color: #999;
|
color:var(--gray-400);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +104,9 @@
|
||||||
.profile-btn {
|
.profile-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1.5rem;
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
opacity: 0.3;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
@ -109,13 +114,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-btn:hover {
|
.profile-btn:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: var(--gray-100);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-pic {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-container {
|
.map-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #F0F0F0;
|
background-color: var(--gray-50);
|
||||||
/* overflow: hidden; */
|
/* overflow: hidden; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,10 +134,9 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1.5rem 2rem;
|
padding: 1.5rem 2rem;
|
||||||
margin-top: 10px;
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 20px 20px 0 0;
|
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 {
|
.past-trips {
|
||||||
|
@ -148,11 +157,11 @@
|
||||||
.hint {
|
.hint {
|
||||||
margin: 0.2rem 0 0 0;
|
margin: 0.2rem 0 0 0;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #666;
|
color: var(--gray-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-trip-btn {
|
.new-trip-btn {
|
||||||
background-color: #38C1D0;
|
background-color: var(--planner-300);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.8rem 1.5rem;
|
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