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:
@@ -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)
|
||||
|
||||
232
Shared/Views/InsightsView/InsightsView.swift
Normal file
232
Shared/Views/InsightsView/InsightsView.swift
Normal 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())
|
||||
}
|
||||
}
|
||||
1083
Shared/Views/InsightsView/InsightsViewModel.swift
Normal file
1083
Shared/Views/InsightsView/InsightsViewModel.swift
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user