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,338 @@
//
// HomeContent_Glassmorphism.swift
// SportsTime
//
// GLASSMORPHISM: Frosted glass, flowing ethereal shapes.
// Stadium lights blur effect, soft glows, dreamy atmosphere.
//
import SwiftUI
import SwiftData
struct HomeContent_Glassmorphism: 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]
// Ethereal color palette
private let glowPurple = Color(red: 0.6, green: 0.4, blue: 1.0)
private let glowBlue = Color(red: 0.3, green: 0.6, blue: 1.0)
private let glowPink = Color(red: 1.0, green: 0.5, blue: 0.7)
var body: some View {
ZStack {
// Gradient background with floating orbs
backgroundLayer
ScrollView {
VStack(spacing: 24) {
// HERO GLASS CARD
heroGlassCard
.padding(.top, 20)
.padding(.horizontal, 16)
// FEATURED TRIPS
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
featuredSection
.padding(.horizontal, 16)
}
// SAVED TRIPS
if !savedTrips.isEmpty {
savedSection
.padding(.horizontal, 16)
}
Spacer(minLength: 32)
}
}
}
}
// MARK: - Background Layer
private var backgroundLayer: some View {
ZStack {
// Base gradient
LinearGradient(
colors: colorScheme == .dark
? [Color(red: 0.1, green: 0.05, blue: 0.2), Color(red: 0.05, green: 0.1, blue: 0.2)]
: [Color(red: 0.95, green: 0.93, blue: 1.0), Color(red: 0.9, green: 0.95, blue: 1.0)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
// Floating orbs (stadium lights effect)
Circle()
.fill(glowPurple.opacity(0.3))
.frame(width: 300, height: 300)
.blur(radius: 80)
.offset(x: -100, y: -200)
Circle()
.fill(glowBlue.opacity(0.3))
.frame(width: 250, height: 250)
.blur(radius: 70)
.offset(x: 120, y: 100)
Circle()
.fill(glowPink.opacity(0.2))
.frame(width: 200, height: 200)
.blur(radius: 60)
.offset(x: -80, y: 400)
}
}
// MARK: - Hero Glass Card
private var heroGlassCard: some View {
VStack(alignment: .leading, spacing: 20) {
// Floating label
Text("Welcome to")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(glowPurple)
// Title with soft glow
Text("Sports Time")
.font(.system(size: 36, weight: .bold, design: .rounded))
.foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15))
Text("Plan your perfect sports road trip with our intelligent route optimizer.")
.font(.system(size: 15, weight: .regular))
.foregroundStyle(colorScheme == .dark ? .white.opacity(0.7) : Color(white: 0.4))
.lineSpacing(4)
// Glass button
Button {
showNewTrip = true
} label: {
HStack(spacing: 12) {
Image(systemName: "sparkles")
.font(.system(size: 16))
Text("Start Planning")
.font(.system(size: 16, weight: .semibold, design: .rounded))
Spacer()
Image(systemName: "arrow.right.circle.fill")
.font(.system(size: 20))
}
.foregroundStyle(.white)
.padding(18)
.background(
LinearGradient(
colors: [glowPurple, glowBlue],
startPoint: .leading,
endPoint: .trailing
)
)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: glowPurple.opacity(0.4), radius: 20, x: 0, y: 10)
}
}
.padding(24)
.background(glassBackground)
.clipShape(RoundedRectangle(cornerRadius: 24))
}
// MARK: - Glass Background
private var glassBackground: some View {
ZStack {
// Frosted glass effect
if colorScheme == .dark {
Color.white.opacity(0.08)
} else {
Color.white.opacity(0.6)
}
}
.background(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 24)
.stroke(
LinearGradient(
colors: [
Color.white.opacity(colorScheme == .dark ? 0.3 : 0.8),
Color.white.opacity(colorScheme == .dark ? 0.1 : 0.3)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
)
}
// MARK: - Featured Section
private var featuredSection: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Featured Trips")
.font(.system(size: 20, weight: .semibold, design: .rounded))
.foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15))
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14))
.foregroundStyle(glowPurple)
.padding(10)
.background(glassCardBackground)
.clipShape(Circle())
}
}
// Horizontal scroll of glass cards
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(5)) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
glassTripCard(suggestedTrip.trip)
}
.buttonStyle(.plain)
}
}
.padding(.vertical, 4)
}
}
}
private func glassTripCard(_ trip: Trip) -> some View {
VStack(alignment: .leading, spacing: 12) {
// Sport icons with glow
HStack(spacing: 8) {
ForEach(Array(trip.uniqueSports.prefix(3)), id: \.self) { sport in
Image(systemName: sport.iconName)
.font(.system(size: 14))
.foregroundStyle(sport.themeColor)
.shadow(color: sport.themeColor.opacity(0.5), radius: 8)
}
}
Text(trip.name)
.font(.system(size: 16, weight: .semibold, design: .rounded))
.foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15))
.lineLimit(2)
Spacer(minLength: 0)
HStack {
Label("\(trip.stops.count)", systemImage: "mappin.circle")
Spacer()
Label("\(trip.totalGames)", systemImage: "sportscourt")
}
.font(.system(size: 12, weight: .medium))
.foregroundStyle(colorScheme == .dark ? .white.opacity(0.6) : Color(white: 0.4))
}
.padding(16)
.frame(width: 160, height: 140)
.background(glassCardBackground)
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.white.opacity(colorScheme == .dark ? 0.15 : 0.5), lineWidth: 1)
)
}
private var glassCardBackground: some View {
ZStack {
if colorScheme == .dark {
Color.white.opacity(0.06)
} else {
Color.white.opacity(0.5)
}
}
.background(.ultraThinMaterial)
}
// MARK: - Saved Section
private var savedSection: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Your Trips")
.font(.system(size: 20, weight: .semibold, design: .rounded))
.foregroundStyle(colorScheme == .dark ? .white : Color(white: 0.15))
Spacer()
Button {
selectedTab = 2
} label: {
Text("See All")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(glowPurple)
}
}
VStack(spacing: 12) {
ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip {
NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games)
} label: {
HStack(spacing: 16) {
// Glow orb
ZStack {
Circle()
.fill(
LinearGradient(
colors: [glowPurple.opacity(0.3), glowBlue.opacity(0.3)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 44, height: 44)
.blur(radius: 4)
Image(systemName: "map.fill")
.font(.system(size: 16))
.foregroundStyle(glowPurple)
}
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.stops.count) cities · \(trip.totalGames) games")
.font(.system(size: 12))
.foregroundStyle(colorScheme == .dark ? .white.opacity(0.5) : Color(white: 0.5))
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(colorScheme == .dark ? .white.opacity(0.3) : Color(white: 0.5))
}
.padding(16)
.background(glassCardBackground)
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.white.opacity(colorScheme == .dark ? 0.1 : 0.4), lineWidth: 1)
)
}
.buttonStyle(.plain)
}
}
}
}
}
}