New Insights tab between Year and Customize with: - 20 insight generators producing 60+ unique insights - 5 random insights selected per section (month, year, all-time) - Categories: dominant mood, streaks, trends, positivity score, weekend vs weekday, mood swings, milestones, patterns, and more - Collapsible sections with mood-colored cards - Subscription paywall support - English and Spanish localization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
233 lines
7.7 KiB
Swift
233 lines
7.7 KiB
Swift
//
|
|
// InsightsView.swift
|
|
// Feels
|
|
//
|
|
// Created by Claude Code on 12/9/24.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct InsightsView: View {
|
|
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
|
@AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
|
|
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
|
@AppStorage(UserDefaultsStore.Keys.moodImages.rawValue, store: GroupUserDefaults.groupDefaults) private var imagePack: MoodImages = .FontAwesome
|
|
|
|
@StateObject private var viewModel = InsightsViewModel()
|
|
@EnvironmentObject var iapManager: IAPManager
|
|
@State private var showSubscriptionStore = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
// Header
|
|
HStack {
|
|
Text("Insights")
|
|
.font(.largeTitle.bold())
|
|
.foregroundColor(textColor)
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal)
|
|
|
|
// This Month Section
|
|
InsightsSectionView(
|
|
title: "This Month",
|
|
icon: "calendar",
|
|
insights: viewModel.monthInsights,
|
|
textColor: textColor,
|
|
moodTint: moodTint,
|
|
imagePack: imagePack,
|
|
theme: theme
|
|
)
|
|
|
|
// This Year Section
|
|
InsightsSectionView(
|
|
title: "This Year",
|
|
icon: "calendar.badge.clock",
|
|
insights: viewModel.yearInsights,
|
|
textColor: textColor,
|
|
moodTint: moodTint,
|
|
imagePack: imagePack,
|
|
theme: theme
|
|
)
|
|
|
|
// All Time Section
|
|
InsightsSectionView(
|
|
title: "All Time",
|
|
icon: "infinity",
|
|
insights: viewModel.allTimeInsights,
|
|
textColor: textColor,
|
|
moodTint: moodTint,
|
|
imagePack: imagePack,
|
|
theme: theme
|
|
)
|
|
}
|
|
.padding(.vertical)
|
|
.padding(.bottom, 100)
|
|
}
|
|
.disabled(iapManager.shouldShowPaywall)
|
|
|
|
if iapManager.shouldShowPaywall {
|
|
Color.black.opacity(0.3)
|
|
.ignoresSafeArea()
|
|
.onTapGesture {
|
|
showSubscriptionStore = true
|
|
}
|
|
|
|
VStack {
|
|
Spacer()
|
|
Button {
|
|
showSubscriptionStore = true
|
|
} label: {
|
|
Text(String(localized: "subscription_required_button"))
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink))
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showSubscriptionStore) {
|
|
FeelsSubscriptionStoreView()
|
|
}
|
|
.background(
|
|
theme.currentTheme.bg
|
|
.edgesIgnoringSafeArea(.all)
|
|
)
|
|
.onAppear {
|
|
EventLogger.log(event: "show_insights_view")
|
|
viewModel.generateInsights()
|
|
}
|
|
.padding(.top)
|
|
}
|
|
}
|
|
|
|
// MARK: - Insights Section View
|
|
struct InsightsSectionView: View {
|
|
let title: String
|
|
let icon: String
|
|
let insights: [Insight]
|
|
let textColor: Color
|
|
let moodTint: MoodTints
|
|
let imagePack: MoodImages
|
|
let theme: Theme
|
|
|
|
@State private var isExpanded = true
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Section Header
|
|
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } }) {
|
|
HStack {
|
|
Image(systemName: icon)
|
|
.font(.title3)
|
|
.foregroundColor(textColor.opacity(0.7))
|
|
|
|
Text(title)
|
|
.font(.title2.bold())
|
|
.foregroundColor(textColor)
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundColor(textColor.opacity(0.5))
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 14)
|
|
}
|
|
.buttonStyle(.plain)
|
|
|
|
// Insights List (collapsible)
|
|
if isExpanded {
|
|
VStack(spacing: 12) {
|
|
ForEach(insights) { insight in
|
|
InsightCardView(
|
|
insight: insight,
|
|
textColor: textColor,
|
|
moodTint: moodTint,
|
|
imagePack: imagePack
|
|
)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.bottom, 16)
|
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
|
}
|
|
}
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(theme.currentTheme.secondaryBGColor)
|
|
)
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
|
|
// MARK: - Insight Card View
|
|
struct InsightCardView: View {
|
|
let insight: Insight
|
|
let textColor: Color
|
|
let moodTint: MoodTints
|
|
let imagePack: MoodImages
|
|
|
|
private var accentColor: Color {
|
|
if let mood = insight.mood {
|
|
return moodTint.color(forMood: mood)
|
|
}
|
|
return textColor.opacity(0.6)
|
|
}
|
|
|
|
var body: some View {
|
|
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(.system(size: 18, 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(accentColor.opacity(0.08))
|
|
)
|
|
}
|
|
}
|
|
|
|
struct InsightsView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
InsightsView()
|
|
.environmentObject(IAPManager())
|
|
}
|
|
}
|