summaryrefslogtreecommitdiff
path: root/Hutch/Extensions/ErrorViews.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-17 23:19:43 -0500
committerChristian Cleberg <[email protected]>2026-03-17 23:19:43 -0500
commit32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5 (patch)
treeee36d421d704e508d5617d803b4c8cbdb4804fe1 /Hutch/Extensions/ErrorViews.swift
parent8f2057c53e9009c2529c9c4849c914666c0e4b40 (diff)
downloadhutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.tar.gz
hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.tar.bz2
hutch-32ad6cab8d58d99ebd8a28e8fa6e6f4e587cb1e5.zip
v1.0
Diffstat (limited to 'Hutch/Extensions/ErrorViews.swift')
-rw-r--r--Hutch/Extensions/ErrorViews.swift159
1 files changed, 159 insertions, 0 deletions
diff --git a/Hutch/Extensions/ErrorViews.swift b/Hutch/Extensions/ErrorViews.swift
new file mode 100644
index 0000000..086c0de
--- /dev/null
+++ b/Hutch/Extensions/ErrorViews.swift
@@ -0,0 +1,159 @@
+import SwiftUI
+
+// MARK: - SRHTErrorBanner ViewModifier
+
+/// Displays a dismissible error banner at the top of the screen.
+/// Attach to any view with `.srhtErrorBanner(error:)`.
+struct SRHTErrorBanner: ViewModifier {
+ @Binding var error: String?
+
+ func body(content: Content) -> some View {
+ content
+ .overlay(alignment: .top) {
+ if let message = error {
+ banner(message)
+ .transition(.move(edge: .top).combined(with: .opacity))
+ }
+ }
+ .animation(.easeInOut(duration: 0.3), value: error)
+ }
+
+ @ViewBuilder
+ private func banner(_ message: String) -> some View {
+ HStack(spacing: 8) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .foregroundStyle(.white)
+
+ Text(message)
+ .font(.subheadline)
+ .foregroundStyle(.white)
+ .lineLimit(3)
+
+ Spacer()
+
+ Button {
+ error = nil
+ } label: {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundStyle(.white.opacity(0.8))
+ }
+ }
+ .padding(12)
+ .background(Color.red.gradient, in: RoundedRectangle(cornerRadius: 12))
+ .padding(.horizontal, 12)
+ .padding(.top, 4)
+ }
+}
+
+extension View {
+ /// Attach an error banner that shows at the top when `error` is non-nil.
+ func srhtErrorBanner(error: Binding<String?>) -> some View {
+ modifier(SRHTErrorBanner(error: error))
+ }
+}
+
+// MARK: - Shared Screen States
+
+struct SRHTLoadingStateView: View {
+ let message: String
+
+ var body: some View {
+ VStack(spacing: 12) {
+ ProgressView()
+ Text(message)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+}
+
+struct SRHTErrorStateView: View {
+ let title: String
+ let message: String
+ let retryAction: (() async -> Void)?
+
+ var body: some View {
+ ContentUnavailableView {
+ SwiftUI.Label(title, systemImage: "exclamationmark.triangle")
+ } description: {
+ Text(message)
+ } actions: {
+ if let retryAction {
+ Button("Retry") {
+ Task { await retryAction() }
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+ }
+}
+
+// MARK: - NoConnectionView
+
+/// Empty state view shown when the device is offline. Includes a retry button.
+struct NoConnectionView: View {
+ var retryAction: () async -> Void
+
+ var body: some View {
+ ContentUnavailableView {
+ SwiftUI.Label("No Connection", systemImage: "wifi.slash")
+ } description: {
+ Text("Check your internet connection and try again.")
+ } actions: {
+ Button {
+ Task { await retryAction() }
+ } label: {
+ Text("Retry")
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ }
+}
+
+// MARK: - Connectivity Overlay
+
+/// ViewModifier that shows NoConnectionView when the device is offline and
+/// there is no content to display. When content exists, shows a subtle
+/// offline indicator instead.
+struct ConnectivityOverlay: ViewModifier {
+ @Environment(NetworkMonitor.self) private var networkMonitor
+ let hasContent: Bool
+ var retryAction: () async -> Void
+
+ func body(content: Content) -> some View {
+ content
+ .overlay {
+ if !networkMonitor.isConnected, !hasContent {
+ NoConnectionView(retryAction: retryAction)
+ }
+ }
+ .safeAreaInset(edge: .bottom) {
+ if !networkMonitor.isConnected, hasContent {
+ offlineBadge
+ }
+ }
+ }
+
+ private var offlineBadge: some View {
+ HStack(spacing: 6) {
+ Image(systemName: "wifi.slash")
+ .font(.caption2)
+ Text("Offline — showing cached data")
+ .font(.caption2)
+ }
+ .foregroundStyle(.white)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(.orange.gradient, in: Capsule())
+ .padding(.bottom, 4)
+ }
+}
+
+extension View {
+ /// Overlay a no-connection view when offline with no content, or a
+ /// subtle offline badge when showing cached data.
+ func connectivityOverlay(hasContent: Bool, retryAction: @escaping () async -> Void) -> some View {
+ modifier(ConnectivityOverlay(hasContent: hasContent, retryAction: retryAction))
+ }
+}