This commit is contained in:
sameeranageshwar
2026-04-11 18:45:43 -04:00
parent 22298fab1a
commit d2e4d78bda
22 changed files with 619 additions and 16 deletions
@@ -0,0 +1,51 @@
<script lang="ts">
import Icon from '@iconify/svelte';
let isOpen = $state(false);
const mockAlerts = [
{ id: 1, loc: 'McKeldin 2nd Floor', level: 78, time: '2 mins ago' },
{ id: 2, loc: 'ESJ Atrium', level: 82, time: '15 mins ago' }
];
</script>
<div class="relative">
<!-- Notification Bell -->
<button onclick={() => isOpen = !isOpen} class="relative w-12 h-12 glass-panel flex items-center justify-center rounded-xl border border-white/10 hover:bg-white/10 transition-colors group {isOpen ? 'bg-white/10 ring-1 ring-neon-alert' : ''}">
<Icon icon="mdi:bell-outline" class="text-2xl text-slate-300 group-hover:text-white" />
<!-- Badge -->
{#if mockAlerts.length > 0}
<span class="absolute top-2 right-2 w-2.5 h-2.5 bg-neon-alert rounded-full shadow-[0_0_8px_rgba(255,7,58,0.8)] animate-pulse border border-slate-900"></span>
{/if}
</button>
<!-- Dropdown Panel -->
{#if isOpen}
<div class="absolute top-14 left-0 w-80 glass-panel rounded-2xl shadow-2xl border border-white/10 overflow-hidden z-30 animate-in fade-in slide-in-from-top-2">
<div class="px-4 py-3 border-b border-white/10 bg-black/40 flex justify-between items-center">
<h3 class="font-display font-medium text-white text-sm">Active Noise Alerts</h3>
<span class="text-xs text-slate-400 bg-white/5 py-0.5 px-2 rounded-full">{mockAlerts.length} New</span>
</div>
<div class="max-h-64 overflow-y-auto">
{#each mockAlerts as alert}
<button class="w-full text-left p-4 border-b border-white/5 hover:bg-white/5 transition-colors flex items-start gap-3">
<Icon icon="mdi:alert-rhombus-outline" class="text-neon-alert text-xl mt-0.5" />
<div class="flex-1">
<p class="text-sm font-medium text-red-200">Spike Detected: {alert.level} dB</p>
<p class="text-xs text-red-400/80 mt-1">{alert.loc}</p>
</div>
<p class="text-[10px] text-slate-500 font-mono mt-1">{alert.time}</p>
</button>
{/each}
{#if mockAlerts.length === 0}
<div class="p-6 text-center text-slate-500">
<Icon icon="mdi:check-circle-outline" class="text-3xl text-neon-primary mx-auto mb-2 opacity-50" />
<p class="text-sm">Campus is quiet right now.</p>
</div>
{/if}
</div>
</div>
{/if}
</div>
@@ -0,0 +1,124 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import { browser } from '$app/environment';
import { mapState, UMD_LOCATIONS } from '$lib/states/map.svelte';
import { themeState } from '$lib/states/theme.svelte';
import 'maplibre-gl/dist/maplibre-gl.css';
let mapContainer: HTMLDivElement | undefined = $state();
let mapInstance: any = $state();
// Watch and react to mapState updates using a Svelte 5 $effect
$effect(() => {
const target = mapState.targetFlyTo;
if (mapInstance && target) {
untrack(() => {
mapInstance.flyTo({
center: [target.lng, target.lat],
zoom: target.zoom,
essential: true,
duration: 1500
});
});
}
});
$effect(() => {
if (mapInstance) {
const style = themeState.isLight
? 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'
: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
untrack(() => { mapInstance.setStyle(style); });
}
});
onMount(async () => {
if (!browser || !mapContainer) return;
const { Map, NavigationControl } = await import('maplibre-gl');
const map = new Map({
container: mapContainer,
style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
center: [-76.9425, 38.9859], // UMD McKeldin Mall
zoom: 15.5,
pitch: 0,
bearing: 0,
attributionControl: false
});
map.addControl(new NavigationControl({
visualizePitch: true
}), 'bottom-right');
mapInstance = map;
map.on('load', () => {
map.addSource('study-locations', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: UMD_LOCATIONS.map(loc => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [loc.lng, loc.lat] },
properties: { name: loc.name }
}))
}
});
map.addLayer({
id: 'study-locations-labels',
type: 'symbol',
source: 'study-locations',
layout: {
'text-field': ['get', 'name'],
'text-size': 13,
'text-variable-anchor': ['top', 'bottom', 'left', 'right'],
'text-radial-offset': 1,
'text-justify': 'center'
},
paint: {
'text-color': '#ffffff',
'text-halo-color': '#000000',
'text-halo-width': 2
}
});
});
return () => {
map.remove();
};
});
</script>
<div class="absolute inset-0 z-0 bg-crust transition-colors duration-500">
<div bind:this={mapContainer} class="absolute inset-0 z-0 {themeState.isLight ? 'mix-blend-normal opacity-100 filter brightness-105 contrast-125 grayscale' : 'mix-blend-luminosity opacity-90'} transition-all duration-500" style="width: 100%; height: 100%;"></div>
<!-- Color Screen Wash -->
<div class="absolute inset-0 pointer-events-none {themeState.isLight ? 'mix-blend-screen bg-white opacity-20' : 'mix-blend-color bg-crust opacity-100'} z-10 transition-all duration-500"></div>
</div>
<style>
:global(.maplibregl-ctrl-group) {
background: rgba(15, 23, 42, 0.8) !important;
backdrop-filter: blur(12px) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-left: 2px solid var(--color-neon-primary) !important;
box-shadow: var(--shadow-glow-primary) !important;
border-radius: 12px !important;
overflow: hidden;
}
:global(.maplibregl-ctrl-group button) {
width: 40px !important;
height: 40px !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important;
}
:global(.maplibregl-ctrl-group button:last-child) {
border-bottom: none !important;
}
:global(.maplibregl-ctrl-icon) {
filter: invert(1) opacity(0.8) !important;
}
:global(.maplibregl-ctrl-group button:hover) {
background: rgba(255, 255, 255, 0.1) !important;
}
</style>
@@ -0,0 +1,48 @@
<script lang="ts">
import { mapState, UMD_LOCATIONS } from '$lib/states/map.svelte';
let { showDropdown = false } = $props<{ showDropdown?: boolean }>();
const legendItems = [
{ label: 'Harmful (80+ dB)', color: '#f38ba8' },
{ label: 'Disruptive (65-79 dB)', color: '#fab387' },
{ label: 'Manageable (55-64 dB)', color: '#f9e2af' },
{ label: 'Great (40-54 dB)', color: '#94e2d5' },
{ label: 'Ideal (0-39 dB)', color: '#89b4fa' }
];
</script>
<div class="absolute top-6 right-6 md:right-8 z-20 flex flex-col gap-3 items-end">
<div class="glass-panel p-4 md:p-5 rounded-2xl border-l-2 border-l-neon-primary flex flex-col gap-3" style="box-shadow: var(--shadow-glow-primary)">
<h3 class="font-display font-medium text-white text-xs md:text-sm tracking-wider uppercase mb-1 border-b border-white/5 pb-2">Noise Levels</h3>
<div class="flex flex-col gap-2.5">
{#each legendItems as item}
<div class="flex items-center gap-3">
<div class="w-3.5 h-3.5 rounded shadow-sm shrink-0 border border-white/20" style="background-color: {item.color}; box-shadow: 0 0 8px {item.color}60"></div>
<span class="text-xs font-mono text-slate-300 font-medium whitespace-nowrap">{item.label}</span>
</div>
{/each}
</div>
</div>
{#if showDropdown}
<div class="glass-panel rounded-xl border-l-2 border-l-neon-primary overflow-hidden flex flex-col w-64" style="box-shadow: var(--shadow-glow-primary)">
<select class="bg-transparent text-white font-display text-sm p-4 border-none outline-none focus:ring-0 cursor-pointer w-full font-medium" onchange={(e) => {
const val = (e.target as HTMLSelectElement).value;
if (val === 'default') {
mapState.flyHome();
} else if (val) {
const loc = UMD_LOCATIONS.find(l => l.name === val);
if (loc) {
mapState.flyTo(loc.lng, loc.lat, 18);
}
}
}}>
<option value="default" class="bg-crust text-white font-semibold text-center hidden md:block">Jump to Campus Area</option>
{#each UMD_LOCATIONS as loc}
<option value={loc.name} class="bg-crust text-white">{loc.name}</option>
{/each}
</select>
</div>
{/if}
</div>
+36
View File
@@ -0,0 +1,36 @@
export interface StudyLocation {
id: string;
name: string;
hasFloors: boolean;
floors?: string[];
lng: number;
lat: number;
}
export const UMD_LOCATIONS: StudyLocation[] = [
{ id: 'esj', name: 'Edward St. John (ESJ)', hasFloors: true, floors: ['Floor 1', 'Floor 2', 'Floor 3'], lng: -76.94209511596014, lat: 38.987133359608755 },
{ id: 'mckeldin', name: 'McKeldin Library', hasFloors: true, floors: ['Floor 1', 'Floor 2', 'Floor 3', 'Floor 4', 'Floor 5', 'Floor 6', 'Floor 7'], lng: -76.94494907523277, lat: 38.986021017749366 },
{ id: 'hornbake', name: 'Hornbake Library', hasFloors: false, lng: -76.94161787005467, lat: 38.988233373664826 },
{ id: 'stem', name: 'STEM Library', hasFloors: false, lng: -76.93942003731279, lat: 38.988991437126195 },
{ id: 'clarice', name: 'Clarice Library', hasFloors: false, lng: -76.94967344225785, lat: 38.99029330703877 },
{ id: 'yahentamitsi', name: 'Yahentamitsi', hasFloors: false, lng: -76.9448027183373, lat: 38.99108961575231 },
{ id: 'iribe', name: 'Iribe', hasFloors: false, lng: -76.93643838603555, lat: 38.98933701397555 },
{ id: 'reckord', name: 'Reckord Armory', hasFloors: false, lng: -76.93897470250619, lat: 38.98609556181066 }
];
export const DEFAULT_VIEW = { lng: -76.9425, lat: 38.9859, zoom: 15.5 };
class MapState {
// The target coordinates the map should fly to
targetFlyTo = $state<{ lng: number; lat: number; zoom: number; timestamp: number } | null>(null);
flyTo(lng: number, lat: number, zoom: number = 18) {
this.targetFlyTo = { lng, lat, zoom, timestamp: Date.now() }; // timestamp ensures reactivity even if same coords
}
flyHome() {
this.flyTo(DEFAULT_VIEW.lng, DEFAULT_VIEW.lat, DEFAULT_VIEW.zoom);
}
}
export const mapState = new MapState();
+18
View File
@@ -0,0 +1,18 @@
import { browser } from '$app/environment';
class ThemeState {
isLight = $state(false);
toggle() {
this.isLight = !this.isLight;
if (browser) {
if (this.isLight) {
document.documentElement.classList.add('light-theme');
} else {
document.documentElement.classList.remove('light-theme');
}
}
}
}
export const themeState = new ThemeState();