// // PollVotingView.swift // SportsTime // // View for ranking trips in a poll // import SwiftUI struct PollVotingView: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme @State private var viewModel = PollVotingViewModel() @State private var showError = false let poll: TripPoll let existingVote: PollVote? var onVoteSubmitted: (() -> Void)? var body: some View { NavigationStack { VStack(spacing: 0) { // Instructions instructionsHeader // Reorderable list rankingList .listStyle(.plain) .scrollContentBackground(.hidden) .environment(\.editMode, .constant(.active)) // Submit button submitButton } .themedBackground() .navigationTitle("Rank Trips") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } .onAppear { viewModel.initializeRankings( tripCount: poll.tripSnapshots.count, existingVote: existingVote ) } .alert("Error", isPresented: $showError) { Button("OK") { viewModel.error = nil } } message: { if let error = viewModel.error { Text(error.localizedDescription) } } .onChange(of: viewModel.error != nil) { _, hasError in showError = hasError } .onChange(of: viewModel.didSubmit) { _, didSubmit in if didSubmit { onVoteSubmitted?() dismiss() } } } } private var rankingList: some View { List { ForEach(Array(viewModel.rankings.enumerated()), id: \.element) { index, tripIndex in if tripIndex < poll.tripSnapshots.count { RankingRow( rank: index + 1, trip: poll.tripSnapshots[tripIndex], canMoveUp: index > 0, canMoveDown: index < viewModel.rankings.count - 1, onMoveUp: { viewModel.moveTripUp(at: index) }, 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) } } } @ViewBuilder private var instructionsHeader: some View { VStack(spacing: 8) { Image(systemName: "arrow.up.arrow.down") .font(.title2) .foregroundStyle(Theme.warmOrange) .accessibilityLabel("Drag to reorder") 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(Theme.textSecondary(colorScheme)) } .padding() .frame(maxWidth: .infinity) .background(Theme.cardBackground(colorScheme)) } @ViewBuilder private var submitButton: some View { Button { Task { if let existingVote { await viewModel.updateVote(existingVote: existingVote) } else { await viewModel.submitVote(pollId: poll.id) } } } label: { HStack { if viewModel.isLoading { ProgressView() .tint(.white) } else { Text(existingVote != nil ? "Update Vote" : "Submit Vote") } } .frame(maxWidth: .infinity) .padding() .background(Theme.warmOrange) .foregroundStyle(.white) .font(.headline) } .disabled(viewModel.isLoading || !viewModel.canSubmit) } } // MARK: - Ranking Row private struct RankingRow: View { @Environment(\.colorScheme) private var colorScheme let rank: Int let trip: Trip let canMoveUp: Bool let canMoveDown: Bool let onMoveUp: () -> Void let onMoveDown: () -> Void var body: some View { HStack(spacing: 12) { // Rank badge Text("\(rank)") .font(.headline) .foregroundStyle(.white) .frame(width: 28, height: 28) .background(rankColor) .clipShape(Circle()) VStack(alignment: .leading, spacing: 2) { Text(trip.displayName) .font(.headline) .foregroundStyle(Theme.textPrimary(colorScheme)) Text(tripSummary) .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } Spacer() VStack(spacing: 6) { Button(action: onMoveUp) { Image(systemName: "chevron.up") .font(.caption.weight(.semibold)) } .minimumHitTarget() .disabled(!canMoveUp) .accessibilityLabel("Move \(trip.displayName) up") Button(action: onMoveDown) { Image(systemName: "chevron.down") .font(.caption.weight(.semibold)) } .minimumHitTarget() .disabled(!canMoveDown) .accessibilityLabel("Move \(trip.displayName) down") } .foregroundStyle(Theme.textSecondary(colorScheme)) } .padding(.vertical, 4) } private var rankColor: Color { switch rank { case 1: return Theme.warmOrange case 2: return .blue case 3: return .green default: return .secondary } } private var tripSummary: String { let cities = trip.stops.map { $0.city }.joined(separator: " → ") return cities } } #Preview { PollVotingView( poll: TripPoll( title: "Test Poll", ownerId: "test", tripSnapshots: [] ), existingVote: nil ) }