Event-Specific Map

This commit is contained in:
Gus Ruben 2025-07-29 01:29:50 -04:00
parent 22ab6be86a
commit 93d4505fce
3 changed files with 449 additions and 3 deletions

View file

@ -0,0 +1,42 @@
import { GEOCODER_API_KEY } from '$env/static/private';
export const prerender = false;
/** @type {import('./$types').PageServerLoad} */
export async function load({ url }) {
const location = url.searchParams.get('location');
if (!GEOCODER_API_KEY || !location) {
return {
location: null,
geocoded: null
};
}
try {
// Geocode the provided location
const geocodeUrl = `https://geocoder.hackclub.com/v1/geocode?address=${encodeURIComponent(location)}&key=${GEOCODER_API_KEY}`;
const geocodeResponse = await fetch(geocodeUrl);
if (geocodeResponse.ok) {
const geocodeData = await geocodeResponse.json();
return {
location,
geocoded: {
lat: geocodeData.lat,
lng: geocodeData.lng,
address: location
}
};
} else {
console.error(`Failed to geocode ${location}: ${geocodeResponse.status}`);
}
} catch (error) {
console.error(`Failed to geocode ${location}:`, error);
}
return {
location,
geocoded: null
};
}

View file

@ -0,0 +1,396 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
/** @type {import('./$types').PageData} */
export let data;
let mapContainer: HTMLElement;
let map: any;
onMount(() => {
if (browser) {
// Mouse trail setup
let mouseTrailPoints: Array<{ x: number; y: number }> = [];
const maxTrailLength = 15;
let animationId: number;
// Rainbow trail configuration - adjust these to tune the effect
const colorChangeSpeed = 400; // Lower = faster color changes (was 800)
const trailColorSpan = 25; // Degrees of color variation across trail length (was 15)
const trailThicknessStart = 3; // Path thickness at tail (oldest points)
const trailThicknessEnd = 8; // Path thickness at mouse (newest points)
function getHueFromIndex(index: number): number {
return (index * 24) % 360; // Rainbow cycle through hues
}
function updateMouseTrail(e: MouseEvent) {
// Get fresh references to all SVG containers
const svgs = [
document.querySelector('#trail-svg-blur'),
// document.querySelector('#trail-svg-main')
];
if (svgs.find(svg => !svg)) return;
const x = e.clientX;
const y = e.clientY;
mouseTrailPoints.push({ x, y });
// Keep trail at max length
if (mouseTrailPoints.length > maxTrailLength) {
mouseTrailPoints.shift();
}
// Clear existing paths and gradients
if (mouseTrailPoints.length > 1) {
svgs.forEach((svg: any) => {
// Clear old elements
const oldPaths = svg.querySelectorAll('.trail-segment');
const oldGradients = svg.querySelectorAll('.segment-gradient');
oldPaths.forEach((path: any) => path.remove());
oldGradients.forEach((grad: any) => grad.remove());
// Ensure defs exists
let defs = svg.querySelector('defs');
if (!defs) {
defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svg.appendChild(defs);
}
});
// Create individual path segments with gradient colors
for (let i = 0; i < mouseTrailPoints.length - 1; i++) {
const progress = i / (mouseTrailPoints.length - 1);
const nextProgress = (i + 1) / (mouseTrailPoints.length - 1);
// Base color changes slowly over time, with subtle variations along trail
const baseHue = getHueFromIndex(Date.now() / colorChangeSpeed);
const hue1 = baseHue - (progress * trailColorSpan);
const hue2 = baseHue - (nextProgress * trailColorSpan);
// Create paths in all SVGs
svgs.forEach((svg: any, svgIndex: number) => {
if (!svg) return;
const defs = svg.querySelector('defs');
if (!defs) return;
// Create gradient for this segment (unique ID per SVG)
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gradient.classList.add('segment-gradient');
gradient.setAttribute('id', `segment-gradient-${svgIndex}-${i}`);
gradient.setAttribute('x1', '0%');
gradient.setAttribute('y1', '0%');
gradient.setAttribute('x2', '100%');
gradient.setAttribute('y2', '0%');
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', `hsl(${hue1}, 70%, 60%)`);
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop2.setAttribute('offset', '100%');
stop2.setAttribute('stop-color', `hsl(${hue2}, 70%, 60%)`);
gradient.appendChild(stop1);
gradient.appendChild(stop2);
defs.appendChild(gradient);
// Create path with gradient stroke - bigger at mouse (newest), smaller at tail (oldest)
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.classList.add('trail-segment');
path.setAttribute('d', `M${mouseTrailPoints[i].x},${mouseTrailPoints[i].y} L${mouseTrailPoints[i + 1].x},${mouseTrailPoints[i + 1].y}`);
path.setAttribute('stroke', `url(#segment-gradient-${svgIndex}-${i})`);
path.setAttribute('stroke-width', `${trailThicknessStart + (trailThicknessEnd - trailThicknessStart) * progress}`);
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
path.setAttribute('fill', 'none');
svg.appendChild(path);
});
}
}
}
function clearMouseTrail() {
mouseTrailPoints = [];
const svgs = [
document.querySelector('#trail-svg-blur'),
// document.querySelector('#trail-svg-main')
];
svgs.forEach((svg: any) => {
if (svg) {
const oldPaths = svg.querySelectorAll('.trail-segment');
oldPaths.forEach((path: any) => path.remove());
}
});
}
// Trail fade animation - only remove points every few frames
let frameCount = 0;
function animateTrail() {
frameCount++;
// Only remove points every 3 frames to slow down the fade
if (frameCount % 3 === 0 && mouseTrailPoints.length > 0) {
mouseTrailPoints.shift();
// Redraw trail segments with updated colors
const svgs = [
document.querySelector('#trail-svg-blur'),
// document.querySelector('#trail-svg-main')
];
if (mouseTrailPoints.length > 1 && !svgs.find(svg => !svg)) {
svgs.forEach((svg: any) => {
// Clear old elements
const oldPaths = svg.querySelectorAll('.trail-segment');
const oldGradients = svg.querySelectorAll('.segment-gradient');
oldPaths.forEach((path: any) => path.remove());
oldGradients.forEach((grad: any) => grad.remove());
// Ensure defs exists
let defs = svg.querySelector('defs');
if (!defs) {
defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svg.appendChild(defs);
}
});
// Redraw segments with gradients
for (let i = 0; i < mouseTrailPoints.length - 1; i++) {
const progress = i / (mouseTrailPoints.length - 1);
const nextProgress = (i + 1) / (mouseTrailPoints.length - 1);
// Base color changes slowly over time, with subtle variations along trail
const baseHue = getHueFromIndex(Date.now() / colorChangeSpeed);
const hue1 = baseHue - (progress * trailColorSpan);
const hue2 = baseHue - (nextProgress * trailColorSpan);
// Create paths in all SVGs
svgs.forEach((svg: any, svgIndex: number) => {
if (!svg) return;
const defs = svg.querySelector('defs');
if (!defs) return;
// Create gradient
const gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
gradient.classList.add('segment-gradient');
gradient.setAttribute('id', `segment-gradient-${svgIndex}-${i}`);
gradient.setAttribute('x1', '0%');
gradient.setAttribute('y1', '0%');
gradient.setAttribute('x2', '100%');
gradient.setAttribute('y2', '0%');
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop1.setAttribute('offset', '0%');
stop1.setAttribute('stop-color', `hsl(${hue1}, 70%, 60%)`);
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop2.setAttribute('offset', '100%');
stop2.setAttribute('stop-color', `hsl(${hue2}, 70%, 60%)`);
gradient.appendChild(stop1);
gradient.appendChild(stop2);
defs.appendChild(gradient);
// Create path - bigger at mouse (newest), smaller at tail (oldest)
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.classList.add('trail-segment');
path.setAttribute('d', `M${mouseTrailPoints[i].x},${mouseTrailPoints[i].y} L${mouseTrailPoints[i + 1].x},${mouseTrailPoints[i + 1].y}`);
path.setAttribute('stroke', `url(#segment-gradient-${svgIndex}-${i})`);
path.setAttribute('stroke-width', `${trailThicknessStart + (trailThicknessEnd - trailThicknessStart) * progress}`);
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
path.setAttribute('fill', 'none');
svg.appendChild(path);
});
}
}
}
animationId = requestAnimationFrame(animateTrail);
}
// Wait for DOM and get elements
setTimeout(() => {
const svgs = [
document.querySelector('#trail-svg-blur'),
// document.querySelector('#trail-svg-main')
];
if (!svgs.find(svg => !svg)) {
// Event listeners
document.addEventListener('mousemove', updateMouseTrail);
document.addEventListener('mouseleave', clearMouseTrail);
// Start animation loop
animateTrail();
}
}, 100);
// Initialize Leaflet
import('leaflet').then((L) => {
// Load CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(link);
// Initialize map
map = L.map(mapContainer, {
minZoom: 2,
maxZoom: 18
}).setView([20, 0], 2);
// Add tile layer with custom styling
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap contributors © CARTO',
detectRetina: true
}).addTo(map);
// Create custom icon using map-flag.png
const flagIcon = L.icon({
iconUrl: '/map-flag.png',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32]
});
// Add marker for the specified location if geocoded successfully
if (data.geocoded && data.geocoded.lat && data.geocoded.lng) {
// Set the map view to the geocoded location with higher zoom
map.setView([data.geocoded.lat, data.geocoded.lng], 13);
const marker = L.marker([data.geocoded.lat, data.geocoded.lng], { icon: flagIcon })
.addTo(map);
// Create popup that shows the address
const popup = L.popup({
closeButton: false,
className: 'custom-popup'
}).setContent(data.geocoded.address);
marker.bindPopup(popup);
// Show popup on hover, hide on mouseout
marker.on('mouseover', () => {
marker.openPopup();
});
marker.on('mouseout', () => {
marker.closePopup();
});
}
});
// Cleanup function
return () => {
document.removeEventListener('mousemove', updateMouseTrail);
document.removeEventListener('mouseleave', clearMouseTrail);
if (animationId) {
cancelAnimationFrame(animationId);
}
};
}
});
</script>
<svelte:head>
<title>Event Location Map</title>
</svelte:head>
<!-- Mouse trailer SVGs -->
<!-- Background blurred layer -->
<!-- svelte-ignore component_name_lowercase -->
<svg id="trail-svg-blur" class="fixed top-0 left-0 w-full h-full pointer-events-none z-[9998]" style="filter: blur(10px); opacity: 0.75">
<!-- Individual path segments will be added here dynamically -->
</svg>
<!-- Foreground sharp layer -->
<!-- svelte-ignore component_name_lowercase -->
<svg id="trail-svg-main" class="fixed top-0 left-0 w-full h-full pointer-events-none z-[9999]" style="filter: blur(3px) saturate(0) brightness(1000); opacity: 0.25">
<!-- Individual path segments will be added here dynamically -->
</svg>
<div bind:this={mapContainer} class="w-full h-screen"></div>
<style>
:global(body) {
margin: 0;
padding: 0;
}
/* Map container styling */
:global(.leaflet-container) {
background: linear-gradient(135deg, #CCF4FD 0%, #B8D9F8 100%) !important;
border-radius: 16px !important;
overflow: hidden !important;
}
/* Map controls styling */
:global(.leaflet-control-zoom a) {
background: #44DBC8 !important;
border: 2px solid #3CC2AF !important;
color: white !important;
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(68, 219, 200, 0.3) !important;
transition: all 0.2s ease-in-out !important;
}
:global(.leaflet-control-zoom a:hover) {
background: #3CC2AF !important;
transform: translateY(-1px) !important;
box-shadow: 0 6px 16px rgba(68, 219, 200, 0.4) !important;
}
:global(.leaflet-control-attribution) {
background: rgba(252, 247, 196, 0.9) !important;
border: 1px solid #D3B180 !important;
border-radius: 8px !important;
color: #78531D !important;
font-family: 'Atkinson Hyperlegible', system-ui, sans-serif !important;
backdrop-filter: blur(8px) !important;
display: none !important;
}
:global(.leaflet-control-attribution a) {
color: #4477A3 !important;
text-decoration: none !important;
}
:global(.leaflet-control-attribution a:hover) {
color: #44DBC8 !important;
}
/* Custom popup styling */
:global(.custom-popup .leaflet-popup-content-wrapper) {
background: #FFFBDF !important;
border: 1px solid #D3B180 !important;
border-radius: 8px !important;
box-shadow: 0 4px 8px rgba(211, 177, 128, 0.15) !important;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
}
:global(.custom-popup .leaflet-popup-content) {
margin: 8px 12px !important;
color: #78531D !important;
font-size: 14px !important;
font-weight: 500 !important;
line-height: 1.2 !important;
}
:global(.custom-popup .leaflet-popup-tip) {
background: #FFFBDF !important;
border: 1px solid #D3B180 !important;
border-top: none !important;
border-right: none !important;
}
/* Tile layer filter for softer appearance */
:global(.leaflet-tile-pane) {
filter: hue-rotate(5deg) saturate(0.8) brightness(1.1) !important;
}
</style>

View file

@ -14,6 +14,8 @@
// Configuration - Put your information here!
const eventName = "Example";
const eventLocation = "Example City";
const eventAddress = "1600 Pennsylvania Avenue, Washington, DC 20500";
const directionsURL = "https://www.google.com/maps/search/1600+pennsylvania+avenue/" // This is optional!
// Sponsors Configuration
const sponsorsEnabled = true; // Set to false to hide the entire sponsors section
@ -1197,7 +1199,7 @@ Mumbai`.split("\n")
<!-- Map container with cloudy edges -->
<div class="relative w-full h-156 overflow-hidden bg-transparent">
<iframe
src="/map"
src="/event-map?location={encodeURIComponent(eventAddress)}"
class="w-full h-full border-0 bg-[#acd4e0]"
style="
mask-image:
@ -1288,14 +1290,20 @@ Mumbai`.split("\n")
</iframe>
</div>
<p class="text-center font-sans text-2xl pt-12 max-sm:text-xl text-[#60574b] z-10000">All daydream events are organized by high school students like yourself! <br> <span class="font-bold"><a class="underline hover:text-pink" href="https://forms.hackclub.com/daydream">Sign up</a> to organize now!</span></p>
<p class="text-center font-sans text-2xl pt-12 max-sm:text-xl text-[#60574b] z-10000">
{#if directionsURL}
Daydream {eventName} is taking place at <a class="underline text-pink" href={directionsURL}>{eventAddress}</a>!
{:else}
Daydream {eventName} is taking place at <span class="underline">{eventAddress}</span>!
{/if}
</p>
</div>
</div>
<div class="absolute top-0 left-0 w-full h-full bg-[url('brushstroking.png')] bg-size-[100vw_100vh] bg-repeat mix-blend-overlay opacity-60 pointer-events-none"></div>
<!-- Macintosh Section -->
<div class="w-full flex justify-center py-16 px-8 mt-24 pb-[clamp(232px,29vw,464px)] max-sm:w-[120vw] max-sm:-translate-x-[10vw]">
<div class="w-full flex justify-center py-16 px-8 mt-48 pb-[clamp(232px,29vw,464px)] max-sm:w-[120vw] max-sm:-translate-x-[10vw]">
<div class="bg-[#c5c2b1] p-4 relative max-w-4xl" style="border-radius: calc(1.5rem + 1rem);">
<div class="bg-[#061E2D] text-[#D1E3EE] rounded-3xl py-18 md:py-16 px-10 md:px-18 relative overflow-visible" style="
border-image: url('/macintosh.png') 128 91 464 91;