// // LoadingTripsView.swift // SportsTime // // Animated loading state for suggested trips carousel. // import SwiftUI struct LoadingTripsView: View { let message: String @Environment(\.colorScheme) private var colorScheme @State private var animationPhase: Double = 0 var body: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { // Header HStack { Text("Featured Trips") .font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded)) .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() } // Loading message with animation HStack(spacing: Theme.Spacing.sm) { LoadingDots() Text(message) .font(.system(size: Theme.FontSize.body)) .foregroundStyle(Theme.textSecondary(colorScheme)) .lineLimit(2) .fixedSize(horizontal: false, vertical: true) } .padding(.vertical, Theme.Spacing.xs) // Placeholder cards ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: Theme.Spacing.md) { ForEach(0..<3, id: \.self) { index in PlaceholderCard(animationPhase: animationPhase, index: index) } } } } .onAppear { withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) { animationPhase = 1 } } } } // MARK: - Loading Dots struct LoadingDots: View { @State private var dotIndex = 0 var body: some View { HStack(spacing: 4) { ForEach(0..<3, id: \.self) { index in Circle() .fill(Theme.warmOrange) .frame(width: 6, height: 6) .opacity(index == dotIndex ? 1.0 : 0.3) } } .onAppear { Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { _ in withAnimation(.easeInOut(duration: 0.2)) { dotIndex = (dotIndex + 1) % 3 } } } } } // MARK: - Placeholder Card struct PlaceholderCard: View { let animationPhase: Double let index: Int @Environment(\.colorScheme) private var colorScheme var body: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { // Header placeholder HStack { shimmerRectangle(width: 60, height: 20) Spacer() shimmerRectangle(width: 40, height: 16) } // Route placeholder VStack(alignment: .leading, spacing: 4) { shimmerRectangle(width: 100, height: 14) shimmerRectangle(width: 20, height: 10) shimmerRectangle(width: 80, height: 14) } // Stats placeholder HStack(spacing: Theme.Spacing.sm) { shimmerRectangle(width: 70, height: 14) shimmerRectangle(width: 60, height: 14) } // Date placeholder shimmerRectangle(width: 120, height: 12) } .padding(Theme.Spacing.md) .frame(width: 200) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } } private func shimmerRectangle(width: CGFloat, height: CGFloat) -> some View { RoundedRectangle(cornerRadius: 4) .fill(shimmerGradient) .frame(width: width, height: height) } private var shimmerGradient: LinearGradient { let baseColor = Theme.textMuted(colorScheme).opacity(0.2) let highlightColor = Theme.textMuted(colorScheme).opacity(0.4) // Offset based on animation phase and index for staggered effect let offset = animationPhase + Double(index) * 0.2 return LinearGradient( colors: [baseColor, highlightColor, baseColor], startPoint: UnitPoint(x: offset - 0.5, y: 0), endPoint: UnitPoint(x: offset + 0.5, y: 0) ) } } #Preview { VStack { LoadingTripsView(message: "Hang tight, we're finding the best routes...") .padding() } }