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:
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user