import SwiftUI /// Persönliches Tagebuch — alle eigenen Posts, auch die anonymen. /// Bleibt für immer. Das ist der Retention-Hook. struct DiaryView: View { @StateObject private var viewModel = DiaryViewModel() @EnvironmentObject var appState: AppState var body: some View { NavigationStack { ZStack { Color.nightBase.ignoresSafeArea() if viewModel.groupedPosts.isEmpty && !viewModel.isLoading { DiaryEmptyView() } else { ScrollView { LazyVStack(alignment: .leading, spacing: 0) { // "Vor genau einem Jahr" — Memory if let memory = viewModel.yearAgoPost { MemoryBanner(post: memory) .padding(.bottom, 4) } ForEach(viewModel.groupedPosts, id: \.nightLabel) { group in // Nacht-Header DiaryNightHeader(label: group.nightLabel, count: group.posts.count) ForEach(group.posts) { post in DiaryPostRow(post: post) { Task { await viewModel.deletePost(post) } } Divider().background(Color.nightBorder).padding(.leading, 16) } } Color.clear.frame(height: 100) } } .refreshable { await viewModel.load() } } } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { HStack(spacing: 7) { Image(systemName: "book.closed.fill") .foregroundColor(.nightPurple) Text("tagebuch") .font(.nightTitle(17)) .foregroundColor(.nightPrimary) } } } } .task { await viewModel.load() } } } // MARK: - Memory Banner struct MemoryBanner: View { let post: Post @State private var show = true var body: some View { if show { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 8) { Image(systemName: "clock.arrow.circlepath") .foregroundColor(.nightPurpleSoft) .font(.system(size: 14)) Text("vor genau einem jahr") .font(.nightLabel(12, weight: .semibold)) .foregroundColor(.nightPurpleSoft) .kerning(0.5) Spacer() Button { withAnimation { show = false } } label: { Image(systemName: "xmark") .font(.system(size: 12)) .foregroundColor(.nightTertiary) } } Text(post.content) .font(.nightBody(15)) .foregroundColor(.nightPrimary.opacity(0.85)) .lineSpacing(4) HStack(spacing: 5) { if let mood = post.mood { Text(mood.emoji).font(.nightMono(11)) Text(mood.label).font(.nightLabel(11)) } Text("·") Text(post.formattedTime) .font(.nightMono(11)) } .foregroundColor(.nightTertiary) } .padding(16) .background( ZStack { RoundedRectangle(cornerRadius: 0) .fill(Color.nightPurple.opacity(0.06)) RoundedRectangle(cornerRadius: 0) .fill( LinearGradient( colors: [Color.nightPurple.opacity(0.08), .clear], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } ) .overlay( Rectangle().fill(Color.nightBorder).frame(height: 1), alignment: .bottom ) } } } // MARK: - Night Group Header struct DiaryNightHeader: View { let label: String let count: Int var body: some View { HStack { Text(label) .font(.nightLabel(12, weight: .semibold)) .foregroundColor(.nightSecondary) .kerning(0.5) Text("· \(count) Gedanke\(count == 1 ? "" : "n")") .font(.nightLabel(12)) .foregroundColor(.nightTertiary) Spacer() } .padding(.horizontal, 16) .padding(.vertical, 10) .background(Color.nightSurface) } } // MARK: - Diary Post Row struct DiaryPostRow: View { let post: Post let onDelete: () -> Void @State private var confirmDelete = false var body: some View { HStack(alignment: .top, spacing: 0) { Rectangle() .fill(post.mood?.color ?? Color.nightTertiary) .frame(width: 2) .padding(.vertical, 14) VStack(alignment: .leading, spacing: 8) { HStack { Text(post.formattedTime) .font(.nightMono(12)) .foregroundColor(.nightTertiary) if post.isAnonymous { Text("· anonym") .font(.nightLabel(11)) .foregroundColor(.nightTertiary) .italic() } if let mood = post.mood { Text("· \(mood.emoji) \(mood.label)") .font(.nightLabel(11)) .foregroundColor(mood.color.opacity(0.7)) } Spacer() if post.resonanceCount > 0 { HStack(spacing: 3) { Image(systemName: "heart.fill") .font(.system(size: 10)) Text("\(post.resonanceCount)") .font(.nightLabel(11)) } .foregroundColor(.nightRed.opacity(0.7)) } } Text(post.content) .font(.nightBody(16)) .foregroundColor(.nightPrimary.opacity(0.88)) .lineSpacing(5) .fixedSize(horizontal: false, vertical: true) } .padding(.horizontal, 14) .padding(.vertical, 14) .contextMenu { Button(role: .destructive) { confirmDelete = true } label: { Label("Löschen", systemImage: "trash") } } } .confirmationDialog( "Post löschen?", isPresented: $confirmDelete, titleVisibility: .visible ) { Button("Löschen", role: .destructive) { onDelete() } } message: { Text("Der Post wird aus dem Feed entfernt, bleibt aber in deinem Tagebuch-Archiv.") } } } // MARK: - Empty State struct DiaryEmptyView: View { var body: some View { VStack(spacing: 18) { Image(systemName: "book.closed") .font(.system(size: 48)) .foregroundColor(.nightTertiary) Text("noch nichts hier") .font(.nightTitle(18)) .foregroundColor(.nightPrimary) Text("Deine Posts landen hier.\nIn einem Jahr kannst du nachlesen, was dich\nmitten in der Nacht beschäftigt hat.") .font(.nightBody(15)) .foregroundColor(.nightSecondary) .multilineTextAlignment(.center) .lineSpacing(4) } .padding(40) } } // MARK: - ViewModel @MainActor class DiaryViewModel: ObservableObject { struct NightGroup { let nightLabel: String; let posts: [Post] } @Published var groupedPosts: [NightGroup] = [] @Published var yearAgoPost: Post? = nil @Published var isLoading = false func load() async { isLoading = true defer { isLoading = false } do { let posts = try await supabase.getDiary() // Gruppiere nach Nacht (Datum - 2h, damit 2–5 Uhr zur selben Nacht gehört) let grouped = Dictionary(grouping: posts) { post -> String in let nightDate = post.createdAt.addingTimeInterval(-2 * 3600) let f = DateFormatter() f.locale = Locale(identifier: "de_DE") f.dateFormat = "EEEE, d. MMMM yyyy" return f.string(from: nightDate) } groupedPosts = grouped .sorted { $0.key > $1.key } .map { NightGroup(nightLabel: $0.key, posts: $0.value) } // Memory: Post von vor genau einem Jahr let oneYearAgo = Calendar.current.date(byAdding: .year, value: -1, to: Date())! yearAgoPost = posts.first { post in Calendar.current.isDate(post.createdAt, equalTo: oneYearAgo, toGranularity: .day) } } catch { #if DEBUG groupedPosts = [NightGroup(nightLabel: "gestern", posts: Post.previews)] #endif } } func deletePost(_ post: Post) async { try? await supabase.softDeletePost(id: post.id) await load() } }