refactor: simplify LoadingErrorState and enhance MarketSearchBar functionality
- Removed unnecessary imports and streamlined the LoadingErrorState component by eliminating redundant props. - Improved keyboard handling in MarketSearchBar to support basic Escape key functionality and enhanced keyboard shortcuts. - Updated MerchantStore and MarketPage components to utilize the revised LoadingErrorState and MarketSearchBar, ensuring consistent loading/error handling and search capabilities. - Enhanced StallView to provide better category filtering and product search experience. These changes improve code clarity and maintainability while enhancing user interaction across the market module.
This commit is contained in:
parent
c8860dc937
commit
e68a7a9ed5
5 changed files with 109 additions and 81 deletions
|
|
@ -65,7 +65,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { AlertCircle } from 'lucide-vue-next'
|
import { AlertCircle } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
|
@ -116,7 +115,7 @@ interface Emits {
|
||||||
(e: 'retry'): void
|
(e: 'retry'): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasError: false,
|
hasError: false,
|
||||||
showRetry: true,
|
showRetry: true,
|
||||||
|
|
@ -124,7 +123,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
fullHeight: true
|
fullHeight: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
defineEmits<Emits>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -292,14 +292,26 @@ const handleClear = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
if (!props.showEnhancements || !useSearchKeyboardShortcuts.value) return
|
if (!props.showEnhancements) return
|
||||||
|
|
||||||
const shouldClear = handleSearchKeydown(event)
|
// Handle basic Escape key for simple mode
|
||||||
if (shouldClear) {
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
handleClear()
|
handleClear()
|
||||||
} else {
|
}
|
||||||
blurSearchInput()
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use enhanced keyboard shortcuts if available
|
||||||
|
if (useSearchKeyboardShortcuts.value && handleSearchKeydown) {
|
||||||
|
const shouldClear = handleSearchKeydown(event)
|
||||||
|
if (shouldClear) {
|
||||||
|
if (searchQuery.value) {
|
||||||
|
handleClear()
|
||||||
|
} else {
|
||||||
|
blurSearchInput()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +359,7 @@ const handleBlur = () => {
|
||||||
// Enhanced mode methods (initialized conditionally)
|
// Enhanced mode methods (initialized conditionally)
|
||||||
let focusSearchInput = () => {}
|
let focusSearchInput = () => {}
|
||||||
let blurSearchInput = () => {}
|
let blurSearchInput = () => {}
|
||||||
let handleSearchKeydown = () => false
|
let handleSearchKeydown = (_event: KeyboardEvent) => false
|
||||||
|
|
||||||
// Initialize keyboard shortcuts for enhanced mode
|
// Initialize keyboard shortcuts for enhanced mode
|
||||||
watch(() => props.showEnhancements, async (showEnhancements) => {
|
watch(() => props.showEnhancements, async (showEnhancements) => {
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<LoadingErrorState
|
<!-- Loading State -->
|
||||||
:is-loading="isLoadingMerchant"
|
<div v-if="isLoadingMerchant" class="flex justify-center items-center py-12">
|
||||||
loading-message="Loading your merchant profile..."
|
<div class="flex flex-col items-center space-y-4">
|
||||||
:has-error="!!merchantCheckError"
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
error-title="Error Loading Merchant Status"
|
<p class="text-gray-600">Loading your merchant profile...</p>
|
||||||
:error-message="merchantCheckError || ''"
|
|
||||||
@retry="checkMerchantProfile"
|
|
||||||
:full-height="false"
|
|
||||||
>
|
|
||||||
<!-- No Merchant Profile Empty State -->
|
|
||||||
<div v-if="!userHasMerchantProfile" class="flex flex-col items-center justify-center py-12">
|
|
||||||
<div class="w-24 h-24 bg-muted rounded-full flex items-center justify-center mb-6">
|
|
||||||
<User class="w-12 h-12 text-muted-foreground" />
|
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl font-bold text-foreground mb-2">Create Your Merchant Profile</h2>
|
|
||||||
<p class="text-muted-foreground text-center mb-6 max-w-md">
|
|
||||||
Before you can create a store, you need to set up your merchant profile. This will create your merchant identity on the Nostr marketplace.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
@click="createMerchantProfile"
|
|
||||||
variant="default"
|
|
||||||
size="lg"
|
|
||||||
:disabled="isCreatingMerchant"
|
|
||||||
>
|
|
||||||
<div v-if="isCreatingMerchant" class="flex items-center">
|
|
||||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
|
||||||
<span>Creating...</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex items-center">
|
|
||||||
<Plus class="w-5 h-5 mr-2" />
|
|
||||||
<span>Create Merchant Profile</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stores Grid (shown when merchant profile exists) -->
|
<!-- Error State -->
|
||||||
|
<div v-else-if="merchantCheckError" class="flex justify-center items-center py-12">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="text-2xl font-bold text-red-600 mb-4">Error Loading Merchant Status</h2>
|
||||||
|
<p class="text-gray-600 mb-4">{{ merchantCheckError }}</p>
|
||||||
|
<Button @click="checkMerchantProfile" variant="outline">
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Header Section -->
|
<!-- No Merchant Profile Empty State -->
|
||||||
<div class="mb-8">
|
<div v-if="!userHasMerchantProfile" class="flex flex-col items-center justify-center py-12">
|
||||||
|
<div class="w-24 h-24 bg-muted rounded-full flex items-center justify-center mb-6">
|
||||||
|
<User class="w-12 h-12 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-foreground mb-2">Create Your Merchant Profile</h2>
|
||||||
|
<p class="text-muted-foreground text-center mb-6 max-w-md">
|
||||||
|
Before you can create a store, you need to set up your merchant profile. This will create your merchant identity on the Nostr marketplace.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
@click="createMerchantProfile"
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
:disabled="isCreatingMerchant"
|
||||||
|
>
|
||||||
|
<div v-if="isCreatingMerchant" class="flex items-center">
|
||||||
|
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||||
|
<span>Creating...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center">
|
||||||
|
<Plus class="w-5 h-5 mr-2" />
|
||||||
|
<span>Create Merchant Profile</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stores Grid (shown when merchant profile exists) -->
|
||||||
|
<div v-else>
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="mb-8">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold text-foreground">My Stores</h2>
|
<h2 class="text-2xl font-bold text-foreground">My Stores</h2>
|
||||||
|
|
@ -297,25 +309,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create Store Dialog -->
|
<!-- Create Store Dialog -->
|
||||||
<CreateStoreDialog
|
<CreateStoreDialog
|
||||||
:is-open="showCreateStoreDialog"
|
:is-open="showCreateStoreDialog"
|
||||||
@close="showCreateStoreDialog = false"
|
@close="showCreateStoreDialog = false"
|
||||||
@created="onStoreCreated"
|
@created="onStoreCreated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Create Product Dialog -->
|
<!-- Create Product Dialog -->
|
||||||
<CreateProductDialog
|
<CreateProductDialog
|
||||||
:is-open="showCreateProductDialog"
|
:is-open="showCreateProductDialog"
|
||||||
:stall="activeStall"
|
:stall="activeStall"
|
||||||
:product="editingProduct"
|
:product="editingProduct"
|
||||||
@close="closeProductDialog"
|
@close="closeProductDialog"
|
||||||
@created="onProductCreated"
|
@created="onProductCreated"
|
||||||
@updated="onProductUpdated"
|
@updated="onProductUpdated"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</LoadingErrorState>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
@ -324,14 +334,12 @@ import { useRouter } from 'vue-router'
|
||||||
import { useMarketStore } from '@/modules/market/stores/market'
|
import { useMarketStore } from '@/modules/market/stores/market'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import LoadingErrorState from './LoadingErrorState.vue'
|
import {
|
||||||
import {
|
Package,
|
||||||
Package,
|
Store,
|
||||||
Store,
|
DollarSign,
|
||||||
DollarSign,
|
Star,
|
||||||
Star,
|
Plus,
|
||||||
Plus,
|
|
||||||
AlertCircle,
|
|
||||||
User
|
User
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import type { NostrmarketAPI, Merchant, Stall } from '../services/nostrmarketAPI'
|
import type { NostrmarketAPI, Merchant, Stall } from '../services/nostrmarketAPI'
|
||||||
|
|
@ -632,4 +640,4 @@ watch(() => activeStallId.value, async (newStallId) => {
|
||||||
stallProducts.value = []
|
stallProducts.value = []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<LoadingErrorState
|
<LoadingErrorState
|
||||||
:is-loading="!isMarketReady && ((marketStore.isLoading ?? false) || marketPreloader.isPreloading)"
|
:is-loading="!isMarketReady && ((marketStore.isLoading ?? false) || marketPreloader.isPreloading.value)"
|
||||||
:loading-message="marketPreloader.isPreloading ? 'Preloading market...' : 'Loading market...'"
|
:loading-message="marketPreloader.isPreloading.value ? 'Preloading market...' : 'Loading market...'"
|
||||||
:has-error="!!(marketStore.error || marketPreloader.preloadError) && marketStore.products.length === 0"
|
:has-error="!!(marketStore.error || marketPreloader.preloadError.value) && marketStore.products.length === 0"
|
||||||
error-title="Failed to load market"
|
error-title="Failed to load market"
|
||||||
:error-message="marketStore.error || marketPreloader.preloadError || ''"
|
:error-message="marketStore.error || marketPreloader.preloadError.value || ''"
|
||||||
@retry="retryLoadMarket"
|
@retry="retryLoadMarket"
|
||||||
>
|
>
|
||||||
<!-- Market Header - Optimized for Mobile -->
|
<!-- Market Header - Optimized for Mobile -->
|
||||||
|
|
@ -82,7 +82,6 @@ import { useMarket } from '../composables/useMarket'
|
||||||
import { useMarketPreloader } from '../composables/useMarketPreloader'
|
import { useMarketPreloader } from '../composables/useMarketPreloader'
|
||||||
import { useCategoryFilter } from '../composables/useCategoryFilter'
|
import { useCategoryFilter } from '../composables/useCategoryFilter'
|
||||||
import { config } from '@/lib/config'
|
import { config } from '@/lib/config'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||||
import MarketSearchBar from '../components/MarketSearchBar.vue'
|
import MarketSearchBar from '../components/MarketSearchBar.vue'
|
||||||
import ProductGrid from '../components/ProductGrid.vue'
|
import ProductGrid from '../components/ProductGrid.vue'
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,9 @@
|
||||||
<CategoryFilterBar
|
<CategoryFilterBar
|
||||||
v-if="stallCategories.length > 0"
|
v-if="stallCategories.length > 0"
|
||||||
:categories="stallCategories"
|
:categories="stallCategories"
|
||||||
:selected-categories="selectedCategories"
|
:selected-count="selectedCategories.length"
|
||||||
:filter-mode="filterMode"
|
:filter-mode="filterMode"
|
||||||
|
:product-count="productCount"
|
||||||
title="Categories"
|
title="Categories"
|
||||||
@toggle-category="toggleCategoryFilter"
|
@toggle-category="toggleCategoryFilter"
|
||||||
@set-filter-mode="setFilterMode"
|
@set-filter-mode="setFilterMode"
|
||||||
|
|
@ -81,7 +82,7 @@
|
||||||
<div class="mb-4 sm:mb-6 flex flex-col sm:flex-row gap-2 sm:gap-4">
|
<div class="mb-4 sm:mb-6 flex flex-col sm:flex-row gap-2 sm:gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<MarketSearchBar
|
<MarketSearchBar
|
||||||
:data="stallProducts"
|
:data="stallProducts as Product[]"
|
||||||
:options="searchOptions"
|
:options="searchOptions"
|
||||||
placeholder="Search products in this stall..."
|
placeholder="Search products in this stall..."
|
||||||
:show-enhancements="false"
|
:show-enhancements="false"
|
||||||
|
|
@ -203,11 +204,20 @@ const stallProducts = computed(() => {
|
||||||
|
|
||||||
// Get unique categories for this stall
|
// Get unique categories for this stall
|
||||||
const stallCategories = computed(() => {
|
const stallCategories = computed(() => {
|
||||||
const categories = new Set<string>()
|
const categoryCount = new Map<string, number>()
|
||||||
stallProducts.value.forEach(product => {
|
stallProducts.value.forEach(product => {
|
||||||
product.categories?.forEach(cat => categories.add(cat))
|
product.categories?.forEach(cat => {
|
||||||
|
categoryCount.set(cat, (categoryCount.get(cat) || 0) + 1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return Array.from(categories).sort()
|
|
||||||
|
return Array.from(categoryCount.entries())
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
.map(([category, count]) => ({
|
||||||
|
category,
|
||||||
|
count,
|
||||||
|
selected: selectedCategories.value.includes(category)
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Product count
|
// Product count
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue