Files
Sportstime/docs/plans/2026-01-13-sharing-overhaul-design.md
Trey t c989b7b1d1 docs: add sharing system overhaul design
Complete redesign of sharing to support:
- Trip summary cards with route maps
- 4 achievement card types (spotlight, collection, milestone, context)
- Stadium progress cards
- 8 user-customizable color themes
- Instagram Stories as primary target (1080×1920)
- Contextual share buttons throughout app

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 22:28:21 -06:00

17 KiB
Raw Blame History

Sharing System Overhaul

Design finalized: January 13, 2026

Overview

Complete overhaul of the sharing system to support shareable cards for trip summaries, achievements, and stadium progress. Primary target is Instagram Stories (1080×1920 vertical cards) with user-customizable color themes.

Goals

  • Share trip summaries as visual cards (not just deep links)
  • Share achievements (single, collection, milestone, contextual)
  • Share stadium progress with map visualization
  • User-customizable color themes (8 presets)
  • Contextual share buttons throughout the app (low friction)
  • Instagram Stories as primary destination

Architecture

Protocol-Based Design

All shareable content types conform to a common protocol:

protocol ShareableContent {
    var cardType: ShareCardType { get }
    func render(theme: ShareTheme) async throws -> UIImage
}

enum ShareCardType {
    case tripSummary
    case achievementSpotlight
    case achievementCollection
    case achievementMilestone
    case achievementContext
    case stadiumProgress
}

Component Hierarchy

ShareableContent (protocol)
├── TripShareContent      → TripCardGenerator
├── AchievementShareContent → AchievementCardGenerator
├── ProgressShareContent  → ProgressCardGenerator
└── CollectionShareContent → CollectionCardGenerator

Unified Components

  • ShareTheme - Color preset definitions
  • ShareCardFooter - Reusable SportsTime branding footer
  • ShareCardHeader - Sport icon + title treatment
  • ShareService - Instagram URL scheme, image export, fallback share sheet

Theme System

8 Color Presets

Theme Background Gradient Accent Text Use Case
Dark #1A1A2E → #16213E #FF6B35 White Default, premium feel
Light #FFFFFF → #F5F5F5 #FF6B35 #1A1A2E Clean, minimal
Midnight #0D1B2A → #1B263B #00D4FF White Night game energy
Forest #1B4332 → #2D6A4F #95D5B2 White Baseball/outdoor
Sunset #FF6B35 → #F7931E #FFFFFF White Bold, attention-grabbing
Berry #4A0E4E → #81267E #FF85A1 White Unique, stands out
Ocean #023E8A → #0077B6 #90E0EF White Cool, professional
Slate #2B2D42 → #3D405B #F4A261 #EDF2F4 Sophisticated neutral

Theme Definition

struct ShareTheme: Identifiable, Hashable {
    let id: String
    let name: String
    let gradientColors: [Color]
    let accentColor: Color
    let textColor: Color
    let secondaryTextColor: Color
    let mapStyle: MapStyle // .light or .dark

    static let all: [ShareTheme] = [.dark, .light, .midnight, .forest, .sunset, .berry, .ocean, .slate]
}

Theme preference is persisted per content type via UserDefaults.

Card Designs

Card Dimensions

All cards use Instagram Story dimensions: 1080 × 1920 pixels (9:16 aspect ratio).

Trip Summary Card

┌─────────────────────────────────┐
│  [Sport Icon]                   │  ← Header: 120px
│  "My Baseball Road Trip"        │
├─────────────────────────────────┤
│                                 │
│    ┌─────────────────────┐      │
│    │                     │      │
│    │   MAP SNAPSHOT      │      │  ← Hero map: 600px
│    │   with route line   │      │     Route drawn in accent color
│    │   + city markers    │      │     Visited cities = filled dots
│    │                     │      │
│    └─────────────────────┘      │
│                                 │
├─────────────────────────────────┤
│  "Jun 15 - Jun 22, 2026"        │  ← Date range
├─────────────────────────────────┤
│                                 │
│  ┌───────┐ ┌───────┐ ┌───────┐  │
│  │ 1,847 │ │   5   │ │   4   │  │  ← Stats row
│  │ miles │ │ games │ │cities │  │
│  └───────┘ └───────┘ └───────┘  │
│                                 │
├─────────────────────────────────┤
│  Chicago → Milwaukee → Detroit  │  ← City trail
│  → Cleveland → Pittsburgh       │
├─────────────────────────────────┤
│  [SportsTime logo]              │  ← Footer: 100px
│  "Plan your stadium adventure"  │
└─────────────────────────────────┘

Map Features:

  • Route line drawn using MKPolyline in accent color
  • City markers: filled circles with abbreviation labels
  • Map style matches theme (muted standard for dark themes)

Achievement Cards

Type 1: Single Spotlight

┌─────────────────────────────────┐
│                                 │
│         ┌───────────┐           │
│         │           │           │
│         │  BADGE    │           │  ← Large badge: 400×400px
│         │  GRAPHIC  │           │
│         │           │           │
│         └───────────┘           │
│                                 │
│      "ALL NL WEST"              │  ← Achievement name
│                                 │
│   "Visited all 5 stadiums       │  ← Description
│    in the National League       │
│    West division"               │
│                                 │
│      ✓ Unlocked Jan 12, 2026    │  ← Unlock date
│                                 │
│  [SportsTime logo + tagline]    │
└─────────────────────────────────┘

Type 2: Collection Grid

┌─────────────────────────────────┐
│  "My 2026 Achievements"         │  ← Header with year
├─────────────────────────────────┤
│  ┌─────┐ ┌─────┐ ┌─────┐        │
│  │Badge│ │Badge│ │Badge│        │  ← 3×3 or 3×4 grid
│  │ #1  │ │ #2  │ │ #3  │        │
│  └─────┘ └─────┘ └─────┘        │
│  ┌─────┐ ┌─────┐ ┌─────┐        │
│  │Badge│ │Badge│ │Badge│        │
│  │ #4  │ │ #5  │ │ #6  │        │
│  └─────┘ └─────┘ └─────┘        │
├─────────────────────────────────┤
│  "12 achievements unlocked"     │  ← Summary count
│  [SportsTime logo + tagline]    │
└─────────────────────────────────┘

Type 3: Milestone Celebration

Same as Single Spotlight with:

  • Animated particle/confetti burst pattern (static for image)
  • Larger badge (500×500px)
  • "MILESTONE" label above badge
  • Gold accent elements regardless of theme

Type 4: Achievement + Context

┌─────────────────────────────────┐
│  ┌─────────┐                    │
│  │  BADGE  │  "Coast to Coast"  │  ← Badge + name side by side
│  └─────────┘   Unlocked!        │
├─────────────────────────────────┤
│                                 │
│    ┌─────────────────────┐      │
│    │   TRIP MAP or       │      │  ← Context: trip/visit
│    │   STADIUM PHOTO     │      │     that unlocked it
│    └─────────────────────┘      │
│                                 │
│  "Unlocked during my            │
│   Summer 2026 Road Trip"        │  ← Trip name or visit date
│                                 │
│  [SportsTime logo + tagline]    │
└─────────────────────────────────┘

Stadium Progress Card

┌─────────────────────────────────┐
│  [Sport Icon]                   │
│  "MLB Stadium Quest"            │  ← Header
├─────────────────────────────────┤
│                                 │
│         ┌───────────┐           │
│                     ╲          │
│       │      18      │          │  ← Progress ring: 320px
│       │    ────      │          │     Accent color fill
│       │   of 30      │          │
│        ╲                       │
│         └───────────┘           │
│                                 │
│          60% Complete           │  ← Percentage
│                                 │
├─────────────────────────────────┤
│  ┌───────┐ ┌───────┐ ┌───────┐  │
│  │  18   │ │  12   │ │  4    │  │  ← Stats row
│  │visited│ │remain │ │trips  │  │
│  └───────┘ └───────┘ └───────┘  │
├─────────────────────────────────┤
│    ┌─────────────────────┐      │
│    │                     │      │
│    │   US MAP SNAPSHOT   │      │  ← Map: 500px height
│    │   ● visited (accent)│      │
│    │   ○ remaining (gray)│      │
│    │                     │      │
│    └─────────────────────┘      │
├─────────────────────────────────┤
│  @username (optional)           │  ← Optional username
│  [SportsTime logo + tagline]    │
└─────────────────────────────────┘

Share UI/UX

Contextual Share Buttons

ShareButton component appears on:

  • Trip cards in saved trips list
  • Trip detail view toolbar
  • Achievement badges (on tap)
  • Progress ring on Progress tab
  • Achievement list header (for collection)
struct ShareButton: View {
    let content: ShareableContent
    @State private var showPreview = false

    var body: some View {
        Button {
            showPreview = true
        } label: {
            Image(systemName: "square.and.arrow.up")
        }
        .sheet(isPresented: $showPreview) {
            SharePreviewView(content: content)
        }
    }
}

SharePreviewView

┌─────────────────────────────────┐
│  ← Cancel          Share →      │  ← Nav bar
├─────────────────────────────────┤
│                                 │
│    ┌─────────────────────┐      │
│    │                     │      │
│    │   LIVE PREVIEW      │      │  ← 40% scale preview
│    │   of card           │      │     Updates in real-time
│    │                     │      │
│    └─────────────────────┘      │
│                                 │
├─────────────────────────────────┤
│  Theme                          │
│  [Dark] [Light] [Midnight] ...  │  ← Horizontal scroll
├─────────────────────────────────┤
│  ☑ Include username             │  ← Toggle (progress only)
│  [@yourhandle____________]      │
├─────────────────────────────────┤
│                                 │
│  ┌─────────────────────────┐    │
│  │   Share to Instagram     │    │  ← Primary CTA
│  └─────────────────────────┘    │
│                                 │
│  [Copy Image]  [More Options]   │  ← Secondary actions
│                                 │
└─────────────────────────────────┘

Instagram Integration

func shareToInstagram(image: UIImage) {
    guard let imageData = image.pngData() else { return }

    // Write to shared container for Instagram
    let pasteboardItems: [String: Any] = [
        "com.instagram.sharedSticker.backgroundImage": imageData
    ]

    UIPasteboard.general.setItems([pasteboardItems])

    // Open Instagram Stories
    if let url = URL(string: "instagram-stories://share?source_application=com.sportstime.app") {
        UIApplication.shared.open(url)
    }
}

Falls back to standard UIActivityViewController if Instagram not installed.

File Structure

Files to Delete

File Lines
SportsTime/Export/Services/ProgressCardGenerator.swift 607

New Files

File Purpose ~Lines
Export/Sharing/ShareableContent.swift Protocol + ShareTheme 120
Export/Sharing/ShareCardComponents.swift Header, footer, stats, map 200
Export/Sharing/TripCardGenerator.swift Trip summary cards 180
Export/Sharing/AchievementCardGenerator.swift All 4 achievement types 350
Export/Sharing/ProgressCardGenerator.swift Stadium progress cards 180
Export/Sharing/ShareService.swift Instagram, export, share sheet 150
Export/Views/SharePreviewView.swift Unified preview UI 250
Export/Views/ShareButton.swift Contextual button 40

Files to Modify

File Changes
TripDetailView.swift Replace shareTrip() with ShareButton, remove old ShareSheet
ProgressTabView.swift Add ShareButton to progress rings
Achievement views Add ShareButton to badges

Implementation Notes

Map Snapshot Generation

Reuse pattern from existing code but extract to ShareCardComponents:

func generateRouteMapSnapshot(
    stops: [TripStop],
    theme: ShareTheme
) async -> UIImage? {
    // Calculate bounding region
    // Configure MKMapSnapshotter with theme-appropriate style
    // Draw route polyline in accent color
    // Draw city markers
}

func generateProgressMapSnapshot(
    visited: [Stadium],
    remaining: [Stadium],
    theme: ShareTheme
) async -> UIImage? {
    // Calculate US-wide region
    // Draw visited markers in accent color
    // Draw remaining markers in gray
}

Image Rendering

Use SwiftUI ImageRenderer at 3x scale for high-res output:

func render(theme: ShareTheme) async throws -> UIImage {
    let cardView = TripCardView(trip: trip, theme: theme)
    let renderer = ImageRenderer(content: cardView)
    renderer.scale = 3.0

    guard let image = renderer.uiImage else {
        throw ShareError.renderingFailed
    }
    return image
}

Theme Persistence

@AppStorage("shareTheme.trip") private var tripThemeId: String = "dark"
@AppStorage("shareTheme.achievement") private var achievementThemeId: String = "dark"
@AppStorage("shareTheme.progress") private var progressThemeId: String = "dark"

Scope Summary

Category Count
New lines ~1,470
Deleted lines ~607
Modified lines ~50
New files 8
Deleted files 1
Modified files 3+

Success Criteria

  • All 3 content types shareable (trips, achievements, progress)
  • All 8 theme presets working
  • Instagram Stories direct share working
  • Fallback to share sheet when Instagram not installed
  • Share buttons appear contextually throughout app
  • Theme preference persisted per content type
  • Cards render at 1080×1920 with proper scaling