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,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)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user