This commit is contained in:
Jesse Winton
2025-04-15 13:47:48 -04:00
parent 4e8273ba68
commit 2d2e53c3ca
3 changed files with 74 additions and 43 deletions

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { classNames } from '$lib/utils/classnames'; import { classNames } from '$lib/utils/classnames';
import { slugify } from '$lib/utils/slugify'; import { slugify } from '$lib/utils/slugify';
import { Tooltip } from 'bits-ui';
import { latLongToSvgPosition } from './utils/projections'; import { latLongToSvgPosition } from './utils/projections';
import { tooltipData } from './map-tooltip.svelte';
interface Props { interface Props {
city: string; city: string;
@@ -16,7 +16,7 @@
west: number; west: number;
east: number; east: number;
}; };
available?: boolean; available: boolean;
class?: string; class?: string;
animate?: boolean; animate?: boolean;
isOpen: boolean; isOpen: boolean;
@@ -28,22 +28,32 @@
index = 0, index = 0,
lat, lat,
lng, lng,
available = false, available,
class: className = '',
animate = false, animate = false,
isOpen = false isOpen = false
}: Props = $props(); }: Props = $props();
const position = $derived(latLongToSvgPosition({ latitude: lat, longitude: lng })); const position = $derived(latLongToSvgPosition({ latitude: lat, longitude: lng }));
let open = $state(isOpen); const handleSetActiveMarker = () => {
tooltipData.set({
$effect(() => { city,
open = isOpen; code,
available
}); });
};
const handleResetActiveMarker = () => {
console.log('reset');
// tooltipData.set({
// city: null,
// code: null,
// available: null
// });
};
</script> </script>
<div <button
class={classNames( class={classNames(
'group absolute z-10 flex size-2 cursor-pointer items-center justify-center opacity-0 [animation-delay:var(--delay)]', 'group absolute z-10 flex size-2 cursor-pointer items-center justify-center opacity-0 [animation-delay:var(--delay)]',
{ 'animate-fade-in': animate } { 'animate-fade-in': animate }
@@ -51,6 +61,11 @@
style="left: {position.x}%; top: {position.y}%;--delay: {index * 10}ms;" style="left: {position.x}%; top: {position.y}%;--delay: {index * 10}ms;"
data-region={slugify(city)} data-region={slugify(city)}
data-active={isOpen} data-active={isOpen}
onmouseenter={handleSetActiveMarker}
onfocus={handleSetActiveMarker}
onmouseout={handleResetActiveMarker}
onblur={handleResetActiveMarker}
aria-label={city}
> >
<span <span
class="from-accent/20 to-accent/10 border-gradient ease-spring absolute inline-flex h-5 w-5 rounded-full bg-gradient-to-b opacity-0 transition-opacity group-hover:animate-ping group-hover:opacity-75 before:rounded-full" class="from-accent/20 to-accent/10 border-gradient ease-spring absolute inline-flex h-5 w-5 rounded-full bg-gradient-to-b opacity-0 transition-opacity group-hover:animate-ping group-hover:opacity-75 before:rounded-full"
@@ -58,30 +73,4 @@
></span> ></span>
<span class="bg-accent absolute inline-flex h-full w-full rounded-full"></span> <span class="bg-accent absolute inline-flex h-full w-full rounded-full"></span>
<span class="absolute size-1/2 rounded-full bg-white/80 transition-all"></span> <span class="absolute size-1/2 rounded-full bg-white/80 transition-all"></span>
</div> </button>
<!-- <div
class={classNames(
'bg-card/90 border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]',
'data-[state="closed"]:animate-menu-out data-[state="instant-open"]:animate-menu-in data-[state="delayed-open"]:animate-menu-in',
className
)}
>
<span class="text-primary text-caption w-fit">
{city}
({code})
</span>
{#if available}
<div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-[#10B981]/24 p-1 text-center text-[#B4F8E2]"
>
<span class="text-micro -tracking-tight">Available now</span>
</div>
{:else}
<div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-white/6 p-1 text-center text-white/60"
>
<span class="text-micro -tracking-tight">Planned</span>
</div>
{/if}
</div> -->

View File

@@ -1,3 +1,18 @@
<script lang="ts" module>
import { classNames } from '$lib/utils/classnames';
import { writable } from 'svelte/store';
export const tooltipData = writable<{
city: string | null;
code: string | null;
available: boolean | null;
}>({
city: null,
code: null,
available: null
});
</script>
<script lang="ts"> <script lang="ts">
type Props = { type Props = {
coords: { coords: {
@@ -6,7 +21,38 @@
}; };
}; };
const { coords } = $props(); const { coords }: Props = $props();
</script> </script>
<div style:left="{coords.x}px" style:top="{coords.y}px">Tooltip</div> {#if $tooltipData.city}
<div
class="pointer-events-none absolute"
style:left="{coords.x + 10}px"
style:top="{coords.y - 25}px"
>
<div
class={classNames(
'bg-card/90 border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]',
'data-[state="closed"]:animate-menu-out data-[state="instant-open"]:animate-menu-in data-[state="delayed-open"]:animate-menu-in'
)}
>
<span class="text-primary text-caption w-fit">
{$tooltipData.city}
({$tooltipData.code})
</span>
{#if $tooltipData.available}
<div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-[#10B981]/24 p-1 text-center text-[#B4F8E2]"
>
<span class="text-micro -tracking-tight">Available now</span>
</div>
{:else}
<div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-white/6 p-1 text-center text-white/60"
>
<span class="text-micro -tracking-tight">Planned</span>
</div>
{/if}
</div>
</div>
{/if}

View File

@@ -11,12 +11,10 @@
import MapMarker from './map-marker.svelte'; import MapMarker from './map-marker.svelte';
import { slugify } from '$lib/utils/slugify'; import { slugify } from '$lib/utils/slugify';
import { classNames } from '$lib/utils/classnames'; import { classNames } from '$lib/utils/classnames';
import { Tooltip } from 'bits-ui';
import MapNav from './map-nav.svelte'; import MapNav from './map-nav.svelte';
import { useMousePosition } from '$lib/actions/mouse-position'; import { useMousePosition } from '$lib/actions/mouse-position';
import { useAnimateInView } from '$lib/actions/animate-in-view'; import { useAnimateInView } from '$lib/actions/animate-in-view';
import { pins, type PinSegment } from './data/pins'; import { pins, type PinSegment } from './data/pins';
import { latLongToSvgPosition } from './utils/projections';
import MapTooltip from './map-tooltip.svelte'; import MapTooltip from './map-tooltip.svelte';
let dimensions = $state({ let dimensions = $state({
@@ -123,15 +121,13 @@
alt="Map of the world" alt="Map of the world"
/> />
{#each pins[activeSegment as PinSegment].map( (pin) => ({ ...pin, isOpen: activeRegion === slugify(pin.city), position: latLongToSvgPosition( { latitude: pin.lat, longitude: pin.lng, ...dimensions } ) }) ) as pin, index} {#each pins[activeSegment as PinSegment].map( (pin) => ({ ...pin, isOpen: activeRegion === slugify(pin.city) }) ) as pin, index}
<MapMarker {...pin} animate={$animate} {index} bounds={MAP_BOUNDS} /> <MapMarker {...pin} animate={$animate} {index} bounds={MAP_BOUNDS} />
{/each} {/each}
</div> </div>
</div> </div>
</div> </div>
<MapTooltip coords={$position} /> <MapTooltip coords={$position} />
<MapNav onValueChange={(value) => (activeSegment = value)} /> <MapNav onValueChange={(value) => (activeSegment = value)} />
<style> <style>