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,338 @@
//
// HomeContent_Fantastical.swift
// SportsTime
//
// FANTASTICAL-INSPIRED: Calendar elegance.
// Data-dense but readable, rich colors.
// Schedule-focused with beautiful typography.
//
import SwiftUI
import SwiftData
struct HomeContent_Fantastical: 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]
// Fantastical-inspired colors
private var bgColor: Color {
colorScheme == .dark
? Color(red: 0.1, green: 0.1, blue: 0.12)
: Color(red: 0.97, green: 0.97, blue: 0.98)
}
private var cardBg: Color {
colorScheme == .dark
? Color(red: 0.15, green: 0.15, blue: 0.17)
: Color.white
}
private let fantasticalRed = Color(red: 0.92, green: 0.26, blue: 0.26)
private let fantasticalBlue = Color(red: 0.2, green: 0.55, blue: 0.95)
private var textPrimary: Color {
colorScheme == .dark ? .white : Color(red: 0.15, green: 0.15, blue: 0.18)
}
private var textSecondary: Color {
colorScheme == .dark ? Color(white: 0.55) : Color(white: 0.45)
}
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Date header
dateHeader
.padding(.horizontal, 16)
.padding(.top, 8)
// Quick add
quickAddButton
.padding(.horizontal, 16)
// Upcoming section
if !savedTrips.isEmpty {
upcomingSection
}
// Suggestions
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
suggestionsSection
}
Spacer(minLength: 40)
}
}
.background(bgColor.ignoresSafeArea())
}
// MARK: - Date Header
private var dateHeader: some View {
HStack(alignment: .bottom) {
VStack(alignment: .leading, spacing: 4) {
Text(dayOfWeek)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(fantasticalRed)
Text(formattedDate)
.font(.system(size: 28, weight: .bold))
.foregroundStyle(textPrimary)
}
Spacer()
// Today indicator
VStack(spacing: 2) {
Text("TODAY")
.font(.system(size: 9, weight: .bold))
.foregroundStyle(.white)
Text(dayNumber)
.font(.system(size: 20, weight: .bold))
.foregroundStyle(.white)
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(fantasticalRed)
)
}
}
private var dayOfWeek: String {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE"
return formatter.string(from: Date())
}
private var formattedDate: String {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM d, yyyy"
return formatter.string(from: Date())
}
private var dayNumber: String {
let formatter = DateFormatter()
formatter.dateFormat = "d"
return formatter.string(from: Date())
}
// MARK: - Quick Add Button
private var quickAddButton: some View {
Button {
showNewTrip = true
} label: {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(fantasticalRed)
.frame(width: 28, height: 28)
Image(systemName: "plus")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(.white)
}
Text("Plan New Trip")
.font(.system(size: 16, weight: .medium))
.foregroundStyle(textPrimary)
Spacer()
}
.padding(14)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.3 : 0.06), radius: 6, y: 2)
)
}
.buttonStyle(.plain)
}
// MARK: - Upcoming Section
private var upcomingSection: some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
Text("Upcoming")
.font(.system(size: 20, weight: .bold))
.foregroundStyle(textPrimary)
Spacer()
Button {
selectedTab = 2
} label: {
Text("See All")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(fantasticalBlue)
}
}
.padding(.horizontal, 16)
VStack(spacing: 2) {
ForEach(savedTrips.prefix(4)) { savedTrip in
if let trip = savedTrip.trip {
NavigationLink {
TripDetailView(trip: trip, games: savedTrip.games)
} label: {
eventRow(trip)
}
.buttonStyle(.plain)
}
}
}
.background(
RoundedRectangle(cornerRadius: 14)
.fill(cardBg)
)
.padding(.horizontal, 16)
}
}
private func eventRow(_ trip: Trip) -> some View {
HStack(spacing: 14) {
// Color bar
RoundedRectangle(cornerRadius: 2)
.fill(trip.uniqueSports.first?.themeColor ?? fantasticalBlue)
.frame(width: 4, height: 44)
VStack(alignment: .leading, spacing: 3) {
Text(trip.name)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(textPrimary)
.lineLimit(1)
HStack(spacing: 8) {
Text(trip.formattedDateRange)
.font(.system(size: 12))
.foregroundStyle(textSecondary)
Text("")
.foregroundStyle(textSecondary)
HStack(spacing: 3) {
Image(systemName: "sportscourt.fill")
.font(.system(size: 10))
Text("\(trip.totalGames)")
.font(.system(size: 12))
}
.foregroundStyle(textSecondary)
}
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(textSecondary.opacity(0.5))
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
}
// MARK: - Suggestions Section
private var suggestionsSection: some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
Text("Suggested Routes")
.font(.system(size: 20, weight: .bold))
.foregroundStyle(textPrimary)
Spacer()
Button {
Task {
await suggestedTripsGenerator.refreshTrips()
}
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 14))
.foregroundStyle(fantasticalBlue)
}
}
.padding(.horizontal, 16)
VStack(spacing: 10) {
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(3)) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
suggestionCard(suggestedTrip.trip)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
}
}
private func suggestionCard(_ trip: Trip) -> some View {
HStack(spacing: 14) {
// Time block style
VStack(spacing: 2) {
Text("\(trip.stops.count)")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(textPrimary)
Text("stops")
.font(.system(size: 10))
.foregroundStyle(textSecondary)
}
.frame(width: 50)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(trip.uniqueSports.first?.themeColor.opacity(0.15) ?? fantasticalBlue.opacity(0.15))
)
VStack(alignment: .leading, spacing: 4) {
Text(trip.name)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(textPrimary)
.lineLimit(1)
HStack(spacing: 6) {
if let sport = trip.uniqueSports.first {
HStack(spacing: 3) {
Image(systemName: sport.iconName)
.font(.system(size: 10))
Text(sport.displayName)
.font(.system(size: 12))
}
.foregroundStyle(sport.themeColor)
}
Text("")
.foregroundStyle(textSecondary)
Text("\(trip.totalGames) games")
.font(.system(size: 12))
.foregroundStyle(textSecondary)
}
}
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(textSecondary.opacity(0.5))
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(cardBg)
.shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.05), radius: 4, y: 2)
)
}
}