Initial commit: nightly iOS app + Supabase backend

iOS SwiftUI app with Supabase auth/realtime, Node.js backend,
Docker/Supabase self-hosted infrastructure, and APNs scheduler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-04-23 23:31:38 +02:00
commit 5bc81d5b3b
80 changed files with 9958 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var appState: AppState
@Environment(\.dismiss) var dismiss
@State private var showLegal = false
@State private var showDeleteConfirm = false
@State private var showDeleteFinal = false
@State private var deletePassword = ""
@State private var isDeleting = false
@State private var deleteError: String?
@State private var notificationsEnabled = false
var body: some View {
NavigationStack {
ZStack {
Color.nightBase.ignoresSafeArea()
List {
// Account
Section {
if let user = appState.currentUser {
HStack(spacing: 12) {
AvatarView(user: user, size: 44)
VStack(alignment: .leading, spacing: 2) {
Text(user.displayName)
.font(.nightLabel(15, weight: .semibold))
.foregroundColor(.nightPrimary)
Text("@\(user.username)")
.font(.nightLabel(13))
.foregroundColor(.nightSecondary)
}
}
.padding(.vertical, 4)
}
}
.listRowBackground(Color.nightSurface)
// Benachrichtigungen
Section("benachrichtigungen") {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("nightly ping")
.font(.nightLabel(15))
.foregroundColor(.nightPrimary)
Text("Wenn das Fenster öffnet")
.font(.nightLabel(12))
.foregroundColor(.nightSecondary)
}
Spacer()
Toggle("", isOn: $notificationsEnabled)
.tint(.nightPurple)
}
// APNs-Hinweis
HStack(spacing: 8) {
Image(systemName: "info.circle")
.foregroundColor(.nightSecondary)
.font(.system(size: 13))
Text("Push-Benachrichtigungen benötigen einen bezahlten Apple Developer Account ($99/Jahr).")
.font(.nightLabel(12))
.foregroundColor(.nightSecondary)
.lineSpacing(3)
}
.padding(.vertical, 2)
}
.listRowBackground(Color.nightSurface)
// Rechtliches
Section("rechtliches") {
Button {
showLegal = true
} label: {
HStack {
Text("Impressum & Datenschutz")
.font(.nightLabel(15))
.foregroundColor(.nightPrimary)
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 12))
.foregroundColor(.nightSecondary)
}
}
HStack {
Text("Version")
.font(.nightLabel(15))
.foregroundColor(.nightPrimary)
Spacer()
Text(appVersion)
.font(.nightMono(13))
.foregroundColor(.nightSecondary)
}
}
.listRowBackground(Color.nightSurface)
// Account-Aktionen
Section("account") {
Button {
appState.signOut()
dismiss()
} label: {
Text("abmelden")
.font(.nightLabel(15))
.foregroundColor(.nightPrimary)
}
Button(role: .destructive) {
showDeleteConfirm = true
} label: {
Text("account löschen")
.font(.nightLabel(15))
.foregroundColor(.nightRed)
}
}
.listRowBackground(Color.nightSurface)
}
.scrollContentBackground(.hidden)
.listStyle(.insetGrouped)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("einstellungen")
.font(.nightTitle(17))
.foregroundColor(.nightPrimary)
}
ToolbarItem(placement: .navigationBarLeading) {
Button("Fertig") { dismiss() }
.foregroundColor(.nightSecondary)
}
}
.sheet(isPresented: $showLegal) { LegalView() }
}
.preferredColorScheme(.dark)
// Schritt 1: Erklärung
.confirmationDialog(
"Account wirklich löschen?",
isPresented: $showDeleteConfirm,
titleVisibility: .visible
) {
Button("Ja, Account löschen", role: .destructive) {
showDeleteFinal = true
}
} message: {
Text("Alle deine Posts, Reaktionen und Daten werden sofort und dauerhaft gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden.")
}
// Schritt 2: Passwort bestätigen
.sheet(isPresented: $showDeleteFinal) {
DeleteAccountSheet(
password: $deletePassword,
isDeleting: isDeleting,
error: deleteError,
onDelete: { Task { await deleteAccount() } }
)
}
.onAppear { checkNotificationStatus() }
}
var appVersion: String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
}
func checkNotificationStatus() {
Task {
let settings = await UNUserNotificationCenter.current().notificationSettings()
notificationsEnabled = settings.authorizationStatus == .authorized
}
}
func deleteAccount() async {
isDeleting = true
deleteError = nil
defer { isDeleting = false }
do {
try await appState.deleteAccount()
showDeleteFinal = false
dismiss()
} catch {
deleteError = error.localizedDescription
}
}
}
struct DeleteAccountSheet: View {
@Binding var password: String
let isDeleting: Bool
let error: String?
let onDelete: () -> Void
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationStack {
ZStack {
Color.nightBase.ignoresSafeArea()
VStack(spacing: 24) {
Image(systemName: "trash.circle.fill")
.font(.system(size: 52))
.foregroundColor(.nightRed)
VStack(spacing: 8) {
Text("Account löschen")
.font(.nightTitle(22))
.foregroundColor(.nightPrimary)
Text("Diese Aktion löscht alle deine Daten dauerhaft. Kein Weg zurück.")
.font(.nightBody(15))
.foregroundColor(.nightSecondary)
.multilineTextAlignment(.center)
}
NightlyField(text: $password, placeholder: "passwort bestätigen", icon: "lock", isSecure: true)
.padding(.horizontal, 24)
if let err = error {
Text(err).font(.nightLabel(13)).foregroundColor(.nightRed)
}
Button {
onDelete()
} label: {
Group {
if isDeleting { ProgressView().tint(.white) }
else { Text("endgültig löschen").font(.nightLabel(16, weight: .semibold)).foregroundColor(.white) }
}
.frame(maxWidth: .infinity).frame(height: 50)
.background(Color.nightRed)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.disabled(password.isEmpty || isDeleting)
.padding(.horizontal, 24)
}
.padding(.top, 32)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Abbrechen") { dismiss() }.foregroundColor(.nightSecondary)
}
}
}
.preferredColorScheme(.dark)
.presentationDetents([.medium])
}
}