TypeScript · 7498 bytes Raw Blame History
1 'use client';
2
3 import { useEffect, useState } from 'react';
4 import L from 'leaflet';
5 import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
6 import 'leaflet/dist/leaflet.css';
7 import { Star, ThumbsUp, ThumbsDown, MessageSquare } from 'lucide-react';
8 import { Restaurant } from '@/lib/api';
9
10 // Fix for default markers in React-Leaflet
11 interface LeafletIconDefault extends L.Icon.Default {
12 _getIconUrl?: string;
13 }
14
15 if (typeof window !== 'undefined') {
16 const iconDefault = L.Icon.Default.prototype as LeafletIconDefault;
17 delete iconDefault._getIconUrl;
18 L.Icon.Default.mergeOptions({
19 iconRetinaUrl: '/leaflet/marker-icon-2x.png',
20 iconUrl: '/leaflet/marker-icon.png',
21 shadowUrl: '/leaflet/marker-shadow.png',
22 });
23 }
24
25 interface MapProps {
26 center: { lat: number; lng: number };
27 restaurants: Restaurant[];
28 onRestaurantClick: (restaurant: Restaurant) => void;
29 onToastStatusUpdate: (restaurantId: number, hasToast: boolean) => void;
30 }
31
32 // Component to recenter map when location changes
33 function RecenterMap({ center }: { center: { lat: number; lng: number } }) {
34 const map = useMap();
35 useEffect(() => {
36 map.setView([center.lat, center.lng], 13);
37 }, [center, map]);
38 return null;
39 }
40
41 export default function Map({ center, restaurants, onRestaurantClick, onToastStatusUpdate }: MapProps) {
42 const [mounted, setMounted] = useState(false);
43
44 useEffect(() => {
45 setMounted(true);
46 }, []);
47
48 if (!mounted) {
49 return <div className="h-full w-full bg-gray-100 animate-pulse" />;
50 }
51
52 // Create icons based on toast status
53 const createRestaurantIcon = (hasToast: boolean | null) => {
54 let iconContent = '';
55 let bgColor = '';
56
57 if (hasToast === null) {
58 // Unknown status - gray question mark
59 iconContent = '❓';
60 bgColor = '#9CA3AF'; // gray-400
61 } else if (hasToast) {
62 // Has toast - green toast
63 iconContent = '🍞';
64 bgColor = '#10B981'; // green-500
65 } else {
66 // No toast - red X
67 iconContent = '❌';
68 bgColor = '#EF4444'; // red-500
69 }
70
71 return new L.Icon({
72 iconUrl: 'data:image/svg+xml,' +
73 encodeURIComponent(`
74 <svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
75 <circle cx="20" cy="20" r="18" fill="${bgColor}" stroke="#374151" stroke-width="2"/>
76 <text x="20" y="27" text-anchor="middle" font-size="20">${iconContent}</text>
77 </svg>
78 `),
79 iconSize: [40, 40],
80 iconAnchor: [20, 40],
81 popupAnchor: [0, -40],
82 });
83 };
84
85 const userIcon = new L.Icon({
86 iconUrl: 'data:image/svg+xml,' +
87 encodeURIComponent(`
88 <svg width="30" height="30" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg">
89 <circle cx="15" cy="15" r="10" fill="#3B82F6" stroke="#1E40AF" stroke-width="2"/>
90 <circle cx="15" cy="15" r="4" fill="white"/>
91 </svg>
92 `),
93 iconSize: [30, 30],
94 iconAnchor: [15, 15],
95 });
96
97 return (
98 <div className="h-full w-full relative">
99 <MapContainer
100 center={[center.lat, center.lng]}
101 zoom={13}
102 className="h-full w-full"
103 style={{ height: '100%', width: '100%' }}
104 scrollWheelZoom={true}
105 >
106 <TileLayer
107 attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a>'
108 url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
109 />
110 <RecenterMap center={center} />
111
112 {/* User location marker */}
113 <Marker position={[center.lat, center.lng]} icon={userIcon}>
114 <Popup>
115 <div className="text-center">
116 <p className="font-semibold">You are here!</p>
117 <p className="text-sm text-gray-600">Ready to find some toast? 🍞</p>
118 </div>
119 </Popup>
120 </Marker>
121
122 {/* Restaurant markers */}
123 {restaurants.map((restaurant) => (
124 <Marker
125 key={restaurant.id}
126 position={[restaurant.latitude, restaurant.longitude]}
127 icon={createRestaurantIcon(restaurant.has_toast)}
128 >
129 <Popup>
130 <div className="p-2 min-w-[250px]">
131 <h3 className="font-bold text-base mb-1">{restaurant.name}</h3>
132 <p className="text-sm text-gray-700 mb-2">{restaurant.address}</p>
133
134 {/* Toast status */}
135 <div className="mb-3 p-2 bg-gray-50 rounded">
136 <p className="text-sm font-medium mb-2">
137 {restaurant.has_toast === null && "🤔 Does this place have toast?"}
138 {restaurant.has_toast === true && "✅ This place serves toast!"}
139 {restaurant.has_toast === false && "❌ No toast here"}
140 </p>
141
142 {/* Toast voting buttons */}
143 <div className="flex gap-2">
144 <button
145 onClick={(e) => {
146 e.stopPropagation();
147 onToastStatusUpdate(restaurant.id, true);
148 }}
149 className={`flex-1 flex items-center justify-center gap-1 px-3 py-1.5 rounded text-sm font-medium transition ${
150 restaurant.has_toast === true
151 ? 'bg-green-500 text-white'
152 : 'bg-gray-200 hover:bg-green-100 text-gray-700'
153 }`}
154 >
155 <ThumbsUp className="w-4 h-4" />
156 <span>Has Toast</span>
157 </button>
158 <button
159 onClick={(e) => {
160 e.stopPropagation();
161 onToastStatusUpdate(restaurant.id, false);
162 }}
163 className={`flex-1 flex items-center justify-center gap-1 px-3 py-1.5 rounded text-sm font-medium transition ${
164 restaurant.has_toast === false
165 ? 'bg-red-500 text-white'
166 : 'bg-gray-200 hover:bg-red-100 text-gray-700'
167 }`}
168 >
169 <ThumbsDown className="w-4 h-4" />
170 <span>No Toast</span>
171 </button>
172 </div>
173 </div>
174
175 {/* Rating info */}
176 {restaurant.average_rating ? (
177 <p className="text-sm font-semibold flex items-center mb-2">
178 <Star className="w-4 h-4 text-yellow-500 fill-current mr-1" />
179 {restaurant.average_rating.toFixed(1)} ({restaurant.total_ratings} ratings)
180 </p>
181 ) : (
182 <p className="text-sm text-gray-600 mb-2">No ratings yet</p>
183 )}
184
185 {/* Review button */}
186 <button
187 onClick={(e) => {
188 e.stopPropagation();
189 onRestaurantClick(restaurant);
190 }}
191 className="w-full bg-amber-600 hover:bg-amber-700 text-white px-3 py-2 rounded text-sm font-medium transition flex items-center justify-center gap-2"
192 >
193 <MessageSquare className="w-4 h-4" />
194 <span>Write Toast Review</span>
195 </button>
196 </div>
197 </Popup>
198 </Marker>
199 ))}
200 </MapContainer>
201 </div>
202 );
203 }