struct ItemInfoView: View { @Environment(\.presentationMode) private var presentationMode @Environment(\.managedObjectContext) var moc @FetchRequest(sortDescriptors: []) var favorites: FetchedResults @Binding var isShowingBottomSheet: Bool @Binding var route: MKRoute? @EnvironmentObject private var inAppPurchaseManager: InAppPurchaseManager var selectedTabBarButton = "" var selectedResult: MKMapItem var url: URL? private var openAddress: String { guard let name = selectedResult.placemark.name, let thoroughFare = selectedResult.placemark.thoroughfare, let subThoroughFare = selectedResult.placemark.subThoroughfare, let locality = selectedResult.placemark.locality, let postalCode = selectedResult.placemark.postalCode else { return "" } return name + " " + thoroughFare + " " + subThoroughFare + " " + locality + " " + postalCode } var chargingPointLocation: ChargingPointLocation { guard let name = selectedResult.name, let type = selectedResult.pointOfInterestCategory, let thoroughfare = selectedResult.placemark.thoroughfare, let subThoroughfare = selectedResult.placemark.subThoroughfare, let locality = selectedResult.placemark.locality, let postalCode = selectedResult.placemark.postalCode, let phoneNumber = selectedResult.phoneNumber, let url = selectedResult.url else { return ChargingPointLocation( title: "", isFavorite: false, type: "", thoroughfare: "", subThoroughfare: "", locality: "", postalCode: "", phoneNumber: "", url: URL(string: "") ) } return ChargingPointLocation( chargingPointUUID: "", title: name, coordinate: selectedResult.placemark.coordinate, isFavorite: false, type: type.rawValue, thoroughfare: thoroughfare, subThoroughfare: subThoroughfare, locality: locality, postalCode: postalCode, phoneNumber: phoneNumber, url:url ) } private var sharedURL: URL { if let url = url { return url } else { return URL(string: "") ?? URL(fileURLWithPath: "/") } } private var travelTime: String? { guard let route else { return nil} let formatter = DateComponentsFormatter() formatter.unitsStyle = .short formatter.allowedUnits = [.hour, .minute] return formatter.string(from: route.expectedTravelTime) } var body: some View { if (isShowingBottomSheet) { ScrollView { VStack { HStack(spacing: 18) { HStack { Text("\(selectedResult.name ?? "")") .font(.system(size: 28)) .fontWeight(.bold) Button(action: { toggleFavorite() }) { Image(systemName: isFavorite(chargingPoint: chargingPointLocation, favoriteChargingPoint: nil) ? "heart.fill" : "heart") .foregroundColor(.red) .frame(width: 32, height: 32) } } Spacer() ShareLink( item: sharedURL, preview: SharePreview( "\(selectedResult.name ?? "")", image: Image(systemName: "plus") ) ) { Image(systemName: "square.and.arrow.up") .resizable() .scaledToFit() .frame(width: 20, height: 20) } .labelStyle(.iconOnly) Button(action: { isShowingBottomSheet = false route = nil }) { Image(systemName: "xmark.circle") .resizable() .scaledToFit() .frame(width: 20, height: 20) } } .frame(minWidth: 0, maxWidth: .infinity) .padding() VStack(alignment: .leading, spacing: 0) { if selectedTabBarButton == "EV" { Text("Electric Vehicle Charging Station") .font(.system(size: 22)) .fontWeight(.semibold) .foregroundColor(.gray) .frame(height: 36, alignment: .leading) } else { Text("Gas Station") .font(.system(size: 22)) .fontWeight(.semibold) .foregroundColor(.gray) .frame(height: 36, alignment: .leading) } } VStack { HStack { Button { if let navigateTo = URL(string: "\(String(describing: sharedURL))") { if UIApplication.shared.canOpenURL(navigateTo) { UIApplication.shared.open(navigateTo) } } } label: { if let travelTime { VStack { Label("\(travelTime)", systemImage: "bolt.car.fill") .labelStyle(.iconOnly) .fontWeight(.regular) Label("\(travelTime)", systemImage: "bolt.car.fill") .labelStyle(.titleOnly) .fontWeight(.regular) } } } .buttonStyle(ChargingPointButtonStyle()) .frame(maxWidth: .infinity) Button { if let phoneNumberURL = selectedResult.phoneNumber, let url = (URL(string: "tel://\(phoneNumberURL)")), UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } label: { VStack { Label("", systemImage: "phone.fill") .labelStyle(.iconOnly) .fontWeight(.regular) Label("Call", systemImage: "phone.fill") .labelStyle(.titleOnly) .fontWeight(.regular) } } .buttonStyle(ChargingPointButtonStyle()) .frame(maxWidth: .infinity) Button { UIApplication.shared.open(selectedResult.url ?? URL(fileURLWithPath: "/"), options: [:], completionHandler: nil) } label: { VStack { Label("", systemImage: "safari.fill") .labelStyle(.iconOnly) .fontWeight(.regular) Label("Website", systemImage: "safari.fill") .labelStyle(.titleOnly) .fontWeight(.regular) } } .buttonStyle(ChargingPointButtonStyle()) .frame(maxWidth: .infinity) Button { // TODO: - More Actions } label: { VStack { Label("", systemImage: "ellipsis") .labelStyle(.iconOnly) .fontWeight(.regular) .padding(2) Label("More", systemImage: "ellipsis") .labelStyle(.titleOnly) .fontWeight(.regular) .padding(2) } } .buttonStyle(ChargingPointButtonStyle()) .frame(maxWidth: .infinity) } .fixedSize(horizontal: false, vertical: true) } .padding(8) .background(Spacer().frame(height: 8)) // Add Spacer for gap Divider() VStack { HStack { VStack { HStack { VStack(alignment: .leading) { Text("CHARGERS") .foregroundColor(.gray) Label("2", systemImage: "powerplug.fill") .labelStyle(.titleAndIcon) // TODO: - Selected Charger Count } Divider() } } VStack { HStack { VStack(alignment: .leading) { Text("PRICING") .foregroundColor(.gray) Label("Paid", systemImage: "dollarsign.circle.fill") .labelStyle(.titleAndIcon) // TODO: - Selected Charger Count } Divider() } } VStack(alignment: .leading) { Text("DISTANCE") .foregroundColor(.gray) HStack { let distanceToTarget = selectedResult.placemark.location?.distance( from: CLLocation( latitude: LocationManager.shared.manager.location?.coordinate.latitude ?? 0.0, longitude: LocationManager.shared.manager.location?.coordinate.longitude ?? 0.0) ) ?? 0.0 Label("\(String(format: "%.2f", distanceToTarget.convert(from: .meters, to: .kilometers))) km", systemImage: "point.topleft.down.curvedto.point.bottomright.up.fill") .labelStyle(.titleAndIcon) } } } } .frame(minWidth: 0, maxWidth: .infinity) Divider() if selectedTabBarButton == "EV" { VStack(alignment: .leading) { List { Section(header: Text("Ratings").foregroundColor(.gray).font(.headline)) { VStack(alignment: .leading) { Label("Open PlugShare", systemImage: "bolt.horizontal.circle.fill") .contentShape(Rectangle()) // Make the entire row tappable .onTapGesture { if let navigateTo = openPlugShare() { if UIApplication.shared.canOpenURL(navigateTo) { UIApplication.shared.open(navigateTo) } } } StarsView(rating: CGFloat(4), maxRating: 5) .frame(width: 100) Text("\(4)") .font(.system(size: 18)) .fontWeight(.bold) } } .listRowSeparator(.hidden) } } .background(Color.clear) .frame(height: 150) .listStyle(.insetGrouped) .scrollIndicators(.hidden) .scrollDisabled(true) } if selectedTabBarButton == "EV" { VStack(alignment: .leading) { List { Section(header: Text("Chargers").foregroundColor(.gray).font(.headline)) { Label("Standard", systemImage: "poweroutlet.type.h.fill") // TODO: - Add Socket Type Data HStack(spacing: 106) { Text("Standard . - kW") Label("2", systemImage: "powerplug.fill") .labelStyle(.titleAndIcon) // TODO: - Selected Charger Count } } .listRowSeparator(.hidden) } .background(Color.clear) .frame(height: 150) .listStyle(.insetGrouped) .scrollIndicators(.hidden) .scrollDisabled(true) } } VStack(alignment: .leading) { List { Section(header: Text("Details").foregroundColor(.gray).font(.headline)) { Text("Hours") .foregroundColor(.gray) Text("10:00 - 22:00") // TODO: - Add open hours data Text("Open") .foregroundColor(.green) // TODO: - Add open/close status } .listRowSeparator(.hidden) } .listStyle(.insetGrouped) .background(Color.clear) .frame(height: 180) .scrollIndicators(.hidden) .scrollDisabled(true) } VStack(alignment: .leading) { List { Section(header: Text("Address").foregroundColor(.gray).font(.headline)) { Label(openAddress, systemImage: "arrow.triangle.turn.up.right.diamond.fill") .contentShape(Rectangle()) // Make the entire row tappable .onTapGesture { if let navigateTo = URL(string: "\(String(describing: sharedURL))") { if UIApplication.shared.canOpenURL(navigateTo) { UIApplication.shared.open(navigateTo) } } } } .listRowSeparator(.hidden) } } .background(Color.clear) .frame(height: 150) .listStyle(.insetGrouped) if !inAppPurchaseManager.hasUnlockedPro { VStack(spacing: 0) { BannerAd(unitID: "ca-app-pub-2327375927149500/2020038616") .background(Color.bannerBG) .frame(maxWidth: .infinity, maxHeight: 50) } .frame(maxWidth: .infinity, maxHeight: 50) .background(Color.bannerBG) .padding(.bottom, -10) } } } .task { _ = Task { do { try await inAppPurchaseManager.loadProducts() } catch { print(error) } } } .onAppear(perform: { isShowingBottomSheet = true }) .onDisappear(perform: { route = nil isShowingBottomSheet = false }) .scrollIndicators(.hidden) } } } extension ItemInfoView { func openPlugShare() -> URL? { let destinationLatitude = selectedResult.placemark.coordinate.latitude let destinationLongitude = selectedResult.placemark.coordinate.longitude return URL(string: "https://api.plugshare.com/view/map?latitude=\(destinationLatitude)&longitude=\(destinationLongitude)&spanLat=0.005&spanLng=0.005") } func toggleFavorite() { if let existingFavorite = favorites.first(where: { $0.name == (chargingPointLocation.title) }) { moc.delete(existingFavorite) } else { let newFavorite = FavoriteChargingPoint(context: moc) newFavorite.id = UUID(uuidString: chargingPointLocation.chargingPointUUID) newFavorite.name = chargingPointLocation.title newFavorite.latitude = chargingPointLocation.coordinate?.latitude ?? 0.0 newFavorite.longitude = chargingPointLocation.coordinate?.longitude ?? 0.0 newFavorite.isFavorite = true newFavorite.type = chargingPointLocation.type newFavorite.thoroughfare = chargingPointLocation.thoroughfare newFavorite.subThoroughfare = chargingPointLocation.subThoroughfare newFavorite.locality = chargingPointLocation.locality newFavorite.postalCode = chargingPointLocation.postalCode newFavorite.phoneNumber = chargingPointLocation.phoneNumber newFavorite.url = chargingPointLocation.url } try? moc.save() } func isFavorite(chargingPoint: ChargingPointLocation?, favoriteChargingPoint: FavoriteChargingPoint?) -> Bool { return favorites.contains(where: { $0.postalCode == chargingPoint?.postalCode }) } }