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,314 @@
|
||||
//
|
||||
// HomeContent_Organic.swift
|
||||
// SportsTime
|
||||
//
|
||||
// ORGANIC: Soft curves, earthy tones, breathing life.
|
||||
// Natural stadium grass vibes, flowing shapes, gentle animations.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_Organic: 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]
|
||||
|
||||
// Earthy organic palette
|
||||
private let leafGreen = Color(red: 0.35, green: 0.65, blue: 0.35)
|
||||
private let earthBrown = Color(red: 0.55, green: 0.4, blue: 0.3)
|
||||
private let warmSand = Color(red: 0.95, green: 0.9, blue: 0.8)
|
||||
private let skyBlue = Color(red: 0.6, green: 0.8, blue: 0.95)
|
||||
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark
|
||||
? Color(red: 0.1, green: 0.12, blue: 0.1)
|
||||
: warmSand
|
||||
}
|
||||
|
||||
private var textPrimary: Color {
|
||||
colorScheme == .dark ? Color(white: 0.9) : earthBrown
|
||||
}
|
||||
|
||||
private var textSecondary: Color {
|
||||
colorScheme == .dark ? Color(white: 0.6) : earthBrown.opacity(0.7)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 28) {
|
||||
// ORGANIC HERO
|
||||
organicHero
|
||||
.padding(.top, 16)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// FEATURED TRIPS - Flowing cards
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
featuredSection
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// SAVED TRIPS
|
||||
if !savedTrips.isEmpty {
|
||||
savedSection
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// FOOTER LEAF
|
||||
footerLeaf
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
.background(bgColor)
|
||||
}
|
||||
|
||||
// MARK: - Organic Hero
|
||||
|
||||
private var organicHero: some View {
|
||||
VStack(spacing: 20) {
|
||||
// Organic shape header
|
||||
ZStack {
|
||||
// Background blob
|
||||
Ellipse()
|
||||
.fill(leafGreen.opacity(0.15))
|
||||
.frame(width: 280, height: 140)
|
||||
.rotationEffect(.degrees(-5))
|
||||
|
||||
Ellipse()
|
||||
.fill(skyBlue.opacity(0.1))
|
||||
.frame(width: 200, height: 100)
|
||||
.offset(x: 60, y: 20)
|
||||
.rotationEffect(.degrees(10))
|
||||
|
||||
VStack(spacing: 8) {
|
||||
// Leaf icon
|
||||
Image(systemName: "leaf.fill")
|
||||
.font(.system(size: 28))
|
||||
.foregroundStyle(leafGreen)
|
||||
|
||||
Text("Sports Time")
|
||||
.font(.system(size: 32, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text("Journey naturally")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textSecondary)
|
||||
.italic()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
|
||||
// Description in organic container
|
||||
Text("Let your sports adventure unfold organically. We'll guide you through the most scenic routes connecting America's greatest stadiums.")
|
||||
.font(.system(size: 15, weight: .regular, design: .rounded))
|
||||
.foregroundStyle(textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(6)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
// Organic CTA button
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "arrow.right.circle")
|
||||
.font(.system(size: 18))
|
||||
|
||||
Text("Begin Your Journey")
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(leafGreen)
|
||||
)
|
||||
.shadow(color: leafGreen.opacity(0.3), radius: 15, y: 8)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 32)
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.7))
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Featured Section
|
||||
|
||||
private var featuredSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Image(systemName: "leaf.fill")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(leafGreen)
|
||||
|
||||
Text("Featured Journeys")
|
||||
.font(.system(size: 18, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await suggestedTripsGenerator.refreshTrips()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(leafGreen)
|
||||
}
|
||||
}
|
||||
|
||||
// Organic flowing cards
|
||||
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
organicTripCard(suggestedTrip.trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func organicTripCard(_ trip: Trip) -> some View {
|
||||
HStack(spacing: 16) {
|
||||
// Organic circle with sport color
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(trip.uniqueSports.first?.themeColor.opacity(0.2) ?? leafGreen.opacity(0.2))
|
||||
.frame(width: 56, height: 56)
|
||||
|
||||
Circle()
|
||||
.fill(trip.uniqueSports.first?.themeColor.opacity(0.3) ?? leafGreen.opacity(0.3))
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt")
|
||||
.font(.system(size: 18))
|
||||
.foregroundStyle(trip.uniqueSports.first?.themeColor ?? leafGreen)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Label("\(trip.stops.count) stops", systemImage: "mappin.circle")
|
||||
Label("\(trip.totalGames) games", systemImage: "sportscourt")
|
||||
}
|
||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right.circle")
|
||||
.font(.system(size: 20))
|
||||
.foregroundStyle(leafGreen.opacity(0.6))
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.8))
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Saved Section
|
||||
|
||||
private var savedSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Image(systemName: "tree.fill")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(earthBrown)
|
||||
|
||||
Text("Your Journeys")
|
||||
.font(.system(size: 18, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("View all")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(leafGreen)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(savedTrips.prefix(3)) { savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: savedTrip.games)
|
||||
} label: {
|
||||
HStack(spacing: 14) {
|
||||
// Organic dot cluster
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(earthBrown.opacity(0.2))
|
||||
.frame(width: 8, height: 8)
|
||||
.offset(x: -6, y: -4)
|
||||
Circle()
|
||||
.fill(leafGreen.opacity(0.3))
|
||||
.frame(width: 10, height: 10)
|
||||
.offset(x: 4, y: 2)
|
||||
Circle()
|
||||
.fill(skyBlue.opacity(0.3))
|
||||
.frame(width: 6, height: 6)
|
||||
.offset(x: -2, y: 6)
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 15, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.system(size: 12, design: .rounded))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Capsule()
|
||||
.fill(leafGreen.opacity(0.15))
|
||||
.frame(width: 40, height: 24)
|
||||
.overlay(
|
||||
Text("\(trip.stops.count)")
|
||||
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(leafGreen)
|
||||
)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Footer Leaf
|
||||
|
||||
private var footerLeaf: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "leaf.fill")
|
||||
.font(.system(size: 16))
|
||||
.foregroundStyle(leafGreen.opacity(0.4))
|
||||
.rotationEffect(.degrees(45))
|
||||
|
||||
Text("Sports Time")
|
||||
.font(.system(size: 11, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(textSecondary.opacity(0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user