Add Insights tab with 60+ randomized analytics

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>
This commit is contained in:
Trey t
2025-12-10 00:11:07 -06:00
parent 1d77adf84e
commit f37b811ab3
6 changed files with 1335 additions and 10 deletions

View File

@@ -34,8 +34,9 @@ struct FeelsApp: App {
// build these here so when tints and other things get updated the views / their data dont
// have to get redrawn#imageLiteral(resourceName: "simulator_screenshot_0017B4DC-100B-42A3-A406-9019704AE275.png")
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
yearView: YearView(viewModel: YearViewModel()),
insightsView: InsightsView(),
customizeView: CustomizeView())
.environment(\.managedObjectContext, persistenceController.viewContext)
.environmentObject(iapManager)

View File

@@ -0,0 +1,232 @@
//
// 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())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,25 +19,31 @@ struct MainTabView: View {
let dayView: DayView
let monthView: MonthView
let yearView: YearView
let insightsView: InsightsView
let customizeView: CustomizeView
var body: some View {
return TabView {
dayView
.tabItem {
Label(String(localized: "content_view_tab_main"), systemImage: "list.dash")
}
monthView
.tabItem {
Label(String(localized: "content_view_tab_month"), systemImage: "calendar")
}
yearView
.tabItem {
Label(String(localized: "content_view_tab_filter"), systemImage: "line.3.horizontal.decrease.circle")
}
insightsView
.tabItem {
Label(String(localized: "content_view_tab_insights"), systemImage: "lightbulb.fill")
}
customizeView
.tabItem {
Label(String(localized: "content_view_tab_customize"), systemImage: "pencil")
@@ -81,8 +87,9 @@ struct MainTabView: View {
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView(dayView: DayView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: false)),
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
monthView: MonthView(viewModel: DayViewViewModel(addMonthStartWeekdayPadding: true)),
yearView: YearView(viewModel: YearViewModel()),
insightsView: InsightsView(),
customizeView: CustomizeView())
}
}

View File

@@ -37,6 +37,7 @@
"content_view_tab_main" = "Day";
"content_view_tab_month" = "Month";
"content_view_tab_filter" = "Year";
"content_view_tab_insights" = "Insights";
"content_view_tab_customize" = "Customize";
"content_view_fill_in_missing_entry" = "Update %@";
"content_view_fill_in_missing_entry_cancel" = "Cancel";

View File

@@ -34,10 +34,11 @@
"add_mood_header_view_title_yesterday" = "How was yesterday?";
"add_mood_header_view_title" = "How was %@?";
"content_view_tab_main" = "Day";
"content_view_tab_month" = "Month";
"content_view_tab_filter" = "Year";
"content_view_tab_customize" = "Customize";
"content_view_tab_main" = "Día";
"content_view_tab_month" = "Mes";
"content_view_tab_filter" = "Año";
"content_view_tab_insights" = "Análisis";
"content_view_tab_customize" = "Personalizar";
"content_view_fill_in_missing_entry" = "Update %@";
"content_view_fill_in_missing_entry_cancel" = "Cancel";
"content_view_delete_entry" = "Delete this rating";