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