learning_ai_clock/ios/ChronoMind/Views/Gamification/WeeklySummaryCard.swift

209 lines
7.6 KiB
Swift

// 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) {}
}