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