Not has address
This commit is contained in:
@@ -9,7 +9,9 @@ struct HomeView: View {
|
||||
@State var selectedCategory = "Stores"
|
||||
@State var scrollOffset: CGFloat = 0
|
||||
@State var hasRequestedLocation = false
|
||||
let locationService = LocationService()
|
||||
@State var isLoadingStores = false
|
||||
@State var storesError: String? = nil
|
||||
@State var stores: [StoreSummary] = []
|
||||
|
||||
private let categories: [CategoryModel] = [
|
||||
.init(title: "Stores", systemIcon: "storefront"),
|
||||
@@ -19,52 +21,6 @@ struct HomeView: View {
|
||||
.init(title: "Dessert", systemIcon: "cup.and.saucer")
|
||||
]
|
||||
|
||||
private let featuredStores: [FeaturedStoreCardModel] = [
|
||||
.init(
|
||||
name: "Burger Kingdom",
|
||||
rating: 4.9,
|
||||
reviews: "1.2k",
|
||||
distance: "1.2 km",
|
||||
category: "Fast Food",
|
||||
promoText: "10% OFF",
|
||||
isFavorite: false,
|
||||
iconName: "takeoutbag.and.cup.and.straw"
|
||||
),
|
||||
.init(
|
||||
name: "Sushi House",
|
||||
rating: 4.8,
|
||||
reviews: "840",
|
||||
distance: "2.2 km",
|
||||
category: "Japanese",
|
||||
promoText: nil,
|
||||
isFavorite: true,
|
||||
iconName: "fork.knife"
|
||||
)
|
||||
]
|
||||
|
||||
private let nearbyStores: [FeaturedStoreCardModel] = [
|
||||
.init(
|
||||
name: "Pizza Prime",
|
||||
rating: 4.7,
|
||||
reviews: "510",
|
||||
distance: "1.8 km",
|
||||
category: "Pizza",
|
||||
promoText: nil,
|
||||
isFavorite: false,
|
||||
iconName: "takeoutbag.and.cup.and.straw"
|
||||
),
|
||||
.init(
|
||||
name: "Aoyama",
|
||||
rating: 4.9,
|
||||
reviews: "2.4k",
|
||||
distance: "3.1 km",
|
||||
category: "Japanese",
|
||||
promoText: nil,
|
||||
isFavorite: true,
|
||||
iconName: "fork.knife"
|
||||
)
|
||||
]
|
||||
|
||||
private let specials: [SpecialOfferCardModel] = [
|
||||
.init(title: "Get 50% OFF", subtitle: "For your first order", colors: [Color(hex: "#1E6B43"), Color(hex: "#58A56C")]),
|
||||
.init(title: "Enjoy spicy day", subtitle: "Delivery in 20 min", colors: [Color(hex: "#F04B3E"), Color(hex: "#FF8C42")])
|
||||
@@ -95,10 +51,6 @@ struct HomeView: View {
|
||||
.padding(.top, headerPadding + 16)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.coordinateSpace(name: "scroll")
|
||||
.onPreferenceChange(ScrollOffsetKey.self) { value in
|
||||
scrollOffset = min(0, value)
|
||||
}
|
||||
#endif
|
||||
|
||||
header(collapseProgress: collapseProgress, height: headerHeight)
|
||||
@@ -110,14 +62,8 @@ struct HomeView: View {
|
||||
.onAppear {
|
||||
if hasRequestedLocation == false {
|
||||
hasRequestedLocation = true
|
||||
locationService.requestLocation { result in
|
||||
switch result {
|
||||
case .success(let coordinate):
|
||||
appState.address.latitude = coordinate.0
|
||||
appState.address.longitude = coordinate.1
|
||||
case .failure:
|
||||
appState.address.display = "Defina seu endereco"
|
||||
}
|
||||
Task {
|
||||
await bootstrapStoresFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +78,7 @@ struct HomeView: View {
|
||||
section(title: "Featured") {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(featuredStores) { store in
|
||||
ForEach(featuredStoresCards) { store in
|
||||
FeaturedStoreCard(store: store)
|
||||
.frame(width: 190)
|
||||
}
|
||||
@@ -154,12 +100,41 @@ struct HomeView: View {
|
||||
}
|
||||
|
||||
section(title: "Near you") {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(nearbyStores) { store in
|
||||
FeaturedStoreCard(store: store)
|
||||
if isLoadingStores {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text("Buscando estabelecimentos próximos...")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
} else if let storesError {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(storesError)
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Button("Tentar novamente") {
|
||||
Task {
|
||||
await bootstrapStoresFlow(forceLocationRefresh: true)
|
||||
}
|
||||
}
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
} else if nearbyStoreCards.isEmpty {
|
||||
Text("Nenhum estabelecimento encontrado próximo à sua localização.")
|
||||
.font(AppTypography.body)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.padding(.horizontal, 20)
|
||||
} else {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(nearbyStoreCards) { store in
|
||||
FeaturedStoreCard(store: store)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +197,6 @@ struct HomeView: View {
|
||||
Text("O que vai querer pedir hoje?")
|
||||
.font(AppTypography.heading1)
|
||||
.foregroundStyle(AppColors.textInverse)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.opacity(titleOpacity)
|
||||
.offset(y: collapseProgress * -20)
|
||||
|
||||
@@ -294,6 +268,120 @@ struct HomeView: View {
|
||||
.offset(x: 70, y: 10)
|
||||
}
|
||||
}
|
||||
|
||||
private var sortedStores: [StoreSummary] {
|
||||
stores.sorted { lhs, rhs in
|
||||
(lhs.distance ?? .greatestFiniteMagnitude) < (rhs.distance ?? .greatestFiniteMagnitude)
|
||||
}
|
||||
}
|
||||
|
||||
private var featuredStoresCards: [FeaturedStoreCardModel] {
|
||||
Array(sortedStores.prefix(6)).map(mapStoreToCard)
|
||||
}
|
||||
|
||||
private var nearbyStoreCards: [FeaturedStoreCardModel] {
|
||||
Array(sortedStores.prefix(20)).map(mapStoreToCard)
|
||||
}
|
||||
|
||||
private func mapStoreToCard(_ store: StoreSummary) -> FeaturedStoreCardModel {
|
||||
FeaturedStoreCardModel(
|
||||
name: store.name,
|
||||
rating: store.rating ?? 0,
|
||||
reviews: "0",
|
||||
distance: formatDistance(store.distance),
|
||||
category: store.category ?? "Loja",
|
||||
promoText: nil,
|
||||
isFavorite: appState.favorites.storeIds.contains(store.id),
|
||||
iconName: "storefront"
|
||||
)
|
||||
}
|
||||
|
||||
private func formatDistance(_ distance: Double?) -> String {
|
||||
guard let distance else { return "Distância indisponível" }
|
||||
if distance >= 1 {
|
||||
return String(format: "%.1f km", distance)
|
||||
}
|
||||
return "\(Int(distance * 1000)) m"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func bootstrapStoresFlow(forceLocationRefresh: Bool = false) async {
|
||||
isLoadingStores = true
|
||||
storesError = nil
|
||||
|
||||
guard let coordinate = await resolveCoordinates(forceRefresh: forceLocationRefresh) else {
|
||||
isLoadingStores = false
|
||||
stores = []
|
||||
storesError = "Defina um endereço para visualizar os estabelecimentos mais próximos."
|
||||
appState.address.onboardingMessage = "Vamos criar um endereço para poder visualizar os estabelecimentos mais próximos de você?"
|
||||
appState.activeModal = .addressPicker
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await ApiService().listStores(lat: coordinate.0, lng: coordinate.1)
|
||||
isLoadingStores = false
|
||||
if response.error {
|
||||
stores = []
|
||||
storesError = response.message ?? "Não foi possível carregar os estabelecimentos."
|
||||
return
|
||||
}
|
||||
stores = response.result ?? []
|
||||
storesError = nil
|
||||
} catch {
|
||||
isLoadingStores = false
|
||||
stores = []
|
||||
storesError = storesUserMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func resolveCoordinates(forceRefresh: Bool) async -> (Double, Double)? {
|
||||
if !forceRefresh {
|
||||
if let lat = appState.address.latitude, let lng = appState.address.longitude {
|
||||
return (lat, lng)
|
||||
}
|
||||
if let cached = LocationService.shared.cachedLocation() {
|
||||
appState.address.latitude = cached.0
|
||||
appState.address.longitude = cached.1
|
||||
return cached
|
||||
}
|
||||
if hasConfiguredAddress() == false {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let coordinate = await LocationService.shared.requestLocationAsync(timeoutSeconds: 3)
|
||||
if let coordinate {
|
||||
appState.address.latitude = coordinate.0
|
||||
appState.address.longitude = coordinate.1
|
||||
}
|
||||
return coordinate
|
||||
}
|
||||
|
||||
private func hasConfiguredAddress() -> Bool {
|
||||
if appState.address.selectedId != nil {
|
||||
return true
|
||||
}
|
||||
if appState.address.latitude != nil, appState.address.longitude != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
let normalized = appState.address.display
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased()
|
||||
|
||||
return normalized.isEmpty == false && normalized != "defina seu endereco"
|
||||
}
|
||||
|
||||
private func storesUserMessage(_ error: Error) -> String {
|
||||
if let service = error as? ApiServiceError {
|
||||
return service.errorDescription ?? "Não foi possível carregar os estabelecimentos."
|
||||
}
|
||||
if let network = error as? NetworkError {
|
||||
return network.errorDescription ?? "Não foi possível carregar os estabelecimentos."
|
||||
}
|
||||
return "Não foi possível carregar os estabelecimentos."
|
||||
}
|
||||
}
|
||||
|
||||
struct CategoryModel: Identifiable {
|
||||
@@ -332,8 +420,7 @@ struct SearchBar: View {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
TextField(placeholder, text: $text)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.appNoAutoCap()
|
||||
Spacer()
|
||||
Button(action: onFilterTap) {
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
|
||||
Reference in New Issue
Block a user