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