diff --git a/SportsTime/Core/Analytics/AnalyticsManager.swift b/SportsTime/Core/Analytics/AnalyticsManager.swift index 9b7ca19..601b57c 100644 --- a/SportsTime/Core/Analytics/AnalyticsManager.swift +++ b/SportsTime/Core/Analytics/AnalyticsManager.swift @@ -79,7 +79,6 @@ final class AnalyticsManager { } #if DEBUG - config.debug = true config.flushAt = 1 #endif diff --git a/SportsTime/Features/Home/Views/HomeView.swift b/SportsTime/Features/Home/Views/HomeView.swift index 644b6cc..a6309ee 100644 --- a/SportsTime/Features/Home/Views/HomeView.swift +++ b/SportsTime/Features/Home/Views/HomeView.swift @@ -450,6 +450,9 @@ struct SavedTripsListView: View { @State private var hasLoadedPolls = false @State private var showCreatePoll = false @State private var selectedPoll: TripPoll? + #if DEBUG + @State private var showDebugPoll = false + #endif /// Trips sorted by most cities (stops) first private var sortedTrips: [SavedTrip] { @@ -489,6 +492,13 @@ struct SavedTripsListView: View { .navigationDestination(for: TripPoll.self) { poll in PollDetailView(pollId: poll.id) } + #if DEBUG + .sheet(isPresented: $showDebugPoll) { + NavigationStack { + DebugPollPreviewView() + } + } + #endif } // MARK: - Polls Section @@ -519,7 +529,17 @@ struct SavedTripsListView: View { .frame(maxWidth: .infinity, alignment: .center) .padding() } else if polls.isEmpty { + #if DEBUG + // Debug sample poll + Button { + showDebugPoll = true + } label: { + PollRowCard(poll: DebugShareExporter.buildSamplePoll()) + } + .buttonStyle(.plain) + #else emptyPollsCard + #endif } else { ForEach(polls) { poll in NavigationLink(value: poll) { @@ -578,15 +598,16 @@ struct SavedTripsListView: View { VStack(spacing: 16) { Image(systemName: "suitcase") .font(.largeTitle) - .foregroundColor(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .accessibilityHidden(true) Text("No Saved Trips") .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Browse featured trips on the Home tab or create your own to get started.") .font(.subheadline) - .foregroundColor(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) diff --git a/SportsTime/Features/Polls/Views/PollCreationView.swift b/SportsTime/Features/Polls/Views/PollCreationView.swift index 6889c7e..fbff543 100644 --- a/SportsTime/Features/Polls/Views/PollCreationView.swift +++ b/SportsTime/Features/Polls/Views/PollCreationView.swift @@ -46,6 +46,8 @@ struct PollCreationView: View { } } } + .scrollContentBackground(.hidden) + .themedBackground() .navigationTitle("Create Poll") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -94,6 +96,7 @@ struct PollCreationView: View { // MARK: - Trip Selection Row private struct TripSelectionRow: View { + @Environment(\.colorScheme) private var colorScheme let trip: Trip let isSelected: Bool let onTap: () -> Void @@ -104,18 +107,18 @@ private struct TripSelectionRow: View { VStack(alignment: .leading, spacing: 4) { Text(trip.displayName) .font(.headline) - .foregroundStyle(.primary) + .foregroundStyle(Theme.textPrimary(colorScheme)) Text(tripSummary) .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .font(.title2) - .foregroundStyle(isSelected ? Theme.warmOrange : .secondary) + .foregroundStyle(isSelected ? Theme.warmOrange : Theme.textMuted(colorScheme)) .accessibilityHidden(true) } .contentShape(Rectangle()) diff --git a/SportsTime/Features/Polls/Views/PollDetailView.swift b/SportsTime/Features/Polls/Views/PollDetailView.swift index f562479..202a086 100644 --- a/SportsTime/Features/Polls/Views/PollDetailView.swift +++ b/SportsTime/Features/Polls/Views/PollDetailView.swift @@ -452,7 +452,7 @@ private struct TripPreviewCard: View { Text(trip.displayName) .font(.headline) - .foregroundStyle(.primary) + .foregroundStyle(Theme.textPrimary(colorScheme)) } // Date range and duration @@ -462,7 +462,7 @@ private struct TripPreviewCard: View { Label("\(trip.tripDuration) days", systemImage: "clock") } .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) HStack { Label("\(trip.stops.count) stops", systemImage: "mappin.and.ellipse") @@ -470,13 +470,13 @@ private struct TripPreviewCard: View { Label("\(trip.stops.flatMap { $0.games }.count) games", systemImage: "sportscourt") } .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } Image(systemName: "chevron.right") .font(.caption) .fontWeight(.semibold) - .foregroundStyle(.tertiary) + .foregroundStyle(Theme.textMuted(colorScheme)) .accessibilityHidden(true) } .padding() @@ -484,7 +484,7 @@ private struct TripPreviewCard: View { .clipShape(RoundedRectangle(cornerRadius: 12)) .overlay( RoundedRectangle(cornerRadius: 12) - .strokeBorder(Color.secondary.opacity(0.1), lineWidth: 1) + .strokeBorder(Theme.surfaceGlow(colorScheme), lineWidth: 1) ) } } diff --git a/SportsTime/Features/Polls/Views/PollVotingView.swift b/SportsTime/Features/Polls/Views/PollVotingView.swift index 5d4a6a4..2bce0a8 100644 --- a/SportsTime/Features/Polls/Views/PollVotingView.swift +++ b/SportsTime/Features/Polls/Views/PollVotingView.swift @@ -34,17 +34,21 @@ struct PollVotingView: View { onMoveDown: { viewModel.moveTripDown(at: index) } ) .accessibilityHint("Drag to change ranking position, or use move up and move down buttons") + .listRowBackground(Theme.cardBackground(colorScheme)) + .listRowSeparatorTint(Theme.surfaceGlow(colorScheme)) } .onMove { source, destination in viewModel.moveTrip(from: source, to: destination) } } .listStyle(.plain) + .scrollContentBackground(.hidden) .environment(\.editMode, .constant(.active)) // Submit button submitButton } + .themedBackground() .navigationTitle("Rank Trips") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -88,10 +92,11 @@ struct PollVotingView: View { Text("Drag to rank your preferences") .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Your top choice should be at the top. You can drag, or use the move buttons.") .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } .padding() .frame(maxWidth: .infinity) @@ -130,6 +135,7 @@ struct PollVotingView: View { // MARK: - Ranking Row private struct RankingRow: View { + @Environment(\.colorScheme) private var colorScheme let rank: Int let trip: Trip let canMoveUp: Bool @@ -150,10 +156,11 @@ private struct RankingRow: View { VStack(alignment: .leading, spacing: 2) { Text(trip.displayName) .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) Text(tripSummary) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) } Spacer() @@ -175,7 +182,7 @@ private struct RankingRow: View { .disabled(!canMoveDown) .accessibilityLabel("Move \(trip.displayName) down") } - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } .padding(.vertical, 4) } diff --git a/SportsTime/Features/Progress/Views/Components/GamesHistoryRow.swift b/SportsTime/Features/Progress/Views/Components/GamesHistoryRow.swift index 494ef1b..1db1a53 100644 --- a/SportsTime/Features/Progress/Views/Components/GamesHistoryRow.swift +++ b/SportsTime/Features/Progress/Views/Components/GamesHistoryRow.swift @@ -1,6 +1,7 @@ import SwiftUI struct GamesHistoryRow: View { + @Environment(\.colorScheme) private var colorScheme let visit: StadiumVisit let stadium: Stadium? @@ -20,16 +21,17 @@ struct GamesHistoryRow: View { // Date Text(visit.visitDate.formatted(date: .abbreviated, time: .omitted)) .font(.subheadline.bold()) + .foregroundStyle(Theme.textPrimary(colorScheme)) // Teams (if game) if let matchup = visit.matchupDescription { Text(matchup) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } else { Text(visit.stadiumNameAtVisit) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } } @@ -38,11 +40,11 @@ struct GamesHistoryRow: View { // Chevron Image(systemName: "chevron.right") .font(.caption) - .foregroundStyle(.tertiary) + .foregroundStyle(Theme.textMuted(colorScheme)) .accessibilityHidden(true) } .padding() - .background(Color(.systemBackground)) + .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 10)) .accessibilityElement(children: .combine) } diff --git a/SportsTime/Features/Progress/Views/Components/VisitListCard.swift b/SportsTime/Features/Progress/Views/Components/VisitListCard.swift index f57e381..5aa8bac 100644 --- a/SportsTime/Features/Progress/Views/Components/VisitListCard.swift +++ b/SportsTime/Features/Progress/Views/Components/VisitListCard.swift @@ -1,6 +1,7 @@ import SwiftUI struct VisitListCard: View { + @Environment(\.colorScheme) private var colorScheme let visit: StadiumVisit @State private var isExpanded = false @@ -17,9 +18,10 @@ struct VisitListCard: View { VStack(alignment: .leading, spacing: 2) { Text(visit.visitDate.formatted(date: .abbreviated, time: .omitted)) .font(.subheadline.bold()) + .foregroundStyle(Theme.textPrimary(colorScheme)) Text(visit.visitType.displayName) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() @@ -28,14 +30,14 @@ struct VisitListCard: View { if let matchup = visit.matchupDescription { Text(matchup) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) .lineLimit(1) } // Chevron Image(systemName: "chevron.right") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .rotationEffect(.degrees(isExpanded ? 90 : 0)) .accessibilityLabel(isExpanded ? "Collapse details" : "Expand details") } @@ -79,12 +81,13 @@ struct VisitListCard: View { .transition(.opacity.combined(with: .move(edge: .top))) } } - .background(Color(.systemBackground)) + .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 12)) } } private struct GameInfoRow: View { + @Environment(\.colorScheme) private var colorScheme let matchup: String let score: String? @@ -93,11 +96,12 @@ private struct GameInfoRow: View { VStack(alignment: .leading, spacing: 4) { Text(matchup) .font(.subheadline.bold()) + .foregroundStyle(Theme.textPrimary(colorScheme)) if let score { Text("Final: \(score)") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } } @@ -107,6 +111,7 @@ private struct GameInfoRow: View { } private struct InfoRow: View { + @Environment(\.colorScheme) private var colorScheme let icon: String let label: String let value: String @@ -115,16 +120,17 @@ private struct InfoRow: View { HStack(spacing: 8) { Image(systemName: icon) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .frame(width: 16) .accessibilityHidden(true) Text(label) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) Text(value) .font(.caption) + .foregroundStyle(Theme.textPrimary(colorScheme)) .lineLimit(2) } } diff --git a/SportsTime/Features/Progress/Views/GamesHistoryView.swift b/SportsTime/Features/Progress/Views/GamesHistoryView.swift index cf387d7..2713d5c 100644 --- a/SportsTime/Features/Progress/Views/GamesHistoryView.swift +++ b/SportsTime/Features/Progress/Views/GamesHistoryView.swift @@ -39,6 +39,7 @@ struct GamesHistoryView: View { } private struct GamesHistoryContent: View { + @Environment(\.colorScheme) private var colorScheme @Bindable var viewModel: GamesHistoryViewModel @Binding var selectedVisit: StadiumVisit? @@ -50,6 +51,7 @@ private struct GamesHistoryContent: View { HStack { Text("\(viewModel.totalGamesCount) Games") .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() if !viewModel.selectedSports.isEmpty { @@ -68,9 +70,10 @@ private struct GamesHistoryContent: View { ) } .padding() - .background(Color(.systemBackground)) + .background(Theme.cardBackground(colorScheme)) Divider() + .overlay(Theme.surfaceGlow(colorScheme)) // Games list grouped by year if viewModel.filteredVisits.isEmpty { @@ -85,7 +88,7 @@ private struct GamesHistoryContent: View { ) } } - .background(Color(.systemGroupedBackground)) + .background(Theme.backgroundGradient(colorScheme)) } } @@ -111,6 +114,7 @@ private struct SportFilterChips: View { } private struct SportChip: View { + @Environment(\.colorScheme) private var colorScheme let sport: Sport let isSelected: Bool let onTap: () -> Void @@ -124,12 +128,12 @@ private struct SportChip: View { Text(sport.rawValue) .font(.caption.bold()) } - .foregroundStyle(isSelected ? .white : .primary) + .foregroundStyle(isSelected ? .white : Theme.textPrimary(colorScheme)) .padding(.horizontal, 12) .padding(.vertical, 6) .background( Capsule() - .fill(isSelected ? sport.themeColor : Color(.systemGray5)) + .fill(isSelected ? sport.themeColor : Theme.cardBackgroundElevated(colorScheme)) ) } .buttonStyle(.plain) @@ -185,33 +189,38 @@ private struct GamesListByYear: View { } private struct YearHeader: View { + @Environment(\.colorScheme) private var colorScheme let year: Int var body: some View { HStack { Text(String(year)) .font(.title3.bold()) + .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() } .padding(.horizontal) .padding(.vertical, 8) - .background(Color(.systemGroupedBackground)) + .background(Theme.backgroundGradient(colorScheme)) } } private struct EmptyGamesView: View { + @Environment(\.colorScheme) private var colorScheme + var body: some View { VStack(spacing: 16) { Image(systemName: "ticket") .font(.largeTitle) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) Text("No games recorded yet") .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Add your first stadium visit to see it here!") .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/SportsTime/Features/Progress/Views/StadiumVisitHistoryView.swift b/SportsTime/Features/Progress/Views/StadiumVisitHistoryView.swift index de14cf6..53f51cb 100644 --- a/SportsTime/Features/Progress/Views/StadiumVisitHistoryView.swift +++ b/SportsTime/Features/Progress/Views/StadiumVisitHistoryView.swift @@ -65,6 +65,7 @@ struct StadiumVisitHistoryView: View { } private struct VisitHistoryList: View { + @Environment(\.colorScheme) private var colorScheme let visits: [StadiumVisit] var body: some View { @@ -74,6 +75,7 @@ private struct VisitHistoryList: View { HStack { Text("\(visits.count) Visit\(visits.count == 1 ? "" : "s")") .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() } .padding(.horizontal) @@ -86,23 +88,26 @@ private struct VisitHistoryList: View { } .padding(.vertical) } - .background(Color(.systemGroupedBackground)) + .background(Theme.backgroundGradient(colorScheme)) } } private struct EmptyVisitHistoryView: View { + @Environment(\.colorScheme) private var colorScheme + var body: some View { VStack(spacing: 16) { Image(systemName: "calendar.badge.plus") .font(.largeTitle) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) Text("No visits recorded") .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) Text("Tap + to add your first visit to this stadium") .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) .multilineTextAlignment(.center) } .padding() diff --git a/SportsTime/Features/Settings/DebugPollPreviewView.swift b/SportsTime/Features/Settings/DebugPollPreviewView.swift new file mode 100644 index 0000000..ac7ae3b --- /dev/null +++ b/SportsTime/Features/Settings/DebugPollPreviewView.swift @@ -0,0 +1,374 @@ +// +// DebugPollPreviewView.swift +// SportsTime +// +// Debug-only preview of the poll detail + voting flow with hardcoded data. +// + +#if DEBUG + +import SwiftUI + +struct DebugPollPreviewView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var colorScheme + @State private var showVotingSheet = false + @State private var selectedTrip: Trip? + + private let poll: TripPoll + private let votes: [PollVote] + + init() { + let p = DebugShareExporter.buildSamplePoll() + self.poll = p + self.votes = DebugShareExporter.buildSampleVotes(for: p) + } + + private var results: PollResults { + PollResults(poll: poll, votes: votes) + } + + var body: some View { + ScrollView { + VStack(spacing: Theme.Spacing.lg) { + // Share Code Card + shareCodeCard + + // Voting Status + votingStatusCard + + // Results + resultsSection + + // Trip Previews + tripPreviewsSection + } + .padding(.horizontal, Theme.Spacing.md) + .padding(.vertical, Theme.Spacing.lg) + } + .background(Theme.backgroundGradient(colorScheme)) + .navigationTitle(poll.title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Done") { dismiss() } + } + ToolbarItem(placement: .primaryAction) { + Button { + if let url = poll.shareURL as URL? { + UIPasteboard.general.string = url.absoluteString + } + } label: { + Image(systemName: "square.and.arrow.up") + } + } + } + .sheet(isPresented: $showVotingSheet) { + PollVotingView(poll: poll, existingVote: nil) + } + .sheet(item: $selectedTrip) { trip in + NavigationStack { + TripDetailView(trip: trip, allowCustomItems: true) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Done") { selectedTrip = nil } + } + } + } + } + } + + // MARK: - Share Code + + @ViewBuilder + private var shareCodeCard: some View { + VStack(spacing: Theme.Spacing.md) { + ZStack { + Circle() + .fill(Theme.warmOrange.opacity(0.15)) + .frame(width: 56, height: 56) + Image(systemName: "link.circle.fill") + .font(.title2) + .foregroundStyle(Theme.warmOrange) + } + + VStack(spacing: Theme.Spacing.xs) { + Text("Share Code") + .font(.subheadline) + .foregroundStyle(Theme.textSecondary(colorScheme)) + + Text(poll.shareCode) + .font(.system(.largeTitle, design: .monospaced).weight(.bold)) + .foregroundStyle(Theme.warmOrange) + .tracking(4) + .lineLimit(1) + .minimumScaleFactor(0.7) + } + + Button { + UIPasteboard.general.string = poll.shareCode + } label: { + Label("Copy Code", systemImage: "doc.on.doc") + .font(.subheadline.weight(.medium)) + .foregroundStyle(Theme.warmOrange) + .padding(.horizontal, Theme.Spacing.md) + .padding(.vertical, Theme.Spacing.sm) + .background(Theme.warmOrange.opacity(0.1)) + .clipShape(Capsule()) + } + } + .frame(maxWidth: .infinity) + .padding(Theme.Spacing.lg) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay( + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .strokeBorder(Theme.warmOrange.opacity(0.2), lineWidth: 1) + ) + } + + // MARK: - Voting Status + + @ViewBuilder + private var votingStatusCard: some View { + HStack(spacing: Theme.Spacing.md) { + ZStack { + Circle() + .fill(Theme.warmOrange.opacity(0.15)) + .frame(width: 44, height: 44) + Image(systemName: "hand.raised.fill") + .font(.title3) + .foregroundStyle(Theme.warmOrange) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Cast your vote") + .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + HStack(spacing: Theme.Spacing.xs) { + Image(systemName: "person.2.fill") + .font(.caption2) + Text("\(votes.count) votes") + .font(.subheadline) + } + .foregroundStyle(Theme.textSecondary(colorScheme)) + } + + Spacer() + + Button { + showVotingSheet = true + } label: { + Text("Vote") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.white) + .padding(.horizontal, Theme.Spacing.md) + .padding(.vertical, Theme.Spacing.sm) + .background(Theme.warmOrange) + .clipShape(Capsule()) + } + } + .padding(Theme.Spacing.md) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + } + + // MARK: - Results + + @ViewBuilder + private var resultsSection: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.md) { + HStack(spacing: Theme.Spacing.sm) { + Image(systemName: "chart.bar.fill") + .foregroundStyle(Theme.warmOrange) + Text("Results") + .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + } + + VStack(spacing: Theme.Spacing.sm) { + ForEach(results.tripScores, id: \.tripIndex) { item in + let trip = results.poll.tripSnapshots[item.tripIndex] + let rank = results.tripScores.firstIndex { $0.tripIndex == item.tripIndex }! + 1 + DebugResultRow( + rank: rank, + tripName: trip.displayName, + score: item.score, + percentage: results.scorePercentage(for: item.tripIndex), + isLeader: rank == 1 && item.score > 0 + ) + } + } + } + .padding(Theme.Spacing.md) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + } + + // MARK: - Trip Previews + + @ViewBuilder + private var tripPreviewsSection: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.md) { + HStack(spacing: Theme.Spacing.sm) { + Image(systemName: "map.fill") + .foregroundStyle(Theme.warmOrange) + Text("Trip Options") + .font(.headline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Spacer() + + Text("\(poll.tripSnapshots.count) trips") + .font(.caption) + .foregroundStyle(Theme.textMuted(colorScheme)) + } + + ForEach(Array(poll.tripSnapshots.enumerated()), id: \.element.id) { index, trip in + Button { + selectedTrip = trip + } label: { + DebugTripPreviewCard(trip: trip, index: index + 1) + } + .buttonStyle(.plain) + } + } + } +} + +// MARK: - Debug Result Row + +private struct DebugResultRow: View { + @Environment(\.colorScheme) private var colorScheme + let rank: Int + let tripName: String + let score: Int + let percentage: Double + var isLeader: Bool = false + + private var rankIcon: String { + switch rank { + case 1: return "trophy.fill" + case 2: return "medal.fill" + case 3: return "medal.fill" + default: return "\(rank).circle.fill" + } + } + + private var rankColor: Color { + switch rank { + case 1: return Theme.warmOrange + case 2: return .gray + case 3: return .brown + default: return .secondary + } + } + + var body: some View { + HStack(spacing: Theme.Spacing.sm) { + ZStack { + Circle() + .fill(rankColor.opacity(0.15)) + .frame(width: 36, height: 36) + Image(systemName: rankIcon) + .font(.subheadline) + .foregroundStyle(rankColor) + } + + VStack(alignment: .leading, spacing: 4) { + Text(tripName) + .font(.subheadline.weight(isLeader ? .semibold : .regular)) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + GeometryReader { geometry in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 4) + .fill(Theme.cardBackgroundElevated(colorScheme)) + .frame(height: 6) + + RoundedRectangle(cornerRadius: 4) + .fill(rankColor) + .frame(width: max(geometry.size.width * percentage, percentage > 0 ? 6 : 0), height: 6) + } + } + .frame(height: 6) + } + + Text("\(score)") + .font(.subheadline.weight(.medium).monospacedDigit()) + .foregroundStyle(rankColor) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(rankColor.opacity(0.1)) + .clipShape(Capsule()) + } + .padding(.vertical, Theme.Spacing.xs) + } +} + +// MARK: - Debug Trip Preview Card + +private struct DebugTripPreviewCard: View { + @Environment(\.colorScheme) private var colorScheme + let trip: Trip + let index: Int + + var body: some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Option \(index)") + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Theme.warmOrange) + .clipShape(Capsule()) + + Text(trip.displayName) + .font(.headline) + .foregroundStyle(.primary) + } + + HStack { + Label(trip.formattedDateRange, systemImage: "calendar") + Spacer() + Label("\(trip.tripDuration) days", systemImage: "clock") + } + .font(.caption) + .foregroundStyle(.secondary) + + HStack { + Label("\(trip.stops.count) stops", systemImage: "mappin.and.ellipse") + Spacer() + Label("\(trip.stops.flatMap { $0.games }.count) games", systemImage: "sportscourt") + } + .font(.caption) + .foregroundStyle(.secondary) + } + + Image(systemName: "chevron.right") + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.tertiary) + } + .padding() + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .strokeBorder(Color.secondary.opacity(0.1), lineWidth: 1) + ) + } +} + +#Preview { + NavigationStack { + DebugPollPreviewView() + } +} + +#endif diff --git a/SportsTime/Features/Settings/DebugShareExporter.swift b/SportsTime/Features/Settings/DebugShareExporter.swift index 4d53133..2d4e30b 100644 --- a/SportsTime/Features/Settings/DebugShareExporter.swift +++ b/SportsTime/Features/Settings/DebugShareExporter.swift @@ -381,6 +381,53 @@ final class DebugShareExporter { isExporting = false } + // MARK: - Save Sample Trips + + func saveSampleTrips(modelContext: ModelContext) { + let trips = Self.buildDummyTrips() + var savedCount = 0 + + for trip in trips { + if let savedTrip = SavedTrip.from(trip, status: .planned) { + modelContext.insert(savedTrip) + savedCount += 1 + } + } + + do { + try modelContext.save() + print("DEBUG: Saved \(savedCount) sample trips") + } catch { + print("DEBUG: Failed to save sample trips: \(error)") + } + } + + // MARK: - Build Sample Poll + + static func buildSamplePoll() -> TripPoll { + let trips = buildDummyTrips() + let sampleVotes = [ + PollVote(pollId: UUID(), odg: "voter1", rankings: [0, 2, 1, 3]), + PollVote(pollId: UUID(), odg: "voter2", rankings: [2, 0, 3, 1]), + PollVote(pollId: UUID(), odg: "voter3", rankings: [0, 1, 2, 3]), + ] + _ = sampleVotes // votes are shown via PollResults, we pass them separately + + return TripPoll( + title: "Summer 2026 Road Trip", + ownerId: "debug-user", + tripSnapshots: trips + ) + } + + static func buildSampleVotes(for poll: TripPoll) -> [PollVote] { + [ + PollVote(pollId: poll.id, odg: "voter-alex", rankings: [0, 2, 1, 3]), + PollVote(pollId: poll.id, odg: "voter-sam", rankings: [2, 0, 3, 1]), + PollVote(pollId: poll.id, odg: "voter-jordan", rankings: [0, 1, 2, 3]), + ] + } + // MARK: - Add All Stadium Visits func addAllStadiumVisits(modelContext: ModelContext) async { diff --git a/SportsTime/Features/Settings/Views/SettingsView.swift b/SportsTime/Features/Settings/Views/SettingsView.swift index 2f4997f..00185bf 100644 --- a/SportsTime/Features/Settings/Views/SettingsView.swift +++ b/SportsTime/Features/Settings/Views/SettingsView.swift @@ -19,6 +19,8 @@ struct SettingsView: View { @State private var selectedSyncStatus: EntitySyncStatus? @State private var exporter = DebugShareExporter() @State private var showExportProgress = false + @State private var showSamplePoll = false + @State private var sampleTripsMessage: String? #endif var body: some View { @@ -570,6 +572,19 @@ struct SettingsView: View { } label: { Label("Add All Stadium Visits", systemImage: "mappin.and.ellipse") } + + Button { + exporter.saveSampleTrips(modelContext: modelContext) + sampleTripsMessage = "Saved 4 sample trips!" + } label: { + Label("Save 4 Sample Trips", systemImage: "suitcase.fill") + } + + Button { + showSamplePoll = true + } label: { + Label("View Sample Poll", systemImage: "chart.bar.doc.horizontal.fill") + } } header: { Text("Debug") } footer: { @@ -579,6 +594,19 @@ struct SettingsView: View { .sheet(isPresented: $showExportProgress) { DebugExportProgressView(exporter: exporter) } + .sheet(isPresented: $showSamplePoll) { + NavigationStack { + DebugPollPreviewView() + } + } + .alert("Sample Trips", isPresented: Binding( + get: { sampleTripsMessage != nil }, + set: { if !$0 { sampleTripsMessage = nil } } + )) { + Button("OK", role: .cancel) { sampleTripsMessage = nil } + } message: { + Text(sampleTripsMessage ?? "") + } } private var syncStatusSection: some View { diff --git a/SportsTime/Features/Trip/Views/AddItem/CategoryPicker.swift b/SportsTime/Features/Trip/Views/AddItem/CategoryPicker.swift index bbfaa2b..a20dce3 100644 --- a/SportsTime/Features/Trip/Views/AddItem/CategoryPicker.swift +++ b/SportsTime/Features/Trip/Views/AddItem/CategoryPicker.swift @@ -93,9 +93,7 @@ private struct CategoryPill: View { if isSelected { return Theme.warmOrange.opacity(0.2) } else { - return colorScheme == .dark - ? Color.white.opacity(0.08) - : Color.black.opacity(0.04) + return Theme.cardBackgroundElevated(colorScheme) } } @@ -103,9 +101,7 @@ private struct CategoryPill: View { if isSelected { return Theme.warmOrange.opacity(0.08) } else { - return colorScheme == .dark - ? Color.white.opacity(0.03) - : Color.black.opacity(0.02) + return Theme.cardBackground(colorScheme) } } diff --git a/SportsTime/Features/Trip/Views/AddItemSheet.swift b/SportsTime/Features/Trip/Views/AddItemSheet.swift index ea30bd0..a392ae1 100644 --- a/SportsTime/Features/Trip/Views/AddItemSheet.swift +++ b/SportsTime/Features/Trip/Views/AddItemSheet.swift @@ -137,7 +137,7 @@ struct AddItemSheet: View { // Search field HStack { Image(systemName: "magnifyingglass") - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .accessibilityHidden(true) TextField("Search for a place...", text: $searchQuery) .textFieldStyle(.plain) @@ -155,14 +155,14 @@ struct AddItemSheet: View { selectedPlace = nil } label: { Image(systemName: "xmark.circle.fill") - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) } .minimumHitTarget() .accessibilityLabel("Clear search") } } .padding(10) - .background(Color(.systemGray6)) + .background(Theme.cardBackgroundElevated(colorScheme)) .cornerRadius(10) // Search results @@ -181,7 +181,7 @@ struct AddItemSheet: View { } } .frame(maxHeight: 300) - .background(Color(.systemBackground)) + .background(Theme.cardBackground(colorScheme)) .cornerRadius(10) } else if !searchQuery.isEmpty && !isSearching { Text("No results found") diff --git a/SportsTime/Features/Trip/Views/TimelineItemView.swift b/SportsTime/Features/Trip/Views/TimelineItemView.swift index a9297ea..16feb37 100644 --- a/SportsTime/Features/Trip/Views/TimelineItemView.swift +++ b/SportsTime/Features/Trip/Views/TimelineItemView.swift @@ -12,6 +12,7 @@ import SwiftUI /// Renders a single timeline item (stop, travel, or rest). struct TimelineItemView: View { + @Environment(\.colorScheme) private var colorScheme let item: TimelineItem let games: [String: RichGame] let isFirst: Bool @@ -69,7 +70,7 @@ struct TimelineItemView: View { } private var connectorColor: Color { - Color.secondary.opacity(0.3) + Theme.surfaceGlow(colorScheme) } @ViewBuilder @@ -124,6 +125,7 @@ struct TimelineItemView: View { // MARK: - Stop Item Content struct StopItemContent: View { + @Environment(\.colorScheme) private var colorScheme let stop: ItineraryStop let games: [String: RichGame] @@ -141,14 +143,14 @@ struct StopItemContent: View { if !stop.state.isEmpty { Text(stop.state) .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() Text(stop.arrivalDate.formatted(date: .abbreviated, time: .omitted)) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } // Games @@ -159,12 +161,12 @@ struct StopItemContent: View { } else { Text(stop.hasGames ? "Game details loading..." : "Waypoint") .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .italic() } } .padding() - .background(Color(.secondarySystemBackground)) + .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 12)) } } @@ -172,6 +174,7 @@ struct StopItemContent: View { // MARK: - Travel Item Content struct TravelItemContent: View { + @Environment(\.colorScheme) private var colorScheme let segment: TravelSegment var body: some View { @@ -182,25 +185,25 @@ struct TravelItemContent: View { .fontWeight(.medium) Text("\u{2022}") - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .accessibilityHidden(true) Text(segment.formattedDistance) .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) Text("\u{2022}") - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .accessibilityHidden(true) Text(segment.formattedDuration) .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } Text("\(segment.fromLocation.name) \u{2192} \(segment.toLocation.name)") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) .accessibilityLabel("\(segment.fromLocation.name) to \(segment.toLocation.name)") // EV Charging stops if applicable @@ -211,13 +214,13 @@ struct TravelItemContent: View { .accessibilityHidden(true) Text("\(segment.evChargingStops.count) charging stop(s)") .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } } } .padding(.vertical, 8) .padding(.horizontal, 12) - .background(Color(.tertiarySystemBackground)) + .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 8)) } } @@ -225,6 +228,7 @@ struct TravelItemContent: View { // MARK: - Rest Item Content struct RestItemContent: View { + @Environment(\.colorScheme) private var colorScheme let rest: RestDay var body: some View { @@ -238,17 +242,17 @@ struct RestItemContent: View { Text(rest.date.formatted(date: .abbreviated, time: .omitted)) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } Text(rest.location.name) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) if let notes = rest.notes { Text(notes) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .italic() } } @@ -262,6 +266,7 @@ struct RestItemContent: View { // MARK: - Timeline Game Row struct TimelineGameRow: View { + @Environment(\.colorScheme) private var colorScheme let richGame: RichGame var body: some View { @@ -286,7 +291,7 @@ struct TimelineGameRow: View { Text(richGame.stadium.name) } .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() @@ -325,6 +330,7 @@ struct TimelineView: View { /// Horizontal scrolling timeline for compact display. struct HorizontalTimelineView: View { + @Environment(\.colorScheme) private var colorScheme let option: ItineraryOption let games: [String: RichGame] @@ -355,19 +361,19 @@ struct HorizontalTimelineView: View { if item.isTravel { // Travel already shows direction, minimal connector Rectangle() - .fill(Color.secondary.opacity(0.3)) + .fill(Theme.surfaceGlow(colorScheme)) .frame(width: 20, height: 2) } else { // Standard connector with arrow HStack(spacing: 0) { Rectangle() - .fill(Color.secondary.opacity(0.3)) + .fill(Theme.surfaceGlow(colorScheme)) .frame(width: 16, height: 2) Image(systemName: "chevron.right") .font(.caption2) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) Rectangle() - .fill(Color.secondary.opacity(0.3)) + .fill(Theme.surfaceGlow(colorScheme)) .frame(width: 16, height: 2) } } @@ -377,6 +383,7 @@ struct HorizontalTimelineView: View { // MARK: - Horizontal Timeline Item View struct HorizontalTimelineItemView: View { + @Environment(\.colorScheme) private var colorScheme let item: TimelineItem let games: [String: RichGame] @@ -386,7 +393,7 @@ struct HorizontalTimelineItemView: View { Text(shortLabel) .font(.caption2) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) .lineLimit(1) .frame(width: 60) } @@ -405,7 +412,7 @@ struct HorizontalTimelineItemView: View { .fontWeight(.bold) } .frame(width: 44, height: 44) - .background(Circle().fill(Color(.secondarySystemBackground))) + .background(Circle().fill(Theme.cardBackgroundElevated(colorScheme))) case .travel(let segment): Image(systemName: segment.travelMode == .drive ? "car.fill" : "airplane") diff --git a/SportsTime/Features/Trip/Views/Wizard/Steps/LocationSearchSheet.swift b/SportsTime/Features/Trip/Views/Wizard/Steps/LocationSearchSheet.swift index d803395..36d34bd 100644 --- a/SportsTime/Features/Trip/Views/Wizard/Steps/LocationSearchSheet.swift +++ b/SportsTime/Features/Trip/Views/Wizard/Steps/LocationSearchSheet.swift @@ -24,6 +24,7 @@ struct LocationSearchSheet: View { let onAdd: (LocationInput) -> Void @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var colorScheme @State private var searchText = "" @State private var searchResults: [LocationSearchResult] = [] @State private var isSearching = false @@ -47,7 +48,7 @@ struct LocationSearchSheet: View { // Search field HStack { Image(systemName: "magnifyingglass") - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) .accessibilityHidden(true) TextField("Search cities, addresses, places...", text: $searchText) .textFieldStyle(.plain) @@ -60,14 +61,14 @@ struct LocationSearchSheet: View { searchResults = [] } label: { Image(systemName: "xmark.circle.fill") - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textMuted(colorScheme)) } .minimumHitTarget() .accessibilityLabel("Clear search") } } .padding() - .background(Color(.secondarySystemBackground)) + .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 10)) .padding() @@ -91,26 +92,28 @@ struct LocationSearchSheet: View { .accessibilityHidden(true) VStack(alignment: .leading) { Text(result.name) - .foregroundStyle(.primary) + .foregroundStyle(Theme.textPrimary(colorScheme)) if !result.address.isEmpty { Text(result.address) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(Theme.textSecondary(colorScheme)) } } Spacer() Image(systemName: "plus.circle") - .foregroundStyle(.blue) + .foregroundStyle(Theme.warmOrange) .accessibilityHidden(true) } } .buttonStyle(.plain) } .listStyle(.plain) + .scrollContentBackground(.hidden) } Spacer() } + .themedBackground() .navigationTitle(navigationTitle) .navigationBarTitleDisplayMode(.inline) .toolbar {