TypeScript · 7246 bytes Raw Blame History
1 'use client';
2
3 import { useState } from 'react';
4 import { X, ThumbsUp, ThumbsDown, MessageSquare, MapPin, Star } from 'lucide-react';
5 import { PlaceSearchResult } from '@/lib/api';
6
7 interface SearchPanelProps {
8 searchResults: PlaceSearchResult[];
9 onClose: () => void;
10 onToastStatusUpdate: (restaurantId: number, hasToast: boolean) => void;
11 onRestaurantClick: (restaurantId: number) => void;
12 }
13
14 export default function SearchPanel({
15 searchResults,
16 onClose,
17 onToastStatusUpdate,
18 onRestaurantClick
19 }: SearchPanelProps) {
20 const [updatingIds, setUpdatingIds] = useState<Set<number>>(new Set());
21
22 const handleToastStatus = async (restaurantId: number, hasToast: boolean) => {
23 setUpdatingIds(prev => new Set(prev).add(restaurantId));
24 try {
25 await onToastStatusUpdate(restaurantId, hasToast);
26 } finally {
27 setUpdatingIds(prev => {
28 const next = new Set(prev);
29 next.delete(restaurantId);
30 return next;
31 });
32 }
33 };
34
35 const getConfidenceColor = (confidence?: string) => {
36 switch (confidence) {
37 case 'high': return 'bg-green-100 text-green-800';
38 case 'medium': return 'bg-yellow-100 text-yellow-800';
39 default: return 'bg-gray-100 text-gray-800';
40 }
41 };
42
43 const getConfidenceText = (confidence?: string) => {
44 switch (confidence) {
45 case 'high': return '🍞 Likely has toast!';
46 case 'medium': return '🤔 Might have toast';
47 default: return '❓ Check menu';
48 }
49 };
50
51 return (
52 <div className="absolute bottom-0 left-0 right-0 bg-white p-6 shadow-xl rounded-t-xl max-h-[40vh] overflow-y-auto z-[1000] slide-up">
53 <div className="flex justify-between items-center mb-4">
54 <div>
55 <h2 className="text-xl font-bold text-gray-900">
56 Found {searchResults.length} Places That Might Serve Toast 🍞
57 </h2>
58 <p className="text-sm text-gray-600 mt-1">
59 Help us map the toast! Vote if these places serve toast.
60 </p>
61 </div>
62 <button
63 onClick={onClose}
64 className="flex items-center gap-1 text-gray-500 hover:text-gray-700 px-2 py-1 rounded hover:bg-gray-100 transition"
65 aria-label="Minimize search results"
66 >
67 <span className="text-sm">Minimize</span>
68 <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
69 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
70 </svg>
71 </button>
72 </div>
73
74 {searchResults.length === 0 ? (
75 <p className="text-center text-gray-600 py-8">
76 No places found nearby. Try moving to a different area.
77 </p>
78 ) : (
79 <div className="space-y-4">
80 {searchResults.map((place) => (
81 <div
82 key={place.place_id}
83 className="border rounded-lg p-4 hover:bg-gray-50 transition-colors"
84 >
85 <div className="flex justify-between items-start gap-4">
86 <div className="flex-1">
87 <h3 className="font-semibold text-gray-900">{place.name}</h3>
88 <p className="text-sm text-gray-600 mt-1 flex items-center">
89 <MapPin className="w-4 h-4 mr-1" />
90 {place.address}
91 </p>
92
93 <div className="flex flex-wrap items-center gap-2 mt-2">
94 {place.category && (
95 <span className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded">
96 {place.category}
97 </span>
98 )}
99 {place.distance && (
100 <span className="text-xs bg-amber-100 text-amber-800 px-2 py-1 rounded font-medium">
101 {place.distance < 1
102 ? `${Math.round(place.distance * 1000)}m away`
103 : `${place.distance.toFixed(1)}km away`
104 }
105 </span>
106 )}
107 {place.confidence && (
108 <span className={`text-xs px-2 py-1 rounded font-medium ${getConfidenceColor(place.confidence)}`}>
109 {getConfidenceText(place.confidence)}
110 </span>
111 )}
112 </div>
113 </div>
114 </div>
115
116 {/* Toast status section */}
117 <div className="mt-3 p-3 bg-gray-50 rounded">
118 <p className="text-sm font-medium mb-2">
119 {place.has_toast === null && "🤔 Does this place have toast?"}
120 {place.has_toast === true && "✅ This place serves toast!"}
121 {place.has_toast === false && "❌ No toast here"}
122 </p>
123
124 {/* Toast voting buttons */}
125 <div className="flex gap-2">
126 <button
127 onClick={() => place.restaurant_id && handleToastStatus(place.restaurant_id, true)}
128 disabled={!place.restaurant_id || updatingIds.has(place.restaurant_id!)}
129 className={`flex-1 flex items-center justify-center gap-1 px-3 py-1.5 rounded text-sm font-medium transition ${
130 place.has_toast === true
131 ? 'bg-green-500 text-white'
132 : 'bg-gray-200 hover:bg-green-100 text-gray-700'
133 } ${(!place.restaurant_id || updatingIds.has(place.restaurant_id!)) ? 'opacity-50 cursor-not-allowed' : ''}`}
134 >
135 <ThumbsUp className="w-4 h-4" />
136 <span>Has Toast</span>
137 </button>
138 <button
139 onClick={() => place.restaurant_id && handleToastStatus(place.restaurant_id, false)}
140 disabled={!place.restaurant_id || updatingIds.has(place.restaurant_id!)}
141 className={`flex-1 flex items-center justify-center gap-1 px-3 py-1.5 rounded text-sm font-medium transition ${
142 place.has_toast === false
143 ? 'bg-red-500 text-white'
144 : 'bg-gray-200 hover:bg-red-100 text-gray-700'
145 } ${(!place.restaurant_id || updatingIds.has(place.restaurant_id!)) ? 'opacity-50 cursor-not-allowed' : ''}`}
146 >
147 <ThumbsDown className="w-4 h-4" />
148 <span>No Toast</span>
149 </button>
150 </div>
151
152 {/* Review button */}
153 <button
154 onClick={() => place.restaurant_id && onRestaurantClick(place.restaurant_id)}
155 disabled={!place.restaurant_id}
156 className="w-full mt-2 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 disabled:opacity-50 disabled:cursor-not-allowed"
157 >
158 <MessageSquare className="w-4 h-4" />
159 <span>Write Toast Review</span>
160 </button>
161 </div>
162 </div>
163 ))}
164 </div>
165 )}
166 </div>
167 );
168 }