feat: redesign all share cards, remove unused achievement types, fix sport selector
Redesign trip, progress, and achievement share cards with premium sports-media aesthetic. Remove unused milestone/context achievement card types (only used in debug exporter). Fix gold text unreadable in light mode. Fix sport selector to only show stroke on selected sport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
// ProgressCardGenerator.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Generates shareable stadium progress cards.
|
||||
// Shareable progress cards — unified design language.
|
||||
// Solid color bg, progress ring with fraction, plain white text, app icon footer.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
@@ -25,20 +26,17 @@ struct ProgressShareContent: ShareableContent {
|
||||
theme: theme
|
||||
)
|
||||
|
||||
let cardView = ProgressCardView(
|
||||
let renderer = ImageRenderer(content: ProgressCardView(
|
||||
progress: progress,
|
||||
tripCount: tripCount,
|
||||
theme: theme,
|
||||
mapSnapshot: mapSnapshot
|
||||
)
|
||||
|
||||
let renderer = ImageRenderer(content: cardView)
|
||||
))
|
||||
renderer.scale = 3.0
|
||||
|
||||
guard let image = renderer.uiImage else {
|
||||
throw ShareError.renderingFailed
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
}
|
||||
@@ -51,62 +49,146 @@ private struct ProgressCardView: View {
|
||||
let theme: ShareTheme
|
||||
let mapSnapshot: UIImage?
|
||||
|
||||
private let gold = Color(hex: "FFD700")
|
||||
|
||||
private var isComplete: Bool {
|
||||
progress.completionPercentage >= 100
|
||||
}
|
||||
|
||||
private var accent: Color {
|
||||
isComplete ? gold : theme.accentColor
|
||||
}
|
||||
|
||||
private var remaining: Int {
|
||||
max(0, progress.totalStadiums - progress.visitedStadiums)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ShareCardBackground(theme: theme, sports: [progress.sport])
|
||||
(theme.gradientColors.first ?? .black)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 40) {
|
||||
ShareCardHeader(
|
||||
title: "\(progress.sport.displayName) Stadium Quest",
|
||||
sport: progress.sport,
|
||||
theme: theme
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Progress ring
|
||||
ShareProgressRing(
|
||||
current: progress.visitedStadiums,
|
||||
total: progress.totalStadiums,
|
||||
theme: theme
|
||||
)
|
||||
|
||||
Text("\(Int(progress.completionPercentage))% Complete")
|
||||
.font(.system(size: 28, weight: .medium))
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
Text(progress.sport.displayName.uppercased())
|
||||
.font(.system(size: 20, weight: .black))
|
||||
.tracking(8)
|
||||
.foregroundStyle(theme.secondaryTextColor)
|
||||
.padding(.top, 20)
|
||||
|
||||
// Stats row
|
||||
ShareStatsRow(
|
||||
stats: [
|
||||
(value: "\(progress.visitedStadiums)", label: "visited"),
|
||||
(value: "\(progress.totalStadiums - progress.visitedStadiums)", label: "remain"),
|
||||
(value: "\(tripCount)", label: "trips")
|
||||
],
|
||||
theme: theme
|
||||
)
|
||||
Text("STADIUM QUEST")
|
||||
.font(.system(size: 44, weight: .black))
|
||||
.foregroundStyle(theme.textColor)
|
||||
.padding(.top, 8)
|
||||
|
||||
// Map
|
||||
if let snapshot = mapSnapshot {
|
||||
Image(uiImage: snapshot)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 960)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(theme.accentColor.opacity(0.3), lineWidth: 2)
|
||||
}
|
||||
if isComplete {
|
||||
Text("COMPLETE")
|
||||
.font(.system(size: 18, weight: .black))
|
||||
.tracking(4)
|
||||
.foregroundStyle(gold)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ShareCardFooter(theme: theme)
|
||||
// Progress ring with fraction inside
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(theme.textColor.opacity(0.1), lineWidth: 24)
|
||||
.frame(width: 400, height: 400)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: progress.completionPercentage / 100)
|
||||
.stroke(accent, style: StrokeStyle(lineWidth: 24, lineCap: .round))
|
||||
.frame(width: 400, height: 400)
|
||||
.rotationEffect(.degrees(-90))
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("\(progress.visitedStadiums)")
|
||||
.font(.system(size: 120, weight: .black, design: .rounded))
|
||||
.foregroundStyle(accent)
|
||||
|
||||
Rectangle()
|
||||
.fill(theme.textColor.opacity(0.2))
|
||||
.frame(width: 140, height: 3)
|
||||
|
||||
Text("\(progress.totalStadiums)")
|
||||
.font(.system(size: 60, weight: .black, design: .rounded))
|
||||
.foregroundStyle(theme.textColor.opacity(0.4))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
|
||||
// Stats
|
||||
HStack(spacing: 50) {
|
||||
statItem(value: "\(progress.visitedStadiums)", label: "VISITED")
|
||||
statItem(value: "\(remaining)", label: "TO GO")
|
||||
statItem(value: "\(tripCount)", label: "TRIPS")
|
||||
}
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
|
||||
// Map
|
||||
if let mapSnapshot {
|
||||
Image(uiImage: mapSnapshot)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24))
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ProgressCardAppFooter(theme: theme)
|
||||
}
|
||||
.padding(ShareCardDimensions.padding)
|
||||
}
|
||||
.frame(
|
||||
width: ShareCardDimensions.cardSize.width,
|
||||
height: ShareCardDimensions.cardSize.height
|
||||
)
|
||||
.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(accent)
|
||||
Text(label)
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.tracking(2)
|
||||
.foregroundStyle(theme.secondaryTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Footer
|
||||
|
||||
private struct ProgressCardAppFooter: 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user