Remove CBB (~5,000+ games per season) to reduce complexity. Changes: - Remove .cbb enum case from Sport - Remove CBB theme color (cbbMint) - Update documentation to reflect 7 supported leagues Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
223 lines
5.9 KiB
Swift
223 lines
5.9 KiB
Swift
//
|
|
// ViewModifiers.swift
|
|
// SportsTime
|
|
//
|
|
// Reusable view modifiers for consistent styling across the app.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
// MARK: - Card Style Modifier
|
|
|
|
struct CardStyle: ViewModifier {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var cornerRadius: CGFloat = Theme.CornerRadius.large
|
|
var padding: CGFloat = Theme.Spacing.lg
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.padding(padding)
|
|
.background(Theme.cardBackground(colorScheme))
|
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: cornerRadius)
|
|
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
|
}
|
|
.shadow(color: Theme.cardShadow(colorScheme), radius: 10, y: 5)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func cardStyle(cornerRadius: CGFloat = Theme.CornerRadius.large, padding: CGFloat = Theme.Spacing.lg) -> some View {
|
|
modifier(CardStyle(cornerRadius: cornerRadius, padding: padding))
|
|
}
|
|
}
|
|
|
|
// MARK: - Glow Effect Modifier
|
|
|
|
struct GlowEffect: ViewModifier {
|
|
var color: Color = Theme.warmOrange
|
|
var radius: CGFloat = 8
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.shadow(color: color.opacity(0.5), radius: radius / 2, y: 0)
|
|
.shadow(color: color.opacity(0.3), radius: radius, y: 0)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func glowEffect(color: Color = Theme.warmOrange, radius: CGFloat = 8) -> some View {
|
|
modifier(GlowEffect(color: color, radius: radius))
|
|
}
|
|
}
|
|
|
|
// MARK: - Pressable Button Style
|
|
|
|
struct PressableButtonStyle: ButtonStyle {
|
|
var scale: CGFloat = 0.96
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.scaleEffect(configuration.isPressed ? scale : 1.0)
|
|
.animation(Theme.Animation.spring, value: configuration.isPressed)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func pressableStyle(scale: CGFloat = 0.96) -> some View {
|
|
buttonStyle(PressableButtonStyle(scale: scale))
|
|
}
|
|
}
|
|
|
|
// MARK: - Shimmer Effect Modifier
|
|
|
|
struct ShimmerEffect: ViewModifier {
|
|
@State private var phase: CGFloat = 0
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.overlay {
|
|
GeometryReader { geo in
|
|
LinearGradient(
|
|
colors: [
|
|
.clear,
|
|
Color.white.opacity(0.3),
|
|
.clear
|
|
],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
.frame(width: geo.size.width * 2)
|
|
.offset(x: -geo.size.width + (geo.size.width * 2 * phase))
|
|
}
|
|
.mask(content)
|
|
}
|
|
.onAppear {
|
|
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
|
|
phase = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func shimmer() -> some View {
|
|
modifier(ShimmerEffect())
|
|
}
|
|
}
|
|
|
|
// MARK: - Staggered Animation Modifier
|
|
|
|
struct StaggeredAnimation: ViewModifier {
|
|
var index: Int
|
|
var delay: Double = Theme.Animation.staggerDelay
|
|
@State private var appeared = false
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.opacity(appeared ? 1 : 0)
|
|
.offset(y: appeared ? 0 : 20)
|
|
.onAppear {
|
|
withAnimation(Theme.Animation.spring.delay(Double(index) * delay)) {
|
|
appeared = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func staggeredAnimation(index: Int, delay: Double = Theme.Animation.staggerDelay) -> some View {
|
|
modifier(StaggeredAnimation(index: index, delay: delay))
|
|
}
|
|
}
|
|
|
|
// MARK: - Badge Style Modifier
|
|
|
|
struct BadgeStyle: ViewModifier {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var color: Color = Theme.warmOrange
|
|
var filled: Bool = true
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.font(.system(size: Theme.FontSize.micro, weight: .semibold))
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 5)
|
|
.background(filled ? color : color.opacity(0.2))
|
|
.foregroundStyle(filled ? .white : color)
|
|
.clipShape(Capsule())
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func badgeStyle(color: Color = Theme.warmOrange, filled: Bool = true) -> some View {
|
|
modifier(BadgeStyle(color: color, filled: filled))
|
|
}
|
|
}
|
|
|
|
// MARK: - Section Header Style
|
|
|
|
struct SectionHeaderStyle: ViewModifier {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
|
.foregroundStyle(Theme.textPrimary(colorScheme))
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func sectionHeaderStyle() -> some View {
|
|
modifier(SectionHeaderStyle())
|
|
}
|
|
}
|
|
|
|
// MARK: - Themed Background Modifier
|
|
|
|
struct ThemedBackground: ViewModifier {
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.background(Theme.backgroundGradient(colorScheme))
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func themedBackground() -> some View {
|
|
modifier(ThemedBackground())
|
|
}
|
|
}
|
|
|
|
// MARK: - Sport Color Bar
|
|
|
|
struct SportColorBar: View {
|
|
let sport: Sport
|
|
|
|
var body: some View {
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(sport.themeColor)
|
|
.frame(width: 4)
|
|
}
|
|
}
|
|
|
|
// MARK: - Sport Extension for Theme Colors
|
|
|
|
extension Sport {
|
|
var themeColor: Color {
|
|
switch self {
|
|
case .mlb: return Theme.mlbRed
|
|
case .nba: return Theme.nbaOrange
|
|
case .nhl: return Theme.nhlBlue
|
|
case .nfl: return Theme.nflBrown
|
|
case .mls: return Theme.mlsGreen
|
|
case .wnba: return Theme.wnbaPurple
|
|
case .nwsl: return Theme.nwslTeal
|
|
}
|
|
}
|
|
}
|