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,384 @@
//
// HomeContent_Flighty.swift
// SportsTime
//
// FLIGHTY-INSPIRED: Aviation dashboard aesthetic.
// Data-rich widgets, clean typography, professional feel.
// Real-time travel data visualization style.
//
import SwiftUI
import SwiftData
struct HomeContent_Flighty: 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]
// Flighty-inspired colors
private var bgColor: Color {
colorScheme == .dark
? Color(red: 0.06, green: 0.06, blue: 0.08)
: Color(red: 0.97, green: 0.97, blue: 0.98)
}
private var cardBg: Color {
colorScheme == .dark
? Color(red: 0.11, green: 0.11, blue: 0.14)
: Color.white
}
private var accentBlue: Color {
Color(red: 0.2, green: 0.5, blue: 1.0)
}
private var textPrimary: Color {
colorScheme == .dark ? .white : Color(red: 0.1, green: 0.1, blue: 0.12)
}
private var textSecondary: Color {
colorScheme == .dark ? Color(white: 0.55) : Color(white: 0.45)
}
var body: some View {
ScrollView {
VStack(spacing: 16) {
// Status header
statusHeader
.padding(.horizontal, 20)
.padding(.top, 8)
// Quick action card
quickActionCard
.padding(.horizontal, 20)
// Upcoming trips widget
if !savedTrips.isEmpty {
upcomingTripsWidget
.padding(.horizontal, 20)
}
// Suggested routes
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
suggestedRoutesSection
.padding(.horizontal, 20)
}
// Stats dashboard
statsDashboard
.padding(.horizontal, 20)
.padding(.bottom, 32)
}
}
.background(bgColor.ignoresSafeArea())
}
// MARK: - Status Header
private var statusHeader: some View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Sports Time")
.font(.system(size: 28, weight: .bold, design: .default))
.foregroundStyle(textPrimary)
Text(statusText)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(textSecondary)
}
Spacer()
// Live indicator
HStack(spacing: 6) {
Circle()
.fill(Color.green)
.frame(width: 8, height: 8)
Text("LIVE")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(Color.green)
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
Capsule()
.fill(Color.green.opacity(0.15))
)
}
}
private var statusText: String {
if savedTrips.isEmpty {
return "No trips planned"
} else {
return "\(savedTrips.count) trip\(savedTrips.count == 1 ? "" : "s") in your schedule"
}
}
// MARK: - Quick Action Card
private var quickActionCard: some View {
Button {
showNewTrip = true
} label: {
HStack(spacing: 16) {
ZStack {
Circle()
.fill(
LinearGradient(
colors: [accentBlue, accentBlue.opacity(0.7)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 48, height: 48)
Image(systemName: "car.fill")
.font(.system(size: 20, weight: .semibold))
.foregroundStyle(.white)
}
VStack(alignment: .leading, spacing: 4) {
Text("Plan New Trip")
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(textPrimary)
Text("Find games along your route")
.font(.system(size: 13))
.foregroundStyle(textSecondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(textSecondary)
}
.padding(16)
.background(
RoundedRectangle(cornerRadius: 14)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.06), radius: 8, y: 2)
)
}
.buttonStyle(.plain)
}
// MARK: - Upcoming Trips Widget
private var upcomingTripsWidget: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("UPCOMING")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(textSecondary)
.tracking(0.5)
Spacer()
Button {
selectedTab = 2
} label: {
Text("See All")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(accentBlue)
}
}
ForEach(savedTrips.prefix(2)) { savedTrip in
if let trip = savedTrip.trip {
NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games)
} label: {
tripCard(trip)
}
.buttonStyle(.plain)
}
}
}
}
private func tripCard(_ trip: Trip) -> some View {
HStack(spacing: 14) {
// Departure indicator
VStack(spacing: 4) {
Circle()
.fill(accentBlue)
.frame(width: 10, height: 10)
Rectangle()
.fill(textSecondary.opacity(0.3))
.frame(width: 2, height: 30)
Circle()
.stroke(textSecondary.opacity(0.5), lineWidth: 2)
.frame(width: 10, height: 10)
}
VStack(alignment: .leading, spacing: 8) {
Text(trip.name)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(textPrimary)
.lineLimit(1)
HStack(spacing: 16) {
dataLabel(icon: "calendar", value: trip.formattedDateRange)
dataLabel(icon: "mappin", value: "\(trip.stops.count) stops")
}
}
Spacer()
// Time badge
VStack(alignment: .trailing, spacing: 2) {
Text("\(trip.totalGames)")
.font(.system(size: 20, weight: .bold, design: .rounded))
.foregroundStyle(textPrimary)
Text("games")
.font(.system(size: 11))
.foregroundStyle(textSecondary)
}
}
.padding(14)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.05), radius: 6, y: 2)
)
}
private func dataLabel(icon: String, value: String) -> some View {
HStack(spacing: 4) {
Image(systemName: icon)
.font(.system(size: 11))
Text(value)
.font(.system(size: 12))
}
.foregroundStyle(textSecondary)
}
// MARK: - Suggested Routes Section
private var suggestedRoutesSection: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("SUGGESTED ROUTES")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(textSecondary)
.tracking(0.5)
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(accentBlue)
}
}
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(3)) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
routeCard(suggestedTrip.trip)
}
.buttonStyle(.plain)
}
}
}
private func routeCard(_ trip: Trip) -> some View {
HStack(spacing: 12) {
// Sport icon
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(trip.uniqueSports.first?.themeColor.opacity(0.15) ?? accentBlue.opacity(0.15))
.frame(width: 40, height: 40)
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
.font(.system(size: 16))
.foregroundStyle(trip.uniqueSports.first?.themeColor ?? accentBlue)
}
VStack(alignment: .leading, spacing: 3) {
Text(trip.name)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(textPrimary)
.lineLimit(1)
Text("\(trip.stops.count) stops • \(trip.totalGames) games")
.font(.system(size: 12))
.foregroundStyle(textSecondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(textSecondary.opacity(0.6))
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.04), radius: 4, y: 1)
)
}
// MARK: - Stats Dashboard
private var statsDashboard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("AT A GLANCE")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(textSecondary)
.tracking(0.5)
HStack(spacing: 12) {
statCard(value: "\(savedTrips.count)", label: "Trips", icon: "map.fill", color: accentBlue)
statCard(value: "\(totalGames)", label: "Games", icon: "sportscourt.fill", color: .orange)
statCard(value: "\(totalStops)", label: "Cities", icon: "building.2.fill", color: .purple)
}
}
}
private func statCard(value: String, label: String, icon: String, color: Color) -> some View {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 18))
.foregroundStyle(color)
Text(value)
.font(.system(size: 24, weight: .bold, design: .rounded))
.foregroundStyle(textPrimary)
Text(label)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(textSecondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.04), radius: 4, y: 1)
)
}
private var totalGames: Int {
savedTrips.compactMap { $0.trip?.totalGames }.reduce(0, +)
}
private var totalStops: Int {
savedTrips.compactMap { $0.trip?.stops.count }.reduce(0, +)
}
}