login flow
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
#if os(iOS)
|
||||
import LCEssentials
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@@ -9,7 +10,7 @@ struct HomeView: View {
|
||||
@State var searchText = ""
|
||||
@State var selectedCategory = "all"
|
||||
@State var categories: [CategoryModel] = [
|
||||
.init(id: "all", title: "Todas", systemIcon: "line.3.horizontal.decrease.circle")
|
||||
.init(id: "all", title: "Todas", systemIcon: "line.3.horizontal.decrease.circle", emojiIcon: nil)
|
||||
]
|
||||
@State var scrollOffset: CGFloat = 0
|
||||
@State var collapseBaseOffset: CGFloat = 0
|
||||
@@ -120,7 +121,8 @@ struct HomeView: View {
|
||||
Task {
|
||||
await bootstrapStoresFlow(
|
||||
forceLocationRefresh: true,
|
||||
category: selectedCategory == "all" ? nil : selectedCategory
|
||||
category: selectedCategory == "all" ? nil : selectedCategory,
|
||||
refreshCategories: true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -166,13 +168,15 @@ struct HomeView: View {
|
||||
.foregroundStyle(AppColors.brandDark)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .center, spacing: 4) {
|
||||
Text("DELIVERY LOCATION")
|
||||
.font(AppTypography.overline)
|
||||
.tracking(AppTypography.captionLetterSpacing)
|
||||
.foregroundStyle(AppColors.brandSoft)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Button {
|
||||
appState.address.onboardingMessage = nil
|
||||
appState.activeModal = .addressPicker
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
@@ -186,8 +190,7 @@ struct HomeView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Circle()
|
||||
.fill(Color.white.opacity(0.18))
|
||||
@@ -199,13 +202,21 @@ struct HomeView: View {
|
||||
}
|
||||
.opacity(topRowOpacity)
|
||||
.offset(y: collapseProgress * -12)
|
||||
|
||||
Text("O que vai querer \npedir hoje?")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textInverse)
|
||||
.opacity(titleOpacity)
|
||||
.offset(y: collapseProgress * -20)
|
||||
|
||||
|
||||
if UIDevice().modelName.lowercased().contains("se") || UIDevice().modelName.lowercased().contains("16e") {
|
||||
Text("O que vai querer \npedir hoje?")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textInverse)
|
||||
.opacity(titleOpacity)
|
||||
.offset(y: collapseProgress * -20)
|
||||
} else {
|
||||
Text("O que vai querer pedir hoje?\n ")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textInverse)
|
||||
.opacity(titleOpacity)
|
||||
.offset(y: collapseProgress * -20)
|
||||
}
|
||||
|
||||
SearchBar(placeholder: "Search menu, restaurant or craving", text: $searchText) {
|
||||
appState.activeModal = .filters
|
||||
}
|
||||
@@ -240,6 +251,7 @@ struct HomeView: View {
|
||||
CategoryChip(
|
||||
title: category.title,
|
||||
systemIcon: category.systemIcon,
|
||||
emojiIcon: category.emojiIcon,
|
||||
isActive: category.id == selectedCategory
|
||||
)
|
||||
.onTapGesture {
|
||||
@@ -293,10 +305,27 @@ struct HomeView: View {
|
||||
category: store.category ?? "Loja",
|
||||
promoText: nil,
|
||||
isFavorite: appState.favorites.storeIds.contains(store.id),
|
||||
iconName: "storefront"
|
||||
iconName: "storefront",
|
||||
imageURL: resolveStoreImageURL(logo: store.logo, cover: store.cover)
|
||||
)
|
||||
}
|
||||
|
||||
private func resolveStoreImageURL(logo: String?, cover: String?) -> String? {
|
||||
let preferred = [logo, cover]
|
||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.first { $0.isEmpty == false }
|
||||
|
||||
guard let raw = preferred else { return nil }
|
||||
|
||||
if raw.lowercased().hasPrefix("http://") || raw.lowercased().hasPrefix("https://") {
|
||||
return raw
|
||||
}
|
||||
|
||||
let base = ApiConfig.baseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
|
||||
return "\(base)\(path)"
|
||||
}
|
||||
|
||||
private func formatDistance(_ distance: Double?) -> String {
|
||||
guard let distance else { return "Distância indisponível" }
|
||||
if distance >= 1 {
|
||||
@@ -314,7 +343,10 @@ struct HomeView: View {
|
||||
isLoadingStores = true
|
||||
storesError = nil
|
||||
|
||||
guard let coordinate = await resolveCoordinates(forceRefresh: forceLocationRefresh) else {
|
||||
let coordinate = await resolveCoordinates(forceRefresh: forceLocationRefresh)
|
||||
let hasAddress = hasConfiguredAddress()
|
||||
|
||||
if coordinate == nil && hasAddress == false {
|
||||
isLoadingStores = false
|
||||
stores = []
|
||||
storesError = "Defina um endereço para visualizar os estabelecimentos mais próximos."
|
||||
@@ -324,7 +356,11 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await ApiService().listStores(lat: coordinate.0, lng: coordinate.1, category: category)
|
||||
let response = try await ApiService().listStores(
|
||||
lat: coordinate?.0,
|
||||
lng: coordinate?.1,
|
||||
category: category
|
||||
)
|
||||
isLoadingStores = false
|
||||
if response.error {
|
||||
stores = []
|
||||
@@ -333,8 +369,8 @@ struct HomeView: View {
|
||||
}
|
||||
let results = response.result ?? []
|
||||
stores = results
|
||||
if refreshCategories {
|
||||
categories = buildCategories(from: results)
|
||||
if refreshCategories || (category == nil && categories.count <= 1) {
|
||||
await loadHomeCategories(withFallbackStores: results)
|
||||
if categories.contains(where: { $0.id == selectedCategory }) == false {
|
||||
selectedCategory = "all"
|
||||
}
|
||||
@@ -349,7 +385,7 @@ struct HomeView: View {
|
||||
|
||||
private func buildCategories(from stores: [StoreSummary]) -> [CategoryModel] {
|
||||
var unique: [CategoryModel] = [
|
||||
.init(id: "all", title: "Todas", systemIcon: "line.3.horizontal.decrease.circle")
|
||||
.init(id: "all", title: "Todas", systemIcon: "line.3.horizontal.decrease.circle", emojiIcon: nil)
|
||||
]
|
||||
var seen = Set<String>()
|
||||
|
||||
@@ -359,11 +395,59 @@ struct HomeView: View {
|
||||
let dedupe = raw.lowercased()
|
||||
if seen.contains(dedupe) { continue }
|
||||
seen.insert(dedupe)
|
||||
unique.append(.init(id: raw, title: raw, systemIcon: categoryIcon(for: raw)))
|
||||
unique.append(.init(id: raw, title: raw, systemIcon: categoryIcon(for: raw), emojiIcon: nil))
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadHomeCategories(withFallbackStores stores: [StoreSummary]) async {
|
||||
do {
|
||||
let response = try await ApiService().listPublicCategories()
|
||||
if response.error == false, let remote = response.result, remote.isEmpty == false {
|
||||
categories = mapPublicCategories(remote)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// Fallback handled below.
|
||||
}
|
||||
|
||||
categories = buildCategories(from: stores)
|
||||
}
|
||||
|
||||
private func mapPublicCategories(_ remote: [PublicCategory]) -> [CategoryModel] {
|
||||
var mapped: [CategoryModel] = []
|
||||
var seen = Set<String>()
|
||||
|
||||
for item in remote {
|
||||
let id = item.id.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let title = item.name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if id.isEmpty || title.isEmpty { continue }
|
||||
if seen.contains(id.lowercased()) { continue }
|
||||
seen.insert(id.lowercased())
|
||||
mapped.append(
|
||||
.init(
|
||||
id: id,
|
||||
title: title,
|
||||
systemIcon: id.lowercased() == "all" ? "line.3.horizontal.decrease.circle" : nil,
|
||||
emojiIcon: item.icon
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if mapped.contains(where: { $0.id.lowercased() == "all" }) == false {
|
||||
mapped.insert(.init(id: "all", title: "Todas", systemIcon: "line.3.horizontal.decrease.circle", emojiIcon: nil), at: 0)
|
||||
} else {
|
||||
mapped.sort { lhs, rhs in
|
||||
if lhs.id.lowercased() == "all" { return true }
|
||||
if rhs.id.lowercased() == "all" { return false }
|
||||
return lhs.title < rhs.title
|
||||
}
|
||||
}
|
||||
|
||||
return mapped
|
||||
}
|
||||
|
||||
private func categoryIcon(for category: String) -> String {
|
||||
let value = category.folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current).lowercased()
|
||||
if value.contains("pizza") { return "takeoutbag.and.cup.and.straw" }
|
||||
@@ -440,18 +524,25 @@ struct HomeView: View {
|
||||
struct CategoryModel: Identifiable {
|
||||
let id: String
|
||||
let title: String
|
||||
let systemIcon: String
|
||||
let systemIcon: String?
|
||||
let emojiIcon: String?
|
||||
}
|
||||
|
||||
struct CategoryChip: View {
|
||||
let title: String
|
||||
let systemIcon: String
|
||||
let systemIcon: String?
|
||||
let emojiIcon: String?
|
||||
let isActive: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: systemIcon)
|
||||
.font(.caption)
|
||||
if let emojiIcon, emojiIcon.isEmpty == false {
|
||||
Text(emojiIcon)
|
||||
.font(.body)
|
||||
} else if let systemIcon, systemIcon.isEmpty == false {
|
||||
Image(systemName: systemIcon)
|
||||
.font(.caption)
|
||||
}
|
||||
Text(title)
|
||||
.font(AppTypography.heading3)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user