174 lines
4.9 KiB
Swift
174 lines
4.9 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 errorMessage: String?
|
|
@State private var showJoinPoll = false
|
|
@State private var joinCode = ""
|
|
@State private var pendingJoinCode: IdentifiableShareCode?
|
|
|
|
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") {
|
|
let normalizedCode = joinCode
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.uppercased()
|
|
|
|
guard normalizedCode.count == 6 else {
|
|
errorMessage = "Share code must be exactly 6 characters."
|
|
return
|
|
}
|
|
|
|
pendingJoinCode = IdentifiableShareCode(id: normalizedCode)
|
|
joinCode = ""
|
|
}
|
|
Button("Cancel", role: .cancel) {
|
|
joinCode = ""
|
|
}
|
|
} message: {
|
|
Text("Enter the 6-character poll code")
|
|
}
|
|
.navigationDestination(item: $pendingJoinCode) { code in
|
|
PollDetailView(shareCode: code.value)
|
|
}
|
|
.alert(
|
|
"Error",
|
|
isPresented: Binding(
|
|
get: { errorMessage != nil },
|
|
set: { if !$0 { errorMessage = nil } }
|
|
)
|
|
) {
|
|
Button("OK", role: .cancel) {
|
|
errorMessage = nil
|
|
}
|
|
} message: {
|
|
Text(errorMessage ?? "Please try again.")
|
|
}
|
|
}
|
|
|
|
@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
|
|
errorMessage = nil
|
|
|
|
do {
|
|
polls = try await PollService.shared.fetchMyPolls()
|
|
} catch let pollError as PollError {
|
|
errorMessage = pollError.localizedDescription
|
|
} catch {
|
|
self.errorMessage = PollError.unknown(error).localizedDescription
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|