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,298 @@
|
||||
//
|
||||
// HomeContent_NikeRunClub.swift
|
||||
// SportsTime
|
||||
//
|
||||
// NIKE RUN CLUB-INSPIRED: Athletic, bold stats.
|
||||
// Dynamic feel, activity-focused design.
|
||||
// Black/white with vibrant accents.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HomeContent_NikeRunClub: 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]
|
||||
|
||||
// Nike-inspired colors
|
||||
private var bgColor: Color {
|
||||
colorScheme == .dark
|
||||
? Color.black
|
||||
: Color.white
|
||||
}
|
||||
|
||||
private let nikeVolt = Color(red: 0.77, green: 1.0, blue: 0.0)
|
||||
private let nikeOrange = Color(red: 1.0, green: 0.35, blue: 0.0)
|
||||
|
||||
private var textPrimary: Color {
|
||||
colorScheme == .dark ? .white : .black
|
||||
}
|
||||
|
||||
private var textSecondary: Color {
|
||||
colorScheme == .dark ? Color(white: 0.5) : Color(white: 0.45)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// Hero stats
|
||||
heroStats
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 16)
|
||||
|
||||
// Start activity button
|
||||
startButton
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// Activity feed
|
||||
if !savedTrips.isEmpty {
|
||||
activityFeed
|
||||
}
|
||||
|
||||
// Challenges/suggestions
|
||||
if !suggestedTripsGenerator.suggestedTrips.isEmpty {
|
||||
challengesSection
|
||||
}
|
||||
|
||||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
.background(bgColor.ignoresSafeArea())
|
||||
}
|
||||
|
||||
// MARK: - Hero Stats
|
||||
|
||||
private var heroStats: some View {
|
||||
VStack(spacing: 8) {
|
||||
Text("SPORTS TIME")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.tracking(2)
|
||||
|
||||
Text("\(savedTrips.count)")
|
||||
.font(.system(size: 72, weight: .bold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text("TRIPS PLANNED")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.tracking(1)
|
||||
|
||||
// Stats row
|
||||
HStack(spacing: 32) {
|
||||
statItem(value: "\(totalGames)", label: "GAMES")
|
||||
statItem(value: "\(totalStops)", label: "CITIES")
|
||||
statItem(value: "\(uniqueSports)", label: "SPORTS")
|
||||
}
|
||||
.padding(.top, 20)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 32)
|
||||
}
|
||||
|
||||
private func statItem(value: String, label: String) -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Text(value)
|
||||
.font(.system(size: 28, weight: .bold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text(label)
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.tracking(0.5)
|
||||
}
|
||||
}
|
||||
|
||||
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 uniqueSports: Int {
|
||||
Set(savedTrips.flatMap { $0.trip?.uniqueSports ?? [] }).count
|
||||
}
|
||||
|
||||
// MARK: - Start Button
|
||||
|
||||
private var startButton: some View {
|
||||
Button {
|
||||
showNewTrip = true
|
||||
} label: {
|
||||
HStack {
|
||||
Text("START")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.tracking(1)
|
||||
}
|
||||
.foregroundStyle(.black)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 18)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 30)
|
||||
.fill(nikeVolt)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
// MARK: - Activity Feed
|
||||
|
||||
private var activityFeed: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("ACTIVITY")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.tracking(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
selectedTab = 2
|
||||
} label: {
|
||||
Text("See All")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
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: {
|
||||
activityRow(trip, index: index)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func activityRow(_ trip: Trip, index: Int) -> some View {
|
||||
HStack(spacing: 16) {
|
||||
// Activity type indicator
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(index == 0 ? nikeVolt : nikeOrange)
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: trip.uniqueSports.first?.iconName ?? "sportscourt.fill")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(.black)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(trip.formattedDateRange)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text("\(trip.totalGames)")
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.foregroundStyle(textPrimary)
|
||||
|
||||
Text("games")
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(colorScheme == .dark ? Color(white: 0.08) : Color(white: 0.97))
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Challenges Section
|
||||
|
||||
private var challengesSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("SUGGESTED ROUTES")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(textSecondary)
|
||||
.tracking(1)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 14) {
|
||||
ForEach(suggestedTripsGenerator.suggestedTrips.prefix(4)) { suggestedTrip in
|
||||
Button {
|
||||
selectedSuggestedTrip = suggestedTrip
|
||||
} label: {
|
||||
challengeCard(suggestedTrip.trip)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func challengeCard(_ trip: Trip) -> some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// Challenge header
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [nikeOrange, nikeOrange.opacity(0.7)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(height: 100)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("CHALLENGE")
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
.foregroundStyle(.white.opacity(0.8))
|
||||
.tracking(1)
|
||||
|
||||
Text("\(trip.totalGames) GAMES")
|
||||
.font(.system(size: 22, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.padding(14)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(trip.name)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
Text("\(trip.stops.count) cities")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(textSecondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 180)
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(colorScheme == .dark ? Color(white: 0.1) : Color(white: 0.96))
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user