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 <noreply@anthropic.com>
This commit is contained in:
190
SportsTime/Core/Design/UIDesignStyle.swift
Normal file
190
SportsTime/Core/Design/UIDesignStyle.swift
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
253
SportsTime/Features/Home/Views/AdaptiveHomeContent.swift
Normal file
253
SportsTime/Features/Home/Views/AdaptiveHomeContent.swift
Normal file
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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, +)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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..<Int(geo.size.height / 4), id: \.self) { _ in
|
||||
Rectangle()
|
||||
.fill(Color.white.opacity(0.03))
|
||||
.frame(height: 1)
|
||||
Rectangle()
|
||||
.fill(Color.clear)
|
||||
.frame(height: 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
// MARK: - Retro Header
|
||||
|
||||
private var retroHeader: some View {
|
||||
VStack(spacing: 8) {
|
||||
// Chrome accent line
|
||||
Rectangle()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [neonCyan.opacity(0), neonCyan, neonCyan.opacity(0)],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.frame(height: 2)
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
// Main title with glow
|
||||
Text("SPORTS TIME")
|
||||
.font(.system(size: 36, weight: .black, design: .rounded))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [neonCyan, neonMagenta],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.shadow(color: neonCyan.opacity(0.8), radius: 20, x: 0, y: 0)
|
||||
.shadow(color: neonMagenta.opacity(0.5), radius: 30, x: 0, y: 0)
|
||||
|
||||
// Subtitle
|
||||
Text("▸ ROAD TRIP COMMAND CENTER ◂")
|
||||
.font(.system(size: 11, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(chrome.opacity(0.7))
|
||||
.tracking(4)
|
||||
|
||||
// Date display - digital clock style
|
||||
Text(Date.now.formatted(.dateTime.month().day().year()))
|
||||
.font(.system(size: 14, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(neonYellow)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(neonYellow.opacity(0.5), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Neon CTA Block
|
||||
|
||||
private var neonCTABlock: some View {
|
||||
VStack(spacing: 16) {
|
||||
// Header bar
|
||||
HStack {
|
||||
Text("◆ INITIATE TRIP SEQUENCE ◆")
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(neonCyan)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Main CTA
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "play.fill")
|
||||
.font(.system(size: 14))
|
||||
|
||||
Text("START PLANNING")
|
||||
.font(.system(size: 16, weight: .black, design: .rounded))
|
||||
.tracking(2)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("►")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
}
|
||||
.foregroundStyle(.black)
|
||||
.padding(20)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [neonCyan, neonMagenta],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.shadow(color: neonCyan.opacity(0.6), radius: 15, x: 0, y: 0)
|
||||
}
|
||||
|
||||
// Status text
|
||||
Text("SYSTEM READY • ALL STADIUMS ONLINE")
|
||||
.font(.system(size: 9, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(chrome.opacity(0.5))
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(neonCyan.opacity(0.3), lineWidth: 1)
|
||||
.background(Color.white.opacity(0.03))
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Featured Section
|
||||
|
||||
private var featuredSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Broadcast style header
|
||||
HStack {
|
||||
Rectangle()
|
||||
.fill(neonMagenta)
|
||||
.frame(width: 4, height: 20)
|
||||
|
||||
Text("FEATURED TRIPS")
|
||||
.font(.system(size: 14, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(chrome)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("LIVE")
|
||||
.font(.system(size: 10, weight: .bold, design: .monospaced))
|
||||
.foregroundStyle(.black)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(neonMagenta)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
|
||||
// Trip cards - broadcast ticker style
|
||||
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
retroTripCard(suggestedTrip.trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func retroTripCard(_ trip: Trip) -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user