// ── Weekly Summary Card ─────────────────────────────────────── // Shareable image card showing weekly stats — designed for social sharing // Like Spotify Wrapped but for productivity import SwiftUI struct WeeklySummaryCard: View { let summary: GamificationEngine.WeeklySummary @State private var shareImage: UIImage? @State private var showShareSheet = false var body: some View { VStack(spacing: CMSpacing.lg) { // The card itself cardContent .background( GeometryReader { _ in Color.clear } ) // Share button Button { HapticEngine.tap() shareImage = renderCardAsImage() if shareImage != nil { showShareSheet = true } } label: { HStack(spacing: CMSpacing.sm) { Image(systemName: "square.and.arrow.up") Text("Share Summary") } .font(CMFonts.body(size: 15, weight: .semibold)) .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, CMSpacing.md) .background(CMColors.accent) .clipShape(RoundedRectangle(cornerRadius: CMRadius.md)) } } .sheet(isPresented: $showShareSheet) { if let image = shareImage { ShareSheet(items: [image]) } } } // MARK: - Card Content (rendered as image for sharing) @ViewBuilder private var cardContent: some View { VStack(spacing: CMSpacing.lg) { // Header HStack { VStack(alignment: .leading, spacing: CMSpacing.xxs) { Text("ChronoMind") .font(CMFonts.body(size: 12, weight: .medium)) .foregroundStyle(CMColors.textMuted) Text("Weekly Focus Report") .font(CMFonts.display(size: 20)) .foregroundStyle(CMColors.text) } Spacer() Text(summary.focusScore.weekLabel) .font(CMFonts.body(size: 12)) .foregroundStyle(CMColors.textMuted) } // Score ring (large) HStack(spacing: CMSpacing.xxl) { ZStack { Circle() .stroke(CMColors.border, lineWidth: 8) .frame(width: 100, height: 100) Circle() .trim(from: 0, to: CGFloat(summary.focusScore.score) / 100) .stroke(gradeColor, style: StrokeStyle(lineWidth: 8, lineCap: .round)) .frame(width: 100, height: 100) .rotationEffect(.degrees(-90)) VStack(spacing: 0) { Text("\(summary.focusScore.score)") .font(CMFonts.mono(size: 32, weight: .bold)) .foregroundStyle(CMColors.text) } } VStack(alignment: .leading, spacing: CMSpacing.md) { SummaryMetric(label: "On-time rate", value: String(format: "%.0f%%", summary.focusScore.onTimeRate * 100)) SummaryMetric(label: "Focus hours", value: String(format: "%.1fh", summary.focusScore.focusHours)) SummaryMetric(label: "Timers completed", value: "\(summary.focusScore.completedCount)") SummaryMetric(label: "Pomodoro sessions", value: "\(summary.pomodoroSessions)") } } Divider() .background(CMColors.border) // Streak + badges HStack { // Streak HStack(spacing: CMSpacing.sm) { Image(systemName: summary.streak.currentStreak > 0 ? "flame.fill" : "flame") .font(.title3) .foregroundStyle(summary.streak.currentStreak > 0 ? CMColors.important : CMColors.textMuted) VStack(alignment: .leading, spacing: 0) { Text("\(summary.streak.currentStreak)-day streak") .font(CMFonts.body(size: 14, weight: .semibold)) .foregroundStyle(CMColors.text) Text("Best: \(summary.streak.longestStreak) days") .font(CMFonts.body(size: 11)) .foregroundStyle(CMColors.textMuted) } } Spacer() // Earned badges count let earnedCount = summary.badges.count HStack(spacing: CMSpacing.sm) { Image(systemName: "trophy.fill") .font(.title3) .foregroundStyle(Color(red: 1, green: 0.84, blue: 0)) Text("\(earnedCount) badge\(earnedCount == 1 ? "" : "s")") .font(CMFonts.body(size: 14, weight: .semibold)) .foregroundStyle(CMColors.text) } } // Grade label Text(summary.focusScore.grade.rawValue) .font(CMFonts.display(size: 16)) .foregroundStyle(gradeColor) .padding(.horizontal, CMSpacing.lg) .padding(.vertical, CMSpacing.sm) .background(gradeColor.opacity(0.15)) .clipShape(Capsule()) } .padding(CMSpacing.xl) .background( LinearGradient( colors: [CMColors.surface, CMColors.bg], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .clipShape(RoundedRectangle(cornerRadius: CMRadius.xl)) .overlay( RoundedRectangle(cornerRadius: CMRadius.xl) .stroke(CMColors.border, lineWidth: 1) ) } // MARK: - Render as Image @MainActor private func renderCardAsImage() -> UIImage? { let renderer = ImageRenderer(content: cardContent .frame(width: 360) .padding(CMSpacing.lg) .background(CMColors.bg) ) renderer.scale = 3.0 return renderer.uiImage } private var gradeColor: Color { switch summary.focusScore.grade { case .excellent: return CMColors.success case .great: return CMColors.accent case .good: return CMColors.standard case .fair: return CMColors.important case .needsWork: return CMColors.critical } } } // MARK: - Summary Metric Row private struct SummaryMetric: View { let label: String let value: String var body: some View { HStack { Text(label) .font(CMFonts.body(size: 12)) .foregroundStyle(CMColors.textSecondary) Spacer() Text(value) .font(CMFonts.mono(size: 13, weight: .bold)) .foregroundStyle(CMColors.text) } } } // MARK: - Share Sheet (UIKit wrapper) struct ShareSheet: UIViewControllerRepresentable { let items: [Any] func makeUIViewController(context: Context) -> UIActivityViewController { UIActivityViewController(activityItems: items, applicationActivities: nil) } func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} }