Created
February 9, 2025 19:21
-
-
Save mireabot/baec7a38ba332781b33ffb7e2fb3e92f to your computer and use it in GitHub Desktop.
Banking Dashboard UI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct BankindDashboard: View { | |
@State private var balance: Double = 3700.75 | |
var body: some View { | |
VStack { | |
// Account Card | |
VStack { | |
// Account holder header | |
HStack { | |
HStack(spacing: 8) { | |
Image(systemName: "person.fill") | |
.font(.system(.callout)) | |
.padding(8) | |
.background( | |
RoundedRectangle(cornerRadius: 5) | |
.strokeBorder(Color(hex: "#1218181B"), lineWidth: 1) | |
.background(.white) | |
) | |
.clipShape(RoundedRectangle(cornerRadius: 5)) | |
Text("Adrian's Account") | |
.font(.system(.headline, weight: .medium)) | |
} | |
Spacer() | |
Button(action: {}, label: { | |
Image(systemName: "ellipsis") | |
.font(.system(.callout)) | |
.foregroundColor(.secondary) | |
}) | |
} | |
.padding(16) | |
Divider() | |
// Balance info and action buttons | |
VStack(alignment: .leading, spacing: 16) { | |
VStack(alignment: .leading, spacing: 2) { | |
Text("Available balance") | |
.font(.system(.subheadline)) | |
.foregroundColor(.secondary) | |
HStack(spacing: 0) { | |
Text("$").foregroundColor(.gray.opacity(0.5)) | |
RollingCounter( | |
value: balance, | |
formattingStyle: .decimal | |
) | |
} | |
.font(.system(.largeTitle, weight: .semibold)) | |
} | |
// Action buttons | |
HStack(spacing: 2) { | |
Button(action: { | |
withAnimation(.smooth) { | |
balance += 1450 | |
} | |
}, label: { | |
HStack(spacing: 4) { | |
Image(systemName: "plus").foregroundColor(.secondary) | |
Text("Top up") | |
} | |
.frame(maxWidth: .infinity) | |
}).buttonStyle(CapsuledButtonStyle()) | |
Button(action: {}, label: { | |
HStack(spacing: 4) { | |
Image(systemName: "paperplane").foregroundColor(.secondary) | |
Text("Transfer") | |
} | |
.frame(maxWidth: .infinity) | |
}).buttonStyle(CapsuledButtonStyle()) | |
Button(action: {}, label: { | |
HStack(spacing: 4) { | |
Image(systemName: "arrow.left.arrow.right").foregroundColor(.secondary) | |
Text("Exchange") | |
} | |
.frame(maxWidth: .infinity) | |
}).buttonStyle(CapsuledButtonStyle()) | |
} | |
} | |
.padding(16) | |
} | |
.background(Color.cardBackground) | |
.cornerRadius(16) | |
.padding(.horizontal, 16) | |
// Widgets | |
VStack(spacing: 16) { | |
HStack { | |
Text("Widgets") | |
.font(.system(.headline, weight: .medium)) | |
.foregroundColor(.primary) | |
Spacer() | |
Button(action: {}, label: { | |
Image(systemName: "plus.circle").foregroundColor(.gray) | |
}) | |
} | |
Grid { | |
GridRow(alignment: .top) { | |
ExpensesWidget() | |
MyCardsWidget() | |
}.frame(height: 180) | |
GridRow(alignment: .top) { | |
ReferalWidget() | |
DepositsWidget() | |
}.frame(height: 180) | |
} | |
} | |
.padding(.top, 16) | |
.padding(.horizontal, 16) | |
} | |
} | |
} | |
#Preview { | |
BankindDashboard() | |
} | |
// MARK: - Extensions | |
extension Color { | |
init(hex: String) { | |
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) | |
var int: UInt64 = 0 | |
Scanner(string: hex).scanHexInt64(&int) | |
let a, r, g, b: UInt64 | |
switch hex.count { | |
case 3: // RGB (12-bit) | |
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) | |
case 6: // RGB (24-bit) | |
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) | |
case 8: // ARGB (32-bit) | |
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) | |
default: | |
(a, r, g, b) = (255, 0, 0, 0) | |
} | |
self.init( | |
.sRGB, | |
red: Double(r) / 255, | |
green: Double(g) / 255, | |
blue: Double(b) / 255, | |
opacity: Double(a) / 255 | |
) | |
} | |
} | |
extension Color { | |
static let cardBackground = Color(hex: "F5F5F5") | |
static let border = Color(hex: "#1218181B") | |
} | |
// MARK: - Button Styles | |
struct CapsuledButtonStyle: ButtonStyle { | |
func makeBody(configuration: Configuration) -> some View { | |
configuration.label | |
.font(.system(.footnote, weight: .medium)) | |
.padding(.horizontal, 14) | |
.padding(.vertical, 12) | |
.background( | |
Capsule() | |
.strokeBorder(Color.border, lineWidth: 1) | |
.background(.white) | |
.clipped() | |
) | |
.clipShape(Capsule()) | |
} | |
} | |
// MARK: - Widgets | |
struct ExpensesWidget: View { | |
var body: some View { | |
VStack(alignment: .leading) { | |
Text("Expenses in Jul") | |
.font(.system(.subheadline, weight: .semibold)) | |
.foregroundColor(.primary) | |
Spacer() | |
VStack(alignment: .leading, spacing: 8) { | |
Text("\(Text("$").foregroundColor(.gray.opacity(0.5)))\(1211.00, specifier: "%.2f")") | |
.font(.system(.title, weight: .semibold)) | |
HStack(spacing: 4) { | |
Text("-$145 (12%)") | |
.foregroundColor(.green) | |
.font(.system(.footnote, weight: .semibold)) | |
Image(systemName: "arrowtriangle.down.fill") | |
.foregroundColor(.green) | |
.font(.system(.caption, weight: .medium)) | |
} | |
SegmentedBar(segments: [ | |
(.orange, 0.5), // 50% | |
(.green, 0.3), // 30% | |
(.purple, 0.15), // 15% | |
(.pink, 0.05) // 5% | |
]) | |
.frame(height: 8) | |
} | |
} | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.padding(16) | |
.background( | |
RoundedRectangle(cornerRadius: 16) | |
.strokeBorder(Color.border, lineWidth: 1) | |
.background(.white) | |
) | |
} | |
} | |
struct MyCardsWidget: View { | |
var body: some View { | |
VStack(alignment: .leading, spacing: 8) { | |
// Background gradient | |
ZStack { | |
LinearGradient( | |
gradient: Gradient(stops: [ | |
.init(color: Color(red: 0.09, green: 0.047, blue: 0.11), location: 0), | |
.init(color: .white, location: 1) | |
]), | |
startPoint: .trailing, | |
endPoint: .leading | |
) | |
// Blurred vector shape | |
LinearGradient( | |
stops: [ | |
Gradient.Stop(color: Color(red: 0.58, green: 0.17, blue: 0.9), location: 0.00), | |
Gradient.Stop(color: Color(red: 0.78, green: 0.27, blue: 0.27), location: 0.34), | |
Gradient.Stop(color: Color(red: 0.96, green: 0.35, blue: 0.18), location: 0.63), | |
Gradient.Stop(color: Color(red: 0.93, green: 0.75, blue: 0.57), location: 1.00), | |
], | |
startPoint: .bottomLeading, | |
endPoint: .topTrailing | |
) | |
.blur(radius: 25) | |
.opacity(0.8) | |
VStack(alignment: .leading, spacing: 8) { | |
Text("My cards") | |
.font(.system(.subheadline, weight: .semibold)) | |
.foregroundColor(.white) | |
.padding(.top, 16) | |
Spacer() | |
// Card stack | |
ZStack { | |
CardView(isBackground: true) | |
.offset(x: 10, y: -10) | |
.opacity(0.6) | |
CardView(isBackground: false) | |
} | |
.padding(.bottom, 16) | |
} | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.padding(.horizontal, 16) | |
} | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.background(Color(red: 0.96, green: 0.96, blue: 0.96)) | |
.cornerRadius(16) | |
.clipped() | |
} | |
} | |
} | |
struct ReferalWidget: View { | |
var body: some View { | |
VStack(alignment: .leading) { | |
Text("Invite and earn!") | |
.font(.system(.subheadline, weight: .semibold)) | |
.multilineTextAlignment(.leading) | |
.foregroundColor(.primary) | |
Spacer() | |
Text("Refer SmartBank to your friends and earn rewards") | |
.font(.system(.subheadline, weight: .regular)) | |
.multilineTextAlignment(.leading) | |
.foregroundColor(.secondary) | |
} | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.padding(16) | |
.background(Color.cardBackground) | |
.cornerRadius(16) | |
} | |
} | |
struct DepositsWidget: View { | |
var body: some View { | |
VStack(alignment: .leading) { | |
VStack(alignment: .leading, spacing: 10, content: { | |
Text("You have \(Text("$\(0.00, specifier: "%.2f")").foregroundStyle(.tertiary)) in scheduled deposits every month") | |
.font(.system(.subheadline, weight: .semibold)) | |
.multilineTextAlignment(.leading) | |
.foregroundColor(.primary) | |
Text("\(0) paychecks") | |
.font(.system(.subheadline, weight: .regular)) | |
.multilineTextAlignment(.leading) | |
.foregroundColor(.secondary) | |
}) | |
Spacer() | |
Button(action: {}, label: { | |
Text("Manage") | |
}) | |
.buttonStyle(CapsuledButtonStyle()) | |
} | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.padding(16) | |
.background( | |
RoundedRectangle(cornerRadius: 16) | |
.strokeBorder(Color.border, lineWidth: 1) | |
.background(.white) | |
) | |
} | |
} | |
// MARK: - Helper Views | |
struct CardView: View { | |
let isBackground: Bool | |
var body: some View { | |
ZStack { | |
// Card background with gradient | |
RoundedRectangle(cornerRadius: 8) | |
.fill( | |
LinearGradient( | |
gradient: Gradient(colors: [ | |
Color.white.opacity(0.4), | |
Color.white.opacity(0) | |
]), | |
startPoint: .topLeading, | |
endPoint: .bottomTrailing | |
) | |
) | |
.background(.ultraThinMaterial) | |
.clipShape(RoundedRectangle(cornerRadius: 8)) | |
VStack { | |
Spacer() | |
HStack { | |
Text("SmartBank") | |
.font(.system(size: 10, weight: .bold)) | |
.foregroundColor(.white) | |
Spacer() | |
// Card circles | |
HStack(spacing: -6) { | |
Circle() | |
.fill(.white.opacity(0.4)) | |
.frame(width: 12, height: 12) | |
Circle() | |
.fill(.white.opacity(0.4)) | |
.frame(width: 12, height: 12) | |
} | |
} | |
.padding(8) | |
} | |
} | |
.frame(width: isBackground ? 129 : 112, height: isBackground ? 80 : 70) | |
} | |
} | |
struct SegmentedBar: View { | |
let segments: [(color: Color, percentage: Double)] | |
var body: some View { | |
GeometryReader { geometry in | |
HStack(spacing: 2) { | |
ForEach(0..<segments.count, id: \.self) { index in | |
Rectangle() | |
.fill(segments[index].color) | |
.frame(width: geometry.size.width * segments[index].percentage) | |
} | |
} | |
.frame(height: 8) | |
.clipShape(RoundedRectangle(cornerRadius: 4)) | |
} | |
} | |
} | |
struct RollingCounter: View { | |
let value: Double | |
let duration: Double | |
let formattingStyle: NumberFormatter.Style | |
@State private var animatedValue: Double | |
private let formatter: NumberFormatter | |
init( | |
value: Double, | |
duration: Double = 0.3, | |
formattingStyle: NumberFormatter.Style = .decimal | |
) { | |
self.value = value | |
self.duration = duration | |
self.formattingStyle = formattingStyle | |
self._animatedValue = State(initialValue: Double(value)) | |
let formatter = NumberFormatter() | |
formatter.numberStyle = formattingStyle | |
self.formatter = formatter | |
} | |
var body: some View { | |
Text(formatter.string(from: NSNumber(value: animatedValue)) ?? "0") | |
.contentTransition(.numericText()) | |
.onChange(of: value, { oldValue, newValue in | |
withAnimation(.spring(duration: duration)) { | |
animatedValue = Double(newValue) | |
} | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment