diff --git a/SWFrontUI/ShopwiseFrontEndUI.xcodeproj/project.xcworkspace/xcuserdata/jchang.xcuserdatad/UserInterfaceState.xcuserstate b/SWFrontUI/ShopwiseFrontEndUI.xcodeproj/project.xcworkspace/xcuserdata/jchang.xcuserdatad/UserInterfaceState.xcuserstate index 4f911f0..1bfebb6 100644 Binary files a/SWFrontUI/ShopwiseFrontEndUI.xcodeproj/project.xcworkspace/xcuserdata/jchang.xcuserdatad/UserInterfaceState.xcuserstate and b/SWFrontUI/ShopwiseFrontEndUI.xcodeproj/project.xcworkspace/xcuserdata/jchang.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/SWFrontUI/ShopwiseFrontEndUI.xcodeproj/xcuserdata/jchang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/SWFrontUI/ShopwiseFrontEndUI.xcodeproj/xcuserdata/jchang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..0829a8e --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI.xcodeproj/xcuserdata/jchang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/SWFrontUI/ShopwiseFrontEndUI/AppToolbar.swift b/SWFrontUI/ShopwiseFrontEndUI/AppToolbar.swift index e8eeb80..023b3a9 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/AppToolbar.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/AppToolbar.swift @@ -6,6 +6,16 @@ struct AppToolbar: ViewModifier { func body(content: Content) -> some View { content .toolbar { + ToolbarItem(placement: .principal) { + HStack(spacing: 8) { + Image("ShopwiseLogo") + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + Text("ShopWise") + .font(.headline) + } + } ToolbarItem(placement: .topBarTrailing) { Button { showAccount = true diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AccentColor.colorset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AccentColor.colorset/Contents.json index 0afb3cf..0bda595 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,11 +1,38 @@ { - "colors": [ + "colors" : [ { - "idiom": "universal" + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xB6", + "red" : "0x6B" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" } ], - "info": { - "author": "xcode", - "version": 1 + "info" : { + "author" : "xcode", + "version" : 1 } } diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/Contents.json index c70a5bf..8c6ea6d 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,35 +1,38 @@ { - "images": [ + "images" : [ { - "idiom": "universal", - "platform": "ios", - "size": "1024x1024" + "filename" : "ShopwiseLogo_cropped.jpg", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" }, { - "appearances": [ + "appearances" : [ { - "appearance": "luminosity", - "value": "dark" + "appearance" : "luminosity", + "value" : "dark" } ], - "idiom": "universal", - "platform": "ios", - "size": "1024x1024" + "filename" : "ShopwiseLogo_cropped 1.jpg", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" }, { - "appearances": [ + "appearances" : [ { - "appearance": "luminosity", - "value": "tinted" + "appearance" : "luminosity", + "value" : "tinted" } ], - "idiom": "universal", - "platform": "ios", - "size": "1024x1024" + "filename" : "ShopwiseLogo_cropped 2.jpg", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], - "info": { - "author": "xcode", - "version": 1 + "info" : { + "author" : "xcode", + "version" : 1 } } diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped 1.jpg b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped 1.jpg new file mode 100644 index 0000000..1008ea1 Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped 1.jpg differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped 2.jpg b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped 2.jpg new file mode 100644 index 0000000..1008ea1 Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped 2.jpg differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped.jpg b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped.jpg new file mode 100644 index 0000000..1008ea1 Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/AppIcon.appiconset/ShopwiseLogo_cropped.jpg differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/PrimaryBlue.colorset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/PrimaryBlue.colorset/Contents.json new file mode 100644 index 0000000..40efb33 --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/PrimaryBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xA5", + "green" : "0x3D", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0xA5", + "green" : "0x3D", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/SecondaryGold.colorset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/SecondaryGold.colorset/Contents.json new file mode 100644 index 0000000..aa448e5 --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/SecondaryGold.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x1C", + "green" : "0xB8", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0x1C", + "green" : "0xB8", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/ShopwiseLogo.imageset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/ShopwiseLogo.imageset/Contents.json new file mode 100644 index 0000000..58e57b7 --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/ShopwiseLogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "rsz_shopwiselogo_cropped.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/ShopwiseLogo.imageset/rsz_shopwiselogo_cropped.jpg b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/ShopwiseLogo.imageset/rsz_shopwiselogo_cropped.jpg new file mode 100644 index 0000000..9fcf1e9 Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/ShopwiseLogo.imageset/rsz_shopwiselogo_cropped.jpg differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/cardenaslogo.imageset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/cardenaslogo.imageset/Contents.json new file mode 100644 index 0000000..a058811 --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/cardenaslogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "cardenaslogo-removebg-preview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/cardenaslogo.imageset/cardenaslogo-removebg-preview.png b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/cardenaslogo.imageset/cardenaslogo-removebg-preview.png new file mode 100644 index 0000000..db4aa41 Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/cardenaslogo.imageset/cardenaslogo-removebg-preview.png differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/majorlogo.imageset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/majorlogo.imageset/Contents.json new file mode 100644 index 0000000..98d3a62 --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/majorlogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "majorlogo-removebg-preview.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/majorlogo.imageset/majorlogo-removebg-preview.png b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/majorlogo.imageset/majorlogo-removebg-preview.png new file mode 100644 index 0000000..5fe7ae5 Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/majorlogo.imageset/majorlogo-removebg-preview.png differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/northgatelogo.imageset/Adobe Express - file.png b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/northgatelogo.imageset/Adobe Express - file.png new file mode 100644 index 0000000..06d319a Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/northgatelogo.imageset/Adobe Express - file.png differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/northgatelogo.imageset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/northgatelogo.imageset/Contents.json new file mode 100644 index 0000000..20962aa --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/northgatelogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Adobe Express - file.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/superlogo.imageset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/superlogo.imageset/Contents.json new file mode 100644 index 0000000..ee2a737 --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/superlogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "superlogo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/superlogo.imageset/superlogo.png b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/superlogo.imageset/superlogo.png new file mode 100644 index 0000000..097e837 Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/superlogo.imageset/superlogo.png differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/vallartalogo.imageset/Contents.json b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/vallartalogo.imageset/Contents.json new file mode 100644 index 0000000..7a23eae --- /dev/null +++ b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/vallartalogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "vallartalogo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/vallartalogo.imageset/vallartalogo.png b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/vallartalogo.imageset/vallartalogo.png new file mode 100644 index 0000000..1b00fca Binary files /dev/null and b/SWFrontUI/ShopwiseFrontEndUI/Assets.xcassets/vallartalogo.imageset/vallartalogo.png differ diff --git a/SWFrontUI/ShopwiseFrontEndUI/AuthView.swift b/SWFrontUI/ShopwiseFrontEndUI/AuthView.swift index b3a8106..2a328f4 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/AuthView.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/AuthView.swift @@ -32,11 +32,11 @@ struct AuthView: View { VStack(spacing: 10) { Image(systemName: "cart") .font(.system(size: 42, weight: .semibold)) - .foregroundStyle(.blue) + .foregroundStyle(Theme.primary) Text("ShopWise") .font(.title3) - .foregroundStyle(.blue) + .foregroundStyle(Theme.primary) .bold() } .padding(.top, 10) diff --git a/SWFrontUI/ShopwiseFrontEndUI/CartStore.swift b/SWFrontUI/ShopwiseFrontEndUI/CartStore.swift index 88516e3..c0d90c8 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/CartStore.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/CartStore.swift @@ -10,8 +10,18 @@ struct CartLineItem: Identifiable, Hashable { var quantity: Int let groupId: String? let groupTitle: String? + let storeName: String? - init(id: String = String(),name: String,unit: String,price: Double, quantity: Int = 1,groupId: String? = nil,groupTitle: String? = nil){ + init( + id: String = String(), + name: String, + unit: String, + price: Double, + quantity: Int = 1, + groupId: String? = nil, + groupTitle: String? = nil, + storeName: String? = nil + ){ self.id = id self.name = name self.unit = unit @@ -19,6 +29,7 @@ struct CartLineItem: Identifiable, Hashable { self.quantity = quantity self.groupId = groupId self.groupTitle = groupTitle + self.storeName = storeName } var lineTotal: Double{ @@ -49,7 +60,7 @@ final class CartStore: ObservableObject { add(id: name, name: name, unit: unit, price: price) } - func add(id: String, name: String, unit: String, price: Double) { + func add(id: String, name: String, unit: String, price: Double, storeName: String? = nil) { if let idx = items.firstIndex(where: { $0.id == id }) { items[idx].quantity += 1 } else { @@ -59,13 +70,14 @@ final class CartStore: ObservableObject { name: name, unit: unit, price: price, - quantity: 1 + quantity: 1, + storeName: storeName ) ) } } - func add(recipeId: String, recipeTitle: String, name: String, unit: String, price: Double) { + func add(recipeId: String, recipeTitle: String, name: String, unit: String, price: Double, storeName: String? = nil) { let id = "\(recipeId)::\(name)" if let idx = items.firstIndex(where: { $0.id == id }) { items[idx].quantity += 1 @@ -77,7 +89,8 @@ final class CartStore: ObservableObject { price: items[idx].price, quantity: items[idx].quantity, groupId: recipeId, - groupTitle: recipeTitle + groupTitle: recipeTitle, + storeName: storeName ?? items[idx].storeName ) } } else { @@ -89,7 +102,8 @@ final class CartStore: ObservableObject { price: price, quantity: 1, groupId: recipeId, - groupTitle: recipeTitle + groupTitle: recipeTitle, + storeName: storeName ) ) } diff --git a/SWFrontUI/ShopwiseFrontEndUI/CartView.swift b/SWFrontUI/ShopwiseFrontEndUI/CartView.swift index 92fbd61..a75e427 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/CartView.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/CartView.swift @@ -54,6 +54,14 @@ struct CartView: View { .foregroundStyle(.secondary) } Spacer() + Button(role: .destructive) { + removeGroup(group.id) + } label: { + Image(systemName: "trash") + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .padding(.trailing, 2) Image(systemName: expandedRecipeIds.contains(group.id) ? "chevron.up" : "chevron.down") .foregroundStyle(.secondary) } @@ -62,9 +70,16 @@ struct CartView: View { if expandedRecipeIds.contains(group.id) { Divider() - ForEach(Array(group.items.enumerated()), id: \.element.id) { index, item in - itemRow(item) - if index != group.items.count - 1 { + let storeGroups = groupItemsByStore(group.items) + ForEach(storeGroups) { storeGroup in + storeHeader(storeGroup.store) + ForEach(Array(storeGroup.items.enumerated()), id: \.element.id) { index, item in + itemRow(item) + if index != storeGroup.items.count - 1 { + Divider() + } + } + if storeGroup.id != storeGroups.last?.id { Divider() } } @@ -91,12 +106,11 @@ struct CartView: View { Text("No individual items") .foregroundStyle(.secondary) } else { - ForEach(individualItems) { item in - itemRow(item) - } - .onDelete { indexSet in - for idx in indexSet { - cartStore.remove(individualItems[idx]) + let storeGroups = groupItemsByStore(individualItems) + ForEach(storeGroups) { storeGroup in + storeHeader(storeGroup.store) + ForEach(storeGroup.items) { item in + itemRow(item) } } } @@ -123,7 +137,7 @@ struct CartView: View { } } } - .navigationTitle("Cart") + .navigationTitle("Shopping Cart") .appToolbar() .navigationDestination(isPresented: $showShoppingList) { ShoppingListView() @@ -169,6 +183,11 @@ struct CartView: View { VStack(alignment: .leading, spacing: 4) { Text(item.name) .font(.headline) + if let store = item.storeName, !store.isEmpty { + Text(store) + .font(.caption.weight(.semibold)) + .foregroundStyle(Theme.secondary) + } if !item.unit.isEmpty { Text(item.unit) .font(.subheadline) @@ -201,6 +220,48 @@ struct CartView: View { } } .padding(.vertical, 4) + .swipeActions { + Button(role: .destructive) { + cartStore.remove(item) + } label: { + Label("Remove", systemImage: "trash") + } + } + } + + private struct StoreGroup: Identifiable { + let id: String + let store: String + let items: [CartLineItem] + } + + private func groupItemsByStore(_ items: [CartLineItem]) -> [StoreGroup] { + let grouped = Dictionary(grouping: items) { item in + let store = item.storeName?.trimmingCharacters(in: .whitespacesAndNewlines) + return (store?.isEmpty == false) ? store! : "Unknown Store" + } + return grouped.map { key, value in + StoreGroup(id: key, store: key, items: value) + } + .sorted { $0.store.localizedCaseInsensitiveCompare($1.store) == .orderedAscending } + } + + @ViewBuilder + private func storeHeader(_ store: String) -> some View { + HStack { + Text(store) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(Theme.primary) + Spacer() + } + .padding(.top, 4) + } + + private func removeGroup(_ groupId: String) { + let itemsToRemove = cartStore.items.filter { $0.groupId == groupId } + for item in itemsToRemove { + cartStore.remove(item) + } } private func groupSubtotal(for group: RecipeGroup) -> Double { diff --git a/SWFrontUI/ShopwiseFrontEndUI/MapView.swift b/SWFrontUI/ShopwiseFrontEndUI/MapView.swift index 7e5e6ea..296aa40 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/MapView.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/MapView.swift @@ -219,7 +219,9 @@ struct MapView: View { ranchMarketSection locationSection } + .listStyle(.plain) .navigationTitle("ShopWise") + .navigationBarTitleDisplayMode(.inline) .appToolbar() .task { } // kroger stores are hardcoded — swap for live fetch once kroger_locations is populated .onAppear { locationManager.requestPermission() } diff --git a/SWFrontUI/ShopwiseFrontEndUI/OnboardingSurveyView.swift b/SWFrontUI/ShopwiseFrontEndUI/OnboardingSurveyView.swift index 3204c3a..e3b9780 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/OnboardingSurveyView.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/OnboardingSurveyView.swift @@ -10,14 +10,12 @@ struct OnboardingSurveyView: View { @State private var errorMessage: String? let dietOptions = [ - "Vegetarian", "Vegan", "Pescatarian", - "Keto", "Gluten-Free", "Dairy-Free", - "Halal", "Kosher" + "Vegan", "Vegetarian", "Pescatarian", "Dairy-Free" ] let allergyOptions = [ - "Peanuts", "Tree Nuts", "Dairy", "Eggs", - "Soy", "Wheat", "Shellfish", "Fish" + "Peanuts", "Tree Nuts", "Dairy", "Egg", + "Fish", "Shellfish", "Soy", "Wheat" ] var onComplete: (() -> Void)? = nil @@ -87,7 +85,7 @@ struct OnboardingSurveyView: View { .disabled(isSaving) } } - .navigationTitle("Your Preferences") + .navigationTitle("Dietary Compass") .task { await loadPreferences() } @@ -157,8 +155,8 @@ private struct PreferenceChipGrid: View { .padding(.horizontal, 12) .padding(.vertical, 8) .frame(maxWidth: .infinity) - .background(isSelected ? Color.blue.opacity(0.18) : Color(.systemGray6)) - .foregroundStyle(isSelected ? Color.blue : Color.primary) + .background(isSelected ? Theme.primary.opacity(0.18) : Color(.systemGray6)) + .foregroundStyle(isSelected ? Theme.primary : Color.primary) .clipShape(Capsule()) } .buttonStyle(.plain) diff --git a/SWFrontUI/ShopwiseFrontEndUI/ProfileView.swift b/SWFrontUI/ShopwiseFrontEndUI/ProfileView.swift index 5510e17..d718272 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/ProfileView.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/ProfileView.swift @@ -17,7 +17,7 @@ struct ProfileView: View { HStack { Image(systemName: "person.circle.fill") .font(.system(size: 36)) - .foregroundStyle(.blue) + .foregroundStyle(Theme.primary) VStack(alignment: .leading) { Text(greetingText) diff --git a/SWFrontUI/ShopwiseFrontEndUI/RecipeView.swift b/SWFrontUI/ShopwiseFrontEndUI/RecipeView.swift index 8f3ebfe..a9e63b4 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/RecipeView.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/RecipeView.swift @@ -35,6 +35,8 @@ struct RecipeView: View { @State private var isLoadingMore = false @State private var excludedIngredientsByRecipe: [Int: Set] = [:] @State private var expandedInstructionIds: Set = [] + @State private var expandedIngredientIds: Set = [] + @State private var selectedMatchIndex: [String: Int] = [:] // Shopping mode – persists across recipe expansions @State private var shoppingMode: ShoppingMode = .value @@ -52,6 +54,7 @@ struct RecipeView: View { List { content } .listStyle(.plain) .navigationTitle("ShopWise") + .navigationBarTitleDisplayMode(.inline) .searchable(text: $query, prompt: "Search recipes…") .appToolbar() .task { await loadRecipes(reset: true) } @@ -128,7 +131,9 @@ struct RecipeView: View { } VStack(alignment: .leading, spacing: 6) { - Text(recipe.title).font(.headline).lineLimit(2) + Text(recipe.title) + .font(.headline) + .lineLimit(expandedID == recipe.id ? nil : 2) } Spacer() @@ -143,81 +148,91 @@ struct RecipeView: View { CardContainer { VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Ingredients").font(.headline) - Spacer() + Button { + withAnimation(.easeInOut(duration: 0.2)) { + toggleIngredients(recipe.id) + } + } label: { + HStack { + Text("Ingredients").font(.headline) + Spacer() + Image(systemName: expandedIngredientIds.contains(recipe.id) ? "chevron.up" : "chevron.down") + .foregroundStyle(.secondary) + } } - - // Mode picker – only shown once matches are loaded - let allMatches = matchesByRecipe[recipe.id] ?? [] - if !allMatches.isEmpty { - HStack(spacing: 4) { - ForEach(ShoppingMode.allCases, id: \.self) { mode in - let isSelected = shoppingMode == mode - Button { - shoppingMode = mode - } label: { - HStack(spacing: 5) { - Image(systemName: mode == .value ? "tag.fill" : "clock.arrow.2.circlepath") - .font(.system(size: 12, weight: .semibold)) - Text(mode.label) - .font(.system(size: 14, weight: .semibold)) + .buttonStyle(.plain) + + if expandedIngredientIds.contains(recipe.id) { + // Mode picker – only shown once matches are loaded + let allMatches = matchesByRecipe[recipe.id] ?? [] + if !allMatches.isEmpty { + HStack(spacing: 4) { + ForEach(ShoppingMode.allCases, id: \.self) { mode in + let isSelected = shoppingMode == mode + Button { + shoppingMode = mode + } label: { + HStack(spacing: 5) { + Image(systemName: mode == .value ? "tag.fill" : "clock.arrow.2.circlepath") + .font(.system(size: 12, weight: .semibold)) + Text(mode.label) + .font(.system(size: 14, weight: .semibold)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 9) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(isSelected ? Color.primary : Color.clear) + ) + .foregroundStyle(isSelected ? Color(UIColor.systemBackground) : Color.secondary) } - .frame(maxWidth: .infinity) - .padding(.vertical, 9) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(isSelected ? Color.primary : Color.clear) - ) - .foregroundStyle(isSelected ? Color(UIColor.systemBackground) : Color.secondary) + .buttonStyle(.plain) } - .buttonStyle(.plain) } + .padding(4) + .background(Color(.systemGray5)) + .clipShape(RoundedRectangle(cornerRadius: 13)) } - .padding(4) - .background(Color(.systemGray5)) - .clipShape(RoundedRectangle(cornerRadius: 13)) - } - let isLoadingMatches = loadingMatchesFor.contains(recipe.id) - let (allGrouped, ingredients) = processMatches(allMatches) - // For Time Save mode, compute the optimal store plan once here. - let timeSavePlan: TimeSavePlan? = shoppingMode == .timeSave - ? computeTimeSavePlan(allGrouped: allGrouped, ingredients: ingredients) - : nil + let isLoadingMatches = loadingMatchesFor.contains(recipe.id) + let (allGrouped, ingredients) = processMatches(allMatches) + // For Time Save mode, compute the optimal store plan once here. + let timeSavePlan: TimeSavePlan? = shoppingMode == .timeSave + ? computeTimeSavePlan(allGrouped: allGrouped, ingredients: ingredients) + : nil - if isLoadingMatches { - HStack { - Spacer() - VStack(spacing: 6) { - ProgressView() - Text("Finding products…") - .font(.caption) - .foregroundStyle(.secondary) + if isLoadingMatches { + HStack { + Spacer() + VStack(spacing: 6) { + ProgressView() + Text("Finding products…") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() } - Spacer() - } - .padding(.vertical, 8) + .padding(.vertical, 8) - } else if allMatches.isEmpty { - ForEach(recipe.ingredientList, id: \.self) { item in - ingredientRow(recipeId: recipe.id, item: item) - } + } else if allMatches.isEmpty { + ForEach(recipe.ingredientList, id: \.self) { item in + ingredientRow(recipeId: recipe.id, item: item) + } - } else { - // Store summary banner — shown for both modes - let valueStores: [String] = { - // Unique stores that would be visited if picking cheapest per ingredient - var seen = Set() - var result: [String] = [] - for item in ingredients { - if let store = (allGrouped[item] ?? []).first?.matched_store, - seen.insert(store).inserted { - result.append(store) + } else { + // Store summary banner — shown for both modes + let valueStores: [String] = { + // Unique stores that would be visited if picking cheapest per ingredient + var seen = Set() + var result: [String] = [] + for item in ingredients { + if let store = (allGrouped[item] ?? []).first?.matched_store, + seen.insert(store).inserted { + result.append(store) + } } - } - return result - }() + return result + }() let displayStores = timeSavePlan?.stores ?? valueStores if !displayStores.isEmpty { @@ -247,6 +262,7 @@ struct RecipeView: View { IngredientMatchRow( ingredient: item, matches: displayMatches, + selectedRank: bindingForSelectedRank(recipeId: recipe.id, ingredient: item, matchCount: displayMatches.count), isExcluded: excludedIngredientsByRecipe[recipe.id]?.contains(item) == true, onToggle: { toggleIngredient(recipeId: recipe.id, item: item) }, onAdd: { match in @@ -255,13 +271,15 @@ struct RecipeView: View { recipeTitle: recipe.title, name: match.matched_name ?? item, unit: match.matched_size ?? "", - price: match.min_price ?? 0 + price: match.min_price ?? 0, + storeName: match.matched_store ) } ) } } - + } + if let instructions = recipe.instructions, !instructions.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Divider().padding(.vertical, 2) @@ -310,7 +328,13 @@ struct RecipeView: View { if let plan { return plan.matchFor[item] ?? allForItem.first } - return allForItem.first // Value: cheapest + // Value mode: honor the user's selected rank if available + let key = matchKey(recipeId: recipe.id, ingredient: item) + let idx = selectedMatchIndex[key] ?? 0 + if idx >= 0 && idx < allForItem.count { + return allForItem[idx] + } + return allForItem.first }() if let top { cartStore.add( @@ -318,7 +342,8 @@ struct RecipeView: View { recipeTitle: recipe.title, name: top.matched_name ?? item, unit: top.matched_size ?? "", - price: top.min_price ?? 0 + price: top.min_price ?? 0, + storeName: top.matched_store ) } else { cartStore.add( @@ -330,7 +355,11 @@ struct RecipeView: View { } } label: { let excluded = excludedIngredientsByRecipe[recipe.id] ?? [] - let selectedCount = max(0, recipe.ingredientList.count - excluded.count) + let matches = matchesByRecipe[recipe.id] ?? [] + let ingredients = matches.isEmpty + ? recipe.ingredientList + : orderedIngredients(from: matches) + let selectedCount = max(0, ingredients.count - excluded.count) VStack(spacing: 4) { Text("\(selectedCount) selected") .font(.footnote) @@ -339,6 +368,7 @@ struct RecipeView: View { } .frame(maxWidth: .infinity) } + .tint(Theme.primary) .buttonStyle(.borderedProminent) } } @@ -519,13 +549,39 @@ struct RecipeView: View { expandedInstructionIds.insert(recipeId) } } + + private func toggleIngredients(_ recipeId: Int) { + if expandedIngredientIds.contains(recipeId) { + expandedIngredientIds.remove(recipeId) + } else { + expandedIngredientIds.insert(recipeId) + } + } + + private func matchKey(recipeId: Int, ingredient: String) -> String { + "\(recipeId)::\(ingredient)" + } + + private func bindingForSelectedRank(recipeId: Int, ingredient: String, matchCount: Int) -> Binding { + let key = matchKey(recipeId: recipeId, ingredient: ingredient) + return Binding( + get: { + let idx = selectedMatchIndex[key] ?? 0 + if matchCount == 0 { return 0 } + return min(max(idx, 0), matchCount - 1) + }, + set: { newValue in + selectedMatchIndex[key] = newValue + } + ) + } private func ingredientRow(recipeId: Int, item: String) -> some View { let isExcluded = excludedIngredientsByRecipe[recipeId]?.contains(item) == true return Button { toggleIngredient(recipeId: recipeId, item: item) } label: { HStack(spacing: 10) { Image(systemName: isExcluded ? "minus.circle" : "checkmark.circle.fill") - .foregroundStyle(isExcluded ? Color.secondary : Color.green) + .foregroundStyle(isExcluded ? Color.secondary : Theme.secondary) Text(item) .foregroundStyle(isExcluded ? Color.secondary : Color.primary) .strikethrough(isExcluded, color: .secondary) @@ -568,19 +624,18 @@ struct RecipeView: View { struct IngredientMatchRow: View { let ingredient: String let matches: [ScrapedRecipeMatch] + @Binding var selectedRank: Int let isExcluded: Bool let onToggle: () -> Void let onAdd: (ScrapedRecipeMatch) -> Void - @State private var selectedRank = 0 - var body: some View { VStack(alignment: .leading, spacing: 6) { Button(action: onToggle) { HStack(spacing: 10) { Image(systemName: isExcluded ? "minus.circle" : "checkmark.circle.fill") - .foregroundStyle(isExcluded ? Color.secondary : Color.green) + .foregroundStyle(isExcluded ? Color.secondary : Theme.secondary) Text(ingredient) .font(.subheadline) .foregroundStyle(isExcluded ? Color.secondary : Color.primary) @@ -637,14 +692,14 @@ struct IngredientMatchRow: View { VStack(alignment: .trailing, spacing: 4) { Text(match.displayPrice) .font(.caption.weight(.semibold)) - .foregroundStyle(.blue) - + .foregroundStyle(Theme.primary) + Button { onAdd(match) } label: { Image(systemName: "cart.badge.plus") .font(.system(size: 14)) .padding(6) - .background(Color.blue.opacity(0.12)) - .foregroundStyle(.blue) + .background(Theme.primary.opacity(0.12)) + .foregroundStyle(Theme.primary) .clipShape(Circle()) } .buttonStyle(.plain) @@ -659,6 +714,10 @@ struct IngredientMatchRow: View { .padding(.vertical, 2) // Reset the picker whenever the matches array changes (e.g. store filter switched). // Without this, selectedRank can point past the end of the new array and nothing renders. - .onChange(of: matches.map { $0.id }) { _, _ in selectedRank = 0 } + .onChange(of: matches.map { $0.id }) { _, _ in + if selectedRank >= matches.count { + selectedRank = 0 + } + } } } diff --git a/SWFrontUI/ShopwiseFrontEndUI/SearchView.swift b/SWFrontUI/ShopwiseFrontEndUI/SearchView.swift index eb8ece9..82e5d49 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/SearchView.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/SearchView.swift @@ -34,6 +34,7 @@ struct SearchView: View { @State private var results: [ScrapedIngredient] = [] @State private var isLoading = false @State private var errorText: String? = nil + @State private var expandedItemId: String? = nil @State private var pageSize = 50 @State private var offset = 0 @@ -44,9 +45,10 @@ struct SearchView: View { ScrollView { VStack(alignment: .leading, spacing: 14) { - Text("ShopWise") + /*Text("ShopWise") .font(.system(size: 34, weight: .bold)) .padding(.top, 6) + .padding(.bottom, 4)*/ // Search bar HStack(spacing: 10) { @@ -124,12 +126,24 @@ struct SearchView: View { unit: item.quantity, priceText: item.displayPrice, // var not func storeName: selectedTab.id == "All" ? item.store : nil, + isExpanded: expandedItemId == item.id, + onToggle: { + withAnimation(.easeInOut(duration: 0.2)) { + if expandedItemId == item.id { + expandedItemId = nil + } else { + expandedItemId = item.id + } + } + }, onAdd: { + let fallbackStore = selectedTab.id == "All" ? nil : selectedTab.id cartStore.add( id: item.id, name: item.name, unit: item.quantity ?? "", - price: item.price ?? 0 + price: item.price ?? 0, + storeName: item.store.isEmpty ? fallbackStore : item.store ) } ) @@ -220,12 +234,12 @@ struct StoreChip: View { } .padding(.horizontal, 14) .padding(.vertical, 8) - .background(isSelected ? Color.blue.opacity(0.15) : Color(.systemGray6)) - .foregroundStyle(isSelected ? Color.blue : Color.primary) + .background(isSelected ? Theme.primary.opacity(0.15) : Color(.systemGray6)) + .foregroundStyle(isSelected ? Theme.primary : Color.primary) .clipShape(Capsule()) .overlay( Capsule() - .strokeBorder(isSelected ? Color.blue.opacity(0.4) : Color.clear, lineWidth: 1) + .strokeBorder(isSelected ? Theme.primary.opacity(0.4) : Color.clear, lineWidth: 1) ) } .buttonStyle(.plain) @@ -240,6 +254,8 @@ struct ProductCard: View { let unit: String? let priceText: String var storeName: String? = nil // shown in "All" tab so user knows which store + let isExpanded: Bool + let onToggle: () -> Void let onAdd: () -> Void var body: some View { @@ -264,7 +280,7 @@ struct ProductCard: View { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.headline) - .lineLimit(2) + .lineLimit(isExpanded ? nil : 2) if let storeName { HStack(spacing: 4) { @@ -304,11 +320,13 @@ struct ProductCard: View { .padding(.horizontal, 14) .padding(.vertical, 10) .foregroundStyle(.white) - .background(Color.blue) + .background(Theme.primary) .clipShape(Capsule()) } .buttonStyle(.plain) } + .contentShape(Rectangle()) + .onTapGesture(perform: onToggle) .padding(14) .background(Color(.systemGray6).opacity(0.65)) .clipShape(RoundedRectangle(cornerRadius: 18)) diff --git a/SWFrontUI/ShopwiseFrontEndUI/ShoppingListView.swift b/SWFrontUI/ShopwiseFrontEndUI/ShoppingListView.swift index eb2e409..cae1a93 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/ShoppingListView.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/ShoppingListView.swift @@ -46,8 +46,12 @@ struct ShoppingListView: View { .buttonStyle(.plain) if expandedRecipeIds.contains(group.id) { - ForEach(group.items) { item in - itemRow(item) + let storeGroups = groupItemsByStore(group.items) + ForEach(storeGroups) { storeGroup in + storeHeader(storeGroup.store) + ForEach(storeGroup.items) { item in + itemRow(item) + } } } } @@ -61,8 +65,12 @@ struct ShoppingListView: View { Text("No individual items") .foregroundStyle(.secondary) } else { - ForEach(individualItems) { item in - itemRow(item) + let storeGroups = groupItemsByStore(individualItems) + ForEach(storeGroups) { storeGroup in + storeHeader(storeGroup.store) + ForEach(storeGroup.items) { item in + itemRow(item) + } } } } @@ -146,6 +154,34 @@ struct ShoppingListView: View { } } + private struct StoreGroup: Identifiable { + let id: String + let store: String + let items: [CartLineItem] + } + + private func groupItemsByStore(_ items: [CartLineItem]) -> [StoreGroup] { + let grouped = Dictionary(grouping: items) { item in + let store = item.storeName?.trimmingCharacters(in: .whitespacesAndNewlines) + return (store?.isEmpty == false) ? store! : "Unknown Store" + } + return grouped.map { key, value in + StoreGroup(id: key, store: key, items: value) + } + .sorted { $0.store.localizedCaseInsensitiveCompare($1.store) == .orderedAscending } + } + + @ViewBuilder + private func storeHeader(_ store: String) -> some View { + HStack { + Text(store) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(Theme.secondary) + Spacer() + } + .padding(.top, 4) + } + private var recipeGroups: [RecipeGroup] { let items = cartStore.items var groups: [String: RecipeGroup] = [:] @@ -183,6 +219,11 @@ struct ShoppingListView: View { VStack(alignment: .leading, spacing: 2) { Text(item.name) .strikethrough(checkedIDs.contains(item.id)) + if let store = item.storeName, !store.isEmpty { + Text(store) + .font(.caption.weight(.semibold)) + .foregroundStyle(Theme.primary) + } if !item.unit.isEmpty { Text("\(item.quantity) × \(item.unit)") .font(.subheadline) diff --git a/SWFrontUI/ShopwiseFrontEndUI/StoreLogoHelper.swift b/SWFrontUI/ShopwiseFrontEndUI/StoreLogoHelper.swift index 10dce7a..8c72f2f 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/StoreLogoHelper.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/StoreLogoHelper.swift @@ -24,6 +24,16 @@ func storeLogoAsset(for storeName: String) -> String? { return "logo_smartfinal" case "99 Ranch", "99 Ranch Market": return "logo_99ranch" + case "Vallarta Supermarkets": + return "vallartalogo" + case "Cardenas Markets": + return "cardenaslogo" + case "El Super": + return "superlogo" + case "Northgate Market": + return "northgatelogo" + case "Major Market": + return "majorlogo" default: return nil } diff --git a/SWFrontUI/ShopwiseFrontEndUI/UIModules.swift b/SWFrontUI/ShopwiseFrontEndUI/UIModules.swift index 0d08d35..8bd40b0 100644 --- a/SWFrontUI/ShopwiseFrontEndUI/UIModules.swift +++ b/SWFrontUI/ShopwiseFrontEndUI/UIModules.swift @@ -1,5 +1,11 @@ import SwiftUI +enum Theme { + static let primary = Color("PrimaryBlue") + static let secondary = Color("SecondaryGold") + static let muted = Color(.secondaryLabel) +} + struct CategoryChip: View { let title: String let isSelected: Bool @@ -81,3 +87,4 @@ struct ItemCardView: View { } } } +