Files
Sportstime/SportsTime/Export/Sharing/TripCardGenerator.swift
Trey t fe36f99bca feat(sharing): implement unified sharing system for social media
Replace old ProgressCardGenerator with protocol-based sharing architecture
supporting trips, achievements, and stadium progress. Features 8 color
themes, Instagram Stories optimization (1080x1920), and reusable card
components with map snapshots.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 08:54:37 -06:00

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)
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)
}
}