Add social media share functionality to Month and Year views
- Add share button to MonthCard and YearCard headers - Create social media-friendly "Monthly Mood Wrap" and "Year in Review" designs - Show top mood, days tracked, and mood breakdown with colorful bar charts - Add ImageOnlyShareSheet to share images without extra text - Uses user's selected theme colors and icon pack 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ struct YearView: View {
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
@StateObject public var viewModel: YearViewModel
|
||||
@StateObject private var filteredDays = DaysFilterClass.shared
|
||||
@StateObject private var shareImage = StupidAssShareObservableObject()
|
||||
@State private var trialWarningHidden = false
|
||||
@State private var showSubscriptionStore = false
|
||||
|
||||
@@ -46,7 +47,11 @@ struct YearView: View {
|
||||
imagePack: imagePack,
|
||||
textColor: textColor,
|
||||
theme: theme,
|
||||
filteredDays: filteredDays.currentFilters
|
||||
filteredDays: filteredDays.currentFilters,
|
||||
onShare: { image in
|
||||
shareImage.fuckingWrappedShrable = image
|
||||
shareImage.showFuckingSheet = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -95,6 +100,11 @@ struct YearView: View {
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
}
|
||||
.sheet(isPresented: $shareImage.showFuckingSheet) {
|
||||
if let uiImage = shareImage.fuckingWrappedShrable {
|
||||
ImageOnlyShareSheet(photo: uiImage)
|
||||
}
|
||||
}
|
||||
.onAppear(perform: {
|
||||
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
|
||||
})
|
||||
@@ -120,6 +130,7 @@ struct YearCard: View {
|
||||
let textColor: Color
|
||||
let theme: Theme
|
||||
let filteredDays: [Int]
|
||||
let onShare: (UIImage) -> Void
|
||||
|
||||
@State private var showStats = true
|
||||
|
||||
@@ -140,30 +151,147 @@ struct YearCard: View {
|
||||
yearEntries.filter { ![Mood.missing, Mood.placeholder].contains($0.mood) }.count
|
||||
}
|
||||
|
||||
private var topMood: Mood? {
|
||||
metrics.filter { $0.total > 0 }.max(by: { $0.total < $1.total })?.mood
|
||||
}
|
||||
|
||||
private var shareableView: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Header with year
|
||||
Text(String(year))
|
||||
.font(.system(size: 48, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(textColor)
|
||||
.padding(.top, 40)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Year in Review")
|
||||
.font(.system(size: 18, weight: .medium, design: .rounded))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
.padding(.bottom, 30)
|
||||
|
||||
// Top mood highlight
|
||||
if let topMood = topMood {
|
||||
VStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: topMood))
|
||||
.frame(width: 120, height: 120)
|
||||
.overlay(
|
||||
imagePack.icon(forMood: topMood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.white)
|
||||
.padding(28)
|
||||
)
|
||||
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 25, x: 0, y: 12)
|
||||
|
||||
Text("Top Mood")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
|
||||
Text(topMood.strValue.uppercased())
|
||||
.font(.system(size: 24, weight: .bold, design: .rounded))
|
||||
.foregroundColor(moodTint.color(forMood: topMood))
|
||||
}
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
|
||||
// Stats row
|
||||
HStack(spacing: 0) {
|
||||
VStack(spacing: 4) {
|
||||
Text("\(totalEntries)")
|
||||
.font(.system(size: 42, weight: .bold, design: .rounded))
|
||||
.foregroundColor(textColor)
|
||||
Text("Days Tracked")
|
||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.bottom, 30)
|
||||
|
||||
// Mood breakdown with bars
|
||||
VStack(spacing: 14) {
|
||||
ForEach(metrics.filter { $0.total > 0 }.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })) { metric in
|
||||
HStack(spacing: 14) {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: metric.mood))
|
||||
.frame(width: 36, height: 36)
|
||||
.overlay(
|
||||
imagePack.icon(forMood: metric.mood)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(.white)
|
||||
.padding(8)
|
||||
)
|
||||
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(moodTint.color(forMood: metric.mood))
|
||||
.frame(width: max(8, geo.size.width * CGFloat(metric.percent / 100)))
|
||||
}
|
||||
}
|
||||
.frame(height: 16)
|
||||
|
||||
Text("\(Int(metric.percent))%")
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(textColor)
|
||||
.frame(width: 45, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
.padding(.bottom, 40)
|
||||
|
||||
// App branding
|
||||
Text("ifeel")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundColor(textColor.opacity(0.3))
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.frame(width: 400)
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Year Header
|
||||
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) {
|
||||
HStack {
|
||||
Text(String(year))
|
||||
.font(.title2.bold())
|
||||
.foregroundColor(textColor)
|
||||
HStack {
|
||||
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) {
|
||||
HStack {
|
||||
Text(String(year))
|
||||
.font(.title2.bold())
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Spacer()
|
||||
Text("\(totalEntries) days")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
Text("\(totalEntries) days")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
|
||||
Image(systemName: showStats ? "chevron.up" : "chevron.down")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.padding(.leading, 4)
|
||||
Image(systemName: showStats ? "chevron.up" : "chevron.down")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(textColor.opacity(0.5))
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
let image = shareableView.asImage(size: CGSize(width: 400, height: 750))
|
||||
onShare(image)
|
||||
}) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(textColor.opacity(0.6))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
// Stats Section (collapsible)
|
||||
if showStats {
|
||||
|
||||
Reference in New Issue
Block a user