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,372 @@
//
// HomeContent_Strava.swift
// SportsTime
//
// STRAVA-INSPIRED: Athletic, data-driven.
// Orange accent, activity stats, route-focused.
// Performance metrics and community feel.
//
import SwiftUI
import SwiftData
struct HomeContent_Strava: 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]
// Strava-inspired colors
private var bgColor: Color {
colorScheme == .dark
? Color(red: 0.08, green: 0.08, blue: 0.1)
: Color(red: 0.96, green: 0.96, blue: 0.97)
}
private var cardBg: Color {
colorScheme == .dark
? Color(red: 0.12, green: 0.12, blue: 0.14)
: Color.white
}
private let stravaOrange = Color(red: 0.99, green: 0.32, blue: 0.15)
private var textPrimary: Color {
colorScheme == .dark ? .white : Color(red: 0.12, green: 0.12, blue: 0.14)
}
private var textSecondary: Color {
colorScheme == .dark ? Color(white: 0.55) : Color(white: 0.45)
}
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Profile header
profileHeader
.padding(.horizontal, 16)
.padding(.top, 8)
// Stats overview
statsOverview
.padding(.horizontal, 16)
// Record button
recordButton
.padding(.horizontal, 16)
// Recent activities
if !savedTrips.isEmpty {
recentActivities
}
// Routes
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
routesSection
}
Spacer(minLength: 40)
}
}
.background(bgColor.ignoresSafeArea())
}
// MARK: - Profile Header
private var profileHeader: some View {
HStack(spacing: 14) {
// Profile avatar
Circle()
.fill(
LinearGradient(
colors: [stravaOrange, stravaOrange.opacity(0.7)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 50, height: 50)
.overlay(
Image(systemName: "person.fill")
.font(.system(size: 22))
.foregroundStyle(.white)
)
VStack(alignment: .leading, spacing: 2) {
Text("Sports Time")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(textPrimary)
Text("Trip Planner")
.font(.system(size: 13))
.foregroundStyle(textSecondary)
}
Spacer()
// Notifications
Button {} label: {
Image(systemName: "bell.fill")
.font(.system(size: 18))
.foregroundStyle(textSecondary)
}
}
}
// MARK: - Stats Overview
private var statsOverview: some View {
HStack(spacing: 0) {
statBlock(value: "\(savedTrips.count)", label: "Trips", color: stravaOrange)
statDivider
statBlock(value: "\(totalGames)", label: "Games", color: .blue)
statDivider
statBlock(value: "\(totalStops)", label: "Cities", color: .green)
}
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.06), radius: 6, y: 2)
)
}
private func statBlock(value: String, label: String, color: Color) -> some View {
VStack(spacing: 4) {
Text(value)
.font(.system(size: 24, weight: .bold))
.foregroundStyle(textPrimary)
Text(label)
.font(.system(size: 12))
.foregroundStyle(textSecondary)
}
.frame(maxWidth: .infinity)
}
private var statDivider: some View {
Rectangle()
.fill(textSecondary.opacity(0.2))
.frame(width: 1, height: 40)
}
private var totalGames: Int {
savedTrips.compactMap { $0.trip?.totalGames }.reduce(0, +)
}
private var totalStops: Int {
savedTrips.compactMap { $0.trip?.stops.count }.reduce(0, +)
}
// MARK: - Record Button
private var recordButton: some View {
Button {
showNewTrip = true
} label: {
HStack(spacing: 10) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 20))
Text("Plan New Trip")
.font(.system(size: 16, weight: .semibold))
}
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(stravaOrange)
)
}
.buttonStyle(.plain)
}
// MARK: - Recent Activities
private var recentActivities: some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
Text("Your Activities")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(textPrimary)
Spacer()
Button {
selectedTab = 2
} label: {
Text("View All")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(stravaOrange)
}
}
.padding(.horizontal, 16)
VStack(spacing: 12) {
ForEach(savedTrips.prefix(3)) { savedTrip in
if let trip = savedTrip.trip {
NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games)
} label: {
activityCard(trip)
}
.buttonStyle(.plain)
}
}
}
.padding(.horizontal, 16)
}
}
private func activityCard(_ trip: Trip) -> some View {
VStack(spacing: 12) {
// Header
HStack(spacing: 10) {
Circle()
.fill(trip.uniqueSports.first?.themeColor ?? stravaOrange)
.frame(width: 36, height: 36)
.overlay(
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
.font(.system(size: 14))
.foregroundStyle(.white)
)
VStack(alignment: .leading, spacing: 2) {
Text(trip.name)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(textPrimary)
.lineLimit(1)
Text(trip.formattedDateRange)
.font(.system(size: 12))
.foregroundStyle(textSecondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12))
.foregroundStyle(textSecondary)
}
// Stats row
HStack(spacing: 24) {
activityStat(value: "\(trip.stops.count)", label: "Cities")
activityStat(value: "\(trip.totalGames)", label: "Games")
if let sport = trip.uniqueSports.first {
activityStat(value: sport.displayName, label: "Sport")
}
}
}
.padding(14)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.2 : 0.05), radius: 4, y: 2)
)
}
private func activityStat(value: String, label: String) -> some View {
VStack(alignment: .leading, spacing: 2) {
Text(value)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(textPrimary)
Text(label)
.font(.system(size: 11))
.foregroundStyle(textSecondary)
}
}
// MARK: - Routes Section
private var routesSection: some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
Text("Suggested Routes")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(textPrimary)
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14))
.foregroundStyle(stravaOrange)
}
}
.padding(.horizontal, 16)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
routeCard(suggestedTrip.trip)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
}
}
}
private func routeCard(_ trip: Trip) -> some View {
VStack(alignment: .leading, spacing: 10) {
// Route preview
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(
LinearGradient(
colors: [
trip.uniqueSports.first?.themeColor.opacity(0.3) ?? stravaOrange.opacity(0.3),
trip.uniqueSports.first?.themeColor.opacity(0.1) ?? stravaOrange.opacity(0.1)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(height: 80)
// Simplified route line
Path { path in
path.move(to: CGPoint(x: 20, y: 60))
path.addCurve(
to: CGPoint(x: 120, y: 20),
control1: CGPoint(x: 50, y: 40),
control2: CGPoint(x: 90, y: 30)
)
}
.stroke(stravaOrange, lineWidth: 3)
}
VStack(alignment: .leading, spacing: 4) {
Text(trip.name)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(textPrimary)
.lineLimit(1)
Text("\(trip.stops.count) cities • \(trip.totalGames) games")
.font(.system(size: 11))
.foregroundStyle(textSecondary)
}
}
.frame(width: 140)
.padding(10)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.2 : 0.05), radius: 4, y: 2)
)
}
}