Fix game times with UTC data, restructure schedule by date
- Update games_canonical.json to use ISO 8601 UTC timestamps (game_datetime_utc) - Fix BootstrapService timezone-aware parsing for venue-local fallback - Fix thread-unsafe shared DateFormatter in RichGame local time display - Bump SchemaVersion to 4 to force re-bootstrap with correct UTC data - Restructure schedule view: group by date instead of sport, with sport icons on each row and date section headers showing game counts - Fix schedule row backgrounds using Theme.cardBackground instead of black - Sort games by UTC time with local-time tiebreaker for same-instant games Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,8 +32,8 @@ final class ScheduleViewModel {
|
||||
|
||||
// MARK: - Pre-computed Groupings (avoid computed property overhead)
|
||||
|
||||
/// Games grouped by sport - pre-computed to avoid re-grouping on every render
|
||||
private(set) var gamesBySport: [(sport: Sport, games: [RichGame])] = []
|
||||
/// Games grouped by date - pre-computed to avoid re-grouping on every render
|
||||
private(set) var gamesByDate: [(date: Date, games: [RichGame])] = []
|
||||
|
||||
/// All games matching current filters (before any display limiting)
|
||||
private var filteredGames: [RichGame] = []
|
||||
@@ -193,15 +193,20 @@ final class ScheduleViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Pre-compute grouping by sport (done once, not per-render)
|
||||
let grouped = Dictionary(grouping: filteredGames) { $0.game.sport }
|
||||
gamesBySport = grouped
|
||||
.sorted { lhs, rhs in
|
||||
let lhsIndex = Sport.allCases.firstIndex(of: lhs.key) ?? 0
|
||||
let rhsIndex = Sport.allCases.firstIndex(of: rhs.key) ?? 0
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
.map { (sport: $0.key, games: $0.value.sorted { $0.game.dateTime < $1.game.dateTime }) }
|
||||
// Step 2: Pre-compute grouping by date (done once, not per-render)
|
||||
let calendar = Calendar.current
|
||||
let grouped = Dictionary(grouping: filteredGames) { calendar.startOfDay(for: $0.game.dateTime) }
|
||||
gamesByDate = grouped
|
||||
.sorted { $0.key < $1.key }
|
||||
.map { (date: $0.key, games: $0.value.sorted { lhs, rhs in
|
||||
if lhs.game.dateTime == rhs.game.dateTime {
|
||||
// Same UTC time: sort by local display time (earlier local times first)
|
||||
let lhsOffset = lhs.stadium.timeZone?.secondsFromGMT(for: lhs.game.dateTime) ?? 0
|
||||
let rhsOffset = rhs.stadium.timeZone?.secondsFromGMT(for: rhs.game.dateTime) ?? 0
|
||||
return lhsOffset < rhsOffset
|
||||
}
|
||||
return lhs.game.dateTime < rhs.game.dateTime
|
||||
}) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ struct ScheduleListView: View {
|
||||
@State private var showDiagnostics = false
|
||||
|
||||
private var hasGames: Bool {
|
||||
!viewModel.gamesBySport.isEmpty
|
||||
!viewModel.gamesByDate.isEmpty
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -109,16 +109,31 @@ struct ScheduleListView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
|
||||
ForEach(viewModel.gamesBySport, id: \.sport) { sportGroup in
|
||||
ForEach(viewModel.gamesByDate, id: \.date) { dateGroup in
|
||||
Section {
|
||||
ForEach(sportGroup.games) { richGame in
|
||||
GameRowView(game: richGame, showDate: true, showLocation: true)
|
||||
ForEach(dateGroup.games) { richGame in
|
||||
GameRowView(game: richGame, showSport: true, showLocation: true)
|
||||
}
|
||||
} header: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: sportGroup.sport.iconName)
|
||||
.accessibilityHidden(true)
|
||||
Text(sportGroup.sport.rawValue)
|
||||
HStack {
|
||||
Text(formatSectionDate(dateGroup.date))
|
||||
|
||||
if Calendar.current.isDateInToday(dateGroup.date) {
|
||||
Text("TODAY")
|
||||
.font(.caption2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(dateGroup.games.count) games")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.headline)
|
||||
}
|
||||
@@ -233,98 +248,70 @@ struct SportFilterChip: View {
|
||||
// MARK: - Game Row View
|
||||
|
||||
struct GameRowView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
let game: RichGame
|
||||
var showDate: Bool = false
|
||||
var showSport: Bool = false
|
||||
var showLocation: Bool = false
|
||||
|
||||
// Static formatter to avoid allocation per row (significant performance improvement)
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEE, MMM d"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
// Cache isToday check to avoid repeated Calendar calls
|
||||
private var isToday: Bool {
|
||||
Calendar.current.isDateInToday(game.game.dateTime)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Date (when grouped by sport)
|
||||
if showDate {
|
||||
HStack(spacing: 6) {
|
||||
Text(Self.dateFormatter.string(from: game.game.dateTime))
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if isToday {
|
||||
Text("TODAY")
|
||||
.font(.caption2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
HStack(spacing: 12) {
|
||||
// Sport icon
|
||||
if showSport {
|
||||
Image(systemName: game.game.sport.iconName)
|
||||
.font(.title3)
|
||||
.foregroundStyle(game.game.sport.themeColor)
|
||||
.frame(width: 28)
|
||||
.accessibilityLabel(game.game.sport.rawValue)
|
||||
}
|
||||
|
||||
// Teams
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 8) {
|
||||
TeamBadge(team: game.awayTeam, isHome: false)
|
||||
Text("@")
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
// Teams
|
||||
HStack(spacing: 8) {
|
||||
TeamBadge(team: game.awayTeam, isHome: false)
|
||||
Text("@")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
TeamBadge(team: game.homeTeam, isHome: true)
|
||||
}
|
||||
|
||||
// Game info
|
||||
HStack(spacing: 12) {
|
||||
Label(game.localGameTimeShort, systemImage: "clock")
|
||||
Label(game.stadium.name, systemImage: "building.2")
|
||||
.lineLimit(1)
|
||||
|
||||
if let broadcast = game.game.broadcastInfo {
|
||||
Label(broadcast, systemImage: "tv")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Location
|
||||
if showLocation {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
.accessibilityHidden(true)
|
||||
Text(game.stadium.city)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
TeamBadge(team: game.homeTeam, isHome: true)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Game info
|
||||
HStack(spacing: 12) {
|
||||
Label(game.localGameTimeShort, systemImage: "clock")
|
||||
Label(game.stadium.name, systemImage: "building.2")
|
||||
|
||||
if let broadcast = game.game.broadcastInfo {
|
||||
Label(broadcast, systemImage: "tv")
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Location info (when grouped by game)
|
||||
if showLocation {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
.accessibilityHidden(true)
|
||||
Text(game.stadium.city)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.listRowBackground(isToday ? Color.orange.opacity(0.1) : nil)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(gameAccessibilityLabel)
|
||||
}
|
||||
|
||||
private var gameAccessibilityLabel: String {
|
||||
var parts = ["\(game.awayTeam.name) at \(game.homeTeam.name)"]
|
||||
parts.append(game.stadium.name)
|
||||
var parts: [String] = []
|
||||
if showSport { parts.append(game.game.sport.rawValue) }
|
||||
parts.append("\(game.awayTeam.name) at \(game.homeTeam.name)")
|
||||
parts.append(game.localGameTimeShort)
|
||||
if showDate {
|
||||
parts.append(Self.dateFormatter.string(from: game.game.dateTime))
|
||||
}
|
||||
parts.append(game.stadium.name)
|
||||
return parts.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
//
|
||||
// CategoryPicker.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Polished category picker for custom itinerary items
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Category picker with labeled pills in a grid layout
|
||||
struct CategoryPicker: View {
|
||||
@Binding var selectedCategory: ItemCategory
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
/// Dynamic columns that adapt to accessibility text sizes
|
||||
private var columns: [GridItem] {
|
||||
[
|
||||
GridItem(.flexible(), spacing: Theme.Spacing.sm),
|
||||
GridItem(.flexible(), spacing: Theme.Spacing.sm),
|
||||
GridItem(.flexible(), spacing: Theme.Spacing.sm)
|
||||
]
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: columns, spacing: Theme.Spacing.sm) {
|
||||
ForEach(ItemCategory.allCases, id: \.self) { category in
|
||||
CategoryPill(
|
||||
category: category,
|
||||
isSelected: selectedCategory == category,
|
||||
colorScheme: colorScheme
|
||||
) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
selectedCategory = category
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Individual category pill with emoji icon and label
|
||||
private struct CategoryPill: View {
|
||||
let category: ItemCategory
|
||||
let isSelected: Bool
|
||||
let colorScheme: ColorScheme
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
// Emoji with background circle
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(emojiBackground)
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Text(category.icon)
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.shadow(
|
||||
color: isSelected ? Theme.warmOrange.opacity(0.3) : .clear,
|
||||
radius: 6,
|
||||
y: 2
|
||||
)
|
||||
|
||||
Text(category.label)
|
||||
.font(.caption)
|
||||
.fontWeight(isSelected ? .semibold : .medium)
|
||||
.foregroundStyle(labelColor)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, Theme.Spacing.md)
|
||||
.padding(.horizontal, Theme.Spacing.xs)
|
||||
.background(pillBackground)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.strokeBorder(borderColor, lineWidth: isSelected ? 2 : 1)
|
||||
)
|
||||
.shadow(
|
||||
color: isSelected ? Theme.warmOrange.opacity(0.15) : .clear,
|
||||
radius: 8,
|
||||
y: 4
|
||||
)
|
||||
}
|
||||
.buttonStyle(CategoryPillButtonStyle())
|
||||
.accessibilityLabel(category.label)
|
||||
.accessibilityHint("Category for \(category.label.lowercased()) items")
|
||||
.accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton)
|
||||
}
|
||||
|
||||
private var emojiBackground: Color {
|
||||
if isSelected {
|
||||
return Theme.warmOrange.opacity(0.2)
|
||||
} else {
|
||||
return Theme.cardBackgroundElevated(colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
private var pillBackground: Color {
|
||||
if isSelected {
|
||||
return Theme.warmOrange.opacity(0.08)
|
||||
} else {
|
||||
return Theme.cardBackground(colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
if isSelected {
|
||||
return Theme.warmOrange
|
||||
} else {
|
||||
return Theme.surfaceGlow(colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
private var labelColor: Color {
|
||||
if isSelected {
|
||||
return Theme.warmOrange
|
||||
} else {
|
||||
return Theme.textSecondary(colorScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom button style with scale effect on press
|
||||
private struct CategoryPillButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||
.animation(Theme.Animation.prefersReducedMotion ? nil : .spring(response: 0.3, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Light Mode") {
|
||||
struct PreviewWrapper: View {
|
||||
@State private var selected: ItemCategory = .restaurant
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
CategoryPicker(selectedCategory: $selected)
|
||||
.padding()
|
||||
|
||||
Text("Selected: \(selected.label)")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
}
|
||||
|
||||
return PreviewWrapper()
|
||||
.preferredColorScheme(.light)
|
||||
}
|
||||
|
||||
#Preview("Dark Mode") {
|
||||
struct PreviewWrapper: View {
|
||||
@State private var selected: ItemCategory = .attraction
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
CategoryPicker(selectedCategory: $selected)
|
||||
.padding()
|
||||
|
||||
Text("Selected: \(selected.label)")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
}
|
||||
|
||||
return PreviewWrapper()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
@@ -8,35 +8,6 @@
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
/// Category for custom itinerary items with emoji icons
|
||||
enum ItemCategory: String, CaseIterable {
|
||||
case restaurant
|
||||
case attraction
|
||||
case fuel
|
||||
case hotel
|
||||
case other
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .restaurant: return "\u{1F37D}" // 🍽️
|
||||
case .attraction: return "\u{1F3A2}" // 🎢
|
||||
case .fuel: return "\u{26FD}" // ⛽
|
||||
case .hotel: return "\u{1F3E8}" // 🏨
|
||||
case .other: return "\u{1F4CC}" // 📌
|
||||
}
|
||||
}
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .restaurant: return "Eat"
|
||||
case .attraction: return "See"
|
||||
case .fuel: return "Fuel"
|
||||
case .hotel: return "Stay"
|
||||
case .other: return "Other"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy sheet for adding/editing custom itinerary items.
|
||||
/// - Note: Use `QuickAddItemSheet` instead for new code.
|
||||
@available(*, deprecated, message: "Use QuickAddItemSheet instead")
|
||||
@@ -56,7 +27,6 @@ struct AddItemSheet: View {
|
||||
}
|
||||
|
||||
@State private var entryMode: EntryMode = .searchPlaces
|
||||
@State private var selectedCategory: ItemCategory = .restaurant
|
||||
@State private var title: String = ""
|
||||
@State private var isSaving = false
|
||||
|
||||
@@ -71,9 +41,6 @@ struct AddItemSheet: View {
|
||||
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) {
|
||||
@@ -113,8 +80,6 @@ struct AddItemSheet: View {
|
||||
}
|
||||
.onAppear {
|
||||
if let existing = existingItem, let info = existing.customInfo {
|
||||
// Find matching category by icon, default to .other
|
||||
selectedCategory = ItemCategory.allCases.first { $0.icon == info.icon } ?? .other
|
||||
title = info.title
|
||||
// If editing a mappable item, switch to custom mode
|
||||
entryMode = .custom
|
||||
@@ -264,20 +229,6 @@ struct AddItemSheet: View {
|
||||
return components.isEmpty ? nil : components.joined(separator: ", ")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var categoryPicker: some View {
|
||||
HStack(spacing: Theme.Spacing.md) {
|
||||
ForEach(ItemCategory.allCases, id: \.self) { category in
|
||||
CategoryButton(
|
||||
category: category,
|
||||
isSelected: selectedCategory == category
|
||||
) {
|
||||
selectedCategory = category
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveItem() {
|
||||
isSaving = true
|
||||
|
||||
@@ -290,7 +241,7 @@ struct AddItemSheet: View {
|
||||
|
||||
let customInfo = CustomInfo(
|
||||
title: trimmedTitle,
|
||||
icon: selectedCategory.icon,
|
||||
icon: "\u{1F4CC}",
|
||||
time: existingInfo.time,
|
||||
latitude: existingInfo.latitude,
|
||||
longitude: existingInfo.longitude,
|
||||
@@ -312,7 +263,7 @@ struct AddItemSheet: View {
|
||||
|
||||
let customInfo = CustomInfo(
|
||||
title: placeName,
|
||||
icon: selectedCategory.icon,
|
||||
icon: "\u{1F4CC}",
|
||||
time: nil,
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude,
|
||||
@@ -333,7 +284,7 @@ struct AddItemSheet: View {
|
||||
|
||||
let customInfo = CustomInfo(
|
||||
title: trimmedTitle,
|
||||
icon: selectedCategory.icon,
|
||||
icon: "\u{1F4CC}",
|
||||
time: nil
|
||||
)
|
||||
|
||||
@@ -403,36 +354,6 @@ private struct PlaceResultRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Category Button
|
||||
|
||||
private struct CategoryButton: View {
|
||||
let category: 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)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddItemSheet(
|
||||
tripId: UUID(),
|
||||
|
||||
@@ -269,7 +269,8 @@ struct TripDetailView: View {
|
||||
Task { await deleteItineraryItem(item) }
|
||||
},
|
||||
onAddButtonTapped: { day in
|
||||
addItemAnchor = AddItemAnchor(day: day)
|
||||
let coord = coordinateForDay(day)
|
||||
addItemAnchor = AddItemAnchor(day: day, regionCoordinate: coord)
|
||||
}
|
||||
)
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
@@ -692,7 +693,8 @@ struct TripDetailView: View {
|
||||
DropTargetIndicator()
|
||||
}
|
||||
InlineAddButton {
|
||||
addItemAnchor = AddItemAnchor(day: day)
|
||||
let coord = coordinateForDay(day)
|
||||
addItemAnchor = AddItemAnchor(day: day, regionCoordinate: coord)
|
||||
}
|
||||
}
|
||||
.onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
|
||||
@@ -1285,6 +1287,12 @@ struct TripDetailView: View {
|
||||
return allItems
|
||||
}
|
||||
|
||||
/// Returns the coordinate of the first stop on the given day, for location-biased search.
|
||||
private func coordinateForDay(_ day: Int) -> CLLocationCoordinate2D? {
|
||||
let tripDay = trip.itineraryDays().first { $0.dayNumber == day }
|
||||
return tripDay?.stops.first?.coordinate
|
||||
}
|
||||
|
||||
private func toggleSaved() {
|
||||
if isSaved {
|
||||
unsaveTrip()
|
||||
@@ -1615,6 +1623,7 @@ enum ItinerarySection {
|
||||
struct AddItemAnchor: Identifiable {
|
||||
let id = UUID()
|
||||
let day: Int
|
||||
let regionCoordinate: CLLocationCoordinate2D?
|
||||
}
|
||||
|
||||
// MARK: - Inline Add Button
|
||||
@@ -2100,7 +2109,8 @@ private struct SheetModifiers: ViewModifier {
|
||||
QuickAddItemSheet(
|
||||
tripId: tripId,
|
||||
day: anchor.day,
|
||||
existingItem: nil
|
||||
existingItem: nil,
|
||||
regionCoordinate: anchor.regionCoordinate
|
||||
) { item in
|
||||
Task { await saveItineraryItem(item) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user