Add 22 new UI tests across 8 test files covering Home, Schedule, Progress, Settings, TabNavigation, TripSaving, and TripOptions. Add accessibility identifiers to 11 view files for test element discovery. Fix sport chip assertion logic (all sports start selected, tap deselects), scroll container issues on iOS 26 nested ScrollViews, toggle interaction, and delete trip flow. Update QA coverage map from 32 to 54 automated test cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
694 lines
25 KiB
Swift
694 lines
25 KiB
Swift
//
|
|
// ProgressTabView.swift
|
|
// SportsTime
|
|
//
|
|
// Main view for stadium progress tracking with league selector and map.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct ProgressTabView: View {
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
@State private var viewModel = ProgressViewModel()
|
|
@State private var showVisitSheet = false
|
|
@State private var showPhotoImport = false
|
|
@State private var selectedStadium: Stadium?
|
|
@State private var selectedVisitId: UUID?
|
|
|
|
@Query private var visits: [StadiumVisit]
|
|
|
|
/// O(1) lookup for visits by ID (built from @Query results)
|
|
private var visitsById: [UUID: StadiumVisit] {
|
|
Dictionary(uniqueKeysWithValues: visits.map { ($0.id, $0) })
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if viewModel.stadiums.isEmpty {
|
|
ProgressView("Loading...")
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else {
|
|
scrollContent
|
|
}
|
|
}
|
|
.themedBackground()
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
ShareButton(
|
|
progress: viewModel.leagueProgress,
|
|
tripCount: viewModel.tripCount,
|
|
style: .icon
|
|
)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
}
|
|
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Menu {
|
|
Button {
|
|
showVisitSheet = true
|
|
} label: {
|
|
Label("Manual Entry", systemImage: "pencil")
|
|
}
|
|
|
|
Button {
|
|
showPhotoImport = true
|
|
} label: {
|
|
Label("Import from Photos", systemImage: "photo.on.rectangle.angled")
|
|
}
|
|
} label: {
|
|
Image(systemName: "plus.circle.fill")
|
|
.font(.title2)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
}
|
|
.accessibilityLabel("Add stadium visit")
|
|
}
|
|
}
|
|
.task {
|
|
viewModel.configure(with: modelContext.container)
|
|
await viewModel.loadData()
|
|
}
|
|
.sheet(isPresented: $showVisitSheet) {
|
|
StadiumVisitSheet(initialSport: viewModel.selectedSport) { _ in
|
|
Task {
|
|
await viewModel.loadData()
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showPhotoImport) {
|
|
PhotoImportView()
|
|
.onDisappear {
|
|
Task {
|
|
await viewModel.loadData()
|
|
}
|
|
}
|
|
}
|
|
.sheet(item: $selectedStadium) { stadium in
|
|
StadiumDetailSheet(
|
|
stadium: stadium,
|
|
visitStatus: viewModel.stadiumVisitStatus[stadium.id] ?? .notVisited,
|
|
sport: viewModel.selectedSport,
|
|
onVisitLogged: {
|
|
Task {
|
|
await viewModel.loadData()
|
|
}
|
|
}
|
|
)
|
|
.presentationDetents([.medium])
|
|
}
|
|
}
|
|
|
|
// MARK: - Scroll Content
|
|
|
|
private var scrollContent: some View {
|
|
ScrollView {
|
|
VStack(spacing: Theme.Spacing.lg) {
|
|
// League Selector
|
|
leagueSelector
|
|
.accessibilityIdentifier("progress.sportSelector")
|
|
.staggeredAnimation(index: 0)
|
|
|
|
// Progress Summary Card
|
|
progressSummaryCard
|
|
.staggeredAnimation(index: 1)
|
|
|
|
// Map View
|
|
ProgressMapView(
|
|
stadiums: viewModel.sportStadiums,
|
|
visitStatus: viewModel.stadiumVisitStatus,
|
|
selectedStadium: $selectedStadium
|
|
)
|
|
.frame(height: 300)
|
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
|
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
|
}
|
|
.staggeredAnimation(index: 2)
|
|
|
|
// Stadium Lists
|
|
stadiumListsSection
|
|
.staggeredAnimation(index: 3)
|
|
|
|
// Achievements Teaser
|
|
achievementsSection
|
|
.staggeredAnimation(index: 4)
|
|
|
|
// Recent Visits
|
|
if !viewModel.recentVisits.isEmpty {
|
|
recentVisitsSection
|
|
.staggeredAnimation(index: 5)
|
|
}
|
|
}
|
|
.padding(Theme.Spacing.md)
|
|
}
|
|
}
|
|
|
|
// MARK: - League Selector
|
|
|
|
private var leagueSelector: some View {
|
|
SportSelectorGrid { sport in
|
|
SportProgressButton(
|
|
sport: sport,
|
|
isSelected: viewModel.selectedSport == sport,
|
|
progress: progressForSport(sport)
|
|
) {
|
|
Theme.Animation.withMotion(Theme.Animation.spring) {
|
|
viewModel.selectSport(sport)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func progressForSport(_ sport: Sport) -> Double {
|
|
let visitedCount = viewModel.visits.filter { $0.sportEnum == sport }.count
|
|
let total = LeagueStructure.stadiumCount(for: sport)
|
|
guard total > 0 else { return 0 }
|
|
return Double(min(visitedCount, total)) / Double(total)
|
|
}
|
|
|
|
// MARK: - Progress Summary Card
|
|
|
|
private var progressSummaryCard: some View {
|
|
let progress = viewModel.leagueProgress
|
|
|
|
return VStack(spacing: Theme.Spacing.lg) {
|
|
// Title and progress ring
|
|
HStack(alignment: .center, spacing: Theme.Spacing.lg) {
|
|
// Progress Ring
|
|
ZStack {
|
|
Circle()
|
|
.stroke(Theme.warmOrange.opacity(0.2), lineWidth: 8)
|
|
.frame(width: 80, height: 80)
|
|
.accessibilityHidden(true)
|
|
|
|
Circle()
|
|
.trim(from: 0, to: progress.progressFraction)
|
|
.stroke(Theme.warmOrange, style: StrokeStyle(lineWidth: 8, lineCap: .round))
|
|
.frame(width: 80, height: 80)
|
|
.rotationEffect(.degrees(-90))
|
|
.animation(
|
|
Theme.Animation.prefersReducedMotion ? nil : .easeInOut(duration: 0.5),
|
|
value: progress.progressFraction
|
|
)
|
|
.accessibilityHidden(true)
|
|
|
|
VStack(spacing: 0) {
|
|
Text("\(progress.visitedStadiums)")
|
|
.font(.title3)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
Text("/\(progress.totalStadiums)")
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
|
|
Text(viewModel.selectedSport.displayName)
|
|
.font(.headline)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
|
|
Text("Stadium Quest")
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
|
.accessibilityIdentifier("progress.stadiumQuest")
|
|
|
|
if progress.isComplete {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "checkmark.seal.fill")
|
|
Text("Complete!")
|
|
}
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
} else {
|
|
Text("\(progress.totalStadiums - progress.visitedStadiums) stadiums remaining")
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
|
|
// Stats row
|
|
HStack(spacing: Theme.Spacing.lg) {
|
|
ProgressStatPill(
|
|
icon: "mappin.circle.fill",
|
|
value: "\(progress.visitedStadiums)",
|
|
label: "Visited"
|
|
)
|
|
|
|
ProgressStatPill(
|
|
icon: "circle.dotted",
|
|
value: "\(progress.totalStadiums - progress.visitedStadiums)",
|
|
label: "Remaining"
|
|
)
|
|
|
|
ProgressStatPill(
|
|
icon: "percent",
|
|
value: String(format: "%.0f%%", progress.completionPercentage),
|
|
label: "Complete"
|
|
)
|
|
}
|
|
}
|
|
.padding(Theme.Spacing.lg)
|
|
.background(Theme.cardBackground(colorScheme))
|
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
|
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
|
}
|
|
.shadow(color: Theme.cardShadow(colorScheme), radius: 10, y: 5)
|
|
}
|
|
|
|
// MARK: - Stadium Lists Section
|
|
|
|
private var stadiumListsSection: some View {
|
|
VStack(alignment: .leading, spacing: Theme.Spacing.md) {
|
|
// Visited Stadiums
|
|
if !viewModel.visitedStadiums.isEmpty {
|
|
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
|
HStack {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
.accessibilityHidden(true)
|
|
Text("Visited (\(viewModel.visitedStadiums.count))")
|
|
.font(.body)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
}
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: Theme.Spacing.sm) {
|
|
ForEach(viewModel.visitedStadiums) { stadium in
|
|
StadiumChip(
|
|
stadium: stadium,
|
|
isVisited: true,
|
|
visitCount: viewModel.stadiumVisitStatus[stadium.id]?.visitCount ?? 1
|
|
) {
|
|
selectedStadium = stadium
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unvisited Stadiums
|
|
if !viewModel.unvisitedStadiums.isEmpty {
|
|
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
|
HStack {
|
|
Image(systemName: "circle.dotted")
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
.accessibilityHidden(true)
|
|
Text("Not Yet Visited (\(viewModel.unvisitedStadiums.count))")
|
|
.font(.body)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
}
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: Theme.Spacing.sm) {
|
|
ForEach(viewModel.unvisitedStadiums) { stadium in
|
|
StadiumChip(
|
|
stadium: stadium,
|
|
isVisited: false
|
|
) {
|
|
selectedStadium = stadium
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Achievements Section
|
|
|
|
private var achievementsSection: some View {
|
|
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
|
HStack {
|
|
Text("Achievements")
|
|
.font(.title2)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
.accessibilityIdentifier("progress.achievementsTitle")
|
|
|
|
Spacer()
|
|
|
|
NavigationLink {
|
|
AchievementsListView()
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Text("View All")
|
|
Image(systemName: "chevron.right")
|
|
.accessibilityHidden(true)
|
|
}
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
}
|
|
}
|
|
|
|
NavigationLink {
|
|
AchievementsListView()
|
|
} label: {
|
|
HStack(spacing: Theme.Spacing.md) {
|
|
// Trophy icon
|
|
ZStack {
|
|
Circle()
|
|
.fill(Theme.warmOrange.opacity(0.15))
|
|
.frame(width: 50, height: 50)
|
|
|
|
Image(systemName: "trophy.fill")
|
|
.font(.title3)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Track Your Progress")
|
|
.font(.body)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
|
|
Text("Earn badges for stadium visits")
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
.accessibilityHidden(true)
|
|
}
|
|
.padding(Theme.Spacing.md)
|
|
.background(Theme.cardBackground(colorScheme))
|
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
|
.stroke(Theme.warmOrange.opacity(0.3), lineWidth: 1)
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
// MARK: - Recent Visits Section
|
|
|
|
private var recentVisitsSection: some View {
|
|
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
|
HStack {
|
|
Text("Recent Visits")
|
|
.font(.title2)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
.accessibilityIdentifier("progress.recentVisitsTitle")
|
|
|
|
Spacer()
|
|
|
|
NavigationLink {
|
|
GamesHistoryView()
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Text("See All")
|
|
Image(systemName: "chevron.right")
|
|
.accessibilityHidden(true)
|
|
}
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
}
|
|
}
|
|
|
|
ForEach(viewModel.recentVisits) { visitSummary in
|
|
if let stadiumVisit = visitsById[visitSummary.id] {
|
|
NavigationLink {
|
|
VisitDetailView(visit: stadiumVisit, stadium: visitSummary.stadium)
|
|
} label: {
|
|
RecentVisitRow(visit: visitSummary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
} else {
|
|
RecentVisitRow(visit: visitSummary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Supporting Views
|
|
|
|
struct ProgressStatPill: View {
|
|
let icon: String
|
|
let value: String
|
|
let label: String
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
VStack(spacing: 4) {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: icon)
|
|
.font(.caption)
|
|
.accessibilityHidden(true)
|
|
Text(value)
|
|
.font(.body)
|
|
}
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
|
|
Text(label)
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
|
|
struct StadiumChip: View {
|
|
let stadium: Stadium
|
|
let isVisited: Bool
|
|
var visitCount: Int = 1
|
|
let action: () -> Void
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: Theme.Spacing.xs) {
|
|
if isVisited {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(.green)
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
HStack(spacing: 4) {
|
|
Text(stadium.name)
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
.lineLimit(1)
|
|
|
|
// Visit count badge (if more than 1)
|
|
if visitCount > 1 {
|
|
Text("\(visitCount)")
|
|
.font(.caption2.bold())
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(Theme.warmOrange, in: Capsule())
|
|
}
|
|
}
|
|
|
|
Text(stadium.city)
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
}
|
|
}
|
|
.padding(.horizontal, Theme.Spacing.sm)
|
|
.padding(.vertical, Theme.Spacing.xs)
|
|
.background(Theme.cardBackground(colorScheme))
|
|
.clipShape(Capsule())
|
|
.overlay {
|
|
Capsule()
|
|
.stroke(isVisited ? Color.green.opacity(0.3) : Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityElement(children: .combine)
|
|
}
|
|
}
|
|
|
|
struct RecentVisitRow: View {
|
|
let visit: VisitSummary
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
HStack(spacing: Theme.Spacing.md) {
|
|
// Sport icon
|
|
ZStack {
|
|
Circle()
|
|
.fill(visit.sport.themeColor.opacity(0.15))
|
|
.frame(width: 40, height: 40)
|
|
|
|
Image(systemName: visit.sport.iconName)
|
|
.foregroundStyle(visit.sport.themeColor)
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(visit.stadium.name)
|
|
.font(.body)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
|
|
// Date, Away @ Home on one line, left aligned
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(visit.shortDateDescription)
|
|
if let away = visit.awayTeamName, let home = visit.homeTeamName {
|
|
Text(away)
|
|
Text("@")
|
|
Text(home)
|
|
}
|
|
}
|
|
.font(.subheadline)
|
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
.accessibilityHidden(true)
|
|
}
|
|
.padding(Theme.Spacing.md)
|
|
.background(Theme.cardBackground(colorScheme))
|
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
|
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
|
}
|
|
.accessibilityElement(children: .combine)
|
|
}
|
|
}
|
|
|
|
struct StadiumDetailSheet: View {
|
|
let stadium: Stadium
|
|
let visitStatus: StadiumVisitStatus
|
|
let sport: Sport
|
|
var onVisitLogged: (() -> Void)?
|
|
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var showLogVisit = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: Theme.Spacing.lg) {
|
|
// Stadium header
|
|
VStack(spacing: Theme.Spacing.sm) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(sport.themeColor.opacity(0.15))
|
|
.frame(width: 80, height: 80)
|
|
|
|
Image(systemName: visitStatus.isVisited ? "checkmark.seal.fill" : sport.iconName)
|
|
.font(.largeTitle)
|
|
.foregroundStyle(visitStatus.isVisited ? .green : sport.themeColor)
|
|
}
|
|
|
|
Text(stadium.name)
|
|
.font(.headline)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(stadium.fullAddress)
|
|
.font(.body)
|
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
|
|
|
if visitStatus.isVisited {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
Text("Visited \(visitStatus.visitCount) time\(visitStatus.visitCount == 1 ? "" : "s")")
|
|
}
|
|
.font(.subheadline)
|
|
.foregroundStyle(.green)
|
|
}
|
|
}
|
|
|
|
// Visit history if visited
|
|
if case .visited(let visits) = visitStatus {
|
|
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
|
|
Text("Visit History")
|
|
.font(.body)
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
|
|
ForEach(visits.sorted(by: { $0.visitDate > $1.visitDate })) { visit in
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(visit.shortDateDescription)
|
|
.font(.subheadline)
|
|
if let matchup = visit.matchup {
|
|
Text(matchup)
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
|
}
|
|
}
|
|
Spacer()
|
|
Text(visit.visitType.displayName)
|
|
.font(.caption)
|
|
.foregroundStyle(Theme.textMuted(colorScheme))
|
|
}
|
|
.padding(Theme.Spacing.sm)
|
|
.background(Theme.cardBackgroundElevated(colorScheme))
|
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.small))
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Action button
|
|
Button {
|
|
showLogVisit = true
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: visitStatus.isVisited ? "plus" : "checkmark.circle")
|
|
Text(visitStatus.isVisited ? "Log Another Visit" : "Log Visit")
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(Theme.Spacing.md)
|
|
.background(Theme.warmOrange)
|
|
.foregroundStyle(.white)
|
|
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
|
}
|
|
.pressableStyle()
|
|
}
|
|
.padding(Theme.Spacing.lg)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Done") { dismiss() }
|
|
}
|
|
}
|
|
.sheet(isPresented: $showLogVisit) {
|
|
StadiumVisitSheet(
|
|
initialStadium: stadium,
|
|
initialSport: sport
|
|
) { _ in
|
|
onVisitLogged?()
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
ProgressTabView()
|
|
}
|
|
.modelContainer(for: StadiumVisit.self, inMemory: true)
|
|
}
|