Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { AddPinModal } from "@/components/AddPinModal";
import { MapCursor } from "@/components/MapCursor";
import { TargetLine } from "@/components/TargetLine";
import { Polyline } from "@/components/Polyline";
import { JEEPNEY_ROUTES } from "@/data/map-layers";
import { Polygon } from "@/components/Polygon";
import { JEEPNEY_ROUTES, CAMPUS_ZONES, ZONE_CATEGORIES } from "@/data/map-layers";
import { useState, useEffect, useRef, useMemo } from "react";
import { trpc } from "@/lib/trpc";

Expand Down Expand Up @@ -45,6 +46,14 @@ export default function Home() {
);
};

const [activeZoneCategories, setActiveZoneCategories] = useState<string[]>([]);

const handleToggleZoneCategory = (categoryId: string) => {
setActiveZoneCategories((prev) =>
prev.includes(categoryId) ? prev.filter(id => id !== categoryId) : [...prev, categoryId]
);
};

const [pendingPinCoords, setPendingPinCoords] = useState<{
lat: number;
lng: number;
Expand Down Expand Up @@ -166,6 +175,23 @@ export default function Home() {
/>
)}

{CAMPUS_ZONES.map((zone) => {
if (!activeZoneCategories.includes(zone.categoryId)) return null;

const categoryDef = ZONE_CATEGORIES.find(c => c.id === zone.categoryId);
const zoneColor = categoryDef ? categoryDef.color : "#FFFFFF";

return (
<Polygon
key={zone.id}
paths={zone.paths}
fillColor={zoneColor}
strokeColor={zoneColor}
isPulsating={true}
/>
);
})}

{JEEPNEY_ROUTES.map((route) => {
if (!activeRoutes.includes(route.id)) return null;

Expand All @@ -190,6 +216,8 @@ export default function Home() {
onSearchChange={setSearchQuery}
activeRoutes={activeRoutes}
onToggleRoute={handleToggleRoute}
activeZoneCategories={activeZoneCategories}
onToggleZoneCategory={handleToggleZoneCategory}
/>

{/* TARGETING CROSSHAIR (Only visible when armed) */}
Expand Down
92 changes: 92 additions & 0 deletions apps/web/components/Polygon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client";

import { useEffect, useRef } from "react";
import { useMap, useApiIsLoaded } from "@vis.gl/react-google-maps";

interface PolygonProps {
paths: { lat: number; lng: number }[];
fillColor?: string;
fillOpacity?: number;
strokeColor?: string;
strokeOpacity?: number;
strokeWeight?: number;
zIndex?: number;
isPulsating?: boolean;
}

export function Polygon({
paths,
fillColor = "#00FF99",
fillOpacity = 0.05,
strokeColor = "#00FF99",
strokeOpacity = 0.8,
strokeWeight = 2,
zIndex = 1,
isPulsating = true
}: PolygonProps) {
const map = useMap();
const isLoaded = useApiIsLoaded();
const polygonRef = useRef<google.maps.Polygon | null>(null);
const animationRef = useRef<number>(0);

useEffect(() => {
if (!map || !isLoaded || !window.google) return;

if (!polygonRef.current) {
polygonRef.current = new window.google.maps.Polygon({
paths,
fillColor,
fillOpacity,
strokeColor,
strokeOpacity,
strokeWeight,
zIndex,
map,
});
} else {
polygonRef.current.setOptions({
paths,
fillColor,
strokeColor,
strokeOpacity,
strokeWeight,
zIndex,
map,
});
}

const animatePulse = () => {
if (!polygonRef.current || !isPulsating) return;

const time = Date.now();
const speed = 800;
const sineValue = Math.sin(time / speed);
const variance = 0.10;
const currentOpacity = fillOpacity + (sineValue * variance);
const clampedOpacity = Math.max(0.05, Math.min(0.4, currentOpacity));

polygonRef.current.setOptions({
fillOpacity: fillOpacity,
strokeOpacity: 0.6 + (sineValue * 0.3)
});

animationRef.current = requestAnimationFrame(animatePulse);
};

if (isPulsating) {
cancelAnimationFrame(animationRef.current);
animationRef.current = requestAnimationFrame(animatePulse);
} else {
polygonRef.current.setOptions({ fillOpacity, strokeOpacity });
}

return () => {
cancelAnimationFrame(animationRef.current);
if (polygonRef.current) {
polygonRef.current.setMap(null);
}
};
}, [map, isLoaded, paths, fillColor, fillOpacity, strokeColor, strokeOpacity, strokeWeight, zIndex, isPulsating]);

return null;
}
48 changes: 47 additions & 1 deletion apps/web/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import { trpc } from "@/lib/trpc";
import { useSession } from "@/lib/auth-client";
import { JEEPNEY_ROUTES } from "@/data/map-layers";
import { JEEPNEY_ROUTES, ZONE_CATEGORIES } from "@/data/map-layers";

export type FilterType = "all" | "academic" | "food" | "social" | "utility";

Expand All @@ -16,6 +16,8 @@ interface TopBarProps {
onSearchChange: (query: string) => void;
activeRoutes?: string[];
onToggleRoute?: (routeId: string) => void;
activeZoneCategories?: string[];
onToggleZoneCategory?: (categoryId: string) => void;
}

export function TopBar({
Expand All @@ -26,10 +28,13 @@ export function TopBar({
onSearchChange,
activeRoutes = [],
onToggleRoute = () => {},
activeZoneCategories = [],
onToggleZoneCategory = () => {},
}: TopBarProps) {
const router = useRouter();
const { data: sessionData } = useSession();
const [isTransitMenuOpen, setIsTransitMenuOpen] = useState(false);
const [isZoneMenuOpen, setIsZoneMenuOpen] = useState(false);

const handleProfileClick = () => {
if (sessionData?.user) {
Expand Down Expand Up @@ -112,6 +117,47 @@ export function TopBar({
})}
</div>
)}
</div>

<div className="transit-system-container">
<button
type="button"
className={`icon-button transit-btn ${isZoneMenuOpen ? 'active' : ''}`}
title="Toggle Zone Layers"
onClick={() => {
setIsZoneMenuOpen(!isZoneMenuOpen);
}}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon>
</svg>
</button>

{isZoneMenuOpen && (
<div className="extruded-menu">
{ZONE_CATEGORIES.map((category) => {
const isActive = activeZoneCategories.includes(category.id);

return (
<button
key={category.id}
type="button"
onClick={() => onToggleZoneCategory(category.id)}
className="route-node"
title={category.label}
style={{
backgroundColor: isActive ? `${category.color}20` : 'rgba(255, 255, 255, 0.05)',
color: isActive ? category.color : '#aaa',
borderColor: isActive ? category.color : 'transparent',
boxShadow: isActive ? `0 0 10px ${category.color}40` : 'none',
}}
>
{category.initial}
</button>
);
})}
</div>
)}
</div>
</div>

Expand Down
Loading
Loading