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,326 @@
|
||||
//
|
||||
// HomeContent_Airbnb.swift
|
||||
// SportsTime
|
||||
//
|
||||
// AIRBNB-INSPIRED: Travel-focused, warm aesthetic.
|
||||
// Experience cards, inviting colors, rounded corners.
|
||||
// Focus on discovery and exploration.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_Airbnb: 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]
|
||||
|
||||
// Airbnb-inspired colors
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark
|
||||
? Color(red: 0.08, green: 0.08, blue: 0.08)
|
||||
: Color.white
|
||||
}
|
||||
|
||||
private let airbnbRed = Color(red: 1.0, green: 0.22, blue: 0.4)
|
||||
private let warmGray = Color(red: 0.45, green: 0.42, blue: 0.4)
|
||||
|
||||
private var textPrimary: Color {
|
||||
colorScheme == .dark ? .white : Color(red: 0.14, green: 0.14, blue: 0.14)
|
||||
}
|
||||
|
||||
private var textSecondary: Color {
|
||||
colorScheme == .dark ? Color(white: 0.55) : Color(red: 0.45, green: 0.45, blue: 0.45)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 28) {
|
||||
// Search bar
|
||||
searchBar
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 8)
|
||||
|
||||
// Categories
|
||||
categoriesSection
|
||||
|
||||
// Your trips
|
||||
if !savedTrips.isEmpty {
|
||||
yourTripsSection
|
||||
}
|
||||
|
||||
// Explore section
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
exploreSection
|
||||
}
|
||||
|
||||
Spacer(minLength: 40)
|
||||
}
|
||||
}
|
||||
.background(bgColor.ignoresSafeArea())
|
||||
}
|
||||
|
||||
// MARK: - Search Bar
|
||||
|
||||
private var searchBar: some View {
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack(spacing: 14) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Where to?")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text("Anywhere • Any week • Any sport")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Filter button
|
||||
Circle()
|
||||
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
|
||||
.frame(width: 36, height: 36)
|
||||
.overlay(
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(textPrimary)
|
||||
)
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 40)
|
||||
.fill(colorScheme == .dark ? Color(white: 0.15) : Color.white)
|
||||
.shadow(color: Color.black.opacity(0.12), radius: 12, y: 4)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Categories Section
|
||||
|
||||
private var categoriesSection: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 28) {
|
||||
ForEach(Sport.supported) { sport in
|
||||
categoryItem(sport)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
|
||||
private func categoryItem(_ sport: Sport) -> some View {
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.system(size: 24))
|
||||
.foregroundStyle(textSecondary)
|
||||
|
||||
Text(sport.displayName)
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
.frame(width: 56)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Your Trips Section
|
||||
|
||||
private var yourTripsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("Your Trips")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("Show all")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
.underline()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(savedTrips.prefix(4)) { savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: savedTrip.games)
|
||||
} label: {
|
||||
tripCard(trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func tripCard(_ trip: Trip) -> some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
// Image placeholder with gradient
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
trip.uniqueSports.first?.themeColor ?? warmGray,
|
||||
(trip.uniqueSports.first?.themeColor ?? warmGray).opacity(0.6)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(height: 140)
|
||||
|
||||
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
|
||||
.font(.system(size: 36))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
|
||||
// Favorite heart
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "heart")
|
||||
.font(.system(size: 16))
|
||||
.foregroundStyle(.white)
|
||||
.padding(10)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.system(size: 11))
|
||||
Text("New")
|
||||
.font(.system(size: 12))
|
||||
}
|
||||
.foregroundStyle(textPrimary)
|
||||
}
|
||||
|
||||
Text("\(trip.stops.count) cities • \(trip.totalGames) games")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(textSecondary)
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 240)
|
||||
}
|
||||
|
||||
// MARK: - Explore Section
|
||||
|
||||
private var exploreSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("Explore Routes")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await suggestedTripsGenerator.refreshTrips()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(3)) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
exploreCard(suggestedTrip.trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
|
||||
private func exploreCard(_ trip: Trip) -> some View {
|
||||
HStack(spacing: 14) {
|
||||
// Small image
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
trip.uniqueSports.first?.themeColor ?? warmGray,
|
||||
(trip.uniqueSports.first?.themeColor ?? warmGray).opacity(0.7)
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: 80, height: 80)
|
||||
.overlay(
|
||||
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
|
||||
.font(.system(size: 24))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
Text("\(trip.stops.count) stops")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(textSecondary)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "sportscourt.fill")
|
||||
.font(.system(size: 11))
|
||||
Text("\(trip.totalGames) games included")
|
||||
.font(.system(size: 13))
|
||||
}
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user