Implement store creation dialog in MerchantStore component

- Introduce a dialog for creating new stores, allowing users to input store name, description, currency, and shipping zones.
- Add functionality to manage shipping zones, including the ability to create new zones and select existing ones.
- Enhance the stall creation process with error handling and loading states, providing better user feedback during store setup.
- Update the NostrmarketAPI to support fetching available currencies and shipping zones, improving integration with the backend services.

These changes streamline the store creation experience for merchants, ensuring a more intuitive and guided process.

Refactor getCurrencies method in NostrmarketAPI to improve currency retrieval logic

- Introduce base currencies and enhance the logic to combine them with API currencies, ensuring no duplicates.
- Update debug logging to provide clearer information on currency retrieval outcomes.
- Simplify fallback mechanism to use base currencies directly in case of API failures.

These changes enhance the reliability and clarity of currency data handling in the NostrmarketAPI.

Refactor MerchantStore component to use NATIVE checkbox selection and add debug information

- Replace Checkbox component with native input checkboxes for zone selection, simplifying the binding with v-model.
- Enhance the user interface by adding debug information displaying the store name, selected zones count, and creation status.
- These changes improve the clarity of the zone selection process and provide useful debugging insights during store creation.

Enhance zone selection functionality in MerchantStore component

- Replace v-model with native checkbox handling for zone selection, improving clarity and user interaction.
- Add debug information to display currently selected zones, aiding in user understanding of their selections.
- Implement a new method to manage zone toggling, ensuring accurate updates to the selected zones array.

These changes streamline the zone selection process and provide better feedback for users during stall creation.

Improve zone selection handling and debugging in MerchantStore component

- Update zone selection to use a custom Checkbox component, enhancing user interaction and clarity.
- Add detailed debug information for selected zones, including raw array output and type, to aid in troubleshooting.
- Refactor the zone toggle logic to handle various input types, ensuring accurate updates to the selected zones array.

These changes enhance the user experience during stall creation by providing better feedback and more robust zone selection functionality.

Refactor Checkbox handling in MerchantStore component for improved zone selection

- Update zone selection to utilize the Shadcn/UI Checkbox component with v-model for better state management.
- Remove manual zone toggle logic and debug information, streamlining the component's functionality.
- Enhance user interaction by following recommended patterns for checkbox usage, ensuring reliable selections.

These changes improve the clarity and reliability of zone selection during stall creation.

Refactor MerchantStore component to utilize Shadcn Form components and improve form handling

- Replace existing form elements with Shadcn Form components (FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage) for better structure and validation.
- Integrate vee-validate and zod for type-safe form validation, enhancing user experience and error handling.
- Update shipping zone selection to use the new form structure, improving clarity and accessibility.
- Implement form submission logic with validation checks, ensuring required fields are filled before submission.

These changes enhance the overall form handling and user interaction during the store creation process.
This commit is contained in:
padreug 2025-09-08 14:57:03 +02:00
parent 8b696c406a
commit e6107839a0
2 changed files with 551 additions and 14 deletions

View file

@ -56,7 +56,7 @@
<p class="text-muted-foreground text-center mb-6 max-w-md">
Great! You have a merchant profile. Now create your first store (stall) to start listing products and receiving orders.
</p>
<Button @click="createStall" variant="default" size="lg">
<Button @click="initializeStallCreation" variant="default" size="lg">
<Plus class="w-5 h-5 mr-2" />
Create Store
</Button>
@ -361,14 +361,235 @@
</div>
</div> <!-- End of Store Content wrapper -->
</div>
<!-- Create Stall Dialog -->
<Dialog v-model:open="showStallDialog">
<DialogContent class="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Create New Store</DialogTitle>
</DialogHeader>
<form @submit="onSubmit" class="space-y-6 py-4">
<!-- Basic Store Info -->
<div class="space-y-4">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Store Name *</FormLabel>
<FormControl>
<Input
placeholder="Enter your store name"
:disabled="isCreatingStall"
v-bind="componentField"
/>
</FormControl>
<FormDescription>
Choose a unique name for your store
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="description">
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Describe your store and products"
:disabled="isCreatingStall"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="currency">
<FormItem>
<FormLabel>Currency *</FormLabel>
<Select :disabled="isCreatingStall" v-bind="componentField">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select currency" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem v-for="currency in availableCurrencies" :key="currency" :value="currency">
{{ currency }}
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
</div>
<!-- Shipping Zones Section -->
<FormField name="selectedZones">
<FormItem>
<div class="mb-4">
<FormLabel class="text-base">Shipping Zones *</FormLabel>
<FormDescription>
Select existing zones or create new ones for your store
</FormDescription>
</div>
<!-- Existing Zones -->
<div v-if="availableZones.length > 0" class="space-y-3">
<FormLabel class="text-sm font-medium">Available Zones:</FormLabel>
<div class="space-y-2 max-h-32 overflow-y-auto">
<FormField
v-for="zone in availableZones"
:key="zone.id"
v-slot="{ value, handleChange }"
type="checkbox"
:value="zone.id"
:unchecked-value="false"
name="selectedZones"
>
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
:model-value="value.includes(zone.id)"
@update:model-value="handleChange"
:disabled="isCreatingStall"
/>
</FormControl>
<FormLabel class="text-sm cursor-pointer font-normal">
{{ zone.name }} - {{ zone.cost }} {{ zone.currency }}
<span class="text-muted-foreground ml-1">({{ zone.countries.slice(0, 2).join(', ') }}{{ zone.countries.length > 2 ? '...' : '' }})</span>
</FormLabel>
</FormItem>
</FormField>
</div>
</div>
<FormMessage />
</FormItem>
</FormField>
<!-- Create New Zone -->
<div class="border-t pt-4">
<Label class="text-sm font-medium">Create New Zone:</Label>
<div class="space-y-3 mt-2">
<FormField v-slot="{ componentField }" name="newZone.name">
<FormItem>
<FormLabel>Zone Name</FormLabel>
<FormControl>
<Input
placeholder="e.g., Europe, Worldwide"
:disabled="isCreatingStall"
v-bind="componentField"
/>
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="newZone.cost">
<FormItem>
<FormLabel>Shipping Cost</FormLabel>
<FormControl>
<Input
type="number"
min="0"
:placeholder="`Cost in ${getFieldValue('currency') || 'sat'}`"
:disabled="isCreatingStall"
v-bind="componentField"
/>
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="newZone.selectedCountries">
<FormItem>
<FormLabel>Countries/Regions</FormLabel>
<FormControl>
<Select multiple :disabled="isCreatingStall" v-bind="componentField">
<SelectTrigger>
<SelectValue placeholder="Select countries/regions" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="country in countries" :key="country" :value="country">
{{ country }}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<div v-if="getFieldValue('newZone')?.selectedCountries?.length > 0" class="mt-2">
<div class="flex flex-wrap gap-1">
<Badge
v-for="country in getFieldValue('newZone').selectedCountries"
:key="country"
variant="secondary"
class="text-xs"
>
{{ country }}
</Badge>
</div>
</div>
</FormItem>
</FormField>
<Button
@click="addNewZone"
type="button"
variant="outline"
size="sm"
:disabled="isCreatingStall || !getFieldValue('newZone')?.name || !getFieldValue('newZone')?.selectedCountries?.length"
>
<Plus class="w-4 h-4 mr-2" />
Add Zone
</Button>
</div>
</div>
<!-- Error Display -->
<div v-if="stallCreateError" class="text-sm text-destructive">
{{ stallCreateError }}
</div>
<div class="flex justify-end space-x-2 pt-4">
<Button
@click="showStallDialog = false"
variant="outline"
:disabled="isCreatingStall"
>
Cancel
</Button>
<Button
type="submit"
:disabled="isCreatingStall || !isFormValid"
>
<span v-if="isCreatingStall">Creating...</span>
<span v-else>Create Store</span>
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import { useMarketStore } from '@/modules/market/stores/market'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Checkbox } from '@/components/ui/checkbox'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import {
Package,
Store,
@ -387,7 +608,7 @@ import {
} from 'lucide-vue-next'
import type { OrderStatus } from '@/modules/market/stores/market'
import type { NostrmarketService } from '../services/nostrmarketService'
import type { NostrmarketAPI, Merchant } from '../services/nostrmarketAPI'
import type { NostrmarketAPI, Merchant, Zone, CreateStallRequest } from '../services/nostrmarketAPI'
import { auth } from '@/composables/useAuthService'
import { useToast } from '@/core/composables/useToast'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
@ -406,6 +627,70 @@ const merchantCheckError = ref<string | null>(null)
const isCreatingMerchant = ref(false)
const merchantCreateError = ref<string | null>(null)
// Stall creation state
const isCreatingStall = ref(false)
const stallCreateError = ref<string | null>(null)
const showStallDialog = ref(false)
const availableCurrencies = ref<string[]>(['sat'])
const availableZones = ref<Zone[]>([])
const countries = ref([
'Free (digital)', 'Worldwide', 'Europe', 'Australia', 'Austria', 'Belgium', 'Brazil', 'Canada',
'Denmark', 'Finland', 'France', 'Germany', 'Greece', 'Hong Kong', 'Hungary',
'Ireland', 'Indonesia', 'Israel', 'Italy', 'Japan', 'Kazakhstan', 'Korea',
'Luxembourg', 'Malaysia', 'Mexico', 'Netherlands', 'New Zealand', 'Norway',
'Poland', 'Portugal', 'Romania', 'Russia', 'Saudi Arabia', 'Singapore',
'Spain', 'Sweden', 'Switzerland', 'Thailand', 'Turkey', 'Ukraine',
'United Kingdom', 'United States', 'Vietnam', 'China'
])
// Stall form schema
const stallFormSchema = toTypedSchema(z.object({
name: z.string().min(1, "Store name is required").max(100, "Store name must be less than 100 characters"),
description: z.string().max(500, "Description must be less than 500 characters").optional(),
currency: z.string().min(1, "Currency is required"),
wallet: z.string().optional(),
selectedZones: z.array(z.string()).min(1, "Select at least one shipping zone"),
newZone: z.object({
name: z.string().optional(),
cost: z.number().min(0, "Cost must be 0 or greater").optional(),
selectedCountries: z.array(z.string()).optional()
}).optional()
}))
// Form setup with vee-validate
const form = useForm({
validationSchema: stallFormSchema,
initialValues: {
name: '',
description: '',
currency: 'sat',
wallet: '',
selectedZones: [] as string[],
newZone: {
name: '',
cost: 0,
selectedCountries: [] as string[]
}
}
})
// Destructure form methods for easier access
const { setFieldValue, resetForm, values, meta } = form
// Helper function to get field values safely
const getFieldValue = (fieldName: string) => {
return fieldName.split('.').reduce((obj, key) => obj?.[key], values)
}
// Form validation computed
const isFormValid = computed(() => meta.value.valid)
// Form submit handler
const onSubmit = form.handleSubmit(async (values) => {
console.log('Form submitted with values:', values)
await createStall(values)
})
// Computed properties
const userHasMerchantProfile = computed(() => {
// Use the actual API response to determine if user has merchant profile
@ -699,10 +984,179 @@ const createMerchantProfile = async () => {
}
}
const createStall = () => {
// TODO: Implement stall creation via LNbits nostrmarket extension
console.log('Create stall functionality to be implemented')
// This should call the LNbits API: POST /nostrmarket/api/v1/stall
const initializeStallCreation = async () => {
const currentUser = auth.currentUser?.value
if (!currentUser?.wallets?.length) {
toast.error('No wallets available')
return
}
// Set default wallet
setFieldValue('wallet', currentUser.wallets[0].id)
// Load currencies and zones
await Promise.all([
loadAvailableCurrencies(),
loadAvailableZones()
])
showStallDialog.value = true
}
const loadAvailableCurrencies = async () => {
try {
const currencies = await nostrmarketAPI.getCurrencies()
availableCurrencies.value = currencies
} catch (error) {
console.error('Failed to load currencies:', error)
// Keep default 'sat'
}
}
const loadAvailableZones = async () => {
const currentUser = auth.currentUser?.value
if (!currentUser?.wallets?.length) return
try {
const zones = await nostrmarketAPI.getZones(currentUser.wallets[0].inkey)
availableZones.value = zones
} catch (error) {
console.error('Failed to load zones:', error)
}
}
const loadStallsList = async () => {
const currentUser = auth.currentUser?.value
if (!currentUser?.wallets?.length) return
try {
const stalls = await nostrmarketAPI.getStalls(currentUser.wallets[0].inkey)
// Update the merchant stalls list - this could be connected to a store if needed
console.log('Updated stalls list:', stalls.length)
} catch (error) {
console.error('Failed to load stalls:', error)
}
}
const addNewZone = async () => {
const currentUser = auth.currentUser?.value
if (!currentUser?.wallets?.length) {
toast.error('No wallets available')
return
}
const newZone = getFieldValue('newZone')
if (!newZone?.name || !newZone.selectedCountries?.length || (newZone.cost ?? -1) < 0) {
toast.error('Please fill in all zone details')
return
}
try {
const createdZone = await nostrmarketAPI.createZone(
currentUser.wallets[0].adminkey,
{
name: newZone.name,
currency: getFieldValue('currency'),
cost: newZone.cost,
countries: newZone.selectedCountries
}
)
// Add to available zones and select it
availableZones.value.push(createdZone)
const currentSelectedZones = getFieldValue('selectedZones') || []
setFieldValue('selectedZones', [...currentSelectedZones, createdZone.id])
// Reset the new zone form
setFieldValue('newZone', {
name: '',
cost: 0,
selectedCountries: []
})
toast.success(`Zone "${newZone.name}" created successfully`)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to create zone'
toast.error(`Failed to create zone: ${errorMessage}`)
}
}
const createStall = async (formData: any) => {
const currentUser = auth.currentUser?.value
if (!currentUser?.wallets?.length) {
toast.error('No wallets available')
return
}
const { name, description, currency, selectedZones } = formData
isCreatingStall.value = true
stallCreateError.value = null
try {
// Get the selected zones data
const selectedZoneData = availableZones.value.filter(zone =>
selectedZones.includes(zone.id)
)
const stallData: CreateStallRequest = {
name,
wallet: currentUser.wallets[0].id,
currency,
shipping_zones: selectedZoneData,
config: {
description: description || undefined
}
}
console.log('Creating stall:', {
name,
currency,
zonesCount: selectedZoneData.length
})
const newStall = await nostrmarketAPI.createStall(
currentUser.wallets[0].adminkey,
stallData
)
console.log('Stall created successfully:', {
stallId: newStall.id,
stallName: newStall.name
})
// Update stalls list
await loadStallsList()
// Reset form and close dialog
resetForm({
values: {
name: '',
description: '',
currency: 'sat',
wallet: currentUser.wallets[0].id,
selectedZones: [],
newZone: {
name: '',
cost: 0,
selectedCountries: []
}
}
})
showStallDialog.value = false
toast.success('Store created successfully! You can now add products.')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to create store'
console.error('Error creating stall:', error)
stallCreateError.value = errorMessage
toast.error(`Failed to create store: ${errorMessage}`)
} finally {
isCreatingStall.value = false
}
}
const navigateToMarket = () => router.push('/market')

View file

@ -47,16 +47,26 @@ export interface CreateMerchantRequest {
}
}
export interface Zone {
id: string
name: string
currency: string
cost: number
countries: string[]
}
export interface CreateZoneRequest {
name: string
currency: string
cost: number
countries: string[]
}
export interface CreateStallRequest {
wallet: string
name: string
currency: string
shipping_zones: Array<{
id: string
name: string
cost: number
countries: string[]
}>
shipping_zones: Zone[]
config: {
image_url?: string
description?: string
@ -97,10 +107,14 @@ export class NostrmarketAPI extends BaseService {
): Promise<T> {
const url = `${this.baseUrl}/nostrmarket${endpoint}`
const headers: HeadersInit = {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-API-KEY': walletKey,
...options.headers,
}
// Merge with any additional headers
if (options.headers) {
Object.assign(headers, options.headers)
}
this.debug('NostrmarketAPI request:', {
@ -232,4 +246,73 @@ export class NostrmarketAPI extends BaseService {
return stall
}
/**
* Get available shipping zones
*/
async getZones(walletInkey: string): Promise<Zone[]> {
try {
const zones = await this.request<Zone[]>(
'/api/v1/zone',
walletInkey,
{ method: 'GET' }
)
this.debug('Retrieved zones:', { count: zones?.length || 0 })
return zones || []
} catch (error) {
this.debug('Failed to get zones:', error)
return []
}
}
/**
* Create a new shipping zone
*/
async createZone(
walletAdminkey: string,
zoneData: CreateZoneRequest
): Promise<Zone> {
const zone = await this.request<Zone>(
'/api/v1/zone',
walletAdminkey,
{
method: 'POST',
body: JSON.stringify(zoneData),
}
)
this.debug('Created zone:', { zoneId: zone.id, zoneName: zone.name })
return zone
}
/**
* Get available currencies
*/
async getCurrencies(): Promise<string[]> {
const baseCurrencies = ['sat']
try {
const apiCurrencies = await this.request<string[]>(
'/api/v1/currencies',
'', // No authentication needed for currencies endpoint
{ method: 'GET' }
)
if (apiCurrencies && Array.isArray(apiCurrencies)) {
// Combine base currencies with API currencies, removing duplicates
const allCurrencies = [...baseCurrencies, ...apiCurrencies.filter(currency => !baseCurrencies.includes(currency))]
this.debug('Retrieved currencies:', { count: allCurrencies.length, currencies: allCurrencies })
return allCurrencies
}
this.debug('No API currencies returned, using base currencies only')
return baseCurrencies
} catch (error) {
this.debug('Failed to get currencies, falling back to base currencies:', error)
return baseCurrencies
}
}
}