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:
Trey t
2026-01-13 14:44:30 -06:00
parent 3d40145ffb
commit 56869ce479
27 changed files with 8636 additions and 27 deletions

View File

@@ -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)
)
)
}
}