InvestmentTrackerApp/PortfolioJournal/Views/Components/LoadingView.swift

290 lines
7.7 KiB
Swift

import SwiftUI
struct LoadingView: View {
var message: String = "Loading..."
var body: some View {
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.5)
Text(message)
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
}
}
struct AppLaunchLoadingView: View {
var messageKey: LocalizedStringKey = "loading"
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("BrandMark")
.resizable()
.scaledToFit()
.frame(width: 140, height: 140)
.padding(16)
.background(Color.appPrimary.opacity(0.08))
.cornerRadius(28)
ProgressView()
.scaleEffect(1.2)
Text(messageKey)
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(AppBackground())
}
}
// MARK: - Skeleton Loading
struct SkeletonView: View {
@State private var isAnimating = false
var body: some View {
Rectangle()
.fill(
LinearGradient(
colors: [
Color.gray.opacity(0.1),
Color.gray.opacity(0.2),
Color.gray.opacity(0.1)
],
startPoint: .leading,
endPoint: .trailing
)
)
.offset(x: isAnimating ? 200 : -200)
.animation(
Animation.linear(duration: 1.5)
.repeatForever(autoreverses: false),
value: isAnimating
)
.mask(Rectangle())
.onAppear {
isAnimating = true
}
}
}
struct SkeletonCardView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
SkeletonView()
.frame(width: 100, height: 16)
.cornerRadius(4)
SkeletonView()
.frame(height: 32)
.cornerRadius(4)
HStack {
SkeletonView()
.frame(width: 80, height: 14)
.cornerRadius(4)
Spacer()
SkeletonView()
.frame(width: 60, height: 14)
.cornerRadius(4)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
}
}
// MARK: - Empty State View
struct EmptyStateView: View {
let icon: String
let title: String
let message: String
var actionTitle: String?
var action: (() -> Void)?
var body: some View {
VStack(spacing: 20) {
Image(systemName: icon)
.font(.system(size: 60))
.foregroundColor(.secondary)
Text(title)
.font(.title2.weight(.semibold))
Text(message)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
if let actionTitle = actionTitle, let action = action {
Button(action: action) {
Text(actionTitle)
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: 200)
.background(Color.appPrimary)
.cornerRadius(AppConstants.UI.cornerRadius)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: - Error View
struct ErrorView: View {
let message: String
var retryAction: (() -> Void)?
var body: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 48))
.foregroundColor(.appWarning)
Text("Something went wrong")
.font(.headline)
Text(message)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
if let retryAction = retryAction {
Button(action: retryAction) {
Label("Try Again", systemImage: "arrow.clockwise")
.font(.subheadline.weight(.semibold))
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.appPrimary)
.foregroundColor(.white)
.cornerRadius(20)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: - Success View
struct SuccessView: View {
let title: String
let message: String
var action: (() -> Void)?
var body: some View {
VStack(spacing: 20) {
ZStack {
Circle()
.fill(Color.positiveGreen.opacity(0.1))
.frame(width: 100, height: 100)
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 60))
.foregroundColor(.positiveGreen)
}
Text(title)
.font(.title2.weight(.bold))
Text(message)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
if let action = action {
Button(action: action) {
Text("Continue")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: 200)
.background(Color.appPrimary)
.cornerRadius(AppConstants.UI.cornerRadius)
}
}
}
}
}
// MARK: - Toast View
struct ToastView: View {
enum ToastType {
case success, error, info
var icon: String {
switch self {
case .success: return "checkmark.circle.fill"
case .error: return "xmark.circle.fill"
case .info: return "info.circle.fill"
}
}
var color: Color {
switch self {
case .success: return .positiveGreen
case .error: return .negativeRed
case .info: return .appPrimary
}
}
}
let message: String
let type: ToastType
var body: some View {
HStack(spacing: 12) {
Image(systemName: type.icon)
.foregroundColor(type.color)
Text(message)
.font(.subheadline)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.shadow(color: .black.opacity(0.1), radius: 10, y: 5)
}
}
#Preview {
VStack(spacing: 20) {
LoadingView()
.frame(height: 100)
SkeletonCardView()
EmptyStateView(
icon: "tray",
title: "No Data",
message: "Start adding your investments to see them here.",
actionTitle: "Get Started",
action: {}
)
.frame(height: 300)
ToastView(message: "Successfully saved!", type: .success)
}
.padding()
}