// // TripDesignSamples.swift // SportsTime // // 5 design explorations for trip summary cards. // All follow the unified design language: solid bg, no panels, app icon footer. // #if DEBUG import SwiftUI import UIKit // MARK: - Sample A: "Route Map Hero" // Large map dominates the card. Trip name at top. Stats + city trail below. struct TripSampleA: View { let trip: Trip let theme: ShareTheme let mapSnapshot: UIImage? private var sortedSports: [Sport] { trip.uniqueSports.sorted { $0.rawValue < $1.rawValue } } var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() VStack(spacing: 0) { // Header Text(sortedSports.map { $0.displayName.uppercased() }.joined(separator: " + ")) .font(.system(size: 20, weight: .black)) .tracking(8) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 20) Text("ROAD TRIP") .font(.system(size: 52, weight: .black)) .foregroundStyle(theme.textColor) .padding(.top, 8) Text(trip.formattedDateRange.uppercased()) .font(.system(size: 18, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 8) Spacer().frame(height: 30) // Large map if let mapSnapshot { Image(uiImage: mapSnapshot) .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 24)) .padding(.horizontal, 16) } Spacer().frame(height: 36) // Stats HStack(spacing: 50) { statItem(value: String(format: "%.0f", trip.totalDistanceMiles), label: "MILES") statItem(value: "\(trip.totalGames)", label: "GAMES") statItem(value: "\(trip.cities.count)", label: "CITIES") } Spacer().frame(height: 30) // City trail Text(trip.cities.joined(separator: " \u{2192} ").uppercased()) .font(.system(size: 18, weight: .bold)) .foregroundStyle(theme.secondaryTextColor) .multilineTextAlignment(.center) .lineLimit(2) .minimumScaleFactor(0.6) .padding(.horizontal, 40) Spacer() TripSampleAppFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height) } private func statItem(value: String, label: String) -> some View { VStack(spacing: 6) { Text(value) .font(.system(size: 44, weight: .black, design: .rounded)) .foregroundStyle(theme.accentColor) Text(label) .font(.system(size: 14, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) } } } // MARK: - Sample B: "City Trail" // City-to-city journey is the hero. Numbered stops with arrows. // Map smaller below. Stats compact at top. struct TripSampleB: View { let trip: Trip let theme: ShareTheme let mapSnapshot: UIImage? private var sortedSports: [Sport] { trip.uniqueSports.sorted { $0.rawValue < $1.rawValue } } var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() VStack(spacing: 0) { // Header Text("ROAD TRIP") .font(.system(size: 20, weight: .black)) .tracking(8) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 20) Text(trip.name.uppercased()) .font(.system(size: 44, weight: .black)) .foregroundStyle(theme.textColor) .multilineTextAlignment(.center) .lineLimit(2) .minimumScaleFactor(0.6) .padding(.horizontal, 40) .padding(.top, 8) Spacer().frame(height: 36) // City journey — vertical list VStack(spacing: 0) { ForEach(Array(trip.cities.enumerated()), id: \.offset) { index, city in HStack(spacing: 20) { // Number badge ZStack { Circle() .fill(theme.accentColor) .frame(width: 48, height: 48) Text("\(index + 1)") .font(.system(size: 22, weight: .black, design: .rounded)) .foregroundStyle(theme.gradientColors.first ?? .black) } Text(city.uppercased()) .font(.system(size: 32, weight: .black)) .foregroundStyle(theme.textColor) .lineLimit(1) .minimumScaleFactor(0.6) Spacer() } if index < trip.cities.count - 1 { // Connector line HStack(spacing: 20) { Rectangle() .fill(theme.textColor.opacity(0.15)) .frame(width: 2, height: 24) .padding(.leading, 23) Spacer() } } } } .padding(.horizontal, 40) Spacer().frame(height: 36) // Stats row HStack(spacing: 50) { statItem(value: String(format: "%.0f", trip.totalDistanceMiles), label: "MILES") statItem(value: "\(trip.totalGames)", label: "GAMES") statItem(value: "\(trip.tripDuration)", label: "DAYS") } Spacer().frame(height: 36) // Map — smaller if let mapSnapshot { Image(uiImage: mapSnapshot) .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 20)) .frame(height: 360) .padding(.horizontal, 20) } Spacer() TripSampleAppFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height) } private func statItem(value: String, label: String) -> some View { VStack(spacing: 6) { Text(value) .font(.system(size: 40, weight: .black, design: .rounded)) .foregroundStyle(theme.accentColor) Text(label) .font(.system(size: 14, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) } } } // MARK: - Sample C: "Big Miles" // Total miles is the massive hero number. Map below. Cities + games as secondary. struct TripSampleC: View { let trip: Trip let theme: ShareTheme let mapSnapshot: UIImage? private var sortedSports: [Sport] { trip.uniqueSports.sorted { $0.rawValue < $1.rawValue } } var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() // Ghost miles number Text(String(format: "%.0f", trip.totalDistanceMiles)) .font(.system(size: 260, weight: .black, design: .rounded)) .foregroundStyle(theme.textColor.opacity(0.04)) .lineLimit(1) .minimumScaleFactor(0.3) VStack(spacing: 0) { // Sport + trip type HStack(spacing: 14) { ForEach(sortedSports.sorted(by: { $0.rawValue < $1.rawValue }), id: \.self) { sport in Image(systemName: sport.iconName) .font(.system(size: 24, weight: .bold)) .foregroundStyle(theme.accentColor) } Text("ROAD TRIP") .font(.system(size: 18, weight: .black)) .tracking(6) .foregroundStyle(theme.secondaryTextColor) } .padding(.top, 20) Spacer() // Massive miles Text(String(format: "%.0f", trip.totalDistanceMiles)) .font(.system(size: 160, weight: .black, design: .rounded)) .foregroundStyle(theme.textColor) .minimumScaleFactor(0.5) Text("MILES DRIVEN") .font(.system(size: 22, weight: .black)) .tracking(6) .foregroundStyle(theme.secondaryTextColor) Spacer().frame(height: 24) // Secondary stats HStack(spacing: 50) { statItem(value: "\(trip.totalGames)", label: "GAMES") statItem(value: "\(trip.cities.count)", label: "CITIES") statItem(value: "\(trip.tripDuration)", label: "DAYS") } Spacer().frame(height: 36) // Map if let mapSnapshot { Image(uiImage: mapSnapshot) .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 24)) .padding(.horizontal, 16) } Spacer() TripSampleAppFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height) } private func statItem(value: String, label: String) -> some View { VStack(spacing: 6) { Text(value) .font(.system(size: 44, weight: .black, design: .rounded)) .foregroundStyle(theme.accentColor) Text(label) .font(.system(size: 14, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) } } } // MARK: - Sample D: "Scoreboard" // Trip score front and center with letter grade. Map + stats below. // Feels like a game recap card. struct TripSampleD: View { let trip: Trip let theme: ShareTheme let mapSnapshot: UIImage? private var sortedSports: [Sport] { trip.uniqueSports.sorted { $0.rawValue < $1.rawValue } } private var scoreGrade: String { trip.score?.scoreGrade ?? "A" } private var scoreValue: String { trip.score?.formattedOverallScore ?? "85" } var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() VStack(spacing: 0) { // Header Text(trip.name.uppercased()) .font(.system(size: 20, weight: .black)) .tracking(6) .foregroundStyle(theme.secondaryTextColor) .padding(.top, 20) Text("TRIP SCORE") .font(.system(size: 44, weight: .black)) .foregroundStyle(theme.textColor) .padding(.top, 8) Spacer() // Score circle ZStack { Circle() .stroke(theme.textColor.opacity(0.1), lineWidth: 20) .frame(width: 320, height: 320) Circle() .trim(from: 0, to: (Double(scoreValue) ?? 85) / 100) .stroke(theme.accentColor, style: StrokeStyle(lineWidth: 20, lineCap: .round)) .frame(width: 320, height: 320) .rotationEffect(.degrees(-90)) VStack(spacing: 4) { Text(scoreGrade) .font(.system(size: 100, weight: .black)) .foregroundStyle(theme.accentColor) Text(scoreValue + " / 100") .font(.system(size: 22, weight: .bold)) .foregroundStyle(theme.secondaryTextColor) } } Spacer().frame(height: 30) // Date range Text(trip.formattedDateRange.uppercased()) .font(.system(size: 18, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) Spacer().frame(height: 30) // Stats HStack(spacing: 40) { statItem(value: String(format: "%.0f", trip.totalDistanceMiles), label: "MILES") statItem(value: "\(trip.totalGames)", label: "GAMES") statItem(value: "\(trip.cities.count)", label: "CITIES") statItem(value: "\(trip.tripDuration)", label: "DAYS") } Spacer().frame(height: 30) // Map — compact if let mapSnapshot { Image(uiImage: mapSnapshot) .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 20)) .frame(height: 340) .padding(.horizontal, 20) } Spacer() TripSampleAppFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height) } private func statItem(value: String, label: String) -> some View { VStack(spacing: 6) { Text(value) .font(.system(size: 36, weight: .black, design: .rounded)) .foregroundStyle(theme.accentColor) .minimumScaleFactor(0.6) Text(label) .font(.system(size: 12, weight: .bold)) .tracking(2) .foregroundStyle(theme.secondaryTextColor) } } } // MARK: - Sample E: "Postcard" // Map takes most of the card like a postcard photo. Trip info overlaid at bottom. // Minimal text, maximum visual impact. struct TripSampleE: View { let trip: Trip let theme: ShareTheme let mapSnapshot: UIImage? private var sortedSports: [Sport] { trip.uniqueSports.sorted { $0.rawValue < $1.rawValue } } var body: some View { ZStack { (theme.gradientColors.first ?? .black) .ignoresSafeArea() VStack(spacing: 0) { // Tiny header HStack(spacing: 10) { ForEach(sortedSports.sorted(by: { $0.rawValue < $1.rawValue }), id: \.self) { sport in Image(systemName: sport.iconName) .font(.system(size: 20, weight: .bold)) .foregroundStyle(theme.accentColor) } Text("ROAD TRIP") .font(.system(size: 16, weight: .black)) .tracking(4) .foregroundStyle(theme.secondaryTextColor) Spacer() Text(trip.formattedDateRange.uppercased()) .font(.system(size: 14, weight: .bold)) .foregroundStyle(theme.secondaryTextColor) } .padding(.top, 16) Spacer().frame(height: 20) // Giant map if let mapSnapshot { Image(uiImage: mapSnapshot) .resizable() .aspectRatio(contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 24)) .padding(.horizontal, 8) } Spacer().frame(height: 30) // Trip name big Text(trip.cities.joined(separator: " \u{2192} ").uppercased()) .font(.system(size: 36, weight: .black)) .foregroundStyle(theme.textColor) .multilineTextAlignment(.center) .lineLimit(3) .minimumScaleFactor(0.5) .padding(.horizontal, 20) Spacer().frame(height: 24) // Stats inline HStack(spacing: 30) { inlineStat(value: String(format: "%.0f", trip.totalDistanceMiles), label: "mi") Text("\u{2022}") .foregroundStyle(theme.textColor.opacity(0.2)) inlineStat(value: "\(trip.totalGames)", label: "games") Text("\u{2022}") .foregroundStyle(theme.textColor.opacity(0.2)) inlineStat(value: "\(trip.tripDuration)", label: "days") Text("\u{2022}") .foregroundStyle(theme.textColor.opacity(0.2)) inlineStat(value: "\(trip.cities.count)", label: "cities") } Spacer() TripSampleAppFooter(theme: theme) } .padding(ShareCardDimensions.padding) } .frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height) } private func inlineStat(value: String, label: String) -> some View { HStack(alignment: .firstTextBaseline, spacing: 4) { Text(value) .font(.system(size: 28, weight: .black, design: .rounded)) .foregroundStyle(theme.accentColor) Text(label) .font(.system(size: 16, weight: .bold)) .foregroundStyle(theme.secondaryTextColor) } } } // MARK: - Shared Footer private struct TripSampleAppFooter: View { let theme: ShareTheme var body: some View { VStack(spacing: 8) { if let icon = Self.loadAppIcon() { Image(uiImage: icon) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 56, height: 56) .clipShape(RoundedRectangle(cornerRadius: 13)) } Text("SportsTime") .font(.system(size: 18, weight: .bold)) .foregroundStyle(theme.textColor.opacity(0.5)) } .padding(.bottom, 10) } private static func loadAppIcon() -> UIImage? { if let icons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any], let primary = icons["CFBundlePrimaryIcon"] as? [String: Any], let files = primary["CFBundleIconFiles"] as? [String], let name = files.last { return UIImage(named: name) } return nil } } #endif