Files
Sportstime/SportsTime/Export/Sharing/AchievementDesignSamples.swift
Trey t 244ea5e107 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>
2026-02-09 14:55:53 -06:00

1204 lines
47 KiB
Swift

//
// AchievementDesignSamples.swift
// SportsTime
//
// 5 design explorations for achievement cards. Render all via Debug > Export Samples.
// Pick the best parts from each to create the final design.
//
#if DEBUG
import SwiftUI
import UIKit
// MARK: - Sample Renderer
@MainActor
struct AchievementSampleRenderer {
static func renderAll(
achievement: AchievementProgress,
milestoneAchievement: AchievementProgress,
theme: ShareTheme
) async throws -> [(label: String, image: UIImage)] {
var results: [(String, UIImage)] = []
let spotlightViews: [(String, AnyView)] = [
("A_Broadcast_Spotlight", AnyView(SampleA_Spotlight(achievement: achievement, theme: theme))),
("B_Editorial_Spotlight", AnyView(SampleB_Spotlight(achievement: achievement, theme: theme))),
("C_Trophy_Spotlight", AnyView(SampleC_Spotlight(achievement: achievement, theme: theme))),
("D_Poster_Spotlight", AnyView(SampleD_Spotlight(achievement: achievement, theme: theme))),
("E_Ticket_Spotlight", AnyView(SampleE_Spotlight(achievement: achievement, theme: theme))),
]
let milestoneViews: [(String, AnyView)] = [
("A_Broadcast_Milestone", AnyView(SampleA_Milestone(achievement: milestoneAchievement, theme: theme))),
("B_Editorial_Milestone", AnyView(SampleB_Milestone(achievement: milestoneAchievement, theme: theme))),
("C_Trophy_Milestone", AnyView(SampleC_Milestone(achievement: milestoneAchievement, theme: theme))),
("D_Poster_Milestone", AnyView(SampleD_Milestone(achievement: milestoneAchievement, theme: theme))),
("E_Ticket_Milestone", AnyView(SampleE_Milestone(achievement: milestoneAchievement, theme: theme))),
]
for (label, view) in spotlightViews + milestoneViews {
let renderer = ImageRenderer(content: view)
renderer.scale = 3.0
if let image = renderer.uiImage {
results.append((label, image))
}
}
return results
}
}
//
// MARK: - SAMPLE A: "Broadcast"
// ESPN-style scoreboard. Colored top banner, badge in a dark panel with
// subtle grid behind it, bold condensed name, stat-block earned date.
//
private struct SampleA_Spotlight: View {
let achievement: AchievementProgress
let theme: ShareTheme
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
VStack(spacing: 0) {
// Top banner bar
HStack {
if let sport = achievement.definition.sport {
Image(systemName: sport.iconName)
.font(.system(size: 28, weight: .bold))
}
Text("ACHIEVEMENT UNLOCKED")
.font(.system(size: 22, weight: .black))
.tracking(3)
Spacer()
}
.foregroundStyle(.white)
.padding(.horizontal, 28)
.padding(.vertical, 20)
.background(theme.accentColor)
Spacer()
// Badge in a dark panel with grid
ZStack {
// Subtle grid
GridPatternA()
.stroke(theme.textColor.opacity(0.06), lineWidth: 1)
SampleBadgeCircle(
definition: achievement.definition,
size: 360
)
}
.frame(height: 500)
.padding(.horizontal, 40)
.background(
RoundedRectangle(cornerRadius: 24)
.fill(.black.opacity(0.3))
)
.padding(.horizontal, 40)
Spacer().frame(height: 40)
// Name bold condensed
Text(achievement.definition.name.uppercased())
.font(.system(size: 58, weight: .black))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.6)
.padding(.horizontal, 60)
Spacer().frame(height: 16)
// Description
Text(achievement.definition.description)
.font(.system(size: 24, weight: .medium))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
.padding(.horizontal, 80)
Spacer().frame(height: 30)
// Stat-block earned date
if let date = achievement.earnedAt {
HStack(spacing: 40) {
VStack(spacing: 4) {
Text(date.formatted(.dateTime.month(.abbreviated)).uppercased())
.font(.system(size: 36, weight: .black, design: .rounded))
.foregroundStyle(theme.accentColor)
Text("MONTH")
.font(.system(size: 14, weight: .bold))
.tracking(2)
.foregroundStyle(theme.secondaryTextColor)
}
Rectangle()
.fill(theme.borderColor)
.frame(width: 1, height: 50)
VStack(spacing: 4) {
Text(date.formatted(.dateTime.year()))
.font(.system(size: 36, weight: .black, design: .rounded))
.foregroundStyle(theme.accentColor)
Text("YEAR")
.font(.system(size: 14, weight: .bold))
.tracking(2)
.foregroundStyle(theme.secondaryTextColor)
}
}
.padding(.vertical, 20)
.padding(.horizontal, 40)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(theme.surfaceColor)
.overlay(RoundedRectangle(cornerRadius: 16).stroke(theme.borderColor, lineWidth: 1))
)
}
Spacer()
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
private struct SampleA_Milestone: View {
let achievement: AchievementProgress
let theme: ShareTheme
private let gold = Color(hex: "FFD700")
private let goldDark = Color(hex: "B8860B")
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
VStack(spacing: 0) {
// Gold milestone banner
HStack {
Image(systemName: "trophy.fill")
.font(.system(size: 24, weight: .bold))
Text("MILESTONE ACHIEVEMENT")
.font(.system(size: 20, weight: .black))
.tracking(3)
Spacer()
}
.foregroundStyle(.black)
.padding(.horizontal, 28)
.padding(.vertical, 18)
.background(
LinearGradient(colors: [gold, goldDark], startPoint: .leading, endPoint: .trailing)
)
Spacer()
// Double gold ring around badge
ZStack {
Circle()
.stroke(
LinearGradient(colors: [gold, goldDark, gold], startPoint: .topLeading, endPoint: .bottomTrailing),
lineWidth: 8
)
.frame(width: 440, height: 440)
Circle()
.stroke(gold.opacity(0.3), lineWidth: 2)
.frame(width: 460, height: 460)
SampleBadgeCircle(definition: achievement.definition, size: 400)
}
Spacer().frame(height: 40)
Text(achievement.definition.name.uppercased())
.font(.system(size: 52, weight: .black))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.6)
.padding(.horizontal, 60)
.padding(.vertical, 16)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(gold.opacity(0.1))
)
Spacer().frame(height: 16)
Text(achievement.definition.description)
.font(.system(size: 24, weight: .medium))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
.padding(.horizontal, 80)
Spacer()
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
//
// MARK: - SAMPLE B: "Editorial"
// Magazine-style. Asymmetric, badge on left, name on right in large heavy
// type. Thin hairline rules. Generous whitespace. Refined and quiet.
//
private struct SampleB_Spotlight: View {
let achievement: AchievementProgress
let theme: ShareTheme
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
VStack(spacing: 0) {
// Minimal top label
HStack {
Text("ACHIEVEMENT")
.font(.system(size: 14, weight: .bold))
.tracking(6)
.foregroundStyle(theme.secondaryTextColor)
Spacer()
if let date = achievement.earnedAt {
Text(date.formatted(date: .abbreviated, time: .omitted).uppercased())
.font(.system(size: 14, weight: .bold))
.tracking(2)
.foregroundStyle(theme.secondaryTextColor)
}
}
.padding(.bottom, 16)
// Hairline
Rectangle().fill(theme.borderColor).frame(height: 1)
Spacer().frame(height: 60)
// Asymmetric: badge left, text right
HStack(alignment: .center, spacing: 40) {
SampleBadgeMinimal(
definition: achievement.definition,
size: 280
)
VStack(alignment: .leading, spacing: 20) {
Text(achievement.definition.name)
.font(.system(size: 52, weight: .heavy))
.foregroundStyle(theme.textColor)
.lineLimit(4)
.fixedSize(horizontal: false, vertical: true)
Rectangle()
.fill(theme.accentColor)
.frame(width: 60, height: 3)
Text(achievement.definition.description)
.font(.system(size: 22, weight: .light))
.foregroundStyle(theme.secondaryTextColor)
.lineLimit(4)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
// Bottom: thin rule + category label
Rectangle().fill(theme.borderColor).frame(height: 1)
HStack {
Text(achievement.definition.category.displayName.uppercased())
.font(.system(size: 14, weight: .bold))
.tracking(4)
.foregroundStyle(theme.secondaryTextColor)
Spacer()
if let sport = achievement.definition.sport {
Text(sport.rawValue)
.font(.system(size: 14, weight: .bold))
.tracking(4)
.foregroundStyle(theme.accentColor)
}
}
.padding(.top, 16)
.padding(.bottom, 40)
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
private struct SampleB_Milestone: View {
let achievement: AchievementProgress
let theme: ShareTheme
private let gold = Color(hex: "FFD700")
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
VStack(spacing: 0) {
HStack {
Text("MILESTONE")
.font(.system(size: 14, weight: .bold))
.tracking(6)
.foregroundStyle(gold)
Spacer()
}
.padding(.bottom, 16)
Rectangle().fill(gold.opacity(0.4)).frame(height: 1)
Spacer()
// Centered badge, larger
SampleBadgeMinimal(definition: achievement.definition, size: 400)
.overlay {
Circle()
.stroke(gold.opacity(0.5), lineWidth: 2)
.frame(width: 420, height: 420)
}
Spacer().frame(height: 50)
Text(achievement.definition.name)
.font(.system(size: 56, weight: .heavy))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.65)
.padding(.horizontal, 40)
Rectangle()
.fill(gold)
.frame(width: 80, height: 3)
.padding(.vertical, 20)
Text(achievement.definition.description)
.font(.system(size: 22, weight: .light))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
.padding(.horizontal, 80)
Spacer()
Rectangle().fill(gold.opacity(0.4)).frame(height: 1)
.padding(.bottom, 16)
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
//
// MARK: - SAMPLE C: "Trophy Case"
// Museum display. Badge on a shelf with spotlight from above. Name on a
// plaque below. Feels like looking into a glass trophy cabinet.
//
private struct SampleC_Spotlight: View {
let achievement: AchievementProgress
let theme: ShareTheme
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
// Spotlight cone from top
RadialGradient(
colors: [theme.accentColor.opacity(0.15), .clear],
center: UnitPoint(x: 0.5, y: 0.25),
startRadius: 10,
endRadius: 500
)
VStack(spacing: 0) {
Spacer().frame(height: 80)
// Top label
Text("UNLOCKED")
.font(.system(size: 18, weight: .black))
.tracking(8)
.foregroundStyle(theme.accentColor)
Spacer()
// Badge on a "shelf"
VStack(spacing: 0) {
SampleBadgeAppIcon(
definition: achievement.definition,
size: 340
)
.shadow(color: .black.opacity(0.4), radius: 20, y: 15)
// Shelf line
Rectangle()
.fill(
LinearGradient(
colors: [.clear, theme.textColor.opacity(0.3), .clear],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: 500, height: 2)
.padding(.top, 20)
// Shadow under shelf
Ellipse()
.fill(.black.opacity(0.2))
.frame(width: 300, height: 20)
.blur(radius: 8)
.offset(y: 4)
}
Spacer().frame(height: 50)
// "Plaque" with name
VStack(spacing: 12) {
Text(achievement.definition.name)
.font(.system(size: 44, weight: .bold))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.65)
Text(achievement.definition.description)
.font(.system(size: 22, weight: .regular))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 40)
.padding(.vertical, 28)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(theme.surfaceColor)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(theme.accentColor.opacity(0.3), lineWidth: 2)
)
)
.padding(.horizontal, 40)
if let date = achievement.earnedAt {
Text(date.formatted(date: .long, time: .omitted))
.font(.system(size: 18, weight: .medium))
.foregroundStyle(theme.secondaryTextColor)
.padding(.top, 20)
}
Spacer()
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
private struct SampleC_Milestone: View {
let achievement: AchievementProgress
let theme: ShareTheme
private let gold = Color(hex: "FFD700")
private let goldDark = Color(hex: "B8860B")
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
// Gold spotlight
RadialGradient(
colors: [gold.opacity(0.12), .clear],
center: UnitPoint(x: 0.5, y: 0.28),
startRadius: 10,
endRadius: 600
)
VStack(spacing: 0) {
Spacer().frame(height: 60)
Text("MILESTONE")
.font(.system(size: 22, weight: .black))
.tracking(8)
.foregroundStyle(gold)
Spacer()
// Badge with gold shelf
VStack(spacing: 0) {
SampleBadgeAppIcon(definition: achievement.definition, size: 380)
.shadow(color: gold.opacity(0.3), radius: 30, y: 10)
.overlay {
RoundedRectangle(cornerRadius: 380 * 0.22)
.stroke(gold.opacity(0.6), lineWidth: 3)
.frame(width: 390, height: 390)
}
// Gold shelf
Rectangle()
.fill(
LinearGradient(colors: [.clear, gold.opacity(0.5), .clear],
startPoint: .leading, endPoint: .trailing)
)
.frame(width: 500, height: 3)
.padding(.top, 20)
}
Spacer().frame(height: 50)
// Gold plaque
VStack(spacing: 12) {
Text(achievement.definition.name)
.font(.system(size: 44, weight: .bold))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.65)
Text(achievement.definition.description)
.font(.system(size: 22, weight: .regular))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 40)
.padding(.vertical, 28)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(gold.opacity(0.08))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(
LinearGradient(colors: [gold, goldDark], startPoint: .top, endPoint: .bottom),
lineWidth: 2
)
)
)
.padding(.horizontal, 40)
Spacer()
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
//
// MARK: - SAMPLE D: "Poster"
// Street-poster / hype-beast style. Achievement name is MASSIVE and fills
// the card. Badge overlaps the type. Diagonal accent stripe. Rotated text.
//
private struct SampleD_Spotlight: View {
let achievement: AchievementProgress
let theme: ShareTheme
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
// Diagonal accent stripe
Rectangle()
.fill(theme.accentColor.opacity(0.15))
.frame(width: 300, height: 3000)
.rotationEffect(.degrees(-25))
.offset(x: -100)
Rectangle()
.fill(theme.accentColor.opacity(0.08))
.frame(width: 200, height: 3000)
.rotationEffect(.degrees(-25))
.offset(x: 200)
VStack(spacing: 0) {
// Top: rotated side label
HStack {
Text("UNLOCKED")
.font(.system(size: 14, weight: .black))
.tracking(6)
.foregroundStyle(theme.accentColor)
.rotationEffect(.degrees(-90))
.fixedSize()
Spacer()
VStack(alignment: .trailing, spacing: 4) {
if let sport = achievement.definition.sport {
Text(sport.rawValue)
.font(.system(size: 20, weight: .black))
.tracking(3)
.foregroundStyle(theme.accentColor)
}
Text(achievement.definition.category.displayName.uppercased())
.font(.system(size: 14, weight: .bold))
.tracking(2)
.foregroundStyle(theme.secondaryTextColor)
}
}
.padding(.top, 20)
Spacer()
// MASSIVE name
ZStack {
Text(achievement.definition.name.uppercased())
.font(.system(size: 90, weight: .black))
.foregroundStyle(theme.textColor.opacity(0.08))
.multilineTextAlignment(.center)
.lineLimit(4)
.minimumScaleFactor(0.4)
.padding(.horizontal, 20)
// Badge floating on top
SampleBadgeDiamond(
definition: achievement.definition,
size: 320
)
.shadow(color: .black.opacity(0.4), radius: 16, y: 8)
}
Spacer().frame(height: 30)
// Name (readable size)
Text(achievement.definition.name)
.font(.system(size: 56, weight: .black))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.6)
.padding(.horizontal, 40)
Spacer().frame(height: 16)
Text(achievement.definition.description)
.font(.system(size: 22, weight: .medium))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
.padding(.horizontal, 80)
if let date = achievement.earnedAt {
Text(date.formatted(date: .abbreviated, time: .omitted).uppercased())
.font(.system(size: 18, weight: .black))
.tracking(3)
.foregroundStyle(theme.accentColor)
.padding(.top, 16)
}
Spacer()
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
private struct SampleD_Milestone: View {
let achievement: AchievementProgress
let theme: ShareTheme
private let gold = Color(hex: "FFD700")
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
// Gold diagonal stripes
Rectangle()
.fill(gold.opacity(0.1))
.frame(width: 300, height: 3000)
.rotationEffect(.degrees(-25))
.offset(x: -100)
Rectangle()
.fill(gold.opacity(0.06))
.frame(width: 200, height: 3000)
.rotationEffect(.degrees(-25))
.offset(x: 200)
VStack(spacing: 0) {
Text("MILESTONE")
.font(.system(size: 20, weight: .black))
.tracking(8)
.foregroundStyle(gold)
.padding(.top, 20)
Spacer()
ZStack {
Text(achievement.definition.name.uppercased())
.font(.system(size: 90, weight: .black))
.foregroundStyle(gold.opacity(0.06))
.multilineTextAlignment(.center)
.lineLimit(4)
.minimumScaleFactor(0.4)
.padding(.horizontal, 20)
SampleBadgeDiamond(definition: achievement.definition, size: 360)
.overlay {
// Gold diamond border
Rectangle()
.stroke(gold.opacity(0.5), lineWidth: 3)
.frame(width: 270, height: 270)
.rotationEffect(.degrees(45))
}
.shadow(color: gold.opacity(0.3), radius: 20, y: 8)
}
Spacer().frame(height: 30)
Text(achievement.definition.name)
.font(.system(size: 52, weight: .black))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.6)
.padding(.horizontal, 40)
Spacer().frame(height: 16)
Text(achievement.definition.description)
.font(.system(size: 22, weight: .medium))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
.padding(.horizontal, 80)
Spacer()
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
//
// MARK: - SAMPLE E: "Ticket Stub"
// Vintage game-day ticket. Notched edges, dashed tear line, compact info
// sections, serial number decoration. Feels like a collectible stub.
//
private struct SampleE_Spotlight: View {
let achievement: AchievementProgress
let theme: ShareTheme
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
VStack(spacing: 0) {
Spacer().frame(height: 40)
// TICKET top portion
VStack(spacing: 16) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("SPORTSTIME ACHIEVEMENTS")
.font(.system(size: 14, weight: .black))
.tracking(2)
.foregroundStyle(theme.secondaryTextColor)
Text("ADMIT ONE")
.font(.system(size: 28, weight: .black))
.foregroundStyle(theme.textColor)
}
Spacer()
if let sport = achievement.definition.sport {
Text(sport.rawValue)
.font(.system(size: 36, weight: .black, design: .rounded))
.foregroundStyle(theme.accentColor)
}
}
// Dashed tear line with notches
HStack(spacing: 0) {
Circle()
.fill(theme.gradientColors.first ?? .black)
.frame(width: 30, height: 30)
.offset(x: -15)
Rectangle()
.stroke(theme.textColor.opacity(0.2), style: StrokeStyle(lineWidth: 2, dash: [8, 6]))
.frame(height: 2)
Circle()
.fill(theme.gradientColors.first ?? .black)
.frame(width: 30, height: 30)
.offset(x: 15)
}
}
.padding(28)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(theme.surfaceColor)
.overlay(RoundedRectangle(cornerRadius: 20).stroke(theme.borderColor, lineWidth: 1))
)
Spacer().frame(height: 30)
// Main content area
VStack(spacing: 24) {
SampleBadgeRoundedRect(
definition: achievement.definition,
size: 300
)
Text(achievement.definition.name)
.font(.system(size: 48, weight: .black))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.6)
Text(achievement.definition.description)
.font(.system(size: 22, weight: .regular))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
}
Spacer().frame(height: 30)
// Bottom stub info
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("DATE")
.font(.system(size: 12, weight: .black))
.tracking(2)
.foregroundStyle(theme.secondaryTextColor)
if let date = achievement.earnedAt {
Text(date.formatted(date: .abbreviated, time: .omitted))
.font(.system(size: 20, weight: .bold))
.foregroundStyle(theme.textColor)
}
}
Spacer()
VStack(alignment: .center, spacing: 4) {
Text("CATEGORY")
.font(.system(size: 12, weight: .black))
.tracking(2)
.foregroundStyle(theme.secondaryTextColor)
Text(achievement.definition.category.displayName.uppercased())
.font(.system(size: 20, weight: .bold))
.foregroundStyle(theme.textColor)
}
Spacer()
VStack(alignment: .trailing, spacing: 4) {
Text("NO.")
.font(.system(size: 12, weight: .black))
.tracking(2)
.foregroundStyle(theme.secondaryTextColor)
Text(String(achievement.definition.id.prefix(8)).uppercased())
.font(.system(size: 16, weight: .bold, design: .monospaced))
.foregroundStyle(theme.accentColor)
}
}
.padding(20)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(theme.surfaceColor)
.overlay(RoundedRectangle(cornerRadius: 16).stroke(theme.borderColor, lineWidth: 1))
)
// Barcode decoration
HStack(spacing: 2) {
ForEach(0..<30, id: \.self) { i in
Rectangle()
.fill(theme.textColor.opacity(0.15))
.frame(width: barWidth(for: i), height: 28)
}
}
.padding(.top, 12)
Spacer()
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
private func barWidth(for index: Int) -> CGFloat {
let widths: [CGFloat] = [3, 2, 4, 2, 3, 5, 2, 3, 2, 4]
return widths[index % widths.count]
}
}
private struct SampleE_Milestone: View {
let achievement: AchievementProgress
let theme: ShareTheme
private let gold = Color(hex: "FFD700")
var body: some View {
ZStack {
ShareCardBackground(theme: theme, sports: achievement.sportSet)
VStack(spacing: 0) {
Spacer().frame(height: 40)
// Gold ticket header
VStack(spacing: 16) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("MILESTONE ACHIEVEMENT")
.font(.system(size: 14, weight: .black))
.tracking(2)
.foregroundStyle(gold)
Text("VIP ACCESS")
.font(.system(size: 28, weight: .black))
.foregroundStyle(theme.textColor)
}
Spacer()
Image(systemName: "trophy.fill")
.font(.system(size: 32))
.foregroundStyle(gold)
}
HStack(spacing: 0) {
Circle()
.fill(theme.gradientColors.first ?? .black)
.frame(width: 30, height: 30)
.offset(x: -15)
Rectangle()
.stroke(gold.opacity(0.3), style: StrokeStyle(lineWidth: 2, dash: [8, 6]))
.frame(height: 2)
Circle()
.fill(theme.gradientColors.first ?? .black)
.frame(width: 30, height: 30)
.offset(x: 15)
}
}
.padding(28)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(theme.surfaceColor)
.overlay(RoundedRectangle(cornerRadius: 20).stroke(gold.opacity(0.4), lineWidth: 2))
)
Spacer().frame(height: 30)
VStack(spacing: 24) {
SampleBadgeRoundedRect(definition: achievement.definition, size: 320)
.overlay {
RoundedRectangle(cornerRadius: 320 * 0.22)
.stroke(gold.opacity(0.5), lineWidth: 3)
.frame(width: 330, height: 330)
}
Text(achievement.definition.name)
.font(.system(size: 48, weight: .black))
.foregroundStyle(theme.textColor)
.multilineTextAlignment(.center)
.lineLimit(3)
.minimumScaleFactor(0.6)
Text(achievement.definition.description)
.font(.system(size: 22, weight: .regular))
.foregroundStyle(theme.secondaryTextColor)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
}
Spacer()
ShareCardFooter(theme: theme)
}
.padding(ShareCardDimensions.padding)
}
.frame(width: ShareCardDimensions.cardSize.width, height: ShareCardDimensions.cardSize.height)
}
}
//
// MARK: - Badge Variants
// Each sample gets its own badge shape for maximum variety.
//
// A: Classic circle with thick gradient ring
private struct SampleBadgeCircle: View {
let definition: AchievementDefinition
let size: CGFloat
var body: some View {
ZStack {
Circle()
.stroke(
LinearGradient(
colors: [definition.iconColor, definition.iconColor.opacity(0.4)],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: size * 0.04
)
.frame(width: size * 0.92, height: size * 0.92)
Circle()
.fill(
RadialGradient(
colors: [definition.iconColor.opacity(0.25), definition.iconColor.opacity(0.05)],
center: .center,
startRadius: 0,
endRadius: size * 0.45
)
)
.frame(width: size * 0.84, height: size * 0.84)
Image(systemName: definition.iconName)
.font(.system(size: size * 0.4, weight: .bold))
.foregroundStyle(definition.iconColor)
}
.frame(width: size, height: size)
}
}
// B: Minimal just color fill, no border, very clean
private struct SampleBadgeMinimal: View {
let definition: AchievementDefinition
let size: CGFloat
var body: some View {
ZStack {
Circle()
.fill(definition.iconColor.opacity(0.12))
.frame(width: size, height: size)
Image(systemName: definition.iconName)
.font(.system(size: size * 0.42, weight: .light))
.foregroundStyle(definition.iconColor)
}
}
}
// C: iOS app icon style rounded square with shadow
private struct SampleBadgeAppIcon: View {
let definition: AchievementDefinition
let size: CGFloat
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: size * 0.22)
.fill(
LinearGradient(
colors: [definition.iconColor.opacity(0.3), definition.iconColor.opacity(0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: size, height: size)
RoundedRectangle(cornerRadius: size * 0.22)
.stroke(definition.iconColor.opacity(0.5), lineWidth: size * 0.02)
.frame(width: size, height: size)
Image(systemName: definition.iconName)
.font(.system(size: size * 0.42, weight: .semibold))
.foregroundStyle(definition.iconColor)
}
.shadow(color: .black.opacity(0.2), radius: 8, y: 4)
}
}
// D: Diamond rotated square
private struct SampleBadgeDiamond: View {
let definition: AchievementDefinition
let size: CGFloat
var body: some View {
ZStack {
Rectangle()
.fill(definition.iconColor.opacity(0.15))
.frame(width: size * 0.65, height: size * 0.65)
.rotationEffect(.degrees(45))
Rectangle()
.stroke(definition.iconColor.opacity(0.6), lineWidth: size * 0.02)
.frame(width: size * 0.65, height: size * 0.65)
.rotationEffect(.degrees(45))
Image(systemName: definition.iconName)
.font(.system(size: size * 0.35, weight: .bold))
.foregroundStyle(definition.iconColor)
}
.frame(width: size, height: size)
}
}
// E: Rounded rectangle with inner circle ticket/stamp look
private struct SampleBadgeRoundedRect: View {
let definition: AchievementDefinition
let size: CGFloat
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: size * 0.18)
.fill(definition.iconColor.opacity(0.1))
.frame(width: size, height: size)
RoundedRectangle(cornerRadius: size * 0.18)
.stroke(definition.iconColor.opacity(0.4), lineWidth: size * 0.02)
.frame(width: size, height: size)
// Inner stamp circle
Circle()
.stroke(definition.iconColor.opacity(0.2), lineWidth: size * 0.01)
.frame(width: size * 0.7, height: size * 0.7)
Image(systemName: definition.iconName)
.font(.system(size: size * 0.38, weight: .bold))
.foregroundStyle(definition.iconColor)
}
}
}
//
// MARK: - Support Shapes
//
private struct GridPatternA: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let spacing: CGFloat = 40
var x: CGFloat = 0
while x <= rect.width {
path.move(to: CGPoint(x: x, y: 0))
path.addLine(to: CGPoint(x: x, y: rect.height))
x += spacing
}
var y: CGFloat = 0
while y <= rect.height {
path.move(to: CGPoint(x: 0, y: y))
path.addLine(to: CGPoint(x: rect.width, y: y))
y += spacing
}
return path
}
}
// Helper for sport set
private extension AchievementProgress {
var sportSet: Set<Sport> {
if let sport = definition.sport {
return [sport]
}
return []
}
}
#endif