From 56869ce479578246821e7bf0ade7a109c0ff23f4 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 13 Jan 2026 14:44:30 -0600 Subject: [PATCH] feat(ui): add 23 home screen design variants with picker Add design style system with 23 unique home screen aesthetics: - Classic (original SportsTime design, now default) - 12 experimental styles (Brutalist, Luxury Editorial, etc.) - 10 polished app-inspired styles (Flighty, SeatGeek, Apple Maps, Things 3, Airbnb, Spotify, Nike Run Club, Fantastical, Strava, Carrot Weather) Includes settings picker to switch between styles and persists selection via UserDefaults. Co-Authored-By: Claude Opus 4.5 --- SportsTime/Core/Design/UIDesignStyle.swift | 190 +++++++ .../Home/Views/AdaptiveHomeContent.swift | 253 +++++++++ SportsTime/Features/Home/Views/HomeView.swift | 35 +- .../Variants/Airbnb/HomeContent_Airbnb.swift | 326 ++++++++++++ .../AppleMaps/HomeContent_AppleMaps.swift | 315 ++++++++++++ .../ArtDeco/HomeContent_ArtDeco.swift | 485 ++++++++++++++++++ .../Brutalist/HomeContent_Brutalist.swift | 272 ++++++++++ .../HomeContent_CarrotWeather.swift | 347 +++++++++++++ .../Classic/HomeContent_Classic.swift | 346 +++++++++++++ .../HomeContent_DarkIndustrial.swift | 427 +++++++++++++++ .../Fantastical/HomeContent_Fantastical.swift | 338 ++++++++++++ .../Flighty/HomeContent_Flighty.swift | 384 ++++++++++++++ .../HomeContent_Glassmorphism.swift | 338 ++++++++++++ .../HomeContent_LuxuryEditorial.swift | 339 ++++++++++++ .../HomeContent_MaximalistChaos.swift | 435 ++++++++++++++++ .../HomeContent_NeoBrutalist.swift | 321 ++++++++++++ .../NikeRunClub/HomeContent_NikeRunClub.swift | 298 +++++++++++ .../Organic/HomeContent_Organic.swift | 314 ++++++++++++ .../Playful/HomeContent_Playful.swift | 372 ++++++++++++++ .../HomeContent_RetroFuturism.swift | 357 +++++++++++++ .../SeatGeek/HomeContent_SeatGeek.swift | 364 +++++++++++++ .../SoftPastel/HomeContent_SoftPastel.swift | 391 ++++++++++++++ .../Spotify/HomeContent_Spotify.swift | 330 ++++++++++++ .../Variants/Strava/HomeContent_Strava.swift | 372 ++++++++++++++ .../HomeContent_SwissModernist.swift | 360 +++++++++++++ .../Things3/HomeContent_Things3.swift | 299 +++++++++++ .../Settings/Views/SettingsView.swift | 55 ++ 27 files changed, 8636 insertions(+), 27 deletions(-) create mode 100644 SportsTime/Core/Design/UIDesignStyle.swift create mode 100644 SportsTime/Features/Home/Views/AdaptiveHomeContent.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Airbnb/HomeContent_Airbnb.swift create mode 100644 SportsTime/Features/Home/Views/Variants/AppleMaps/HomeContent_AppleMaps.swift create mode 100644 SportsTime/Features/Home/Views/Variants/ArtDeco/HomeContent_ArtDeco.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Brutalist/HomeContent_Brutalist.swift create mode 100644 SportsTime/Features/Home/Views/Variants/CarrotWeather/HomeContent_CarrotWeather.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Classic/HomeContent_Classic.swift create mode 100644 SportsTime/Features/Home/Views/Variants/DarkIndustrial/HomeContent_DarkIndustrial.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Fantastical/HomeContent_Fantastical.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Flighty/HomeContent_Flighty.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Glassmorphism/HomeContent_Glassmorphism.swift create mode 100644 SportsTime/Features/Home/Views/Variants/LuxuryEditorial/HomeContent_LuxuryEditorial.swift create mode 100644 SportsTime/Features/Home/Views/Variants/MaximalistChaos/HomeContent_MaximalistChaos.swift create mode 100644 SportsTime/Features/Home/Views/Variants/NeoBrutalist/HomeContent_NeoBrutalist.swift create mode 100644 SportsTime/Features/Home/Views/Variants/NikeRunClub/HomeContent_NikeRunClub.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Organic/HomeContent_Organic.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Playful/HomeContent_Playful.swift create mode 100644 SportsTime/Features/Home/Views/Variants/RetroFuturism/HomeContent_RetroFuturism.swift create mode 100644 SportsTime/Features/Home/Views/Variants/SeatGeek/HomeContent_SeatGeek.swift create mode 100644 SportsTime/Features/Home/Views/Variants/SoftPastel/HomeContent_SoftPastel.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Spotify/HomeContent_Spotify.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Strava/HomeContent_Strava.swift create mode 100644 SportsTime/Features/Home/Views/Variants/SwissModernist/HomeContent_SwissModernist.swift create mode 100644 SportsTime/Features/Home/Views/Variants/Things3/HomeContent_Things3.swift diff --git a/SportsTime/Core/Design/UIDesignStyle.swift b/SportsTime/Core/Design/UIDesignStyle.swift new file mode 100644 index 0000000..741c99e --- /dev/null +++ b/SportsTime/Core/Design/UIDesignStyle.swift @@ -0,0 +1,190 @@ +// +// UIDesignStyle.swift +// SportsTime +// +// 22 distinctive aesthetic variants for the home screen. +// + +import SwiftUI + +/// Available UI design aesthetics for the home screen +enum UIDesignStyle: String, CaseIterable, Identifiable, Codable { + // Default + case classic = "Classic" + + // Original experimental aesthetics + case brutalist = "Brutalist" + case luxuryEditorial = "Luxury Editorial" + case retroFuturism = "Retro-Futurism" + case glassmorphism = "Glassmorphism" + case neoBrutalist = "Neo-Brutalist" + case organic = "Organic" + case maximalistChaos = "Maximalist" + case swissModernist = "Swiss Modern" + case playful = "Playful" + case artDeco = "Art Deco" + case darkIndustrial = "Industrial" + case softPastel = "Soft Pastel" + + // Polished app-inspired aesthetics + case flighty = "Flighty" + case seatGeek = "SeatGeek" + case appleMaps = "Apple Maps" + case things3 = "Things 3" + case airbnb = "Airbnb" + case spotify = "Spotify" + case nikeRunClub = "Nike Run Club" + case fantastical = "Fantastical" + case strava = "Strava" + case carrotWeather = "Carrot Weather" + + var id: String { rawValue } + + var description: String { + switch self { + case .classic: + return "The original SportsTime design" + case .brutalist: + return "Raw, unpolished anti-design rebellion" + case .luxuryEditorial: + return "Magazine-quality dramatic typography" + case .retroFuturism: + return "80s sci-fi meets modern sports tech" + case .glassmorphism: + return "Frosted glass, flowing ethereal shapes" + case .neoBrutalist: + return "Bold blocks, harsh shadows, high contrast" + case .organic: + return "Soft curves, earthy tones, breathing life" + case .maximalistChaos: + return "Dense, layered, gloriously overwhelming" + case .swissModernist: + return "Grid-obsessed clinical precision" + case .playful: + return "Bouncy, candy colors, pure delight" + case .artDeco: + return "1920s glamour with gold accents" + case .darkIndustrial: + return "Moody metallic dashboard aesthetic" + case .softPastel: + return "Light, airy, dreamy gradients" + case .flighty: + return "Aviation dashboard, data-rich elegance" + case .seatGeek: + return "Sports ticketing, vibrant modern cards" + case .appleMaps: + return "Native iOS, clean and familiar" + case .things3: + return "Ultra-clean, beautiful spacing" + case .airbnb: + return "Travel-focused, warm and inviting" + case .spotify: + return "Dark elegance, bold typography" + case .nikeRunClub: + return "Athletic stats, dynamic energy" + case .fantastical: + return "Calendar elegance, data-dense" + case .strava: + return "Athletic tracking, orange accent" + case .carrotWeather: + return "Bold personality, gradient skies" + } + } + + var iconName: String { + switch self { + case .classic: return "star.fill" + case .brutalist: return "hammer.fill" + case .luxuryEditorial: return "book.fill" + case .retroFuturism: return "tv.fill" + case .glassmorphism: return "drop.fill" + case .neoBrutalist: return "square.fill" + case .organic: return "leaf.fill" + case .maximalistChaos: return "sparkles" + case .swissModernist: return "grid" + case .playful: return "face.smiling.fill" + case .artDeco: return "diamond.fill" + case .darkIndustrial: return "gearshape.fill" + case .softPastel: return "cloud.fill" + case .flighty: return "airplane" + case .seatGeek: return "ticket.fill" + case .appleMaps: return "map.fill" + case .things3: return "checkmark.circle.fill" + case .airbnb: return "house.fill" + case .spotify: return "waveform" + case .nikeRunClub: return "figure.run" + case .fantastical: return "calendar" + case .strava: return "location.fill" + case .carrotWeather: return "sun.max.fill" + } + } + + var accentColor: Color { + switch self { + case .classic: return Color(red: 1.0, green: 0.45, blue: 0.2) // Warm Orange + case .brutalist: return .red + case .luxuryEditorial: return Color(red: 0.85, green: 0.65, blue: 0.13) // Gold + case .retroFuturism: return Color(red: 0.0, green: 1.0, blue: 0.8) // Cyan + case .glassmorphism: return Color(red: 0.6, green: 0.4, blue: 1.0) // Purple + case .neoBrutalist: return Color(red: 1.0, green: 0.8, blue: 0.0) // Yellow + case .organic: return Color(red: 0.4, green: 0.7, blue: 0.4) // Green + case .maximalistChaos: return Color(red: 1.0, green: 0.2, blue: 0.6) // Magenta + case .swissModernist: return .red + case .playful: return Color(red: 1.0, green: 0.4, blue: 0.6) // Pink + case .artDeco: return Color(red: 0.85, green: 0.65, blue: 0.13) // Gold + case .darkIndustrial: return Color(red: 1.0, green: 0.5, blue: 0.0) // Orange + case .softPastel: return Color(red: 0.7, green: 0.8, blue: 1.0) // Soft blue + case .flighty: return Color(red: 0.2, green: 0.5, blue: 1.0) // Blue + case .seatGeek: return Color(red: 0.85, green: 0.15, blue: 0.5) // Magenta + case .appleMaps: return Color(red: 0.0, green: 0.48, blue: 1.0) // Apple Blue + case .things3: return Color(red: 0.35, green: 0.6, blue: 0.95) // Things Blue + case .airbnb: return Color(red: 1.0, green: 0.22, blue: 0.4) // Airbnb Red + case .spotify: return Color(red: 0.12, green: 0.84, blue: 0.38) // Spotify Green + case .nikeRunClub: return Color(red: 0.77, green: 1.0, blue: 0.0) // Nike Volt + case .fantastical: return Color(red: 0.92, green: 0.26, blue: 0.26) // Fantastical Red + case .strava: return Color(red: 0.99, green: 0.32, blue: 0.15) // Strava Orange + case .carrotWeather: return Color(red: 1.0, green: 0.45, blue: 0.2) // Carrot Orange + } + } +} + +// MARK: - Design Style Manager + +@Observable +final class DesignStyleManager { + static let shared = DesignStyleManager() + + private let userDefaultsKey = "selectedUIDesignStyle" + + private(set) var currentStyle: UIDesignStyle { + didSet { + UserDefaults.standard.set(currentStyle.rawValue, forKey: userDefaultsKey) + } + } + + private init() { + if let savedValue = UserDefaults.standard.string(forKey: userDefaultsKey), + let style = UIDesignStyle(rawValue: savedValue) { + self.currentStyle = style + } else { + self.currentStyle = .classic // Default to original design + } + } + + func setStyle(_ style: UIDesignStyle) { + currentStyle = style + } +} + +// MARK: - Environment Key + +private struct DesignStyleKey: EnvironmentKey { + static let defaultValue: UIDesignStyle = .classic +} + +extension EnvironmentValues { + var designStyle: UIDesignStyle { + get { self[DesignStyleKey.self] } + set { self[DesignStyleKey.self] = newValue } + } +} diff --git a/SportsTime/Features/Home/Views/AdaptiveHomeContent.swift b/SportsTime/Features/Home/Views/AdaptiveHomeContent.swift new file mode 100644 index 0000000..b72db22 --- /dev/null +++ b/SportsTime/Features/Home/Views/AdaptiveHomeContent.swift @@ -0,0 +1,253 @@ +// +// AdaptiveHomeContent.swift +// SportsTime +// +// Routes to the appropriate home content variant based on the selected design style. +// + +import SwiftUI +import SwiftData + +struct AdaptiveHomeContent: View { + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + var body: some View { + switch DesignStyleManager.shared.currentStyle { + case .classic: + HomeContent_Classic( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .brutalist: + HomeContent_Brutalist( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .luxuryEditorial: + HomeContent_LuxuryEditorial( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .retroFuturism: + HomeContent_RetroFuturism( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .glassmorphism: + HomeContent_Glassmorphism( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .neoBrutalist: + HomeContent_NeoBrutalist( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .organic: + HomeContent_Organic( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .maximalistChaos: + HomeContent_MaximalistChaos( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .swissModernist: + HomeContent_SwissModernist( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .playful: + HomeContent_Playful( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .artDeco: + HomeContent_ArtDeco( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .darkIndustrial: + HomeContent_DarkIndustrial( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .softPastel: + HomeContent_SoftPastel( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .flighty: + HomeContent_Flighty( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .seatGeek: + HomeContent_SeatGeek( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .appleMaps: + HomeContent_AppleMaps( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .things3: + HomeContent_Things3( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .airbnb: + HomeContent_Airbnb( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .spotify: + HomeContent_Spotify( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .nikeRunClub: + HomeContent_NikeRunClub( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .fantastical: + HomeContent_Fantastical( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .strava: + HomeContent_Strava( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + + case .carrotWeather: + HomeContent_CarrotWeather( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) + } + } +} diff --git a/SportsTime/Features/Home/Views/HomeView.swift b/SportsTime/Features/Home/Views/HomeView.swift index ad61b3e..4356f06 100644 --- a/SportsTime/Features/Home/Views/HomeView.swift +++ b/SportsTime/Features/Home/Views/HomeView.swift @@ -23,33 +23,14 @@ struct HomeView: View { TabView(selection: $selectedTab) { // Home Tab NavigationStack { - ScrollView { - VStack(spacing: Theme.Spacing.xl) { - // Hero Card - heroCard - .staggeredAnimation(index: 0) - - // Quick Actions - quickActions - .staggeredAnimation(index: 1) - - // Suggested Trips - suggestedTripsSection - .staggeredAnimation(index: 2) - - // Saved Trips - if !savedTrips.isEmpty { - savedTripsSection - .staggeredAnimation(index: 3) - } - - // Featured / Tips - tipsSection - .staggeredAnimation(index: 4) - } - .padding(Theme.Spacing.md) - } - .themedBackground() + AdaptiveHomeContent( + showNewTrip: $showNewTrip, + selectedTab: $selectedTab, + selectedSuggestedTrip: $selectedSuggestedTrip, + savedTrips: savedTrips, + suggestedTripsGenerator: suggestedTripsGenerator, + displayedTips: displayedTips + ) .toolbar { ToolbarItem(placement: .primaryAction) { Button { diff --git a/SportsTime/Features/Home/Views/Variants/Airbnb/HomeContent_Airbnb.swift b/SportsTime/Features/Home/Views/Variants/Airbnb/HomeContent_Airbnb.swift new file mode 100644 index 0000000..31371e8 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Airbnb/HomeContent_Airbnb.swift @@ -0,0 +1,326 @@ +// +// HomeContent_Airbnb.swift +// SportsTime +// +// AIRBNB-INSPIRED: Travel-focused, warm aesthetic. +// Experience cards, inviting colors, rounded corners. +// Focus on discovery and exploration. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Airbnb: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Airbnb-inspired colors + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.08, green: 0.08, blue: 0.08) + : Color.white + } + + private let airbnbRed = Color(red: 1.0, green: 0.22, blue: 0.4) + private let warmGray = Color(red: 0.45, green: 0.42, blue: 0.4) + + private var textPrimary: Color { + colorScheme == .dark ? .white : Color(red: 0.14, green: 0.14, blue: 0.14) + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.55) : Color(red: 0.45, green: 0.45, blue: 0.45) + } + + var body: some View { + ScrollView { + VStack(spacing: 28) { + // Search bar + searchBar + .padding(.horizontal, 20) + .padding(.top, 8) + + // Categories + categoriesSection + + // Your trips + if !savedTrips.isEmpty { + yourTripsSection + } + + // Explore section + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + exploreSection + } + + Spacer(minLength: 40) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Search Bar + + private var searchBar: some View { + Button { + showNewTrip = true + } label: { + HStack(spacing: 14) { + Image(systemName: "magnifyingglass") + .font(.system(size: 18, weight: .medium)) + .foregroundStyle(textPrimary) + + VStack(alignment: .leading, spacing: 2) { + Text("Where to?") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(textPrimary) + + Text("Anywhere • Any week • Any sport") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + + Spacer() + + // Filter button + Circle() + .stroke(Color.gray.opacity(0.3), lineWidth: 1) + .frame(width: 36, height: 36) + .overlay( + Image(systemName: "slider.horizontal.3") + .font(.system(size: 14)) + .foregroundStyle(textPrimary) + ) + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 40) + .fill(colorScheme == .dark ? Color(white: 0.15) : Color.white) + .shadow(color: Color.black.opacity(0.12), radius: 12, y: 4) + ) + } + .buttonStyle(.plain) + } + + // MARK: - Categories Section + + private var categoriesSection: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 28) { + ForEach(Sport.supported) { sport in + categoryItem(sport) + } + } + .padding(.horizontal, 20) + } + } + + private func categoryItem(_ sport: Sport) -> some View { + Button { + showNewTrip = true + } label: { + VStack(spacing: 8) { + Image(systemName: sport.iconName) + .font(.system(size: 24)) + .foregroundStyle(textSecondary) + + Text(sport.displayName) + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + .frame(width: 56) + } + .buttonStyle(.plain) + } + + // MARK: - Your Trips Section + + private var yourTripsSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Your Trips") + .font(.system(size: 22, weight: .semibold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("Show all") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(textPrimary) + .underline() + } + } + .padding(.horizontal, 20) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(savedTrips.prefix(4)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + tripCard(trip) + } + .buttonStyle(.plain) + } + } + } + .padding(.horizontal, 20) + } + } + } + + private func tripCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 10) { + // Image placeholder with gradient + ZStack { + RoundedRectangle(cornerRadius: 12) + .fill( + LinearGradient( + colors: [ + trip.uniqueSports.first?.themeColor ?? warmGray, + (trip.uniqueSports.first?.themeColor ?? warmGray).opacity(0.6) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(height: 140) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 36)) + .foregroundStyle(.white.opacity(0.9)) + + // Favorite heart + VStack { + HStack { + Spacer() + Image(systemName: "heart") + .font(.system(size: 16)) + .foregroundStyle(.white) + .padding(10) + } + Spacer() + } + } + + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(trip.name) + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Spacer() + + HStack(spacing: 2) { + Image(systemName: "star.fill") + .font(.system(size: 11)) + Text("New") + .font(.system(size: 12)) + } + .foregroundStyle(textPrimary) + } + + Text("\(trip.stops.count) cities • \(trip.totalGames) games") + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + + Text(trip.formattedDateRange) + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + } + } + .frame(width: 240) + } + + // MARK: - Explore Section + + private var exploreSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Explore Routes") + .font(.system(size: 22, weight: .semibold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14)) + .foregroundStyle(textSecondary) + } + } + .padding(.horizontal, 20) + + VStack(spacing: 20) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(3)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + exploreCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 20) + } + } + + private func exploreCard(_ trip: Trip) -> some View { + HStack(spacing: 14) { + // Small image + RoundedRectangle(cornerRadius: 10) + .fill( + LinearGradient( + colors: [ + trip.uniqueSports.first?.themeColor ?? warmGray, + (trip.uniqueSports.first?.themeColor ?? warmGray).opacity(0.7) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 80, height: 80) + .overlay( + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 24)) + .foregroundStyle(.white.opacity(0.9)) + ) + + VStack(alignment: .leading, spacing: 6) { + Text(trip.name) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) stops") + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + + HStack(spacing: 4) { + Image(systemName: "sportscourt.fill") + .font(.system(size: 11)) + Text("\(trip.totalGames) games included") + .font(.system(size: 13)) + } + .foregroundStyle(textSecondary) + } + + Spacer() + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/AppleMaps/HomeContent_AppleMaps.swift b/SportsTime/Features/Home/Views/Variants/AppleMaps/HomeContent_AppleMaps.swift new file mode 100644 index 0000000..25280a5 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/AppleMaps/HomeContent_AppleMaps.swift @@ -0,0 +1,315 @@ +// +// HomeContent_AppleMaps.swift +// SportsTime +// +// APPLE MAPS-INSPIRED: Native iOS aesthetic. +// Clean cards, location-focused, subtle shadows. +// Professional and polished feel. +// + +import SwiftUI +import SwiftData + +struct HomeContent_AppleMaps: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Apple Maps-inspired colors + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.0, green: 0.0, blue: 0.0) + : Color(red: 0.95, green: 0.95, blue: 0.97) + } + + private var cardBg: Color { + colorScheme == .dark + ? Color(red: 0.11, green: 0.11, blue: 0.12) + : Color.white + } + + private let mapsBlue = Color(red: 0.0, green: 0.48, blue: 1.0) + private let mapsGreen = Color(red: 0.2, green: 0.78, blue: 0.35) + + private var textPrimary: Color { + colorScheme == .dark ? .white : .black + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.6) : Color(white: 0.4) + } + + var body: some View { + ScrollView { + VStack(spacing: 16) { + // Search-style action card + searchCard + .padding(.horizontal, 16) + .padding(.top, 8) + + // Recents section + if !savedTrips.isEmpty { + recentsSection + } + + // Guides section (suggested trips) + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + guidesSection + } + + // Explore section + exploreSection + .padding(.horizontal, 16) + .padding(.bottom, 32) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Search Card + + private var searchCard: some View { + Button { + showNewTrip = true + } label: { + HStack(spacing: 12) { + Image(systemName: "magnifyingglass") + .font(.system(size: 17)) + .foregroundStyle(textSecondary) + + Text("Plan a trip or search destinations") + .font(.system(size: 17)) + .foregroundStyle(textSecondary) + + Spacer() + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.08), radius: 8, y: 2) + ) + } + .buttonStyle(.plain) + } + + // MARK: - Recents Section + + private var recentsSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("Recents") + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("See All") + .font(.system(size: 15)) + .foregroundStyle(mapsBlue) + } + } + .padding(.horizontal, 16) + + VStack(spacing: 0) { + ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + recentRow(trip, isLast: index == min(2, savedTrips.count - 1)) + } + .buttonStyle(.plain) + } + } + } + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + ) + .padding(.horizontal, 16) + } + } + + private func recentRow(_ trip: Trip, isLast: Bool) -> some View { + VStack(spacing: 0) { + HStack(spacing: 14) { + // Location pin icon + ZStack { + Circle() + .fill(mapsBlue.opacity(0.15)) + .frame(width: 36, height: 36) + + Image(systemName: "mappin.circle.fill") + .font(.system(size: 20)) + .foregroundStyle(mapsBlue) + } + + VStack(alignment: .leading, spacing: 3) { + Text(trip.name) + .font(.system(size: 16)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) stops • \(trip.totalGames) games") + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(Color(white: 0.75)) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + if !isLast { + Divider() + .padding(.leading, 66) + } + } + } + + // MARK: - Guides Section + + private var guidesSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("Suggested Routes") + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 15)) + .foregroundStyle(mapsBlue) + } + } + .padding(.horizontal, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + guideCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 16) + } + } + } + + private func guideCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 10) { + // Header with icon + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill( + LinearGradient( + colors: [mapsGreen, mapsGreen.opacity(0.8)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(height: 80) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 28)) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(2) + .multilineTextAlignment(.leading) + + Text("\(trip.stops.count) places") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + .padding(.horizontal, 10) + .padding(.bottom, 10) + } + .frame(width: 150) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.06), radius: 6, y: 2) + ) + } + + // MARK: - Explore Section + + private var exploreSection: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Explore") + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(textPrimary) + + VStack(spacing: 0) { + ForEach(Array(Sport.supported.prefix(4).enumerated()), id: \.element.id) { index, sport in + Button { + showNewTrip = true + } label: { + exploreRow(sport, isLast: index == min(3, Sport.supported.count - 1)) + } + .buttonStyle(.plain) + } + } + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + ) + } + } + + private func exploreRow(_ sport: Sport, isLast: Bool) -> some View { + VStack(spacing: 0) { + HStack(spacing: 14) { + Image(systemName: sport.iconName) + .font(.system(size: 18)) + .foregroundStyle(sport.themeColor) + .frame(width: 28) + + Text(sport.displayName) + .font(.system(size: 16)) + .foregroundStyle(textPrimary) + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(Color(white: 0.75)) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + + if !isLast { + Divider() + .padding(.leading, 58) + } + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/ArtDeco/HomeContent_ArtDeco.swift b/SportsTime/Features/Home/Views/Variants/ArtDeco/HomeContent_ArtDeco.swift new file mode 100644 index 0000000..e4718b4 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/ArtDeco/HomeContent_ArtDeco.swift @@ -0,0 +1,485 @@ +// +// HomeContent_ArtDeco.swift +// SportsTime +// +// ART DECO: 1920s glamour, geometric patterns, gold/black elegance. +// Sunburst motifs, stepped shapes, vintage stadium marquee vibes. +// + +import SwiftUI +import SwiftData + +struct HomeContent_ArtDeco: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Art Deco palette + private let decoGold = Color(red: 0.85, green: 0.7, blue: 0.35) + private let decoTeal = Color(red: 0.0, green: 0.5, blue: 0.5) + private let decoCream = Color(red: 0.98, green: 0.95, blue: 0.88) + + private var bgColor: Color { + colorScheme == .dark ? Color(red: 0.08, green: 0.06, blue: 0.1) : decoCream + } + + private var textPrimary: Color { + colorScheme == .dark ? decoCream : Color(red: 0.1, green: 0.08, blue: 0.06) + } + + private var textSecondary: Color { + colorScheme == .dark ? decoCream.opacity(0.6) : Color(red: 0.1, green: 0.08, blue: 0.06).opacity(0.6) + } + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + // Deco pattern overlay + decoPatternOverlay + + ScrollView { + VStack(spacing: 0) { + // DECO MARQUEE HEADER + decoMarquee + .padding(.top, 24) + + // DECO HERO + decoHero + .padding(.top, 32) + .padding(.horizontal, 24) + + // FEATURED TRIPS + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.top, 48) + .padding(.horizontal, 24) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.top, 48) + .padding(.horizontal, 24) + } + + // DECO FOOTER + decoFooter + .padding(.top, 56) + .padding(.bottom, 32) + } + } + } + } + + // MARK: - Deco Pattern Overlay + + private var decoPatternOverlay: some View { + GeometryReader { geo in + // Corner fan patterns + ZStack { + // Top corners + decoFan + .frame(width: 120, height: 120) + .position(x: 0, y: 0) + + decoFan + .frame(width: 120, height: 120) + .scaleEffect(x: -1) + .position(x: geo.size.width, y: 0) + + // Vertical lines accent + HStack(spacing: 40) { + ForEach(0..<5, id: \.self) { _ in + Rectangle() + .fill(decoGold.opacity(0.08)) + .frame(width: 1) + } + } + .frame(height: geo.size.height) + } + } + .allowsHitTesting(false) + } + + private var decoFan: some View { + ZStack { + ForEach(0..<5, id: \.self) { i in + Rectangle() + .fill(decoGold.opacity(0.1)) + .frame(width: 2, height: 80) + .rotationEffect(.degrees(Double(i) * 15 - 30)) + .offset(y: -40) + } + } + } + + // MARK: - Deco Marquee + + private var decoMarquee: some View { + VStack(spacing: 0) { + // Top decorative border + HStack(spacing: 8) { + decoCorner + Rectangle() + .fill(decoGold) + .frame(height: 2) + decoCorner + .scaleEffect(x: -1) + } + .frame(height: 20) + .padding(.horizontal, 16) + + // Marquee content + VStack(spacing: 4) { + Text("★ SPORTS TIME ★") + .font(.system(size: 12, weight: .bold)) + .tracking(6) + .foregroundStyle(decoGold) + + Text("EST. MMXXVI") + .font(.system(size: 9, weight: .medium)) + .tracking(4) + .foregroundStyle(textSecondary) + } + .padding(.vertical, 12) + + // Bottom decorative border + HStack(spacing: 8) { + decoCorner + .scaleEffect(y: -1) + Rectangle() + .fill(decoGold) + .frame(height: 2) + decoCorner + .scaleEffect(x: -1, y: -1) + } + .frame(height: 20) + .padding(.horizontal, 16) + } + } + + private var decoCorner: some View { + ZStack { + Rectangle() + .fill(decoGold) + .frame(width: 20, height: 2) + + Rectangle() + .fill(decoGold) + .frame(width: 2, height: 20) + .offset(x: -9) + } + } + + // MARK: - Deco Hero + + private var decoHero: some View { + VStack(spacing: 24) { + // Sunburst title + ZStack { + // Rays + ForEach(0..<12, id: \.self) { i in + Rectangle() + .fill(decoGold.opacity(0.15)) + .frame(width: 2, height: 60) + .offset(y: -50) + .rotationEffect(.degrees(Double(i) * 30)) + } + + // Title container + VStack(spacing: 4) { + Text("YOUR") + .font(.system(size: 14, weight: .medium)) + .tracking(8) + .foregroundStyle(textSecondary) + + Text("JOURNEY") + .font(.system(size: 36, weight: .bold)) + .tracking(4) + .foregroundStyle(textPrimary) + + Text("AWAITS") + .font(.system(size: 14, weight: .medium)) + .tracking(8) + .foregroundStyle(textSecondary) + } + } + .padding(.vertical, 20) + + // Description with deco borders + VStack(spacing: 12) { + decoLine + Text("Plan an unforgettable road trip through America's greatest stadiums and arenas.") + .font(.system(size: 15, weight: .regular)) + .foregroundStyle(textSecondary) + .multilineTextAlignment(.center) + .lineSpacing(6) + .padding(.horizontal, 16) + decoLine + } + + // Deco CTA Button + Button { + showNewTrip = true + } label: { + HStack(spacing: 16) { + decoDiamond + Text("BEGIN") + .font(.system(size: 14, weight: .bold)) + .tracking(4) + decoDiamond + } + .foregroundStyle(colorScheme == .dark ? .black : decoCream) + .padding(.horizontal, 40) + .padding(.vertical, 18) + .background( + Rectangle() + .fill(decoGold) + ) + .overlay( + Rectangle() + .stroke(decoGold.opacity(0.5), lineWidth: 1) + .padding(4) + ) + } + } + .padding(32) + .background( + RoundedRectangle(cornerRadius: 0) + .fill(colorScheme == .dark ? Color.white.opacity(0.03) : Color.white.opacity(0.5)) + .overlay( + Rectangle() + .stroke(decoGold.opacity(0.3), lineWidth: 1) + ) + ) + } + + private var decoLine: some View { + HStack(spacing: 12) { + Rectangle() + .fill(decoGold.opacity(0.4)) + .frame(height: 1) + decoDiamond + .foregroundStyle(decoGold.opacity(0.6)) + Rectangle() + .fill(decoGold.opacity(0.4)) + .frame(height: 1) + } + } + + private var decoDiamond: some View { + Rectangle() + .fill(decoGold) + .frame(width: 6, height: 6) + .rotationEffect(.degrees(45)) + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 20) { + // Section header + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("FEATURED") + .font(.system(size: 11, weight: .bold)) + .tracking(4) + .foregroundStyle(decoGold) + + Text("Itineraries") + .font(.system(size: 24, weight: .bold)) + .foregroundStyle(textPrimary) + } + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14)) + .foregroundStyle(decoGold) + .padding(12) + .overlay( + Rectangle() + .stroke(decoGold.opacity(0.5), lineWidth: 1) + ) + } + } + + // Deco cards + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + decoTripCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + } + + private func decoTripCard(_ trip: Trip) -> some View { + HStack(spacing: 16) { + // Stepped shape icon container + ZStack { + // Stepped background + Rectangle() + .fill(decoGold.opacity(0.15)) + .frame(width: 50, height: 50) + + Rectangle() + .fill(decoGold.opacity(0.1)) + .frame(width: 44, height: 44) + .offset(x: 3, y: 3) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt") + .font(.system(size: 20)) + .foregroundStyle(decoGold) + } + + VStack(alignment: .leading, spacing: 6) { + Text(trip.name.uppercased()) + .font(.system(size: 14, weight: .bold)) + .tracking(1) + .foregroundStyle(textPrimary) + .lineLimit(1) + + HStack(spacing: 16) { + HStack(spacing: 4) { + decoDiamond + .scaleEffect(0.6) + .foregroundStyle(decoTeal) + Text("\(trip.stops.count) CITIES") + .font(.system(size: 10, weight: .medium)) + .tracking(1) + .foregroundStyle(textSecondary) + } + + HStack(spacing: 4) { + decoDiamond + .scaleEffect(0.6) + .foregroundStyle(decoTeal) + Text("\(trip.totalGames) GAMES") + .font(.system(size: 10, weight: .medium)) + .tracking(1) + .foregroundStyle(textSecondary) + } + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(decoGold) + } + .padding(16) + .background( + Rectangle() + .fill(colorScheme == .dark ? Color.white.opacity(0.03) : Color.white.opacity(0.6)) + ) + .overlay( + Rectangle() + .stroke(decoGold.opacity(0.2), lineWidth: 1) + ) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 20) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("YOUR") + .font(.system(size: 11, weight: .bold)) + .tracking(4) + .foregroundStyle(decoTeal) + + Text("Collection") + .font(.system(size: 24, weight: .bold)) + .foregroundStyle(textPrimary) + } + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("VIEW ALL") + .font(.system(size: 10, weight: .bold)) + .tracking(2) + .foregroundStyle(decoGold) + } + } + + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + HStack { + decoDiamond + .foregroundStyle(decoTeal) + + Text(trip.name) + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(textPrimary) + + Spacer() + + Text("\(trip.stops.count)") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(decoGold) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + Rectangle() + .fill(decoGold.opacity(0.15)) + ) + } + .padding(.vertical, 14) + .overlay(alignment: .bottom) { + Rectangle() + .fill(decoGold.opacity(0.15)) + .frame(height: 1) + } + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Deco Footer + + private var decoFooter: some View { + VStack(spacing: 12) { + // Stepped pyramid + VStack(spacing: 2) { + Rectangle() + .fill(decoGold.opacity(0.4)) + .frame(width: 30, height: 2) + Rectangle() + .fill(decoGold.opacity(0.3)) + .frame(width: 50, height: 2) + Rectangle() + .fill(decoGold.opacity(0.2)) + .frame(width: 70, height: 2) + } + + Text("SPORTS TIME") + .font(.system(size: 9, weight: .bold)) + .tracking(6) + .foregroundStyle(textSecondary.opacity(0.5)) + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Brutalist/HomeContent_Brutalist.swift b/SportsTime/Features/Home/Views/Variants/Brutalist/HomeContent_Brutalist.swift new file mode 100644 index 0000000..13c0901 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Brutalist/HomeContent_Brutalist.swift @@ -0,0 +1,272 @@ +// +// HomeContent_Brutalist.swift +// SportsTime +// +// BRUTALIST: Raw, unpolished, anti-design rebellion. +// Monospace typography, harsh borders, no rounded corners. +// Ticket stub perforations, stadium concrete vibes. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Brutalist: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + private var bgColor: Color { + colorScheme == .dark ? .black : Color(white: 0.95) + } + + private var textColor: Color { + colorScheme == .dark ? .white : .black + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + // HEADER - Raw, bold, commanding + brutalistHeader + + // PERFORATED DIVIDER + perforatedDivider + + // ACTION BLOCK + actionBlock + .padding(.vertical, 24) + + perforatedDivider + + // TRIPS SECTION + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + tripsSection + .padding(.vertical, 24) + + perforatedDivider + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.vertical, 24) + } + + // FOOTER STAMP + footerStamp + .padding(.vertical, 32) + } + .padding(.horizontal, 16) + } + .background(bgColor) + } + + // MARK: - Brutalist Header + + private var brutalistHeader: some View { + VStack(alignment: .leading, spacing: 8) { + // Date stamp - like a ticket + Text(Date.now.formatted(.dateTime.year().month().day()).uppercased()) + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.5)) + .padding(.top, 24) + + // Main title - LOUD + Text("SPORTS") + .font(.system(size: 64, weight: .black, design: .default)) + .foregroundStyle(textColor) + .tracking(-2) + + Text("TIME") + .font(.system(size: 64, weight: .black, design: .default)) + .foregroundStyle(.red) + .tracking(-2) + .offset(y: -16) + + // Subtitle + Text("PLAN YOUR ROAD TRIP") + .font(.system(.subheadline, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.7)) + .tracking(4) + } + } + + // MARK: - Perforated Divider (Ticket Stub Style) + + private var perforatedDivider: some View { + HStack(spacing: 8) { + ForEach(0..<20, id: \.self) { _ in + Circle() + .fill(textColor.opacity(0.3)) + .frame(width: 6, height: 6) + } + } + .frame(maxWidth: .infinity) + } + + // MARK: - Action Block + + private var actionBlock: some View { + VStack(alignment: .leading, spacing: 16) { + Text("[ NEW TRIP ]") + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.5)) + + Button { + showNewTrip = true + } label: { + HStack { + Text("START PLANNING →") + .font(.system(.title3, design: .monospaced).bold()) + + Spacer() + } + .foregroundStyle(colorScheme == .dark ? .black : .white) + .padding(20) + .background(.red) + } + + Text("CREATE YOUR ULTIMATE SPORTS ROAD TRIP. NO FRILLS. JUST GAMES.") + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.6)) + .lineSpacing(4) + } + } + + // MARK: - Trips Section + + private var tripsSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("[ FEATURED ]") + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.5)) + + Spacer() + + Text("\(suggestedTripsGenerator.suggestedTrips.count) TRIPS") + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(.red) + } + + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + brutalistTripCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + } + + private func brutalistTripCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 4) { + Text(trip.name.uppercased()) + .font(.system(.headline, design: .monospaced).bold()) + .foregroundStyle(textColor) + + Text("\(trip.stops.count) STOPS • \(trip.totalGames) GAMES") + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.5)) + } + + Spacer() + + // Sport icons as harsh symbols + HStack(spacing: 4) { + ForEach(Array(trip.uniqueSports.prefix(3)), id: \.self) { sport in + Text("●") + .font(.caption) + .foregroundStyle(sport.themeColor) + } + } + } + + // Route as raw text + Text(trip.stops.map { $0.city.uppercased() }.joined(separator: " → ")) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.4)) + .lineLimit(1) + } + .padding(16) + .background(textColor.opacity(0.05)) + .border(textColor.opacity(0.2), width: 1) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("[ YOUR TRIPS ]") + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.5)) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("VIEW ALL →") + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(.red) + } + } + + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + HStack { + Text(trip.name.uppercased()) + .font(.system(.subheadline, design: .monospaced)) + .foregroundStyle(textColor) + + Spacer() + + Text("\(trip.stops.count)") + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.5)) + } + .padding(.vertical, 12) + .overlay(alignment: .bottom) { + Rectangle() + .fill(textColor.opacity(0.1)) + .frame(height: 1) + } + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Footer Stamp + + private var footerStamp: some View { + VStack(spacing: 8) { + Rectangle() + .fill(.red) + .frame(width: 60, height: 4) + + Text("SPORTSTIME") + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.3)) + .tracking(6) + + Text("EST. 2024") + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(textColor.opacity(0.2)) + } + .frame(maxWidth: .infinity) + } +} diff --git a/SportsTime/Features/Home/Views/Variants/CarrotWeather/HomeContent_CarrotWeather.swift b/SportsTime/Features/Home/Views/Variants/CarrotWeather/HomeContent_CarrotWeather.swift new file mode 100644 index 0000000..edabe84 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/CarrotWeather/HomeContent_CarrotWeather.swift @@ -0,0 +1,347 @@ +// +// HomeContent_CarrotWeather.swift +// SportsTime +// +// CARROT WEATHER-INSPIRED: Bold personality, clean layout. +// Dynamic content, good use of gradients. +// Data-focused with character. +// + +import SwiftUI +import SwiftData + +struct HomeContent_CarrotWeather: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Carrot-inspired colors + private var bgGradient: LinearGradient { + LinearGradient( + colors: colorScheme == .dark + ? [Color(red: 0.1, green: 0.12, blue: 0.18), Color(red: 0.08, green: 0.08, blue: 0.12)] + : [Color(red: 0.45, green: 0.65, blue: 0.9), Color(red: 0.35, green: 0.55, blue: 0.85)], + startPoint: .top, + endPoint: .bottom + ) + } + + private var cardBg: Color { + colorScheme == .dark + ? Color.white.opacity(0.08) + : Color.white.opacity(0.25) + } + + private let carrotOrange = Color(red: 1.0, green: 0.45, blue: 0.2) + + private var textPrimary: Color { + .white + } + + private var textSecondary: Color { + Color.white.opacity(0.7) + } + + var body: some View { + ScrollView { + VStack(spacing: 24) { + // Main display + mainDisplay + .padding(.horizontal, 20) + .padding(.top, 20) + + // Trip summary card + tripSummaryCard + .padding(.horizontal, 20) + + // Recent trips + if !savedTrips.isEmpty { + recentTripsSection + } + + // Suggestions + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + suggestionsSection + } + + Spacer(minLength: 50) + } + } + .background(bgGradient.ignoresSafeArea()) + } + + // MARK: - Main Display + + private var mainDisplay: some View { + VStack(spacing: 12) { + // Big number display (like temperature) + Text("\(savedTrips.count)") + .font(.system(size: 96, weight: .thin)) + .foregroundStyle(textPrimary) + + Text(tripStatusMessage) + .font(.system(size: 18, weight: .medium)) + .foregroundStyle(textSecondary) + .multilineTextAlignment(.center) + + // Personality message + Text(personalityMessage) + .font(.system(size: 14)) + .foregroundStyle(textSecondary.opacity(0.8)) + .italic() + .padding(.top, 4) + } + } + + private var tripStatusMessage: String { + if savedTrips.isEmpty { + return "No trips planned" + } else if savedTrips.count == 1 { + return "Trip planned" + } else { + return "Trips in your queue" + } + } + + private var personalityMessage: String { + if savedTrips.isEmpty { + return "Time to hit the road, sports fan!" + } else if savedTrips.count >= 3 { + return "Someone's serious about their sports!" + } else { + return "Adventure awaits..." + } + } + + // MARK: - Trip Summary Card + + private var tripSummaryCard: some View { + Button { + showNewTrip = true + } label: { + VStack(spacing: 16) { + HStack(spacing: 20) { + summaryItem(value: "\(totalGames)", label: "Games", icon: "sportscourt.fill") + summaryItem(value: "\(totalStops)", label: "Cities", icon: "building.2.fill") + summaryItem(value: "\(uniqueSportsCount)", label: "Sports", icon: "figure.run") + } + + // CTA + HStack { + Text("Plan a Trip") + .font(.system(size: 15, weight: .semibold)) + + Image(systemName: "arrow.right") + .font(.system(size: 13, weight: .semibold)) + } + .foregroundStyle(textPrimary) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background( + Capsule() + .fill(carrotOrange) + ) + } + .padding(20) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(cardBg) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(.ultraThinMaterial) + ) + ) + } + .buttonStyle(.plain) + } + + private func summaryItem(value: String, label: String, icon: String) -> some View { + VStack(spacing: 6) { + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundStyle(textSecondary) + + Text(value) + .font(.system(size: 22, weight: .semibold)) + .foregroundStyle(textPrimary) + + Text(label) + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + .frame(maxWidth: .infinity) + } + + private var totalGames: Int { + savedTrips.compactMap { $0.trip?.totalGames }.reduce(0, +) + } + + private var totalStops: Int { + savedTrips.compactMap { $0.trip?.stops.count }.reduce(0, +) + } + + private var uniqueSportsCount: Int { + Set(savedTrips.flatMap { $0.trip?.uniqueSports ?? [] }).count + } + + // MARK: - Recent Trips Section + + private var recentTripsSection: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("YOUR TRIPS") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(1) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("See All") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(textPrimary) + } + } + .padding(.horizontal, 20) + + VStack(spacing: 10) { + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + tripRow(trip) + } + .buttonStyle(.plain) + } + } + } + .padding(.horizontal, 20) + } + } + + private func tripRow(_ trip: Trip) -> some View { + HStack(spacing: 14) { + // Sport indicator + Circle() + .fill(trip.uniqueSports.first?.themeColor ?? carrotOrange) + .frame(width: 40, height: 40) + .overlay( + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 16)) + .foregroundStyle(.white) + ) + + VStack(alignment: .leading, spacing: 3) { + Text(trip.name) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) stops • \(trip.totalGames) games") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12)) + .foregroundStyle(textSecondary.opacity(0.6)) + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(cardBg) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(.ultraThinMaterial) + ) + ) + } + + // MARK: - Suggestions Section + + private var suggestionsSection: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("SUGGESTED") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(1) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 13)) + .foregroundStyle(textPrimary) + } + } + .padding(.horizontal, 20) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + suggestionCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 20) + } + } + } + + private func suggestionCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 10) { + // Header with big stat + VStack(spacing: 4) { + Text("\(trip.totalGames)") + .font(.system(size: 32, weight: .semibold)) + .foregroundStyle(textPrimary) + + Text("games") + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(2) + + Text("\(trip.stops.count) cities") + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + } + .frame(width: 130) + .padding(14) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(cardBg) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(.ultraThinMaterial) + ) + ) + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Classic/HomeContent_Classic.swift b/SportsTime/Features/Home/Views/Variants/Classic/HomeContent_Classic.swift new file mode 100644 index 0000000..505903e --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Classic/HomeContent_Classic.swift @@ -0,0 +1,346 @@ +// +// HomeContent_Classic.swift +// SportsTime +// +// CLASSIC: The original SportsTime design. +// Uses the app's Theme system with warm orange accents. +// Clean cards, glow effects, and familiar layout. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Classic: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + var body: some View { + ScrollView { + VStack(spacing: Theme.Spacing.lg) { + // Hero Card + heroCard + .padding(.horizontal, Theme.Spacing.md) + .padding(.top, Theme.Spacing.sm) + + // Quick Actions + quickActions + .padding(.horizontal, Theme.Spacing.md) + + // Suggested Trips + suggestedTripsSection + .padding(.horizontal, Theme.Spacing.md) + + // Saved Trips + if !savedTrips.isEmpty { + savedTripsSection + .padding(.horizontal, Theme.Spacing.md) + } + + // Planning Tips + if !displayedTips.isEmpty { + tipsSection + .padding(.horizontal, Theme.Spacing.md) + } + + Spacer(minLength: 40) + } + } + .themedBackground() + } + + // MARK: - Hero Card + + private var heroCard: some View { + VStack(spacing: Theme.Spacing.lg) { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + Text("Adventure Awaits") + .font(.largeTitle) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Text("Plan your ultimate sports road trip. Visit stadiums, catch games, and create unforgettable memories.") + .font(.body) + .foregroundStyle(Theme.textSecondary(colorScheme)) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + + Button { + showNewTrip = true + } label: { + HStack(spacing: Theme.Spacing.xs) { + Image(systemName: "map.fill") + Text("Start Planning") + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .padding(Theme.Spacing.md) + .background(Theme.warmOrange) + .foregroundStyle(.white) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + } + .pressableStyle() + .glowEffect(color: Theme.warmOrange, radius: 12) + } + .padding(Theme.Spacing.lg) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + .shadow(color: Theme.cardShadow(colorScheme), radius: 15, y: 8) + } + + // MARK: - Quick Actions + + private var quickActions: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + Text("Quick Start") + .font(.title2) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + SportSelectorGrid { _ in + showNewTrip = true + } + .padding(.horizontal, Theme.Spacing.md) + .padding(.vertical, Theme.Spacing.md) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + } + } + + // MARK: - Suggested Trips + + @ViewBuilder + private var suggestedTripsSection: some View { + if suggestedTripsGenerator.isLoading { + LoadingTripsView(message: suggestedTripsGenerator.loadingMessage) + } else if !suggestedTripsGenerator.suggestedTrips.isEmpty { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + // Header with refresh button + HStack { + Text("Featured Trips") + .font(.title2) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.subheadline) + .foregroundStyle(Theme.warmOrange) + } + } + + // Horizontal carousel grouped by region + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: Theme.Spacing.lg) { + ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + // Region header + HStack(spacing: Theme.Spacing.xs) { + Image(systemName: regionGroup.region.iconName) + .font(.caption) + Text(regionGroup.region.shortName) + .font(.subheadline) + } + .foregroundStyle(Theme.textSecondary(colorScheme)) + + // Trip cards for this region + HStack(spacing: Theme.Spacing.md) { + ForEach(regionGroup.trips) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + SuggestedTripCard(suggestedTrip: suggestedTrip) + } + .buttonStyle(.plain) + } + } + } + } + } + .padding(.horizontal, 1) + } + } + } else if let error = suggestedTripsGenerator.error { + // Error state + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + Text("Featured Trips") + .font(.title2) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + HStack { + Image(systemName: "exclamationmark.triangle") + .foregroundStyle(.orange) + Text(error) + .font(.subheadline) + .foregroundStyle(Theme.textSecondary(colorScheme)) + + Spacer() + + Button("Retry") { + Task { + await suggestedTripsGenerator.generateTrips() + } + } + .font(.subheadline) + .foregroundStyle(Theme.warmOrange) + } + .padding(Theme.Spacing.md) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + } + } + } + + // MARK: - Saved Trips + + private var savedTripsSection: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + HStack { + Text("Recent Trips") + .font(.title2) + .foregroundStyle(Theme.textPrimary(colorScheme)) + Spacer() + Button { + selectedTab = 2 + } label: { + HStack(spacing: 4) { + Text("See All") + Image(systemName: "chevron.right") + .font(.caption) + } + .font(.subheadline) + .foregroundStyle(Theme.warmOrange) + } + } + + ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + classicTripCard(savedTrip: savedTrip, trip: trip) + } + .buttonStyle(.plain) + .staggeredAnimation(index: index, delay: 0.05) + } + } + } + } + + private func classicTripCard(savedTrip: SavedTrip, trip: Trip) -> some View { + HStack(spacing: Theme.Spacing.md) { + // Route preview icon + ZStack { + Circle() + .fill(Theme.warmOrange.opacity(0.15)) + .frame(width: 44, height: 44) + + Image(systemName: "map.fill") + .foregroundStyle(Theme.warmOrange) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.body) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Text(trip.formattedDateRange) + .font(.subheadline) + .foregroundStyle(Theme.textSecondary(colorScheme)) + + HStack(spacing: Theme.Spacing.sm) { + HStack(spacing: 4) { + Image(systemName: "mappin") + .font(.caption2) + Text("\(trip.stops.count) cities") + } + HStack(spacing: 4) { + Image(systemName: "sportscourt") + .font(.caption2) + Text("\(trip.totalGames) games") + } + } + .font(.caption) + .foregroundStyle(Theme.textMuted(colorScheme)) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(Theme.textMuted(colorScheme)) + } + .padding(Theme.Spacing.md) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.medium) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + } + + // MARK: - Tips Section + + private var tipsSection: some View { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + Text("Planning Tips") + .font(.title2) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + VStack(spacing: Theme.Spacing.xs) { + ForEach(displayedTips) { tip in + classicTipRow(icon: tip.icon, title: tip.title, subtitle: tip.subtitle) + } + } + .padding(Theme.Spacing.md) + .background(Theme.cardBackground(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) + .overlay { + RoundedRectangle(cornerRadius: Theme.CornerRadius.large) + .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) + } + } + } + + private func classicTipRow(icon: String, title: String, subtitle: String) -> some View { + HStack(spacing: Theme.Spacing.sm) { + ZStack { + Circle() + .fill(Theme.routeGold.opacity(0.15)) + .frame(width: 36, height: 36) + + Image(systemName: icon) + .font(.subheadline) + .foregroundStyle(Theme.routeGold) + } + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.subheadline) + .foregroundStyle(Theme.textPrimary(colorScheme)) + Text(subtitle) + .font(.caption) + .foregroundStyle(Theme.textSecondary(colorScheme)) + } + + Spacer() + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/DarkIndustrial/HomeContent_DarkIndustrial.swift b/SportsTime/Features/Home/Views/Variants/DarkIndustrial/HomeContent_DarkIndustrial.swift new file mode 100644 index 0000000..00e8694 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/DarkIndustrial/HomeContent_DarkIndustrial.swift @@ -0,0 +1,427 @@ +// +// HomeContent_DarkIndustrial.swift +// SportsTime +// +// DARK INDUSTRIAL: Steel, concrete, utility. +// Stadium infrastructure vibes, warning stripes, exposed structure. +// Functional brutalism with sports facility aesthetics. +// + +import SwiftUI +import SwiftData + +struct HomeContent_DarkIndustrial: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Industrial palette + private let warningYellow = Color(red: 1.0, green: 0.8, blue: 0.0) + private let steelGray = Color(red: 0.4, green: 0.45, blue: 0.5) + private let concreteGray = Color(red: 0.3, green: 0.32, blue: 0.35) + private let alertRed = Color(red: 0.9, green: 0.2, blue: 0.2) + + private var bgColor: Color { + colorScheme == .dark ? Color(red: 0.08, green: 0.09, blue: 0.1) : Color(red: 0.15, green: 0.16, blue: 0.18) + } + + private var textPrimary: Color { + Color(white: 0.9) + } + + private var textSecondary: Color { + Color(white: 0.55) + } + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + // Industrial texture overlay + industrialTexture + + ScrollView { + VStack(spacing: 0) { + // WARNING STRIPE HEADER + warningStripeHeader + + // INDUSTRIAL HERO + industrialHero + .padding(.top, 32) + .padding(.horizontal, 20) + + // FEATURED TRIPS + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.top, 40) + .padding(.horizontal, 20) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.top, 40) + .padding(.horizontal, 20) + } + + // INDUSTRIAL FOOTER + industrialFooter + .padding(.top, 48) + .padding(.bottom, 32) + } + } + } + } + + // MARK: - Industrial Texture + + private var industrialTexture: some View { + ZStack { + // Grid pattern (exposed structure) + GeometryReader { geo in + Path { path in + let spacing: CGFloat = 40 + for x in stride(from: CGFloat(0), to: geo.size.width, by: spacing) { + path.move(to: CGPoint(x: x, y: 0)) + path.addLine(to: CGPoint(x: x, y: geo.size.height)) + } + for y in stride(from: CGFloat(0), to: geo.size.height, by: spacing) { + path.move(to: CGPoint(x: 0, y: y)) + path.addLine(to: CGPoint(x: geo.size.width, y: y)) + } + } + .stroke(steelGray.opacity(0.1), lineWidth: 0.5) + } + + // Corner rivets/bolts + VStack { + HStack { + rivetCluster + Spacer() + rivetCluster + } + Spacer() + } + .padding(20) + } + .allowsHitTesting(false) + } + + private var rivetCluster: some View { + HStack(spacing: 8) { + rivet + rivet + } + } + + private var rivet: some View { + Circle() + .fill(steelGray.opacity(0.3)) + .frame(width: 8, height: 8) + .overlay( + Circle() + .fill(steelGray.opacity(0.5)) + .frame(width: 4, height: 4) + ) + } + + // MARK: - Warning Stripe Header + + private var warningStripeHeader: some View { + VStack(spacing: 0) { + // Warning stripes + HStack(spacing: 0) { + ForEach(0..<20, id: \.self) { i in + Rectangle() + .fill(i % 2 == 0 ? warningYellow : .black) + .frame(width: 20, height: 8) + } + } + + // System status bar + HStack { + HStack(spacing: 6) { + Circle() + .fill(Color.green) + .frame(width: 6, height: 6) + Text("SYSTEM ONLINE") + .font(.system(size: 9, weight: .bold, design: .monospaced)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Text(Date.now.formatted(.dateTime.month().day().year())) + .font(.system(size: 9, weight: .medium, design: .monospaced)) + .foregroundStyle(textSecondary) + } + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(concreteGray.opacity(0.5)) + } + } + + // MARK: - Industrial Hero + + private var industrialHero: some View { + VStack(alignment: .leading, spacing: 24) { + // Sector label + HStack(spacing: 8) { + Rectangle() + .fill(warningYellow) + .frame(width: 4, height: 20) + + Text("SECTOR A-1") + .font(.system(size: 10, weight: .bold, design: .monospaced)) + .foregroundStyle(warningYellow) + } + + // Main title - stencil style + VStack(alignment: .leading, spacing: 4) { + Text("SPORTS") + .font(.system(size: 42, weight: .black)) + .foregroundStyle(textPrimary) + + Text("TIME") + .font(.system(size: 42, weight: .black)) + .foregroundStyle(warningYellow) + } + + // Description panel + VStack(alignment: .leading, spacing: 8) { + Text("// TRIP PLANNING SYSTEM") + .font(.system(size: 10, weight: .medium, design: .monospaced)) + .foregroundStyle(steelGray) + + Text("Route optimization for stadium road trips. Multi-sport scheduling. Real-time coordination.") + .font(.system(size: 14, weight: .regular)) + .foregroundStyle(textSecondary) + .lineSpacing(4) + } + .padding(16) + .background( + Rectangle() + .fill(concreteGray.opacity(0.3)) + .overlay( + Rectangle() + .stroke(steelGray.opacity(0.3), lineWidth: 1) + ) + ) + + // Industrial CTA + Button { + showNewTrip = true + } label: { + HStack { + Image(systemName: "play.fill") + .font(.system(size: 12)) + + Text("INITIATE PLANNING") + .font(.system(size: 14, weight: .bold, design: .monospaced)) + + Spacer() + + Text("▶") + .font(.system(size: 16, weight: .bold)) + } + .foregroundStyle(.black) + .padding(18) + .background(warningYellow) + } + } + .padding(24) + .background( + Rectangle() + .fill(bgColor) + .overlay( + Rectangle() + .stroke(steelGray.opacity(0.3), lineWidth: 2) + ) + ) + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 20) { + // Section header + HStack { + HStack(spacing: 8) { + Rectangle() + .fill(alertRed) + .frame(width: 4, height: 20) + + Text("FEATURED ROUTES") + .font(.system(size: 12, weight: .bold, design: .monospaced)) + .foregroundStyle(textPrimary) + } + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.clockwise") + .font(.system(size: 12)) + Text("REFRESH") + .font(.system(size: 9, weight: .bold, design: .monospaced)) + } + .foregroundStyle(steelGray) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .overlay( + Rectangle() + .stroke(steelGray.opacity(0.5), lineWidth: 1) + ) + } + } + + // Industrial cards + ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + industrialTripCard(suggestedTrip.trip, index: index + 1) + } + .buttonStyle(.plain) + } + } + } + + private func industrialTripCard(_ trip: Trip, index: Int) -> some View { + HStack(spacing: 16) { + // Index plate + Text(String(format: "%02d", index)) + .font(.system(size: 18, weight: .bold, design: .monospaced)) + .foregroundStyle(.black) + .frame(width: 44, height: 44) + .background(warningYellow) + + VStack(alignment: .leading, spacing: 6) { + Text(trip.name.uppercased()) + .font(.system(size: 13, weight: .bold, design: .monospaced)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + HStack(spacing: 16) { + HStack(spacing: 4) { + Image(systemName: "mappin") + .font(.system(size: 10)) + Text("\(trip.stops.count) STOPS") + .font(.system(size: 10, weight: .medium, design: .monospaced)) + } + .foregroundStyle(steelGray) + + HStack(spacing: 4) { + Image(systemName: "sportscourt") + .font(.system(size: 10)) + Text("\(trip.totalGames) EVENTS") + .font(.system(size: 10, weight: .medium, design: .monospaced)) + } + .foregroundStyle(steelGray) + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(steelGray) + } + .padding(16) + .background( + Rectangle() + .fill(concreteGray.opacity(0.2)) + .overlay( + Rectangle() + .stroke(steelGray.opacity(0.2), lineWidth: 1) + ) + ) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 20) { + HStack { + HStack(spacing: 8) { + Rectangle() + .fill(steelGray) + .frame(width: 4, height: 20) + + Text("SAVED ROUTES") + .font(.system(size: 12, weight: .bold, design: .monospaced)) + .foregroundStyle(textPrimary) + } + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("VIEW ALL ▶") + .font(.system(size: 9, weight: .bold, design: .monospaced)) + .foregroundStyle(steelGray) + } + } + + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + HStack { + Rectangle() + .fill(steelGray.opacity(0.5)) + .frame(width: 3, height: 32) + + Text(trip.name.uppercased()) + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundStyle(textPrimary) + + Spacer() + + Text("[\(trip.stops.count)]") + .font(.system(size: 11, weight: .bold, design: .monospaced)) + .foregroundStyle(warningYellow) + } + .padding(.vertical, 12) + .overlay(alignment: .bottom) { + Rectangle() + .fill(steelGray.opacity(0.2)) + .frame(height: 1) + } + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Industrial Footer + + private var industrialFooter: some View { + VStack(spacing: 12) { + // Warning stripe + HStack(spacing: 0) { + ForEach(0..<8, id: \.self) { i in + Rectangle() + .fill(i % 2 == 0 ? warningYellow.opacity(0.3) : .clear) + .frame(width: 12, height: 4) + } + } + + Text("// SPORTS TIME SYSTEMS") + .font(.system(size: 9, weight: .medium, design: .monospaced)) + .foregroundStyle(steelGray.opacity(0.5)) + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Fantastical/HomeContent_Fantastical.swift b/SportsTime/Features/Home/Views/Variants/Fantastical/HomeContent_Fantastical.swift new file mode 100644 index 0000000..7d0d089 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Fantastical/HomeContent_Fantastical.swift @@ -0,0 +1,338 @@ +// +// HomeContent_Fantastical.swift +// SportsTime +// +// FANTASTICAL-INSPIRED: Calendar elegance. +// Data-dense but readable, rich colors. +// Schedule-focused with beautiful typography. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Fantastical: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Fantastical-inspired colors + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.1, green: 0.1, blue: 0.12) + : Color(red: 0.97, green: 0.97, blue: 0.98) + } + + private var cardBg: Color { + colorScheme == .dark + ? Color(red: 0.15, green: 0.15, blue: 0.17) + : Color.white + } + + private let fantasticalRed = Color(red: 0.92, green: 0.26, blue: 0.26) + private let fantasticalBlue = Color(red: 0.2, green: 0.55, blue: 0.95) + + private var textPrimary: Color { + colorScheme == .dark ? .white : Color(red: 0.15, green: 0.15, blue: 0.18) + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.55) : Color(white: 0.45) + } + + var body: some View { + ScrollView { + VStack(spacing: 20) { + // Date header + dateHeader + .padding(.horizontal, 16) + .padding(.top, 8) + + // Quick add + quickAddButton + .padding(.horizontal, 16) + + // Upcoming section + if !savedTrips.isEmpty { + upcomingSection + } + + // Suggestions + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + suggestionsSection + } + + Spacer(minLength: 40) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Date Header + + private var dateHeader: some View { + HStack(alignment: .bottom) { + VStack(alignment: .leading, spacing: 4) { + Text(dayOfWeek) + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(fantasticalRed) + + Text(formattedDate) + .font(.system(size: 28, weight: .bold)) + .foregroundStyle(textPrimary) + } + + Spacer() + + // Today indicator + VStack(spacing: 2) { + Text("TODAY") + .font(.system(size: 9, weight: .bold)) + .foregroundStyle(.white) + + Text(dayNumber) + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(.white) + } + .padding(.horizontal, 10) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(fantasticalRed) + ) + } + } + + private var dayOfWeek: String { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE" + return formatter.string(from: Date()) + } + + private var formattedDate: String { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM d, yyyy" + return formatter.string(from: Date()) + } + + private var dayNumber: String { + let formatter = DateFormatter() + formatter.dateFormat = "d" + return formatter.string(from: Date()) + } + + // MARK: - Quick Add Button + + private var quickAddButton: some View { + Button { + showNewTrip = true + } label: { + HStack(spacing: 12) { + ZStack { + Circle() + .fill(fantasticalRed) + .frame(width: 28, height: 28) + + Image(systemName: "plus") + .font(.system(size: 14, weight: .bold)) + .foregroundStyle(.white) + } + + Text("Plan New Trip") + .font(.system(size: 16, weight: .medium)) + .foregroundStyle(textPrimary) + + Spacer() + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.06), radius: 6, y: 2) + ) + } + .buttonStyle(.plain) + } + + // MARK: - Upcoming Section + + private var upcomingSection: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("Upcoming") + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("See All") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(fantasticalBlue) + } + } + .padding(.horizontal, 16) + + VStack(spacing: 2) { + ForEach(savedTrips.prefix(4)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + eventRow(trip) + } + .buttonStyle(.plain) + } + } + } + .background( + RoundedRectangle(cornerRadius: 14) + .fill(cardBg) + ) + .padding(.horizontal, 16) + } + } + + private func eventRow(_ trip: Trip) -> some View { + HStack(spacing: 14) { + // Color bar + RoundedRectangle(cornerRadius: 2) + .fill(trip.uniqueSports.first?.themeColor ?? fantasticalBlue) + .frame(width: 4, height: 44) + + VStack(alignment: .leading, spacing: 3) { + Text(trip.name) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + HStack(spacing: 8) { + Text(trip.formattedDateRange) + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + + Text("•") + .foregroundStyle(textSecondary) + + HStack(spacing: 3) { + Image(systemName: "sportscourt.fill") + .font(.system(size: 10)) + Text("\(trip.totalGames)") + .font(.system(size: 12)) + } + .foregroundStyle(textSecondary) + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(textSecondary.opacity(0.5)) + } + .padding(.horizontal, 14) + .padding(.vertical, 12) + } + + // MARK: - Suggestions Section + + private var suggestionsSection: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("Suggested Routes") + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14)) + .foregroundStyle(fantasticalBlue) + } + } + .padding(.horizontal, 16) + + VStack(spacing: 10) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(3)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + suggestionCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 16) + } + } + + private func suggestionCard(_ trip: Trip) -> some View { + HStack(spacing: 14) { + // Time block style + VStack(spacing: 2) { + Text("\(trip.stops.count)") + .font(.system(size: 18, weight: .bold)) + .foregroundStyle(textPrimary) + + Text("stops") + .font(.system(size: 10)) + .foregroundStyle(textSecondary) + } + .frame(width: 50) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(trip.uniqueSports.first?.themeColor.opacity(0.15) ?? fantasticalBlue.opacity(0.15)) + ) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + HStack(spacing: 6) { + if let sport = trip.uniqueSports.first { + HStack(spacing: 3) { + Image(systemName: sport.iconName) + .font(.system(size: 10)) + Text(sport.displayName) + .font(.system(size: 12)) + } + .foregroundStyle(sport.themeColor) + } + + Text("•") + .foregroundStyle(textSecondary) + + Text("\(trip.totalGames) games") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(textSecondary.opacity(0.5)) + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.05), radius: 4, y: 2) + ) + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Flighty/HomeContent_Flighty.swift b/SportsTime/Features/Home/Views/Variants/Flighty/HomeContent_Flighty.swift new file mode 100644 index 0000000..ad9d050 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Flighty/HomeContent_Flighty.swift @@ -0,0 +1,384 @@ +// +// HomeContent_Flighty.swift +// SportsTime +// +// FLIGHTY-INSPIRED: Aviation dashboard aesthetic. +// Data-rich widgets, clean typography, professional feel. +// Real-time travel data visualization style. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Flighty: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Flighty-inspired colors + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.06, green: 0.06, blue: 0.08) + : Color(red: 0.97, green: 0.97, blue: 0.98) + } + + private var cardBg: Color { + colorScheme == .dark + ? Color(red: 0.11, green: 0.11, blue: 0.14) + : Color.white + } + + private var accentBlue: Color { + Color(red: 0.2, green: 0.5, blue: 1.0) + } + + private var textPrimary: Color { + colorScheme == .dark ? .white : Color(red: 0.1, green: 0.1, blue: 0.12) + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.55) : Color(white: 0.45) + } + + var body: some View { + ScrollView { + VStack(spacing: 16) { + // Status header + statusHeader + .padding(.horizontal, 20) + .padding(.top, 8) + + // Quick action card + quickActionCard + .padding(.horizontal, 20) + + // Upcoming trips widget + if !savedTrips.isEmpty { + upcomingTripsWidget + .padding(.horizontal, 20) + } + + // Suggested routes + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + suggestedRoutesSection + .padding(.horizontal, 20) + } + + // Stats dashboard + statsDashboard + .padding(.horizontal, 20) + .padding(.bottom, 32) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Status Header + + private var statusHeader: some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text("Sports Time") + .font(.system(size: 28, weight: .bold, design: .default)) + .foregroundStyle(textPrimary) + + Text(statusText) + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(textSecondary) + } + + Spacer() + + // Live indicator + HStack(spacing: 6) { + Circle() + .fill(Color.green) + .frame(width: 8, height: 8) + Text("LIVE") + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(Color.green) + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background( + Capsule() + .fill(Color.green.opacity(0.15)) + ) + } + } + + private var statusText: String { + if savedTrips.isEmpty { + return "No trips planned" + } else { + return "\(savedTrips.count) trip\(savedTrips.count == 1 ? "" : "s") in your schedule" + } + } + + // MARK: - Quick Action Card + + private var quickActionCard: some View { + Button { + showNewTrip = true + } label: { + HStack(spacing: 16) { + ZStack { + Circle() + .fill( + LinearGradient( + colors: [accentBlue, accentBlue.opacity(0.7)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 48, height: 48) + + Image(systemName: "car.fill") + .font(.system(size: 20, weight: .semibold)) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text("Plan New Trip") + .font(.system(size: 17, weight: .semibold)) + .foregroundStyle(textPrimary) + + Text("Find games along your route") + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(textSecondary) + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.06), radius: 8, y: 2) + ) + } + .buttonStyle(.plain) + } + + // MARK: - Upcoming Trips Widget + + private var upcomingTripsWidget: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("UPCOMING") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(0.5) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("See All") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(accentBlue) + } + } + + ForEach(savedTrips.prefix(2)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + tripCard(trip) + } + .buttonStyle(.plain) + } + } + } + } + + private func tripCard(_ trip: Trip) -> some View { + HStack(spacing: 14) { + // Departure indicator + VStack(spacing: 4) { + Circle() + .fill(accentBlue) + .frame(width: 10, height: 10) + + Rectangle() + .fill(textSecondary.opacity(0.3)) + .frame(width: 2, height: 30) + + Circle() + .stroke(textSecondary.opacity(0.5), lineWidth: 2) + .frame(width: 10, height: 10) + } + + VStack(alignment: .leading, spacing: 8) { + Text(trip.name) + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + HStack(spacing: 16) { + dataLabel(icon: "calendar", value: trip.formattedDateRange) + dataLabel(icon: "mappin", value: "\(trip.stops.count) stops") + } + } + + Spacer() + + // Time badge + VStack(alignment: .trailing, spacing: 2) { + Text("\(trip.totalGames)") + .font(.system(size: 20, weight: .bold, design: .rounded)) + .foregroundStyle(textPrimary) + Text("games") + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.05), radius: 6, y: 2) + ) + } + + private func dataLabel(icon: String, value: String) -> some View { + HStack(spacing: 4) { + Image(systemName: icon) + .font(.system(size: 11)) + Text(value) + .font(.system(size: 12)) + } + .foregroundStyle(textSecondary) + } + + // MARK: - Suggested Routes Section + + private var suggestedRoutesSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("SUGGESTED ROUTES") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(0.5) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(accentBlue) + } + } + + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(3)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + routeCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + } + + private func routeCard(_ trip: Trip) -> some View { + HStack(spacing: 12) { + // Sport icon + ZStack { + RoundedRectangle(cornerRadius: 8) + .fill(trip.uniqueSports.first?.themeColor.opacity(0.15) ?? accentBlue.opacity(0.15)) + .frame(width: 40, height: 40) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 16)) + .foregroundStyle(trip.uniqueSports.first?.themeColor ?? accentBlue) + } + + VStack(alignment: .leading, spacing: 3) { + Text(trip.name) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) stops • \(trip.totalGames) games") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(textSecondary.opacity(0.6)) + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.04), radius: 4, y: 1) + ) + } + + // MARK: - Stats Dashboard + + private var statsDashboard: some View { + VStack(alignment: .leading, spacing: 12) { + Text("AT A GLANCE") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(0.5) + + HStack(spacing: 12) { + statCard(value: "\(savedTrips.count)", label: "Trips", icon: "map.fill", color: accentBlue) + statCard(value: "\(totalGames)", label: "Games", icon: "sportscourt.fill", color: .orange) + statCard(value: "\(totalStops)", label: "Cities", icon: "building.2.fill", color: .purple) + } + } + } + + private func statCard(value: String, label: String, icon: String, color: Color) -> some View { + VStack(spacing: 8) { + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundStyle(color) + + Text(value) + .font(.system(size: 24, weight: .bold, design: .rounded)) + .foregroundStyle(textPrimary) + + Text(label) + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(textSecondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.04), radius: 4, y: 1) + ) + } + + private var totalGames: Int { + savedTrips.compactMap { $0.trip?.totalGames }.reduce(0, +) + } + + private var totalStops: Int { + savedTrips.compactMap { $0.trip?.stops.count }.reduce(0, +) + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Glassmorphism/HomeContent_Glassmorphism.swift b/SportsTime/Features/Home/Views/Variants/Glassmorphism/HomeContent_Glassmorphism.swift new file mode 100644 index 0000000..5941a8e --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Glassmorphism/HomeContent_Glassmorphism.swift @@ -0,0 +1,338 @@ +// +// HomeContent_Glassmorphism.swift +// SportsTime +// +// GLASSMORPHISM: Frosted glass, flowing ethereal shapes. +// Stadium lights blur effect, soft glows, dreamy atmosphere. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Glassmorphism: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Ethereal color palette + private let glowPurple = Color(red: 0.6, green: 0.4, blue: 1.0) + private let glowBlue = Color(red: 0.3, green: 0.6, blue: 1.0) + private let glowPink = Color(red: 1.0, green: 0.5, blue: 0.7) + + var body: some View { + ZStack { + // Gradient background with floating orbs + backgroundLayer + + ScrollView { + VStack(spacing: 24) { + // HERO GLASS CARD + heroGlassCard + .padding(.top, 20) + .padding(.horizontal, 16) + + // FEATURED TRIPS + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.horizontal, 16) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.horizontal, 16) + } + + Spacer(minLength: 32) + } + } + } + } + + // MARK: - Background Layer + + private var backgroundLayer: some View { + ZStack { + // Base gradient + LinearGradient( + colors: colorScheme == .dark + ? [Color(red: 0.1, green: 0.05, blue: 0.2), Color(red: 0.05, green: 0.1, blue: 0.2)] + : [Color(red: 0.95, green: 0.93, blue: 1.0), Color(red: 0.9, green: 0.95, blue: 1.0)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + + // Floating orbs (stadium lights effect) + Circle() + .fill(glowPurple.opacity(0.3)) + .frame(width: 300, height: 300) + .blur(radius: 80) + .offset(x: -100, y: -200) + + Circle() + .fill(glowBlue.opacity(0.3)) + .frame(width: 250, height: 250) + .blur(radius: 70) + .offset(x: 120, y: 100) + + Circle() + .fill(glowPink.opacity(0.2)) + .frame(width: 200, height: 200) + .blur(radius: 60) + .offset(x: -80, y: 400) + } + } + + // MARK: - Hero Glass Card + + private var heroGlassCard: some View { + VStack(alignment: .leading, spacing: 20) { + // Floating label + Text("Welcome to") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(glowPurple) + + // Title with soft glow + Text("Sports Time") + .font(.system(size: 36, weight: .bold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + + Text("Plan your perfect sports road trip with our intelligent route optimizer.") + .font(.system(size: 15, weight: .regular)) + .foregroundStyle(colorScheme == .dark ? .white.opacity(0.7) : Color(white: 0.4)) + .lineSpacing(4) + + // Glass button + Button { + showNewTrip = true + } label: { + HStack(spacing: 12) { + Image(systemName: "sparkles") + .font(.system(size: 16)) + + Text("Start Planning") + .font(.system(size: 16, weight: .semibold, design: .rounded)) + + Spacer() + + Image(systemName: "arrow.right.circle.fill") + .font(.system(size: 20)) + } + .foregroundStyle(.white) + .padding(18) + .background( + LinearGradient( + colors: [glowPurple, glowBlue], + startPoint: .leading, + endPoint: .trailing + ) + ) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .shadow(color: glowPurple.opacity(0.4), radius: 20, x: 0, y: 10) + } + } + .padding(24) + .background(glassBackground) + .clipShape(RoundedRectangle(cornerRadius: 24)) + } + + // MARK: - Glass Background + + private var glassBackground: some View { + ZStack { + // Frosted glass effect + if colorScheme == .dark { + Color.white.opacity(0.08) + } else { + Color.white.opacity(0.6) + } + } + .background(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: 24) + .stroke( + LinearGradient( + colors: [ + Color.white.opacity(colorScheme == .dark ? 0.3 : 0.8), + Color.white.opacity(colorScheme == .dark ? 0.1 : 0.3) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 1 + ) + ) + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Featured Trips") + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14)) + .foregroundStyle(glowPurple) + .padding(10) + .background(glassCardBackground) + .clipShape(Circle()) + } + } + + // Horizontal scroll of glass cards + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(5)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + glassTripCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.vertical, 4) + } + } + } + + private func glassTripCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 12) { + // Sport icons with glow + HStack(spacing: 8) { + ForEach(Array(trip.uniqueSports.prefix(3)), id: \.self) { sport in + Image(systemName: sport.iconName) + .font(.system(size: 14)) + .foregroundStyle(sport.themeColor) + .shadow(color: sport.themeColor.opacity(0.5), radius: 8) + } + } + + Text(trip.name) + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + .lineLimit(2) + + Spacer(minLength: 0) + + HStack { + Label("\(trip.stops.count)", systemImage: "mappin.circle") + Spacer() + Label("\(trip.totalGames)", systemImage: "sportscourt") + } + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(colorScheme == .dark ? .white.opacity(0.6) : Color(white: 0.4)) + } + .padding(16) + .frame(width: 160, height: 140) + .background(glassCardBackground) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(Color.white.opacity(colorScheme == .dark ? 0.15 : 0.5), lineWidth: 1) + ) + } + + private var glassCardBackground: some View { + ZStack { + if colorScheme == .dark { + Color.white.opacity(0.06) + } else { + Color.white.opacity(0.5) + } + } + .background(.ultraThinMaterial) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Your Trips") + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("See All") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(glowPurple) + } + } + + VStack(spacing: 12) { + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + HStack(spacing: 16) { + // Glow orb + ZStack { + Circle() + .fill( + LinearGradient( + colors: [glowPurple.opacity(0.3), glowBlue.opacity(0.3)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 44, height: 44) + .blur(radius: 4) + + Image(systemName: "map.fill") + .font(.system(size: 16)) + .foregroundStyle(glowPurple) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + + Text("\(trip.stops.count) cities · \(trip.totalGames) games") + .font(.system(size: 12)) + .foregroundStyle(colorScheme == .dark ? .white.opacity(0.5) : Color(white: 0.5)) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(colorScheme == .dark ? .white.opacity(0.3) : Color(white: 0.5)) + } + .padding(16) + .background(glassCardBackground) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.white.opacity(colorScheme == .dark ? 0.1 : 0.4), lineWidth: 1) + ) + } + .buttonStyle(.plain) + } + } + } + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/LuxuryEditorial/HomeContent_LuxuryEditorial.swift b/SportsTime/Features/Home/Views/Variants/LuxuryEditorial/HomeContent_LuxuryEditorial.swift new file mode 100644 index 0000000..994ced5 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/LuxuryEditorial/HomeContent_LuxuryEditorial.swift @@ -0,0 +1,339 @@ +// +// HomeContent_LuxuryEditorial.swift +// SportsTime +// +// LUXURY EDITORIAL: Magazine-quality, dramatic typography. +// Elegant serif headlines, cinematic composition, gold accents. +// Premium sports journalism aesthetic. +// + +import SwiftUI +import SwiftData + +struct HomeContent_LuxuryEditorial: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + private let gold = Color(red: 0.85, green: 0.65, blue: 0.13) + + private var bgColor: Color { + colorScheme == .dark ? Color(white: 0.08) : Color(white: 0.98) + } + + private var textPrimary: Color { + colorScheme == .dark ? .white : Color(white: 0.1) + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.6) : Color(white: 0.4) + } + + var body: some View { + ScrollView { + VStack(spacing: 0) { + // MASTHEAD + masthead + .padding(.top, 20) + .padding(.bottom, 40) + + // HERO FEATURE + heroFeature + .padding(.horizontal, 24) + .padding(.bottom, 48) + + // EDITORIAL DIVIDER + editorialDivider + .padding(.bottom, 48) + + // FEATURED TRIPS - Magazine Grid + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.horizontal, 24) + .padding(.bottom, 48) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.horizontal, 24) + .padding(.bottom, 48) + } + + // COLOPHON + colophon + .padding(.bottom, 32) + } + } + .background(bgColor) + } + + // MARK: - Masthead + + private var masthead: some View { + VStack(spacing: 4) { + // Thin rule + Rectangle() + .fill(textSecondary) + .frame(height: 0.5) + .padding(.horizontal, 24) + + HStack { + Text(Date.now.formatted(.dateTime.month(.wide).year())) + .font(.system(size: 10, weight: .medium, design: .serif)) + .tracking(2) + .foregroundStyle(textSecondary) + + Spacer() + + Text("SPORTS TIME") + .font(.system(size: 10, weight: .semibold)) + .tracking(4) + .foregroundStyle(gold) + + Spacer() + + Text("TRAVEL EDITION") + .font(.system(size: 10, weight: .medium, design: .serif)) + .tracking(2) + .foregroundStyle(textSecondary) + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + + // Thin rule + Rectangle() + .fill(textSecondary) + .frame(height: 0.5) + .padding(.horizontal, 24) + } + } + + // MARK: - Hero Feature + + private var heroFeature: some View { + VStack(alignment: .leading, spacing: 20) { + // Kicker + Text("THE JOURNEY BEGINS") + .font(.system(size: 11, weight: .semibold)) + .tracking(3) + .foregroundStyle(gold) + + // Headline - Large serif + Text("Your Ultimate\nSports Road Trip\nAwaits") + .font(.system(size: 42, weight: .regular, design: .serif)) + .foregroundStyle(textPrimary) + .lineSpacing(4) + + // Deck + Text("Meticulously planned routes connecting the greatest stadiums, arenas, and ballparks across America.") + .font(.system(size: 16, weight: .regular, design: .serif)) + .foregroundStyle(textSecondary) + .lineSpacing(6) + .italic() + + // CTA - Elegant button + Button { + showNewTrip = true + } label: { + HStack(spacing: 12) { + Text("Begin Planning") + .font(.system(size: 14, weight: .medium, design: .serif)) + .tracking(1) + + Image(systemName: "arrow.right") + .font(.system(size: 12, weight: .medium)) + } + .foregroundStyle(colorScheme == .dark ? .black : .white) + .padding(.horizontal, 32) + .padding(.vertical, 16) + .background(gold) + } + .padding(.top, 8) + } + } + + // MARK: - Editorial Divider + + private var editorialDivider: some View { + HStack(spacing: 16) { + Rectangle() + .fill(textSecondary.opacity(0.3)) + .frame(height: 0.5) + + Image(systemName: "diamond.fill") + .font(.system(size: 6)) + .foregroundStyle(gold) + + Rectangle() + .fill(textSecondary.opacity(0.3)) + .frame(height: 0.5) + } + .padding(.horizontal, 48) + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 24) { + // Section header + HStack(alignment: .firstTextBaseline) { + Text("Featured Itineraries") + .font(.system(size: 24, weight: .regular, design: .serif)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 12)) + .foregroundStyle(gold) + } + } + + // Magazine grid - 2 column + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + editorialTripCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + } + } + + private func editorialTripCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 12) { + // Sport badge + HStack(spacing: 4) { + ForEach(Array(trip.uniqueSports.prefix(2)), id: \.self) { sport in + Text(sport.rawValue.uppercased()) + .font(.system(size: 9, weight: .semibold)) + .tracking(1) + .foregroundStyle(gold) + } + } + + // Title + Text(trip.name) + .font(.system(size: 18, weight: .regular, design: .serif)) + .foregroundStyle(textPrimary) + .lineLimit(2) + .multilineTextAlignment(.leading) + + // Stats + Text("\(trip.stops.count) Cities · \(trip.totalGames) Games") + .font(.system(size: 11, weight: .regular, design: .serif)) + .foregroundStyle(textSecondary) + .italic() + + Spacer(minLength: 0) + + // Read more + HStack(spacing: 4) { + Text("View Details") + .font(.system(size: 10, weight: .medium)) + .tracking(1) + Image(systemName: "arrow.right") + .font(.system(size: 8)) + } + .foregroundStyle(gold) + } + .padding(16) + .frame(minHeight: 160) + .overlay( + Rectangle() + .stroke(textSecondary.opacity(0.2), lineWidth: 0.5) + ) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 20) { + HStack(alignment: .firstTextBaseline) { + Text("Your Collection") + .font(.system(size: 24, weight: .regular, design: .serif)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("View All") + .font(.system(size: 12, weight: .medium)) + .tracking(1) + .foregroundStyle(gold) + } + } + + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + HStack(alignment: .top, spacing: 16) { + // Number + Text(String(format: "%02d", (savedTrips.firstIndex(where: { $0.id == savedTrip.id }) ?? 0) + 1)) + .font(.system(size: 11, weight: .regular, design: .serif)) + .foregroundStyle(gold) + .frame(width: 20) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 16, weight: .regular, design: .serif)) + .foregroundStyle(textPrimary) + + Text(trip.formattedDateRange) + .font(.system(size: 11, weight: .regular, design: .serif)) + .foregroundStyle(textSecondary) + .italic() + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 10)) + .foregroundStyle(textSecondary) + } + .padding(.vertical, 16) + .overlay(alignment: .bottom) { + Rectangle() + .fill(textSecondary.opacity(0.15)) + .frame(height: 0.5) + } + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Colophon + + private var colophon: some View { + VStack(spacing: 8) { + Rectangle() + .fill(textSecondary.opacity(0.2)) + .frame(width: 40, height: 0.5) + + Text("SPORTS TIME") + .font(.system(size: 9, weight: .semibold)) + .tracking(4) + .foregroundStyle(textSecondary.opacity(0.5)) + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/MaximalistChaos/HomeContent_MaximalistChaos.swift b/SportsTime/Features/Home/Views/Variants/MaximalistChaos/HomeContent_MaximalistChaos.swift new file mode 100644 index 0000000..35ea5bc --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/MaximalistChaos/HomeContent_MaximalistChaos.swift @@ -0,0 +1,435 @@ +// +// HomeContent_MaximalistChaos.swift +// SportsTime +// +// MAXIMALIST CHAOS: More is more. Patterns, gradients, overlapping elements. +// Controlled visual chaos with sports memorabilia collage aesthetic. +// + +import SwiftUI +import SwiftData + +struct HomeContent_MaximalistChaos: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Chaotic color palette + private let hotOrange = Color(red: 1.0, green: 0.4, blue: 0.0) + private let electricPurple = Color(red: 0.6, green: 0.0, blue: 1.0) + private let limeGreen = Color(red: 0.5, green: 1.0, blue: 0.0) + private let hotPink = Color(red: 1.0, green: 0.0, blue: 0.5) + private let skyBlue = Color(red: 0.0, green: 0.7, blue: 1.0) + + var body: some View { + ZStack { + // Chaotic background pattern + chaoticBackground + + ScrollView { + ZStack { + // Floating decorative elements + floatingDecorations + + VStack(spacing: 20) { + // MAXIMALIST HERO + maximalistHero + .padding(.top, 40) + .padding(.horizontal, 16) + + // FEATURED TRIPS - Stacked chaos + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.horizontal, 16) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.horizontal, 16) + } + + Spacer(minLength: 60) + } + } + } + } + } + + // MARK: - Chaotic Background + + private var chaoticBackground: some View { + ZStack { + // Base gradient + LinearGradient( + colors: colorScheme == .dark + ? [Color(red: 0.1, green: 0.0, blue: 0.15), Color(red: 0.0, green: 0.1, blue: 0.15)] + : [Color(red: 1.0, green: 0.95, blue: 0.9), Color(red: 0.95, green: 0.9, blue: 1.0)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + .ignoresSafeArea() + + // Diagonal stripes + GeometryReader { geo in + ForEach(0..<20, id: \.self) { i in + Rectangle() + .fill(hotOrange.opacity(0.05)) + .frame(width: 30, height: geo.size.height * 2) + .rotationEffect(.degrees(45)) + .offset(x: CGFloat(i * 60) - 200) + } + } + .allowsHitTesting(false) + + // Random circles + Circle() + .fill(electricPurple.opacity(0.1)) + .frame(width: 300, height: 300) + .offset(x: 150, y: -100) + + Circle() + .fill(limeGreen.opacity(0.1)) + .frame(width: 200, height: 200) + .offset(x: -100, y: 300) + + Circle() + .fill(hotPink.opacity(0.08)) + .frame(width: 250, height: 250) + .offset(x: 100, y: 500) + } + } + + // MARK: - Floating Decorations + + private var floatingDecorations: some View { + ZStack { + // Sports icons scattered + Image(systemName: "sportscourt.fill") + .font(.system(size: 60)) + .foregroundStyle(hotOrange.opacity(0.15)) + .rotationEffect(.degrees(-15)) + .offset(x: 120, y: 50) + + Image(systemName: "ticket.fill") + .font(.system(size: 50)) + .foregroundStyle(electricPurple.opacity(0.15)) + .rotationEffect(.degrees(20)) + .offset(x: -100, y: 200) + + Image(systemName: "car.fill") + .font(.system(size: 45)) + .foregroundStyle(limeGreen.opacity(0.15)) + .rotationEffect(.degrees(-10)) + .offset(x: 80, y: 400) + + Image(systemName: "map.fill") + .font(.system(size: 55)) + .foregroundStyle(hotPink.opacity(0.12)) + .rotationEffect(.degrees(25)) + .offset(x: -80, y: 600) + } + .allowsHitTesting(false) + } + + // MARK: - Maximalist Hero + + private var maximalistHero: some View { + ZStack { + // Multiple layered backgrounds + RoundedRectangle(cornerRadius: 20) + .fill(electricPurple.opacity(colorScheme == .dark ? 0.3 : 0.15)) + .offset(x: 8, y: 8) + .rotationEffect(.degrees(2)) + + RoundedRectangle(cornerRadius: 20) + .fill(hotOrange.opacity(colorScheme == .dark ? 0.3 : 0.15)) + .offset(x: -4, y: 4) + .rotationEffect(.degrees(-1)) + + // Main card + VStack(spacing: 16) { + // Stacked badges + HStack(spacing: 8) { + ForEach(["🏟️", "🚗", "⚾", "🏀", "🏈"], id: \.self) { emoji in + Text(emoji) + .font(.system(size: 20)) + .padding(8) + .background( + Circle() + .fill(Color.white.opacity(colorScheme == .dark ? 0.1 : 0.8)) + ) + } + } + + // Title with multiple colors + HStack(spacing: 4) { + Text("SPORTS") + .foregroundStyle(hotOrange) + Text("TIME") + .foregroundStyle(electricPurple) + Text("!") + .foregroundStyle(limeGreen) + } + .font(.system(size: 36, weight: .black, design: .rounded)) + + // Subtitle in pill + Text("THE ULTIMATE ROAD TRIP EXPERIENCE") + .font(.system(size: 11, weight: .bold)) + .tracking(2) + .foregroundStyle(.white) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background( + Capsule() + .fill( + LinearGradient( + colors: [hotPink, electricPurple], + startPoint: .leading, + endPoint: .trailing + ) + ) + ) + + // CTA Button - Maximalist style + Button { + showNewTrip = true + } label: { + ZStack { + // Shadow layers + RoundedRectangle(cornerRadius: 16) + .fill(limeGreen) + .offset(x: 4, y: 4) + + RoundedRectangle(cornerRadius: 16) + .fill(hotPink) + .offset(x: 2, y: 2) + + HStack { + Text("START PLANNING") + .font(.system(size: 16, weight: .black)) + + Image(systemName: "arrow.right.circle.fill") + .font(.system(size: 20)) + } + .foregroundStyle(.black) + .padding(.horizontal, 24) + .padding(.vertical, 16) + .background(hotOrange) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + } + .padding(.top, 8) + } + .padding(24) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(colorScheme == .dark ? Color.white.opacity(0.1) : Color.white.opacity(0.9)) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke( + LinearGradient( + colors: [hotOrange, electricPurple, limeGreen, hotPink], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: 3 + ) + ) + ) + } + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 16) { + // Header with multiple elements + HStack { + // Stacked text + VStack(alignment: .leading, spacing: 0) { + Text("FEATURED") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(hotPink) + Text("TRIPS") + .font(.system(size: 22, weight: .black)) + .foregroundStyle(colorScheme == .dark ? .white : .black) + } + + Spacer() + + // Refresh with chaos + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + ZStack { + Circle() + .fill(limeGreen.opacity(0.3)) + .frame(width: 44, height: 44) + .offset(x: 2, y: 2) + + Image(systemName: "arrow.clockwise") + .font(.system(size: 16, weight: .bold)) + .foregroundStyle(limeGreen) + .padding(12) + .background(Circle().fill(colorScheme == .dark ? Color.white.opacity(0.1) : .white)) + } + } + } + + // Chaotic card stack + ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + chaoticTripCard(suggestedTrip.trip, colorIndex: index) + } + .buttonStyle(.plain) + } + } + } + + private func chaoticTripCard(_ trip: Trip, colorIndex: Int) -> some View { + let colors = [hotOrange, electricPurple, limeGreen, hotPink, skyBlue] + let accentColor = colors[colorIndex % colors.count] + + return ZStack { + // Shadow offset + RoundedRectangle(cornerRadius: 16) + .fill(accentColor.opacity(0.4)) + .offset(x: 4, y: 4) + + HStack(spacing: 14) { + // Sport icon in chaotic circle + ZStack { + Circle() + .fill(accentColor.opacity(0.3)) + .frame(width: 54, height: 54) + .offset(x: 2, y: 2) + + Circle() + .fill(colorScheme == .dark ? Color.white.opacity(0.15) : .white) + .frame(width: 50, height: 50) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt") + .font(.system(size: 22)) + .foregroundStyle(accentColor) + } + + VStack(alignment: .leading, spacing: 6) { + Text(trip.name) + .font(.system(size: 16, weight: .bold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : .black) + .lineLimit(1) + + HStack(spacing: 8) { + Text("\(trip.stops.count) CITIES") + .font(.system(size: 10, weight: .black)) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(accentColor) + .foregroundStyle(.white) + + Text("\(trip.totalGames) GAMES") + .font(.system(size: 10, weight: .black)) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(colors[(colorIndex + 1) % colors.count]) + .foregroundStyle(.white) + } + } + + Spacer() + + Image(systemName: "chevron.right.circle.fill") + .font(.system(size: 24)) + .foregroundStyle(accentColor) + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(colorScheme == .dark ? Color.white.opacity(0.08) : Color.white.opacity(0.95)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(accentColor.opacity(0.5), lineWidth: 2) + ) + ) + } + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + VStack(alignment: .leading, spacing: 0) { + Text("YOUR") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(skyBlue) + Text("TRIPS") + .font(.system(size: 22, weight: .black)) + .foregroundStyle(colorScheme == .dark ? .white : .black) + } + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("VIEW ALL →") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(skyBlue) + } + } + + ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + let colors = [skyBlue, hotOrange, electricPurple] + let accentColor = colors[index % colors.count] + + HStack { + Rectangle() + .fill(accentColor) + .frame(width: 6) + + Text(trip.name) + .font(.system(size: 14, weight: .semibold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : .black) + + Spacer() + + Text("\(trip.stops.count)") + .font(.system(size: 14, weight: .black)) + .foregroundStyle(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(accentColor) + .clipShape(Capsule()) + } + .padding(.vertical, 14) + .padding(.horizontal, 12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.8)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(accentColor.opacity(0.3), lineWidth: 1) + ) + ) + } + .buttonStyle(.plain) + } + } + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/NeoBrutalist/HomeContent_NeoBrutalist.swift b/SportsTime/Features/Home/Views/Variants/NeoBrutalist/HomeContent_NeoBrutalist.swift new file mode 100644 index 0000000..2a1f83b --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/NeoBrutalist/HomeContent_NeoBrutalist.swift @@ -0,0 +1,321 @@ +// +// HomeContent_NeoBrutalist.swift +// SportsTime +// +// NEO-BRUTALIST: Bold blocks, harsh shadows, high contrast. +// Offset elements, thick borders, punchy colors. +// Ticket stub aesthetic with hard edges. +// + +import SwiftUI +import SwiftData + +struct HomeContent_NeoBrutalist: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Neo-brutalist palette + private let primaryYellow = Color(red: 1.0, green: 0.9, blue: 0.0) + private let hotPink = Color(red: 1.0, green: 0.2, blue: 0.6) + private let electricBlue = Color(red: 0.2, green: 0.4, blue: 1.0) + + private var bgColor: Color { + colorScheme == .dark ? Color(white: 0.08) : Color(white: 0.95) + } + + private var textColor: Color { + colorScheme == .dark ? .white : .black + } + + var body: some View { + ScrollView { + VStack(spacing: 24) { + // HERO BLOCK with offset shadow + heroBlock + .padding(.top, 16) + .padding(.horizontal, 16) + + // ACTION BUTTONS - Bold stacked blocks + actionBlocks + .padding(.horizontal, 16) + + // FEATURED TRIPS + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.horizontal, 16) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.horizontal, 16) + } + + Spacer(minLength: 32) + } + } + .background(bgColor) + } + + // MARK: - Hero Block + + private var heroBlock: some View { + ZStack { + // Shadow block (offset) + RoundedRectangle(cornerRadius: 0) + .fill(textColor) + .offset(x: 8, y: 8) + + // Main block + VStack(alignment: .leading, spacing: 16) { + // Badge + Text("SPORTS TRIP PLANNER") + .font(.system(size: 10, weight: .black)) + .tracking(2) + .foregroundStyle(bgColor) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(primaryYellow) + + // Main title + VStack(alignment: .leading, spacing: 4) { + Text("PLAN YOUR") + .font(.system(size: 32, weight: .black)) + .foregroundStyle(textColor) + + HStack(spacing: 0) { + Text("ROAD") + .font(.system(size: 32, weight: .black)) + .foregroundStyle(hotPink) + Text(" TRIP") + .font(.system(size: 32, weight: .black)) + .foregroundStyle(textColor) + } + } + + // Description + Text("Hit every stadium. Catch every game. Make memories that last.") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(textColor.opacity(0.7)) + } + .padding(24) + .frame(maxWidth: .infinity, alignment: .leading) + .background(bgColor) + .border(textColor, width: 3) + } + } + + // MARK: - Action Blocks + + private var actionBlocks: some View { + VStack(spacing: 12) { + // Primary CTA + Button { + showNewTrip = true + } label: { + ZStack { + // Shadow + Rectangle() + .fill(textColor) + .offset(x: 6, y: 6) + + HStack { + Text("START PLANNING") + .font(.system(size: 16, weight: .black)) + .tracking(1) + + Spacer() + + Text("→") + .font(.system(size: 24, weight: .black)) + } + .foregroundStyle(.black) + .padding(20) + .background(primaryYellow) + .border(.black, width: 3) + } + } + .frame(height: 70) + + // Secondary actions + HStack(spacing: 12) { + Button { + selectedTab = 1 + } label: { + secondaryBlock(text: "SCHEDULE", color: electricBlue) + } + + Button { + selectedTab = 2 + } label: { + secondaryBlock(text: "MY TRIPS", color: hotPink) + } + } + } + } + + private func secondaryBlock(text: String, color: Color) -> some View { + ZStack { + Rectangle() + .fill(textColor) + .offset(x: 4, y: 4) + + Text(text) + .font(.system(size: 12, weight: .black)) + .tracking(1) + .foregroundStyle(.white) + .padding(16) + .frame(maxWidth: .infinity) + .background(color) + .border(.black, width: 2) + } + .frame(height: 56) + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 16) { + // Section header + HStack { + Text("FEATURED") + .font(.system(size: 12, weight: .black)) + .tracking(2) + .foregroundStyle(textColor) + + Rectangle() + .fill(primaryYellow) + .frame(height: 4) + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14, weight: .bold)) + .foregroundStyle(textColor) + } + } + + // Trip cards - stacked blocks + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + neoBrutalistTripCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + } + + private func neoBrutalistTripCard(_ trip: Trip) -> some View { + ZStack { + // Shadow + Rectangle() + .fill(textColor) + .offset(x: 5, y: 5) + + HStack(spacing: 16) { + // Color block indicator + Rectangle() + .fill(trip.uniqueSports.first?.themeColor ?? primaryYellow) + .frame(width: 8) + + VStack(alignment: .leading, spacing: 6) { + Text(trip.name.uppercased()) + .font(.system(size: 14, weight: .black)) + .foregroundStyle(textColor) + .lineLimit(1) + + HStack(spacing: 12) { + Text("\(trip.stops.count) CITIES") + .font(.system(size: 10, weight: .bold)) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(primaryYellow) + .foregroundStyle(.black) + + Text("\(trip.totalGames) GAMES") + .font(.system(size: 10, weight: .bold)) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(hotPink) + .foregroundStyle(.white) + } + } + + Spacer() + + Text("→") + .font(.system(size: 18, weight: .black)) + .foregroundStyle(textColor) + } + .padding(16) + .background(bgColor) + .border(textColor, width: 2) + } + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("YOUR TRIPS") + .font(.system(size: 12, weight: .black)) + .tracking(2) + .foregroundStyle(textColor) + + Rectangle() + .fill(hotPink) + .frame(height: 4) + + Button { + selectedTab = 2 + } label: { + Text("ALL →") + .font(.system(size: 12, weight: .black)) + .foregroundStyle(hotPink) + } + } + + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + HStack { + Rectangle() + .fill(electricBlue) + .frame(width: 4) + + Text(trip.name.uppercased()) + .font(.system(size: 13, weight: .bold)) + .foregroundStyle(textColor) + + Spacer() + + Text("\(trip.stops.count)") + .font(.system(size: 12, weight: .black)) + .foregroundStyle(.black) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(primaryYellow) + } + .padding(12) + .border(textColor.opacity(0.3), width: 2) + } + .buttonStyle(.plain) + } + } + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/NikeRunClub/HomeContent_NikeRunClub.swift b/SportsTime/Features/Home/Views/Variants/NikeRunClub/HomeContent_NikeRunClub.swift new file mode 100644 index 0000000..1c5ade2 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/NikeRunClub/HomeContent_NikeRunClub.swift @@ -0,0 +1,298 @@ +// +// HomeContent_NikeRunClub.swift +// SportsTime +// +// NIKE RUN CLUB-INSPIRED: Athletic, bold stats. +// Dynamic feel, activity-focused design. +// Black/white with vibrant accents. +// + +import SwiftUI +import SwiftData + +struct HomeContent_NikeRunClub: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Nike-inspired colors + private var bgColor: Color { + colorScheme == .dark + ? Color.black + : Color.white + } + + private let nikeVolt = Color(red: 0.77, green: 1.0, blue: 0.0) + private let nikeOrange = Color(red: 1.0, green: 0.35, blue: 0.0) + + private var textPrimary: Color { + colorScheme == .dark ? .white : .black + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.5) : Color(white: 0.45) + } + + var body: some View { + ScrollView { + VStack(spacing: 24) { + // Hero stats + heroStats + .padding(.horizontal, 20) + .padding(.top, 16) + + // Start activity button + startButton + .padding(.horizontal, 20) + + // Activity feed + if !savedTrips.isEmpty { + activityFeed + } + + // Challenges/suggestions + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + challengesSection + } + + Spacer(minLength: 50) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Hero Stats + + private var heroStats: some View { + VStack(spacing: 8) { + Text("SPORTS TIME") + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(2) + + Text("\(savedTrips.count)") + .font(.system(size: 72, weight: .bold)) + .foregroundStyle(textPrimary) + + Text("TRIPS PLANNED") + .font(.system(size: 13, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(1) + + // Stats row + HStack(spacing: 32) { + statItem(value: "\(totalGames)", label: "GAMES") + statItem(value: "\(totalStops)", label: "CITIES") + statItem(value: "\(uniqueSports)", label: "SPORTS") + } + .padding(.top, 20) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 32) + } + + private func statItem(value: String, label: String) -> some View { + VStack(spacing: 4) { + Text(value) + .font(.system(size: 28, weight: .bold)) + .foregroundStyle(textPrimary) + + Text(label) + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(textSecondary) + .tracking(0.5) + } + } + + private var totalGames: Int { + savedTrips.compactMap { $0.trip?.totalGames }.reduce(0, +) + } + + private var totalStops: Int { + savedTrips.compactMap { $0.trip?.stops.count }.reduce(0, +) + } + + private var uniqueSports: Int { + Set(savedTrips.flatMap { $0.trip?.uniqueSports ?? [] }).count + } + + // MARK: - Start Button + + private var startButton: some View { + Button { + showNewTrip = true + } label: { + HStack { + Text("START") + .font(.system(size: 18, weight: .bold)) + .tracking(1) + } + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .background( + RoundedRectangle(cornerRadius: 30) + .fill(nikeVolt) + ) + } + .buttonStyle(.plain) + } + + // MARK: - Activity Feed + + private var activityFeed: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("ACTIVITY") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(1) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("See All") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(textPrimary) + } + } + .padding(.horizontal, 20) + + VStack(spacing: 0) { + ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + activityRow(trip, index: index) + } + .buttonStyle(.plain) + } + } + } + } + } + + private func activityRow(_ trip: Trip, index: Int) -> some View { + HStack(spacing: 16) { + // Activity type indicator + ZStack { + Circle() + .fill(index == 0 ? nikeVolt : nikeOrange) + .frame(width: 44, height: 44) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(.black) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text(trip.formattedDateRange) + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text("\(trip.totalGames)") + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(textPrimary) + + Text("games") + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + } + .padding(.horizontal, 20) + .padding(.vertical, 14) + .background( + Rectangle() + .fill(colorScheme == .dark ? Color(white: 0.08) : Color(white: 0.97)) + ) + } + + // MARK: - Challenges Section + + private var challengesSection: some View { + VStack(alignment: .leading, spacing: 16) { + Text("SUGGESTED ROUTES") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(textSecondary) + .tracking(1) + .padding(.horizontal, 20) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 14) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + challengeCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 20) + } + } + } + + private func challengeCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 12) { + // Challenge header + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: 12) + .fill( + LinearGradient( + colors: [nikeOrange, nikeOrange.opacity(0.7)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(height: 100) + + VStack(alignment: .leading, spacing: 4) { + Text("CHALLENGE") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(.white.opacity(0.8)) + .tracking(1) + + Text("\(trip.totalGames) GAMES") + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(.white) + } + .padding(14) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) cities") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + } + .frame(width: 180) + .padding(12) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(colorScheme == .dark ? Color(white: 0.1) : Color(white: 0.96)) + ) + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Organic/HomeContent_Organic.swift b/SportsTime/Features/Home/Views/Variants/Organic/HomeContent_Organic.swift new file mode 100644 index 0000000..1d21ed3 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Organic/HomeContent_Organic.swift @@ -0,0 +1,314 @@ +// +// HomeContent_Organic.swift +// SportsTime +// +// ORGANIC: Soft curves, earthy tones, breathing life. +// Natural stadium grass vibes, flowing shapes, gentle animations. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Organic: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Earthy organic palette + private let leafGreen = Color(red: 0.35, green: 0.65, blue: 0.35) + private let earthBrown = Color(red: 0.55, green: 0.4, blue: 0.3) + private let warmSand = Color(red: 0.95, green: 0.9, blue: 0.8) + private let skyBlue = Color(red: 0.6, green: 0.8, blue: 0.95) + + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.1, green: 0.12, blue: 0.1) + : warmSand + } + + private var textPrimary: Color { + colorScheme == .dark ? Color(white: 0.9) : earthBrown + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.6) : earthBrown.opacity(0.7) + } + + var body: some View { + ScrollView { + VStack(spacing: 28) { + // ORGANIC HERO + organicHero + .padding(.top, 16) + .padding(.horizontal, 20) + + // FEATURED TRIPS - Flowing cards + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.horizontal, 20) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.horizontal, 20) + } + + // FOOTER LEAF + footerLeaf + .padding(.bottom, 32) + } + } + .background(bgColor) + } + + // MARK: - Organic Hero + + private var organicHero: some View { + VStack(spacing: 20) { + // Organic shape header + ZStack { + // Background blob + Ellipse() + .fill(leafGreen.opacity(0.15)) + .frame(width: 280, height: 140) + .rotationEffect(.degrees(-5)) + + Ellipse() + .fill(skyBlue.opacity(0.1)) + .frame(width: 200, height: 100) + .offset(x: 60, y: 20) + .rotationEffect(.degrees(10)) + + VStack(spacing: 8) { + // Leaf icon + Image(systemName: "leaf.fill") + .font(.system(size: 28)) + .foregroundStyle(leafGreen) + + Text("Sports Time") + .font(.system(size: 32, weight: .semibold, design: .rounded)) + .foregroundStyle(textPrimary) + + Text("Journey naturally") + .font(.system(size: 14, weight: .medium, design: .rounded)) + .foregroundStyle(textSecondary) + .italic() + } + } + .padding(.vertical, 20) + + // Description in organic container + Text("Let your sports adventure unfold organically. We'll guide you through the most scenic routes connecting America's greatest stadiums.") + .font(.system(size: 15, weight: .regular, design: .rounded)) + .foregroundStyle(textSecondary) + .multilineTextAlignment(.center) + .lineSpacing(6) + .padding(.horizontal, 16) + + // Organic CTA button + Button { + showNewTrip = true + } label: { + HStack(spacing: 12) { + Image(systemName: "arrow.right.circle") + .font(.system(size: 18)) + + Text("Begin Your Journey") + .font(.system(size: 16, weight: .semibold, design: .rounded)) + } + .foregroundStyle(.white) + .padding(.horizontal, 32) + .padding(.vertical, 16) + .background( + Capsule() + .fill(leafGreen) + ) + .shadow(color: leafGreen.opacity(0.3), radius: 15, y: 8) + } + } + .padding(24) + .background( + RoundedRectangle(cornerRadius: 32) + .fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.7)) + ) + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Image(systemName: "leaf.fill") + .font(.system(size: 14)) + .foregroundStyle(leafGreen) + + Text("Featured Journeys") + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.triangle.2.circlepath") + .font(.system(size: 14)) + .foregroundStyle(leafGreen) + } + } + + // Organic flowing cards + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + organicTripCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + } + + private func organicTripCard(_ trip: Trip) -> some View { + HStack(spacing: 16) { + // Organic circle with sport color + ZStack { + Circle() + .fill(trip.uniqueSports.first?.themeColor.opacity(0.2) ?? leafGreen.opacity(0.2)) + .frame(width: 56, height: 56) + + Circle() + .fill(trip.uniqueSports.first?.themeColor.opacity(0.3) ?? leafGreen.opacity(0.3)) + .frame(width: 40, height: 40) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt") + .font(.system(size: 18)) + .foregroundStyle(trip.uniqueSports.first?.themeColor ?? leafGreen) + } + + VStack(alignment: .leading, spacing: 6) { + Text(trip.name) + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + HStack(spacing: 12) { + Label("\(trip.stops.count) stops", systemImage: "mappin.circle") + Label("\(trip.totalGames) games", systemImage: "sportscourt") + } + .font(.system(size: 12, weight: .medium, design: .rounded)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Image(systemName: "chevron.right.circle") + .font(.system(size: 20)) + .foregroundStyle(leafGreen.opacity(0.6)) + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 24) + .fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.8)) + ) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Image(systemName: "tree.fill") + .font(.system(size: 14)) + .foregroundStyle(earthBrown) + + Text("Your Journeys") + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("View all") + .font(.system(size: 14, weight: .medium, design: .rounded)) + .foregroundStyle(leafGreen) + } + } + + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + HStack(spacing: 14) { + // Organic dot cluster + ZStack { + Circle() + .fill(earthBrown.opacity(0.2)) + .frame(width: 8, height: 8) + .offset(x: -6, y: -4) + Circle() + .fill(leafGreen.opacity(0.3)) + .frame(width: 10, height: 10) + .offset(x: 4, y: 2) + Circle() + .fill(skyBlue.opacity(0.3)) + .frame(width: 6, height: 6) + .offset(x: -2, y: 6) + } + .frame(width: 24, height: 24) + + VStack(alignment: .leading, spacing: 2) { + Text(trip.name) + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(textPrimary) + + Text(trip.formattedDateRange) + .font(.system(size: 12, design: .rounded)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Capsule() + .fill(leafGreen.opacity(0.15)) + .frame(width: 40, height: 24) + .overlay( + Text("\(trip.stops.count)") + .font(.system(size: 12, weight: .semibold, design: .rounded)) + .foregroundStyle(leafGreen) + ) + } + .padding(.vertical, 12) + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Footer Leaf + + private var footerLeaf: some View { + VStack(spacing: 8) { + Image(systemName: "leaf.fill") + .font(.system(size: 16)) + .foregroundStyle(leafGreen.opacity(0.4)) + .rotationEffect(.degrees(45)) + + Text("Sports Time") + .font(.system(size: 11, weight: .medium, design: .rounded)) + .foregroundStyle(textSecondary.opacity(0.5)) + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Playful/HomeContent_Playful.swift b/SportsTime/Features/Home/Views/Variants/Playful/HomeContent_Playful.swift new file mode 100644 index 0000000..594d08c --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Playful/HomeContent_Playful.swift @@ -0,0 +1,372 @@ +// +// HomeContent_Playful.swift +// SportsTime +// +// PLAYFUL: Bouncy, toy-like, rounded everything. +// Bright candy colors, wobbly shapes, fun animations. +// Sports meets playground aesthetic. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Playful: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Candy color palette + private let candyPink = Color(red: 1.0, green: 0.4, blue: 0.6) + private let candyBlue = Color(red: 0.4, green: 0.7, blue: 1.0) + private let candyYellow = Color(red: 1.0, green: 0.85, blue: 0.3) + private let candyGreen = Color(red: 0.4, green: 0.9, blue: 0.6) + private let candyPurple = Color(red: 0.7, green: 0.5, blue: 1.0) + + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.12, green: 0.1, blue: 0.18) + : Color(red: 0.98, green: 0.97, blue: 1.0) + } + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + // Floating blobs + floatingBlobs + + ScrollView { + VStack(spacing: 28) { + // PLAYFUL HERO + playfulHero + .padding(.top, 20) + .padding(.horizontal, 20) + + // FEATURED TRIPS + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.horizontal, 20) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.horizontal, 20) + } + + // FUN FOOTER + funFooter + .padding(.bottom, 32) + } + } + } + } + + // MARK: - Floating Blobs + + private var floatingBlobs: some View { + ZStack { + Ellipse() + .fill(candyPink.opacity(0.15)) + .frame(width: 200, height: 160) + .rotationEffect(.degrees(-20)) + .offset(x: -100, y: -150) + + Ellipse() + .fill(candyBlue.opacity(0.15)) + .frame(width: 180, height: 140) + .rotationEffect(.degrees(15)) + .offset(x: 120, y: 100) + + Ellipse() + .fill(candyYellow.opacity(0.12)) + .frame(width: 150, height: 120) + .rotationEffect(.degrees(-10)) + .offset(x: -60, y: 400) + + Ellipse() + .fill(candyGreen.opacity(0.1)) + .frame(width: 130, height: 100) + .rotationEffect(.degrees(25)) + .offset(x: 100, y: 550) + } + .allowsHitTesting(false) + } + + // MARK: - Playful Hero + + private var playfulHero: some View { + VStack(spacing: 20) { + // Bouncy mascot area + ZStack { + // Background wobble + RoundedRectangle(cornerRadius: 40) + .fill(candyYellow.opacity(0.3)) + .frame(width: 140, height: 100) + .rotationEffect(.degrees(-5)) + + RoundedRectangle(cornerRadius: 40) + .fill(candyPink.opacity(0.3)) + .frame(width: 120, height: 80) + .offset(x: 30, y: 10) + .rotationEffect(.degrees(5)) + + // Sports emojis + HStack(spacing: 12) { + Text("⚾") + .font(.system(size: 32)) + .rotationEffect(.degrees(-10)) + Text("🏀") + .font(.system(size: 36)) + Text("🏈") + .font(.system(size: 32)) + .rotationEffect(.degrees(10)) + } + } + .padding(.vertical, 16) + + // Title with bounce + VStack(spacing: 4) { + Text("Sports Time!") + .font(.system(size: 34, weight: .heavy, design: .rounded)) + .foregroundStyle( + LinearGradient( + colors: [candyPink, candyPurple], + startPoint: .leading, + endPoint: .trailing + ) + ) + + Text("Your adventure starts here") + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.7) : Color(white: 0.4)) + } + + // Playful CTA + Button { + showNewTrip = true + } label: { + HStack(spacing: 12) { + Text("Let's Go!") + .font(.system(size: 18, weight: .bold, design: .rounded)) + + Image(systemName: "arrow.right.circle.fill") + .font(.system(size: 22)) + } + .foregroundStyle(.white) + .padding(.horizontal, 36) + .padding(.vertical, 18) + .background( + Capsule() + .fill( + LinearGradient( + colors: [candyPink, candyPurple], + startPoint: .leading, + endPoint: .trailing + ) + ) + ) + .shadow(color: candyPink.opacity(0.4), radius: 20, y: 10) + } + } + .padding(28) + .background( + RoundedRectangle(cornerRadius: 36) + .fill(colorScheme == .dark ? Color.white.opacity(0.08) : Color.white.opacity(0.9)) + .shadow(color: Color.black.opacity(0.08), radius: 20, y: 8) + ) + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 18) { + HStack { + Text("Cool Trips") + .font(.system(size: 22, weight: .bold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + + Text("✨") + .font(.system(size: 18)) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 16, weight: .medium)) + .foregroundStyle(candyBlue) + .padding(12) + .background( + Circle() + .fill(candyBlue.opacity(0.15)) + ) + } + } + + ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + playfulTripCard(suggestedTrip.trip, colorIndex: index) + } + .buttonStyle(.plain) + } + } + } + + private func playfulTripCard(_ trip: Trip, colorIndex: Int) -> some View { + let colors = [candyPink, candyBlue, candyGreen, candyPurple, candyYellow] + let accentColor = colors[colorIndex % colors.count] + + return HStack(spacing: 16) { + // Bouncy icon + ZStack { + Circle() + .fill(accentColor.opacity(0.2)) + .frame(width: 56, height: 56) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 24)) + .foregroundStyle(accentColor) + } + + VStack(alignment: .leading, spacing: 6) { + Text(trip.name) + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + .lineLimit(1) + + HStack(spacing: 10) { + HStack(spacing: 4) { + Image(systemName: "mappin.circle.fill") + .font(.system(size: 12)) + Text("\(trip.stops.count)") + .font(.system(size: 13, weight: .medium, design: .rounded)) + } + .foregroundStyle(accentColor) + + HStack(spacing: 4) { + Image(systemName: "sportscourt.fill") + .font(.system(size: 12)) + Text("\(trip.totalGames)") + .font(.system(size: 13, weight: .medium, design: .rounded)) + } + .foregroundStyle(colorScheme == .dark ? .white.opacity(0.6) : Color(white: 0.5)) + } + } + + Spacer() + + Image(systemName: "chevron.right.circle.fill") + .font(.system(size: 24)) + .foregroundStyle(accentColor.opacity(0.5)) + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 24) + .fill(colorScheme == .dark ? Color.white.opacity(0.06) : Color.white.opacity(0.9)) + .shadow(color: accentColor.opacity(0.15), radius: 12, y: 4) + ) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 18) { + HStack { + Text("Your Trips") + .font(.system(size: 22, weight: .bold, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + + Text("🎒") + .font(.system(size: 18)) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("See all") + .font(.system(size: 14, weight: .medium, design: .rounded)) + .foregroundStyle(candyPurple) + } + } + + ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + let colors = [candyYellow, candyGreen, candyBlue] + let accentColor = colors[index % colors.count] + + HStack(spacing: 14) { + // Number bubble + Text("\(index + 1)") + .font(.system(size: 14, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + .frame(width: 32, height: 32) + .background( + Circle() + .fill(accentColor) + ) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15)) + + Text(trip.formattedDateRange) + .font(.system(size: 12, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white.opacity(0.5) : Color(white: 0.5)) + } + + Spacer() + + Capsule() + .fill(accentColor.opacity(0.2)) + .frame(width: 44, height: 28) + .overlay( + Text("\(trip.stops.count)") + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .foregroundStyle(accentColor) + ) + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.85)) + ) + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Fun Footer + + private var funFooter: some View { + VStack(spacing: 8) { + HStack(spacing: 8) { + Text("🏟️") + Text("🚗") + Text("🎉") + } + .font(.system(size: 16)) + + Text("Sports Time") + .font(.system(size: 12, weight: .medium, design: .rounded)) + .foregroundStyle(colorScheme == .dark ? .white.opacity(0.4) : Color(white: 0.5)) + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/RetroFuturism/HomeContent_RetroFuturism.swift b/SportsTime/Features/Home/Views/Variants/RetroFuturism/HomeContent_RetroFuturism.swift new file mode 100644 index 0000000..dceb4cb --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/RetroFuturism/HomeContent_RetroFuturism.swift @@ -0,0 +1,357 @@ +// +// HomeContent_RetroFuturism.swift +// SportsTime +// +// RETRO-FUTURISM: 80s sci-fi meets modern sports tech. +// Neon colors, CRT effects, chrome accents, sports broadcast graphics. +// + +import SwiftUI +import SwiftData + +struct HomeContent_RetroFuturism: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Retro color palette + private let neonCyan = Color(red: 0.0, green: 1.0, blue: 0.9) + private let neonMagenta = Color(red: 1.0, green: 0.0, blue: 0.8) + private let neonYellow = Color(red: 1.0, green: 1.0, blue: 0.0) + private let darkBg = Color(red: 0.05, green: 0.0, blue: 0.15) + private let chrome = Color(white: 0.85) + + var body: some View { + ZStack { + // Deep dark background + darkBg.ignoresSafeArea() + + // Scan lines overlay + scanLinesOverlay + + ScrollView { + VStack(spacing: 32) { + // RETRO HEADER + retroHeader + .padding(.top, 24) + + // NEON CTA BLOCK + neonCTABlock + .padding(.horizontal, 16) + + // FEATURED TRIPS - Broadcast style + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.horizontal, 16) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.horizontal, 16) + } + + // RETRO FOOTER + retroFooter + .padding(.bottom, 32) + } + } + } + } + + // MARK: - Scan Lines Overlay + + private var scanLinesOverlay: some View { + GeometryReader { geo in + VStack(spacing: 2) { + ForEach(0.. some View { + HStack(spacing: 12) { + // Sport icon with glow + ZStack { + Circle() + .fill(trip.uniqueSports.first?.themeColor.opacity(0.2) ?? neonCyan.opacity(0.2)) + .frame(width: 44, height: 44) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt") + .font(.system(size: 18)) + .foregroundStyle(trip.uniqueSports.first?.themeColor ?? neonCyan) + } + .shadow(color: trip.uniqueSports.first?.themeColor.opacity(0.5) ?? neonCyan.opacity(0.5), radius: 10) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name.uppercased()) + .font(.system(size: 14, weight: .bold, design: .rounded)) + .foregroundStyle(chrome) + + HStack(spacing: 8) { + Text("\(trip.stops.count) CITIES") + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(neonCyan) + + Text("•") + .foregroundStyle(chrome.opacity(0.3)) + + Text("\(trip.totalGames) GAMES") + .font(.system(size: 10, design: .monospaced)) + .foregroundStyle(neonMagenta) + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(neonCyan) + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 8) + .stroke( + LinearGradient( + colors: [neonCyan.opacity(0.5), neonMagenta.opacity(0.5)], + startPoint: .leading, + endPoint: .trailing + ), + lineWidth: 1 + ) + .background(Color.white.opacity(0.02)) + ) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Rectangle() + .fill(neonYellow) + .frame(width: 4, height: 20) + + Text("YOUR TRIPS") + .font(.system(size: 14, weight: .bold, design: .rounded)) + .foregroundStyle(chrome) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("VIEW ALL ▸") + .font(.system(size: 10, weight: .bold, design: .monospaced)) + .foregroundStyle(neonYellow) + } + } + + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + HStack { + Text(trip.name) + .font(.system(size: 13, weight: .medium, design: .rounded)) + .foregroundStyle(chrome.opacity(0.8)) + + Spacer() + + Text("\(trip.stops.count)") + .font(.system(size: 12, design: .monospaced)) + .foregroundStyle(neonYellow) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(neonYellow.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + .padding(.vertical, 12) + .overlay(alignment: .bottom) { + Rectangle() + .fill(chrome.opacity(0.1)) + .frame(height: 1) + } + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Retro Footer + + private var retroFooter: some View { + VStack(spacing: 8) { + Rectangle() + .fill( + LinearGradient( + colors: [neonMagenta.opacity(0), neonMagenta, neonMagenta.opacity(0)], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(height: 1) + .padding(.horizontal, 60) + + Text("◀ SPORTS TIME SYSTEMS ▶") + .font(.system(size: 9, weight: .bold, design: .monospaced)) + .foregroundStyle(chrome.opacity(0.3)) + .tracking(2) + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/SeatGeek/HomeContent_SeatGeek.swift b/SportsTime/Features/Home/Views/Variants/SeatGeek/HomeContent_SeatGeek.swift new file mode 100644 index 0000000..fe5140d --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/SeatGeek/HomeContent_SeatGeek.swift @@ -0,0 +1,364 @@ +// +// HomeContent_SeatGeek.swift +// SportsTime +// +// SEATGEEK-INSPIRED: Sports ticketing aesthetic. +// Modern cards, vibrant accents, event-focused design. +// Professional yet energetic feel. +// + +import SwiftUI +import SwiftData + +struct HomeContent_SeatGeek: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // SeatGeek-inspired colors + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.08, green: 0.08, blue: 0.10) + : Color(red: 0.96, green: 0.96, blue: 0.97) + } + + private var cardBg: Color { + colorScheme == .dark + ? Color(red: 0.14, green: 0.14, blue: 0.16) + : Color.white + } + + private let accentMagenta = Color(red: 0.85, green: 0.15, blue: 0.5) + private let accentTeal = Color(red: 0.0, green: 0.75, blue: 0.7) + + private var textPrimary: Color { + colorScheme == .dark ? .white : Color(red: 0.12, green: 0.12, blue: 0.15) + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.5) : Color(white: 0.4) + } + + var body: some View { + ScrollView { + VStack(spacing: 20) { + // Hero banner + heroBanner + .padding(.horizontal, 16) + .padding(.top, 8) + + // Your trips section + if !savedTrips.isEmpty { + yourTripsSection + } + + // Trending trips + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + trendingSection + } + + // Browse by sport + browseBySportSection + .padding(.horizontal, 16) + .padding(.bottom, 32) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Hero Banner + + private var heroBanner: some View { + Button { + showNewTrip = true + } label: { + ZStack(alignment: .bottomLeading) { + // Gradient background + RoundedRectangle(cornerRadius: 16) + .fill( + LinearGradient( + colors: [ + accentMagenta, + Color(red: 0.55, green: 0.1, blue: 0.6) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(height: 160) + + // Pattern overlay + GeometryReader { geo in + Path { path in + let w = geo.size.width + let h = geo.size.height + for i in stride(from: 0, to: w, by: 30) { + path.move(to: CGPoint(x: i, y: h)) + path.addLine(to: CGPoint(x: i + 60, y: 0)) + } + } + .stroke(Color.white.opacity(0.1), lineWidth: 1) + } + + // Content + VStack(alignment: .leading, spacing: 8) { + Text("Plan Your Trip") + .font(.system(size: 24, weight: .bold)) + .foregroundStyle(.white) + + Text("Find games along any route") + .font(.system(size: 14)) + .foregroundStyle(.white.opacity(0.85)) + + HStack(spacing: 6) { + Text("Get Started") + .font(.system(size: 13, weight: .semibold)) + Image(systemName: "arrow.right") + .font(.system(size: 11, weight: .bold)) + } + .foregroundStyle(.white) + .padding(.horizontal, 14) + .padding(.vertical, 8) + .background( + Capsule() + .fill(.white.opacity(0.2)) + ) + .padding(.top, 4) + } + .padding(20) + } + } + .buttonStyle(.plain) + } + + // MARK: - Your Trips Section + + private var yourTripsSection: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("Your Trips") + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("View All") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(accentMagenta) + } + } + .padding(.horizontal, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(savedTrips.prefix(4)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + yourTripCard(trip) + } + .buttonStyle(.plain) + } + } + } + .padding(.horizontal, 16) + } + } + } + + private func yourTripCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 10) { + // Sport badge + HStack { + if let sport = trip.uniqueSports.first { + HStack(spacing: 4) { + Image(systemName: sport.iconName) + .font(.system(size: 10)) + Text(sport.displayName) + .font(.system(size: 10, weight: .semibold)) + } + .foregroundStyle(sport.themeColor) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(sport.themeColor.opacity(0.15)) + ) + } + Spacer() + } + + Text(trip.name) + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(2) + .multilineTextAlignment(.leading) + + Spacer() + + HStack(spacing: 12) { + Label("\(trip.stops.count)", systemImage: "mappin") + Label("\(trip.totalGames)", systemImage: "ticket.fill") + } + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + + Text(trip.formattedDateRange) + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + .padding(14) + .frame(width: 160, height: 150) + .background( + RoundedRectangle(cornerRadius: 14) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.08), radius: 8, y: 3) + ) + } + + // MARK: - Trending Section + + private var trendingSection: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + HStack(spacing: 6) { + Image(systemName: "flame.fill") + .foregroundStyle(.orange) + Text("Trending Routes") + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(textPrimary) + } + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(textSecondary) + } + } + .padding(.horizontal, 16) + + VStack(spacing: 10) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + trendingCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 16) + } + } + + private func trendingCard(_ trip: Trip) -> some View { + HStack(spacing: 14) { + // Event icon + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill( + LinearGradient( + colors: [accentTeal, accentTeal.opacity(0.7)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 52, height: 52) + + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 22)) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) cities • \(trip.totalGames) games") + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + } + + Spacer() + + // Deal badge + Text("HOT") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(.orange) + ) + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.06), radius: 6, y: 2) + ) + } + + // MARK: - Browse by Sport + + private var browseBySportSection: some View { + VStack(alignment: .leading, spacing: 14) { + Text("Browse by Sport") + .font(.system(size: 20, weight: .bold)) + .foregroundStyle(textPrimary) + + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { + ForEach(Sport.supported.prefix(4)) { sport in + sportCard(sport) + } + } + } + } + + private func sportCard(_ sport: Sport) -> some View { + Button { + showNewTrip = true + } label: { + HStack(spacing: 10) { + Image(systemName: sport.iconName) + .font(.system(size: 18)) + .foregroundStyle(sport.themeColor) + + Text(sport.displayName) + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(textPrimary) + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(textSecondary.opacity(0.5)) + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.2 : 0.04), radius: 4, y: 1) + ) + } + .buttonStyle(.plain) + } +} diff --git a/SportsTime/Features/Home/Views/Variants/SoftPastel/HomeContent_SoftPastel.swift b/SportsTime/Features/Home/Views/Variants/SoftPastel/HomeContent_SoftPastel.swift new file mode 100644 index 0000000..3696789 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/SoftPastel/HomeContent_SoftPastel.swift @@ -0,0 +1,391 @@ +// +// HomeContent_SoftPastel.swift +// SportsTime +// +// SOFT PASTEL: Gentle, dreamy, calming. +// Muted colors, soft shadows, rounded everything. +// Cozy travel journal aesthetic. +// + +import SwiftUI +import SwiftData + +struct HomeContent_SoftPastel: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Soft pastel palette + private let pastelPink = Color(red: 1.0, green: 0.85, blue: 0.88) + private let pastelBlue = Color(red: 0.85, green: 0.92, blue: 1.0) + private let pastelMint = Color(red: 0.85, green: 0.98, blue: 0.92) + private let pastelLavender = Color(red: 0.92, green: 0.88, blue: 1.0) + private let pastelPeach = Color(red: 1.0, green: 0.9, blue: 0.85) + + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.12, green: 0.12, blue: 0.14) + : Color(red: 0.98, green: 0.97, blue: 0.99) + } + + private var textPrimary: Color { + colorScheme == .dark ? Color(white: 0.9) : Color(red: 0.3, green: 0.28, blue: 0.35) + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.6) : Color(red: 0.5, green: 0.48, blue: 0.55) + } + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + // Soft gradient clouds + softClouds + + ScrollView { + VStack(spacing: 28) { + // PASTEL HERO + pastelHero + .padding(.top, 20) + .padding(.horizontal, 20) + + // FEATURED TRIPS + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.horizontal, 20) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.horizontal, 20) + } + + // SOFT FOOTER + softFooter + .padding(.bottom, 32) + } + } + } + } + + // MARK: - Soft Clouds + + private var softClouds: some View { + ZStack { + // Soft gradient blobs + Ellipse() + .fill( + LinearGradient( + colors: [pastelPink.opacity(0.4), pastelPink.opacity(0.1)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 300, height: 200) + .blur(radius: 50) + .offset(x: -80, y: -150) + + Ellipse() + .fill( + LinearGradient( + colors: [pastelBlue.opacity(0.4), pastelBlue.opacity(0.1)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 250, height: 180) + .blur(radius: 45) + .offset(x: 100, y: 150) + + Ellipse() + .fill( + LinearGradient( + colors: [pastelMint.opacity(0.3), pastelMint.opacity(0.1)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 220, height: 160) + .blur(radius: 40) + .offset(x: -50, y: 450) + + Ellipse() + .fill( + LinearGradient( + colors: [pastelLavender.opacity(0.3), pastelLavender.opacity(0.1)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 200, height: 150) + .blur(radius: 35) + .offset(x: 80, y: 600) + } + .allowsHitTesting(false) + } + + // MARK: - Pastel Hero + + private var pastelHero: some View { + VStack(spacing: 24) { + // Soft icon cluster + ZStack { + Circle() + .fill(pastelPink.opacity(colorScheme == .dark ? 0.3 : 0.6)) + .frame(width: 80, height: 80) + .offset(x: -30, y: -10) + + Circle() + .fill(pastelBlue.opacity(colorScheme == .dark ? 0.3 : 0.6)) + .frame(width: 70, height: 70) + .offset(x: 25, y: 5) + + Circle() + .fill(pastelMint.opacity(colorScheme == .dark ? 0.3 : 0.6)) + .frame(width: 60, height: 60) + .offset(x: -5, y: 25) + + Image(systemName: "car.fill") + .font(.system(size: 28)) + .foregroundStyle(textPrimary.opacity(0.7)) + } + .padding(.vertical, 8) + + // Title + VStack(spacing: 8) { + Text("Sports Time") + .font(.system(size: 32, weight: .semibold, design: .rounded)) + .foregroundStyle(textPrimary) + + Text("Plan your cozy road trip adventure") + .font(.system(size: 15, weight: .regular, design: .rounded)) + .foregroundStyle(textSecondary) + } + + // Soft CTA + Button { + showNewTrip = true + } label: { + HStack(spacing: 12) { + Text("Start Planning") + .font(.system(size: 16, weight: .semibold, design: .rounded)) + + Image(systemName: "arrow.right") + .font(.system(size: 14, weight: .medium)) + } + .foregroundStyle(textPrimary) + .padding(.horizontal, 32) + .padding(.vertical, 16) + .background( + Capsule() + .fill( + LinearGradient( + colors: colorScheme == .dark + ? [pastelPink.opacity(0.4), pastelLavender.opacity(0.4)] + : [pastelPink, pastelLavender], + startPoint: .leading, + endPoint: .trailing + ) + ) + ) + .shadow(color: pastelPink.opacity(0.3), radius: 15, y: 6) + } + } + .padding(28) + .background( + RoundedRectangle(cornerRadius: 32) + .fill(colorScheme == .dark ? Color.white.opacity(0.06) : Color.white.opacity(0.8)) + .shadow(color: Color.black.opacity(0.05), radius: 20, y: 8) + ) + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 18) { + HStack { + Text("Featured Trips") + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14)) + .foregroundStyle(textSecondary) + .padding(10) + .background( + Circle() + .fill(colorScheme == .dark ? Color.white.opacity(0.06) : Color.white.opacity(0.8)) + ) + } + } + + ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + pastelTripCard(suggestedTrip.trip, colorIndex: index) + } + .buttonStyle(.plain) + } + } + } + + private func pastelTripCard(_ trip: Trip, colorIndex: Int) -> some View { + let colors = [pastelPink, pastelBlue, pastelMint, pastelLavender, pastelPeach] + let accentColor = colors[colorIndex % colors.count] + + return HStack(spacing: 16) { + // Soft circle + Circle() + .fill(accentColor.opacity(colorScheme == .dark ? 0.3 : 0.6)) + .frame(width: 50, height: 50) + .overlay( + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 20)) + .foregroundStyle(textPrimary.opacity(0.7)) + ) + + VStack(alignment: .leading, spacing: 6) { + Text(trip.name) + .font(.system(size: 16, weight: .medium, design: .rounded)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + HStack(spacing: 12) { + HStack(spacing: 4) { + Image(systemName: "mappin.circle.fill") + .font(.system(size: 12)) + Text("\(trip.stops.count) stops") + .font(.system(size: 13, design: .rounded)) + } + .foregroundStyle(textSecondary) + + HStack(spacing: 4) { + Image(systemName: "sportscourt.fill") + .font(.system(size: 12)) + Text("\(trip.totalGames) games") + .font(.system(size: 13, design: .rounded)) + } + .foregroundStyle(textSecondary) + } + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(textSecondary.opacity(0.6)) + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.8)) + .shadow(color: accentColor.opacity(0.2), radius: 10, y: 4) + ) + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 18) { + HStack { + Text("Your Trips") + .font(.system(size: 20, weight: .semibold, design: .rounded)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("See all") + .font(.system(size: 14, weight: .medium, design: .rounded)) + .foregroundStyle(textSecondary) + } + } + + ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + let colors = [pastelPeach, pastelMint, pastelLavender] + let accentColor = colors[index % colors.count] + + HStack(spacing: 14) { + // Soft dot + Circle() + .fill(accentColor.opacity(colorScheme == .dark ? 0.4 : 0.7)) + .frame(width: 12, height: 12) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(textPrimary) + + Text(trip.formattedDateRange) + .font(.system(size: 12, design: .rounded)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Text("\(trip.stops.count)") + .font(.system(size: 13, weight: .medium, design: .rounded)) + .foregroundStyle(textPrimary.opacity(0.7)) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + Capsule() + .fill(accentColor.opacity(colorScheme == .dark ? 0.2 : 0.4)) + ) + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(colorScheme == .dark ? Color.white.opacity(0.04) : Color.white.opacity(0.7)) + ) + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Soft Footer + + private var softFooter: some View { + VStack(spacing: 12) { + // Soft dots + HStack(spacing: 8) { + Circle() + .fill(pastelPink.opacity(0.5)) + .frame(width: 6, height: 6) + Circle() + .fill(pastelBlue.opacity(0.5)) + .frame(width: 6, height: 6) + Circle() + .fill(pastelMint.opacity(0.5)) + .frame(width: 6, height: 6) + } + + Text("Sports Time") + .font(.system(size: 11, weight: .medium, design: .rounded)) + .foregroundStyle(textSecondary.opacity(0.5)) + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Spotify/HomeContent_Spotify.swift b/SportsTime/Features/Home/Views/Variants/Spotify/HomeContent_Spotify.swift new file mode 100644 index 0000000..d3406d4 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Spotify/HomeContent_Spotify.swift @@ -0,0 +1,330 @@ +// +// HomeContent_Spotify.swift +// SportsTime +// +// SPOTIFY-INSPIRED: Dark elegance, bold typography. +// Content-focused cards, horizontal scrolling. +// Green accent, immersive feel. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Spotify: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Spotify-inspired colors (dark-first design) + private let bgColor = Color(red: 0.07, green: 0.07, blue: 0.07) + private let cardBg = Color(red: 0.11, green: 0.11, blue: 0.11) + private let spotifyGreen = Color(red: 0.12, green: 0.84, blue: 0.38) + + private let textPrimary = Color.white + private let textSecondary = Color(white: 0.65) + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 28) { + // Greeting header + greetingHeader + .padding(.horizontal, 16) + .padding(.top, 12) + + // Quick actions + quickActions + .padding(.horizontal, 16) + + // Your trips + if !savedTrips.isEmpty { + yourTripsSection + } + + // Made for you + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + madeForYouSection + } + + // Browse sports + browseSportsSection + + Spacer(minLength: 50) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Greeting Header + + private var greetingHeader: some View { + HStack { + Text(greetingText) + .font(.system(size: 24, weight: .bold)) + .foregroundStyle(textPrimary) + + Spacer() + + // Settings/gear button + Button { + // Navigate to settings + } label: { + Image(systemName: "gearshape.fill") + .font(.system(size: 18)) + .foregroundStyle(textPrimary) + } + } + } + + private var greetingText: String { + let hour = Calendar.current.component(.hour, from: Date()) + if hour < 12 { + return "Good morning" + } else if hour < 17 { + return "Good afternoon" + } else { + return "Good evening" + } + } + + // MARK: - Quick Actions + + private var quickActions: some View { + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) { + // New trip button + quickActionButton(title: "Plan Trip", icon: "plus.circle.fill", isPrimary: true) { + showNewTrip = true + } + + // Saved trips button + if !savedTrips.isEmpty { + quickActionButton(title: "Your Trips", icon: "folder.fill", isPrimary: false) { + selectedTab = 2 + } + } + + // First saved trip quick access + if let firstTrip = savedTrips.first?.trip { + quickActionButton(title: firstTrip.name, icon: "play.circle.fill", isPrimary: false) { + // Could navigate to trip + } + } + + // Refresh suggestions + quickActionButton(title: "Refresh", icon: "arrow.clockwise", isPrimary: false) { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } + } + } + + private func quickActionButton(title: String, icon: String, isPrimary: Bool, action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack(spacing: 10) { + Image(systemName: icon) + .font(.system(size: 22)) + .foregroundStyle(isPrimary ? spotifyGreen : textPrimary) + + Text(title) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Spacer() + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(cardBg) + ) + } + .buttonStyle(.plain) + } + + // MARK: - Your Trips Section + + private var yourTripsSection: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Your Trips") + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(textPrimary) + .padding(.horizontal, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(savedTrips.prefix(5)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + tripCoverCard(trip) + } + .buttonStyle(.plain) + } + } + } + .padding(.horizontal, 16) + } + } + } + + private func tripCoverCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 10) { + // Album art style cover + ZStack { + RoundedRectangle(cornerRadius: 6) + .fill( + LinearGradient( + colors: [ + trip.uniqueSports.first?.themeColor ?? spotifyGreen, + (trip.uniqueSports.first?.themeColor ?? spotifyGreen).opacity(0.4) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 150, height: 150) + + VStack(spacing: 8) { + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 40)) + .foregroundStyle(.white) + + Text("\(trip.totalGames) games") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.white.opacity(0.9)) + } + } + .shadow(color: Color.black.opacity(0.4), radius: 8, y: 4) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) stops") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + } + .frame(width: 150) + } + + // MARK: - Made For You Section + + private var madeForYouSection: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Made For You") + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(textPrimary) + .padding(.horizontal, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(5)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + suggestionCoverCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 16) + } + } + } + + private func suggestionCoverCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 10) { + // Cover with gradient mesh + ZStack { + RoundedRectangle(cornerRadius: 6) + .fill( + LinearGradient( + colors: [ + Color(red: 0.3, green: 0.2, blue: 0.5), + Color(red: 0.1, green: 0.4, blue: 0.5), + Color(red: 0.2, green: 0.3, blue: 0.4) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 150, height: 150) + + VStack(spacing: 6) { + Image(systemName: "sparkles") + .font(.system(size: 24)) + .foregroundStyle(.white.opacity(0.8)) + + Text("Daily Mix") + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(.white.opacity(0.8)) + } + } + .shadow(color: Color.black.opacity(0.4), radius: 8, y: 4) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(2) + + Text("\(trip.stops.count) cities") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + } + .frame(width: 150) + } + + // MARK: - Browse Sports Section + + private var browseSportsSection: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Browse Sports") + .font(.system(size: 22, weight: .bold)) + .foregroundStyle(textPrimary) + .padding(.horizontal, 16) + + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { + ForEach(Sport.supported.prefix(4)) { sport in + sportBrowseCard(sport) + } + } + .padding(.horizontal, 16) + } + } + + private func sportBrowseCard(_ sport: Sport) -> some View { + Button { + showNewTrip = true + } label: { + ZStack(alignment: .bottomLeading) { + RoundedRectangle(cornerRadius: 6) + .fill( + LinearGradient( + colors: [sport.themeColor, sport.themeColor.opacity(0.6)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(height: 100) + + Text(sport.displayName) + .font(.system(size: 16, weight: .bold)) + .foregroundStyle(.white) + .padding(12) + } + } + .buttonStyle(.plain) + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Strava/HomeContent_Strava.swift b/SportsTime/Features/Home/Views/Variants/Strava/HomeContent_Strava.swift new file mode 100644 index 0000000..ba36252 --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Strava/HomeContent_Strava.swift @@ -0,0 +1,372 @@ +// +// HomeContent_Strava.swift +// SportsTime +// +// STRAVA-INSPIRED: Athletic, data-driven. +// Orange accent, activity stats, route-focused. +// Performance metrics and community feel. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Strava: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Strava-inspired colors + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.08, green: 0.08, blue: 0.1) + : Color(red: 0.96, green: 0.96, blue: 0.97) + } + + private var cardBg: Color { + colorScheme == .dark + ? Color(red: 0.12, green: 0.12, blue: 0.14) + : Color.white + } + + private let stravaOrange = Color(red: 0.99, green: 0.32, blue: 0.15) + + private var textPrimary: Color { + colorScheme == .dark ? .white : Color(red: 0.12, green: 0.12, blue: 0.14) + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.55) : Color(white: 0.45) + } + + var body: some View { + ScrollView { + VStack(spacing: 20) { + // Profile header + profileHeader + .padding(.horizontal, 16) + .padding(.top, 8) + + // Stats overview + statsOverview + .padding(.horizontal, 16) + + // Record button + recordButton + .padding(.horizontal, 16) + + // Recent activities + if !savedTrips.isEmpty { + recentActivities + } + + // Routes + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + routesSection + } + + Spacer(minLength: 40) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Profile Header + + private var profileHeader: some View { + HStack(spacing: 14) { + // Profile avatar + Circle() + .fill( + LinearGradient( + colors: [stravaOrange, stravaOrange.opacity(0.7)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 50, height: 50) + .overlay( + Image(systemName: "person.fill") + .font(.system(size: 22)) + .foregroundStyle(.white) + ) + + VStack(alignment: .leading, spacing: 2) { + Text("Sports Time") + .font(.system(size: 18, weight: .bold)) + .foregroundStyle(textPrimary) + + Text("Trip Planner") + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + } + + Spacer() + + // Notifications + Button {} label: { + Image(systemName: "bell.fill") + .font(.system(size: 18)) + .foregroundStyle(textSecondary) + } + } + } + + // MARK: - Stats Overview + + private var statsOverview: some View { + HStack(spacing: 0) { + statBlock(value: "\(savedTrips.count)", label: "Trips", color: stravaOrange) + statDivider + statBlock(value: "\(totalGames)", label: "Games", color: .blue) + statDivider + statBlock(value: "\(totalStops)", label: "Cities", color: .green) + } + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.06), radius: 6, y: 2) + ) + } + + private func statBlock(value: String, label: String, color: Color) -> some View { + VStack(spacing: 4) { + Text(value) + .font(.system(size: 24, weight: .bold)) + .foregroundStyle(textPrimary) + + Text(label) + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + .frame(maxWidth: .infinity) + } + + private var statDivider: some View { + Rectangle() + .fill(textSecondary.opacity(0.2)) + .frame(width: 1, height: 40) + } + + private var totalGames: Int { + savedTrips.compactMap { $0.trip?.totalGames }.reduce(0, +) + } + + private var totalStops: Int { + savedTrips.compactMap { $0.trip?.stops.count }.reduce(0, +) + } + + // MARK: - Record Button + + private var recordButton: some View { + Button { + showNewTrip = true + } label: { + HStack(spacing: 10) { + Image(systemName: "plus.circle.fill") + .font(.system(size: 20)) + + Text("Plan New Trip") + .font(.system(size: 16, weight: .semibold)) + } + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(stravaOrange) + ) + } + .buttonStyle(.plain) + } + + // MARK: - Recent Activities + + private var recentActivities: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("Your Activities") + .font(.system(size: 18, weight: .bold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("View All") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(stravaOrange) + } + } + .padding(.horizontal, 16) + + VStack(spacing: 12) { + ForEach(savedTrips.prefix(3)) { savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + activityCard(trip) + } + .buttonStyle(.plain) + } + } + } + .padding(.horizontal, 16) + } + } + + private func activityCard(_ trip: Trip) -> some View { + VStack(spacing: 12) { + // Header + HStack(spacing: 10) { + Circle() + .fill(trip.uniqueSports.first?.themeColor ?? stravaOrange) + .frame(width: 36, height: 36) + .overlay( + Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill") + .font(.system(size: 14)) + .foregroundStyle(.white) + ) + + VStack(alignment: .leading, spacing: 2) { + Text(trip.name) + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text(trip.formattedDateRange) + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12)) + .foregroundStyle(textSecondary) + } + + // Stats row + HStack(spacing: 24) { + activityStat(value: "\(trip.stops.count)", label: "Cities") + activityStat(value: "\(trip.totalGames)", label: "Games") + if let sport = trip.uniqueSports.first { + activityStat(value: sport.displayName, label: "Sport") + } + } + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.2 : 0.05), radius: 4, y: 2) + ) + } + + private func activityStat(value: String, label: String) -> some View { + VStack(alignment: .leading, spacing: 2) { + Text(value) + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(textPrimary) + + Text(label) + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + } + + // MARK: - Routes Section + + private var routesSection: some View { + VStack(alignment: .leading, spacing: 14) { + HStack { + Text("Suggested Routes") + .font(.system(size: 18, weight: .bold)) + .foregroundStyle(textPrimary) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14)) + .foregroundStyle(stravaOrange) + } + } + .padding(.horizontal, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + routeCard(suggestedTrip.trip) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 16) + } + } + } + + private func routeCard(_ trip: Trip) -> some View { + VStack(alignment: .leading, spacing: 10) { + // Route preview + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill( + LinearGradient( + colors: [ + trip.uniqueSports.first?.themeColor.opacity(0.3) ?? stravaOrange.opacity(0.3), + trip.uniqueSports.first?.themeColor.opacity(0.1) ?? stravaOrange.opacity(0.1) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(height: 80) + + // Simplified route line + Path { path in + path.move(to: CGPoint(x: 20, y: 60)) + path.addCurve( + to: CGPoint(x: 120, y: 20), + control1: CGPoint(x: 50, y: 40), + control2: CGPoint(x: 90, y: 30) + ) + } + .stroke(stravaOrange, lineWidth: 3) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) cities • \(trip.totalGames) games") + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + } + .frame(width: 140) + .padding(10) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(cardBg) + .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.2 : 0.05), radius: 4, y: 2) + ) + } +} diff --git a/SportsTime/Features/Home/Views/Variants/SwissModernist/HomeContent_SwissModernist.swift b/SportsTime/Features/Home/Views/Variants/SwissModernist/HomeContent_SwissModernist.swift new file mode 100644 index 0000000..d47ed1b --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/SwissModernist/HomeContent_SwissModernist.swift @@ -0,0 +1,360 @@ +// +// HomeContent_SwissModernist.swift +// SportsTime +// +// SWISS MODERNIST: Grid perfection, Helvetica vibes, mathematical precision. +// Clean typography, strict alignment, minimalist color with bold accents. +// + +import SwiftUI +import SwiftData + +struct HomeContent_SwissModernist: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Swiss design palette - restrained with bold accent + private let swissRed = Color(red: 1.0, green: 0.0, blue: 0.0) + private let swissBlack = Color(white: 0.0) + + private var bgColor: Color { + colorScheme == .dark ? Color(white: 0.06) : Color(white: 0.98) + } + + private var textPrimary: Color { + colorScheme == .dark ? .white : swissBlack + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.5) : Color(white: 0.4) + } + + private var gridLine: Color { + colorScheme == .dark ? Color(white: 0.15) : Color(white: 0.85) + } + + var body: some View { + ZStack { + bgColor.ignoresSafeArea() + + // Subtle grid overlay + gridOverlay + + ScrollView { + VStack(spacing: 0) { + // HEADER - Strict typographic hierarchy + swissHeader + .padding(.top, 32) + .padding(.horizontal, 24) + + // HERO - Mathematical precision + swissHero + .padding(.top, 48) + .padding(.horizontal, 24) + + // FEATURED TRIPS + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + featuredSection + .padding(.top, 64) + .padding(.horizontal, 24) + } + + // SAVED TRIPS + if !savedTrips.isEmpty { + savedSection + .padding(.top, 64) + .padding(.horizontal, 24) + } + + // FOOTER + swissFooter + .padding(.top, 80) + .padding(.bottom, 40) + } + } + } + } + + // MARK: - Grid Overlay + + private var gridOverlay: some View { + GeometryReader { geo in + // Vertical grid lines + HStack(spacing: geo.size.width / 12) { + ForEach(0..<12, id: \.self) { _ in + Rectangle() + .fill(gridLine.opacity(0.3)) + .frame(width: 0.5) + } + } + .padding(.horizontal, 24) + } + .allowsHitTesting(false) + } + + // MARK: - Swiss Header + + private var swissHeader: some View { + VStack(alignment: .leading, spacing: 12) { + // Date - small caps style + Text(Date.now.formatted(.dateTime.month(.wide).year()).uppercased()) + .font(.system(size: 10, weight: .medium)) + .tracking(3) + .foregroundStyle(textSecondary) + + // Rule + Rectangle() + .fill(textPrimary) + .frame(height: 2) + .frame(maxWidth: 60) + } + } + + // MARK: - Swiss Hero + + private var swissHero: some View { + VStack(alignment: .leading, spacing: 32) { + // Title block - strict typographic scale + VStack(alignment: .leading, spacing: 8) { + Text("Sports") + .font(.system(size: 56, weight: .bold)) + .foregroundStyle(textPrimary) + + HStack(spacing: 0) { + Text("Time") + .font(.system(size: 56, weight: .bold)) + .foregroundStyle(textPrimary) + + // Red accent dot + Circle() + .fill(swissRed) + .frame(width: 14, height: 14) + .offset(y: 16) + } + } + .tracking(-1) + + // Description - rational grid width + Text("Plan your perfect sports road trip with mathematical precision. Every route optimized. Every game aligned.") + .font(.system(size: 16, weight: .regular)) + .foregroundStyle(textSecondary) + .lineSpacing(6) + .frame(maxWidth: 280, alignment: .leading) + + // CTA - Swiss button + Button { + showNewTrip = true + } label: { + HStack(spacing: 16) { + Text("Begin") + .font(.system(size: 14, weight: .semibold)) + + Rectangle() + .fill(Color.white) + .frame(width: 24, height: 1) + + Image(systemName: "arrow.right") + .font(.system(size: 12, weight: .medium)) + } + .foregroundStyle(.white) + .padding(.horizontal, 32) + .padding(.vertical, 18) + .background(swissRed) + } + } + } + + // MARK: - Featured Section + + private var featuredSection: some View { + VStack(alignment: .leading, spacing: 32) { + // Section header - typographic hierarchy + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 8) { + Text("01") + .font(.system(size: 11, weight: .medium, design: .monospaced)) + .foregroundStyle(swissRed) + + Text("Featured") + .font(.system(size: 28, weight: .bold)) + .foregroundStyle(textPrimary) + } + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 14)) + .foregroundStyle(textSecondary) + } + } + + // Rule + Rectangle() + .fill(textPrimary) + .frame(height: 1) + + // Grid of trips + VStack(spacing: 0) { + ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + swissTripRow(suggestedTrip.trip, index: index + 1) + } + .buttonStyle(.plain) + } + } + } + } + + private func swissTripRow(_ trip: Trip, index: Int) -> some View { + VStack(spacing: 0) { + HStack(alignment: .center, spacing: 24) { + // Index number + Text(String(format: "%02d", index)) + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundStyle(textSecondary) + .frame(width: 24) + + // Trip name + Text(trip.name) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Spacer() + + // Stats - tabular + HStack(spacing: 24) { + VStack(alignment: .trailing, spacing: 2) { + Text("\(trip.stops.count)") + .font(.system(size: 14, weight: .semibold, design: .monospaced)) + .foregroundStyle(textPrimary) + Text("cities") + .font(.system(size: 9, weight: .medium)) + .foregroundStyle(textSecondary) + } + + VStack(alignment: .trailing, spacing: 2) { + Text("\(trip.totalGames)") + .font(.system(size: 14, weight: .semibold, design: .monospaced)) + .foregroundStyle(textPrimary) + Text("games") + .font(.system(size: 9, weight: .medium)) + .foregroundStyle(textSecondary) + } + } + + // Arrow + Image(systemName: "arrow.right") + .font(.system(size: 10, weight: .medium)) + .foregroundStyle(swissRed) + } + .padding(.vertical, 20) + + // Divider + Rectangle() + .fill(gridLine) + .frame(height: 0.5) + } + } + + // MARK: - Saved Section + + private var savedSection: some View { + VStack(alignment: .leading, spacing: 32) { + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 8) { + Text("02") + .font(.system(size: 11, weight: .medium, design: .monospaced)) + .foregroundStyle(swissRed) + + Text("Saved") + .font(.system(size: 28, weight: .bold)) + .foregroundStyle(textPrimary) + } + + Spacer() + + Button { + selectedTab = 2 + } label: { + Text("All") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(textSecondary) + } + } + + Rectangle() + .fill(textPrimary) + .frame(height: 1) + + VStack(spacing: 0) { + ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + VStack(spacing: 0) { + HStack(spacing: 24) { + Text(String(format: "%02d", index + 1)) + .font(.system(size: 12, weight: .medium, design: .monospaced)) + .foregroundStyle(textSecondary) + .frame(width: 24) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(textPrimary) + + Text(trip.formattedDateRange) + .font(.system(size: 11)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Image(systemName: "arrow.right") + .font(.system(size: 10, weight: .medium)) + .foregroundStyle(textSecondary) + } + .padding(.vertical, 20) + + Rectangle() + .fill(gridLine) + .frame(height: 0.5) + } + } + .buttonStyle(.plain) + } + } + } + } + } + + // MARK: - Swiss Footer + + private var swissFooter: some View { + VStack(spacing: 12) { + Rectangle() + .fill(textPrimary) + .frame(width: 40, height: 2) + + Text("SPORTS TIME") + .font(.system(size: 9, weight: .medium)) + .tracking(4) + .foregroundStyle(textSecondary) + } + } +} diff --git a/SportsTime/Features/Home/Views/Variants/Things3/HomeContent_Things3.swift b/SportsTime/Features/Home/Views/Variants/Things3/HomeContent_Things3.swift new file mode 100644 index 0000000..c479cea --- /dev/null +++ b/SportsTime/Features/Home/Views/Variants/Things3/HomeContent_Things3.swift @@ -0,0 +1,299 @@ +// +// HomeContent_Things3.swift +// SportsTime +// +// THINGS 3-INSPIRED: Ultra-clean task management aesthetic. +// Beautiful spacing, elegant typography, minimalist. +// Focus on clarity and completion. +// + +import SwiftUI +import SwiftData + +struct HomeContent_Things3: View { + @Environment(\.colorScheme) private var colorScheme + @Binding var showNewTrip: Bool + @Binding var selectedTab: Int + @Binding var selectedSuggestedTrip: SuggestedTrip? + + let savedTrips: [SavedTrip] + let suggestedTripsGenerator: SuggestedTripsGenerator + let displayedTips: [PlanningTip] + + // Things 3-inspired colors + private var bgColor: Color { + colorScheme == .dark + ? Color(red: 0.11, green: 0.11, blue: 0.12) + : Color.white + } + + private let thingsBlue = Color(red: 0.35, green: 0.6, blue: 0.95) + private let thingsGray = Color(red: 0.55, green: 0.55, blue: 0.58) + + private var textPrimary: Color { + colorScheme == .dark ? .white : Color(red: 0.15, green: 0.15, blue: 0.17) + } + + private var textSecondary: Color { + colorScheme == .dark ? Color(white: 0.5) : Color(white: 0.5) + } + + private var dividerColor: Color { + colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.9) + } + + var body: some View { + ScrollView { + VStack(spacing: 0) { + // Header + header + .padding(.horizontal, 24) + .padding(.top, 20) + .padding(.bottom, 28) + + // New Trip action + newTripRow + .padding(.horizontal, 24) + + Divider() + .background(dividerColor) + .padding(.horizontal, 24) + .padding(.vertical, 16) + + // Your trips section + if !savedTrips.isEmpty { + tripsSection + .padding(.horizontal, 24) + } + + // Suggestions section + if !suggestedTripsGenerator.suggestedTrips.isEmpty { + suggestionsSection + .padding(.horizontal, 24) + .padding(.top, savedTrips.isEmpty ? 0 : 28) + } + + Spacer(minLength: 60) + } + } + .background(bgColor.ignoresSafeArea()) + } + + // MARK: - Header + + private var header: some View { + VStack(alignment: .leading, spacing: 6) { + Text("Sports Time") + .font(.system(size: 34, weight: .bold)) + .foregroundStyle(textPrimary) + + Text(headerSubtitle) + .font(.system(size: 15)) + .foregroundStyle(textSecondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var headerSubtitle: String { + if savedTrips.isEmpty { + return "Plan your first trip" + } else { + let upcoming = savedTrips.filter { ($0.trip?.startDate ?? Date()) > Date() }.count + if upcoming > 0 { + return "\(upcoming) upcoming trip\(upcoming == 1 ? "" : "s")" + } + return "\(savedTrips.count) trip\(savedTrips.count == 1 ? "" : "s") planned" + } + } + + // MARK: - New Trip Row + + private var newTripRow: some View { + Button { + showNewTrip = true + } label: { + HStack(spacing: 14) { + // Checkbox circle + ZStack { + Circle() + .stroke(thingsBlue, lineWidth: 2) + .frame(width: 22, height: 22) + + Image(systemName: "plus") + .font(.system(size: 12, weight: .bold)) + .foregroundStyle(thingsBlue) + } + + Text("New Trip") + .font(.system(size: 17)) + .foregroundStyle(thingsBlue) + + Spacer() + } + } + .buttonStyle(.plain) + } + + // MARK: - Trips Section + + private var tripsSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Your Trips") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(textSecondary) + .textCase(.uppercase) + .tracking(0.5) + + Spacer() + + if savedTrips.count > 3 { + Button { + selectedTab = 2 + } label: { + Text("See All") + .font(.system(size: 13)) + .foregroundStyle(thingsBlue) + } + } + } + + VStack(spacing: 0) { + ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in + if let trip = savedTrip.trip { + NavigationLink { + TripDetailView(trip: trip, games: savedTrip.games) + } label: { + tripRow(trip) + } + .buttonStyle(.plain) + + if index < min(2, savedTrips.count - 1) { + Divider() + .background(dividerColor) + .padding(.leading, 36) + .padding(.vertical, 8) + } + } + } + } + } + } + + private func tripRow(_ trip: Trip) -> some View { + HStack(spacing: 14) { + // Completion circle + Circle() + .stroke(thingsGray.opacity(0.5), lineWidth: 1.5) + .frame(width: 22, height: 22) + + VStack(alignment: .leading, spacing: 4) { + Text(trip.name) + .font(.system(size: 17)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + HStack(spacing: 12) { + if let sport = trip.uniqueSports.first { + HStack(spacing: 4) { + Image(systemName: sport.iconName) + .font(.system(size: 11)) + Text(sport.displayName) + .font(.system(size: 13)) + } + .foregroundStyle(sport.themeColor) + } + + Text("•") + .foregroundStyle(textSecondary) + + Text(trip.formattedDateRange) + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + } + } + + Spacer() + + // Count badge + Text("\(trip.totalGames)") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(textSecondary) + } + } + + // MARK: - Suggestions Section + + private var suggestionsSection: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Suggestions") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(textSecondary) + .textCase(.uppercase) + .tracking(0.5) + + Spacer() + + Button { + Task { + await suggestedTripsGenerator.refreshTrips() + } + } label: { + Image(systemName: "arrow.clockwise") + .font(.system(size: 13)) + .foregroundStyle(thingsBlue) + } + } + + VStack(spacing: 0) { + ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in + Button { + selectedSuggestedTrip = suggestedTrip + } label: { + suggestionRow(suggestedTrip.trip) + } + .buttonStyle(.plain) + + if index < min(3, suggestedTripsGenerator.suggestedTrips.count - 1) { + Divider() + .background(dividerColor) + .padding(.leading, 36) + .padding(.vertical, 8) + } + } + } + } + } + + private func suggestionRow(_ trip: Trip) -> some View { + HStack(spacing: 14) { + // Light gray circle + Circle() + .fill(thingsGray.opacity(0.15)) + .frame(width: 22, height: 22) + .overlay( + Image(systemName: "sparkles") + .font(.system(size: 10)) + .foregroundStyle(thingsGray) + ) + + VStack(alignment: .leading, spacing: 3) { + Text(trip.name) + .font(.system(size: 17)) + .foregroundStyle(textPrimary) + .lineLimit(1) + + Text("\(trip.stops.count) stops • \(trip.totalGames) games") + .font(.system(size: 13)) + .foregroundStyle(textSecondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(thingsGray.opacity(0.5)) + } + } +} diff --git a/SportsTime/Features/Settings/Views/SettingsView.swift b/SportsTime/Features/Settings/Views/SettingsView.swift index 19e75a3..462e2e2 100644 --- a/SportsTime/Features/Settings/Views/SettingsView.swift +++ b/SportsTime/Features/Settings/Views/SettingsView.swift @@ -19,6 +19,9 @@ struct SettingsView: View { // Theme Selection themeSection + // UI Design Style + designStyleSection + // Sports Preferences sportsSection @@ -96,6 +99,58 @@ struct SettingsView: View { .listRowBackground(Theme.cardBackground(colorScheme)) } + // MARK: - Design Style Section + + private var designStyleSection: some View { + Section { + ForEach(UIDesignStyle.allCases) { style in + Button { + withAnimation(.easeInOut(duration: 0.2)) { + DesignStyleManager.shared.setStyle(style) + } + } label: { + HStack(spacing: 12) { + // Icon with accent color + ZStack { + Circle() + .fill(style.accentColor.opacity(0.15)) + .frame(width: 36, height: 36) + + Image(systemName: style.iconName) + .font(.system(size: 16)) + .foregroundStyle(style.accentColor) + } + + VStack(alignment: .leading, spacing: 2) { + Text(style.rawValue) + .font(.body) + .foregroundStyle(.primary) + Text(style.description) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + + Spacer() + + if DesignStyleManager.shared.currentStyle == style { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(style.accentColor) + .font(.title3) + } + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } header: { + Text("Home Screen Style") + } footer: { + Text("Choose a visual aesthetic for the home screen.") + } + .listRowBackground(Theme.cardBackground(colorScheme)) + } + // MARK: - Sports Section private var sportsSection: some View {