Add unified MapPanel component for left map panels
All checks were successful
Build Docker Image / build (push) Successful in 3m25s
All checks were successful
Build Docker Image / build (push) Successful in 3m25s
- Create MapPanel with white glass header, dark content - Refactor SelectionPanel to use MapPanel - Refactor QuotePanel to use MapPanel - Single source of truth for panel styling
This commit is contained in:
13
app/components/catalog/MapPanel.vue
Normal file
13
app/components/catalog/MapPanel.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full -m-4">
|
||||||
|
<!-- Header: белое стекло -->
|
||||||
|
<div class="sticky top-0 z-10 p-4 rounded-t-xl bg-white/90 backdrop-blur-md border-b border-white/20">
|
||||||
|
<slot name="header" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content: тёмный (наследует контейнер) -->
|
||||||
|
<div class="flex-1 px-4 pt-3 pb-4 overflow-y-auto">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,36 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 h-full">
|
<MapPanel>
|
||||||
<!-- Header -->
|
<template #header>
|
||||||
<div class="flex items-center justify-between flex-shrink-0">
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="font-semibold text-lg">{{ $t('catalog.headers.offers') }}</h3>
|
<h3 class="font-semibold text-base text-base-content">{{ $t('catalog.headers.offers') }}</h3>
|
||||||
<span class="badge badge-neutral">{{ offers.length }}</span>
|
<span class="badge badge-neutral">{{ offers.length }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-md" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results section -->
|
<div v-else-if="offers.length === 0" class="text-center py-8 text-white/60">
|
||||||
<div class="flex-1 overflow-y-auto -mx-1 px-1">
|
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
||||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
<p>{{ $t('catalog.empty.noOffers') }}</p>
|
||||||
<span class="loading loading-spinner loading-md" />
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="offers.length === 0" class="text-center py-8 text-base-content/60">
|
<div v-else class="flex flex-col gap-3">
|
||||||
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
<div
|
||||||
<p>{{ $t('catalog.empty.noOffers') }}</p>
|
v-for="offer in offers"
|
||||||
</div>
|
:key="offer.uuid"
|
||||||
|
class="cursor-pointer"
|
||||||
<div v-else class="flex flex-col gap-3">
|
@click="emit('select-offer', offer)"
|
||||||
<div
|
>
|
||||||
v-for="offer in offers"
|
<slot name="offer-card" :offer="offer">
|
||||||
:key="offer.uuid"
|
<OfferCard :offer="offer" linkable />
|
||||||
class="cursor-pointer"
|
</slot>
|
||||||
@click="emit('select-offer', offer)"
|
|
||||||
>
|
|
||||||
<slot name="offer-card" :offer="offer">
|
|
||||||
<OfferCard :offer="offer" linkable />
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MapPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full -m-4">
|
<MapPanel>
|
||||||
<!-- Header + Search (dark glass, sticky) -->
|
<template #header>
|
||||||
<div class="sticky top-0 z-10 p-4 rounded-t-xl bg-black/50 backdrop-blur-md border-b border-white/10">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<h3 class="font-semibold text-base text-white">{{ title }}</h3>
|
<h3 class="font-semibold text-base text-base-content">{{ title }}</h3>
|
||||||
<button class="btn btn-ghost btn-xs btn-circle text-white/60 hover:text-white" @click="emit('close')">
|
<button class="btn btn-ghost btn-xs btn-circle text-base-content/60 hover:text-base-content" @click="emit('close')">
|
||||||
<Icon name="lucide:x" size="16" />
|
<Icon name="lucide:x" size="16" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,85 +11,83 @@
|
|||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="searchPlaceholder"
|
:placeholder="searchPlaceholder"
|
||||||
class="input input-sm w-full bg-white/10 border-white/20 text-white placeholder:text-white/50"
|
class="input input-sm w-full bg-white/50 border-base-300/50 text-base-content placeholder:text-base-content/50"
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-md" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- List -->
|
<div v-else-if="filteredItems.length === 0" class="text-center py-8 text-white/60">
|
||||||
<div class="flex-1 px-4 pt-3 pb-4 overflow-y-auto">
|
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
||||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
<p>{{ $t('catalog.empty.noResults') }}</p>
|
||||||
<span class="loading loading-spinner loading-md" />
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="filteredItems.length === 0" class="text-center py-8 text-white/60">
|
<div v-else class="flex flex-col gap-2">
|
||||||
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
<!-- Products -->
|
||||||
<p>{{ $t('catalog.empty.noResults') }}</p>
|
<template v-if="selectMode === 'product'">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="flex flex-col gap-2">
|
|
||||||
<!-- Products -->
|
|
||||||
<template v-if="selectMode === 'product'">
|
|
||||||
<div
|
|
||||||
v-for="item in filteredItems"
|
|
||||||
:key="item.uuid"
|
|
||||||
@mouseenter="emit('hover', item.uuid)"
|
|
||||||
@mouseleave="emit('hover', null)"
|
|
||||||
>
|
|
||||||
<ProductCard
|
|
||||||
:product="item"
|
|
||||||
selectable
|
|
||||||
compact
|
|
||||||
:is-selected="selectedId === item.uuid"
|
|
||||||
@select="onSelect(item)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Hubs -->
|
|
||||||
<template v-else-if="selectMode === 'hub'">
|
|
||||||
<div
|
|
||||||
v-for="item in filteredItems"
|
|
||||||
:key="item.uuid"
|
|
||||||
@mouseenter="emit('hover', item.uuid)"
|
|
||||||
@mouseleave="emit('hover', null)"
|
|
||||||
>
|
|
||||||
<HubCard
|
|
||||||
:hub="item"
|
|
||||||
selectable
|
|
||||||
:is-selected="selectedId === item.uuid"
|
|
||||||
@select="onSelect(item)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Suppliers -->
|
|
||||||
<template v-else-if="selectMode === 'supplier'">
|
|
||||||
<div
|
|
||||||
v-for="item in filteredItems"
|
|
||||||
:key="item.uuid"
|
|
||||||
@mouseenter="emit('hover', item.uuid)"
|
|
||||||
@mouseleave="emit('hover', null)"
|
|
||||||
>
|
|
||||||
<SupplierCard
|
|
||||||
:supplier="item"
|
|
||||||
selectable
|
|
||||||
:is-selected="selectedId === item.uuid"
|
|
||||||
@select="onSelect(item)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Infinite scroll sentinel -->
|
|
||||||
<div
|
<div
|
||||||
v-if="hasMore && !searchQuery"
|
v-for="item in filteredItems"
|
||||||
ref="loadMoreSentinel"
|
:key="item.uuid"
|
||||||
class="flex items-center justify-center py-4"
|
@mouseenter="emit('hover', item.uuid)"
|
||||||
|
@mouseleave="emit('hover', null)"
|
||||||
>
|
>
|
||||||
<span v-if="loadingMore" class="loading loading-spinner loading-sm text-white/60" />
|
<ProductCard
|
||||||
|
:product="item"
|
||||||
|
selectable
|
||||||
|
compact
|
||||||
|
:is-selected="selectedId === item.uuid"
|
||||||
|
@select="onSelect(item)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Hubs -->
|
||||||
|
<template v-else-if="selectMode === 'hub'">
|
||||||
|
<div
|
||||||
|
v-for="item in filteredItems"
|
||||||
|
:key="item.uuid"
|
||||||
|
@mouseenter="emit('hover', item.uuid)"
|
||||||
|
@mouseleave="emit('hover', null)"
|
||||||
|
>
|
||||||
|
<HubCard
|
||||||
|
:hub="item"
|
||||||
|
selectable
|
||||||
|
:is-selected="selectedId === item.uuid"
|
||||||
|
@select="onSelect(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Suppliers -->
|
||||||
|
<template v-else-if="selectMode === 'supplier'">
|
||||||
|
<div
|
||||||
|
v-for="item in filteredItems"
|
||||||
|
:key="item.uuid"
|
||||||
|
@mouseenter="emit('hover', item.uuid)"
|
||||||
|
@mouseleave="emit('hover', null)"
|
||||||
|
>
|
||||||
|
<SupplierCard
|
||||||
|
:supplier="item"
|
||||||
|
selectable
|
||||||
|
:is-selected="selectedId === item.uuid"
|
||||||
|
@select="onSelect(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Infinite scroll sentinel -->
|
||||||
|
<div
|
||||||
|
v-if="hasMore && !searchQuery"
|
||||||
|
ref="loadMoreSentinel"
|
||||||
|
class="flex items-center justify-center py-4"
|
||||||
|
>
|
||||||
|
<span v-if="loadingMore" class="loading loading-spinner loading-sm text-white/60" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MapPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
Reference in New Issue
Block a user