feat(progress): add progress tracking enhancements
- Enable zoom/pan on progress map with reset button - Add visit count badges to stadium chips - Create GamesHistoryView with year grouping and sport filters - Create StadiumVisitHistoryView for viewing all visits to a stadium - Add VisitListCard and GamesHistoryRow components - Add "See All" navigation from Recent Visits to Games History - Add tests for map interactions, visit lists, and games history Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GamesHistoryRow: View {
|
||||
let visit: StadiumVisit
|
||||
let stadium: Stadium?
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
// Sport icon
|
||||
if let stadium {
|
||||
Image(systemName: sportIcon(for: stadium.sport))
|
||||
.font(.title3)
|
||||
.foregroundStyle(stadium.sport.themeColor)
|
||||
.frame(width: 32)
|
||||
}
|
||||
|
||||
// Visit info
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Date
|
||||
Text(visit.visitDate.formatted(date: .abbreviated, time: .omitted))
|
||||
.font(.subheadline.bold())
|
||||
|
||||
// Teams (if game)
|
||||
if let matchup = visit.matchupDescription {
|
||||
Text(matchup)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text(visit.stadiumNameAtVisit)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Chevron
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
private func sportIcon(for sport: Sport) -> String {
|
||||
switch sport {
|
||||
case .mlb: return "baseball"
|
||||
case .nba: return "basketball"
|
||||
case .nhl: return "hockey.puck"
|
||||
case .nfl: return "football"
|
||||
case .mls: return "soccerball"
|
||||
@unknown default: return "sportscourt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
Text("GamesHistoryRow Preview")
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import SwiftUI
|
||||
|
||||
struct VisitListCard: View {
|
||||
let visit: StadiumVisit
|
||||
@State private var isExpanded = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Header row (always visible)
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
isExpanded.toggle()
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
// Date
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(visit.visitDate.formatted(date: .abbreviated, time: .omitted))
|
||||
.font(.subheadline.bold())
|
||||
Text(visit.visitType.displayName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Game info preview (if available)
|
||||
if let matchup = visit.matchupDescription {
|
||||
Text(matchup)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
// Chevron
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.rotationEffect(.degrees(isExpanded ? 90 : 0))
|
||||
}
|
||||
.padding()
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// Expanded content
|
||||
if isExpanded {
|
||||
Divider()
|
||||
.padding(.horizontal)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// Game details
|
||||
if let matchup = visit.matchupDescription {
|
||||
GameInfoRow(matchup: matchup, score: visit.finalScore)
|
||||
}
|
||||
|
||||
// Seat location
|
||||
if let seat = visit.seatLocation, !seat.isEmpty {
|
||||
InfoRow(icon: "ticket", label: "Seat", value: seat)
|
||||
}
|
||||
|
||||
// Notes
|
||||
if let notes = visit.notes, !notes.isEmpty {
|
||||
InfoRow(icon: "note.text", label: "Notes", value: notes)
|
||||
}
|
||||
|
||||
// Photos count
|
||||
if let photos = visit.photoMetadata, !photos.isEmpty {
|
||||
InfoRow(
|
||||
icon: "photo.on.rectangle",
|
||||
label: "Photos",
|
||||
value: "\(photos.count) photo\(photos.count == 1 ? "" : "s")"
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||
}
|
||||
}
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
}
|
||||
|
||||
private struct GameInfoRow: View {
|
||||
let matchup: String
|
||||
let score: String?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(matchup)
|
||||
.font(.subheadline.bold())
|
||||
|
||||
if let score {
|
||||
Text("Final: \(score)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct InfoRow: View {
|
||||
let icon: String
|
||||
let label: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 16)
|
||||
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text(value)
|
||||
.font(.caption)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
Text("VisitListCard Preview")
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
Reference in New Issue
Block a user