refactor(catalog): replace InfoPanel tabs with vertical sections
All checks were successful
Build Docker Image / build (push) Successful in 3m57s

- Remove all tabs from InfoPanel, use stacked sections instead
- Load suppliers (for hub) and hubs (for supplier) immediately
- Show entity header as text, not card
- Simplify relatedPoints to show all points on map
- Add translations for new section titles
This commit is contained in:
Ruslan Bakiev
2026-01-26 19:34:04 +07:00
parent 69bb978526
commit e905098cb5
5 changed files with 236 additions and 323 deletions

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<!-- Header --> <!-- Header with close button -->
<div class="flex-shrink-0 p-4 border-b border-white/10"> <div class="flex-shrink-0 p-4 border-b border-white/10">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
class="flex items-center justify-center w-6 h-6 rounded-full" class="flex items-center justify-center w-6 h-6 rounded-full"
@@ -27,148 +27,103 @@
<!-- Content --> <!-- Content -->
<div v-else-if="entity" class="flex flex-col gap-4"> <div v-else-if="entity" class="flex flex-col gap-4">
<!-- Entity details card --> <!-- Entity Info Header (text, not card) -->
<div> <div class="mb-2">
<HubCard v-if="entityType === 'hub'" :hub="entity" /> <!-- Location for hub/supplier -->
<SupplierCard v-else-if="entityType === 'supplier'" :supplier="entity" /> <p v-if="entityLocation" class="text-sm text-white/70 flex items-center gap-1">
<OfferCard v-else-if="entityType === 'offer'" :offer="entity" compact /> <Icon name="lucide:map-pin" size="14" />
</div> {{ entityLocation }}
</p>
<!-- Tabs for related objects --> <!-- Price for offer -->
<div role="tablist" class="tabs tabs-boxed bg-white/10"> <p v-if="entityType === 'offer' && entity?.pricePerUnit" class="text-sm text-white/70 flex items-center gap-1">
<a <Icon name="lucide:tag" size="14" />
v-for="tab in availableTabs" {{ formatPrice(entity.pricePerUnit) }} {{ entity.currency || 'RUB' }}/{{ entity.unit || 't' }}
:key="tab.id" </p>
role="tab"
class="tab text-white/70" <!-- Supplier link for offer -->
:class="{ <button
'tab-active !text-white !bg-white/20': activeTab === tab.id, v-if="entityType === 'offer' && entity?.teamUuid"
'pointer-events-none opacity-50': tab.loading class="text-sm text-primary hover:underline flex items-center gap-1 mt-1"
}" @click="emit('open-info', 'supplier', entity.teamUuid)"
@click="!tab.loading && onTabClick(tab.id)"
> >
{{ tab.label }} <Icon name="lucide:factory" size="14" />
<span v-if="tab.loading" class="ml-1"> {{ entity.teamName || $t('catalog.info.viewSupplier') }}
<span class="loading loading-spinner loading-xs" /> </button>
</span>
<span v-else-if="tab.count !== undefined" class="ml-1 opacity-70">({{ tab.count }})</span>
</a>
</div> </div>
<!-- Tab content --> <!-- Products Section (for hub/supplier) -->
<div class="flex flex-col gap-2"> <section v-if="entityType === 'hub' || entityType === 'supplier'">
<!-- Products tab (only for Offer entity) --> <h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
<div v-if="activeTab === 'products' && entityType === 'offer'"> <Icon name="lucide:package" size="16" />
<div v-if="relatedProducts.length === 0" class="text-center py-8 text-white/60"> {{ productsSectionTitle }}
<Icon name="lucide:package" size="32" class="mb-2 opacity-50" /> <span v-if="loadingProducts" class="loading loading-spinner loading-xs" />
<p>{{ $t('catalog.empty.noProducts') }}</p> <span v-else-if="relatedProducts.length > 0" class="text-white/50">({{ relatedProducts.length }})</span>
</div> </h3>
<div v-else class="flex flex-col gap-2">
<ProductCard <div v-if="!loadingProducts && relatedProducts.length === 0" class="text-white/50 text-sm py-2">
v-for="product in relatedProducts" {{ $t('catalog.empty.noProducts') }}
:key="product.uuid"
:product="product"
selectable
compact
@select="onProductSelect(product)"
/>
</div>
</div> </div>
<div v-else-if="!loadingProducts" class="flex flex-col gap-2">
<!-- Hubs tab --> <ProductCard
<div v-if="activeTab === 'hubs'"> v-for="product in relatedProducts"
<div v-if="relatedHubs.length === 0" class="text-center py-8 text-white/60"> :key="product.uuid"
<Icon name="lucide:warehouse" size="32" class="mb-2 opacity-50" /> :product="product"
<p>{{ $t('catalog.info.noHubs') }}</p> compact
</div> selectable
<div v-else class="flex flex-col gap-2"> @select="onProductSelect(product)"
<HubCard />
v-for="hub in relatedHubs"
:key="hub.uuid"
:hub="hub"
selectable
@select="onHubSelect(hub)"
/>
</div>
</div> </div>
</section>
<!-- Suppliers tab --> <!-- Suppliers Section (for hub only) -->
<div v-if="activeTab === 'suppliers'"> <section v-if="entityType === 'hub'">
<div v-if="relatedSuppliers.length === 0" class="text-center py-8 text-white/60"> <h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
<Icon name="lucide:factory" size="32" class="mb-2 opacity-50" /> <Icon name="lucide:factory" size="16" />
<p>{{ $t('catalog.info.noSuppliers') }}</p> {{ $t('catalog.info.suppliersNearby') }}
</div> <span v-if="loadingSuppliers" class="loading loading-spinner loading-xs" />
<div v-else class="flex flex-col gap-2"> <span v-else-if="relatedSuppliers.length > 0" class="text-white/50">({{ relatedSuppliers.length }})</span>
<SupplierCard </h3>
v-for="supplier in relatedSuppliers"
:key="supplier.uuid" <div v-if="!loadingSuppliers && relatedSuppliers.length === 0" class="text-white/50 text-sm py-2">
:supplier="supplier" {{ $t('catalog.info.noSuppliers') }}
selectable
@select="onSupplierSelect(supplier)"
/>
</div>
</div> </div>
<div v-else-if="!loadingSuppliers" class="flex flex-col gap-2">
<!-- Offers tab (two-step for Hub/Supplier) --> <SupplierCard
<div v-if="activeTab === 'offers'"> v-for="supplier in relatedSuppliers"
<!-- Step 1: Select product (for Hub/Supplier) --> :key="supplier.uuid"
<div v-if="!selectedProduct && (entityType === 'hub' || entityType === 'supplier')"> :supplier="supplier"
<div v-if="relatedProducts.length === 0" class="text-center py-8 text-white/60"> selectable
<Icon name="lucide:package" size="32" class="mb-2 opacity-50" /> @select="onSupplierSelect(supplier)"
<p>{{ $t('catalog.empty.noProducts') }}</p> />
</div>
<div v-else class="flex flex-col gap-2">
<ProductCard
v-for="product in relatedProducts"
:key="product.uuid"
:product="product"
selectable
compact
@select="onProductSelect(product)"
/>
</div>
</div>
<!-- Step 2: Show offers for selected product -->
<div v-else-if="selectedProduct">
<!-- Back button to products -->
<button
class="btn btn-sm btn-ghost mb-2 text-white/80 hover:text-white"
@click="emit('select-product', null)"
>
<Icon name="lucide:arrow-left" size="16" />
{{ $t('common.back') }}
</button>
<div v-if="relatedOffers.length === 0" class="text-center py-8 text-white/60">
<Icon name="lucide:shopping-bag" size="32" class="mb-2 opacity-50" />
<p>{{ $t('catalog.empty.noOffers') }}</p>
</div>
<div v-else class="flex flex-col gap-2">
<OfferResultCard
v-for="offer in relatedOffers"
:key="offer.uuid"
:location-name="offer.locationName || offer.productName"
:product-name="offer.productName"
:price-per-unit="offer.pricePerUnit ? Number(offer.pricePerUnit) : null"
:currency="offer.currency"
:unit="offer.unit"
:stages="getOfferStages(offer)"
@select="onOfferSelect(offer)"
/>
</div>
</div>
<!-- For Offer entity type - just show message -->
<div v-else class="text-center py-8 text-white/60">
<Icon name="lucide:info" size="32" class="mb-2 opacity-50" />
<p>{{ $t('catalog.info.selectProductFirst') }}</p>
</div>
</div> </div>
</div> </section>
<!-- Hubs Section (for supplier/offer) -->
<section v-if="entityType === 'supplier' || entityType === 'offer'">
<h3 class="text-sm font-semibold text-white/80 mb-2 flex items-center gap-2">
<Icon name="lucide:warehouse" size="16" />
{{ $t('catalog.info.nearestHubs') }}
<span v-if="loadingHubs" class="loading loading-spinner loading-xs" />
<span v-else-if="relatedHubs.length > 0" class="text-white/50">({{ relatedHubs.length }})</span>
</h3>
<div v-if="!loadingHubs && relatedHubs.length === 0" class="text-white/50 text-sm py-2">
{{ $t('catalog.info.noHubs') }}
</div>
<div v-else-if="!loadingHubs" class="flex flex-col gap-2">
<HubCard
v-for="hub in relatedHubs"
:key="hub.uuid"
:hub="hub"
selectable
@select="onHubSelect(hub)"
/>
</div>
</section>
<!-- Add to filter button --> <!-- Add to filter button -->
<button class="btn btn-primary btn-sm" @click="emit('add-to-filter')"> <button class="btn btn-primary btn-sm mt-2" @click="emit('add-to-filter')">
<Icon name="lucide:filter-plus" size="16" /> <Icon name="lucide:filter-plus" size="16" />
{{ $t('catalog.info.addToFilter') }} {{ $t('catalog.info.addToFilter') }}
</button> </button>
@@ -212,13 +167,26 @@ const { entityColors } = useCatalogSearch()
const relatedProducts = computed(() => props.relatedProducts ?? []) const relatedProducts = computed(() => props.relatedProducts ?? [])
const relatedHubs = computed(() => props.relatedHubs ?? []) const relatedHubs = computed(() => props.relatedHubs ?? [])
const relatedSuppliers = computed(() => props.relatedSuppliers ?? []) const relatedSuppliers = computed(() => props.relatedSuppliers ?? [])
const relatedOffers = computed(() => props.relatedOffers ?? [])
// Entity name // Entity name
const entityName = computed(() => { const entityName = computed(() => {
return props.entity?.name || props.entity?.productName || props.entityId.slice(0, 8) + '...' return props.entity?.name || props.entity?.productName || props.entityId.slice(0, 8) + '...'
}) })
// Entity location (address, city, country)
const entityLocation = computed(() => {
if (!props.entity) return null
const parts = [props.entity.address, props.entity.city, props.entity.country].filter(Boolean)
return parts.length > 0 ? parts.join(', ') : null
})
// Products section title based on entity type
const productsSectionTitle = computed(() => {
return props.entityType === 'hub'
? t('catalog.info.productsHere')
: t('catalog.info.productsFromSupplier')
})
// Badge color // Badge color
const badgeColor = computed(() => { const badgeColor = computed(() => {
if (props.entityType === 'hub') return entityColors.hub if (props.entityType === 'hub') return entityColors.hub
@@ -234,92 +202,16 @@ const entityIcon = computed(() => {
return 'lucide:info' return 'lucide:info'
}) })
// Available tabs based on entity type and data // Format price
const availableTabs = computed(() => { const formatPrice = (price: number | string) => {
const tabs: Array<{ id: string; label: string; count?: number; loading?: boolean }> = [] const num = typeof price === 'string' ? parseFloat(price) : price
return new Intl.NumberFormat('ru-RU').format(num)
if (props.entityType === 'hub') {
// For hub: offers tab shows products first, then offers after product selection
const offersLoading = props.selectedProduct ? props.loadingOffers : props.loadingProducts
const offersCount = props.selectedProduct
? props.relatedOffers?.length
: props.relatedProducts?.length
tabs.push({
id: 'offers',
label: t('catalog.tabs.offers'),
count: offersLoading ? undefined : (offersCount || 0),
loading: offersLoading
})
tabs.push({
id: 'suppliers',
label: t('catalog.tabs.suppliers'),
count: props.loadingSuppliers ? undefined : (props.relatedSuppliers?.length || 0),
loading: props.loadingSuppliers
})
} else if (props.entityType === 'supplier') {
// For supplier: offers tab shows products first, then offers after product selection
const offersLoading = props.selectedProduct ? props.loadingOffers : props.loadingProducts
const offersCount = props.selectedProduct
? props.relatedOffers?.length
: props.relatedProducts?.length
tabs.push({
id: 'offers',
label: t('catalog.tabs.offers'),
count: offersLoading ? undefined : (offersCount || 0),
loading: offersLoading
})
tabs.push({
id: 'hubs',
label: t('catalog.tabs.hubs'),
count: props.loadingHubs ? undefined : (props.relatedHubs?.length || 0),
loading: props.loadingHubs
})
} else if (props.entityType === 'offer') {
if (props.relatedProducts && props.relatedProducts.length > 0) {
tabs.push({
id: 'products',
label: t('catalog.tabs.product')
})
}
tabs.push({
id: 'hubs',
label: t('catalog.tabs.hubs'),
count: props.loadingHubs ? undefined : (props.relatedHubs?.length || 0),
loading: props.loadingHubs
})
if (props.relatedSuppliers && props.relatedSuppliers.length > 0) {
tabs.push({
id: 'suppliers',
label: t('catalog.tabs.supplier'),
count: props.loadingSuppliers ? undefined : (props.relatedSuppliers?.length || 0),
loading: props.loadingSuppliers
})
}
}
return tabs
})
// Active tab - use prop or default to first available
const activeTab = computed(() => {
// If prop is set and is a valid tab, use it
if (props.currentTab && availableTabs.value.some(t => t.id === props.currentTab)) {
return props.currentTab
}
// Default to first tab
return availableTabs.value[0]?.id || 'offers'
})
// Handler for tab click
const onTabClick = (tabId: string) => {
emit('update:current-tab', tabId)
} }
// Handlers for selecting related items // Handlers for selecting related items
const onProductSelect = (product: any) => { const onProductSelect = (product: any) => {
if (product.uuid) { if (product.uuid) {
// Navigate to offer info for this product
emit('select-product', product.uuid) emit('select-product', product.uuid)
} }
} }
@@ -335,20 +227,4 @@ const onSupplierSelect = (supplier: any) => {
emit('open-info', 'supplier', supplier.uuid) emit('open-info', 'supplier', supplier.uuid)
} }
} }
const onOfferSelect = (offer: any) => {
if (offer.uuid) {
emit('open-info', 'offer', offer.uuid)
}
}
// Extract route stages for OfferResultCard
const getOfferStages = (offer: any) => {
const route = offer.routes?.[0]
if (!route?.stages) return []
return route.stages.filter(Boolean).map((stage: any) => ({
transportType: stage?.transportType,
distanceKm: stage?.distanceKm
}))
}
</script> </script>

View File

@@ -29,7 +29,7 @@ export function useCatalogInfo() {
const isLoadingSuppliers = ref(false) const isLoadingSuppliers = ref(false)
const isLoadingOffers = ref(false) const isLoadingOffers = ref(false)
// Load hub info: hub details + products + suppliers // Load hub info: hub details + products + suppliers (in parallel)
const loadHubInfo = async (uuid: string) => { const loadHubInfo = async (uuid: string) => {
try { try {
// Load hub node details // Load hub node details
@@ -44,23 +44,27 @@ export function useCatalogInfo() {
// Set default active tab to offers (first step shows products) // Set default active tab to offers (first step shows products)
activeTab.value = 'offers' activeTab.value = 'offers'
// Load products (offers grouped by product) // Load products AND suppliers in parallel
isLoadingProducts.value = true isLoadingProducts.value = true
try { isLoadingSuppliers.value = true
const offersData = await execute(
NearestOffersDocument,
{
lat: entity.value.latitude,
lon: entity.value.longitude,
radius: 500
},
'public',
'geo'
)
// Load products (offers grouped by product) and extract suppliers
execute(
NearestOffersDocument,
{
lat: entity.value.latitude,
lon: entity.value.longitude,
radius: 500
},
'public',
'geo'
).then(offersData => {
// Group offers by product // Group offers by product
const productsMap = new Map<string, any>() const productsMap = new Map<string, any>()
const suppliersMap = new Map<string, any>()
offersData?.nearestOffers?.forEach((offer: any) => { offersData?.nearestOffers?.forEach((offer: any) => {
// Products
if (offer?.productUuid) { if (offer?.productUuid) {
if (!productsMap.has(offer.productUuid)) { if (!productsMap.has(offer.productUuid)) {
productsMap.set(offer.productUuid, { productsMap.set(offer.productUuid, {
@@ -71,19 +75,50 @@ export function useCatalogInfo() {
} }
productsMap.get(offer.productUuid)!.offersCount++ productsMap.get(offer.productUuid)!.offersCount++
} }
})
relatedProducts.value = Array.from(productsMap.values())
} finally {
isLoadingProducts.value = false
}
// Note: Suppliers and Offers loaded after product selection via loadOffersForHub // Suppliers (extract from offers)
if (offer?.supplierUuid) {
if (!suppliersMap.has(offer.supplierUuid)) {
suppliersMap.set(offer.supplierUuid, {
uuid: offer.supplierUuid,
name: offer.supplierName || 'Supplier',
latitude: offer.latitude,
longitude: offer.longitude
})
}
}
})
relatedProducts.value = Array.from(productsMap.values())
// Load supplier profiles for detailed info
const supplierUuids = Array.from(suppliersMap.keys()).slice(0, 12)
if (supplierUuids.length > 0) {
Promise.all(
supplierUuids.map(supplierId =>
execute(GetSupplierProfileDocument, { uuid: supplierId }, 'public', 'exchange')
.then(data => data?.getSupplierProfile)
.catch(() => suppliersMap.get(supplierId)) // Fallback to basic info
)
).then(profiles => {
relatedSuppliers.value = profiles.filter(Boolean)
isLoadingSuppliers.value = false
})
} else {
relatedSuppliers.value = []
isLoadingSuppliers.value = false
}
}).finally(() => {
isLoadingProducts.value = false
})
} catch (error) { } catch (error) {
console.error('Error loading hub info:', error) console.error('Error loading hub info:', error)
isLoadingProducts.value = false
isLoadingSuppliers.value = false
} }
} }
// Load supplier info: supplier details + products // Load supplier info: supplier details + products + hubs (in parallel)
const loadSupplierInfo = async (uuid: string) => { const loadSupplierInfo = async (uuid: string) => {
try { try {
// Load supplier node details (might be geo node) // Load supplier node details (might be geo node)
@@ -113,20 +148,21 @@ export function useCatalogInfo() {
// Set default active tab to offers (first step shows products) // Set default active tab to offers (first step shows products)
activeTab.value = 'offers' activeTab.value = 'offers'
// Load products (offers grouped by product) // Load products AND hubs in parallel
isLoadingProducts.value = true isLoadingProducts.value = true
try { isLoadingHubs.value = true
const offersData = await execute(
NearestOffersDocument,
{
lat: entity.value.latitude,
lon: entity.value.longitude,
radius: 500
},
'public',
'geo'
)
// Load products (offers grouped by product)
execute(
NearestOffersDocument,
{
lat: entity.value.latitude,
lon: entity.value.longitude,
radius: 500
},
'public',
'geo'
).then(offersData => {
// Group offers by product // Group offers by product
const productsMap = new Map<string, any>() const productsMap = new Map<string, any>()
offersData?.nearestOffers?.forEach((offer: any) => { offersData?.nearestOffers?.forEach((offer: any) => {
@@ -142,13 +178,30 @@ export function useCatalogInfo() {
} }
}) })
relatedProducts.value = Array.from(productsMap.values()) relatedProducts.value = Array.from(productsMap.values())
} finally { }).finally(() => {
isLoadingProducts.value = false isLoadingProducts.value = false
} })
// Note: Hubs will be loaded after product selection // Load hubs near supplier
execute(
NearestHubsDocument,
{
lat: entity.value.latitude,
lon: entity.value.longitude,
radius: 1000,
limit: 12
},
'public',
'geo'
).then(hubsData => {
relatedHubs.value = hubsData?.nearestHubs || []
}).finally(() => {
isLoadingHubs.value = false
})
} catch (error) { } catch (error) {
console.error('Error loading supplier info:', error) console.error('Error loading supplier info:', error)
isLoadingProducts.value = false
isLoadingHubs.value = false
} }
} }

View File

@@ -44,7 +44,6 @@
:related-suppliers="relatedSuppliers" :related-suppliers="relatedSuppliers"
:related-offers="relatedOffers" :related-offers="relatedOffers"
:selected-product="infoProduct ?? null" :selected-product="infoProduct ?? null"
:current-tab="infoTab"
:loading="infoLoading" :loading="infoLoading"
:loading-products="isLoadingProducts" :loading-products="isLoadingProducts"
:loading-hubs="isLoadingHubs" :loading-hubs="isLoadingHubs"
@@ -54,7 +53,6 @@
@add-to-filter="onInfoAddToFilter" @add-to-filter="onInfoAddToFilter"
@open-info="onInfoOpenRelated" @open-info="onInfoOpenRelated"
@select-product="onInfoSelectProduct" @select-product="onInfoSelectProduct"
@update:current-tab="setInfoTab"
/> />
<!-- Quote results: show offers after search --> <!-- Quote results: show offers after search -->
@@ -111,7 +109,6 @@ const {
catalogMode, catalogMode,
selectMode, selectMode,
infoId, infoId,
infoTab,
infoProduct, infoProduct,
productId, productId,
supplierId, supplierId,
@@ -124,7 +121,6 @@ const {
cancelSelect, cancelSelect,
openInfo, openInfo,
closeInfo, closeInfo,
setInfoTab,
setInfoProduct, setInfoProduct,
setLabel setLabel
} = useCatalogSearch() } = useCatalogSearch()
@@ -261,7 +257,7 @@ watch(infoProduct, async (productUuid) => {
} }
}) })
// Related points for Info mode (shown on map) - filtered by active tab // Related points for Info mode (shown on map) - show all related entities
const relatedPoints = computed(() => { const relatedPoints = computed(() => {
if (!infoId.value) return [] if (!infoId.value) return []
@@ -273,57 +269,31 @@ const relatedPoints = computed(() => {
type: 'hub' | 'supplier' | 'offer' type: 'hub' | 'supplier' | 'offer'
}> = [] }> = []
const currentTab = infoTab.value // Add all hubs
relatedHubs.value.forEach(hub => {
if (hub.latitude && hub.longitude) {
points.push({
uuid: hub.uuid,
name: hub.name,
latitude: hub.latitude,
longitude: hub.longitude,
type: 'hub'
})
}
})
// Show content based on active tab // Add all suppliers
// Hub entity: offers tab → offers, suppliers tab → suppliers relatedSuppliers.value.forEach(supplier => {
// Supplier entity: offers tab → offers, hubs tab → hubs if (supplier.latitude && supplier.longitude) {
// Offer entity: hubs tab → hubs, suppliers tab → suppliers points.push({
uuid: supplier.uuid,
// Add hubs (for supplier's hubs tab or offer's hubs tab) name: supplier.name,
if (currentTab === 'hubs') { latitude: supplier.latitude,
relatedHubs.value.forEach(hub => { longitude: supplier.longitude,
if (hub.latitude && hub.longitude) { type: 'supplier'
points.push({ })
uuid: hub.uuid, }
name: hub.name, })
latitude: hub.latitude,
longitude: hub.longitude,
type: 'hub'
})
}
})
}
// Add suppliers (for hub's suppliers tab or offer's suppliers tab)
if (currentTab === 'suppliers') {
relatedSuppliers.value.forEach(supplier => {
if (supplier.latitude && supplier.longitude) {
points.push({
uuid: supplier.uuid,
name: supplier.name,
latitude: supplier.latitude,
longitude: supplier.longitude,
type: 'supplier'
})
}
})
}
// Add offers (for hub's/supplier's offers tab when product is selected)
if (currentTab === 'offers' && infoProduct.value) {
relatedOffers.value.forEach(offer => {
if (offer.latitude && offer.longitude) {
points.push({
uuid: offer.uuid,
name: offer.productName || offer.name,
latitude: offer.latitude,
longitude: offer.longitude,
type: 'offer'
})
}
})
}
return points return points
}) })

View File

@@ -56,7 +56,14 @@
}, },
"info": { "info": {
"selectProductFirst": "Select a product first", "selectProductFirst": "Select a product first",
"addToFilter": "Add to filter" "addToFilter": "Add to filter",
"productsHere": "Products available here",
"productsFromSupplier": "Products from this supplier",
"nearestHubs": "Nearest hubs",
"suppliersNearby": "Suppliers nearby",
"noHubs": "No hubs found",
"noSuppliers": "No suppliers found",
"viewSupplier": "View supplier"
}, },
"modes": { "modes": {
"explore": "Explore", "explore": "Explore",

View File

@@ -56,7 +56,14 @@
}, },
"info": { "info": {
"selectProductFirst": "Сначала выберите товар", "selectProductFirst": "Сначала выберите товар",
"addToFilter": "Добавить в фильтр" "addToFilter": "Добавить в фильтр",
"productsHere": "Товары доступные здесь",
"productsFromSupplier": "Товары от этого поставщика",
"nearestHubs": "Ближайшие хабы",
"suppliersNearby": "Поставщики рядом",
"noHubs": "Хабы не найдены",
"noSuppliers": "Поставщики не найдены",
"viewSupplier": "Посмотреть поставщика"
}, },
"modes": { "modes": {
"explore": "Исследовать", "explore": "Исследовать",