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,372 @@
|
||||
//
|
||||
// HomeContent_Playful.swift
|
||||
// SportsTime
|
||||
//
|
||||
// PLAYFUL: Bouncy, toy-like, rounded everything.
|
||||
// Bright candy colors, wobbly shapes, fun animations.
|
||||
// Sports meets playground aesthetic.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_Playful: 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]
|
||||
|
||||
// Candy color palette
|
||||
private let candyPink = Color(red: 1.0, green: 0.4, blue: 0.6)
|
||||
private let candyBlue = Color(red: 0.4, green: 0.7, blue: 1.0)
|
||||
private let candyYellow = Color(red: 1.0, green: 0.85, blue: 0.3)
|
||||
private let candyGreen = Color(red: 0.4, green: 0.9, blue: 0.6)
|
||||
private let candyPurple = Color(red: 0.7, green: 0.5, blue: 1.0)
|
||||
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark
|
||||
? Color(red: 0.12, green: 0.1, blue: 0.18)
|
||||
: Color(red: 0.98, green: 0.97, blue: 1.0)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
bgColor.ignoresSafeArea()
|
||||
|
||||
// Floating blobs
|
||||
floatingBlobs
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 28) {
|
||||
// PLAYFUL HERO
|
||||
playfulHero
|
||||
.padding(.top, 20)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// FEATURED TRIPS
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
featuredSection
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// SAVED TRIPS
|
||||
if !savedTrips.isEmpty {
|
||||
savedSection
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
// FUN FOOTER
|
||||
funFooter
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Floating Blobs
|
||||
|
||||
private var floatingBlobs: some View {
|
||||
ZStack {
|
||||
Ellipse()
|
||||
.fill(candyPink.opacity(0.15))
|
||||
.frame(width: 200, height: 160)
|
||||
.rotationEffect(.degrees(-20))
|
||||
.offset(x: -100, y: -150)
|
||||
|
||||
Ellipse()
|
||||
.fill(candyBlue.opacity(0.15))
|
||||
.frame(width: 180, height: 140)
|
||||
.rotationEffect(.degrees(15))
|
||||
.offset(x: 120, y: 100)
|
||||
|
||||
Ellipse()
|
||||
.fill(candyYellow.opacity(0.12))
|
||||
.frame(width: 150, height: 120)
|
||||
.rotationEffect(.degrees(-10))
|
||||
.offset(x: -60, y: 400)
|
||||
|
||||
Ellipse()
|
||||
.fill(candyGreen.opacity(0.1))
|
||||
.frame(width: 130, height: 100)
|
||||
.rotationEffect(.degrees(25))
|
||||
.offset(x: 100, y: 550)
|
||||
}
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
// MARK: - Playful Hero
|
||||
|
||||
private var playfulHero: some View {
|
||||
VStack(spacing: 20) {
|
||||
// Bouncy mascot area
|
||||
ZStack {
|
||||
// Background wobble
|
||||
RoundedRectangle(cornerRadius: 40)
|
||||
.fill(candyYellow.opacity(0.3))
|
||||
.frame(width: 140, height: 100)
|
||||
.rotationEffect(.degrees(-5))
|
||||
|
||||
RoundedRectangle(cornerRadius: 40)
|
||||
.fill(candyPink.opacity(0.3))
|
||||
.frame(width: 120, height: 80)
|
||||
.offset(x: 30, y: 10)
|
||||
.rotationEffect(.degrees(5))
|
||||
|
||||
// Sports emojis
|
||||
HStack(spacing: 12) {
|
||||
Text("⚾")
|
||||
.font(.system(size: 32))
|
||||
.rotationEffect(.degrees(-10))
|
||||
Text("🏀")
|
||||
.font(.system(size: 36))
|
||||
Text("🏈")
|
||||
.font(.system(size: 32))
|
||||
.rotationEffect(.degrees(10))
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
|
||||
// Title with bounce
|
||||
VStack(spacing: 4) {
|
||||
Text("Sports Time!")
|
||||
.font(.system(size: 34, weight: .heavy, design: .rounded))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [candyPink, candyPurple],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
|
||||
Text("Your adventure starts here")
|
||||
.font(.system(size: 15, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.7) : Color(white: 0.4))
|
||||
}
|
||||
|
||||
// Playful CTA
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Text("Let's Go!")
|
||||
.font(.system(size: 18, weight: .bold, design: .rounded))
|
||||
|
||||
Image(systemName: "arrow.right.circle.fill")
|
||||
.font(.system(size: 22))
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 36)
|
||||
.padding(.vertical, 18)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [candyPink, candyPurple],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
)
|
||||
.shadow(color: candyPink.opacity(0.4), radius: 20, y: 10)
|
||||
}
|
||||
}
|
||||
.padding(28)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 36)
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.08) : Color.white.opacity(0.9))
|
||||
.shadow(color: Color.black.opacity(0.08), radius: 20, y: 8)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Featured Section
|
||||
|
||||
private var featuredSection: some View {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
HStack {
|
||||
Text("Cool Trips")
|
||||
.font(.system(size: 22, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15))
|
||||
|
||||
Text("✨")
|
||||
.font(.system(size: 18))
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await suggestedTripsGenerator.refreshTrips()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundStyle(candyBlue)
|
||||
.padding(12)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(candyBlue.opacity(0.15))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
playfulTripCard(suggestedTrip.trip, colorIndex: index)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func playfulTripCard(_ trip: Trip, colorIndex: Int) -> some View {
|
||||
let colors = [candyPink, candyBlue, candyGreen, candyPurple, candyYellow]
|
||||
let accentColor = colors[colorIndex % colors.count]
|
||||
|
||||
return HStack(spacing: 16) {
|
||||
// Bouncy icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(accentColor.opacity(0.2))
|
||||
.frame(width: 56, height: 56)
|
||||
|
||||
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
|
||||
.font(.system(size: 24))
|
||||
.foregroundStyle(accentColor)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15))
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "mappin.circle.fill")
|
||||
.font(.system(size: 12))
|
||||
Text("\(trip.stops.count)")
|
||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
||||
}
|
||||
.foregroundStyle(accentColor)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.font(.system(size: 12))
|
||||
Text("\(trip.totalGames)")
|
||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
||||
}
|
||||
.foregroundStyle(colorScheme == .dark ? .white.opacity(0.6) : Color(white: 0.5))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right.circle.fill")
|
||||
.font(.system(size: 24))
|
||||
.foregroundStyle(accentColor.opacity(0.5))
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.06) : Color.white.opacity(0.9))
|
||||
.shadow(color: accentColor.opacity(0.15), radius: 12, y: 4)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Saved Section
|
||||
|
||||
private var savedSection: some View {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
HStack {
|
||||
Text("Your Trips")
|
||||
.font(.system(size: 22, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15))
|
||||
|
||||
Text("🎒")
|
||||
.font(.system(size: 18))
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("See all")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(candyPurple)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(Array(savedTrips.prefix(3).enumerated()), id: \.element.id) { index, savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: savedTrip.games)
|
||||
} label: {
|
||||
let colors = [candyYellow, candyGreen, candyBlue]
|
||||
let accentColor = colors[index % colors.count]
|
||||
|
||||
HStack(spacing: 14) {
|
||||
// Number bubble
|
||||
Text("\(index + 1)")
|
||||
.font(.system(size: 14, weight: .bold, design: .rounded))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(accentColor)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 15, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15))
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.system(size: 12, design: .rounded))
|
||||
.foregroundStyle(colorScheme == .dark ? .white.opacity(0.5) : Color(white: 0.5))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Capsule()
|
||||
.fill(accentColor.opacity(0.2))
|
||||
.frame(width: 44, height: 28)
|
||||
.overlay(
|
||||
Text("\(trip.stops.count)")
|
||||
.font(.system(size: 13, weight: .semibold, design: .rounded))
|
||||
.foregroundStyle(accentColor)
|
||||
)
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(colorScheme == .dark ? Color.white.opacity(0.05) : Color.white.opacity(0.85))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fun Footer
|
||||
|
||||
private var funFooter: some View {
|
||||
VStack(spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
Text("🏟️")
|
||||
Text("🚗")
|
||||
Text("🎉")
|
||||
}
|
||||
.font(.system(size: 16))
|
||||
|
||||
Text("Sports Time")
|
||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(colorScheme == .dark ? .white.opacity(0.4) : Color(white: 0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user