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