import SwiftUI struct ComposeView: View { @EnvironmentObject var appState: AppState @Environment(\.dismiss) var dismiss @State private var text = "" @State private var selectedMood: Mood? = nil @State private var isAnonymous = false @State private var isPosting = false @State private var errorMessage: String? private let maxChars = 280 var remaining: Int { maxChars - text.count } var canPost: Bool { !text.trimmingCharacters(in: .whitespaces).isEmpty && selectedMood != nil } // Background tint based on mood var moodBackground: Color { selectedMood?.color.opacity(0.06) ?? .clear } var body: some View { NavigationStack { ZStack { Color.nightBase.ignoresSafeArea() moodBackground.ignoresSafeArea() .animation(.easeInOut(duration: 0.5), value: selectedMood) VStack(spacing: 0) { // Top meta bar HStack { Label(currentTime, systemImage: "moon.stars.fill") .font(.nightMono(12)) .foregroundColor(.nightPurple.opacity(0.7)) .labelStyle(.titleAndIcon) Spacer() // Character count Group { if remaining <= 30 { Text("\(remaining)") .foregroundColor(remaining <= 10 ? .nightRed : .nightSecondary) } } .font(.nightMono(13)) .animation(.easeInOut, value: remaining) } .padding(.horizontal, 20) .padding(.vertical, 12) Divider().background(Color.nightBorder) // Text field area ScrollView { HStack(alignment: .top, spacing: 12) { // Left: Avatar VStack(spacing: 0) { if isAnonymous { AnonymousAvatar(size: 38) } else if let user = appState.currentUser { AvatarView(user: user, size: 38) } else { Circle() .fill(Color.nightRaised) .frame(width: 38, height: 38) } // Connector line (visual polish) Rectangle() .fill(Color.nightBorder) .frame(width: 1) .frame(maxHeight: .infinity) .padding(.top, 8) } .frame(width: 38) // Right: Content VStack(alignment: .leading, spacing: 8) { // Name Text(isAnonymous ? "anonym" : (appState.currentUser?.displayName ?? "")) .font(.nightLabel(15, weight: .semibold)) .foregroundColor(isAnonymous ? .nightSecondary : .nightPrimary) .italic(isAnonymous) // TextEditor with placeholder ZStack(alignment: .topLeading) { if text.isEmpty { Text("Was geht dir gerade durch den Kopf?") .font(.nightBody(17)) .foregroundColor(.nightTertiary) .allowsHitTesting(false) .padding(.top, 8) .padding(.leading, 5) } TextEditor(text: $text) .scrollContentBackground(.hidden) .background(.clear) .foregroundColor(.nightPrimary) .font(.nightBody(17)) .lineSpacing(5) .frame(minHeight: 160) .onChange(of: text) { _, new in if new.count > maxChars { text = String(new.prefix(maxChars)) } } } // Mood picker inline MoodPickerRow(selected: $selectedMood) .padding(.top, 4) Spacer().frame(height: 20) } } .padding(.horizontal, 16) .padding(.top, 18) } Spacer() // Bottom bar: anonymous toggle + countdown Divider().background(Color.nightBorder) HStack(spacing: 14) { // Anonymous toggle Button { withAnimation(.spring(duration: 0.3, bounce: 0.4)) { isAnonymous.toggle() } } label: { HStack(spacing: 6) { Image(systemName: isAnonymous ? "theatermasks.fill" : "theatermasks") .font(.system(size: 15)) Text(isAnonymous ? "anonym" : "anonym posten") .font(.nightLabel(13)) } .foregroundColor(isAnonymous ? .nightPrimary : .nightSecondary) .padding(.horizontal, 12) .padding(.vertical, 7) .background(isAnonymous ? Color.nightRaised : .clear) .clipShape(Capsule()) .overlay( Capsule() .strokeBorder( isAnonymous ? Color.nightBorder : .clear, lineWidth: 1 ) ) } Spacer() WindowCountdownView() } .padding(.horizontal, 20) .padding(.vertical, 10) .padding(.bottom, 8) if let err = errorMessage { Text(err) .font(.nightLabel(13)) .foregroundColor(.nightRed) .padding(.horizontal, 20) .padding(.bottom, 8) } } } .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Abbrechen") { dismiss() } .foregroundColor(.nightSecondary) .font(.nightBody(16)) } ToolbarItem(placement: .navigationBarTrailing) { PostButton(canPost: canPost, isPosting: isPosting) { Task { await submit() } } } } } .preferredColorScheme(.dark) } var currentTime: String { let f = DateFormatter(); f.dateFormat = "HH:mm" return f.string(from: Date()) } func submit() async { guard let mood = selectedMood else { return } isPosting = true errorMessage = nil defer { isPosting = false } do { try await APIService.shared.createPost( content: text.trimmingCharacters(in: .whitespacesAndNewlines), mood: mood, isAnonymous: isAnonymous ) appState.markAsPosted() dismiss() } catch { errorMessage = error.localizedDescription } } } // MARK: - Mood Picker struct MoodPickerRow: View { @Binding var selected: Mood? var body: some View { VStack(alignment: .leading, spacing: 10) { Text("stimmung") .font(.nightLabel(11, weight: .medium)) .foregroundColor(.nightTertiary) .kerning(0.8) HStack(spacing: 7) { ForEach(Mood.allCases, id: \.self) { mood in MoodChip(mood: mood, isSelected: selected == mood) { withAnimation(.spring(duration: 0.3, bounce: 0.3)) { selected = selected == mood ? nil : mood } } } } } } } struct MoodChip: View { let mood: Mood let isSelected: Bool let action: () -> Void var body: some View { Button(action: action) { HStack(spacing: 5) { Text(mood.emoji) .font(.nightMono(12)) .foregroundColor(isSelected ? mood.color : .nightSecondary) Text(mood.label) .font(.nightLabel(12, weight: isSelected ? .semibold : .regular)) .foregroundColor(isSelected ? .nightPrimary : .nightSecondary) } .padding(.horizontal, 11) .padding(.vertical, 7) .background( ZStack { if isSelected { Capsule().fill(mood.color.opacity(0.14)) Capsule().strokeBorder(mood.color.opacity(0.4), lineWidth: 1) } else { Capsule().fill(Color.nightRaised) Capsule().strokeBorder(Color.nightBorder, lineWidth: 1) } } ) } } } // MARK: - Post Button struct PostButton: View { let canPost: Bool let isPosting: Bool let action: () -> Void var body: some View { Button(action: action) { Group { if isPosting { ProgressView().tint(.black).frame(width: 20, height: 20) } else { Text("posten") .font(.nightLabel(15, weight: .bold)) .foregroundColor(canPost ? Color.nightBase : .nightTertiary) } } .frame(width: 74, height: 34) .background(canPost ? Color.nightPrimary : Color.nightRaised) .clipShape(Capsule()) } .disabled(!canPost || isPosting) .animation(.easeInOut(duration: 0.2), value: canPost) } } // MARK: - Countdown struct WindowCountdownView: View { @State private var label = "" var body: some View { HStack(spacing: 5) { Image(systemName: "clock") .font(.system(size: 11)) Text(label) .font(.nightMono(11)) } .foregroundColor(.nightPurple.opacity(0.5)) .onAppear { tick() } .onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in tick() } } func tick() { var c = Calendar.current.dateComponents([.year, .month, .day], from: Date()) c.hour = 5; c.minute = 0; c.second = 0 guard let end = Calendar.current.date(from: c) else { return } let diff = max(0, Int(end.timeIntervalSince(Date()))) label = diff > 0 ? String(format: "%d:%02d bis 05:00", diff / 60, diff % 60) : "fenster zu" } }