// // PollDetailView.swift // SportsTime // // View for displaying poll details and results // import SwiftUI struct PollDetailView: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme @State private var viewModel = PollDetailViewModel() @State private var showShareSheet = false @State private var showDeleteConfirmation = false @State private var showVotingSheet = false @State private var isOwner = false let pollId: UUID? let shareCode: String? init(pollId: UUID) { self.pollId = pollId self.shareCode = nil } init(shareCode: String) { self.pollId = nil self.shareCode = shareCode } var body: some View { Group { if viewModel.isLoading && viewModel.poll == nil { ProgressView("Loading poll...") } else if let poll = viewModel.poll { pollContent(poll) } else if let error = viewModel.error { ContentUnavailableView( "Poll Not Found", systemImage: "exclamationmark.triangle", description: Text(error.localizedDescription) ) } } .navigationTitle(viewModel.poll?.title ?? "Poll") .navigationBarTitleDisplayMode(.inline) .toolbar { if viewModel.poll != nil { ToolbarItem(placement: .primaryAction) { Menu { Button { showShareSheet = true } label: { Label("Share Poll", systemImage: "square.and.arrow.up") } if isOwner { Divider() Button(role: .destructive) { showDeleteConfirmation = true } label: { Label("Delete Poll", systemImage: "trash") } } } label: { Image(systemName: "ellipsis.circle") } } } } .refreshable { await viewModel.refresh() } .task { await loadPoll() isOwner = await viewModel.isOwner } .task(id: viewModel.poll?.id) { if viewModel.poll != nil { isOwner = await viewModel.isOwner } } .onDisappear { Task { await viewModel.cleanup() } } .sheet(isPresented: $showShareSheet) { if let url = viewModel.shareURL { ShareSheet(items: [url]) } } .sheet(isPresented: $showVotingSheet) { if let poll = viewModel.poll { PollVotingView(poll: poll, existingVote: viewModel.myVote) { Task { await viewModel.refresh() } } } } .confirmationDialog("Delete Poll", isPresented: $showDeleteConfirmation, titleVisibility: .visible) { Button("Delete", role: .destructive) { Task { if await viewModel.deletePoll() { dismiss() } } } Button("Cancel", role: .cancel) {} } message: { Text("This will permanently delete the poll and all votes. This action cannot be undone.") } } @ViewBuilder private func pollContent(_ poll: TripPoll) -> some View { ScrollView { VStack(spacing: 20) { // Share Code Card shareCodeCard(poll) // Voting Status votingStatusCard // Results if let results = viewModel.results { resultsSection(results) } // Trip Previews tripPreviewsSection(poll) } .padding() } } @ViewBuilder private func shareCodeCard(_ poll: TripPoll) -> some View { VStack(spacing: 8) { Text("Share Code") .font(.caption) .foregroundStyle(.secondary) Text(poll.shareCode) .font(.system(size: 32, weight: .bold, design: .monospaced)) .foregroundStyle(Theme.warmOrange) Text("sportstime://poll/\(poll.shareCode)") .font(.caption2) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity) .padding() .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 12)) } @ViewBuilder private var votingStatusCard: some View { HStack { VStack(alignment: .leading, spacing: 4) { Text(viewModel.hasVoted ? "You voted" : "You haven't voted yet") .font(.headline) Text("\(viewModel.votes.count) total votes") .font(.subheadline) .foregroundStyle(.secondary) } Spacer() Button(viewModel.hasVoted ? "Change Vote" : "Vote Now") { showVotingSheet = true } .buttonStyle(.borderedProminent) .tint(Theme.warmOrange) } .padding() .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 12)) } @ViewBuilder private func resultsSection(_ results: PollResults) -> some View { VStack(alignment: .leading, spacing: 12) { Text("Results") .font(.headline) ForEach(results.tripScores, id: \.tripIndex) { item in let trip = results.poll.tripSnapshots[item.tripIndex] ResultRow( rank: results.tripScores.firstIndex { $0.tripIndex == item.tripIndex }! + 1, tripName: trip.name, score: item.score, percentage: results.scorePercentage(for: item.tripIndex) ) } } .padding() .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 12)) } @ViewBuilder private func tripPreviewsSection(_ poll: TripPoll) -> some View { VStack(alignment: .leading, spacing: 12) { Text("Trip Options") .font(.headline) ForEach(Array(poll.tripSnapshots.enumerated()), id: \.element.id) { index, trip in TripPreviewCard(trip: trip, index: index + 1) } } } private func loadPoll() async { if let pollId { await viewModel.loadPoll(byId: pollId) } else if let shareCode { await viewModel.loadPoll(byShareCode: shareCode) } } } // MARK: - Result Row private struct ResultRow: View { let rank: Int let tripName: String let score: Int let percentage: Double var body: some View { HStack(spacing: 12) { Text("#\(rank)") .font(.headline) .foregroundStyle(rank == 1 ? Theme.warmOrange : .secondary) .frame(width: 30) VStack(alignment: .leading, spacing: 4) { Text(tripName) .font(.subheadline) GeometryReader { geometry in ZStack(alignment: .leading) { Rectangle() .fill(Color.secondary.opacity(0.2)) .frame(height: 8) .clipShape(RoundedRectangle(cornerRadius: 4)) Rectangle() .fill(rank == 1 ? Theme.warmOrange : Color.secondary) .frame(width: geometry.size.width * percentage, height: 8) .clipShape(RoundedRectangle(cornerRadius: 4)) } } .frame(height: 8) } Text("\(score)") .font(.caption) .foregroundStyle(.secondary) .frame(width: 40, alignment: .trailing) } } } // MARK: - Trip Preview Card private struct TripPreviewCard: View { @Environment(\.colorScheme) private var colorScheme let trip: Trip let index: Int var body: some View { 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.name) .font(.headline) } 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) // Show cities Text(trip.stops.map { $0.city }.joined(separator: " → ")) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } .padding() .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: 12)) } } #Preview { NavigationStack { PollDetailView(shareCode: "ABC123") } }