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,346 @@
//
// HomeContent_Classic.swift
// SportsTime
//
// CLASSIC: The original SportsTime design.
// Uses the app's Theme system with warm orange accents.
// Clean cards, glow effects, and familiar layout.
//
import SwiftUI
import SwiftData
struct HomeContent_Classic: 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]
var body: some View {
ScrollView {
VStack(spacing: Theme.Spacing.lg) {
// Hero Card
heroCard
.padding(.horizontal, Theme.Spacing.md)
.padding(.top, Theme.Spacing.sm)
// Quick Actions
quickActions
.padding(.horizontal, Theme.Spacing.md)
// Suggested Trips
suggestedTripsSection
.padding(.horizontal, Theme.Spacing.md)
// Saved Trips
if !savedTrips.isEmpty {
savedTripsSection
.padding(.horizontal, Theme.Spacing.md)
}
// Planning Tips
if !displayedTips.isEmpty {
tipsSection
.padding(.horizontal, Theme.Spacing.md)
}
Spacer(minLength: 40)
}
}
.themedBackground()
}
// MARK: - Hero Card
private var heroCard: some View {
VStack(spacing: Theme.Spacing.lg) {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Adventure Awaits")
.font(.largeTitle)
.foregroundStyle(Theme.textPrimary(colorScheme))
Text("Plan your ultimate sports road trip. Visit stadiums, catch games, and create unforgettable memories.")
.font(.body)
.foregroundStyle(Theme.textSecondary(colorScheme))
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxWidth: .infinity, alignment: .leading)
Button {
showNewTrip = true
} label: {
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: "map.fill")
Text("Start Planning")
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding(Theme.Spacing.md)
.background(Theme.warmOrange)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
}
.pressableStyle()
.glowEffect(color: Theme.warmOrange, radius: 12)
}
.padding(Theme.Spacing.lg)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.xlarge)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.shadow(color: Theme.cardShadow(colorScheme), radius: 15, y: 8)
}
// MARK: - Quick Actions
private var quickActions: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Quick Start")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
SportSelectorGrid { _ in
showNewTrip = true
}
.padding(.horizontal, Theme.Spacing.md)
.padding(.vertical, Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
}
}
// MARK: - Suggested Trips
@ViewBuilder
private var suggestedTripsSection: some View {
if suggestedTripsGenerator.isLoading {
LoadingTripsView(message: suggestedTripsGenerator.loadingMessage)
} else if !suggestedTripsGenerator.suggestedTrips.isEmpty {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Header with refresh button
HStack {
Text("Featured Trips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.subheadline)
.foregroundStyle(Theme.warmOrange)
}
}
// Horizontal carousel grouped by region
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.lg) {
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Region header
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: regionGroup.region.iconName)
.font(.caption)
Text(regionGroup.region.shortName)
.font(.subheadline)
}
.foregroundStyle(Theme.textSecondary(colorScheme))
// Trip cards for this region
HStack(spacing: Theme.Spacing.md) {
ForEach(regionGroup.trips) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
SuggestedTripCard(suggestedTrip: suggestedTrip)
}
.buttonStyle(.plain)
}
}
}
}
}
.padding(.horizontal, 1)
}
}
} else if let error = suggestedTripsGenerator.error {
// Error state
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Featured Trips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
HStack {
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(.orange)
Text(error)
.font(.subheadline)
.foregroundStyle(Theme.textSecondary(colorScheme))
Spacer()
Button("Retry") {
Task {
await suggestedTripsGenerator.generateTrips()
}
}
.font(.subheadline)
.foregroundStyle(Theme.warmOrange)
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
}
}
}
// MARK: - Saved Trips
private var savedTripsSection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
HStack {
Text("Recent Trips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
Button {
selectedTab = 2
} label: {
HStack(spacing: 4) {
Text("See All")
Image(systemName: "chevron.right")
.font(.caption)
}
.font(.subheadline)
.foregroundStyle(Theme.warmOrange)
}
}
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: {
classicTripCard(savedTrip: savedTrip, trip: trip)
}
.buttonStyle(.plain)
.staggeredAnimation(index: index, delay: 0.05)
}
}
}
}
private func classicTripCard(savedTrip: SavedTrip, trip: Trip) -> some View {
HStack(spacing: Theme.Spacing.md) {
// Route preview icon
ZStack {
Circle()
.fill(Theme.warmOrange.opacity(0.15))
.frame(width: 44, height: 44)
Image(systemName: "map.fill")
.foregroundStyle(Theme.warmOrange)
}
VStack(alignment: .leading, spacing: 4) {
Text(trip.name)
.font(.body)
.foregroundStyle(Theme.textPrimary(colorScheme))
Text(trip.formattedDateRange)
.font(.subheadline)
.foregroundStyle(Theme.textSecondary(colorScheme))
HStack(spacing: Theme.Spacing.sm) {
HStack(spacing: 4) {
Image(systemName: "mappin")
.font(.caption2)
Text("\(trip.stops.count) cities")
}
HStack(spacing: 4) {
Image(systemName: "sportscourt")
.font(.caption2)
Text("\(trip.totalGames) games")
}
}
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
}
// MARK: - Tips Section
private var tipsSection: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
Text("Planning Tips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
VStack(spacing: Theme.Spacing.xs) {
ForEach(displayedTips) { tip in
classicTipRow(icon: tip.icon, title: tip.title, subtitle: tip.subtitle)
}
}
.padding(Theme.Spacing.md)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
}
}
private func classicTipRow(icon: String, title: String, subtitle: String) -> some View {
HStack(spacing: Theme.Spacing.sm) {
ZStack {
Circle()
.fill(Theme.routeGold.opacity(0.15))
.frame(width: 36, height: 36)
Image(systemName: icon)
.font(.subheadline)
.foregroundStyle(Theme.routeGold)
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.subheadline)
.foregroundStyle(Theme.textPrimary(colorScheme))
Text(subtitle)
.font(.caption)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
Spacer()
}
}
}