// // AddItemSheet.swift // SportsTime // // Sheet for adding/editing custom itinerary items // import SwiftUI import MapKit struct AddItemSheet: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme let tripId: UUID let day: Int let existingItem: CustomItineraryItem? var onSave: (CustomItineraryItem) -> Void // Entry mode enum EntryMode: String, CaseIterable { case searchPlaces = "Search Places" case custom = "Custom" } @State private var entryMode: EntryMode = .searchPlaces @State private var selectedCategory: CustomItineraryItem.ItemCategory = .restaurant @State private var title: String = "" @State private var isSaving = false // MapKit search state @State private var searchQuery = "" @State private var searchResults: [MKMapItem] = [] @State private var selectedPlace: MKMapItem? @State private var isSearching = false private var isEditing: Bool { existingItem != nil } var body: some View { NavigationStack { VStack(spacing: Theme.Spacing.lg) { // Category picker categoryPicker // Entry mode picker (only for new items) if !isEditing { Picker("Entry Mode", selection: $entryMode) { ForEach(EntryMode.allCases, id: \.self) { mode in Text(mode.rawValue).tag(mode) } } .pickerStyle(.segmented) } if entryMode == .searchPlaces && !isEditing { // MapKit search UI searchPlacesView } else { // Custom text entry TextField("What's the plan?", text: $title) .textFieldStyle(.roundedBorder) .font(.body) } Spacer() } .padding() .background(Theme.backgroundGradient(colorScheme)) .navigationTitle(isEditing ? "Edit Item" : "Add Item") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(isEditing ? "Save" : "Add") { saveItem() } .disabled(!canSave || isSaving) } } .onAppear { if let existing = existingItem { selectedCategory = existing.category title = existing.title // If editing a mappable item, switch to custom mode entryMode = .custom } } } } private var canSave: Bool { if entryMode == .searchPlaces && !isEditing { return selectedPlace != nil } else { return !title.trimmingCharacters(in: .whitespaces).isEmpty } } @ViewBuilder private var searchPlacesView: some View { VStack(spacing: Theme.Spacing.md) { // Search field HStack { Image(systemName: "magnifyingglass") .foregroundStyle(.secondary) TextField("Search for a place...", text: $searchQuery) .textFieldStyle(.plain) .autocorrectionDisabled() .onSubmit { performSearch() } if isSearching { ProgressView() .scaleEffect(0.8) } else if !searchQuery.isEmpty { Button { searchQuery = "" searchResults = [] selectedPlace = nil } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.secondary) } } } .padding(10) .background(Color(.systemGray6)) .cornerRadius(10) // Search results if !searchResults.isEmpty { ScrollView { LazyVStack(spacing: 0) { ForEach(searchResults, id: \.self) { item in PlaceResultRow( item: item, isSelected: selectedPlace == item ) { selectedPlace = item } Divider() } } } .frame(maxHeight: 300) .background(Color(.systemBackground)) .cornerRadius(10) } else if !searchQuery.isEmpty && !isSearching { Text("No results found") .foregroundStyle(.secondary) .font(.subheadline) .padding() } // Selected place preview if let place = selectedPlace { selectedPlacePreview(place) } } } @ViewBuilder private func selectedPlacePreview(_ place: MKMapItem) -> some View { VStack(alignment: .leading, spacing: 4) { HStack { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text(place.name ?? "Unknown Place") .font(.headline) Spacer() } if let address = formatAddress(for: place) { Text(address) .font(.caption) .foregroundStyle(.secondary) } } .padding() .background(Color.green.opacity(0.1)) .cornerRadius(10) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.green.opacity(0.3), lineWidth: 1) ) } private func performSearch() { let trimmedQuery = searchQuery.trimmingCharacters(in: .whitespaces) guard !trimmedQuery.isEmpty else { return } isSearching = true selectedPlace = nil let request = MKLocalSearch.Request() request.naturalLanguageQuery = trimmedQuery let search = MKLocalSearch(request: request) search.start { response, error in isSearching = false if let response = response { searchResults = response.mapItems } else { searchResults = [] } } } private func formatAddress(for item: MKMapItem) -> String? { let placemark = item.placemark var components: [String] = [] if let thoroughfare = placemark.thoroughfare { if let subThoroughfare = placemark.subThoroughfare { components.append("\(subThoroughfare) \(thoroughfare)") } else { components.append(thoroughfare) } } if let locality = placemark.locality { components.append(locality) } if let administrativeArea = placemark.administrativeArea { components.append(administrativeArea) } return components.isEmpty ? nil : components.joined(separator: ", ") } @ViewBuilder private var categoryPicker: some View { HStack(spacing: Theme.Spacing.md) { ForEach(CustomItineraryItem.ItemCategory.allCases, id: \.self) { category in CategoryButton( category: category, isSelected: selectedCategory == category ) { selectedCategory = category } } } } private func saveItem() { isSaving = true let item: CustomItineraryItem if let existing = existingItem { // Editing existing item - preserve day and sortOrder let trimmedTitle = title.trimmingCharacters(in: .whitespaces) guard !trimmedTitle.isEmpty else { return } item = CustomItineraryItem( id: existing.id, tripId: existing.tripId, category: selectedCategory, title: trimmedTitle, day: existing.day, sortOrder: existing.sortOrder, createdAt: existing.createdAt, modifiedAt: Date(), latitude: existing.latitude, longitude: existing.longitude, address: existing.address ) } else if entryMode == .searchPlaces, let place = selectedPlace { // Creating from MapKit search let placeName = place.name ?? "Unknown Place" let coordinate = place.placemark.coordinate // New items get sortOrder 0 (will be placed at beginning, caller can adjust) item = CustomItineraryItem( tripId: tripId, category: selectedCategory, title: placeName, day: day, sortOrder: 0.0, latitude: coordinate.latitude, longitude: coordinate.longitude, address: formatAddress(for: place) ) } else { // Creating custom item (no location) let trimmedTitle = title.trimmingCharacters(in: .whitespaces) guard !trimmedTitle.isEmpty else { return } // New items get sortOrder 0 (will be placed at beginning, caller can adjust) item = CustomItineraryItem( tripId: tripId, category: selectedCategory, title: trimmedTitle, day: day, sortOrder: 0.0 ) } onSave(item) dismiss() } } // MARK: - Place Result Row private struct PlaceResultRow: View { let item: MKMapItem let isSelected: Bool let onTap: () -> Void var body: some View { Button(action: onTap) { HStack { VStack(alignment: .leading, spacing: 2) { Text(item.name ?? "Unknown") .font(.subheadline) .foregroundStyle(.primary) if let address = formattedAddress { Text(address) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } Spacer() if isSelected { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) } } .padding(.vertical, 10) .padding(.horizontal, 12) .background(isSelected ? Color.green.opacity(0.1) : Color.clear) } .buttonStyle(.plain) } private var formattedAddress: String? { let placemark = item.placemark var components: [String] = [] if let locality = placemark.locality { components.append(locality) } if let administrativeArea = placemark.administrativeArea { components.append(administrativeArea) } return components.isEmpty ? nil : components.joined(separator: ", ") } } // MARK: - Category Button private struct CategoryButton: View { let category: CustomItineraryItem.ItemCategory let isSelected: Bool let action: () -> Void var body: some View { Button(action: action) { VStack(spacing: 4) { Text(category.icon) .font(.title2) Text(category.label) .font(.caption2) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(isSelected ? Theme.warmOrange.opacity(0.2) : Color.clear) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(isSelected ? Theme.warmOrange : Color.secondary.opacity(0.3), lineWidth: isSelected ? 2 : 1) ) } .buttonStyle(.plain) } } #Preview { AddItemSheet( tripId: UUID(), day: 1, existingItem: nil ) { _ in } }