Instantly share code, notes, and snippets.
Last active
September 16, 2025 04:43
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save luthviar/66b38d050c9fa6137f3074ec817a261a 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
| // | |
| // ContentView19.swift | |
| // Coba14July2025 | |
| // | |
| // Created by Luthfi Abdurrahim on 21/08/25. | |
| // | |
| import SwiftUI | |
| import ThemeEazy | |
| // MARK: - Models | |
| struct Product: Identifiable { | |
| let id = UUID() | |
| let name: String | |
| let price: Double | |
| let image: String | |
| let category: ProductCategory | |
| let description: String | |
| let rating: Double | |
| } | |
| enum ProductCategory: String, CaseIterable { | |
| case all = "All" | |
| case ipCamera = "IP Camera" | |
| case cloudRecording = "Cloud Recording" | |
| case accessories = "Accessories" | |
| case storage = "Storage" | |
| } | |
| // MARK: - View Model | |
| class ShoppingViewModel: ObservableObject { | |
| @Published var products: [Product] = [] | |
| @Published var cartItems: [Product] = [] | |
| @Published var selectedCategory: ProductCategory = .all | |
| @Published var isLoading = false | |
| init() { | |
| loadProducts() | |
| /// Simulate 20 items in cart initially | |
| for _ in 0..<20 { | |
| cartItems.append(sampleProducts.randomElement()!) | |
| } | |
| } | |
| var filteredProducts: [Product] { | |
| if selectedCategory == .all { | |
| return products | |
| } | |
| return products.filter { $0.category == selectedCategory } | |
| } | |
| var cartItemCount: Int { | |
| cartItems.count | |
| } | |
| func addToCart(_ product: Product) { | |
| cartItems.append(product) | |
| } | |
| func removeFromCart(_ product: Product) { | |
| if let index = cartItems.firstIndex(where: { $0.id == product.id }) { | |
| cartItems.remove(at: index) | |
| } | |
| } | |
| func loadProducts() { | |
| isLoading = true | |
| // Simulate API call | |
| DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { | |
| self.products = sampleProducts | |
| self.isLoading = false | |
| } | |
| } | |
| } | |
| // MARK: - Sample Data | |
| let sampleProducts: [Product] = [ | |
| Product(name: "WiFi Security Camera", price: 299.99, image: "camera.fill", category: .ipCamera, description: "High-definition wireless security camera with night vision", rating: 4.5), | |
| Product(name: "Smart Doorbell Camera", price: 199.99, image: "bell.fill", category: .ipCamera, description: "Smart doorbell with two-way audio and motion detection", rating: 4.7), | |
| Product(name: "PTZ Camera", price: 449.99, image: "video.fill", category: .ipCamera, description: "Pan-tilt-zoom camera with remote control", rating: 4.3), | |
| Product(name: "Cloud Storage 1TB", price: 99.99, image: "icloud.fill", category: .cloudRecording, description: "1TB cloud storage for 1 year", rating: 4.6), | |
| Product(name: "Premium Cloud Plan", price: 199.99, image: "icloud.and.arrow.up.fill", category: .cloudRecording, description: "Premium cloud recording with AI features", rating: 4.8), | |
| Product(name: "Camera Mount", price: 29.99, image: "mount.fill", category: .accessories, description: "Universal camera mounting bracket", rating: 4.2), | |
| Product(name: "Power Adapter", price: 19.99, image: "powerplug.fill", category: .accessories, description: "12V power adapter for cameras", rating: 4.4), | |
| Product(name: "MicroSD Card 64GB", price: 39.99, image: "sdcard.fill", category: .storage, description: "High-speed microSD card for local storage", rating: 4.5) | |
| ] | |
| // MARK: - Main Content View | |
| struct ContentView19: View { | |
| @StateObject private var viewModel = ShoppingViewModel() | |
| @State private var showingCart = false | |
| var body: some View { | |
| NavigationView { | |
| VStack(spacing: 0) { | |
| // Header | |
| headerView | |
| // Category Filter | |
| categoryFilterView | |
| // Products Grid | |
| if viewModel.isLoading { | |
| Spacer() | |
| ProgressView("Loading products...") | |
| .font(.headline) | |
| Spacer() | |
| } else { | |
| productsGridView | |
| } | |
| } | |
| .navigationBarHidden(true) | |
| } | |
| .sheet(isPresented: $showingCart) { | |
| CartView(viewModel: viewModel) | |
| } | |
| } | |
| // MARK: - Header View | |
| private var headerView: some View { | |
| HStack { | |
| Text("Antares Eazy") | |
| .font(LGNFont.heading5) | |
| .foregroundColor(.black) | |
| Spacer() | |
| Button(action: { | |
| showingCart = true | |
| }) { | |
| ZStack(alignment: .topTrailing) { | |
| Image(systemName: "cart.fill") | |
| .font(.title2) | |
| .foregroundColor(.black) | |
| if viewModel.cartItemCount > 0 { | |
| Text("\(viewModel.cartItemCount)") | |
| .font(.caption2) | |
| .fontWeight(.bold) | |
| .foregroundColor(.white) | |
| .frame(minWidth: 20, minHeight: 20) | |
| .background(Color.red) | |
| .clipShape(Circle()) | |
| .offset(x: 8, y: -8) | |
| } | |
| } | |
| } | |
| } | |
| .padding(.horizontal, 20) | |
| .padding(.top, 10) | |
| .padding(.bottom, 20) | |
| } | |
| // MARK: - Category Filter View | |
| private var categoryFilterView: some View { | |
| ScrollView(.horizontal, showsIndicators: false) { | |
| HStack(spacing: 12) { | |
| ForEach(ProductCategory.allCases, id: \.self) { category in | |
| CategoryButton( | |
| title: category.rawValue, | |
| isSelected: viewModel.selectedCategory == category | |
| ) { | |
| withAnimation(.easeInOut(duration: 0.3)) { | |
| viewModel.selectedCategory = category | |
| } | |
| } | |
| } | |
| } | |
| .padding(.horizontal, 20) | |
| } | |
| .padding(.bottom, 0) | |
| } | |
| // MARK: - Products Grid View | |
| private var productsGridView: some View { | |
| ScrollView { | |
| LazyVStack { | |
| /// Use enumerated array to have stable RandomAccessCollection and access index & product | |
| ForEach(Array(viewModel.filteredProducts.enumerated()), id: \.element.id) { index, product in | |
| ProductCardViewLiveStore(product: ProductCardViewLiveStore.ProductCardData(imageUrl: "", name: "IP Camera", price: 0, originalPrice: 0, discountPercentage: "5%")) | |
| .padding(.top, index == 0 ? 11 : 0) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - Category Button | |
| struct CategoryButton: View { | |
| let title: String | |
| let isSelected: Bool | |
| let action: () -> Void | |
| var body: some View { | |
| Button(action: action) { | |
| Text(title) | |
| .font(.system(size: 12, weight: .semibold)) | |
| .foregroundColor(isSelected ? .white : .blue) | |
| .padding(.horizontal, 16) | |
| .padding(.vertical, 4) | |
| } | |
| .background( | |
| ZStack { | |
| // Background fill | |
| RoundedRectangle(cornerRadius: 25) | |
| .fill(isSelected ? Color.blue : Color.clear) | |
| // Custom border using inset | |
| if !isSelected { | |
| RoundedRectangle(cornerRadius: 25) | |
| .fill(Color.blue) | |
| .mask( | |
| RoundedRectangle(cornerRadius: 25) | |
| .stroke(lineWidth: 2) // Double the desired width | |
| ) | |
| } | |
| } | |
| ) | |
| } | |
| } | |
| // MARK: - Product Card | |
| struct ProductCard: View { | |
| let product: Product | |
| let onAddToCart: () -> Void | |
| var body: some View { | |
| VStack(alignment: .leading, spacing: 8) { | |
| // Product Image | |
| ZStack { | |
| RoundedRectangle(cornerRadius: 12) | |
| .fill(Color.gray.opacity(0.1)) | |
| .frame(height: 140) | |
| Image(systemName: product.image) | |
| .font(.system(size: 40)) | |
| .foregroundColor(.blue) | |
| } | |
| // Product Info | |
| VStack(alignment: .leading, spacing: 4) { | |
| Text(product.name) | |
| .font(.system(size: 14, weight: .medium)) | |
| .lineLimit(2) | |
| .multilineTextAlignment(.leading) | |
| // Rating | |
| HStack(spacing: 2) { | |
| ForEach(0..<5) { index in | |
| Image(systemName: index < Int(product.rating) ? "star.fill" : "star") | |
| .font(.caption2) | |
| .foregroundColor(.yellow) | |
| } | |
| Text(String(format: "%.1f", product.rating)) | |
| .font(.caption2) | |
| .foregroundColor(.gray) | |
| } | |
| // Price and Add Button | |
| HStack { | |
| Text("$\(String(format: "%.2f", product.price))") | |
| .font(.system(size: 16, weight: .bold)) | |
| .foregroundColor(.black) | |
| Spacer() | |
| Button(action: onAddToCart) { | |
| Image(systemName: "plus.circle.fill") | |
| .font(.title3) | |
| .foregroundColor(.blue) | |
| } | |
| } | |
| } | |
| .padding(.horizontal, 4) | |
| } | |
| .padding(8) | |
| .background(Color.white) | |
| .cornerRadius(12) | |
| .shadow(color: .black.opacity(0.1), radius: 3, x: 0, y: 2) | |
| } | |
| } | |
| // MARK: - Cart View | |
| struct CartView: View { | |
| @ObservedObject var viewModel: ShoppingViewModel | |
| @Environment(\.presentationMode) var presentationMode | |
| private var totalPrice: Double { | |
| viewModel.cartItems.reduce(0) { $0 + $1.price } | |
| } | |
| var body: some View { | |
| NavigationView { | |
| VStack { | |
| if viewModel.cartItems.isEmpty { | |
| // Empty Cart State | |
| Spacer() | |
| Image(systemName: "cart") | |
| .font(.system(size: 60)) | |
| .foregroundColor(.gray) | |
| Text("Your cart is empty") | |
| .font(.headline) | |
| .foregroundColor(.gray) | |
| .padding(.top, 10) | |
| Spacer() | |
| } else { | |
| // Cart Items List | |
| List { | |
| ForEach(viewModel.cartItems) { product in | |
| CartItemRow(product: product) { | |
| viewModel.removeFromCart(product) | |
| } | |
| } | |
| } | |
| .listStyle(PlainListStyle()) | |
| // Cart Summary | |
| VStack(spacing: 15) { | |
| Divider() | |
| HStack { | |
| Text("Total (\(viewModel.cartItemCount) items)") | |
| .font(.headline) | |
| Spacer() | |
| Text("$\(String(format: "%.2f", totalPrice))") | |
| .font(.headline) | |
| .fontWeight(.bold) | |
| } | |
| Button(action: { | |
| // Checkout action | |
| }) { | |
| Text("Checkout") | |
| .font(.headline) | |
| .foregroundColor(.white) | |
| .frame(maxWidth: .infinity) | |
| .padding(.vertical, 15) | |
| .background(Color.blue) | |
| .cornerRadius(12) | |
| } | |
| } | |
| .padding() | |
| .background(Color.white) | |
| } | |
| } | |
| .navigationTitle("Shopping Cart") | |
| .navigationBarItems( | |
| trailing: Button("Done") { | |
| presentationMode.wrappedValue.dismiss() | |
| } | |
| ) | |
| } | |
| } | |
| } | |
| // MARK: - Cart Item Row | |
| struct CartItemRow: View { | |
| let product: Product | |
| let onRemove: () -> Void | |
| var body: some View { | |
| HStack(spacing: 12) { | |
| // Product Image | |
| Image(systemName: product.image) | |
| .font(.title2) | |
| .foregroundColor(.blue) | |
| .frame(width: 40, height: 40) | |
| .background(Color.gray.opacity(0.1)) | |
| .cornerRadius(8) | |
| // Product Info | |
| VStack(alignment: .leading, spacing: 4) { | |
| Text(product.name) | |
| .font(.system(size: 16, weight: .medium)) | |
| .lineLimit(1) | |
| Text(product.category.rawValue) | |
| .font(.caption) | |
| .foregroundColor(.gray) | |
| } | |
| Spacer() | |
| // Price and Remove Button | |
| VStack(alignment: .trailing, spacing: 4) { | |
| Text("$\(String(format: "%.2f", product.price))") | |
| .font(.system(size: 16, weight: .bold)) | |
| Button(action: onRemove) { | |
| Image(systemName: "trash") | |
| .font(.caption) | |
| .foregroundColor(.red) | |
| } | |
| } | |
| } | |
| .padding(.vertical, 8) | |
| } | |
| } | |
| // MARK: - Preview | |
| struct ContentView_Previews: PreviewProvider { | |
| static var previews: some View { | |
| ContentView() | |
| } | |
| } | |
| // | |
| // ContentView18.swift | |
| // Coba14July2025 | |
| // | |
| // Created by Luthfi Abdurrahim on 19/08/25. | |
| // | |
| import CoreResource | |
| import SwiftUI | |
| import ThemeEazy | |
| public struct ProductCardViewLiveStore: View { | |
| public struct ProductCardData: Identifiable { | |
| public let id: UUID = UUID() | |
| public let imageUrl: String | |
| public let name: String | |
| public let price: Double | |
| public let originalPrice: Double? | |
| public let discountPercentage: String? | |
| public var formattedPrice: String { | |
| let formatter = NumberFormatter() | |
| formatter.numberStyle = .currency | |
| formatter.currencyCode = "IDR" | |
| formatter.currencySymbol = "Rp " | |
| formatter.maximumFractionDigits = 0 | |
| return formatter.string(from: NSNumber(value: price)) ?? "Rp \(Int(price))" | |
| } | |
| public var formattedOriginalPrice: String { | |
| guard let originalPrice else { return "" } | |
| let formatter = NumberFormatter() | |
| formatter.numberStyle = .currency | |
| formatter.currencyCode = "IDR" | |
| formatter.currencySymbol = "Rp " | |
| formatter.maximumFractionDigits = 0 | |
| return formatter.string(from: NSNumber(value: originalPrice)) ?? "Rp \(Int(originalPrice))" | |
| } | |
| } | |
| public let product: ProductCardData | |
| public var body: some View { | |
| HStack(alignment: .top) { | |
| AsyncImage(url: URL(string: "https://iot-ihsmart-images-public-stage.oss-ap-southeast-5.aliyuncs.com/eazy/perangkat/foto%20produk/BARDI%20-%20Smart%20IP%20Camera%20Static%20Indoor_icon.jpg")) { newImage in | |
| newImage | |
| .resizable() | |
| .scaledToFit() | |
| .frame(height: 120) | |
| } placeholder: { | |
| Image(.ipCamExample) | |
| .resizable() | |
| .scaledToFit() | |
| .frame(height: 120) | |
| } | |
| .padding(.trailing, 16) | |
| ZStack(alignment: .topLeading) { | |
| VStack(alignment: .leading, spacing: 0) { | |
| Text(product.name) | |
| .font(LGNFont.bodySmallSemiBold) | |
| .foregroundColor(Color.customApplicationColor(.tertiary80)) | |
| .padding(.bottom, 8) | |
| Text("Rp 364.000") | |
| .font(LGNFont.bodyMediumBold) | |
| .foregroundColor(Color.customApplicationColor(.tertiary80)) | |
| HStack(spacing: 1) { | |
| Text("Rp 473.000") | |
| .font(LGNFont.bodyCaptionRegular) | |
| .foregroundColor(Color.customApplicationColor(.black70)) | |
| .strikethrough() | |
| ZStack(alignment: .leading) { | |
| Image(.discountLabelBackgroundRed) | |
| .resizable() | |
| Text("23%") | |
| .font(LGNFont.bodyCaptionSemiBold) | |
| .foregroundColor(LGNColor.error500) | |
| .padding(.leading, 6) | |
| .padding(.trailing, 2) | |
| .layoutPriority(1) | |
| } | |
| .frame(height: 16) | |
| } | |
| } | |
| VStack { | |
| Spacer() | |
| HStack { | |
| Spacer() | |
| Button(action: { | |
| // Handle buy action | |
| print("Buy button tapped for") | |
| }) { | |
| Text("Beli") | |
| .font(LGNFont.bodySmallSemiBold) | |
| .foregroundColor(.white) | |
| .padding(.vertical, 7) | |
| .padding(.horizontal, 22) | |
| .background(LGNColor.primary500) | |
| .cornerRadius(8) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| .padding(.vertical, 13) | |
| .padding(.horizontal, 16) | |
| .background(Color.white) | |
| .cornerRadius(12) | |
| .shadow( | |
| color: Color.black.opacity(0.12), | |
| radius: 6.24, | |
| x: 0, | |
| y: 0 | |
| ) | |
| .padding(.horizontal, 26) | |
| .padding(.bottom, 14) | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WhatsApp.Video.2025-08-21.at.14.22.56.mp4