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>
|
||||
<div class="flex flex-col gap-4 h-full">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between flex-shrink-0">
|
||||
<h3 class="font-semibold text-lg">{{ $t('catalog.headers.offers') }}</h3>
|
||||
<span class="badge badge-neutral">{{ offers.length }}</span>
|
||||
<MapPanel>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold text-base text-base-content">{{ $t('catalog.headers.offers') }}</h3>
|
||||
<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>
|
||||
|
||||
<!-- Results section -->
|
||||
<div class="flex-1 overflow-y-auto -mx-1 px-1">
|
||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-md" />
|
||||
</div>
|
||||
<div v-else-if="offers.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
||||
<p>{{ $t('catalog.empty.noOffers') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="offers.length === 0" class="text-center py-8 text-base-content/60">
|
||||
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
||||
<p>{{ $t('catalog.empty.noOffers') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="offer in offers"
|
||||
:key="offer.uuid"
|
||||
class="cursor-pointer"
|
||||
@click="emit('select-offer', offer)"
|
||||
>
|
||||
<slot name="offer-card" :offer="offer">
|
||||
<OfferCard :offer="offer" linkable />
|
||||
</slot>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="offer in offers"
|
||||
:key="offer.uuid"
|
||||
class="cursor-pointer"
|
||||
@click="emit('select-offer', offer)"
|
||||
>
|
||||
<slot name="offer-card" :offer="offer">
|
||||
<OfferCard :offer="offer" linkable />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MapPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full -m-4">
|
||||
<!-- Header + Search (dark glass, sticky) -->
|
||||
<div class="sticky top-0 z-10 p-4 rounded-t-xl bg-black/50 backdrop-blur-md border-b border-white/10">
|
||||
<MapPanel>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="font-semibold text-base text-white">{{ title }}</h3>
|
||||
<button class="btn btn-ghost btn-xs btn-circle text-white/60 hover:text-white" @click="emit('close')">
|
||||
<h3 class="font-semibold text-base text-base-content">{{ title }}</h3>
|
||||
<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" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -12,85 +11,83 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
: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>
|
||||
|
||||
<!-- List -->
|
||||
<div class="flex-1 px-4 pt-3 pb-4 overflow-y-auto">
|
||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-md" />
|
||||
</div>
|
||||
<div v-else-if="filteredItems.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
||||
<p>{{ $t('catalog.empty.noResults') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredItems.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
||||
<p>{{ $t('catalog.empty.noResults') }}</p>
|
||||
</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 v-else class="flex flex-col gap-2">
|
||||
<!-- Products -->
|
||||
<template v-if="selectMode === 'product'">
|
||||
<div
|
||||
v-if="hasMore && !searchQuery"
|
||||
ref="loadMoreSentinel"
|
||||
class="flex items-center justify-center py-4"
|
||||
v-for="item in filteredItems"
|
||||
:key="item.uuid"
|
||||
@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>
|
||||
</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>
|
||||
</MapPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
Reference in New Issue
Block a user