Last active
September 26, 2025 03:12
-
-
Save luthviar/0585a05951831bb6aa68079291e829dd to your computer and use it in GitHub Desktop.
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 ShippingInfoCardV2: View { | |
| /// Sample data (replace with your real bindings/models) | |
| let recipientName: String | |
| let phoneNumber: String | |
| let shippingAddress: String | |
| /// UI states (UI-only interactions) | |
| @State private var isAddressExpanded: Bool = false | |
| @State private var showCopiedPhone: Bool = false | |
| @State private var showCopiedAddress: Bool = false | |
| @State private var pressedRow: Int? = nil | |
| var body: some View { | |
| /// Card container | |
| VStack(alignment: .leading, spacing: 0) { | |
| row( | |
| index: 0, | |
| title: "Nama Penerima", | |
| content: { | |
| HStack(alignment: .firstTextBaseline, spacing: 8) { | |
| Text(recipientName) | |
| .font(.system(size: 12, weight: .semibold)) | |
| .foregroundColor(.primary) | |
| .accessibilityLabel("Nama penerima") | |
| .accessibilityValue(recipientName) | |
| } | |
| }, | |
| trailingAccessory: { EmptyView() } | |
| ) | |
| Divider().opacity(0.06) | |
| row( | |
| index: 1, | |
| title: "Nomor HP", | |
| content: { | |
| Text(phoneNumber) | |
| .font(.system(size: 12, weight: .semibold)) | |
| .foregroundColor(.primary) | |
| .textSelection(.enabled) | |
| .accessibilityLabel("Nomor HP") | |
| .accessibilityValue(phoneNumber) | |
| }, | |
| trailingAccessory: { | |
| EmptyView() | |
| } | |
| ) | |
| .overlay(alignment: .trailing) { | |
| if showCopiedPhone { | |
| copiedBadge | |
| .transition(.opacity.combined(with: .move(edge: .trailing))) | |
| .padding(.trailing, 8) | |
| } | |
| } | |
| Divider().opacity(0.06) | |
| row( | |
| index: 2, | |
| title: "Alamat Pengiriman", | |
| content: { | |
| VStack(alignment: .leading, spacing: 6) { | |
| Text(shippingAddress) | |
| .font(.system(size: 12, weight: .semibold)) | |
| .foregroundStyle(.primary) | |
| .lineLimit(nil) | |
| .multilineTextAlignment(.leading) | |
| .textSelection(.enabled) | |
| .accessibilityLabel("Alamat Pengiriman") | |
| .accessibilityValue(shippingAddress) | |
| } | |
| }, | |
| trailingAccessory: { | |
| EmptyView() | |
| } | |
| ) | |
| .overlay(alignment: .trailing) { // same note as above | |
| if showCopiedAddress { | |
| copiedBadge | |
| .transition(.opacity.combined(with: .move(edge: .trailing))) | |
| .padding(.trailing, 8) | |
| } | |
| } | |
| } | |
| .padding(12) | |
| .background( | |
| RoundedRectangle(cornerRadius: 16, style: .continuous) | |
| .fill(Color(.systemBackground)) | |
| ) | |
| .overlay( | |
| RoundedRectangle(cornerRadius: 16, style: .continuous) | |
| .stroke(Color.black.opacity(0.08), lineWidth: 1) | |
| ) | |
| .shadow(color: Color.black.opacity(0.04), radius: 8, x: 0, y: 2) | |
| .padding(.horizontal, 16) | |
| .padding(.vertical, 8) | |
| } | |
| // MARK: - Subviews | |
| @ViewBuilder | |
| private func row<Content: View, Accessory: View>( | |
| index: Int, | |
| title: String, | |
| @ViewBuilder content: () -> Content, | |
| @ViewBuilder trailingAccessory: () -> Accessory | |
| ) -> some View { | |
| HStack(alignment: .top, spacing: 16) { | |
| Text(title) | |
| .font(.system(size: 12, weight: .regular)) | |
| .foregroundColor(.secondary) | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| .accessibilityAddTraits(.isStaticText) | |
| HStack(alignment: .top, spacing: 8) { | |
| content() | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| trailingAccessory() | |
| } | |
| } | |
| .padding(.vertical, 8) | |
| .contentShape(Rectangle()) | |
| .background(pressedRow == index ? Color.black.opacity(0.04) : Color.clear) | |
| .onLongPressGesture(minimumDuration: 0.01, pressing: { pressing in | |
| withAnimation(.easeInOut(duration: 0.12)) { | |
| pressedRow = pressing ? index : nil | |
| } | |
| }, perform: { }) | |
| } | |
| private var copiedBadge: some View { | |
| HStack(spacing: 6) { | |
| Image(systemName: "checkmark.circle.fill") | |
| Text("Disalin") | |
| } | |
| .font(.footnote.weight(.semibold)) | |
| .foregroundColor(.green) | |
| .padding(.horizontal, 8) | |
| .padding(.vertical, 4) | |
| .background( | |
| Capsule(style: .continuous) | |
| .fill(Color.green.opacity(0.12)) | |
| ) | |
| .accessibilityHidden(true) | |
| } | |
| @ViewBuilder | |
| private func copyButton(action: @escaping () -> Void) -> some View { | |
| Button(action: action) { | |
| Image(systemName: "doc.on.doc") | |
| .font(.title3) | |
| } | |
| .buttonStyle(.plain) | |
| .tint(.secondary) | |
| .accessibilityLabel("Salin") | |
| } | |
| // MARK: - Helpers | |
| // FIX: use Binding instead of inout so we can update it from an escaping closure safely | |
| private func showCopiedTemporarily(_ flag: Binding<Bool>) { | |
| withAnimation(.spring(response: 0.25, dampingFraction: 0.9)) { | |
| flag.wrappedValue = true | |
| } | |
| DispatchQueue.main.asyncAfter(deadline: .now() + 1.4) { | |
| withAnimation(.easeOut(duration: 0.2)) { | |
| flag.wrappedValue = false | |
| } | |
| } | |
| } | |
| } | |
| struct ShippingInfoCourierCardV2: View { | |
| /// Sample data (replace with your real bindings/models) | |
| let shippingType: String | |
| let trackingNumber: String | |
| /// UI states (UI-only interactions) | |
| @State private var isAddressExpanded: Bool = false | |
| @State private var showCopiedPhone: Bool = false | |
| @State private var showCopiedAddress: Bool = false | |
| @State private var pressedRow: Int? = nil | |
| var body: some View { | |
| /// Card container | |
| VStack(alignment: .leading, spacing: 0) { | |
| row( | |
| index: 0, | |
| title: "Jenis Pengiriman", | |
| textToCopy: "", | |
| content: { | |
| HStack(alignment: .firstTextBaseline, spacing: 8) { | |
| Text(shippingType) | |
| .font(.system(size: 12, weight: .semibold)) | |
| .foregroundColor(.primary) | |
| .accessibilityLabel("Nama penerima") | |
| .accessibilityValue(shippingType) | |
| } | |
| }, | |
| trailingAccessory: { EmptyView() } | |
| ) | |
| Divider().opacity(0.06) | |
| row( | |
| index: 1, | |
| title: "Nomor Resi", | |
| textToCopy: trackingNumber, | |
| content: { | |
| Text(trackingNumber) | |
| .font(.system(size: 12, weight: .semibold)) | |
| .foregroundColor(.primary) | |
| .textSelection(.enabled) | |
| .accessibilityLabel("Nomor HP") | |
| .accessibilityValue(trackingNumber) | |
| }, | |
| trailingAccessory: { | |
| EmptyView() | |
| } | |
| ) | |
| .overlay(alignment: .trailing) { // if you used .trailingLastTextBaseline before, .trailing is safer | |
| } | |
| } | |
| .padding(12) | |
| .background( | |
| RoundedRectangle(cornerRadius: 16, style: .continuous) | |
| .fill(Color(.systemBackground)) | |
| ) | |
| .overlay( | |
| RoundedRectangle(cornerRadius: 16, style: .continuous) | |
| .stroke(Color.black.opacity(0.08), lineWidth: 1) | |
| ) | |
| .shadow(color: Color.black.opacity(0.04), radius: 8, x: 0, y: 2) | |
| .padding(.horizontal, 16) | |
| .padding(.vertical, 8) | |
| } | |
| // MARK: - Subviews | |
| @ViewBuilder | |
| private func row<Content: View, Accessory: View>( | |
| index: Int, | |
| title: String, | |
| textToCopy: String = "", | |
| @ViewBuilder content: () -> Content, | |
| @ViewBuilder trailingAccessory: () -> Accessory | |
| ) -> some View { | |
| HStack(alignment: .top, spacing: 16) { | |
| HStack(alignment: .center, spacing: 8) { | |
| Text(title) | |
| .font(.system(size: 12, weight: .regular)) | |
| .foregroundColor(.secondary) | |
| if textToCopy.isEmpty == false { | |
| Button { | |
| UIPasteboard.general.string = textToCopy | |
| UIImpactFeedbackGenerator(style: .light).impactOccurred() | |
| CUISimpleToast.show("Berhasil disalin: \(textToCopy)") | |
| } label: { | |
| Image(systemName: "doc.on.doc") | |
| .resizable() | |
| .scaledToFit() | |
| .frame(width: 12.6) | |
| .symbolRenderingMode(.hierarchical) | |
| } | |
| .buttonStyle(.plain) | |
| .foregroundColor(.primary) | |
| .opacity(1) | |
| .accessibilityLabel("Salin textToCopy") | |
| .accessibilityHint("Menyalin textToCopy ke clipboard") | |
| } | |
| } | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| .accessibilityAddTraits(.isStaticText) | |
| HStack(alignment: .top, spacing: 8) { | |
| content() | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| trailingAccessory() | |
| } | |
| } | |
| .padding(.vertical, 8) | |
| .contentShape(Rectangle()) | |
| .background(pressedRow == index ? Color.black.opacity(0.04) : Color.clear) | |
| .onLongPressGesture(minimumDuration: 0.01, pressing: { pressing in | |
| withAnimation(.easeInOut(duration: 0.12)) { | |
| pressedRow = pressing ? index : nil | |
| } | |
| }, perform: { }) | |
| } | |
| } | |
| /* | |
| ScrollView { | |
| ShippingInfoCardV2( | |
| recipientName: "Budhi Aghanim", | |
| phoneNumber: "+6282112345678", | |
| shippingAddress: """ | |
| Jln. Alamat Raya No.1 | |
| RT.002 RW.003, Perumahan | |
| Mutiara Gading, Dayeuh | |
| Kolot, 17462 | |
| """ | |
| ) | |
| .preferredColorScheme(.light) | |
| VStack(spacing: 20) { | |
| ShippingInfoCourierCardV2( | |
| shippingType: "JNE Regular", | |
| trackingNumber: "JNE1234567890ID" | |
| ) | |
| ShippingInfoCourierCardV2( | |
| shippingType: "JNE Regular", | |
| trackingNumber: "JNE1234567890ID" | |
| ) | |
| } | |
| } | |
| */ |
Author
luthviar
commented
Sep 26, 2025

Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment