import SwiftUI struct OnboardingView: View { @EnvironmentObject var appState: AppState @State private var phase: Phase = .welcome @State private var isLogin = false enum Phase { case welcome, auth } var body: some View { ZStack { Color.nightBase.ignoresSafeArea() StarField() VStack(spacing: 0) { Spacer() switch phase { case .welcome: WelcomeScreen() .transition(.opacity) Spacer() WelcomeActions( onStart: { isLogin = false withAnimation(.spring(duration: 0.4)) { phase = .auth } }, onLogin: { isLogin = true withAnimation(.spring(duration: 0.4)) { phase = .auth } } ) case .auth: AuthScreen(isLogin: $isLogin) .environmentObject(appState) .transition(.move(edge: .trailing).combined(with: .opacity)) Spacer() } } } .preferredColorScheme(.dark) } } // MARK: - Welcome struct WelcomeScreen: View { @State private var appeared = false var body: some View { VStack(spacing: 28) { ZStack { ForEach([130, 100, 70], id: \.self) { size in Circle() .fill(Color.nightPurple.opacity(size == 70 ? 0 : (size == 100 ? 0.08 : 0.04))) .frame(width: CGFloat(size), height: CGFloat(size)) } Text("◐") .font(.system(size: 52)) .foregroundColor(.nightPurpleSoft) } .scaleEffect(appeared ? 1 : 0.75) .opacity(appeared ? 1 : 0) VStack(spacing: 12) { Text("nightly") .font(.system(size: 44, weight: .bold, design: .rounded)) .foregroundColor(.nightPrimary) VStack(spacing: 5) { Text("Zwischen 2 und 5 Uhr.") Text("Kein Filter. Keine Maske.") Text("Nur echte Gedanken.") } .font(.nightBody(17)) .foregroundColor(.nightSecondary) .multilineTextAlignment(.center) .lineSpacing(3) } .opacity(appeared ? 1 : 0) .offset(y: appeared ? 0 : 10) } .onAppear { withAnimation(.spring(duration: 0.8, bounce: 0.25).delay(0.1)) { appeared = true } } } } struct WelcomeActions: View { let onStart: () -> Void let onLogin: () -> Void var body: some View { VStack(spacing: 12) { Button("loslegen", action: onStart).buttonStyle(NightlyPrimaryButton()) Button("ich hab schon einen account", action: onLogin) .font(.nightLabel(14)) .foregroundColor(.nightSecondary) } .padding(.horizontal, 24) .padding(.bottom, 52) } } // MARK: - Auth Screen struct AuthScreen: View { @EnvironmentObject var appState: AppState @Binding var isLogin: Bool // Registrierung @State private var username = "" @State private var displayName = "" @State private var email = "" @State private var password = "" @State private var isLoading = false @State private var error: String? var body: some View { VStack(spacing: 22) { Text(isLogin ? "willkommen zurück" : "mitmachen") .font(.nightTitle(28)) .foregroundColor(.nightPrimary) VStack(spacing: 10) { if !isLogin { NightlyField(text: $displayName, placeholder: "anzeigename", icon: "person") NightlyField(text: $username, placeholder: "benutzername", icon: "at") .textInputAutocapitalization(.never).autocorrectionDisabled() } NightlyField(text: $email, placeholder: "e-mail", icon: "envelope") .textInputAutocapitalization(.never) .keyboardType(.emailAddress) .autocorrectionDisabled() NightlyField(text: $password, placeholder: "passwort", icon: "lock", isSecure: true) } .padding(.horizontal, 24) if let err = error { Text(err) .font(.nightLabel(13)) .foregroundColor(.nightRed) .multilineTextAlignment(.center) .padding(.horizontal, 24) } Button(isLogin ? "einloggen" : "account erstellen") { Task { await submit() } } .buttonStyle(NightlyPrimaryButton(isLoading: isLoading)) .disabled(isLoading) .padding(.horizontal, 24) Button(isLogin ? "noch kein account?" : "schon dabei?") { withAnimation { isLogin.toggle() } } .font(.nightLabel(14)) .foregroundColor(.nightSecondary) // Rechtliches LegalNotice() } } func submit() async { guard !email.isEmpty && !password.isEmpty else { error = "Bitte alle Felder ausfüllen." return } isLoading = true error = nil defer { isLoading = false } do { if isLogin { try await appState.signIn(email: email, password: password) } else { guard !username.isEmpty && !displayName.isEmpty else { error = "Bitte alle Felder ausfüllen." return } guard username.count >= 3 else { error = "Benutzername muss mindestens 3 Zeichen haben." return } guard password.count >= 8 else { error = "Passwort muss mindestens 8 Zeichen haben." return } try await appState.signUp( email: email, password: password, username: username.lowercased(), displayName: displayName ) } } catch { self.error = error.localizedDescription } } } struct LegalNotice: View { @State private var showLegal = false var body: some View { VStack(spacing: 4) { Text("Mit der Registrierung stimmst du zu:") .font(.nightLabel(11)) .foregroundColor(.nightTertiary) HStack(spacing: 4) { Button("Nutzungsbedingungen") { showLegal = true } Text("·") Button("Datenschutzerklärung") { showLegal = true } } .font(.nightLabel(11, weight: .medium)) .foregroundColor(.nightSecondary) } .multilineTextAlignment(.center) .sheet(isPresented: $showLegal) { LegalView() } } } // MARK: - Reusable components struct NightlyField: View { @Binding var text: String let placeholder: String let icon: String var isSecure = false var body: some View { HStack(spacing: 12) { Image(systemName: icon) .font(.system(size: 15)) .foregroundColor(.nightSecondary) .frame(width: 18) Group { if isSecure { SecureField(placeholder, text: $text) } else { TextField(placeholder, text: $text) } } .font(.nightBody(16)) .foregroundColor(.nightPrimary) } .padding(16) .background(Color.nightSurface) .clipShape(RoundedRectangle(cornerRadius: 12)) .overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(Color.nightBorder, lineWidth: 1)) .tint(.nightPurpleSoft) } } struct NightlyPrimaryButton: ButtonStyle { var isLoading = false func makeBody(configuration: Configuration) -> some View { Group { if isLoading { ProgressView().tint(.nightBase).frame(maxWidth: .infinity, maxHeight: 52) } else { configuration.label .font(.nightLabel(17, weight: .semibold)) .foregroundColor(.nightBase) .frame(maxWidth: .infinity).frame(height: 52) } } .background(Color.nightPrimary.opacity(configuration.isPressed ? 0.85 : 1)) .clipShape(RoundedRectangle(cornerRadius: 14)) .scaleEffect(configuration.isPressed ? 0.98 : 1) .animation(.spring(duration: 0.2), value: configuration.isPressed) } } struct StarField: View { struct Star: Identifiable { let id: Int; let x, y, size, opacity: CGFloat } private let stars: [Star] = (0..<120).map { Star(id: $0, x: .random(in: 0...1), y: .random(in: 0...1), size: .random(in: 1...2.5), opacity: .random(in: 0.07...0.3)) } @State private var twinkle = false var body: some View { GeometryReader { geo in ForEach(stars) { s in Circle().fill(Color.white) .frame(width: s.size, height: s.size) .opacity(twinkle && s.id % 5 == 0 ? s.opacity * 0.3 : s.opacity) .position(x: s.x * geo.size.width, y: s.y * geo.size.height) } } .ignoresSafeArea() .onAppear { withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) { twinkle = true } } } }