sky-and-soil/src/components/ConnectivityMap.tsx
2025-12-05 21:43:53 +05:30

269 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect, useMemo } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Fix for default markers in Next.js
const DefaultIcon = L.icon({
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41]
});
L.Marker.prototype.options.icon = DefaultIcon;
// Custom Icons
const createCustomIcon = (color: string, number?: string) => {
return L.divIcon({
className: 'custom-icon',
html: `<div style="
background-color: ${color};
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: white;
font-weight: bold;
font-size: 14px;
">${number || ''}</div>`,
iconSize: [36, 36],
iconAnchor: [18, 18],
});
};
const homeIcon = L.divIcon({
className: 'home-icon',
html: `<div style="
background-color: #7C3AED;
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4px solid white;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
color: white;
">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" style="width: 28px; height: 28px;">
<path d="M11.47 3.84a.75.75 0 011.06 0l8.69 8.69a.75.75 0 101.06-1.06l-8.689-8.69a2.25 2.25 0 00-3.182 0l-8.69 8.69a.75.75 0 001.061 1.06l8.69-8.69z" />
<path d="M12 5.432l8.159 8.159c.03.03.06.058.091.086v6.198c0 1.035-.84 1.875-1.875 1.875H15a.75.75 0 01-.75-.75v-4.5a.75.75 0 00-.75-.75h-3a.75.75 0 00-.75.75V21a.75.75 0 01-.75.75H5.625a1.875 1.875 0 01-1.875-1.875v-6.198a2.29 2.29 0 00.091-.086L12 5.43z" />
</svg>
</div>`,
iconSize: [48, 48],
iconAnchor: [24, 24],
});
// Mock Data
type LocationData = {
category: string;
items: {
id: number;
name: string;
dist: string;
time: string;
lat: number;
lng: number;
count: number;
}[];
};
const PROPERTY_LOCATION: [number, number] = [12.9385, 77.7297]; // Approximate Varthur
const MOCK_DATA: Record<string, LocationData['items']> = {
'Commute': [
{ id: 1, name: "Dommasandra Circle Metro Station", dist: "3.62 Km", time: "7 mins", lat: 12.9250, lng: 77.7450, count: 6 },
{ id: 2, name: "Sompura Metro Station", dist: "7.05 Km", time: "15 mins", lat: 12.9100, lng: 77.7600, count: 4 },
{ id: 3, name: "Sarjapur Metro Station", dist: "8.28 Km", time: "18 mins", lat: 12.8900, lng: 77.7800, count: 3 },
{ id: 4, name: "Ambedkar nagar Metro Station", dist: "8.89 Km", time: "18 mins", lat: 12.9550, lng: 77.7100, count: 5 },
],
'Education': [
{ id: 5, name: "Whitefield Global School", dist: "2.5 Km", time: "6 mins", lat: 12.9550, lng: 77.7350, count: 8 },
{ id: 6, name: "Greenwood High", dist: "4.1 Km", time: "10 mins", lat: 12.9150, lng: 77.7550, count: 5 },
],
'Hospitals': [
{ id: 7, name: "Manipal Hospital Varthur", dist: "1.2 Km", time: "4 mins", lat: 12.9420, lng: 77.7320, count: 2 },
],
'Work': [
{ id: 8, name: "RGA Tech Park", dist: "5.5 Km", time: "12 mins", lat: 12.9050, lng: 77.7150, count: 12 },
],
'Entertainment': [
{ id: 9, name: "Nexus Whitefield", dist: "3.2 Km", time: "9 mins", lat: 12.9600, lng: 77.7400, count: 7 },
],
};
function MapController({ center }: { center: [number, number] }) {
const map = useMap();
useEffect(() => {
map.setView(center, 13);
}, [center, map]);
return null;
}
function ZoomHandler({ zoomIn, zoomOut }: { zoomIn: () => void, zoomOut: () => void }) {
return (
<div className="absolute top-4 left-4 flex flex-col gap-2 z-[400]">
<button
onClick={zoomIn}
className="w-8 h-8 bg-white dark:bg-gray-800 rounded shadow-md flex items-center justify-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<span className="text-gray-700 dark:text-gray-300 font-bold text-lg">+</span>
</button>
<button
onClick={zoomOut}
className="w-8 h-8 bg-white dark:bg-gray-800 rounded shadow-md flex items-center justify-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
style={{ lineHeight: '0' }}
>
<span className="text-gray-700 dark:text-gray-300 font-bold text-lg"></span>
</button>
</div>
);
}
export default function ConnectivityMap() {
const [activeTab, setActiveTab] = useState("Commute");
const [searchQuery, setSearchQuery] = useState("");
const [mapZoom, setMapZoom] = useState(13);
const [mapRef, setMapRef] = useState<L.Map | null>(null);
const activeData = useMemo(() => {
let data = activeTab === "Search"
? Object.values(MOCK_DATA).flat()
: MOCK_DATA[activeTab] || [];
if (searchQuery) {
data = data.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
return data;
}, [activeTab, searchQuery]);
const handleZoomIn = () => {
if (mapRef) mapRef.zoomIn();
};
const handleZoomOut = () => {
if (mapRef) mapRef.zoomOut();
};
return (
<div className="bg-white dark:bg-gray-900 rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700">
{/* Tabs */}
<div className="flex overflow-x-auto border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 scrollbar-hide">
{["Commute", "Education", "Hospitals", "Work", "Entertainment", "Search"].map((tab) => (
<button
key={tab}
onClick={() => {
setActiveTab(tab);
setSearchQuery(""); // Clear search when switching tabs
}}
className={`px-6 py-3 text-sm font-medium whitespace-nowrap flex items-center gap-2 transition-colors border-b-2 ${activeTab === tab
? "text-orange-500 border-orange-500 bg-orange-50/30 dark:bg-orange-900/10"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border-transparent"
}`}
>
{/* Icons */}
{tab === "Commute" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path d="M6.5 3c-1.051 0-2.093.04-3.125.117A1.49 1.49 0 002 4.607V10.5h9V4.606c0-.771-.59-1.43-1.375-1.489A41.568 41.568 0 006.5 3zM2 12v2.5A1.5 1.5 0 003.5 16h.041a3 3 0 015.918 0h.791a.75.75 0 00.75-.75V12H2z" /><path d="M6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM13.25 5a.75.75 0 00-.75.75v8.514a3.001 3.001 0 014.893 1.44c.37-.275.61-.719.595-1.227a24.905 24.905 0 00-1.784-8.549A1.486 1.486 0 0014.823 5H13.25zM14.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" /></svg>}
{tab === "Education" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path d="M10.75 16.82A7.462 7.462 0 0115 15.5c.71 0 1.396.098 2.046.282A.75.75 0 0018 15.06v-11a.75.75 0 00-.546-.721A9.006 9.006 0 0015 3a8.963 8.963 0 00-4.25 1.065V16.82zM9.25 4.065A8.963 8.963 0 005 3c-.85 0-1.673.118-2.454.339A.75.75 0 002 4.06v11a.75.75 0 00.954.721A7.506 7.506 0 015 15.5c1.579 0 3.042.487 4.25 1.32V4.065z" /></svg>}
{tab === "Hospitals" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path fillRule="evenodd" d="M10 2a6 6 0 00-6 6c0 1.887-.454 3.665-1.257 5.234a.75.75 0 00.515 1.076 32.91 32.91 0 003.256.508 3.5 3.5 0 006.972 0 32.903 32.903 0 003.256-.508.75.75 0 00.515-1.076A11.448 11.448 0 0116 8a6 6 0 00-6-6zM8.05 14.943a33.54 33.54 0 003.9 0 2 2 0 01-3.9 0z" clipRule="evenodd" /></svg>}
{tab === "Work" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path fillRule="evenodd" d="M4 16.5v-13h-.25a.75.75 0 010-1.5h12.5a.75.75 0 010 1.5H16v13h.25a.75.75 0 010 1.5h-3.5a.75.75 0 01-.75-.75v-2.5a.75.75 0 00-.75-.75h-2.5a.75.75 0 00-.75.75v2.5a.75.75 0 01-.75.75h-3.5a.75.75 0 010-1.5H4zm3-11a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-1zM7.5 9a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1zM11 5.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-1zm.5 3.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1z" clipRule="evenodd" /></svg>}
{tab === "Entertainment" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path fillRule="evenodd" d="M6 5v1H4.667a1.75 1.75 0 00-1.743 1.598l-.826 9.5A1.75 1.75 0 003.84 19H16.16a1.75 1.75 0 001.743-1.902l-.826-9.5A1.75 1.75 0 0015.333 6H14V5a4 4 0 00-8 0zm4-2.5A2.5 2.5 0 007.5 5v1h5V5A2.5 2.5 0 0010 2.5zM7.5 10a2.5 2.5 0 005 0V8.75a.75.75 0 011.5 0V10a4 4 0 01-8 0V8.75a.75.75 0 011.5 0V10z" clipRule="evenodd" /></svg>}
{tab === "Search" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" /></svg>}
{tab}
</button>
))}
</div>
<div className="flex flex-col lg:flex-row h-[450px] relative">
{/* Map Area */}
<div className="flex-1 h-full relative">
<MapContainer
center={PROPERTY_LOCATION}
zoom={13}
style={{ height: '100%', width: '100%' }}
zoomControl={false}
ref={setMapRef}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapController center={PROPERTY_LOCATION} />
<ZoomHandler zoomIn={handleZoomIn} zoomOut={handleZoomOut} />
{/* Property Marker */}
<Marker position={PROPERTY_LOCATION} icon={homeIcon} />
{/* POI Markers */}
{activeData.map((item) => (
<Marker
key={item.id}
position={[item.lat, item.lng]}
icon={createCustomIcon('#F97316', item.count.toString())}
>
<Popup>{item.name}</Popup>
</Marker>
))}
</MapContainer>
{/* Layer Toggle Button (Bottom Left) */}
<div className="absolute bottom-4 left-4 z-[400]">
<button className="w-10 h-10 bg-white dark:bg-gray-800 rounded shadow-md flex items-center justify-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5 text-gray-600 dark:text-gray-400">
<path fillRule="evenodd" d="M3.25 3A2.25 2.25 0 001 5.25v9.5A2.25 2.25 0 003.25 17h9.5A2.25 2.25 0 0015 14.75v-9.5A2.25 2.25 0 0012.75 3h-9.5zm9.5 1.5a.75.75 0 00-.75.75V8a.75.75 0 001.5 0V5.25a.75.75 0 00-.75-.75zm0 5.5a.75.75 0 00-.75.75v2.75a.75.75 0 001.5 0v-2.75a.75.75 0 00-.75-.75zM10 7a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 7zm-3.25.75a.75.75 0 00-1.5 0v4.5a.75.75 0 001.5 0v-4.5z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
{/* Floating List Card (Right Side) */}
<div className="absolute top-4 right-4 bottom-4 w-full max-w-sm z-[400] flex flex-col pointer-events-none">
<div className="rounded-lg shadow-xl overflow-hidden flex flex-col h-full pointer-events-auto border border-gray-100 dark:border-gray-800">
{/* Search Input for Search Tab */}
{activeTab === "Search" && (
<div className="p-3 border-b border-gray-100 dark:border-gray-800">
<input
type="text"
placeholder="Search location..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
)}
<div className="flex-1 overflow-y-auto custom-scrollbar p-2">
{activeData.length > 0 ? (
activeData.map((item) => (
<div key={item.id} className="p-4 mb-2 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 rounded-lg cursor-pointer transition-all border border-gray-100 dark:border-gray-800 shadow-sm hover:shadow-md">
<h4 className="font-semibold text-gray-900 dark:text-white text-base mb-2">{item.name}</h4>
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span className="font-medium">{item.dist}</span>
<span className="text-orange-300">|</span>
<span className="text-gray-500 font-medium">{item.time}</span>
</div>
</div>
))
) : (
<div className="p-8 text-center text-gray-400">
No results found
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}