import SwiftUI struct ProfilesView: View { @EnvironmentObject private var app: AppModel @State private var profiles: [ViewerProfile] = [] @State private var error: String? @State private var pinTarget: ViewerProfile? @State private var pinDigits = "" @State private var pinError: String? @State private var loading = true @State private var selecting = false private let cols = [GridItem(.adaptive(minimum: 200, maximum: 240), spacing: 36)] var body: some View { ZStack { LinearGradient( colors: [Color(red: 0.08, green: 0.02, blue: 0.03), TvColors.bg], startPoint: .top, endPoint: .bottom ) .ignoresSafeArea() VStack(spacing: 40) { Text("Wie kijkt er?") .font(TvFonts.title) .foregroundStyle(TvColors.text) .padding(.top, 60) if loading { ProgressView().tint(.white).scaleEffect(1.5) } else if let error, profiles.isEmpty { Text(error).font(TvFonts.body).foregroundStyle(TvColors.danger) Button("Opnieuw") { Task { await load() } }.buttonStyle(TvPrimaryButtonStyle()) } else { if let error { Text(error).font(TvFonts.caption).foregroundStyle(TvColors.danger) } LazyVGrid(columns: cols, spacing: 40) { ForEach(profiles) { profile in Button { Task { await select(profile) } } label: { VStack(spacing: 16) { Text(ProfileAvatars.emoji(for: profile.avatarId)) .font(.system(size: 72)) .frame(width: 160, height: 160) .background( LinearGradient( colors: [TvColors.surfaceSoft, TvColors.surface], startPoint: .topLeading, endPoint: .bottomTrailing ), in: RoundedRectangle(cornerRadius: 28, style: .continuous) ) .overlay( RoundedRectangle(cornerRadius: 28, style: .continuous) .stroke(TvColors.accent.opacity(0.3), lineWidth: 2) ) Text(profile.name) .font(TvFonts.headline) .foregroundStyle(TvColors.text) .lineLimit(1) if profile.hasPin == true { Image(systemName: "lock.fill") .font(.system(size: 18)) .foregroundStyle(TvColors.textDim) } } } .buttonStyle(FocusableCardStyle()) .disabled(selecting) } } .padding(.horizontal, 64) .focusSection() } Spacer() } if let pinTarget { PinPadOverlay( profileName: pinTarget.name, avatarId: pinTarget.avatarId, digits: $pinDigits, error: pinError, busy: selecting, onCancel: { self.pinTarget = nil pinDigits = "" pinError = nil }, onSubmit: { code in Task { await submitPin(for: pinTarget, code: code) } } ) .transition(.opacity) .zIndex(20) } } .animation(.easeOut(duration: 0.2), value: pinTarget?.id) .task { await load() } } private func load() async { loading = true error = nil do { let payload = try await app.api.listProfiles() profiles = payload.profiles loading = false } catch { self.error = error.localizedDescription loading = false } } private func select(_ profile: ViewerProfile, pin: String? = nil) async { if profile.hasPin == true, pin == nil { pinDigits = "" pinError = nil pinTarget = profile return } selecting = true error = nil do { let selected = try await app.api.selectProfile(profileId: profile.id, pin: pin) pinTarget = nil pinDigits = "" pinError = nil selecting = false app.didSelectProfile(selected) } catch { selecting = false if pinTarget != nil { pinError = error.localizedDescription pinDigits = "" } else { self.error = error.localizedDescription } } } private func submitPin(for profile: ViewerProfile, code: String) async { let digits = code.filter(\.isNumber) guard digits.count == 4 else { pinError = "PIN moet 4 cijfers zijn" return } await select(profile, pin: String(digits)) } } struct PinPadOverlay: View { let profileName: String let avatarId: String? @Binding var digits: String let error: String? let busy: Bool let onCancel: () -> Void let onSubmit: (String) -> Void private let keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "⌫"] var body: some View { ZStack { Color.black.opacity(0.72).ignoresSafeArea() VStack(spacing: 28) { Text(ProfileAvatars.emoji(for: avatarId)) .font(.system(size: 64)) Text("PIN voor \(profileName)") .font(TvFonts.headline) .foregroundStyle(TvColors.text) HStack(spacing: 18) { ForEach(0..<4, id: \.self) { i in Circle() .fill(i < digits.count ? TvColors.accent : TvColors.surfaceSoft) .frame(width: 22, height: 22) .shadow(color: i < digits.count ? TvColors.accent.opacity(0.5) : .clear, radius: 8) } } if let error { Text(error).font(TvFonts.caption).foregroundStyle(TvColors.danger) } LazyVGrid(columns: Array(repeating: GridItem(.fixed(100), spacing: 16), count: 3), spacing: 16) { ForEach(keys, id: \.self) { key in if key.isEmpty { Color.clear.frame(width: 100, height: 72) } else { Button { tap(key) } label: { Text(key) .font(.system(size: 32, weight: .semibold, design: .rounded)) .foregroundStyle(TvColors.text) .frame(width: 100, height: 72) .background(TvColors.surface, in: RoundedRectangle(cornerRadius: 16, style: .continuous)) } .buttonStyle(FocusableCardStyle()) .disabled(busy) } } } .focusSection() Button("Annuleer", action: onCancel) .buttonStyle(TvSecondaryButtonStyle()) if busy { ProgressView().tint(.white) } } .padding(48) .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 28, style: .continuous)) } } private func tap(_ key: String) { if key == "⌫" { if !digits.isEmpty { digits.removeLast() } return } guard digits.count < 4 else { return } digits.append(key) if digits.count == 4 { onSubmit(digits) } } }