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>
This commit is contained in:
430
docs/plans/2026-01-13-sharing-overhaul-design.md
Normal file
430
docs/plans/2026-01-13-sharing-overhaul-design.md
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
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
|
||||||
|
|
||||||
|
```swift
|
||||||
|
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)
|
||||||
|
|
||||||
|
```swift
|
||||||
|
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
|
||||||
|
|
||||||
|
```swift
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
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:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
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
|
||||||
|
|
||||||
|
```swift
|
||||||
|
@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
|
||||||
Reference in New Issue
Block a user