Fix game times with UTC data, restructure schedule by date
- Update games_canonical.json to use ISO 8601 UTC timestamps (game_datetime_utc) - Fix BootstrapService timezone-aware parsing for venue-local fallback - Fix thread-unsafe shared DateFormatter in RichGame local time display - Bump SchemaVersion to 4 to force re-bootstrap with correct UTC data - Restructure schedule view: group by date instead of sport, with sport icons on each row and date section headers showing game counts - Fix schedule row backgrounds using Theme.cardBackground instead of black - Sort games by UTC time with local-time tiebreaker for same-instant games Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,176 +0,0 @@
|
||||
//
|
||||
// CategoryPicker.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Polished category picker for custom itinerary items
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Category picker with labeled pills in a grid layout
|
||||
struct CategoryPicker: View {
|
||||
@Binding var selectedCategory: ItemCategory
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
/// Dynamic columns that adapt to accessibility text sizes
|
||||
private var columns: [GridItem] {
|
||||
[
|
||||
GridItem(.flexible(), spacing: Theme.Spacing.sm),
|
||||
GridItem(.flexible(), spacing: Theme.Spacing.sm),
|
||||
GridItem(.flexible(), spacing: Theme.Spacing.sm)
|
||||
]
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: columns, spacing: Theme.Spacing.sm) {
|
||||
ForEach(ItemCategory.allCases, id: \.self) { category in
|
||||
CategoryPill(
|
||||
category: category,
|
||||
isSelected: selectedCategory == category,
|
||||
colorScheme: colorScheme
|
||||
) {
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) {
|
||||
selectedCategory = category
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Individual category pill with emoji icon and label
|
||||
private struct CategoryPill: View {
|
||||
let category: ItemCategory
|
||||
let isSelected: Bool
|
||||
let colorScheme: ColorScheme
|
||||
let onTap: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onTap) {
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
// Emoji with background circle
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(emojiBackground)
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Text(category.icon)
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.shadow(
|
||||
color: isSelected ? Theme.warmOrange.opacity(0.3) : .clear,
|
||||
radius: 6,
|
||||
y: 2
|
||||
)
|
||||
|
||||
Text(category.label)
|
||||
.font(.caption)
|
||||
.fontWeight(isSelected ? .semibold : .medium)
|
||||
.foregroundStyle(labelColor)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, Theme.Spacing.md)
|
||||
.padding(.horizontal, Theme.Spacing.xs)
|
||||
.background(pillBackground)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
|
||||
.strokeBorder(borderColor, lineWidth: isSelected ? 2 : 1)
|
||||
)
|
||||
.shadow(
|
||||
color: isSelected ? Theme.warmOrange.opacity(0.15) : .clear,
|
||||
radius: 8,
|
||||
y: 4
|
||||
)
|
||||
}
|
||||
.buttonStyle(CategoryPillButtonStyle())
|
||||
.accessibilityLabel(category.label)
|
||||
.accessibilityHint("Category for \(category.label.lowercased()) items")
|
||||
.accessibilityAddTraits(isSelected ? [.isButton, .isSelected] : .isButton)
|
||||
}
|
||||
|
||||
private var emojiBackground: Color {
|
||||
if isSelected {
|
||||
return Theme.warmOrange.opacity(0.2)
|
||||
} else {
|
||||
return Theme.cardBackgroundElevated(colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
private var pillBackground: Color {
|
||||
if isSelected {
|
||||
return Theme.warmOrange.opacity(0.08)
|
||||
} else {
|
||||
return Theme.cardBackground(colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
if isSelected {
|
||||
return Theme.warmOrange
|
||||
} else {
|
||||
return Theme.surfaceGlow(colorScheme)
|
||||
}
|
||||
}
|
||||
|
||||
private var labelColor: Color {
|
||||
if isSelected {
|
||||
return Theme.warmOrange
|
||||
} else {
|
||||
return Theme.textSecondary(colorScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom button style with scale effect on press
|
||||
private struct CategoryPillButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||
.animation(Theme.Animation.prefersReducedMotion ? nil : .spring(response: 0.3, dampingFraction: 0.7), value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Light Mode") {
|
||||
struct PreviewWrapper: View {
|
||||
@State private var selected: ItemCategory = .restaurant
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
CategoryPicker(selectedCategory: $selected)
|
||||
.padding()
|
||||
|
||||
Text("Selected: \(selected.label)")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
}
|
||||
|
||||
return PreviewWrapper()
|
||||
.preferredColorScheme(.light)
|
||||
}
|
||||
|
||||
#Preview("Dark Mode") {
|
||||
struct PreviewWrapper: View {
|
||||
@State private var selected: ItemCategory = .attraction
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
CategoryPicker(selectedCategory: $selected)
|
||||
.padding()
|
||||
|
||||
Text("Selected: \(selected.label)")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
}
|
||||
|
||||
return PreviewWrapper()
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
Reference in New Issue
Block a user