TypeScript · 8369 bytes Raw Blame History
1 'use client';
2
3 import { useState, useEffect } from 'react';
4 import dynamic from 'next/dynamic';
5 import { MapPin, Plus, Loader2 } from 'lucide-react';
6 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
7 import { Providers } from './providers';
8 import { Loading } from '@/components/Loading';
9 import ReviewModal from '@/components/ReviewModal';
10 import SearchPanel from '@/components/SearchPanel';
11 import { restaurantApi, Restaurant, CreateRatingData, PlaceSearchResult } from '@/lib/api';
12
13 // Dynamic import for Map to avoid SSR issues
14 const Map = dynamic(() => import('@/components/Map'), {
15 ssr: false,
16 loading: () => <div className="h-full w-full bg-gray-100 animate-pulse" />,
17 });
18
19 function HomePage() {
20 const [userLocation, setUserLocation] = useState<{ lat: number; lng: number } | null>(null);
21 const [selectedRestaurant, setSelectedRestaurant] = useState<Restaurant | null>(null);
22 const [showSearchResults, setShowSearchResults] = useState(false);
23 const [searchResultsMinimized, setSearchResultsMinimized] = useState(false);
24 const [searchResults, setSearchResults] = useState<PlaceSearchResult[]>([]);
25 const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
26 const queryClient = useQueryClient();
27
28 // Get user's location
29 useEffect(() => {
30 if (navigator.geolocation) {
31 navigator.geolocation.getCurrentPosition(
32 (position) => {
33 setUserLocation({
34 lat: position.coords.latitude,
35 lng: position.coords.longitude
36 });
37 },
38 (error) => {
39 console.error('Error getting location:', error);
40 // Default to NYC
41 setUserLocation({ lat: 40.7128, lng: -74.0060 });
42 }
43 );
44 } else {
45 setUserLocation({ lat: 40.7128, lng: -74.0060 });
46 }
47 }, []);
48
49 // Add rating mutation
50 const addRatingMutation = useMutation({
51 mutationFn: ({ restaurantId, data }: { restaurantId: number; data: CreateRatingData }) =>
52 restaurantApi.addRating(restaurantId, data),
53 onSuccess: async () => {
54 // Refresh restaurants list
55 if (userLocation) {
56 const updatedRestaurants = await restaurantApi.getNearby(userLocation.lat, userLocation.lng);
57 setRestaurants(updatedRestaurants);
58 }
59 alert('Toast rating added! 🍞');
60 },
61 onError: (error: any) => {
62 alert(error.response?.data?.error || 'Failed to add rating');
63 },
64 });
65
66 // Search places mutation
67 const searchMutation = useMutation({
68 mutationFn: async () => {
69 const searchPlaces = await restaurantApi.searchPlaces(userLocation!.lat, userLocation!.lng, 2000);
70
71 // All places are now auto-created as restaurants, so fetch the updated list
72 const updatedRestaurants = await restaurantApi.getNearby(userLocation!.lat, userLocation!.lng);
73 setRestaurants(updatedRestaurants);
74
75 return searchPlaces;
76 },
77 onSuccess: (data) => {
78 setSearchResults(data);
79 setShowSearchResults(true);
80 setSelectedRestaurant(null); // Close any open restaurant panel
81 },
82 onError: () => {
83 alert('Failed to search for places');
84 },
85 });
86
87 // Update toast status mutation
88 const updateToastStatusMutation = useMutation({
89 mutationFn: ({ restaurantId, hasToast }: { restaurantId: number; hasToast: boolean }) =>
90 restaurantApi.updateToastStatus(restaurantId, hasToast),
91 onSuccess: async () => {
92 // Refresh restaurants
93 if (userLocation) {
94 const updatedRestaurants = await restaurantApi.getNearby(userLocation.lat, userLocation.lng);
95 setRestaurants(updatedRestaurants);
96 }
97 },
98 onError: () => {
99 alert('Failed to update toast status');
100 },
101 });
102
103 const handleAddRating = async (data: CreateRatingData) => {
104 if (!selectedRestaurant) return;
105 await addRatingMutation.mutateAsync({ restaurantId: selectedRestaurant.id, data });
106 };
107
108 if (!userLocation) {
109 return <Loading />;
110 }
111
112 return (
113 <div className="h-screen flex flex-col">
114 {/* Header */}
115 <header className="bg-amber-600 text-white p-4 shadow-lg relative z-10">
116 <div className="max-w-7xl mx-auto flex items-center justify-between">
117 <div className="flex items-center space-x-2">
118 <span className="text-2xl">🍞</span>
119 <h1 className="text-2xl font-bold">LocalToast</h1>
120 </div>
121 <div className="flex items-center space-x-4">
122 <p className="text-sm hidden sm:block">Find and rate the best toast in town!</p>
123 <button
124 onClick={() => searchMutation.mutate()}
125 disabled={searchMutation.isPending}
126 className="bg-amber-700 hover:bg-amber-800 px-4 py-2 rounded-lg text-sm font-medium transition disabled:opacity-50 flex items-center space-x-2"
127 >
128 {searchMutation.isPending ? (
129 <Loader2 className="w-4 h-4 animate-spin" />
130 ) : (
131 <Plus className="w-4 h-4" />
132 )}
133 <span>Find Toast</span>
134 </button>
135 </div>
136 </div>
137 </header>
138
139 {/* Main Content */}
140 <div className="flex-1 relative">
141 <Map
142 center={userLocation}
143 restaurants={restaurants}
144 onRestaurantClick={setSelectedRestaurant}
145 onToastStatusUpdate={(restaurantId, hasToast) =>
146 updateToastStatusMutation.mutate({ restaurantId, hasToast })
147 }
148 />
149
150 {/* Loading indicator */}
151 {searchMutation.isPending && (
152 <div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white rounded-lg shadow-md px-4 py-2 flex items-center space-x-2">
153 <Loader2 className="w-4 h-4 animate-spin text-amber-600" />
154 <span>Searching for toast spots...</span>
155 </div>
156 )}
157
158 {/* Restaurant count - moved to top right */}
159 {restaurants.length > 0 && !searchMutation.isPending && (
160 <div className="absolute top-4 right-4 bg-white rounded-lg shadow-md px-4 py-2">
161 <p className="text-sm font-medium">
162 {restaurants.length} toast spot{restaurants.length !== 1 ? 's' : ''} nearby 🍞
163 </p>
164 </div>
165 )}
166
167 {/* Review Modal */}
168 {selectedRestaurant && (
169 <ReviewModal
170 restaurant={selectedRestaurant}
171 onClose={() => setSelectedRestaurant(null)}
172 onSubmit={handleAddRating}
173 />
174 )}
175
176 {/* Search results panel */}
177 {showSearchResults && (
178 <>
179 {searchResultsMinimized ? (
180 // Minimized state - just a bar at the bottom
181 <div className="absolute bottom-0 left-0 right-0 bg-white shadow-xl rounded-t-xl p-3 z-[1000] cursor-pointer"
182 onClick={() => setSearchResultsMinimized(false)}>
183 <div className="flex justify-between items-center">
184 <p className="text-sm font-medium">
185 {searchResults.length} search results (click to expand)
186 </p>
187 <button className="text-gray-500 hover:text-gray-700">
188 <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
189 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
190 </svg>
191 </button>
192 </div>
193 </div>
194 ) : (
195 // Full search panel
196 <SearchPanel
197 searchResults={searchResults}
198 onClose={() => setSearchResultsMinimized(true)}
199 onToastStatusUpdate={(restaurantId, hasToast) =>
200 updateToastStatusMutation.mutate({ restaurantId, hasToast })
201 }
202 onRestaurantClick={(restaurantId) => {
203 const restaurant = restaurants.find(r => r.id === restaurantId);
204 if (restaurant) {
205 setSelectedRestaurant(restaurant);
206 // Don't minimize - let user continue browsing
207 }
208 }}
209 />
210 )}
211 </>
212 )}
213 </div>
214 </div>
215 );
216 }
217
218 export default function Page() {
219 return (
220 <Providers>
221 <HomePage />
222 </Providers>
223 );
224 }