JavaScript · 12537 bytes Raw Blame History
1 // Load environment variables at the very top
2 require('dotenv').config();
3
4 const express = require('express');
5 const cors = require('cors');
6 const path = require('path');
7 const axios = require('axios');
8
9 // Import our new database module
10 const {
11 initializeDatabase,
12 getAllRestaurants,
13 getNearbyRestaurants,
14 getRestaurantByPlaceId,
15 addRestaurant,
16 addRating,
17 getRestaurantRatings,
18 migrateFromJSON
19 } = require('./database');
20
21 const app = express();
22 const PORT = process.env.PORT || 3000;
23
24 // Middleware
25 app.use(cors());
26 app.use(express.json());
27
28 // Test Google Places API
29 app.get('/api/test-google', async (req, res) => {
30 if (!process.env.GOOGLE_PLACES_API_KEY) {
31 return res.json({ error: 'Google Places API key not configured' });
32 }
33
34 try {
35 const response = await axios.get('https://maps.googleapis.com/maps/api/place/textsearch/json', {
36 params: {
37 query: 'french toast restaurant',
38 key: process.env.GOOGLE_PLACES_API_KEY
39 }
40 });
41
42 res.json({
43 status: 'success',
44 found: response.data.results.length,
45 sample: response.data.results[0]?.name || 'No results'
46 });
47 } catch (error) {
48 res.json({
49 status: 'error',
50 message: error.response?.data?.error_message || error.message
51 });
52 }
53 });
54
55 // Routes
56 app.get('/api/health', (req, res) => {
57 res.json({ status: 'LocalToast is cooking! 🍞' });
58 });
59
60 // Get nearby restaurants
61 app.get('/api/restaurants/nearby', async (req, res) => {
62 try {
63 const { lat, lng, radius = 5 } = req.query;
64
65 if (!lat || !lng) {
66 return res.status(400).json({ error: 'Latitude and longitude are required' });
67 }
68
69 const restaurants = await getNearbyRestaurants(
70 parseFloat(lat),
71 parseFloat(lng),
72 parseFloat(radius)
73 );
74
75 // If we have no restaurants, try to fetch some from OpenStreetMap
76 if (restaurants.length === 0) {
77 console.log('No restaurants in database, fetching from OpenStreetMap...');
78
79 const overpassQuery = `
80 [out:json][timeout:25];
81 (
82 node["amenity"="cafe"](around:2000,${lat},${lng});
83 node["shop"="bakery"](around:2000,${lat},${lng});
84 node["amenity"="restaurant"]["cuisine"~"breakfast"](around:2000,${lat},${lng});
85 );
86 out body;
87 `;
88
89 try {
90 const response = await axios.post(
91 'https://overpass-api.de/api/interpreter',
92 `data=${encodeURIComponent(overpassQuery)}`,
93 { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
94 );
95
96 const places = response.data.elements
97 .filter(place => place.tags && place.tags.name)
98 .slice(0, 5); // Just add first 5 automatically
99
100 for (const place of places) {
101 try {
102 const newRestaurant = await addRestaurant({
103 place_id: `osm_${place.id}`,
104 name: place.tags.name,
105 address: [
106 place.tags['addr:street'],
107 place.tags['addr:city']
108 ].filter(Boolean).join(', ') || 'Address not available',
109 latitude: place.lat,
110 longitude: place.lon
111 });
112 restaurants.push(newRestaurant);
113 } catch (err) {
114 console.error('Error adding restaurant from OSM:', err);
115 }
116 }
117 } catch (apiError) {
118 console.error('Failed to fetch from OpenStreetMap:', apiError.message);
119 }
120 }
121
122 res.json(restaurants);
123 } catch (error) {
124 console.error('Error fetching restaurants:', error);
125 res.status(500).json({ error: 'Failed to fetch restaurants' });
126 }
127 });
128
129 // Add a new restaurant
130 app.post('/api/restaurants', async (req, res) => {
131 try {
132 const { place_id, name, address, latitude, longitude } = req.body;
133
134 // Check if restaurant already exists
135 const existing = await getRestaurantByPlaceId(place_id);
136 if (existing) {
137 return res.json(existing);
138 }
139
140 const newRestaurant = await addRestaurant({
141 place_id,
142 name,
143 address,
144 latitude,
145 longitude
146 });
147
148 res.json(newRestaurant);
149 } catch (error) {
150 console.error('Error adding restaurant:', error);
151 res.status(500).json({ error: 'Failed to add restaurant' });
152 }
153 });
154
155 // Add a toast rating
156 app.post('/api/restaurants/:id/ratings', async (req, res) => {
157 try {
158 const { id } = req.params;
159 const { rating, review } = req.body;
160
161 // Simple toast detection
162 if (review && review.length > 0) {
163 const toastKeywords = ['toast', 'bread', 'butter', 'jam', 'marmalade', 'french toast', 'avocado'];
164 const reviewLower = review.toLowerCase();
165 const mentionsToast = toastKeywords.some(keyword => reviewLower.includes(keyword));
166
167 if (!mentionsToast) {
168 return res.status(400).json({
169 error: 'Reviews must be about toast! Please mention the toast in your review.'
170 });
171 }
172 }
173
174 const result = await addRating(parseInt(id), rating, review);
175 res.json(result);
176 } catch (error) {
177 console.error('Error adding rating:', error);
178 res.status(500).json({ error: 'Failed to add rating' });
179 }
180 });
181
182 // Get ratings for a restaurant
183 app.get('/api/restaurants/:id/ratings', async (req, res) => {
184 try {
185 const { id } = req.params;
186 const ratings = await getRestaurantRatings(parseInt(id));
187 res.json(ratings);
188 } catch (error) {
189 console.error('Error fetching ratings:', error);
190 res.status(500).json({ error: 'Failed to fetch ratings' });
191 }
192 });
193
194 // Search for real restaurants using multiple APIs
195 app.get('/api/search/places', async (req, res) => {
196 try {
197 const { lat, lng, radius = 1000 } = req.query;
198
199 console.log('Searching for places at:', { lat, lng, radius });
200
201 // Try Google Places API first if available
202 if (process.env.GOOGLE_PLACES_API_KEY) {
203 console.log('Using Google Places API...');
204 const textSearchUrl = 'https://maps.googleapis.com/maps/api/place/textsearch/json';
205
206 try {
207 const response = await axios.get(textSearchUrl, {
208 params: {
209 query: 'french toast OR avocado toast restaurant',
210 location: `${lat},${lng}`,
211 radius: radius,
212 type: 'restaurant|cafe|bakery',
213 key: process.env.GOOGLE_PLACES_API_KEY
214 }
215 });
216
217 console.log('Google Places response:', response.data.status);
218
219 if (response.data.results && response.data.results.length > 0) {
220 const places = response.data.results.map(place => ({
221 place_id: place.place_id,
222 name: place.name,
223 address: place.formatted_address || place.vicinity,
224 latitude: place.geometry.location.lat,
225 longitude: place.geometry.location.lng,
226 rating: place.rating || null,
227 price_level: place.price_level || null,
228 category: 'restaurant',
229 source: 'google_places',
230 confidence: place.name.toLowerCase().includes('toast') ? 'high' : 'medium'
231 }));
232
233 return res.json(places);
234 }
235 } catch (googleError) {
236 console.error('Google Places API error:', googleError.response?.data || googleError.message);
237 }
238 } else {
239 console.log('No Google API key, using OpenStreetMap...');
240 }
241
242 // Fallback to Overpass API (OpenStreetMap)
243 const overpassQuery = `
244 [out:json][timeout:25];
245 (
246 node["amenity"="restaurant"](around:${radius},${lat},${lng});
247 node["amenity"="cafe"](around:${radius},${lat},${lng});
248 node["shop"="bakery"](around:${radius},${lat},${lng});
249 node["amenity"="fast_food"]["cuisine"~"breakfast"](around:${radius},${lat},${lng});
250 );
251 out body;
252 `;
253
254 const overpassUrl = 'https://overpass-api.de/api/interpreter';
255
256 try {
257 console.log('Calling Overpass API...');
258 const response = await axios.post(overpassUrl, `data=${encodeURIComponent(overpassQuery)}`, {
259 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
260 timeout: 10000 // 10 second timeout
261 });
262
263 console.log('Overpass response elements:', response.data.elements?.length || 0);
264
265 const places = response.data.elements
266 .filter(place => place.tags && place.tags.name)
267 .map(place => ({
268 place_id: `osm_${place.id}`,
269 name: place.tags.name,
270 address: [
271 place.tags['addr:housenumber'],
272 place.tags['addr:street'],
273 place.tags['addr:city']
274 ].filter(Boolean).join(' ') || 'Address not available',
275 latitude: place.lat,
276 longitude: place.lon,
277 category: place.tags.amenity || place.tags.shop,
278 cuisine: place.tags.cuisine,
279 opening_hours: place.tags.opening_hours,
280 source: 'openstreetmap',
281 confidence: 'low'
282 }));
283
284 // Sort by likely to serve toast
285 const sortedPlaces = places.sort((a, b) => {
286 const toastLikely = ['cafe', 'bakery', 'breakfast'];
287 const aScore = toastLikely.some(cat =>
288 a.category?.includes(cat) || a.cuisine?.includes(cat) || a.name.toLowerCase().includes(cat)
289 ) ? 1 : 0;
290 const bScore = toastLikely.some(cat =>
291 b.category?.includes(cat) || b.cuisine?.includes(cat) || b.name.toLowerCase().includes(cat)
292 ) ? 1 : 0;
293 return bScore - aScore;
294 });
295
296 res.json(sortedPlaces.slice(0, 20));
297 } catch (apiError) {
298 console.error('Overpass API error:', apiError.message);
299 console.error('Error details:', apiError.response?.data || apiError.code);
300
301 // Return mock data as last resort
302 res.json([
303 {
304 place_id: 'mock_1',
305 name: 'The Breakfast Club',
306 address: '123 Toast Lane',
307 latitude: parseFloat(lat) + 0.01,
308 longitude: parseFloat(lng) + 0.01,
309 category: 'restaurant',
310 source: 'mock',
311 confidence: 'low'
312 }
313 ]);
314 }
315 } catch (error) {
316 console.error('Error searching places:', error);
317 res.status(500).json({ error: 'Failed to search places' });
318 }
319 });
320
321 // Seed data endpoint
322 app.post('/api/seed', async (req, res) => {
323 try {
324 const { lat, lng } = req.body;
325
326 const seedRestaurants = [
327 { place_id: 'seed_1', name: 'Toast & Jam Café', address: '100 Breakfast Blvd', latitude: parseFloat(lat) + 0.005, longitude: parseFloat(lng) + 0.005 },
328 { place_id: 'seed_2', name: 'The Golden Toast', address: '200 Butter Lane', latitude: parseFloat(lat) - 0.005, longitude: parseFloat(lng) + 0.005 },
329 { place_id: 'seed_3', name: 'Morning Glory Diner', address: '300 Sunrise Ave', latitude: parseFloat(lat) + 0.005, longitude: parseFloat(lng) - 0.005 },
330 { place_id: 'seed_4', name: 'Crispy Corner', address: '400 Crunch St', latitude: parseFloat(lat) - 0.005, longitude: parseFloat(lng) - 0.005 }
331 ];
332
333 let added = 0;
334
335 for (const seedRestaurant of seedRestaurants) {
336 try {
337 await addRestaurant(seedRestaurant);
338 added++;
339 } catch (err) {
340 if (err.message.includes('UNIQUE constraint failed')) {
341 console.log(`Seed restaurant already exists: ${seedRestaurant.name}`);
342 } else {
343 console.error(`Failed to add seed restaurant:`, err);
344 }
345 }
346 }
347
348 res.json({ message: `Seeded ${added} restaurants with toast! 🍞` });
349 } catch (error) {
350 console.error('Error seeding data:', error);
351 res.status(500).json({ error: 'Failed to seed data' });
352 }
353 });
354
355 // Migration endpoint (one-time use)
356 app.post('/api/migrate', async (req, res) => {
357 try {
358 await migrateFromJSON();
359 res.json({ message: 'Migration completed successfully!' });
360 } catch (error) {
361 console.error('Migration error:', error);
362 res.status(500).json({ error: 'Migration failed' });
363 }
364 });
365
366 // Initialize database and start server
367 initializeDatabase()
368 .then(() => {
369 app.listen(PORT, () => {
370 console.log(`LocalToast backend running on http://localhost:${PORT} 🍞`);
371 console.log('Database: SQLite');
372
373 // Offer to migrate on first run
374 console.log('\nIf you have existing JSON data, run:');
375 console.log(`curl -X POST http://localhost:${PORT}/api/migrate`);
376 });
377 })
378 .catch(err => {
379 console.error('Failed to initialize database:', err);
380 process.exit(1);
381 });