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,299 @@
|
||||
//
|
||||
// HomeContent_Things3.swift
|
||||
// SportsTime
|
||||
//
|
||||
// THINGS 3-INSPIRED: Ultra-clean task management aesthetic.
|
||||
// Beautiful spacing, elegant typography, minimalist.
|
||||
// Focus on clarity and completion.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_Things3: 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]
|
||||
|
||||
// Things 3-inspired colors
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark
|
||||
? Color(red: 0.11, green: 0.11, blue: 0.12)
|
||||
: Color.white
|
||||
}
|
||||
|
||||
private let thingsBlue = Color(red: 0.35, green: 0.6, blue: 0.95)
|
||||
private let thingsGray = Color(red: 0.55, green: 0.55, blue: 0.58)
|
||||
|
||||
private var textPrimary: Color {
|
||||
colorScheme == .dark ? .white : Color(red: 0.15, green: 0.15, blue: 0.17)
|
||||
}
|
||||
|
||||
private var textSecondary: Color {
|
||||
colorScheme == .dark ? Color(white: 0.5) : Color(white: 0.5)
|
||||
}
|
||||
|
||||
private var dividerColor: Color {
|
||||
colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.9)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
header
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 28)
|
||||
|
||||
// New Trip action
|
||||
newTripRow
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Divider()
|
||||
.background(dividerColor)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 16)
|
||||
|
||||
// Your trips section
|
||||
if !savedTrips.isEmpty {
|
||||
tripsSection
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
|
||||
// Suggestions section
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
suggestionsSection
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, savedTrips.isEmpty ? 0 : 28)
|
||||
}
|
||||
|
||||
Spacer(minLength: 60)
|
||||
}
|
||||
}
|
||||
.background(bgColor.ignoresSafeArea())
|
||||
}
|
||||
|
||||
// MARK: - Header
|
||||
|
||||
private var header: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Sports Time")
|
||||
.font(.system(size: 34, weight: .bold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text(headerSubtitle)
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var headerSubtitle: String {
|
||||
if savedTrips.isEmpty {
|
||||
return "Plan your first trip"
|
||||
} else {
|
||||
let upcoming = savedTrips.filter { ($0.trip?.startDate ?? Date()) > Date() }.count
|
||||
if upcoming > 0 {
|
||||
return "\(upcoming) upcoming trip\(upcoming == 1 ? "" : "s")"
|
||||
}
|
||||
return "\(savedTrips.count) trip\(savedTrips.count == 1 ? "" : "s") planned"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - New Trip Row
|
||||
|
||||
private var newTripRow: some View {
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack(spacing: 14) {
|
||||
// Checkbox circle
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(thingsBlue, lineWidth: 2)
|
||||
.frame(width: 22, height: 22)
|
||||
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(thingsBlue)
|
||||
}
|
||||
|
||||
Text("New Trip")
|
||||
.font(.system(size: 17))
|
||||
.foregroundStyle(thingsBlue)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Trips Section
|
||||
|
||||
private var tripsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("Your Trips")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
|
||||
Spacer()
|
||||
|
||||
if savedTrips.count > 3 {
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("See All")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(thingsBlue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: {
|
||||
tripRow(trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if index < min(2, savedTrips.count - 1) {
|
||||
Divider()
|
||||
.background(dividerColor)
|
||||
.padding(.leading, 36)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func tripRow(_ trip: Trip) -> some View {
|
||||
HStack(spacing: 14) {
|
||||
// Completion circle
|
||||
Circle()
|
||||
.stroke(thingsGray.opacity(0.5), lineWidth: 1.5)
|
||||
.frame(width: 22, height: 22)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 17))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if let sport = trip.uniqueSports.first {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.system(size: 11))
|
||||
Text(sport.displayName)
|
||||
.font(.system(size: 13))
|
||||
}
|
||||
.foregroundStyle(sport.themeColor)
|
||||
}
|
||||
|
||||
Text("•")
|
||||
.foregroundStyle(textSecondary)
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Count badge
|
||||
Text("\(trip.totalGames)")
|
||||
.font(.system(size: 13, weight: .medium))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Suggestions Section
|
||||
|
||||
private var suggestionsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("Suggestions")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task {
|
||||
await suggestedTripsGenerator.refreshTrips()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(thingsBlue)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(suggestedTripsGenerator.suggestedTrips.prefix(4).enumerated()), id: \.element.id) { index, suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
suggestionRow(suggestedTrip.trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if index < min(3, suggestedTripsGenerator.suggestedTrips.count - 1) {
|
||||
Divider()
|
||||
.background(dividerColor)
|
||||
.padding(.leading, 36)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func suggestionRow(_ trip: Trip) -> some View {
|
||||
HStack(spacing: 14) {
|
||||
// Light gray circle
|
||||
Circle()
|
||||
.fill(thingsGray.opacity(0.15))
|
||||
.frame(width: 22, height: 22)
|
||||
.overlay(
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: 10))
|
||||
.foregroundStyle(thingsGray)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 17))
|
||||
.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: 12, weight: .medium))
|
||||
.foregroundStyle(thingsGray.opacity(0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user