mirror of
https://github.com/LukeHagar/website.git
synced 2025-12-10 21:07:46 +00:00
149 lines
5.3 KiB
Svelte
149 lines
5.3 KiB
Svelte
<script lang="ts" module>
|
|
export const MAP_BOUNDS = $state({
|
|
west: -137,
|
|
east: 165,
|
|
north: 80,
|
|
south: -60
|
|
});
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import MapMarker from './map-marker.svelte';
|
|
import { slugify } from '$lib/utils/slugify';
|
|
import { classNames } from '$lib/utils/classnames';
|
|
import MapNav from './map-nav.svelte';
|
|
import { useMousePosition } from '$lib/actions/mouse-position';
|
|
import { useAnimateInView } from '$lib/actions/animate-in-view';
|
|
import { pins, type PinSegment } from './data/pins';
|
|
import MapTooltip from './map-tooltip.svelte';
|
|
import { dev } from '$app/environment';
|
|
|
|
let dimensions = $state({
|
|
width: 0,
|
|
height: 0
|
|
});
|
|
|
|
let activeRegion = $state<string | null>(null);
|
|
let activeMarker: HTMLElement | null = null;
|
|
let activeSegment = $state<string>('pop-locations');
|
|
|
|
const { action: mousePosition, position } = useMousePosition();
|
|
const { action: inView, animate } = useAnimateInView({});
|
|
|
|
const scrollMarkerIntoView = (marker: HTMLElement) => {
|
|
return new Promise<void>((resolve) => {
|
|
marker.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'nearest',
|
|
inline: 'center'
|
|
});
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].intersectionRatio > 0.5) {
|
|
observer.disconnect();
|
|
resolve();
|
|
}
|
|
},
|
|
{ threshold: [0, 0.25, 0.5, 0.75, 1] }
|
|
);
|
|
|
|
observer.observe(marker);
|
|
});
|
|
};
|
|
|
|
const handleSetActiveMarker = async (city: string) => {
|
|
const citySlug = slugify(city);
|
|
|
|
if (activeRegion === citySlug) {
|
|
activeMarker = null;
|
|
activeRegion = null;
|
|
return;
|
|
}
|
|
|
|
activeMarker = document.querySelector(`[data-region="${citySlug}"]`);
|
|
|
|
if (activeMarker) {
|
|
await scrollMarkerIntoView(activeMarker);
|
|
activeRegion = citySlug;
|
|
}
|
|
};
|
|
|
|
const debug = false;
|
|
</script>
|
|
|
|
{#if dev && debug}
|
|
<div class="absolute z-1000 flex flex-col gap-4">
|
|
{#each Object.entries(MAP_BOUNDS) as [key, value]}
|
|
<input
|
|
type="number"
|
|
onchange={(e) =>
|
|
(MAP_BOUNDS[key as keyof typeof MAP_BOUNDS] = e.currentTarget.valueAsNumber)}
|
|
{value}
|
|
/>
|
|
{/each}
|
|
<pre>{JSON.stringify(MAP_BOUNDS, null, 4)}</pre>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="w-full overflow-scroll [scrollbar-width:none]">
|
|
<div
|
|
class="sticky left-0 z-10 mb-8 hidden w-screen gap-2 overflow-scroll px-8 [scrollbar-width:none]"
|
|
>
|
|
{#each pins[activeSegment as PinSegment] as pin}
|
|
<button
|
|
class={classNames(
|
|
'border-gradient grow rounded-full bg-gradient-to-br px-4 py-1 text-nowrap text-white backdrop-blur-lg transition-colors before:rounded-full after:rounded-full',
|
|
{
|
|
'from-accent to-accent/50': activeRegion === slugify(pin.city),
|
|
'from-greyscale-800/30 to-greyscale-700/30':
|
|
activeRegion !== slugify(pin.city)
|
|
}
|
|
)}
|
|
onclick={() => handleSetActiveMarker(pin.city)}
|
|
>
|
|
{pin.city}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
|
|
<div
|
|
class="relative container mx-auto flex h-full w-[250vw] flex-col justify-center overflow-scroll px-0 py-10 transition-all delay-250 duration-250 [scrollbar-width:none] md:w-fit md:flex-row md:overflow-auto md:py-0"
|
|
use:inView
|
|
use:mousePosition
|
|
>
|
|
<div
|
|
class="relative w-full origin-bottom -translate-y-20 transform-[perspective(20px)_rotateX(1deg)_scale3d(1.5,_1.5,_1)] overflow-scroll transition-all [scrollbar-width:none]"
|
|
bind:clientWidth={dimensions.width}
|
|
bind:clientHeight={dimensions.height}
|
|
>
|
|
<div
|
|
class="absolute inset-0 mask-[image:url('/images/appwrite-network/map.svg')] mask-contain mask-no-repeat"
|
|
>
|
|
<div
|
|
class={classNames(
|
|
'relative block aspect-square size-40 rounded-full blur-3xl transition-opacity',
|
|
'from-accent bg-radial-[circle_at_center] via-white/70 to-white/70',
|
|
'transform-[translate3d(calc(var(--mouse-x,_-100%)_*_1_-_16rem),_calc(var(--mouse-y,_-100%)_*_1_-_28rem),0)]'
|
|
)}
|
|
style:--mouse-x="{$position.x}px"
|
|
style:--mouse-y="{$position.y}px"
|
|
></div>
|
|
</div>
|
|
|
|
<img
|
|
src="/images/appwrite-network/map.svg"
|
|
class="pointer-events-none relative -z-10 opacity-10"
|
|
draggable="false"
|
|
alt="Map of the world"
|
|
/>
|
|
|
|
{#each pins[activeSegment as PinSegment] as pin, index}
|
|
<MapMarker {...pin} animate={$animate} {index} bounds={MAP_BOUNDS} />
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<MapTooltip coords={$position} />
|
|
<MapNav onValueChange={(value) => (activeSegment = value)} />
|