Adds .contentShape(Rectangle()) or .contentShape(Capsule()) to 11 buttons, NavigationLinks, and onTapGesture handlers across 8 files where only the visible content (text/icons) was receiving taps instead of the full row. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
4.2 KiB
Swift
159 lines
4.2 KiB
Swift
//
|
|
// PollsListView.swift
|
|
// SportsTime
|
|
//
|
|
// View for listing user's polls
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct PollsListView: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@State private var polls: [TripPoll] = []
|
|
@State private var isLoading = false
|
|
@State private var hasLoadedInitially = false
|
|
@State private var error: PollError?
|
|
@State private var showJoinPoll = false
|
|
@State private var joinCode = ""
|
|
|
|
var body: some View {
|
|
Group {
|
|
if isLoading && polls.isEmpty {
|
|
ProgressView("Loading polls...")
|
|
} else if polls.isEmpty {
|
|
emptyState
|
|
} else {
|
|
pollsList
|
|
}
|
|
}
|
|
.navigationTitle("Group Polls")
|
|
.accessibilityIdentifier("polls.list")
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button {
|
|
showJoinPoll = true
|
|
} label: {
|
|
Image(systemName: "link.badge.plus")
|
|
.accessibilityLabel("Join a poll")
|
|
}
|
|
}
|
|
}
|
|
.refreshable {
|
|
await loadPolls()
|
|
}
|
|
.task {
|
|
guard !hasLoadedInitially else { return }
|
|
hasLoadedInitially = true
|
|
await loadPolls()
|
|
}
|
|
.alert("Join Poll", isPresented: $showJoinPoll) {
|
|
TextField("Enter code", text: $joinCode)
|
|
.textInputAutocapitalization(.characters)
|
|
Button("Join") {
|
|
// Navigation will be handled by deep link
|
|
if !joinCode.isEmpty {
|
|
// TODO: Navigate to poll detail
|
|
}
|
|
}
|
|
Button("Cancel", role: .cancel) {
|
|
joinCode = ""
|
|
}
|
|
} message: {
|
|
Text("Enter the 6-character poll code")
|
|
}
|
|
.alert("Error", isPresented: .constant(error != nil)) {
|
|
Button("OK") {
|
|
error = nil
|
|
}
|
|
} message: {
|
|
if let error {
|
|
Text(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var emptyState: some View {
|
|
ContentUnavailableView {
|
|
Label("No Polls", systemImage: "chart.bar.doc.horizontal")
|
|
} description: {
|
|
Text("Create a poll from your saved trips to let friends vote on which trip to take.")
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var pollsList: some View {
|
|
List {
|
|
ForEach(polls) { poll in
|
|
NavigationLink(value: poll) {
|
|
PollRowView(poll: poll)
|
|
.contentShape(Rectangle())
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
.navigationDestination(for: TripPoll.self) { poll in
|
|
PollDetailView(poll: poll)
|
|
}
|
|
}
|
|
|
|
private func loadPolls() async {
|
|
isLoading = true
|
|
error = nil
|
|
|
|
do {
|
|
polls = try await PollService.shared.fetchMyPolls()
|
|
} catch let pollError as PollError {
|
|
error = pollError
|
|
} catch {
|
|
self.error = .unknown(error)
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Poll Row View
|
|
|
|
private struct PollRowView: View {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let poll: TripPoll
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text(poll.title)
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
Text(poll.shareCode)
|
|
.font(.caption)
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(Theme.warmOrange)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(Theme.warmOrange.opacity(0.15))
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
HStack {
|
|
Label("\(poll.tripSnapshots.count) trips", systemImage: "map")
|
|
|
|
Spacer()
|
|
|
|
Text(poll.createdAt, style: .date)
|
|
}
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
PollsListView()
|
|
}
|
|
}
|