Files
Sportstime/SportsTime/Features/Trip/Views/AddItem/POIDetailSheet.swift
Trey t e6c4b8e12b Add nearby POIs to Add-to-Day sheet and improve PlaceSearchSheet empty state
- Add mapItem field to POISearchService.POI for Apple Maps integration
- Merge description + location into single combined card in QuickAddItemSheet
- Auto-load nearby POIs when regionCoordinate is available, with detail sheet
- Create POIDetailSheet with map preview, metadata, and one-tap add-to-day
- Add poiAddedToDay/poiDetailViewed analytics events
- Add initial state to PlaceSearchSheet with search suggestions and flow layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 10:45:36 -06:00

181 lines
6.3 KiB
Swift

//
// POIDetailSheet.swift
// SportsTime
//
// Detail sheet for a nearby point of interest with map preview and add-to-day action.
//
import SwiftUI
import MapKit
struct POIDetailSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
let poi: POISearchService.POI
let day: Int
let onAddToDay: (POISearchService.POI) -> Void
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 0) {
mapPreview
detailContent
}
}
.background(Theme.backgroundGradient(colorScheme))
.navigationTitle(poi.name)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close") {
dismiss()
}
.foregroundStyle(Theme.textSecondary(colorScheme))
}
}
.onAppear {
AnalyticsManager.shared.track(.poiDetailViewed(
poiName: poi.name,
category: poi.category.displayName
))
}
}
.presentationDetents([.medium, .large])
}
// MARK: - Map Preview
private var mapPreview: some View {
Map(initialPosition: .region(MKCoordinateRegion(
center: poi.coordinate,
latitudinalMeters: 800,
longitudinalMeters: 800
))) {
Marker(poi.name, coordinate: poi.coordinate)
.tint(Theme.warmOrange)
}
.mapStyle(.standard(pointsOfInterest: .excludingAll))
.frame(height: 160)
.allowsHitTesting(false)
.accessibilityLabel("Map showing \(poi.name)")
}
// MARK: - Detail Content
private var detailContent: some View {
VStack(spacing: Theme.Spacing.lg) {
// Info section
VStack(alignment: .leading, spacing: Theme.Spacing.md) {
// Category badge
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: poi.category.iconName)
.font(.caption.weight(.semibold))
.foregroundStyle(.white)
.frame(width: 24, height: 24)
.background(categoryColor)
.clipShape(RoundedRectangle(cornerRadius: 6))
Text(poi.category.displayName)
.font(.subheadline)
.fontWeight(.medium)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
// Name
Text(poi.name)
.font(.title2)
.fontWeight(.bold)
.foregroundStyle(Theme.textPrimary(colorScheme))
// Metadata rows
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
if let address = poi.address {
metadataRow(icon: "mappin.circle.fill", text: address)
}
metadataRow(
icon: "figure.walk",
text: "\(poi.formattedDistance) away",
highlight: true
)
if let url = poi.mapItem?.url {
Link(destination: url) {
metadataRow(icon: "globe", text: url.host ?? "Website", isLink: true)
}
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(Theme.Spacing.lg)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.strokeBorder(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.shadow(color: Theme.cardShadow(colorScheme), radius: 8, y: 4)
// Action buttons
VStack(spacing: Theme.Spacing.sm) {
Button {
onAddToDay(poi)
dismiss()
} label: {
Label("Add to Day \(day)", systemImage: "plus.circle.fill")
.font(.body.weight(.semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, Theme.Spacing.md)
}
.buttonStyle(.borderedProminent)
.tint(Theme.warmOrange)
.accessibilityLabel("Add \(poi.name) to Day \(day)")
if poi.mapItem != nil {
Button {
poi.mapItem?.openInMaps()
} label: {
Label("Open in Apple Maps", systemImage: "map.fill")
.font(.body.weight(.medium))
.frame(maxWidth: .infinity)
.padding(.vertical, Theme.Spacing.md)
}
.buttonStyle(.bordered)
.tint(Theme.textSecondary(colorScheme))
.accessibilityLabel("Open \(poi.name) in Apple Maps")
}
}
}
.padding(Theme.Spacing.lg)
}
// MARK: - Helpers
private func metadataRow(icon: String, text: String, highlight: Bool = false, isLink: Bool = false) -> some View {
HStack(spacing: Theme.Spacing.sm) {
Image(systemName: icon)
.font(.caption)
.foregroundStyle(highlight ? Theme.warmOrange : Theme.textMuted(colorScheme))
.frame(width: 20)
.accessibilityHidden(true)
Text(text)
.font(.subheadline)
.foregroundStyle(isLink ? Theme.warmOrange : Theme.textSecondary(colorScheme))
.lineLimit(2)
}
}
private var categoryColor: Color {
switch poi.category {
case .restaurant: return .orange
case .attraction: return .yellow
case .entertainment: return .purple
case .nightlife: return .indigo
case .museum: return .teal
}
}
}