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,339 @@
|
||||
//
|
||||
// HomeContent_LuxuryEditorial.swift
|
||||
// SportsTime
|
||||
//
|
||||
// LUXURY EDITORIAL: Magazine-quality, dramatic typography.
|
||||
// Elegant serif headlines, cinematic composition, gold accents.
|
||||
// Premium sports journalism aesthetic.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_LuxuryEditorial: 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]
|
||||
|
||||
private let gold = Color(red: 0.85, green: 0.65, blue: 0.13)
|
||||
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark ? Color(white: 0.08) : Color(white: 0.98)
|
||||
}
|
||||
|
||||
private var textPrimary: Color {
|
||||
colorScheme == .dark ? .white : Color(white: 0.1)
|
||||
}
|
||||
|
||||
private var textSecondary: Color {
|
||||
colorScheme == .dark ? Color(white: 0.6) : Color(white: 0.4)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// MASTHEAD
|
||||
masthead
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 40)
|
||||
|
||||
// HERO FEATURE
|
||||
heroFeature
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 48)
|
||||
|
||||
// EDITORIAL DIVIDER
|
||||
editorialDivider
|
||||
.padding(.bottom, 48)
|
||||
|
||||
// FEATURED TRIPS - Magazine Grid
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
featuredSection
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 48)
|
||||
}
|
||||
|
||||
// SAVED TRIPS
|
||||
if !savedTrips.isEmpty {
|
||||
savedSection
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 48)
|
||||
}
|
||||
|
||||
// COLOPHON
|
||||
colophon
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
.background(bgColor)
|
||||
}
|
||||
|
||||
// MARK: - Masthead
|
||||
|
||||
private var masthead: some View {
|
||||
VStack(spacing: 4) {
|
||||
// Thin rule
|
||||
Rectangle()
|
||||
.fill(textSecondary)
|
||||
.frame(height: 0.5)
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
HStack {
|
||||
Text(Date.now.formatted(.dateTime.month(.wide).year()))
|
||||
.font(.system(size: 10, weight: .medium, design: .serif))
|
||||
.tracking(2)
|
||||
.foregroundStyle(textSecondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("SPORTS TIME")
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.tracking(4)
|
||||
.foregroundStyle(gold)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("TRAVEL EDITION")
|
||||
.font(.system(size: 10, weight: .medium, design: .serif))
|
||||
.tracking(2)
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
// Thin rule
|
||||
Rectangle()
|
||||
.fill(textSecondary)
|
||||
.frame(height: 0.5)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hero Feature
|
||||
|
||||
private var heroFeature: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
// Kicker
|
||||
Text("THE JOURNEY BEGINS")
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.tracking(3)
|
||||
.foregroundStyle(gold)
|
||||
|
||||
// Headline - Large serif
|
||||
Text("Your Ultimate\nSports Road Trip\nAwaits")
|
||||
.font(.system(size: 42, weight: .regular, design: .serif))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineSpacing(4)
|
||||
|
||||
// Deck
|
||||
Text("Meticulously planned routes connecting the greatest stadiums, arenas, and ballparks across America.")
|
||||
.font(.system(size: 16, weight: .regular, design: .serif))
|
||||
.foregroundStyle(textSecondary)
|
||||
.lineSpacing(6)
|
||||
.italic()
|
||||
|
||||
// CTA - Elegant button
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Text("Begin Planning")
|
||||
.font(.system(size: 14, weight: .medium, design: .serif))
|
||||
.tracking(1)
|
||||
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
}
|
||||
.foregroundStyle(colorScheme == .dark ? .black : .white)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 16)
|
||||
.background(gold)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Editorial Divider
|
||||
|
||||
private var editorialDivider: some View {
|
||||
HStack(spacing: 16) {
|
||||
Rectangle()
|
||||
.fill(textSecondary.opacity(0.3))
|
||||
.frame(height: 0.5)
|
||||
|
||||
Image(systemName: "diamond.fill")
|
||||
.font(.system(size: 6))
|
||||
.foregroundStyle(gold)
|
||||
|
||||
Rectangle()
|
||||
.fill(textSecondary.opacity(0.3))
|
||||
.frame(height: 0.5)
|
||||
}
|
||||
.padding(.horizontal, 48)
|
||||
}
|
||||
|
||||
// MARK: - Featured Section
|
||||
|
||||
private var featuredSection: some View {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
// Section header
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text("Featured Itineraries")
|
||||
.font(.system(size: 24, weight: .regular, design: .serif))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await suggestedTripsGenerator.refreshTrips()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(gold)
|
||||
}
|
||||
}
|
||||
|
||||
// Magazine grid - 2 column
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) {
|
||||
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
editorialTripCard(suggestedTrip.trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func editorialTripCard(_ trip: Trip) -> some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// Sport badge
|
||||
HStack(spacing: 4) {
|
||||
ForEach(Array(trip.uniqueSports.prefix(2)), id: \.self) { sport in
|
||||
Text(sport.rawValue.uppercased())
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.tracking(1)
|
||||
.foregroundStyle(gold)
|
||||
}
|
||||
}
|
||||
|
||||
// Title
|
||||
Text(trip.name)
|
||||
.font(.system(size: 18, weight: .regular, design: .serif))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
// Stats
|
||||
Text("\(trip.stops.count) Cities · \(trip.totalGames) Games")
|
||||
.font(.system(size: 11, weight: .regular, design: .serif))
|
||||
.foregroundStyle(textSecondary)
|
||||
.italic()
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
// Read more
|
||||
HStack(spacing: 4) {
|
||||
Text("View Details")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.tracking(1)
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.system(size: 8))
|
||||
}
|
||||
.foregroundStyle(gold)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(minHeight: 160)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.stroke(textSecondary.opacity(0.2), lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Saved Section
|
||||
|
||||
private var savedSection: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text("Your Collection")
|
||||
.font(.system(size: 24, weight: .regular, design: .serif))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("View All")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.tracking(1)
|
||||
.foregroundStyle(gold)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(savedTrips.prefix(3)) { savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: savedTrip.games)
|
||||
} label: {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
// Number
|
||||
Text(String(format: "%02d", (savedTrips.firstIndex(where: { $0.id == savedTrip.id }) ?? 0) + 1))
|
||||
.font(.system(size: 11, weight: .regular, design: .serif))
|
||||
.foregroundStyle(gold)
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 16, weight: .regular, design: .serif))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.system(size: 11, weight: .regular, design: .serif))
|
||||
.foregroundStyle(textSecondary)
|
||||
.italic()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 10))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle()
|
||||
.fill(textSecondary.opacity(0.15))
|
||||
.frame(height: 0.5)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Colophon
|
||||
|
||||
private var colophon: some View {
|
||||
VStack(spacing: 8) {
|
||||
Rectangle()
|
||||
.fill(textSecondary.opacity(0.2))
|
||||
.frame(width: 40, height: 0.5)
|
||||
|
||||
Text("SPORTS TIME")
|
||||
.font(.system(size: 9, weight: .semibold))
|
||||
.tracking(4)
|
||||
.foregroundStyle(textSecondary.opacity(0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user