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:
@@ -0,0 +1,347 @@
|
||||
//
|
||||
// HomeContent_CarrotWeather.swift
|
||||
// SportsTime
|
||||
//
|
||||
// CARROT WEATHER-INSPIRED: Bold personality, clean layout.
|
||||
// Dynamic content, good use of gradients.
|
||||
// Data-focused with character.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_CarrotWeather: 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]
|
||||
|
||||
// Carrot-inspired colors
|
||||
private var bgGradient: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: colorScheme == .dark
|
||||
? [Color(red: 0.1, green: 0.12, blue: 0.18), Color(red: 0.08, green: 0.08, blue: 0.12)]
|
||||
: [Color(red: 0.45, green: 0.65, blue: 0.9), Color(red: 0.35, green: 0.55, blue: 0.85)],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
|
||||
private var cardBg: Color {
|
||||
colorScheme == .dark
|
||||
? Color.white.opacity(0.08)
|
||||
: Color.white.opacity(0.25)
|
||||
}
|
||||
|
||||
private let carrotOrange = Color(red: 1.0, green: 0.45, blue: 0.2)
|
||||
|
||||
private var textPrimary: Color {
|
||||
.white
|
||||
}
|
||||
|
||||
private var textSecondary: Color {
|
||||
Color.white.opacity(0.7)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// Main display
|
||||
mainDisplay
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
|
||||
// Trip summary card
|
||||
tripSummaryCard
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// Recent trips
|
||||
if !savedTrips.isEmpty {
|
||||
recentTripsSection
|
||||
}
|
||||
|
||||
// Suggestions
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
suggestionsSection
|
||||
}
|
||||
|
||||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
.background(bgGradient.ignoresSafeArea())
|
||||
}
|
||||
|
||||
// MARK: - Main Display
|
||||
|
||||
private var mainDisplay: some View {
|
||||
VStack(spacing: 12) {
|
||||
// Big number display (like temperature)
|
||||
Text("\(savedTrips.count)")
|
||||
.font(.system(size: 96, weight: .thin))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text(tripStatusMessage)
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundStyle(textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
// Personality message
|
||||
Text(personalityMessage)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(textSecondary.opacity(0.8))
|
||||
.italic()
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
|
||||
private var tripStatusMessage: String {
|
||||
if savedTrips.isEmpty {
|
||||
return "No trips planned"
|
||||
} else if savedTrips.count == 1 {
|
||||
return "Trip planned"
|
||||
} else {
|
||||
return "Trips in your queue"
|
||||
}
|
||||
}
|
||||
|
||||
private var personalityMessage: String {
|
||||
if savedTrips.isEmpty {
|
||||
return "Time to hit the road, sports fan!"
|
||||
} else if savedTrips.count >= 3 {
|
||||
return "Someone's serious about their sports!"
|
||||
} else {
|
||||
return "Adventure awaits..."
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Trip Summary Card
|
||||
|
||||
private var tripSummaryCard: some View {
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
VStack(spacing: 16) {
|
||||
HStack(spacing: 20) {
|
||||
summaryItem(value: "\(totalGames)", label: "Games", icon: "sportscourt.fill")
|
||||
summaryItem(value: "\(totalStops)", label: "Cities", icon: "building.2.fill")
|
||||
summaryItem(value: "\(uniqueSportsCount)", label: "Sports", icon: "figure.run")
|
||||
}
|
||||
|
||||
// CTA
|
||||
HStack {
|
||||
Text("Plan a Trip")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
}
|
||||
.foregroundStyle(textPrimary)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(carrotOrange)
|
||||
)
|
||||
}
|
||||
.padding(20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(cardBg)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func summaryItem(value: String, label: String, icon: String) -> some View {
|
||||
VStack(spacing: 6) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 18))
|
||||
.foregroundStyle(textSecondary)
|
||||
|
||||
Text(value)
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text(label)
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private var totalGames: Int {
|
||||
savedTrips.compactMap { $0.trip?.totalGames }.reduce(0, +)
|
||||
}
|
||||
|
||||
private var totalStops: Int {
|
||||
savedTrips.compactMap { $0.trip?.stops.count }.reduce(0, +)
|
||||
}
|
||||
|
||||
private var uniqueSportsCount: Int {
|
||||
Set(savedTrips.flatMap { $0.trip?.uniqueSports ?? [] }).count
|
||||
}
|
||||
|
||||
// MARK: - Recent Trips Section
|
||||
|
||||
private var recentTripsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack {
|
||||
Text("YOUR TRIPS")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.tracking(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("See All")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(textPrimary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
ForEach(savedTrips.prefix(3)) { savedTrip in
|
||||
if let trip = savedTrip.trip {
|
||||
NavigationLink {
|
||||
TripDetailView(trip: trip, games: savedTrip.games)
|
||||
} label: {
|
||||
tripRow(trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
|
||||
private func tripRow(_ trip: Trip) -> some View {
|
||||
HStack(spacing: 14) {
|
||||
// Sport indicator
|
||||
Circle()
|
||||
.fill(trip.uniqueSports.first?.themeColor ?? carrotOrange)
|
||||
.frame(width: 40, height: 40)
|
||||
.overlay(
|
||||
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
|
||||
.font(.system(size: 16))
|
||||
.foregroundStyle(.white)
|
||||
)
|
||||
|
||||
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))
|
||||
.foregroundStyle(textSecondary.opacity(0.6))
|
||||
}
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(cardBg)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Suggestions Section
|
||||
|
||||
private var suggestionsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack {
|
||||
Text("SUGGESTED")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.tracking(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await suggestedTripsGenerator.refreshTrips()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(textPrimary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 12) {
|
||||
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
suggestionCard(suggestedTrip.trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func suggestionCard(_ trip: Trip) -> some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
// Header with big stat
|
||||
VStack(spacing: 4) {
|
||||
Text("\(trip.totalGames)")
|
||||
.font(.system(size: 32, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text("games")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(2)
|
||||
|
||||
Text("\(trip.stops.count) cities")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 130)
|
||||
.padding(14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(cardBg)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(.ultraThinMaterial)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user