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
102 changes: 96 additions & 6 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import { TargetLine } from "@/components/TargetLine";
import { Polyline } from "@/components/Polyline";
import { Polygon } from "@/components/Polygon";
import { Sidebar } from "@/components/Sidebar";
import { NavigationHUD } from "@/components/NavigationHUD";
import { DirectionsRenderer } from "@/components/DirectionsRenderer";
import { useUserLocation } from "@/hooks/useUserLocation";
import { useNavigationRoute } from "@/hooks/useNavigationRoute";
import {
JEEPNEY_ROUTES,
CAMPUS_ZONES,
Expand Down Expand Up @@ -46,10 +50,13 @@ export default function Home() {
const {
mode,
selectedPinId,
navigationPinId,
selectPin,
clearSelection,
toggleMenu,
toggleLock,
startNavigation,
stopNavigation,
} = useWaypointState();
const [activeFilter, setActiveFilter] = useState<FilterType>("all");
const [searchQuery, setSearchQuery] = useState("");
Expand Down Expand Up @@ -89,9 +96,26 @@ export default function Home() {
});
}, []);

const mockUserLocation = { lat: 14.6549, lng: 121.0645 };
// Real user location from browser geolocation
const {
location: userLocation,
isLoading: isLocationLoading,
error: locationError,
} = useUserLocation();

// Fallback to mock location if real location not available yet
const effectiveUserLocation = userLocation ?? { lat: 14.6549, lng: 121.0645 };
const mockHeading = 45;

// Navigation route management
const {
route: navigationRoute,
error: routeError,
isLoading: isRouteLoading,
fetchRoute,
clearRoute,
} = useNavigationRoute();

const { theme } = useTheme();

const [pendingPinCoords, setPendingPinCoords] = useState<{
Expand All @@ -112,6 +136,46 @@ export default function Home() {
return pinsParsed.find((p) => p.id === selectedPinId);
}, [pinsParsed, selectedPinId]);

// Find the navigation destination pin
const navigationDestination = useMemo(() => {
if (!navigationPinId) return null;
return pinsParsed.find((p) => p.id === navigationPinId);
}, [navigationPinId, pinsParsed]);

// Fetch route when navigation starts (only when navigationPinId changes, not on location updates)
useEffect(() => {


if (
navigationPinId &&
navigationDestination &&
effectiveUserLocation
) {
fetchRoute(
{
latitude: effectiveUserLocation.lat,
longitude: effectiveUserLocation.lng,
},
{
latitude: navigationDestination.latitude,
longitude: navigationDestination.longitude,
},
);
}
// Only depend on navigationPinId to avoid re-fetching on every location update
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigationPinId]);

// Handle navigation click
const handleNavigateClick = useCallback(() => {
if (mode === "NAVIGATING" && navigationPinId) {
stopNavigation();
clearRoute();
} else if (selectedPinId) {
startNavigation(selectedPinId);
}
}, [mode, navigationPinId, selectedPinId, startNavigation, stopNavigation, clearRoute]);

useEffect(() => {
if (!isAddingPin) return;
const handleMouseMove = (e: MouseEvent) => {
Expand Down Expand Up @@ -207,18 +271,31 @@ export default function Home() {
);
})}

<AdvancedMarker position={mockUserLocation} zIndex={50}>
<AdvancedMarker position={effectiveUserLocation} zIndex={50}>
<MapCursor heading={mockHeading} />
</AdvancedMarker>

{activePinObj && (
{activePinObj && mode !== "NAVIGATING" && (
<TargetLine
start={mockUserLocation}
start={effectiveUserLocation}
end={{ lat: activePinObj.latitude, lng: activePinObj.longitude }}
color="--neon-blue"
/>
)}

{/* Navigation Route Polyline */}
{mode === "NAVIGATING" && navigationRoute && (
<DirectionsRenderer
path={navigationRoute.coordinates.map((c) => ({
lat: c.latitude,
lng: c.longitude,
}))}
color="#00b0ff"
weight={5}
visible={true}
/>
)}

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

Expand Down Expand Up @@ -266,10 +343,21 @@ export default function Home() {
onToggleRoute={handleToggleRoute}
activeZoneCategories={activeZoneCategories}
onToggleZoneCategory={handleToggleZoneCategory}
userLocation={mockUserLocation}
hideControls={!!selectedPinId}
userLocation={effectiveUserLocation}
hideControls={!!selectedPinId || mode === "NAVIGATING"}
/>

{/* Navigation HUD - shown when navigating */}
{mode === "NAVIGATING" && navigationRoute && (
<NavigationHUD
distanceMeters={navigationRoute.distanceMeters}
durationSeconds={navigationRoute.durationSeconds}
isLoading={isRouteLoading}
error={routeError}
onCancel={handleNavigateClick}
/>
)}

{/* TARGETING CROSSHAIR (Only visible when armed) */}
{isAddingPin && (
<div
Expand Down Expand Up @@ -299,7 +387,9 @@ export default function Home() {
<HeadsUpDisplay
selectedPinId={selectedPinId}
isLocked={mode === "LOCKED"}
isNavigating={mode === "NAVIGATING"}
onLockClick={toggleLock}
onNavigateClick={handleNavigateClick}
onClearSelection={clearSelection}
onAddPinClick={() => {
clearSelection();
Expand Down
62 changes: 62 additions & 0 deletions apps/web/components/DirectionsRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

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

interface DirectionsRendererProps {
path: { lat: number; lng: number }[];
color?: string;
weight?: number;
visible?: boolean;
}

export function DirectionsRenderer({
path,
color = "#00b0ff",
weight = 5,
visible = true,
}: DirectionsRendererProps) {
const map = useMap();
const isLoaded = useApiIsLoaded();
const polylineRef = useRef<google.maps.Polyline | null>(null);

useEffect(() => {
if (!map || !isLoaded || !window.google || !visible || path.length === 0) {
// Clean up polyline if it exists
if (polylineRef.current) {
polylineRef.current.setMap(null);
polylineRef.current = null;
}
return;
}

if (!polylineRef.current) {
polylineRef.current = new window.google.maps.Polyline({
path,
strokeColor: color,
strokeWeight: weight,
strokeOpacity: 0.9,
geodesic: true,
map,
});
} else {
polylineRef.current.setOptions({
path,
strokeColor: color,
strokeWeight: weight,
});
}
}, [map, isLoaded, path, color, weight, visible]);

// Cleanup on unmount or when visibility changes
useEffect(() => {
return () => {
if (polylineRef.current) {
polylineRef.current.setMap(null);
polylineRef.current = null;
}
};
}, []);

return null;
}
6 changes: 6 additions & 0 deletions apps/web/components/HeadsUpDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ interface HUDProps {
selectedPinId: string | null;
onLockClick: () => void;
isLocked: boolean;
isNavigating?: boolean;
onNavigateClick: () => void;
onClearSelection?: () => void;
onAddPinClick?: () => void;
}
Expand All @@ -17,6 +19,8 @@ export function HeadsUpDisplay({
selectedPinId,
onLockClick,
isLocked,
isNavigating = false,
onNavigateClick,
onClearSelection,
onAddPinClick,
}: HUDProps) {
Expand Down Expand Up @@ -58,7 +62,9 @@ export function HeadsUpDisplay({
<PinDetailsCard
pinId={selectedPinId}
isLocked={isLocked}
isNavigating={isNavigating}
onLockClick={onLockClick}
onNavigateClick={onNavigateClick}
onClose={onClearSelection}
onExpand={() => setIsExpanded(true)}
/>
Expand Down
131 changes: 131 additions & 0 deletions apps/web/components/NavigationHUD.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"use client";

import { clsxm } from "@repo/ui/clsxm";

interface NavigationHUDProps {
distanceMeters: number;
durationSeconds: number;
isLoading?: boolean;
error?: string | null;
onCancel: () => void;
}

function formatDistance(meters: number): string {
if (meters < 1000) {
return `${Math.round(meters)} m`;
}
const km = meters / 1000;
return `${km.toFixed(1)} km`;
}

function formatDuration(seconds: number): string {
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}min`;
}

export function NavigationHUD({
distanceMeters,
durationSeconds,
isLoading,
error,
onCancel,
}: NavigationHUDProps) {
return (
<div className="absolute top-20 left-1/2 -translate-x-1/2 z-[20] pointer-events-auto">
<div
className={clsxm(
"flex flex-col items-center gap-2 px-6 py-4 rounded-2xl",
"bg-[var(--bg-panel)] backdrop-blur-[20px] border border-[var(--border-color)]",
"shadow-[0_8px_32px_var(--shadow-color)]",
"transition-all duration-300 ease-out",
)}
>
{isLoading ? (
<div className="flex items-center gap-3">
<div className="w-5 h-5 border-2 border-neon-blue border-t-transparent rounded-full animate-spin" />
<span className="font-chakra text-[14px] text-primary">
Calculating route...
</span>
</div>
) : error ? (
<div className="flex flex-col items-center gap-2">
<span className="font-nunito text-[13px] text-red-500">
{error}
</span>
<button
type="button"
onClick={onCancel}
className="px-4 py-1.5 rounded-lg bg-transparent border border-border-color text-secondary font-chakra text-[12px] font-bold cursor-pointer transition-colors hover:bg-panel-hover"
>
DISMISS
</button>
</div>
) : (
<>
<div className="flex items-center gap-6">
{/* Distance */}
<div className="flex flex-col items-center">
<span className="font-cubao-wide text-[10px] uppercase tracking-[0.15em] text-secondary">
Distance
</span>
<span className="font-chakra text-[22px] font-black text-neon-blue">
{formatDistance(distanceMeters)}
</span>
</div>

{/* Divider */}
<div className="w-[1px] h-10 bg-border-color" />

{/* Duration */}
<div className="flex flex-col items-center">
<span className="font-cubao-wide text-[10px] uppercase tracking-[0.15em] text-secondary">
Walking Time
</span>
<span className="font-chakra text-[22px] font-black text-neon-blue">
{formatDuration(durationSeconds)}
</span>
</div>
</div>

{/* Walking indicator */}
<div className="flex items-center gap-2 mt-1">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-neon-blue"
>
<circle cx="12" cy="5" r="2" />
<path d="m9 20 3-6 3 6" />
<path d="m6 8 6 2 6-2" />
<path d="M12 10v4" />
</svg>
<span className="font-cubao text-[11px] text-secondary">
Follow the blue route on map
</span>
</div>

{/* Cancel button */}
<button
type="button"
onClick={onCancel}
className="mt-2 px-4 py-1.5 rounded-lg bg-transparent border border-border-color text-secondary font-chakra text-[11px] font-bold cursor-pointer transition-colors hover:bg-panel-hover hover:text-primary"
>
END NAVIGATION
</button>
</>
)}
</div>
</div>
);
}
Loading
Loading