KasztelanKarta/Kasztelan/ProfileView.swift
2026-10-23 01:28:42 +01:00

181 lines
9.3 KiB
Swift

import SwiftUI
struct ProfileView: View {
@EnvironmentObject private var model: AppModel
@State private var editingProfile = false
@State private var changingPassword = false
@State private var deletingAccount = false
@State private var deletePassword = ""
@State private var cardNumber = ""
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text(model.text("Twój profil", "Your profile")).font(.largeTitle.bold())
Spacer()
Button { editingProfile = true } label: {
Image(systemName: "pencil.circle.fill").font(.title).foregroundStyle(KTheme.orange)
}
.accessibilityLabel(model.text("Edytuj profil", "Edit profile"))
}
VStack(alignment: .leading, spacing: 16) {
ProfileValue(label: model.text("Imię i nazwisko", "Name"), value: model.user?.displayName ?? name)
ProfileValue(label: "E-mail", value: model.user?.email ?? "—")
ProfileValue(label: model.text("Telefon", "Phone"), value: model.user?.mobile ?? "—")
ProfileValue(label: model.text("Kod pocztowy", "Postcode"), value: model.user?.postcode ?? "—")
ProfileValue(label: model.text("Data urodzenia", "Date of birth"), value: model.profileForm().dob.nonEmpty ?? "—")
ProfileValue(label: model.text("Numer karty", "Card number"), value: model.cardNumber.nonEmpty ?? "—")
}
.frame(maxWidth: .infinity, alignment: .leading)
.kasztelanPanel()
if model.cardNumber.isEmpty { cardAssignment }
Button(model.text("Zmień hasło", "Change password")) { changingPassword = true }
.buttonStyle(GlassButtonStyle())
Button(model.text("Wyloguj", "Sign out")) { Task { await model.logout() } }
.buttonStyle(PrimaryButtonStyle())
Button(model.text("Usuń konto", "Delete account"), role: .destructive) { deletingAccount = true }
.frame(maxWidth: .infinity, minHeight: 48)
VStack(spacing: 12) {
Link(model.text("Polityka prywatności", "Privacy policy"), destination: URL(string: "https://kasztelan.co.uk/privacy-policy/")!)
Link(model.text("Informacje o usuwaniu konta", "Account deletion information"), destination: URL(string: "https://kasztelan.co.uk/delete-account/")!)
Link(model.text("Regulamin", "Terms"), destination: URL(string: "https://kasztelan.co.uk/regulamin/")!)
}
.font(.subheadline.bold())
.foregroundStyle(KTheme.orange)
.frame(maxWidth: .infinity)
.padding(.top, 4)
}
.padding(20)
}
.background(AmbientBackground())
.toolbar { AppToolbar() }
.toolbarBackground(.hidden, for: .navigationBar)
.fullScreenCover(isPresented: $editingProfile) { EditProfileView() }
.sheet(isPresented: $changingPassword) { ChangePasswordView() }
.sheet(isPresented: $deletingAccount) {
NavigationStack {
VStack(spacing: 16) {
Text(model.text("Usunięcie konta jest nieodwracalne. Potwierdź swoim hasłem.", "Deleting your account cannot be undone. Confirm with your password."))
KasztelanField(title: model.text("Hasło", "Password"), text: $deletePassword, secure: true, contentType: .password)
Button(model.text("Usuń konto", "Delete account"), role: .destructive) {
Task { if await model.deleteAccount(password: deletePassword) { deletingAccount = false } }
}
.buttonStyle(PrimaryButtonStyle())
Spacer()
}
.padding(20)
.background(AmbientBackground())
.navigationTitle(model.text("Usuń konto", "Delete account"))
.toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { deletingAccount = false } } }
}
.presentationDetents([.medium])
}
}
private var name: String {
let value = "\(model.user?.firstName ?? "") \(model.user?.lastName ?? "")".trimmingCharacters(in: .whitespaces)
return value.nonEmpty ?? "—"
}
private var cardAssignment: some View {
VStack(alignment: .leading, spacing: 10) {
Text(model.text("Przypisz kartę", "Assign a card")).font(.headline)
KasztelanField(title: model.text("13-cyfrowy numer karty", "13-digit card number"), text: $cardNumber, keyboard: .numberPad)
Text(model.text("Po przypisaniu numeru nie można go zmienić.", "The number cannot be changed after assignment."))
.font(.caption).foregroundStyle(.secondary)
Button(model.text("Przypisz", "Assign")) {
Task { if await model.assignCard(cardNumber) { cardNumber = "" } }
}
.buttonStyle(PrimaryButtonStyle())
Button(model.text("Nie mam karty — wygeneruj cyfrową", "I don't have a card — create a digital one")) {
Task { _ = await model.generateCard() }
}
.buttonStyle(GlassButtonStyle())
}
.kasztelanPanel()
}
}
private struct ProfileValue: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 3) {
Text(label).font(.caption).foregroundStyle(.secondary)
Text(value.nonEmpty ?? "—").font(.title3.bold())
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct EditProfileView: View {
@EnvironmentObject private var model: AppModel
@Environment(\.dismiss) private var dismiss
@State private var form = ProfileForm()
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 12) {
KasztelanField(title: model.text("Imię", "First name"), text: $form.firstName, contentType: .givenName)
KasztelanField(title: model.text("Nazwisko", "Last name"), text: $form.lastName, contentType: .familyName)
KasztelanField(title: "E-mail", text: $form.email, keyboard: .emailAddress, contentType: .emailAddress)
KasztelanField(title: model.text("Telefon UK lub PL", "UK or Polish phone number"), text: $form.mobile, keyboard: .phonePad, contentType: .telephoneNumber)
KasztelanField(title: model.text("Kod pocztowy UK", "UK postcode"), text: $form.postcode, contentType: .postalCode)
KasztelanField(title: model.text("Data urodzenia: RRRR-MM-DD", "Date of birth: YYYY-MM-DD"), text: dateBinding, keyboard: .numberPad)
Button(model.text("Zapisz", "Save")) {
Task { if await model.saveProfile(form) { dismiss() } }
}
.buttonStyle(PrimaryButtonStyle())
}
.frame(maxWidth: .infinity)
.padding(20)
}
.background(AmbientBackground())
.navigationTitle(model.text("Edytuj profil", "Edit profile"))
.toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() } } }
.onAppear { form = model.profileForm() }
}
}
private var dateBinding: Binding<String> {
Binding(get: { form.dob }, set: { form.dob = AuthView.formatDateInput($0) })
}
}
struct ChangePasswordView: View {
@EnvironmentObject private var model: AppModel
@Environment(\.dismiss) private var dismiss
@State private var current = ""
@State private var newPassword = ""
@State private var confirmation = ""
var body: some View {
NavigationStack {
VStack(spacing: 14) {
KasztelanField(title: model.text("Obecne hasło", "Current password"), text: $current, secure: true, contentType: .password)
KasztelanField(title: model.text("Nowe hasło", "New password"), text: $newPassword, secure: true, contentType: .newPassword)
KasztelanField(title: model.text("Powtórz nowe hasło", "Repeat new password"), text: $confirmation, secure: true, contentType: .newPassword)
Button(model.text("Zmień hasło", "Change password")) {
guard newPassword == confirmation else {
model.errorMessage = model.text("Nowe hasła nie są takie same.", "The new passwords do not match.")
return
}
Task { if await model.changePassword(current: current, new: newPassword) { dismiss() } }
}
.buttonStyle(PrimaryButtonStyle())
Spacer()
}
.padding(20)
.background(AmbientBackground())
.navigationTitle(model.text("Zmień hasło", "Change password"))
.toolbar { ToolbarItem(placement: .cancellationAction) { Button(model.text("Anuluj", "Cancel")) { dismiss() } } }
}
.presentationDetents([.medium])
}
}