- Add ShareCardSportBackground with floating sport icons for share cards - Share cards now show sport-specific backgrounds (single or multiple sports) - Achievement collection share respects sport filter selection - Add ability to share individual achievements from detail sheet - Trip wizard ReviewStep highlights missing required fields in red - Add FieldValidation model to TripWizardViewModel Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
127 lines
3.5 KiB
Swift
127 lines
3.5 KiB
Swift
//
|
|
// TripCardGenerator.swift
|
|
// SportsTime
|
|
//
|
|
// Generates shareable trip summary cards with route map.
|
|
//
|
|
|
|
import SwiftUI
|
|
import UIKit
|
|
|
|
// MARK: - Trip Share Content
|
|
|
|
struct TripShareContent: ShareableContent {
|
|
let trip: Trip
|
|
|
|
var cardType: ShareCardType { .tripSummary }
|
|
|
|
@MainActor
|
|
func render(theme: ShareTheme) async throws -> UIImage {
|
|
let mapGenerator = ShareMapSnapshotGenerator()
|
|
let mapSnapshot = await mapGenerator.generateRouteMap(
|
|
stops: trip.stops,
|
|
theme: theme
|
|
)
|
|
|
|
let cardView = TripCardView(
|
|
trip: trip,
|
|
theme: theme,
|
|
mapSnapshot: mapSnapshot
|
|
)
|
|
|
|
let renderer = ImageRenderer(content: cardView)
|
|
renderer.scale = 3.0
|
|
|
|
guard let image = renderer.uiImage else {
|
|
throw ShareError.renderingFailed
|
|
}
|
|
|
|
return image
|
|
}
|
|
}
|
|
|
|
// MARK: - Trip Card View
|
|
|
|
private struct TripCardView: View {
|
|
let trip: Trip
|
|
let theme: ShareTheme
|
|
let mapSnapshot: UIImage?
|
|
|
|
private var sportTitle: String {
|
|
if trip.uniqueSports.count == 1, let sport = trip.uniqueSports.first {
|
|
return "My \(sport.displayName) Road Trip"
|
|
}
|
|
return "My Sports Road Trip"
|
|
}
|
|
|
|
private var primarySport: Sport? {
|
|
trip.uniqueSports.first
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ShareCardBackground(theme: theme, sports: trip.uniqueSports)
|
|
|
|
VStack(spacing: 40) {
|
|
ShareCardHeader(
|
|
title: sportTitle,
|
|
sport: primarySport,
|
|
theme: theme
|
|
)
|
|
|
|
// Map
|
|
if let snapshot = mapSnapshot {
|
|
Image(uiImage: snapshot)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(maxWidth: 960, maxHeight: 600)
|
|
.clipShape(RoundedRectangle(cornerRadius: 20))
|
|
.overlay {
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.stroke(theme.accentColor.opacity(0.3), lineWidth: 2)
|
|
}
|
|
}
|
|
|
|
// Date range
|
|
Text(trip.formattedDateRange)
|
|
.font(.system(size: 32, weight: .medium))
|
|
.foregroundStyle(theme.textColor)
|
|
|
|
// Stats row
|
|
ShareStatsRow(
|
|
stats: [
|
|
(value: String(format: "%.0f", trip.totalDistanceMiles), label: "miles"),
|
|
(value: "\(trip.totalGames)", label: "games"),
|
|
(value: "\(trip.cities.count)", label: "cities")
|
|
],
|
|
theme: theme
|
|
)
|
|
|
|
// City trail
|
|
cityTrail
|
|
|
|
Spacer()
|
|
|
|
ShareCardFooter(theme: theme)
|
|
}
|
|
.padding(ShareCardDimensions.padding)
|
|
}
|
|
.frame(
|
|
width: ShareCardDimensions.cardSize.width,
|
|
height: ShareCardDimensions.cardSize.height
|
|
)
|
|
}
|
|
|
|
private var cityTrail: some View {
|
|
let cities = trip.cities
|
|
let displayText = cities.joined(separator: " → ")
|
|
|
|
return Text(displayText)
|
|
.font(.system(size: 24, weight: .medium))
|
|
.foregroundStyle(theme.secondaryTextColor)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(3)
|
|
.padding(.horizontal, 40)
|
|
}
|
|
}
|