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