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,315 @@
//
// HomeContent_AppleMaps.swift
// SportsTime
//
// APPLE MAPS-INSPIRED: Native iOS aesthetic.
// Clean cards, location-focused, subtle shadows.
// Professional and polished feel.
//
import SwiftUI
import SwiftData
struct HomeContent_AppleMaps: 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]
// Apple Maps-inspired colors
private var bgColor: Color {
colorScheme == .dark
? Color(red: 0.0, green: 0.0, blue: 0.0)
: Color(red: 0.95, green: 0.95, blue: 0.97)
}
private var cardBg: Color {
colorScheme == .dark
? Color(red: 0.11, green: 0.11, blue: 0.12)
: Color.white
}
private let mapsBlue = Color(red: 0.0, green: 0.48, blue: 1.0)
private let mapsGreen = Color(red: 0.2, green: 0.78, blue: 0.35)
private var textPrimary: Color {
colorScheme == .dark ? .white : .black
}
private var textSecondary: Color {
colorScheme == .dark ? Color(white: 0.6) : Color(white: 0.4)
}
var body: some View {
ScrollView {
VStack(spacing: 16) {
// Search-style action card
searchCard
.padding(.horizontal, 16)
.padding(.top, 8)
// Recents section
if !savedTrips.isEmpty {
recentsSection
}
// Guides section (suggested trips)
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
guidesSection
}
// Explore section
exploreSection
.padding(.horizontal, 16)
.padding(.bottom, 32)
}
}
.background(bgColor.ignoresSafeArea())
}
// MARK: - Search Card
private var searchCard: some View {
Button {
showNewTrip = true
} label: {
HStack(spacing: 12) {
Image(systemName: "magnifyingglass")
.font(.system(size: 17))
.foregroundStyle(textSecondary)
Text("Plan a trip or search destinations")
.font(.system(size: 17))
.foregroundStyle(textSecondary)
Spacer()
}
.padding(14)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.08), radius: 8, y: 2)
)
}
.buttonStyle(.plain)
}
// MARK: - Recents Section
private var recentsSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Recents")
.font(.system(size: 22, weight: .bold))
.foregroundStyle(textPrimary)
Spacer()
Button {
selectedTab = 2
} label: {
Text("See All")
.font(.system(size: 15))
.foregroundStyle(mapsBlue)
}
}
.padding(.horizontal, 16)
VStack(spacing: 0) {
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: {
recentRow(trip, isLast: index == min(2, savedTrips.count - 1))
}
.buttonStyle(.plain)
}
}
}
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
)
.padding(.horizontal, 16)
}
}
private func recentRow(_ trip: Trip, isLast: Bool) -> some View {
VStack(spacing: 0) {
HStack(spacing: 14) {
// Location pin icon
ZStack {
Circle()
.fill(mapsBlue.opacity(0.15))
.frame(width: 36, height: 36)
Image(systemName: "mappin.circle.fill")
.font(.system(size: 20))
.foregroundStyle(mapsBlue)
}
VStack(alignment: .leading, spacing: 3) {
Text(trip.name)
.font(.system(size: 16))
.foregroundStyle(textPrimary)
.lineLimit(1)
Text("\(trip.stops.count) stops • \(trip.totalGames) games")
.font(.system(size: 13))
.foregroundStyle(textSecondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(Color(white: 0.75))
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
if !isLast {
Divider()
.padding(.leading, 66)
}
}
}
// MARK: - Guides Section
private var guidesSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Suggested Routes")
.font(.system(size: 22, weight: .bold))
.foregroundStyle(textPrimary)
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 15))
.foregroundStyle(mapsBlue)
}
}
.padding(.horizontal, 16)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
guideCard(suggestedTrip.trip)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
}
}
}
private func guideCard(_ trip: Trip) -> some View {
VStack(alignment: .leading, spacing: 10) {
// Header with icon
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(
LinearGradient(
colors: [mapsGreen, mapsGreen.opacity(0.8)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(height: 80)
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
.font(.system(size: 28))
.foregroundStyle(.white)
}
VStack(alignment: .leading, spacing: 4) {
Text(trip.name)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(textPrimary)
.lineLimit(2)
.multilineTextAlignment(.leading)
Text("\(trip.stops.count) places")
.font(.system(size: 12))
.foregroundStyle(textSecondary)
}
.padding(.horizontal, 10)
.padding(.bottom, 10)
}
.frame(width: 150)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.06), radius: 6, y: 2)
)
}
// MARK: - Explore Section
private var exploreSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Explore")
.font(.system(size: 22, weight: .bold))
.foregroundStyle(textPrimary)
VStack(spacing: 0) {
ForEach(Array(Sport.supported.prefix(4).enumerated()), id: \.element.id) { index, sport in
Button {
showNewTrip = true
} label: {
exploreRow(sport, isLast: index == min(3, Sport.supported.count - 1))
}
.buttonStyle(.plain)
}
}
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
)
}
}
private func exploreRow(_ sport: Sport, isLast: Bool) -> some View {
VStack(spacing: 0) {
HStack(spacing: 14) {
Image(systemName: sport.iconName)
.font(.system(size: 18))
.foregroundStyle(sport.themeColor)
.frame(width: 28)
Text(sport.displayName)
.font(.system(size: 16))
.foregroundStyle(textPrimary)
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(Color(white: 0.75))
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
if !isLast {
Divider()
.padding(.leading, 58)
}
}
}
}