diff --git a/Shared/Services/ExportableInsightsViews.swift b/Shared/Services/ExportableInsightsViews.swift new file mode 100644 index 0000000..c9e3997 --- /dev/null +++ b/Shared/Services/ExportableInsightsViews.swift @@ -0,0 +1,380 @@ +// +// ExportableInsightsViews.swift +// Feels +// +// Exportable insights views with sample AI-generated insights for screenshots. +// + +#if DEBUG +import SwiftUI + +// MARK: - Sample Insights Data + +struct SampleInsightsData { + + static let monthInsights: [Insight] = [ + Insight( + icon: "chart.line.uptrend.xyaxis", + title: "Weekend Mood Boost", + description: "Your mood is consistently 23% higher on weekends. Saturday mornings seem particularly positive for you.", + mood: .great + ), + Insight( + icon: "moon.stars", + title: "Sleep Connection", + description: "Days following 7+ hours of sleep show notably better moods. Prioritizing rest could help maintain your wellbeing.", + mood: .good + ), + Insight( + icon: "figure.walk", + title: "Movement Matters", + description: "Your mood averages 'Good' on days with 6,000+ steps, compared to 'Average' on less active days.", + mood: .good + ), + Insight( + icon: "calendar.badge.checkmark", + title: "Consistent Logger", + description: "You've logged your mood 28 out of 31 days this month. This consistency helps reveal meaningful patterns.", + mood: nil + ), + Insight( + icon: "arrow.up.right", + title: "Upward Trend", + description: "Your average mood has improved from 'Average' to 'Good' over the past two weeks. Keep it up!", + mood: .good + ) + ] + + static let yearInsights: [Insight] = [ + Insight( + icon: "sun.max", + title: "Summer Peak", + description: "June through August showed your highest mood scores of the year, with 67% of days rated 'Good' or better.", + mood: .great + ), + Insight( + icon: "leaf", + title: "Seasonal Patterns", + description: "Your mood tends to dip slightly in late winter. Consider planning mood-boosting activities for February.", + mood: .average + ), + Insight( + icon: "star.fill", + title: "Personal Best", + description: "You achieved a 45-day streak of logging in March - your longest streak this year!", + mood: nil + ), + Insight( + icon: "heart.fill", + title: "Most Common Mood", + description: "'Good' has been your most frequent mood this year, appearing 42% of the time.", + mood: .good + ), + Insight( + icon: "chart.bar.fill", + title: "Year in Review", + description: "You've logged 312 mood entries this year. That's 85% consistency - well above average!", + mood: nil + ) + ] + + static let allTimeInsights: [Insight] = [ + Insight( + icon: "trophy.fill", + title: "Mood Champion", + description: "Since you started, your overall mood average has steadily improved by 18%. Real progress!", + mood: .great + ), + Insight( + icon: "flame.fill", + title: "Longest Streak", + description: "Your all-time best logging streak is 89 days. You're building a powerful habit.", + mood: nil + ), + Insight( + icon: "calendar", + title: "Tuesday Tendency", + description: "Historically, Tuesdays are your most challenging day. Consider scheduling something enjoyable.", + mood: .average + ), + Insight( + icon: "sparkles", + title: "Growth Mindset", + description: "Your 'Great' days have increased from 12% to 24% since you started tracking. Wonderful progress!", + mood: .great + ), + Insight( + icon: "person.fill.checkmark", + title: "Self-Awareness", + description: "With 847 total entries, you've built remarkable insight into your emotional patterns.", + mood: nil + ) + ] +} + +// MARK: - Exportable Insights View + +struct ExportableInsightsView: View { + let colorScheme: ColorScheme + let moodTint: MoodTints + let imagePack: MoodImages + + private var textColor: Color { + colorScheme == .dark ? .white : .black + } + + private var backgroundColor: Color { + colorScheme == .dark ? Color(red: 0.11, green: 0.11, blue: 0.12) : Color(red: 0.95, green: 0.95, blue: 0.97) + } + + var body: some View { + VStack(spacing: 0) { + // Status bar area (dynamic island space) + Color.clear + .frame(height: 59) + + // Main content + VStack(spacing: 20) { + // Header - matches InsightsView exactly + HStack { + Text("Insights") + .font(.title.weight(.bold)) + .foregroundColor(textColor) + Spacer() + + // AI badge + HStack(spacing: 4) { + Image(systemName: "sparkles") + .font(.caption.weight(.medium)) + Text("AI") + .font(.caption.weight(.semibold)) + } + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + LinearGradient( + colors: [.purple, .blue], + startPoint: .leading, + endPoint: .trailing + ) + ) + .clipShape(Capsule()) + } + .padding(.horizontal) + + // This Month Section (show first 3 insights to fit) + ExportableInsightsSectionView( + title: "This Month", + icon: "calendar", + insights: Array(SampleInsightsData.monthInsights.prefix(3)), + textColor: textColor, + moodTint: moodTint, + imagePack: imagePack, + colorScheme: colorScheme + ) + + // This Year Section (show first 2 insights to fit) + ExportableInsightsSectionView( + title: "This Year", + icon: "calendar.badge.clock", + insights: Array(SampleInsightsData.yearInsights.prefix(2)), + textColor: textColor, + moodTint: moodTint, + imagePack: imagePack, + colorScheme: colorScheme + ) + + Spacer() + } + .padding(.top, 8) + + // Tab bar + ExportableTabBar(colorScheme: colorScheme, selectedTab: "Insights") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(backgroundColor) + } +} + +// MARK: - Exportable Insights Section View + +struct ExportableInsightsSectionView: View { + let title: String + let icon: String + let insights: [Insight] + let textColor: Color + let moodTint: MoodTints + let imagePack: MoodImages + let colorScheme: ColorScheme + + var body: some View { + VStack(spacing: 0) { + // Section Header - matches InsightsSectionView exactly + HStack { + Image(systemName: icon) + .font(.headline.weight(.medium)) + .foregroundColor(textColor.opacity(0.6)) + + Text(title) + .font(.title3.weight(.bold)) + .foregroundColor(textColor) + + Spacer() + + Image(systemName: "chevron.up") + .font(.caption.weight(.semibold)) + .foregroundColor(textColor.opacity(0.4)) + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + + // Insights List - matches InsightsSectionView exactly + VStack(spacing: 10) { + ForEach(insights) { insight in + ExportableInsightCardView( + insight: insight, + textColor: textColor, + moodTint: moodTint, + imagePack: imagePack, + colorScheme: colorScheme + ) + } + } + .padding(.horizontal, 16) + .padding(.bottom, 16) + } + .background( + RoundedRectangle(cornerRadius: 16) + .fill(colorScheme == .dark ? Color(.systemGray6) : .white) + ) + .padding(.horizontal) + } +} + +// MARK: - Exportable Insight Card View + +struct ExportableInsightCardView: View { + let insight: Insight + let textColor: Color + let moodTint: MoodTints + let imagePack: MoodImages + let colorScheme: ColorScheme + + private var accentColor: Color { + if let mood = insight.mood { + return moodTint.color(forMood: mood) + } + return .accentColor + } + + var body: some View { + // Matches InsightCardView exactly + HStack(alignment: .top, spacing: 14) { + // Icon + ZStack { + Circle() + .fill(accentColor.opacity(0.15)) + .frame(width: 44, height: 44) + + if let mood = insight.mood { + imagePack.icon(forMood: mood) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 22, height: 22) + .foregroundColor(accentColor) + } else { + Image(systemName: insight.icon) + .font(.headline.weight(.semibold)) + .foregroundColor(accentColor) + } + } + + // Text Content + VStack(alignment: .leading, spacing: 4) { + Text(insight.title) + .font(.subheadline.weight(.semibold)) + .foregroundColor(textColor) + + Text(insight.description) + .font(.subheadline) + .foregroundColor(textColor.opacity(0.7)) + .fixedSize(horizontal: false, vertical: true) + } + + Spacer() + } + .padding(14) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(colorScheme == .dark ? Color(.systemGray5) : Color(.systemGray6)) + ) + } +} + +// MARK: - Exportable Tab Bar + +struct ExportableTabBar: View { + let colorScheme: ColorScheme + let selectedTab: String + + private var textColor: Color { + colorScheme == .dark ? .white : .black + } + + private var tabBarBackground: Color { + colorScheme == .dark ? Color(red: 0.11, green: 0.11, blue: 0.12) : .white + } + + var body: some View { + HStack(spacing: 0) { + tabItem(icon: "list.bullet", title: "Day", isSelected: selectedTab == "Day") + tabItem(icon: "calendar", title: "Month", isSelected: selectedTab == "Month") + tabItem(icon: "chart.bar.fill", title: "Year", isSelected: selectedTab == "Year") + tabItem(icon: "lightbulb.fill", title: "Insights", isSelected: selectedTab == "Insights") + tabItem(icon: "gearshape.fill", title: "Settings", isSelected: selectedTab == "Settings") + } + .padding(.top, 8) + .padding(.bottom, 28) // Home indicator space + .background(tabBarBackground) + } + + private func tabItem(icon: String, title: String, isSelected: Bool) -> some View { + VStack(spacing: 4) { + Image(systemName: icon) + .font(.system(size: 22)) + Text(title) + .font(.caption2) + } + .foregroundColor(isSelected ? .accentColor : textColor.opacity(0.4)) + .frame(maxWidth: .infinity) + } +} + +// MARK: - Insights Export Container + +struct ExportableInsightsContainer: View { + let width: CGFloat + let height: CGFloat + let colorScheme: ColorScheme + let content: Content + + init(width: CGFloat, height: CGFloat, colorScheme: ColorScheme, @ViewBuilder content: () -> Content) { + self.width = width + self.height = height + self.colorScheme = colorScheme + self.content = content() + } + + private var backgroundColor: Color { + colorScheme == .dark ? Color(red: 0.11, green: 0.11, blue: 0.12) : Color(red: 0.95, green: 0.95, blue: 0.97) + } + + var body: some View { + content + .environment(\.colorScheme, colorScheme) + .frame(width: width, height: height) + .background(backgroundColor) + } +} +#endif diff --git a/Shared/Services/InsightsExporter.swift b/Shared/Services/InsightsExporter.swift new file mode 100644 index 0000000..79df8a4 --- /dev/null +++ b/Shared/Services/InsightsExporter.swift @@ -0,0 +1,103 @@ +// +// InsightsExporter.swift +// Feels +// +// Debug utility to export insights view screenshots with sample AI data. +// + +#if DEBUG +import SwiftUI +import UIKit + +/// Exports insights view screenshots for App Store marketing +@MainActor +class InsightsExporter { + + // MARK: - Screen Sizes (iPhone 15 Pro Max @ 3x) + + /// Full screen size for insights export + static let screenSize = CGSize(width: 430, height: 932) + + // MARK: - Export All Insights Screenshots + + /// Exports insights screenshots in light and dark mode + /// - Returns: URL to the export directory, or nil if failed + static func exportInsightsScreenshots() async -> URL? { + let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let exportPath = documentsPath.appendingPathComponent("InsightsExports", isDirectory: true) + + // Clean and create export directory + try? FileManager.default.removeItem(at: exportPath) + try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true) + + var totalExported = 0 + + // Export with default tint and emoji pack + let moodTint: MoodTints = .Default + let imagePack: MoodImages = .Emoji + + // Export light mode + await exportInsightsView( + colorScheme: .light, + moodTint: moodTint, + imagePack: imagePack, + to: exportPath, + name: "insights_light" + ) + totalExported += 1 + + // Export dark mode + await exportInsightsView( + colorScheme: .dark, + moodTint: moodTint, + imagePack: imagePack, + to: exportPath, + name: "insights_dark" + ) + totalExported += 1 + + print("✨ Total \(totalExported) insights screenshots exported to: \(exportPath.path)") + return exportPath + } + + // MARK: - Export Single Screenshot + + private static func exportInsightsView( + colorScheme: ColorScheme, + moodTint: MoodTints, + imagePack: MoodImages, + to folder: URL, + name: String + ) async { + let content = ExportableInsightsView( + colorScheme: colorScheme, + moodTint: moodTint, + imagePack: imagePack + ) + + let view = ExportableInsightsContainer( + width: screenSize.width, + height: screenSize.height, + colorScheme: colorScheme + ) { + content + } + + await renderAndSave(view: view, to: folder, name: name) + } + + // MARK: - Render and Save + + private static func renderAndSave(view: V, to folder: URL, name: String) async { + let renderer = ImageRenderer(content: view.frame(width: screenSize.width, height: screenSize.height)) + renderer.scale = 3.0 // 3x for high res + + if let image = renderer.uiImage { + let url = folder.appendingPathComponent("\(name).png") + if let data = image.pngData() { + try? data.write(to: url) + } + } + } +} +#endif diff --git a/Shared/Views/SettingsView/SettingsView.swift b/Shared/Views/SettingsView/SettingsView.swift index 491d19d..05e0e09 100644 --- a/Shared/Views/SettingsView/SettingsView.swift +++ b/Shared/Views/SettingsView/SettingsView.swift @@ -25,6 +25,8 @@ struct SettingsContentView: View { @State private var votingLayoutExportPath: URL? @State private var isExportingWatchViews = false @State private var watchExportPath: URL? + @State private var isExportingInsights = false + @State private var insightsExportPath: URL? @State private var isDeletingHealthKitData = false @State private var healthKitDeleteResult: String? @StateObject private var healthService = HealthService.shared @@ -70,6 +72,7 @@ struct SettingsContentView: View { exportWidgetsButton exportVotingLayoutsButton exportWatchViewsButton + exportInsightsButton deleteHealthKitDataButton clearDataButton @@ -437,10 +440,9 @@ struct SettingsContentView: View { Task { widgetExportPath = await WidgetExporter.exportAllWidgets() isExportingWidgets = false - // Open Files app to the export location if let path = widgetExportPath { - // Show share sheet or alert with path print("📸 Widgets exported to: \(path.path)") + openInFilesApp(path) } } } label: { @@ -494,6 +496,7 @@ struct SettingsContentView: View { isExportingVotingLayouts = false if let path = votingLayoutExportPath { print("📸 Voting layouts exported to: \(path.path)") + openInFilesApp(path) } } } label: { @@ -547,6 +550,7 @@ struct SettingsContentView: View { isExportingWatchViews = false if let path = watchExportPath { print("⌚ Watch views exported to: \(path.path)") + openInFilesApp(path) } } } label: { @@ -590,6 +594,66 @@ struct SettingsContentView: View { .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } + private var exportInsightsButton: some View { + ZStack { + theme.currentTheme.secondaryBGColor + Button { + isExportingInsights = true + Task { + insightsExportPath = await InsightsExporter.exportInsightsScreenshots() + isExportingInsights = false + if let path = insightsExportPath { + print("✨ Insights exported to: \(path.path)") + openInFilesApp(path) + } + } + } label: { + HStack(spacing: 12) { + if isExportingInsights { + ProgressView() + .frame(width: 32) + } else { + Image(systemName: "sparkles") + .font(.title2) + .foregroundStyle( + LinearGradient( + colors: [.purple, .blue], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(width: 32) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Export Insights Screenshots") + .foregroundColor(textColor) + + if let path = insightsExportPath { + Text("Saved to Documents/InsightsExports") + .font(.caption) + .foregroundColor(.green) + } else { + Text("AI insights in light & dark mode") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: "arrow.down.doc.fill") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding() + } + .disabled(isExportingInsights) + } + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) + } + private var deleteHealthKitDataButton: some View { ZStack { theme.currentTheme.secondaryBGColor @@ -975,6 +1039,21 @@ struct SettingsContentView: View { .fixedSize(horizontal: false, vertical: true) .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) } + + // MARK: - Helper Functions + + private func openInFilesApp(_ url: URL) { + #if targetEnvironment(simulator) + // On simulator, copy path to clipboard for easy Finder access (Cmd+Shift+G) + UIPasteboard.general.string = url.path + #else + // On device, open Files app + let filesAppURL = URL(string: "shareddocuments://\(url.path)")! + if UIApplication.shared.canOpenURL(filesAppURL) { + UIApplication.shared.open(filesAppURL) + } + #endif + } } // MARK: - Reminder Time Picker View diff --git a/feels-promo/public/insights_light.png b/feels-promo/public/insights_light.png new file mode 100644 index 0000000..195618b Binary files /dev/null and b/feels-promo/public/insights_light.png differ diff --git a/feels-promo/public/timeline_light_large_voting.png b/feels-promo/public/timeline_light_large_voting.png new file mode 100644 index 0000000..07f0d50 Binary files /dev/null and b/feels-promo/public/timeline_light_large_voting.png differ diff --git a/feels-promo/public/timeline_light_medium_voting.png b/feels-promo/public/timeline_light_medium_voting.png new file mode 100644 index 0000000..114867d Binary files /dev/null and b/feels-promo/public/timeline_light_medium_voting.png differ diff --git a/feels-promo/src/ConceptB-NoJournalJournal.tsx b/feels-promo/src/ConceptB-NoJournalJournal.tsx index 6129fb6..4c3c4ea 100644 --- a/feels-promo/src/ConceptB-NoJournalJournal.tsx +++ b/feels-promo/src/ConceptB-NoJournalJournal.tsx @@ -327,7 +327,7 @@ const SingleTapScene: React.FC = () => { }} > { }} > { visibility: "hidden", }} /> - {/* Screenshot positioned to align notch */} - + > + + {/* Phone frame on top */} { visibility: "hidden", }} /> - {/* Screenshot positioned to align notch */} - + > + + {/* Phone frame on top */}