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