Deploy Ladill Mini / deploy (push) Successful in 35s
Ship full-featured mobile clients with auth, payments, QRs, push notifications, and iOS polish including Figtree typography, app icons, and QR previews. Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
4.1 KiB
Swift
112 lines
4.1 KiB
Swift
import SwiftUI
|
|
|
|
@MainActor
|
|
final class NotificationsViewModel: ObservableObject {
|
|
@Published var notifications: [AppNotification] = []
|
|
@Published var unreadCount = 0
|
|
@Published var loading = false
|
|
@Published var isRefreshing = false
|
|
@Published var error: String?
|
|
|
|
func load() async {
|
|
loading = notifications.isEmpty
|
|
error = nil
|
|
defer { loading = false }
|
|
do {
|
|
let response = try await APIClient.shared.notifications()
|
|
notifications = response.data
|
|
unreadCount = response.unreadCount ?? 0
|
|
} catch {
|
|
self.error = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
|
|
}
|
|
}
|
|
|
|
func refresh() async {
|
|
isRefreshing = true
|
|
defer { isRefreshing = false }
|
|
await load()
|
|
}
|
|
|
|
func markAllRead() async {
|
|
try? await APIClient.shared.markAllNotificationsRead()
|
|
await load()
|
|
}
|
|
|
|
func markRead(_ notification: AppNotification) async {
|
|
guard notification.readAt == nil else { return }
|
|
try? await APIClient.shared.markNotificationRead(id: notification.id)
|
|
await load()
|
|
}
|
|
}
|
|
|
|
struct NotificationsSheet: View {
|
|
let onDismiss: () -> Void
|
|
let onUnreadChanged: (Int) -> Void
|
|
|
|
@StateObject private var vm = NotificationsViewModel()
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if vm.loading {
|
|
LoadingView()
|
|
} else if let error = vm.error {
|
|
ErrorView(message: error) { Task { await vm.load() } }
|
|
} else if vm.notifications.isEmpty {
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "bell.slash")
|
|
.font(.system(size: 40))
|
|
.foregroundStyle(LadillColors.muted)
|
|
Text("You're all caught up")
|
|
.font(.ladill(size: 16, weight: .semibold))
|
|
Text("Payment alerts and account updates will show up here.")
|
|
.font(.ladill(size: 14))
|
|
.foregroundStyle(LadillColors.muted)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.padding(32)
|
|
} else {
|
|
List(vm.notifications) { notification in
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(notification.title)
|
|
.font(.ladill(size: 15, weight: notification.isRead ? .regular : .semibold))
|
|
Text(notification.message)
|
|
.font(.ladill(size: 13))
|
|
.foregroundStyle(LadillColors.muted)
|
|
if let date = notification.createdAt {
|
|
Text(Formatters.date(iso: date))
|
|
.font(.ladill(size: 11))
|
|
.foregroundStyle(LadillColors.subtle)
|
|
}
|
|
}
|
|
.opacity(notification.isRead ? 0.7 : 1)
|
|
.onTapGesture { Task { await vm.markRead(notification) } }
|
|
}
|
|
.listStyle(.plain)
|
|
}
|
|
}
|
|
.background(LadillColors.page)
|
|
.navigationTitle("Notifications")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
if vm.unreadCount > 0 {
|
|
Button("Mark all read") { Task { await vm.markAllRead() } }
|
|
}
|
|
}
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("Done") {
|
|
onUnreadChanged(vm.unreadCount)
|
|
dismiss()
|
|
onDismiss()
|
|
}
|
|
}
|
|
}
|
|
.refreshable { await vm.refresh() }
|
|
.task { await vm.load() }
|
|
}
|
|
.presentationDetents([.medium, .large])
|
|
}
|
|
}
|