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:
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user