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:
Trey t
2026-02-09 14:55:53 -06:00
parent 1a7ce78ae4
commit 244ea5e107
16 changed files with 3441 additions and 748 deletions

View File

@@ -83,7 +83,7 @@ struct AchievementsListView: View {
let earned = displayAchievements.filter { $0.isEarned }.count
let total = displayAchievements.count
let progress = total > 0 ? Double(earned) / Double(total) : 0
let completedGold = Color(hex: "FFD700")
let completedGold = colorScheme == .dark ? Color(hex: "FFD700") : Color(hex: "B8860B")
let filterTitle = selectedSport?.displayName ?? "All Sports"
let accentColor = selectedSport?.themeColor ?? Theme.warmOrange
@@ -297,8 +297,10 @@ struct AchievementCard: View {
@Environment(\.colorScheme) private var colorScheme
// Gold color for completed achievements
private let completedGold = Color(hex: "FFD700")
// Gold that's readable in both light and dark mode
private var completedGold: Color {
colorScheme == .dark ? Color(hex: "FFD700") : Color(hex: "B8860B")
}
var body: some View {
VStack(spacing: Theme.Spacing.sm) {
@@ -460,8 +462,10 @@ struct AchievementDetailSheet: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.dismiss) private var dismiss
// Gold color for completed achievements
private let completedGold = Color(hex: "FFD700")
// Gold that's readable in both light and dark mode
private var completedGold: Color {
colorScheme == .dark ? Color(hex: "FFD700") : Color(hex: "B8860B")
}
var body: some View {
NavigationStack {

View File

@@ -34,8 +34,8 @@ final class DebugShareExporter {
exportedCount = 0
let achievementCount = AchievementRegistry.all.count
// spotlight + milestone + context = 3 * achievements, collection ~5, progress 12, trips 4, icons 1
totalCount = (achievementCount * 3) + 5 + 12 + 4 + 1
// spotlight = 1 * achievements, collection ~5, progress 12, trips 4, icons 1
totalCount = achievementCount + 5 + 12 + 4 + 1
do {
// Step 1: Create export directory
@@ -51,13 +51,10 @@ final class DebugShareExporter {
let engine = AchievementEngine(modelContext: modelContext)
_ = try await engine.recalculateAllAchievements()
// Step 4: Export achievement spotlight + milestone cards
// Step 4: Export achievement spotlight cards
currentStep = "Exporting spotlight cards..."
let spotlightTheme = ShareThemePreferences.theme(for: .achievementSpotlight)
let milestoneTheme = ShareThemePreferences.theme(for: .achievementMilestone)
let spotlightDir = exportDir.appendingPathComponent("achievements/spotlight")
let milestoneDir = exportDir.appendingPathComponent("achievements/milestone")
for definition in AchievementRegistry.all {
let achievement = AchievementProgress(
@@ -68,21 +65,12 @@ final class DebugShareExporter {
earnedAt: Date()
)
// Spotlight
currentStep = "Spotlight: \(definition.name)"
let spotlightContent = AchievementSpotlightContent(achievement: achievement)
let spotlightImage = try await spotlightContent.render(theme: spotlightTheme)
try savePNG(spotlightImage, to: spotlightDir.appendingPathComponent("\(definition.id).png"))
exportedCount += 1
updateProgress()
// Milestone
currentStep = "Milestone: \(definition.name)"
let milestoneContent = AchievementMilestoneContent(achievement: achievement)
let milestoneImage = try await milestoneContent.render(theme: milestoneTheme)
try savePNG(milestoneImage, to: milestoneDir.appendingPathComponent("\(definition.id).png"))
exportedCount += 1
updateProgress()
}
// Step 5: Export achievement collection cards
@@ -139,36 +127,7 @@ final class DebugShareExporter {
exportedCount += 1
updateProgress()
// Step 6: Export achievement context cards
currentStep = "Generating map snapshot for context cards..."
let contextTheme = ShareThemePreferences.theme(for: .achievementContext)
let contextDir = exportDir.appendingPathComponent("achievements/context")
let contextStops = Self.eastCoastStops()
let mapGenerator = ShareMapSnapshotGenerator()
let mapSnapshot = await mapGenerator.generateRouteMap(stops: contextStops, theme: contextTheme)
for definition in AchievementRegistry.all {
currentStep = "Context: \(definition.name)"
let achievement = AchievementProgress(
definition: definition,
currentProgress: totalRequired(for: definition),
totalRequired: totalRequired(for: definition),
hasStoredAchievement: true,
earnedAt: Date()
)
let contextContent = AchievementContextContent(
achievement: achievement,
tripName: "Road Trip 2026",
mapSnapshot: mapSnapshot
)
let image = try await contextContent.render(theme: contextTheme)
try savePNG(image, to: contextDir.appendingPathComponent("\(definition.id).png"))
exportedCount += 1
updateProgress()
}
// Step 7: Export progress cards
// Step 6: Export progress cards
currentStep = "Exporting progress cards..."
let progressTheme = ShareThemePreferences.theme(for: .stadiumProgress)
let progressDir = exportDir.appendingPathComponent("progress")
@@ -204,7 +163,7 @@ final class DebugShareExporter {
}
}
// Step 8: Export trip cards
// Step 7: Export trip cards
currentStep = "Exporting trip cards..."
let tripTheme = ShareThemePreferences.theme(for: .tripSummary)
let tripDir = exportDir.appendingPathComponent("trips")
@@ -220,7 +179,7 @@ final class DebugShareExporter {
updateProgress()
}
// Step 9: Export sports icon
// Step 8: Export sports icon
currentStep = "Exporting sports icon..."
let iconDir = exportDir.appendingPathComponent("icons")
if let iconData = SportsIconImageGenerator.generatePNGData(),
@@ -244,6 +203,184 @@ final class DebugShareExporter {
isExporting = false
}
// MARK: - Export Achievement Samples
func exportAchievementSamples() async {
guard !isExporting else { return }
isExporting = true
error = nil
exportPath = nil
exportedCount = 0
do {
currentStep = "Creating export directory..."
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd_HHmmss"
let timestamp = formatter.string(from: Date())
let exportDir = docs.appendingPathComponent("DebugExport/samples_\(timestamp)")
try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true)
// Pick a few representative achievements across sports
let defs = AchievementRegistry.all
let sampleDefs = [
defs.first { $0.sport == .mlb } ?? defs[0],
defs.first { $0.sport == .nba } ?? defs[1],
defs.first { $0.sport == .nhl } ?? defs[2],
defs.first { $0.name.lowercased().contains("complete") } ?? defs[3],
defs.first { $0.category == .journey } ?? defs[min(4, defs.count - 1)]
]
totalCount = sampleDefs.count
let spotlightTheme = ShareThemePreferences.theme(for: .achievementSpotlight)
for def in sampleDefs {
let achievement = AchievementProgress(
definition: def,
currentProgress: totalRequired(for: def),
totalRequired: totalRequired(for: def),
hasStoredAchievement: true,
earnedAt: Date()
)
let safeName = def.id.replacingOccurrences(of: " ", with: "_")
currentStep = "Spotlight: \(def.name)"
let spotlightContent = AchievementSpotlightContent(achievement: achievement)
let spotlightImage = try await spotlightContent.render(theme: spotlightTheme)
try savePNG(spotlightImage, to: exportDir.appendingPathComponent("spotlight_\(safeName).png"))
exportedCount += 1
updateProgress()
}
exportPath = exportDir.path
currentStep = "Export complete!"
print("DEBUG SAMPLES: \(exportDir.path)")
} catch {
self.error = error.localizedDescription
currentStep = "Export failed"
print("DEBUG SAMPLE ERROR: \(error)")
}
isExporting = false
}
// MARK: - Export Progress Samples
func exportProgressSamples() async {
guard !isExporting else { return }
isExporting = true
error = nil
exportPath = nil
exportedCount = 0
do {
currentStep = "Creating export directory..."
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd_HHmmss"
let timestamp = formatter.string(from: Date())
let exportDir = docs.appendingPathComponent("DebugExport/progress_samples_\(timestamp)")
try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true)
let allStadiums = AppDataProvider.shared.stadiums
let theme = ShareThemePreferences.theme(for: .stadiumProgress)
// Render real progress cards at different percentages per sport
let sports: [Sport] = [.mlb, .nba, .nhl]
let percentages = [25, 50, 75, 100]
totalCount = sports.count * percentages.count
for sport in sports {
let sportStadiums = allStadiums.filter { $0.sport == sport }
let total = sportStadiums.count
for pct in percentages {
let visitedCount = (total * pct) / 100
let visited = Array(sportStadiums.prefix(visitedCount))
let remaining = Array(sportStadiums.dropFirst(visitedCount))
let leagueProgress = LeagueProgress(
sport: sport,
totalStadiums: total,
visitedStadiums: visitedCount,
stadiumsVisited: visited,
stadiumsRemaining: remaining
)
let tripCount = pct == 100 ? 5 : pct / 25
currentStep = "Progress: \(sport.rawValue) \(pct)%"
let content = ProgressShareContent(
progress: leagueProgress,
tripCount: tripCount
)
let image = try await content.render(theme: theme)
try savePNG(image, to: exportDir.appendingPathComponent("\(sport.rawValue)_\(pct).png"))
exportedCount += 1
updateProgress()
}
}
exportPath = exportDir.path
currentStep = "Export complete!"
print("DEBUG PROGRESS SAMPLES: \(exportDir.path)")
} catch {
self.error = error.localizedDescription
currentStep = "Export failed"
print("DEBUG PROGRESS SAMPLE ERROR: \(error)")
}
isExporting = false
}
// MARK: - Export Trip Samples
func exportTripSamples() async {
guard !isExporting else { return }
isExporting = true
error = nil
exportPath = nil
exportedCount = 0
do {
currentStep = "Creating export directory..."
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd_HHmmss"
let timestamp = formatter.string(from: Date())
let exportDir = docs.appendingPathComponent("DebugExport/trip_samples_\(timestamp)")
try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true)
let theme = ShareThemePreferences.theme(for: .tripSummary)
let dummyTrips = Self.buildDummyTrips()
totalCount = dummyTrips.count
for trip in dummyTrips {
currentStep = "Trip: \(trip.name)"
let content = TripShareContent(trip: trip)
let image = try await content.render(theme: theme)
let safeName = trip.name.replacingOccurrences(of: " ", with: "_")
try savePNG(image, to: exportDir.appendingPathComponent("\(safeName).png"))
exportedCount += 1
updateProgress()
}
exportPath = exportDir.path
currentStep = "Export complete!"
print("DEBUG TRIP SAMPLES: \(exportDir.path)")
} catch {
self.error = error.localizedDescription
currentStep = "Export failed"
print("DEBUG TRIP SAMPLE ERROR: \(error)")
}
isExporting = false
}
// MARK: - Add All Stadium Visits
func addAllStadiumVisits(modelContext: ModelContext) async {
@@ -311,9 +448,7 @@ final class DebugShareExporter {
let subdirs = [
"achievements/spotlight",
"achievements/milestone",
"achievements/collection",
"achievements/context",
"progress",
"trips",
"icons"

View File

@@ -360,6 +360,33 @@ struct SettingsView: View {
Label("Export All Shareables", systemImage: "square.and.arrow.up.on.square")
}
Button {
showExportProgress = true
Task {
await exporter.exportAchievementSamples()
}
} label: {
Label("Export Achievement Samples", systemImage: "paintbrush")
}
Button {
showExportProgress = true
Task {
await exporter.exportProgressSamples()
}
} label: {
Label("Export Progress Samples", systemImage: "chart.bar.fill")
}
Button {
showExportProgress = true
Task {
await exporter.exportTripSamples()
}
} label: {
Label("Export Trip Samples", systemImage: "car.fill")
}
Button {
Task { await exporter.addAllStadiumVisits(modelContext: modelContext) }
} label: {