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

@@ -493,33 +493,14 @@ struct TripCreationView: View {
}
private var sportsSection: some View {
let sports = Sport.supported
let rows = sports.chunked(into: 4)
return ThemedSection(title: "Sports") {
VStack(spacing: Theme.Spacing.sm) {
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
HStack(spacing: Theme.Spacing.sm) {
ForEach(row) { sport in
SportSelectionChip(
sport: sport,
isSelected: viewModel.selectedSports.contains(sport),
onTap: {
if viewModel.selectedSports.contains(sport) {
viewModel.selectedSports.remove(sport)
} else {
viewModel.selectedSports.insert(sport)
}
}
)
}
// Fill remaining space if row has fewer than 4 items
if row.count < 4 {
ForEach(0..<(4 - row.count), id: \.self) { _ in
Color.clear.frame(maxWidth: .infinity)
}
}
}
ThemedSection(title: "Sports") {
SportSelectorGrid(
selectedSports: viewModel.selectedSports
) { sport in
if viewModel.selectedSports.contains(sport) {
viewModel.selectedSports.remove(sport)
} else {
viewModel.selectedSports.insert(sport)
}
}
.padding(.vertical, Theme.Spacing.xs)
@@ -2224,53 +2205,6 @@ struct DayCell: View {
}
}
struct SportSelectionChip: View {
let sport: Sport
let isSelected: Bool
let onTap: () -> Void
@Environment(\.colorScheme) private var colorScheme
@State private var isPressed = false
var body: some View {
Button(action: onTap) {
VStack(spacing: 6) {
ZStack {
Circle()
.fill(isSelected ? sport.themeColor : sport.themeColor.opacity(0.15))
.frame(width: 48, height: 48)
.overlay {
if isSelected {
Circle()
.stroke(sport.themeColor.opacity(0.3), lineWidth: 3)
.frame(width: 54, height: 54)
}
}
Image(systemName: sport.iconName)
.font(.title3)
.foregroundStyle(isSelected ? .white : sport.themeColor)
}
Text(sport.rawValue)
.font(.system(size: 10, weight: isSelected ? .semibold : .medium))
.foregroundStyle(isSelected ? Theme.textPrimary(colorScheme) : 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 }
}
)
}
}
#Preview {
TripCreationView(viewModel: TripCreationViewModel())
}