269 lines
15 KiB
TypeScript
269 lines
15 KiB
TypeScript
"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='© <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>
|
||
);
|
||
}
|