import SwiftUI // MARK: - Feed struct FeedView: View { @StateObject private var viewModel = FeedViewModel() @EnvironmentObject var appState: AppState var realtime: RealtimeService? = nil // Gerade Jetzt = posts younger than 10 minutes var rightNowPosts: [Post] { viewModel.posts.filter { $0.isRightNow } } var nightPosts: [Post] { viewModel.posts } var body: some View { NavigationStack { ZStack { Color.nightBase.ignoresSafeArea() if viewModel.posts.isEmpty && !viewModel.isLoading { EmptyNightView(windowState: appState.windowState) } else { ScrollView { LazyVStack(spacing: 0) { // ── GERADE JETZT ────────────────────────────────── // Nur sichtbar wenn: Fenster offen ODER du hast gerade gepostet // UND es gibt Leute die gleichzeitig posten if !rightNowPosts.isEmpty && appState.windowState == .posted { RightNowSection( posts: rightNowPosts, onResonate: { post in Task { await viewModel.resonate(post) } } ) .padding(.bottom, 2) } // ── TRENNLINIE MIT KONTEXT ────────────────────── NightContextBar( windowState: appState.windowState, totalCount: nightPosts.count, liveCount: rightNowPosts.count ) // ── HEUTE NACHT ──────────────────────────────── // Alle Posts dieser Nacht, chronologisch ForEach(nightPosts) { post in PostRowView(post: post) { Task { await viewModel.resonate(post) } } Divider() .background(Color.nightBorder) .padding(.leading, 16) } if viewModel.isLoading { ProgressView() .tint(.nightPurple) .padding(40) } Color.clear.frame(height: 100) } } .refreshable { await viewModel.load() } } } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { NightlyWordmark() } } } .task { await viewModel.load() if let realtime { await realtime.startListening { [weak viewModel] post in viewModel?.prepend(post) } } } } } // MARK: - Wordmark struct NightlyWordmark: View { var body: some View { HStack(spacing: 7) { Text("◐") .font(.system(size: 15)) .foregroundColor(.nightPurple) Text("nightly") .font(.system(size: 17, weight: .bold, design: .rounded)) .foregroundColor(.nightPrimary) } } } // MARK: - Gerade Jetzt Section // // WANN ERSCHEINT DAS? // → Du hast in den letzten 10 Minuten gepostet // → Und mindestens eine andere Person auch // // WAS IST DER UNTERSCHIED ZU "HEUTE NACHT"? // → Gerade Jetzt = buchstäblich gerade, gleichzeitig, diese Minute // → Heute Nacht = alle Posts seit dem Öffnen des Fensters // // ANALOGIE: Gerade Jetzt = du bist gerade im selben Raum wie jemand. // Heute Nacht = der gesamte Raum-Verlauf dieser Nacht. struct RightNowSection: View { let posts: [Post] let onResonate: (Post) -> Void @State private var pulse = false var body: some View { VStack(alignment: .leading, spacing: 0) { // Header HStack(spacing: 10) { // Pulsierender grüner Punkt = LIVE ZStack { Circle() .fill(Color.nightGreen.opacity(0.25)) .frame(width: 16, height: 16) .scaleEffect(pulse ? 1.8 : 1.0) .opacity(pulse ? 0 : 1) Circle() .fill(Color.nightGreen) .frame(width: 7, height: 7) } .onAppear { withAnimation(.easeInOut(duration: 1.4).repeatForever(autoreverses: false)) { pulse = true } } Text("gerade jetzt") .font(.nightLabel(12, weight: .semibold)) .foregroundColor(.nightGreen) .kerning(0.8) Text("· \(posts.count) \(posts.count == 1 ? "Person" : "Personen") gleichzeitig wach") .font(.nightLabel(12)) .foregroundColor(.nightSecondary) Spacer() // Info-Tooltip HelpTooltip( text: "Leute die in den letzten 10 Minuten gepostet haben — ihr seid buchstäblich gleichzeitig wach." ) } .padding(.horizontal, 16) .padding(.top, 16) .padding(.bottom, 12) // Horizontal Cards ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(posts) { post in RightNowCard(post: post, onResonate: { onResonate(post) }) } } .padding(.horizontal, 16) .padding(.bottom, 16) } } .background( ZStack { Color.nightSurface Color.nightGreen.opacity(0.025) } ) .overlay( Rectangle() .fill(Color.nightGreen.opacity(0.15)) .frame(height: 1), alignment: .bottom ) } } struct RightNowCard: View { let post: Post let onResonate: () -> Void var body: some View { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 8) { if post.isAnonymous { AnonymousAvatar(size: 26) } else { AvatarView(user: post.author, size: 26) } Text(post.isAnonymous ? "anonym" : "@\(post.author.username)") .font(.nightLabel(12)) .foregroundColor(post.isAnonymous ? .nightSecondary : .nightPrimary) .lineLimit(1) Spacer() if let mood = post.mood { Text(mood.emoji) .font(.nightMono(11)) .foregroundColor(mood.color.opacity(0.7)) } } Text(post.content) .font(.nightBody(14)) .foregroundColor(.nightPrimary.opacity(0.88)) .lineSpacing(4) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) Spacer(minLength: 0) Button(action: onResonate) { HStack(spacing: 4) { Image(systemName: post.hasResonated ? "heart.fill" : "heart") .font(.system(size: 12)) .foregroundColor(post.hasResonated ? .nightRed : .nightSecondary) if post.resonanceCount > 0 { Text("\(post.resonanceCount)") .font(.nightLabel(11)) .foregroundColor(.nightSecondary) } } } } .padding(14) .frame(width: 210, height: 148) .background( ZStack { RoundedRectangle(cornerRadius: 14) .fill(Color.nightRaised) if let mood = post.mood { RoundedRectangle(cornerRadius: 14) .fill( LinearGradient( colors: [mood.color.opacity(0.07), .clear], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } RoundedRectangle(cornerRadius: 14) .strokeBorder(Color.nightGreen.opacity(0.18), lineWidth: 1) } ) } } // MARK: - Context Bar (der Übergang zwischen Gerade Jetzt und Heute Nacht) struct NightContextBar: View { let windowState: AppState.WindowState let totalCount: Int let liveCount: Int var body: some View { HStack { VStack(alignment: .leading, spacing: 3) { HStack(spacing: 6) { Text("heute nacht") .font(.nightLabel(12, weight: .semibold)) .foregroundColor(.nightSecondary) .kerning(0.5) if totalCount > 0 { Text("· \(totalCount) Gedanken") .font(.nightLabel(12)) .foregroundColor(.nightTertiary) } } Text(statusSubtitle) .font(.nightLabel(11)) .foregroundColor(.nightTertiary) } Spacer() // Window status pill HStack(spacing: 5) { Circle() .fill(windowState == .open ? Color.nightGreen : Color.nightTertiary) .frame(width: 6, height: 6) Text(windowState == .open ? "offen" : "geschlossen") .font(.nightLabel(11)) .foregroundColor(.nightSecondary) } .padding(.horizontal, 10) .padding(.vertical, 5) .background(Color.nightRaised) .clipShape(Capsule()) } .padding(.horizontal, 16) .padding(.vertical, 12) .overlay( Rectangle() .fill(Color.nightBorder) .frame(height: 1), alignment: .bottom ) } var statusSubtitle: String { switch windowState { case .open: return "Du kannst noch posten — bis 05:00" case .posted: return "Dein Post ist sichtbar bis morgen früh" case .closed: return "Fenster öffnet später heute Nacht" case .missed: return "Nächste Chance: heute Nacht" } } } // MARK: - Help Tooltip struct HelpTooltip: View { let text: String @State private var show = false var body: some View { Button { withAnimation(.spring(duration: 0.3)) { show.toggle() } DispatchQueue.main.asyncAfter(deadline: .now() + 3) { withAnimation { show = false } } } label: { Image(systemName: "info.circle") .font(.system(size: 13)) .foregroundColor(.nightTertiary) } .overlay(alignment: .topTrailing) { if show { Text(text) .font(.nightBody(12)) .foregroundColor(.nightPrimary) .padding(10) .background( RoundedRectangle(cornerRadius: 10) .fill(Color.nightRaised) .shadow(color: .black.opacity(0.4), radius: 8, y: 4) ) .frame(width: 200) .offset(x: -160, y: 28) .transition(.opacity.combined(with: .scale(scale: 0.9, anchor: .topTrailing))) .zIndex(100) } } } } // MARK: - Empty State struct EmptyNightView: View { let windowState: AppState.WindowState var body: some View { VStack(spacing: 20) { Text("◐") .font(.system(size: 52)) .foregroundColor(.nightPurple.opacity(0.4)) VStack(spacing: 8) { Text(windowState == .open ? "sei der erste heute nacht" : "noch ruhig hier") .font(.nightTitle(19)) .foregroundColor(.nightPrimary) Text(windowState == .open ? "Das Fenster ist offen.\nPoste einen Gedanken — andere sind auch wach." : "Wenn dein Fenster öffnet, kannst du posten.\nErst dann siehst du alle anderen." ) .font(.nightBody(15)) .foregroundColor(.nightSecondary) .multilineTextAlignment(.center) .lineSpacing(4) } } .padding(40) } }