Comparing changes

Choose two branches to see what's changed or to start a new pull request.

base: django-switch
Choose a base ref
django-switch trunk default
compare: trunk
Choose a head ref
django-switch trunk default
Create pull request
Able to merge. These branches can be automatically merged.
14 commits 12 files changed 2 contributors

Commits on trunk

backend/api/urls.pymodified
@@ -5,6 +5,9 @@ urlpatterns = [
55
     # Health check
66
     path('health/', views.health_check, name='health_check'),
77
     
8
+    # CORS test endpoint
9
+    path('cors-test/', views.cors_test, name='cors_test'),
10
+    
811
     # Restaurant endpoints
912
     path('restaurants/', views.RestaurantListCreateView.as_view(), name='restaurant_list_create'),
1013
     path('restaurants/nearby/', views.NearbyRestaurantsView.as_view(), name='restaurants_nearby'),
backend/api/views.pymodified
@@ -1,4 +1,4 @@
1
-from django.http import JsonResponse
1
+from django.http import JsonResponse, HttpResponse
22
 from django.utils import timezone
33
 from django.conf import settings
44
 from django.db.models import Q
@@ -39,6 +39,15 @@ def calculate_distance(lat1, lon1, lat2, lon2):
3939
 class NearbyRestaurantsView(APIView):
4040
     """Get restaurants near a location"""
4141
     
42
+    def options(self, request, *args, **kwargs):
43
+        """Handle preflight requests"""
44
+        response = HttpResponse()
45
+        response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
46
+        response['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
47
+        response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
48
+        response['Access-Control-Max-Age'] = '3600'
49
+        return response
50
+    
4251
     def get(self, request):
4352
         lat = request.query_params.get('lat')
4453
         lng = request.query_params.get('lng')
@@ -93,6 +102,15 @@ class RestaurantListCreateView(generics.ListCreateAPIView):
93102
     queryset = Restaurant.objects.all()
94103
     serializer_class = RestaurantSerializer
95104
     
105
+    def options(self, request, *args, **kwargs):
106
+        """Handle preflight requests"""
107
+        response = HttpResponse()
108
+        response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
109
+        response['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
110
+        response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
111
+        response['Access-Control-Max-Age'] = '3600'
112
+        return response
113
+    
96114
     def create(self, request, *args, **kwargs):
97115
         # Check if restaurant already exists
98116
         place_id = request.data.get('place_id')
@@ -109,11 +127,29 @@ class RestaurantDetailView(generics.RetrieveAPIView):
109127
     """Get detailed info about a restaurant"""
110128
     queryset = Restaurant.objects.all()
111129
     serializer_class = RestaurantDetailSerializer
130
+    
131
+    def options(self, request, *args, **kwargs):
132
+        """Handle preflight requests"""
133
+        response = HttpResponse()
134
+        response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
135
+        response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
136
+        response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
137
+        response['Access-Control-Max-Age'] = '3600'
138
+        return response
112139
 
113140
 
114141
 class RestaurantRatingView(APIView):
115142
     """Add a rating to a restaurant or get all ratings"""
116143
     
144
+    def options(self, request, *args, **kwargs):
145
+        """Handle preflight requests"""
146
+        response = HttpResponse()
147
+        response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
148
+        response['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
149
+        response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
150
+        response['Access-Control-Max-Age'] = '3600'
151
+        return response
152
+    
117153
     def get(self, request, pk):
118154
         try:
119155
             restaurant = Restaurant.objects.get(pk=pk)
@@ -152,6 +188,15 @@ class RestaurantRatingView(APIView):
152188
 class RestaurantToastStatusView(APIView):
153189
     """Update restaurant's toast status"""
154190
     
191
+    def options(self, request, *args, **kwargs):
192
+        """Handle preflight requests"""
193
+        response = HttpResponse()
194
+        response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
195
+        response['Access-Control-Allow-Methods'] = 'PATCH, OPTIONS'
196
+        response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
197
+        response['Access-Control-Max-Age'] = '3600'
198
+        return response
199
+    
155200
     def patch(self, request, pk):
156201
         try:
157202
             restaurant = Restaurant.objects.get(pk=pk)
@@ -181,6 +226,15 @@ class RestaurantToastStatusView(APIView):
181226
 class SearchPlacesView(APIView):
182227
     """Search for places that might serve toast"""
183228
     
229
+    def options(self, request, *args, **kwargs):
230
+        """Handle preflight requests"""
231
+        response = HttpResponse()
232
+        response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
233
+        response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
234
+        response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
235
+        response['Access-Control-Max-Age'] = '3600'
236
+        return response
237
+    
184238
     def get(self, request):
185239
         lat = request.query_params.get('lat')
186240
         lng = request.query_params.get('lng')
@@ -353,6 +407,23 @@ class SearchPlacesView(APIView):
353407
         ]
354408
 
355409
 
410
+@api_view(['GET', 'OPTIONS'])
411
+def cors_test(request):
412
+    """Test endpoint for CORS"""
413
+    if request.method == 'OPTIONS':
414
+        response = HttpResponse()
415
+        response['Access-Control-Allow-Origin'] = '*'
416
+        response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
417
+        response['Access-Control-Allow-Headers'] = '*'
418
+        return response
419
+    
420
+    return Response({
421
+        'status': 'CORS test successful',
422
+        'origin': request.headers.get('Origin', 'No origin header'),
423
+        'method': request.method
424
+    })
425
+
426
+
356427
 @api_view(['POST'])
357428
 def seed_data(request):
358429
     """Seed the database with sample restaurants"""
backend/localtoast/settings.pymodified
@@ -31,7 +31,7 @@ INSTALLED_APPS = [
3131
 
3232
 MIDDLEWARE = [
3333
     'django.middleware.security.SecurityMiddleware',
34
-    'corsheaders.middleware.CorsMiddleware',
34
+    'corsheaders.middleware.CorsMiddleware',  # Must be before CommonMiddleware
3535
     'django.middleware.common.CommonMiddleware',
3636
     'django.middleware.csrf.CsrfViewMiddleware',
3737
     'django.contrib.sessions.middleware.SessionMiddleware',
@@ -106,16 +106,45 @@ REST_FRAMEWORK = {
106106
     ],
107107
 }
108108
 
109
-# CORS settings
109
+# CORS settings - Updated for better compatibility
110110
 CORS_ALLOWED_ORIGINS = [
111111
     "http://localhost:3000",
112112
     "http://localhost:3001",
113
-    "https://localtoast.vercel.app", # change me matt
113
+    "https://localtoast-frontend.vercel.app",
114
+    "https://www.localtoast-frontend.vercel.app",
115
+    "https://www.localtoast.fyi",
116
+    "https://localtoast.fyi",
114117
 ]
115118
 
116
-# Allow CORS during development
117
-if DEBUG:
118
-    CORS_ALLOW_ALL_ORIGINS = True
119
+# Add frontend URL from environment if provided
120
+if os.getenv('FRONTEND_URL'):
121
+    CORS_ALLOWED_ORIGINS.append(os.getenv('FRONTEND_URL'))
122
+
123
+# Explicit CORS configuration
124
+CORS_ALLOW_ALL_ORIGINS = False  # Never True in production
125
+CORS_ALLOW_CREDENTIALS = True
126
+CORS_PREFLIGHT_MAX_AGE = 86400
127
+
128
+CORS_ALLOW_METHODS = [
129
+    'DELETE',
130
+    'GET',
131
+    'OPTIONS',
132
+    'PATCH',
133
+    'POST',
134
+    'PUT',
135
+]
136
+
137
+CORS_ALLOW_HEADERS = [
138
+    'accept',
139
+    'accept-encoding',
140
+    'authorization',
141
+    'content-type',
142
+    'dnt',
143
+    'origin',
144
+    'user-agent',
145
+    'x-csrftoken',
146
+    'x-requested-with',
147
+]
119148
 
120149
 # Media files
121150
 MEDIA_URL = '/media/'
backend/railway.jsonmodified
@@ -2,10 +2,10 @@
22
   "$schema": "https://railway.app/railway.schema.json",
33
   "build": {
44
     "builder": "NIXPACKS",
5
-    "buildCommand": "python manage.py collectstatic --noinput"
5
+    "buildCommand": "python manage.py collectstatic --noinput && python manage.py migrate"
66
   },
77
   "deploy": {
8
-    "startCommand": "python manage.py migrate && gunicorn localtoast.wsgi",
8
+    "startCommand": "gunicorn localtoast.wsgi:application --bind 0.0.0.0:$PORT",
99
     "restartPolicyType": "ON_FAILURE",
1010
     "restartPolicyMaxRetries": 10
1111
   }
frontend/app/page.tsxmodified
@@ -2,8 +2,8 @@
22
 
33
 import { useState, useEffect } from 'react';
44
 import dynamic from 'next/dynamic';
5
-import { MapPin, Plus, Loader2 } from 'lucide-react';
6
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
5
+import { Plus, Loader2 } from 'lucide-react';
6
+import { useMutation } from '@tanstack/react-query';
77
 import { Providers } from './providers';
88
 import { Loading } from '@/components/Loading';
99
 import ReviewModal from '@/components/ReviewModal';
@@ -16,6 +16,15 @@ const Map = dynamic(() => import('@/components/Map'), {
1616
   loading: () => <div className="h-full w-full bg-gray-100 animate-pulse" />,
1717
 });
1818
 
19
+// Type for API errors
20
+type ApiError = {
21
+  response?: {
22
+    data?: {
23
+      error?: string;
24
+    };
25
+  };
26
+};
27
+
1928
 function HomePage() {
2029
   const [userLocation, setUserLocation] = useState<{ lat: number; lng: number } | null>(null);
2130
   const [selectedRestaurant, setSelectedRestaurant] = useState<Restaurant | null>(null);
@@ -23,7 +32,6 @@ function HomePage() {
2332
   const [searchResultsMinimized, setSearchResultsMinimized] = useState(false);
2433
   const [searchResults, setSearchResults] = useState<PlaceSearchResult[]>([]);
2534
   const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
26
-  const queryClient = useQueryClient();
2735
 
2836
   // Get user's location
2937
   useEffect(() => {
@@ -58,8 +66,9 @@ function HomePage() {
5866
       }
5967
       alert('Toast rating added! 🍞');
6068
     },
61
-    onError: (error: any) => {
62
-      alert(error.response?.data?.error || 'Failed to add rating');
69
+    onError: (error: unknown) => {
70
+      const apiError = error as ApiError;
71
+      alert(apiError.response?.data?.error || 'Failed to add rating');
6372
     },
6473
   });
6574
 
@@ -79,8 +88,9 @@ function HomePage() {
7988
       setShowSearchResults(true);
8089
       setSelectedRestaurant(null); // Close any open restaurant panel
8190
     },
82
-    onError: () => {
83
-      alert('Failed to search for places');
91
+    onError: (error: unknown) => {
92
+      const apiError = error as ApiError;
93
+      alert(apiError.response?.data?.error || 'Failed to search for places');
8494
     },
8595
   });
8696
 
@@ -95,8 +105,9 @@ function HomePage() {
95105
         setRestaurants(updatedRestaurants);
96106
       }
97107
     },
98
-    onError: () => {
99
-      alert('Failed to update toast status');
108
+    onError: (error: unknown) => {
109
+      const apiError = error as ApiError;
110
+      alert(apiError.response?.data?.error || 'Failed to update toast status');
100111
     },
101112
   });
102113
 
frontend/components/Map.tsxmodified
@@ -8,8 +8,13 @@ import { Star, ThumbsUp, ThumbsDown, MessageSquare } from 'lucide-react';
88
 import { Restaurant } from '@/lib/api';
99
 
1010
 // Fix for default markers in React-Leaflet
11
+interface LeafletIconDefault extends L.Icon.Default {
12
+  _getIconUrl?: string;
13
+}
14
+
1115
 if (typeof window !== 'undefined') {
12
-  delete (L.Icon.Default.prototype as any)._getIconUrl;
16
+  const iconDefault = L.Icon.Default.prototype as LeafletIconDefault;
17
+  delete iconDefault._getIconUrl;
1318
   L.Icon.Default.mergeOptions({
1419
     iconRetinaUrl: '/leaflet/marker-icon-2x.png',
1520
     iconUrl: '/leaflet/marker-icon.png',
frontend/components/RestaurantPanel.tsxmodified
@@ -26,8 +26,6 @@ export default function RestaurantPanel({ restaurant, onClose, onAddRating }: Re
2626
       setShowRatingForm(false);
2727
       setRating(5);
2828
       setReview('');
29
-    } catch (error) {
30
-      // Error handling done in parent
3129
     } finally {
3230
       setIsSubmitting(false);
3331
     }
frontend/components/ReviewModal.tsxmodified
@@ -23,7 +23,7 @@ export default function ReviewModal({ restaurant, onClose, onSubmit }: ReviewMod
2323
     try {
2424
       await onSubmit({ rating, review });
2525
       onClose();
26
-    } catch (error) {
26
+    } catch {
2727
       // Error handling done in parent
2828
     } finally {
2929
       setIsSubmitting(false);
frontend/components/SearchPanel.tsxmodified
@@ -1,7 +1,7 @@
11
 'use client';
22
 
33
 import { useState } from 'react';
4
-import { X, ThumbsUp, ThumbsDown, MessageSquare, MapPin, Star } from 'lucide-react';
4
+import { ThumbsUp, ThumbsDown, MessageSquare, MapPin } from 'lucide-react';
55
 import { PlaceSearchResult } from '@/lib/api';
66
 
77
 interface SearchPanelProps {
frontend/lib/api.tsmodified
@@ -90,12 +90,23 @@ export const restaurantApi = {
9090
     return response.data;
9191
   },
9292
 
93
-  // Search places
93
+  // Search places - FIXED to handle 404 with data
9494
   searchPlaces: async (lat: number, lng: number, radius: number = 1000) => {
95
-    const response = await api.get<PlaceSearchResult[]>('/search/places/', {
96
-      params: { lat, lng, radius }
97
-    });
98
-    return response.data;
95
+    try {
96
+      const response = await api.get<PlaceSearchResult[]>('/search/places/', {
97
+        params: { lat, lng, radius }
98
+      });
99
+      return response.data;
100
+    } catch (error) {
101
+      // Handle the bizarre 404-with-data issue from Railway
102
+      if (axios.isAxiosError(error)) {
103
+        if (error.response?.status === 404 && Array.isArray(error.response?.data)) {
104
+          console.warn('Got 404 with valid data - returning data anyway');
105
+          return error.response.data as PlaceSearchResult[];
106
+        }
107
+      }
108
+      throw error;
109
+    }
99110
   },
100111
 
101112
   // Seed data
frontend/next.config.tsmodified
@@ -1,6 +1,8 @@
11
 import type { NextConfig } from "next";
22
 
33
 const nextConfig: NextConfig = {
4
+
5
+  output: 'standalone',
46
   // Disable SSR for Leaflet components
57
   transpilePackages: ['leaflet'],
68
   
frontend/vercel.jsonadded
@@ -0,0 +1,4 @@
1
+{
2
+  "framework": "nextjs",
3
+  "outputDirectory": ".next"
4
+}