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,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()
}
}
}