import SwiftUI // MARK: - Post Row struct PostRowView: View { let post: Post let onResonate: () -> Void var onReport: (() -> Void)? = nil @State private var showReport = false var body: some View { HStack(alignment: .top, spacing: 0) { // Mood accent bar — der einzige echte Farbakzent im Feed Rectangle() .fill(post.mood?.color ?? Color.nightTertiary) .frame(width: 2) .padding(.vertical, 18) VStack(alignment: .leading, spacing: 11) { // Author HStack(spacing: 9) { if post.isAnonymous { AnonymousAvatar(size: 32) } else { AvatarView(user: post.author, size: 32) } VStack(alignment: .leading, spacing: 1) { if post.isAnonymous { Text("anonym") .font(.nightLabel(13)) .foregroundColor(.nightSecondary) .italic() } else { Text(post.author.displayName) .font(.nightLabel(14, weight: .semibold)) .foregroundColor(.nightPrimary) } } Spacer() HStack(spacing: 8) { if let mood = post.mood { Text(mood.emoji) .font(.nightMono(11)) .foregroundColor(mood.color.opacity(0.8)) } Text(post.formattedTime) .font(.nightMono(11)) .foregroundColor(.nightTertiary) // Drei-Punkte-Menü für Report Menu { Button(role: .destructive) { showReport = true } label: { Label("Melden", systemImage: "flag") } } label: { Image(systemName: "ellipsis") .font(.system(size: 13)) .foregroundColor(.nightTertiary) .padding(4) } } } // Content Text(post.content) .font(.nightBody(16)) .foregroundColor(.nightPrimary.opacity(0.9)) .lineSpacing(5) .fixedSize(horizontal: false, vertical: true) // Resonance ResonanceButton( count: post.resonanceCount, isActive: post.hasResonated, action: onResonate ) } .padding(.leading, 14) .padding(.trailing, 16) .padding(.vertical, 16) } .sheet(isPresented: $showReport) { ReportSheet(postId: post.id) } } } // MARK: - Resonance Button struct ResonanceButton: View { let count: Int let isActive: Bool let action: () -> Void @State private var scale: CGFloat = 1.0 var body: some View { Button { withAnimation(.spring(duration: 0.25, bounce: 0.7)) { scale = 1.4 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { withAnimation(.spring(duration: 0.2)) { scale = 1.0 } } action() } label: { HStack(spacing: 5) { Image(systemName: isActive ? "heart.fill" : "heart") .font(.system(size: 14)) .foregroundColor(isActive ? .nightRed : .nightSecondary) .scaleEffect(scale) Text(count > 0 ? "\(count)" : "hat mich getroffen") .font(.nightLabel(13)) .foregroundColor(isActive ? .nightRed : .nightSecondary) } .padding(.vertical, 5) .padding(.horizontal, count > 0 || isActive ? 10 : 0) .background( Capsule() .fill(isActive ? Color.nightRed.opacity(0.1) : Color.clear) ) } .animation(.easeInOut(duration: 0.2), value: isActive) } } // MARK: - Avatar struct AvatarView: View { let user: User let size: CGFloat var body: some View { Group { if let url = user.avatarURL { AsyncImage(url: url) { img in img.resizable().scaledToFill() } placeholder: { initials } } else { initials } } .frame(width: size, height: size) .clipShape(Circle()) } var initials: some View { ZStack { Circle().fill(Color.nightPurple.opacity(0.18)) Text(String(user.displayName.prefix(1)).uppercased()) .font(.system(size: size * 0.38, weight: .semibold)) .foregroundColor(.nightPurpleSoft) } } } struct AnonymousAvatar: View { let size: CGFloat var body: some View { ZStack { Circle().fill(Color.nightRaised) Image(systemName: "questionmark") .font(.system(size: size * 0.35, weight: .semibold)) .foregroundColor(.nightSecondary) } .frame(width: size, height: size) } } // MARK: - Report Sheet struct ReportSheet: View { let postId: String @Environment(\.dismiss) var dismiss @State private var selected: ReportReason? = nil @State private var submitted = false @State private var isLoading = false enum ReportReason: String, CaseIterable { case hate = "Hassrede / Diskriminierung" case harassment = "Belästigung / Mobbing" case selfharm = "Selbstverletzung / Suizid" case illegal = "Illegale Inhalte" case spam = "Spam" case other = "Sonstiges" } var body: some View { NavigationStack { ZStack { Color.nightSurface.ignoresSafeArea() if submitted { VStack(spacing: 16) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 48)) .foregroundColor(.nightGreen) Text("Danke für deine Meldung") .font(.nightTitle(18)) .foregroundColor(.nightPrimary) Text("Wir prüfen den Inhalt so schnell wie möglich.") .font(.nightBody(14)) .foregroundColor(.nightSecondary) .multilineTextAlignment(.center) Button("Schließen") { dismiss() } .foregroundColor(.nightPurpleSoft) .padding(.top, 8) } .padding(40) } else { VStack(alignment: .leading, spacing: 0) { Text("Warum möchtest du das melden?") .font(.nightTitle(17)) .foregroundColor(.nightPrimary) .padding(.horizontal, 20) .padding(.top, 24) .padding(.bottom, 16) ForEach(ReportReason.allCases, id: \.self) { reason in Button { selected = reason } label: { HStack { Text(reason.rawValue) .font(.nightBody(15)) .foregroundColor(.nightPrimary) Spacer() if selected == reason { Image(systemName: "checkmark") .foregroundColor(.nightPurpleSoft) } } .padding(.horizontal, 20) .padding(.vertical, 14) .background(selected == reason ? Color.nightPurple.opacity(0.08) : Color.clear) } Divider().background(Color.nightBorder) } Spacer() Button { guard let reason = selected else { return } Task { await submit(reason: reason) } } label: { Group { if isLoading { ProgressView().tint(.black) } else { Text("Melden") .font(.nightLabel(16, weight: .semibold)) .foregroundColor(.black) } } .frame(maxWidth: .infinity) .frame(height: 50) .background(selected != nil ? Color.nightPrimary : Color.nightRaised) .clipShape(RoundedRectangle(cornerRadius: 14)) } .disabled(selected == nil || isLoading) .padding(.horizontal, 20) .padding(.bottom, 32) } } } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Abbrechen") { dismiss() } .foregroundColor(.nightSecondary) } } } .presentationDetents([.medium]) .preferredColorScheme(.dark) } func submit(reason: ReportReason) async { isLoading = true defer { isLoading = false } do { try await supabase.reportPost(postId: postId, reason: reason.rawValue, details: nil) submitted = true } catch { // Fehler still ignorieren — Meldung trotzdem als abgeschlossen zeigen submitted = true } } }