Untitled
unknown
plain_text
2 years ago
20 kB
4
Indexable
import SwiftUI import CoreLocation import Combine struct ContentView: View { @StateObject private var locationManager = LocationManager() @State private var restaurants: [String] = [] @State private var currentIndex: Int = 0 @State private var isMenuOpen: Bool = false @ObservedObject private var favoritesManager = FavoritesManager() @State private var showFavorites: Bool = false @GestureState private var dragState: DragState = .inactive @State private var swipedRightIndices: Set<Int> = [] @State private var swipedIndices: Set<Int> = [] @State private var searchRadius: Int = 5000 @State private var showNoMoreRestaurants: Bool = false @State private var cardOffset: CGSize = .zero var body: some View { ZStack { VStack { if !restaurants.isEmpty { GeometryReader { geometry in ZStack { if !swipedIndices.contains(currentIndex) { let components = restaurants[currentIndex].components(separatedBy: "|") let distance = Double(components[2]) ?? 0.0 let rating = Float(components[3]) ?? 0.0 let cardSwipedPublisher = PassthroughSubject<Void, Never>() GeometryReader { geometry in if currentIndex < restaurants.count { let components = restaurants[currentIndex].components(separatedBy: "|") let distance = Double(components[2]) ?? 0.0 let rating = Float(components[3]) ?? 0.0 RestaurantCard(restaurantInfo: restaurants[currentIndex], distance: distance, cardWidth: geometry.size.width * 0.9, cardHeight: geometry.size.height * 0.6, favoritesManager: favoritesManager) .offset(x: cardOffset.width, y: cardOffset.height) .animation(.spring(response: 0.6, dampingFraction: 0.6)) .zIndex(1) .position(x: geometry.size.width / 2, y: geometry.size.height / 2) .gesture( DragGesture() .updating($dragState) { value, state, transaction in state = .active(translation: value.translation) cardOffset = value.translation } .onEnded { value in if value.translation.width < -50 || value.translation.width > 50 { if value.translation.width > 50 { favoritesManager.addFavorite(restaurant: restaurants[currentIndex]) } let screenWidth = UIScreen.main.bounds.size.width let xTranslation = value.translation.width > 0 ? screenWidth : -screenWidth withAnimation { cardOffset = CGSize(width: xTranslation, height: 0) } cardSwipedPublisher.send() } else { cardOffset = .zero } } ) .onReceive(cardSwipedPublisher) { swipedIndices.insert(currentIndex) currentIndex += 1 if currentIndex == restaurants.count { showNoMoreRestaurants = true } DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { cardOffset = .zero } } } } } } } } else { if showNoMoreRestaurants { VStack { Text("No more restaurants nearby, increase radius?") .font(.title2) .multilineTextAlignment(.center) .padding(.bottom, 10) Button(action: { searchRadius += 1500 fetchNearbyRestaurants(latitude: locationManager.lastLocation?.coordinate.latitude ?? 0, longitude: locationManager.lastLocation?.coordinate.longitude ?? 0) }) { Text("Find More") .padding(.horizontal, 24) .padding(.vertical, 12) .foregroundColor(.white) .background(Color.blue) .cornerRadius(8) } } } else { Text("Loading restaurants...") } } } VStack { Spacer() HamburgerMenu(isOpen: $isMenuOpen, showFavorites: $showFavorites) } if showFavorites { NavigationView { List(favoritesManager.favorites, id: \.self) { favorite in let components = favorite.components(separatedBy: "|") let restaurantName = components[0] Text(restaurantName) } .navigationBarTitle("Favorites", displayMode: .inline) .toolbar { Button("Done") { showFavorites = false } } } } } .onAppear { locationManager.requestAuthorization() } .onChange(of: locationManager.lastLocation) { location in DispatchQueue.main.async { if let location = location { fetchNearbyRestaurants(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) } } } // MARK: - Fetch Nearby Restaurants func fetchNearbyRestaurants(latitude: CLLocationDegrees, longitude: CLLocationDegrees) { let apiKey = "AIzaSyCWUFGPrVuiFqwqWhwvrw9Glf9HNlkuvM0" let urlString = "https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=\(latitude),\(longitude)&radius=\(searchRadius)&type=restaurant&key=\(apiKey)" guard let url = URL(string: urlString) else { print("Invalid URL") return } URLSession.shared.dataTask(with: url) { data, response, error in if let data = data { do { let jsonDecoder = JSONDecoder() let response = try jsonDecoder.decode(GooglePlacesResponse.self, from: data) DispatchQueue.main.async { if response.results.count < 10 && searchRadius < 10000 { fetchNearbyRestaurants(latitude: latitude, longitude: longitude) } else { let userLocation = CLLocation(latitude: latitude, longitude: longitude) restaurants = response.results.map { result in let restaurantLocation = CLLocation(latitude: result.geometry.location.lat, longitude: result.geometry.location.lng) let distanceInMiles = restaurantLocation.distance(from: userLocation) * 0.000621371 let rating = result.rating ?? 0.0 if let photoReference = result.photos?.first?.photo_reference { let photoUrl = "https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=\(photoReference)&key=\(apiKey)" return "\(result.name)|\(photoUrl)|\(distanceInMiles)|\(rating)" } else { return "\(result.name)||\(distanceInMiles)|\(rating)" } } } } } catch { print("Error decoding JSON: \(error)") } } else { print("Error fetching nearby restaurants: \(error?.localizedDescription ?? "Unknown error")") } }.resume() } } struct NoMoreRestaurantsView: View { @Binding var searchRadius: Int let fetchNearbyRestaurants: (CLLocationDegrees, CLLocationDegrees) -> Void let locationManager: LocationManager var body: some View { VStack { Text("No more restaurants nearby, increase radius?") .font(.title2) .multilineTextAlignment(.center) .padding(.bottom, 10) Button(action: { searchRadius += 1500 if let location = locationManager.lastLocation { fetchNearbyRestaurants(location.coordinate.latitude, location.coordinate.longitude) } }) { Text("Find More") .padding(.horizontal, 24) .padding(.vertical, 12) .foregroundColor(.white) .background(Color.blue) .cornerRadius(8) } } } } //....RestaurantCard........................................................................... struct RestaurantCard: View { let restaurantInfo: String let distance: Double let cardWidth: CGFloat let cardHeight: CGFloat @ObservedObject var favoritesManager: FavoritesManager var body: some View { let components = restaurantInfo.components(separatedBy: "|") let restaurantName = components[0] let imageUrl = components.count > 1 ? components[1] : "" let rating = Float(components.count > 3 ? components[3] : "0") ?? 0.0 VStack { if !imageUrl.isEmpty { AsyncImage(url: URL(string: imageUrl)) { image in image.resizable() } placeholder: { ProgressView() } .frame(maxWidth: .infinity, maxHeight: cardHeight * 2 / 3, alignment: .top) .aspectRatio(contentMode: .fill) .clipped() } VStack(alignment: .leading) { HStack { Text(restaurantName) .font(.custom("Roboto-Thin", size: 24)) .foregroundColor(.primary) Spacer() if favoritesManager.isFavorite(restaurant: restaurantInfo) { // Check if the restaurant is a favorite Image(systemName: "heart.fill") .foregroundColor(.red) } } Text(String(format: "%.2f miles", distance)) .font(.footnote) .foregroundColor(.gray) .padding(.bottom) StarsView(rating: rating) } .padding([.leading, .trailing]) Spacer() } .frame(width: cardWidth, height: cardHeight) .background(Color(.systemGray5)) .cornerRadius(10) .shadow(radius: 10) } } struct StarsView: View { let rating: Float var body: some View { HStack { ForEach(0..<5) { index in Image(systemName: rating >= Float(index + 1) ? "star.fill" : (rating >= Float(index) + 0.5 ? "star.leadinghalf.fill" : "star")) .resizable() .scaledToFit() .frame(height: 16) .foregroundColor(.yellow) } } } } //....End of RestaurnatCard........................................................................... //....HamburgerMenu........................................................................... struct HamburgerMenu: View { @Binding var isOpen: Bool @Binding var showFavorites: Bool var body: some View { VStack { Spacer() if isOpen { VStack { HStack { Button(action: { withAnimation { isOpen.toggle() } }) { Image(systemName: "xmark") .font(.system(size: 20)) .foregroundColor(.primary) .padding() } Spacer() } Button("Filters") { print("Option 1 selected") } .frame(maxWidth: .infinity, minHeight: 130) .background(Color(.systemGray5)) .foregroundColor(.primary) .font(.system(size: 36)) .background(Color.clear) // Add this line Button("Favorites") { isOpen = false showFavorites = true } .frame(maxWidth: .infinity, minHeight: 130) .background(Color(.systemGray5)) .foregroundColor(.primary) .font(.system(size: 36)) .background(Color.clear) // Add this line Button("About") { print("Option 3 selected") } .frame(maxWidth: .infinity, minHeight: 130) .background(Color(.systemGray5)) .foregroundColor(.primary) .font(.system(size: 36)) .background(Color.clear) // Add this line Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(.systemBackground)) // Use systemBackground to adapt to dark and light modes .edgesIgnoringSafeArea(.all) .transition(.move(edge: .bottom)) } Button(action: { withAnimation { isOpen.toggle() } }) { Image(systemName: "line.horizontal.3") .font(.system(size: 20)) .foregroundColor(.primary) .padding() } } } } //....End of HamburgerMenu........................................................................... //....GooglePlacesResponse........................................................................... struct GooglePlacesResponse: Codable { let results: [Place] struct Place: Codable { let name: String let photos: [Photo]? let geometry: Geometry let rating: Float? struct Geometry: Codable { let location: Location struct Location: Codable { let lat: Double let lng: Double } } struct Photo: Codable { let photo_reference: String } } } //.... End of GooglePlacesResponse..................................................................... //.....DragState...................................................................................... enum DragState { case inactive case active(translation: CGSize) var translation: CGSize { switch self { case .inactive: return .zero case .active(let translation): return translation } } var isActive: Bool { switch self { case .inactive: return false case .active: return true } } } //.....End of DragState....................................................................................... }
Editor is loading...