refactor: extract reusable SportSelectorGrid component

Create unified sport selector grid used across Home (Quick Start),
Trip Creation, and Progress views. Removes duplicate button implementations
and ensures consistent grid layout with centered bottom row.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-11 10:38:10 -06:00
parent a292b5c20c
commit 475f444288
4 changed files with 321 additions and 211 deletions

View File

@@ -169,51 +169,15 @@ struct HomeView: View {
// MARK: - Quick Actions
private var quickActions: some View {
let sports = Sport.supported
let rows = sports.chunked(into: 4)
return VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Quick Start")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
GeometryReader { geometry in
let spacing = Theme.Spacing.sm
let buttonWidth = (geometry.size.width - 3 * spacing) / 4
VStack(spacing: Theme.Spacing.md) {
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
if row.count == 4 {
// Full row - evenly distributed
HStack(spacing: spacing) {
ForEach(row) { sport in
QuickSportButton(sport: sport) {
selectedSport = sport
showNewTrip = true
}
.frame(width: buttonWidth)
}
}
} else {
// Partial row - centered with same button width and spacing
HStack {
Spacer()
HStack(spacing: spacing) {
ForEach(row) { sport in
QuickSportButton(sport: sport) {
selectedSport = sport
showNewTrip = true
}
.frame(width: buttonWidth)
}
}
Spacer()
}
}
}
}
SportSelectorGrid { sport in
selectedSport = sport
showNewTrip = true
}
.frame(height: 180) // Approximate height for 2 rows
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
@@ -371,45 +335,6 @@ struct HomeView: View {
// MARK: - Supporting Views
struct QuickSportButton: View {
let sport: Sport
let action: () -> Void
@Environment(\.colorScheme) private var colorScheme
@State private var isPressed = false
var body: some View {
Button(action: action) {
VStack(spacing: 6) {
ZStack {
Circle()
.fill(sport.themeColor.opacity(0.15))
.frame(width: 48, height: 48)
Image(systemName: sport.iconName)
.font(.title3)
.foregroundStyle(sport.themeColor)
}
Text(sport.rawValue)
.font(.caption2)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
.frame(maxWidth: .infinity)
.scaleEffect(isPressed ? 0.9 : 1.0)
}
.buttonStyle(.plain)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
withAnimation(Theme.Animation.spring) { isPressed = true }
}
.onEnded { _ in
withAnimation(Theme.Animation.spring) { isPressed = false }
}
)
}
}
struct SavedTripCard: View {
let savedTrip: SavedTrip
let trip: Trip