diff --git a/ShopHub/ShopHub.xcodeproj/project.pbxproj b/ShopHub/ShopHub.xcodeproj/project.pbxproj index 71da725..f6fda65 100644 --- a/ShopHub/ShopHub.xcodeproj/project.pbxproj +++ b/ShopHub/ShopHub.xcodeproj/project.pbxproj @@ -22,8 +22,7 @@ 4271EC902A88729C00E2C85B /* SalesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4271EC8F2A88729C00E2C85B /* SalesView.swift */; }; 4271EC922A88753900E2C85B /* ProductView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4271EC912A88753900E2C85B /* ProductView.swift */; }; 428D1B9D2ABDF28300F65A65 /* LaunchScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428D1B9C2ABDF28300F65A65 /* LaunchScreenView.swift */; }; - 428D1B9F2ABE53F900F65A65 /* LaunchScreenPhase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428D1B9E2ABE53F900F65A65 /* LaunchScreenPhase.swift */; }; - 42994BF12A9D4B39007248F0 /* MockCartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42994BF02A9D4B39007248F0 /* MockCartView.swift */; }; + 428D1B9F2ABE53F900F65A65 /* LaunchScreenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428D1B9E2ABE53F900F65A65 /* LaunchScreenManager.swift */; }; 42A3BCEF2AC2A4C3006E9DCA /* DayNightToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A3BCEE2AC2A4C3006E9DCA /* DayNightToggleView.swift */; }; 42A3BCF32AC2ADF4006E9DCA /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A3BCF22AC2ADF4006E9DCA /* IconView.swift */; }; 42A3BCF92AC350AA006E9DCA /* OrderIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42A3BCF82AC350AA006E9DCA /* OrderIconView.swift */; }; @@ -51,6 +50,7 @@ AEB27CAA2A8857E5009E7A80 /* ToolBarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB27CA92A8857E5009E7A80 /* ToolBarStyle.swift */; }; AEBD622F2A99A1E10014C335 /* CartItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEBD622E2A99A1E10014C335 /* CartItemView.swift */; }; AEC27B292AACDCA000B22C40 /* TextFieldQuantityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEC27B282AACDCA000B22C40 /* TextFieldQuantityView.swift */; }; + AECB45CC2B1C24E30012D629 /* EmptyCartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECB45CB2B1C24E30012D629 /* EmptyCartView.swift */; }; AED85CD52A8B347B00E8B347 /* BannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AED85CD42A8B347B00E8B347 /* BannerView.swift */; }; AEE12AAE2AAE5199002FE29D /* AddToCartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE12AAD2AAE5199002FE29D /* AddToCartView.swift */; }; AEF37BC12A891F710003F44E /* ShopHubViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF37BC02A891F710003F44E /* ShopHubViewModel.swift */; }; @@ -83,8 +83,7 @@ 4271EC912A88753900E2C85B /* ProductView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductView.swift; sourceTree = ""; }; 428D1B9A2ABDDE4000F65A65 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 428D1B9C2ABDF28300F65A65 /* LaunchScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenView.swift; sourceTree = ""; }; - 428D1B9E2ABE53F900F65A65 /* LaunchScreenPhase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenPhase.swift; sourceTree = ""; }; - 42994BF02A9D4B39007248F0 /* MockCartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCartView.swift; sourceTree = ""; }; + 428D1B9E2ABE53F900F65A65 /* LaunchScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenManager.swift; sourceTree = ""; }; 42A3BCEE2AC2A4C3006E9DCA /* DayNightToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayNightToggleView.swift; sourceTree = ""; }; 42A3BCF22AC2ADF4006E9DCA /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; 42A3BCF82AC350AA006E9DCA /* OrderIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderIconView.swift; sourceTree = ""; }; @@ -114,6 +113,7 @@ AEB27CA92A8857E5009E7A80 /* ToolBarStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolBarStyle.swift; sourceTree = ""; }; AEBD622E2A99A1E10014C335 /* CartItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartItemView.swift; sourceTree = ""; }; AEC27B282AACDCA000B22C40 /* TextFieldQuantityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldQuantityView.swift; sourceTree = ""; }; + AECB45CB2B1C24E30012D629 /* EmptyCartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyCartView.swift; sourceTree = ""; }; AED85CD42A8B347B00E8B347 /* BannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerView.swift; sourceTree = ""; }; AEE12AAD2AAE5199002FE29D /* AddToCartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToCartView.swift; sourceTree = ""; }; AEF37BC02A891F710003F44E /* ShopHubViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopHubViewModel.swift; sourceTree = ""; }; @@ -166,19 +166,11 @@ isa = PBXGroup; children = ( 428D1B9C2ABDF28300F65A65 /* LaunchScreenView.swift */, - 428D1B9E2ABE53F900F65A65 /* LaunchScreenPhase.swift */, + 428D1B9E2ABE53F900F65A65 /* LaunchScreenManager.swift */, ); path = LaunchScreen; sourceTree = ""; }; - 42994BEF2A9D4AFE007248F0 /* Mock */ = { - isa = PBXGroup; - children = ( - 42994BF02A9D4B39007248F0 /* MockCartView.swift */, - ); - path = Mock; - sourceTree = ""; - }; 42A3BCF42AC34292006E9DCA /* UserComponent */ = { isa = PBXGroup; children = ( @@ -242,7 +234,6 @@ children = ( 428D1B9B2ABDF25800F65A65 /* LaunchScreen */, 428D1B9A2ABDDE4000F65A65 /* Info.plist */, - 42994BEF2A9D4AFE007248F0 /* Mock */, AEB27CAB2A886A02009E7A80 /* Extensions */, 4271EC822A88717E00E2C85B /* JSON Data */, 4269E81E2A944D8E00AB9475 /* ViewModels */, @@ -346,6 +337,7 @@ AEAC80192A9C1DBA0074F0CC /* CartTranscationView.swift */, 42F49D652A91CF12002C3C35 /* CartListView.swift */, AEBD622E2A99A1E10014C335 /* CartItemView.swift */, + AECB45CB2B1C24E30012D629 /* EmptyCartView.swift */, ); path = Cart; sourceTree = ""; @@ -524,7 +516,6 @@ AEAC801A2A9C1DBA0074F0CC /* CartTranscationView.swift in Sources */, 4237B37D2A8AC17900C214AE /* ProductDetailView.swift in Sources */, AE2B3B7B2A871EAE00889805 /* ContentView.swift in Sources */, - 42994BF12A9D4B39007248F0 /* MockCartView.swift in Sources */, 4271EC902A88729C00E2C85B /* SalesView.swift in Sources */, AEF37BC12A891F710003F44E /* ShopHubViewModel.swift in Sources */, AEE12AAE2AAE5199002FE29D /* AddToCartView.swift in Sources */, @@ -536,11 +527,12 @@ 4271EC922A88753900E2C85B /* ProductView.swift in Sources */, 4260484A2AC0B26D00C54B0C /* OrderView.swift in Sources */, AE2B3B792A871EAE00889805 /* ShopHubApp.swift in Sources */, - 428D1B9F2ABE53F900F65A65 /* LaunchScreenPhase.swift in Sources */, + 428D1B9F2ABE53F900F65A65 /* LaunchScreenManager.swift in Sources */, 4260484C2AC0B2B500C54B0C /* HistoryView.swift in Sources */, AEB27CAA2A8857E5009E7A80 /* ToolBarStyle.swift in Sources */, 42A3BCFB2AC350C8006E9DCA /* WalletIconView.swift in Sources */, 42A3BCFF2AC350E6006E9DCA /* BookmarkIconView.swift in Sources */, + AECB45CC2B1C24E30012D629 /* EmptyCartView.swift in Sources */, 42A3BCFD2AC350D8006E9DCA /* HistoryIconView.swift in Sources */, AE5FAA402AB79787004D1EFA /* NavigationCustomBackButton.swift in Sources */, 4271EC882A8871BE00E2C85B /* Bundle+Decode.swift in Sources */, diff --git a/ShopHub/ShopHub/ContentView.swift b/ShopHub/ShopHub/ContentView.swift index 484fdcf..904a2bd 100644 --- a/ShopHub/ShopHub/ContentView.swift +++ b/ShopHub/ShopHub/ContentView.swift @@ -8,29 +8,26 @@ import SwiftUI struct ContentView: View { - // State Object + // Environment - @StateObject var viewModel: ShopHubViewModel = ShopHubViewModel() - @StateObject var cartViewModel: CartViewModel = CartViewModel() + @Environment(LaunchScreenManager.self) private var launchScreenManager - // Environment Object + // Internal State - @EnvironmentObject var launchScreenManager: LaunchScreenManager + @State var viewModel: ShopHubViewModel = ShopHubViewModel() + @StateObject var cartViewModel: CartViewModel = CartViewModel() + @State var selectedTab: MenuTab = .products // App Storage @AppStorage("dark-mode") private var isDarkModeOn: Bool = false - // Internal State - - @State var selectedTab: MenuTab = .products - var body: some View { TabView(selection: $selectedTab) { MenuTabView() } .onAppear(perform: dismissLaunchScreen) - .environmentObject(viewModel) + .environment(viewModel) .environmentObject(cartViewModel) .environment(\.selectedMenuTab, $selectedTab) .preferredColorScheme(isDarkModeOn ? .dark : .light) @@ -43,5 +40,5 @@ struct ContentView: View { #Preview("Content View") { ContentView() - .environmentObject(LaunchScreenManager()) + .environment(LaunchScreenManager()) } diff --git a/ShopHub/ShopHub/JSON Data/ProductList.json b/ShopHub/ShopHub/JSON Data/ProductList.json index 53759b5..bdc4f46 100644 --- a/ShopHub/ShopHub/JSON Data/ProductList.json +++ b/ShopHub/ShopHub/JSON Data/ProductList.json @@ -49,7 +49,7 @@ }, { "id": 7, - "name": "Nintendo Switch™ with Neon Blue and Neon Red Joy‑Con™", + "name": "Nintendo Switch", "price": 299.99, "type": "Electronics", "description": "Play at home or on the go with one system The Nintendo Switch™ system is designed to go wherever you do, instantly transforming from a home console you play on TV to a portable system you can play anywhere. So you get more time to play the games you love, however you like.", diff --git a/ShopHub/ShopHub/LaunchScreen/LaunchScreenPhase.swift b/ShopHub/ShopHub/LaunchScreen/LaunchScreenManager.swift similarity index 58% rename from ShopHub/ShopHub/LaunchScreen/LaunchScreenPhase.swift rename to ShopHub/ShopHub/LaunchScreen/LaunchScreenManager.swift index 549243f..ab0742c 100644 --- a/ShopHub/ShopHub/LaunchScreen/LaunchScreenPhase.swift +++ b/ShopHub/ShopHub/LaunchScreen/LaunchScreenManager.swift @@ -1,5 +1,5 @@ // -// LaunchScreenPhase.swift +// LaunchScreenManager.swift // ShopHub // // Created by CHENGTAO on 9/22/23. @@ -13,8 +13,12 @@ enum LaunchScreenPhase { case completed } -final class LaunchScreenManager: ObservableObject { - @Published private(set) var state: LaunchScreenPhase = .first +/// This `Observable` class is the view model that manages the life cycle of launch screen. +@Observable +final class LaunchScreenManager { + + private(set) var state: LaunchScreenPhase = .first + func dismiss() { self.state = .second diff --git a/ShopHub/ShopHub/LaunchScreen/LaunchScreenView.swift b/ShopHub/ShopHub/LaunchScreen/LaunchScreenView.swift index a02e32f..6510128 100644 --- a/ShopHub/ShopHub/LaunchScreen/LaunchScreenView.swift +++ b/ShopHub/ShopHub/LaunchScreen/LaunchScreenView.swift @@ -7,11 +7,12 @@ import SwiftUI +/// This struct creates the initial lanuch screen view when user enters the app. struct LaunchScreenView: View { - // Environment Object - - @EnvironmentObject var launchScreenManager: LaunchScreenManager + // Environment + + @Environment(LaunchScreenManager.self) private var launchScreenManager // Internal State @@ -21,7 +22,7 @@ struct LaunchScreenView: View { private let timer = Timer.publish(every: 0.65, on: .main, in: .common).autoconnect() var body: some View { - ZStack{ + ZStack { background logo } diff --git a/ShopHub/ShopHub/Mock/MockCartView.swift b/ShopHub/ShopHub/Mock/MockCartView.swift deleted file mode 100644 index a08195d..0000000 --- a/ShopHub/ShopHub/Mock/MockCartView.swift +++ /dev/null @@ -1,174 +0,0 @@ -// -// MockCartView.swift -// ShopHub -// -// Created by CHENGTAO on 8/28/23. -// - -import SwiftUI - -// 1. Define your models -//struct MProduct: Identifiable, Equatable { -// let id: Int -// let name: String -// let type: String -// // Add more properties as needed -//} - -// 1. Modify the MProduct struct to conform to Hashable -struct MProduct: Identifiable, Equatable, Hashable { - let id: Int - let name: String - let type: String - // Add more properties as needed -} - -class MockCartViewModel: ObservableObject { - - // MARK: List -// @Published var products: [MProduct] = [ -// MProduct(id: 1, name: "SteamDeck", type: "Electronics"), -// MProduct(id: 2, name: "Clothing1", type: "Clothing"), -// MProduct(id: 3, name: "House1", type: "House"), -// MProduct(id: 4, name: "Art1", type: "Art"), -// MProduct(id: 5, name: "Food1", type: "Food") -// ] -// -// var sectionHeaders: [String] { -// Set(products.map { $0.type }).sorted() -// } -// -// func sectionContent(for type: String) -> [MProduct] { -// products.filter { $0.type == type } -// } -// -// func delete(product: MProduct) { -// if let index = products.firstIndex(of: product) { -// products.remove(at: index) -// } -// } - - // MARK: Dictionary - // 2. Initialize the products dictionary -// @Published var products: [MProduct: Int] = [ -// MProduct(id: 1, name: "SteamDeck", type: "Electronics"): 1, -// MProduct(id: 2, name: "Clothing1", type: "Clothing"): 2, -// MProduct(id: 3, name: "House1", type: "House"): 3, -// MProduct(id: 4, name: "Art1", type: "Art"): 4, -// MProduct(id: 5, name: "Food1", type: "Food"): 5 -// ] - @Published var products: [Product: Int] = [:] - - - var sectionHeaders: [String] { - Set(products.keys.map { $0.type }).sorted() - } - - func sectionContent(for type: String) -> [Product] { - products.keys.filter { $0.type == type } - } - - func delete(product: Product) { - products.removeValue(forKey: product) - } - - init() { - let products: [Product] = Bundle.main.decode("ProductList.json") - self.products = [ - products[0]: 1, - products[1]: 2, - products[2]: 3, - products[3]: 4, - ] - } - -} - -struct MockCartListView: View { - - @ObservedObject var cart: MockCartViewModel - // Internal State - @State private var searchText: String = "" - @State private var isLogoPressed = false - - - var body: some View { - NavigationStack { - HStack { - ZStack { - List { - ForEach(cart.sectionHeaders, id: \.self) { type in -// Section(header: Text(type)) - Section - { - ForEach(cart.sectionContent(for: type)) { product in - ExtractedView(product: product, cart: cart) - } - } header: { - Text("HHHHH") - .font(.subheadline) - } - } - } - } - } - .toolBarStyle(title: "Shopping Cart", titleImage: "cart.fill", isLogoPressed: $isLogoPressed) - .searchable(text: $searchText, prompt: "Search for your product in cart") - } - } -} - -#Preview { - @StateObject var cart: MockCartViewModel = MockCartViewModel() - return MockCartListView(cart: cart) -} - -//@main -//struct CartApp: App { -// @StateObject private var cart = CartViewModel() -// -// var body: some Scene { -// WindowGroup { -// NavigationView { -// CartListView() -// .environmentObject(cart) -// } -// } -// } -//} - - -struct ExtractedView: View { - let product: Product - @ObservedObject var cart = MockCartViewModel() - var body: some View { - HStack { - Image(systemName: "trash.fill") - Text(product.name) - HStack { - Button { - buttonFunc() - } label: { - Image(systemName: "minus.square") - } - } - .buttonStyle(.borderless) - } - .padding([.leading, .trailing, .top], 10) - .background(Color(.systemBackground)) - .cornerRadius(20) - .swipeActions(edge: .leading, allowsFullSwipe: false) { - Button(role: .destructive) { - cart.delete(product: product) - } label: { - Label("Delete", systemImage: "trash") - } - .tint(.red) - .labelStyle(.titleAndIcon) - } - } - - func buttonFunc() { - print("Button is pressed.") - } -} diff --git a/ShopHub/ShopHub/Models/Product.swift b/ShopHub/ShopHub/Models/Product.swift index 2e11197..d59f2d1 100644 --- a/ShopHub/ShopHub/Models/Product.swift +++ b/ShopHub/ShopHub/Models/Product.swift @@ -8,22 +8,36 @@ import Foundation import SwiftUI -// a model that defines the Sale Product -struct Product: Codable, Identifiable, Hashable { +/// a model that defines the Sale Product. +public struct Product: Codable, Identifiable, Hashable { // database required properties - var id: Int - var name: String - var price: Double - var type: String - var description: String? - var image: String + public var id: Int + + public var name: String + + public var price: Double + + public var type: String + + public var description: String? + + public var image: String + + public init(id: Int, name: String, price: Double, type: String, description: String? = nil, image: String) { + self.id = id + self.name = name + self.price = price + self.type = type + self.description = description + self.image = image + } } extension Product: Comparable { /// Conformation to `Comparable` protocol to allow for `sorted()` operations. - static func <(lhs: Product, rhs: Product) -> Bool { + public static func <(lhs: Product, rhs: Product) -> Bool { if lhs.name != rhs.name { return lhs.name < rhs.name } else if lhs.price != rhs.price { diff --git a/ShopHub/ShopHub/ShopHubApp.swift b/ShopHub/ShopHub/ShopHubApp.swift index 29b2a93..327d0c1 100644 --- a/ShopHub/ShopHub/ShopHubApp.swift +++ b/ShopHub/ShopHub/ShopHubApp.swift @@ -9,7 +9,9 @@ import SwiftUI @main struct ShopHubApp: App { - @StateObject var launchScreenManager: LaunchScreenManager = LaunchScreenManager() + + @State var launchScreenManager: LaunchScreenManager = LaunchScreenManager() + var body: some Scene { WindowGroup { ZStack { @@ -18,7 +20,7 @@ struct ShopHubApp: App { LaunchScreenView() } } - .environmentObject(launchScreenManager) + .environment(launchScreenManager) } } } diff --git a/ShopHub/ShopHub/User Interface/Accessory Views/AddToCartView.swift b/ShopHub/ShopHub/User Interface/Accessory Views/AddToCartView.swift index 9ced50a..d4cd7d5 100644 --- a/ShopHub/ShopHub/User Interface/Accessory Views/AddToCartView.swift +++ b/ShopHub/ShopHub/User Interface/Accessory Views/AddToCartView.swift @@ -9,20 +9,27 @@ import SwiftUI struct AddToCartView: View { + // Parameter + @Binding var isAddToCartShown: Bool let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + // Internal State + @State private var second: Int = 0 // keep track of how many seconds the view has been appeared var body: some View { HStack { + Circle() - .stroke(.blue, lineWidth: 2.0) + .stroke(.blue, lineWidth: 3.0) .frame(width: 25) .overlay { Image(systemName: "checkmark") .foregroundStyle(.green) } + Text("Added to cart") .foregroundStyle(.white) .font(.headline) diff --git a/ShopHub/ShopHub/User Interface/Accessory Views/CartSubmissionView.swift b/ShopHub/ShopHub/User Interface/Accessory Views/CartSubmissionView.swift index 1bd5bde..2339e18 100644 --- a/ShopHub/ShopHub/User Interface/Accessory Views/CartSubmissionView.swift +++ b/ShopHub/ShopHub/User Interface/Accessory Views/CartSubmissionView.swift @@ -9,10 +9,12 @@ import SwiftUI struct CartSubmissionView: View { + // Parameter /// show the submission view when `Continue` is pressed. @Binding var showSubmission: Bool // Internal State + @State private var isAnimating: Bool = false @State private var second: Int = 0 // keep track of how many seconds the view has been appeared diff --git a/ShopHub/ShopHub/User Interface/Accessory Views/ToolbarView/ToolBarStyle.swift b/ShopHub/ShopHub/User Interface/Accessory Views/ToolbarView/ToolBarStyle.swift index 56498a0..bc6999e 100644 --- a/ShopHub/ShopHub/User Interface/Accessory Views/ToolbarView/ToolBarStyle.swift +++ b/ShopHub/ShopHub/User Interface/Accessory Views/ToolbarView/ToolBarStyle.swift @@ -9,6 +9,9 @@ import SwiftUI /// a view modifier that is used for customize the top bar leading logo and brand name struct ToolBarStyleModifier: ViewModifier { + + // Parameters + let title: String let titleImage: String @Binding var isLogoPressed: Bool diff --git a/ShopHub/ShopHub/User Interface/Menu Navigation/MenuTabView.swift b/ShopHub/ShopHub/User Interface/Menu Navigation/MenuTabView.swift index 2250133..9660ce9 100644 --- a/ShopHub/ShopHub/User Interface/Menu Navigation/MenuTabView.swift +++ b/ShopHub/ShopHub/User Interface/Menu Navigation/MenuTabView.swift @@ -11,7 +11,10 @@ import SwiftUI struct MenuTabView: View { @EnvironmentObject var shoppingCart: CartViewModel - @StateObject var menuViewModel: MenuTabViewModel = MenuTabViewModel() + + // Internal State + + @State var menuViewModel: MenuTabViewModel = MenuTabViewModel() var body: some View { ForEach(menuViewModel.tabs) { tab in @@ -27,8 +30,8 @@ struct MenuTabView: View { } /// a view model that stores the information about the menu tabs -final class MenuTabViewModel: ObservableObject { - @Published var tabs: [MenuTab] = MenuTab.allCases +@Observable final class MenuTabViewModel { + var tabs: [MenuTab] = MenuTab.allCases } /// A enum that represents the menu tab at the bottom of the screen diff --git a/ShopHub/ShopHub/User Interface/Page Views/Cart/CartListView.swift b/ShopHub/ShopHub/User Interface/Page Views/Cart/CartListView.swift index eed887b..fab0e26 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/Cart/CartListView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/Cart/CartListView.swift @@ -85,61 +85,11 @@ struct CartListView: View { } } -struct EmptyCartView: View { - - @Environment(\.selectedMenuTab) private var selectMenuTab - @Environment(\.colorScheme) private var colorScheme // detect whether dark mode is turned on - - var body: some View { - VStack(spacing: 20) { - - Text(colorScheme == .dark ? "dark" : "light") - - Image("empty-cart") - .resizable() - .scaledToFit() - .frame(width: 150, height: 150) - .shadow(radius: 10) - .offset(x: -15) - .padding(.top, 50) - - Text("Add items to start a cart") - .foregroundStyle(.primary) - .font(.system(.title2, weight: .semibold)) - - Text("Once you add items from the store, your products from cart will appear.") - .multilineTextAlignment(.center) - .padding(.horizontal, 10) - .font(.system(size: 18)) - .foregroundStyle(.primary) - - Button { - selectMenuTab.wrappedValue = .products - } label: { - Text("Start shopping") - .frame(maxWidth: 120) - .font(.system(.subheadline, weight: .medium)) - .foregroundStyle(.primary) - } - .padding(.vertical, 10) - .buttonStyle(.bordered) - - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.gray.opacity(0.1)) - } -} - - #Preview { CartListView() .environmentObject(CartViewModel()) } -#Preview("Empty cart") { - EmptyCartView() -} - //#Preview("Cart Item View") { // let products: [Product] = Bundle.main.decode("ProductList.json") // let product = products[1] diff --git a/ShopHub/ShopHub/User Interface/Page Views/Cart/EmptyCartView.swift b/ShopHub/ShopHub/User Interface/Page Views/Cart/EmptyCartView.swift new file mode 100644 index 0000000..ab5fba3 --- /dev/null +++ b/ShopHub/ShopHub/User Interface/Page Views/Cart/EmptyCartView.swift @@ -0,0 +1,55 @@ +// +// EmptyCartView.swift +// ShopHub +// +// Created by Yongye Tan on 12/2/23. +// + +import SwiftUI + +struct EmptyCartView: View { + + @Environment(\.selectedMenuTab) private var selectMenuTab + @Environment(\.colorScheme) private var colorScheme // detect whether dark mode is turned on + + var body: some View { + VStack(spacing: 20) { + + Image("empty-cart") + .resizable() + .scaledToFit() + .frame(width: 150, height: 150) + .shadow(radius: 10) + .offset(x: -15) + .padding(.top, 50) + + Text("Add items to start a cart") + .foregroundStyle(.primary) + .font(.system(.title2, weight: .semibold)) + + Text("Once you add items from the store, your products from cart will appear.") + .multilineTextAlignment(.center) + .padding(.horizontal, 10) + .font(.system(size: 18)) + .foregroundStyle(.primary) + + Button { + selectMenuTab.wrappedValue = .products + } label: { + Text("Start shopping") + .frame(maxWidth: 120) + .font(.system(.subheadline, weight: .medium)) + .foregroundStyle(.primary) + } + .padding(.vertical, 10) + .buttonStyle(.bordered) + + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.gray.opacity(0.1)) + } +} + +#Preview("Empty cart") { + EmptyCartView() +} diff --git a/ShopHub/ShopHub/User Interface/Page Views/Products/Banner/BannerCardView.swift b/ShopHub/ShopHub/User Interface/Page Views/Products/Banner/BannerCardView.swift index 99cd777..0ff909e 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/Products/Banner/BannerCardView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/Products/Banner/BannerCardView.swift @@ -38,5 +38,5 @@ struct BannerCardView: View { #Preview("cardview") { BannerView() - .environmentObject(ShopHubViewModel()) + .environment(ShopHubViewModel()) } diff --git a/ShopHub/ShopHub/User Interface/Page Views/Products/Banner/BannerView.swift b/ShopHub/ShopHub/User Interface/Page Views/Products/Banner/BannerView.swift index 2fc5ab2..ad83e5b 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/Products/Banner/BannerView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/Products/Banner/BannerView.swift @@ -9,12 +9,12 @@ import SwiftUI struct BannerView: View { - // Environment Object + // Environment - @EnvironmentObject var viewModel: ShopHubViewModel + @Environment(ShopHubViewModel.self) private var viewModel var body: some View { - ScrollView(.horizontal, showsIndicators: false) { + ScrollView(.horizontal) { LazyHStack { ForEach(viewModel.allProducts) { product in BannerCardView(product: product) @@ -22,7 +22,7 @@ struct BannerView: View { } .scrollTargetLayout(isEnabled: true) } - .scrollTargetBehavior(.viewAligned(limitBehavior: .automatic)) + .scrollIndicators(.hidden) .frame(height: 200) // TODO: (Optional) add Animation } @@ -30,5 +30,5 @@ struct BannerView: View { #Preview { BannerView() - .environmentObject(ShopHubViewModel()) + .environment(ShopHubViewModel()) } diff --git a/ShopHub/ShopHub/User Interface/Page Views/Products/ProductsView.swift b/ShopHub/ShopHub/User Interface/Page Views/Products/ProductsView.swift index 76b33e0..d6db14d 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/Products/ProductsView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/Products/ProductsView.swift @@ -9,9 +9,9 @@ import SwiftUI struct ProductsView: View { - // Environment Object + // Environment - @EnvironmentObject var viewModel: ShopHubViewModel + @Environment(ShopHubViewModel.self) private var viewModel // Internal State @@ -70,7 +70,7 @@ struct ProductsView: View { #Preview { TabView { ProductsView() - .environmentObject(ShopHubViewModel()) + .environment(ShopHubViewModel()) .tabItem { Label("Product", systemImage: "bag") } diff --git a/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/ProductDetailView.swift b/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/ProductDetailView.swift index 70745d2..63196c2 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/ProductDetailView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/ProductDetailView.swift @@ -22,7 +22,7 @@ struct ProductDetailView: View { // Environment object @EnvironmentObject var shoppingCart: CartViewModel - + // Calculate total price base on products' quantity private var totalPrice: Double { let singlePrice = product.price @@ -33,89 +33,9 @@ struct ProductDetailView: View { var body: some View { NavigationStack { + ZStack { - VStack { - // MARK: Top image - ZStack { - Image("\(product.image)") - .resizable(resizingMode: .stretch) - .aspectRatio(contentMode: .fit) - .frame(width: 200, height: 200) - .clipShape(RoundedRectangle(cornerRadius: 20)) - } - - Divider() - - // MARK: Name and type stack - HStack { - - TypeTagView(productType: product.type, - backgroundColor: .red, - fontSize: 12) - - Spacer() - - Text(product.name) - .fontWeight(.bold) - .font(.system(size: 20)) - - } - .padding() - - // MARK: Description - Text(product.description ?? "N/A") - .multilineTextAlignment(.leading) - .font(.body) - .fontWeight(.light) - .padding() - - // MARK: Price && Quantity - HStack { - - Text(totalPrice, format: .currency(code: Locale.current.currency?.identifier ?? "USD")) - .font(.system(size: 20)) - .fontWeight(.heavy) - - Spacer() - - HStack { - Button { - quantity -= 1 - } label: { - Image(systemName: "minus.square") - } - .disabled(quantity == 1) - - TextFieldQuantityView(value: $quantity, focusState: _isQuantityFocused) - - Button { - quantity += 1 - } label: { - Image(systemName: "plus.square") - } - } - .font(.system(size: 20)) - .fontWeight(.medium) - } - .padding() - - Button { - isAddButtonPressed.toggle() - - // give a 2 second delay of adding an product to the cart - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - shoppingCart.add(product: product, with: quantity) - } - } label: { - Text("Add to cart") - .frame(maxWidth: .infinity) - } - .padding() - .buttonStyle(.borderedProminent) - .sensoryFeedback(.success, trigger: isAddButtonPressed) // add haptic effect when item adds to cart - } - .blur(radius: isAddButtonPressed ? 2 : 0) - .disabled(isAddButtonPressed) + productDetailView if isAddButtonPressed { AddToCartView(isAddToCartShown: $isAddButtonPressed) @@ -140,6 +60,105 @@ struct ProductDetailView: View { } } + + var productDetailView: some View { + VStack { + + // MARK: Top image + + Image("\(product.image)") + .resizable(resizingMode: .stretch) + .aspectRatio(contentMode: .fit) + .frame(width: 200, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 20)) + + Divider() + + // MARK: Name and type stack + tagAndNameView + + // MARK: Description + Text(product.description ?? "N/A") + .multilineTextAlignment(.leading) + .font(.body) + .fontWeight(.light) + .padding() + + // MARK: Price && Quantity + priceAndQuantityView + + Button { + isAddButtonPressed.toggle() + + // give a 2 second delay of adding an product to the cart + + Task { @MainActor in + try await Task.sleep(nanoseconds: 2_000_000_000) + shoppingCart.add(product: product, with: quantity) + } + + } label: { + Text("Add to cart") + .frame(maxWidth: .infinity) + } + .padding() + + .buttonStyle(.borderedProminent) + + // add haptic effect when item adds to cart + .sensoryFeedback(.success, trigger: isAddButtonPressed) + } + .blur(radius: isAddButtonPressed ? 2 : 0) + .disabled(isAddButtonPressed) + } + + private var tagAndNameView: some View { + HStack { + + TypeTagView(productType: product.type, + backgroundColor: .red, + fontSize: 12) + + Spacer() + + Text(product.name) + .fontWeight(.bold) + .font(.system(size: 20)) + + } + .padding() + } + + private var priceAndQuantityView: some View { + HStack { + + Text(totalPrice, format: .currency(code: Locale.current.currency?.identifier ?? "USD")) + .font(.system(size: 20)) + .fontWeight(.heavy) + + Spacer() + + HStack { + Button { + quantity -= 1 + } label: { + Image(systemName: "minus.square") + } + .disabled(quantity == 1) + + TextFieldQuantityView(value: $quantity, focusState: _isQuantityFocused) + + Button { + quantity += 1 + } label: { + Image(systemName: "plus.square") + } + } + .font(.system(size: 20)) + .fontWeight(.medium) + } + .padding() + } } #Preview("Clothing") { diff --git a/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/ProductView.swift b/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/ProductView.swift index 109fb57..ca20369 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/ProductView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/ProductView.swift @@ -15,7 +15,9 @@ struct ProductView: View { let product: Product var body: some View { + VStack(alignment: .center){ + // put the type to the top right ZStack(alignment: .topTrailing) { Image(product.image) @@ -59,11 +61,11 @@ struct ProductView: View { } } -//#Preview("SteamDeck") { -// let products: [Product] = Bundle.main.decode("ProductList.json") -// let product: Product = products[0] -// return ProductView(product: product) -//} +#Preview("SteamDeck") { + let products: [Product] = Bundle.main.decode("ProductList.json") + let product: Product = products[0] + return ProductView(product: product) +} // //#Preview("Clothing") { // let products: [Product] = Bundle.main.decode("ProductList.json") diff --git a/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/SalesView.swift b/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/SalesView.swift index 39c9e30..a4227d0 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/SalesView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/Products/Sale/SalesView.swift @@ -10,31 +10,46 @@ import SwiftUI struct SalesView: View { - // Enviornment Object - @EnvironmentObject var viewModel: ShopHubViewModel + // Enviornment + + @Environment(ShopHubViewModel.self) private var viewModel + + // Parameter let columns = [GridItem](repeating: GridItem(.flexible()), count: 2) var body: some View { ScrollView { + HStack { + Text("Sales") .font(.system(size: 45)) .padding(.horizontal, 20) .fontWeight(.bold) + Spacer() + } .padding(-2) + LazyVGrid(columns: columns, spacing: 20) { + ForEach(viewModel.allProducts) { product in + NavigationLink { + ProductDetailView(product: product) + } label: { + ProductView(product: product) } + } } + } } } diff --git a/ShopHub/ShopHub/User Interface/Page Views/User/SettingsComponent/DayNightToggleView.swift b/ShopHub/ShopHub/User Interface/Page Views/User/SettingsComponent/DayNightToggleView.swift index e2a5087..0cc24e2 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/User/SettingsComponent/DayNightToggleView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/User/SettingsComponent/DayNightToggleView.swift @@ -9,10 +9,8 @@ import SwiftUI struct DayNightToggleView: View { - // store user preference in the disk @AppStorage("dark-mode") var isDarkModeOn: Bool = false - var body: some View { HStack { Toggle(isOn: $isDarkModeOn, label: { diff --git a/ShopHub/ShopHub/User Interface/Page Views/User/UserComponentsView.swift b/ShopHub/ShopHub/User Interface/Page Views/User/UserComponentsView.swift index c1dc6fc..f1ce1fa 100644 --- a/ShopHub/ShopHub/User Interface/Page Views/User/UserComponentsView.swift +++ b/ShopHub/ShopHub/User Interface/Page Views/User/UserComponentsView.swift @@ -9,7 +9,10 @@ import Foundation import SwiftUI struct UserComponentsView: View { - @StateObject var viewModel: UserComponentsViewModel = UserComponentsViewModel() + + // Internal State + + @State var viewModel: UserComponentsViewModel = UserComponentsViewModel() var body: some View { List (viewModel.pages) { page in @@ -25,8 +28,8 @@ struct UserComponentsView: View { /// a view model that stores the information about the UserComponents tabs -final class UserComponentsViewModel: ObservableObject { - @Published var pages: [UserComponents] = UserComponents.allCases +@Observable final class UserComponentsViewModel { + var pages: [UserComponents] = UserComponents.allCases } /// A enum that represents the user component of the screen diff --git a/ShopHub/ShopHub/ViewModels/ShopHubViewModel.swift b/ShopHub/ShopHub/ViewModels/ShopHubViewModel.swift index a6e926a..bcc8f5d 100644 --- a/ShopHub/ShopHub/ViewModels/ShopHubViewModel.swift +++ b/ShopHub/ShopHub/ViewModels/ShopHubViewModel.swift @@ -7,12 +7,14 @@ import Foundation -class ShopHubViewModel: ObservableObject { +@Observable +class ShopHubViewModel { + /// public storage to store all the products with only read access public private(set) var allProducts: [Product] /// filtered products to be shown to the users. Filtered based on the search text. - @Published var filteredProducts: [Product] + var filteredProducts: [Product]! /// the search text to filter out the products var searchText = ""