import ManaAuthUI import ManaCore import SwiftUI /// Phase ζ-0 minimal: zeigt Auth-Status und Healthz-Probe-Ergebnis. /// Phase ζ-3 erweitert um Submission-History-Link (via WebShell auf /// `zitare.mana.how/me`). Login-Sheet schon hier, damit Guests einen /// Anmelden-Button finden. struct AccountView: View { @Environment(AuthClient.self) private var auth @Environment(ManaAuthGate.self) private var authGate @Environment(SubmissionQueue.self) private var submissionQueue let healthStatus: HealthStatus @State private var pendingEntries: [PendingSubmission] = [] @State private var refreshTick: Int = 0 private var isSignedIn: Bool { if case .signedIn = auth.status { return true } return false } var body: some View { ScrollView { VStack(spacing: 24) { header statusCard authActionCard if !pendingEntries.isEmpty { pendingQueueCard } Spacer(minLength: 32) aboutCard } .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(ZitareTheme.background) .task { reload() } .refreshable { reload() } } private var pendingQueueCard: some View { VStack(alignment: .leading, spacing: 12) { HStack { Label("Offline-Warteschlange", systemImage: "tray.and.arrow.up") .font(.headline) Spacer() Button { Task { let api = ZitareAPI(auth: auth) _ = await submissionQueue.tryFlush(api: api) reload() } } label: { Label("Jetzt versuchen", systemImage: "arrow.clockwise") .font(.callout) } .tint(ZitareTheme.primary) } ForEach(pendingEntries) { entry in pendingRow(entry) if entry.id != pendingEntries.last?.id { Divider() } } } .padding() .background(ZitareTheme.surface) .clipShape(RoundedRectangle(cornerRadius: 12)) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(ZitareTheme.border, lineWidth: 1) ) } private func pendingRow(_ entry: PendingSubmission) -> some View { VStack(alignment: .leading, spacing: 6) { Text(entry.text.prefix(120) + (entry.text.count > 120 ? "…" : "")) .font(.callout) .foregroundStyle(ZitareTheme.foreground) HStack(spacing: 8) { if let author = entry.authorName { Text(author) .font(.caption.weight(.medium)) } Text(entry.createdAt, format: .dateTime.day().month().hour().minute()) .font(.caption) if entry.retryCount > 0 { Text("· \(entry.retryCount) Versuch(e)") .font(.caption) } Spacer() Button(role: .destructive) { submissionQueue.delete(id: entry.id) reload() } label: { Image(systemName: "trash") .foregroundStyle(ZitareTheme.mutedForeground) } .buttonStyle(.plain) } .foregroundStyle(ZitareTheme.mutedForeground) if let err = entry.lastError { Text(err) .font(.caption) .foregroundStyle(ZitareTheme.error) } } } private func reload() { pendingEntries = submissionQueue.loadAll() refreshTick &+= 1 } @ViewBuilder private var authActionCard: some View { if isSignedIn { Button(role: .destructive) { Task { await auth.signOut(keepGuestMode: true) } } label: { Label("Abmelden", systemImage: "arrow.left.square") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .controlSize(.large) } else { Button { authGate.isPresentingSignIn = true } label: { Label("Mit mana-Konto anmelden", systemImage: "arrow.right.square") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) .tint(ZitareTheme.primary) } } private var header: some View { VStack(spacing: 12) { // Eigenes Anführungszeichen-Glyph in der gleichen Variante // wie das App-Icon — sienna auf transparentem Hintergrund, // serif. SF-Symbol "quote.opening" rendert zwei Glyphen // asymmetrisch nebeneinander, das wirkt unsauber. Text(verbatim: "\u{201C}") .font(.custom("Georgia-Bold", size: 96)) .foregroundStyle(ZitareTheme.primary) .frame(height: 64, alignment: .top) Text("Zitare") .font(.largeTitle) .fontWeight(.semibold) Text("Öffentlicher Zitat-Korpus von mana e.V.") .font(.callout) .foregroundStyle(ZitareTheme.mutedForeground) .multilineTextAlignment(.center) } .padding(.top, 32) } private var statusCard: some View { VStack(alignment: .leading, spacing: 12) { row("Auth", value: authStatusLabel) Divider() row("API", value: healthLabel) } .padding() .background(ZitareTheme.surface) .clipShape(RoundedRectangle(cornerRadius: 12)) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(ZitareTheme.border, lineWidth: 1) ) } private var aboutCard: some View { VStack(alignment: .leading, spacing: 8) { Text("Phase ζ-0 — Setup") .font(.caption) .foregroundStyle(ZitareTheme.mutedForeground) Text( "Diese App ist noch im Aufbau. Web-App live auf " + "zitare.com und zitare.mana.how. " + "Plan in mana/docs/playbooks/ZITARE_NATIVE_GREENFIELD.md." ) .font(.footnote) .foregroundStyle(ZitareTheme.foreground) } .padding() .frame(maxWidth: .infinity, alignment: .leading) } private func row(_ label: String, value: String) -> some View { HStack { Text(label) .foregroundStyle(ZitareTheme.mutedForeground) Spacer() Text(value) .foregroundStyle(ZitareTheme.foreground) .fontWeight(.medium) } } private var authStatusLabel: String { switch auth.status { case .unknown: "—" case .signedOut: "Nicht eingeloggt" case .guest: "Gast" case .signingIn: "Login läuft …" case .twoFactorRequired: "2FA erforderlich" case let .signedIn(email): email case .error: "Fehler" } } private var healthLabel: String { switch healthStatus { case .unknown: "—" case .ok: "OK" case .down: "nicht erreichbar" } } }