turash/bugulma/frontend/components/community/CreateCommunityListingForm.tsx
2025-12-15 10:06:41 +01:00

510 lines
16 KiB
TypeScript

import { useTranslation } from '@/hooks/useI18n';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import {
communityListingFormSchema,
CONDITION_OPTIONS,
LISTING_CATEGORIES,
PRICE_TYPE_OPTIONS,
RATE_TYPE_OPTIONS,
SERVICE_TYPE_OPTIONS,
type CommunityListingFormData,
} from '@/schemas/community';
import { createCommunityListing } from '@/services/discovery-api';
import { Alert } from '@/components/ui/Alert';
import Button from '@/components/ui/Button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import Checkbox from '@/components/ui/Checkbox';
import { FormField } from '@/components/ui/FormField';
import ImageGallery from '@/components/ui/ImageGallery';
import Input from '@/components/ui/Input';
import { Stack } from '@/components/ui/layout';
import MapPicker from '@/components/ui/MapPicker';
import Select from '@/components/ui/Select';
import Spinner from '@/components/ui/Spinner';
import Textarea from '@/components/ui/Textarea';
import { Heading, Text } from '@/components/ui/Typography';
import { Euro, MapPin, Tag, Upload } from 'lucide-react';
interface CreateCommunityListingFormProps {
onSuccess?: (listing: any) => void;
onCancel?: () => void;
}
const CreateCommunityListingForm: React.FC<CreateCommunityListingFormProps> = ({
onSuccess,
onCancel,
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 3;
const {
register,
handleSubmit,
watch,
setValue,
formState: { errors, isValid },
trigger,
} = useForm<CommunityListingFormData>({
resolver: zodResolver(communityListingFormSchema),
mode: 'onChange',
defaultValues: {
pickup_available: true,
delivery_available: false,
},
});
const listingType = watch('listing_type');
const priceType = watch('price_type');
const deliveryAvailable = watch('delivery_available');
const createMutation = useMutation({
mutationFn: createCommunityListing,
onSuccess: (data) => {
onSuccess?.(data);
navigate('/discovery', {
state: { message: t('community.createSuccess') },
});
},
});
const onSubmit = useCallback(
async (data: CommunityListingFormData) => {
try {
await createMutation.mutateAsync(data);
} catch (error) {
console.error('Failed to create listing:', error);
}
},
[createMutation]
);
const handleNext = async () => {
const isStepValid = await trigger();
if (isStepValid) {
setCurrentStep((prev) => Math.min(prev + 1, totalSteps));
}
};
const handlePrev = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1));
};
const handleLocationChange = useCallback(
(location: { lat: number; lng: number }) => {
setValue('latitude', location.lat);
setValue('longitude', location.lng);
},
[setValue]
);
const handleImagesChange = useCallback(
(images: string[]) => {
setValue('images', images);
},
[setValue]
);
const handleTagsChange = useCallback(
(tags: string[]) => {
setValue('tags', tags);
},
[setValue]
);
const renderStep1 = () => (
<Stack spacing="xl">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
{t('community.form.basicInfo')}
</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<FormField label={t('community.form.title')} error={errors.title?.message} required>
<Input
{...register('title')}
placeholder={t('community.form.titlePlaceholder')}
className={errors.title ? 'border-destructive' : ''}
/>
</FormField>
<FormField label={t('community.form.description')} error={errors.description?.message}>
<Textarea
{...register('description')}
placeholder={t('community.form.descriptionPlaceholder')}
rows={4}
className={errors.description ? 'border-destructive' : ''}
/>
</FormField>
<FormField
label={t('community.form.listingType')}
error={errors.listing_type?.message}
required
>
<Select {...register('listing_type')}>
<option value="">{t('community.form.selectType')}</option>
<option value="product">{t('community.types.product')}</option>
<option value="service">{t('community.types.service')}</option>
<option value="tool">{t('community.types.tool')}</option>
<option value="skill">{t('community.types.skill')}</option>
<option value="need">{t('community.types.need')}</option>
</Select>
</FormField>
{listingType && (
<FormField
label={t('community.form.category')}
error={errors.category?.message}
required
>
<Select {...register('category')}>
<option value="">{t('community.form.selectCategory')}</option>
{LISTING_CATEGORIES[listingType as keyof typeof LISTING_CATEGORIES]?.map(
(category) => (
<option key={category} value={category}>
{category}
</option>
)
)}
</Select>
</FormField>
)}
</Stack>
</CardContent>
</Card>
</Stack>
);
const renderStep2 = () => (
<Stack spacing="xl">
{(listingType === 'product' || listingType === 'tool') && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Euro className="h-5 w-5" />
{t('community.form.productDetails')}
</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<FormField
label={t('community.form.condition')}
error={errors.condition?.message}
required
>
<Select {...register('condition')}>
<option value="">{t('community.form.selectCondition')}</option>
{CONDITION_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</FormField>
<FormField label={t('community.form.priceType')} error={errors.price_type?.message}>
<Select {...register('price_type')}>
<option value="">{t('community.form.selectPriceType')}</option>
{PRICE_TYPE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</FormField>
{priceType && priceType !== 'free' && (
<FormField label={t('community.form.price')} error={errors.price?.message}>
<Input
{...register('price', { valueAsNumber: true })}
type="number"
step="0.01"
min="0"
placeholder="0.00"
className={errors.price ? 'border-destructive' : ''}
/>
</FormField>
)}
<FormField label={t('community.form.quantity')} error={errors.quantity?.message}>
<Input
{...register('quantity', { valueAsNumber: true })}
type="number"
min="1"
placeholder="1"
className={errors.quantity ? 'border-destructive' : ''}
/>
</FormField>
</Stack>
</CardContent>
</Card>
)}
{(listingType === 'service' || listingType === 'skill') && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
{t('community.form.serviceDetails')}
</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<FormField
label={t('community.form.serviceType')}
error={errors.service_type?.message}
required
>
<Select {...register('service_type')}>
<option value="">{t('community.form.selectServiceType')}</option>
{SERVICE_TYPE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</FormField>
<FormField label={t('community.form.rateType')} error={errors.rate_type?.message}>
<Select {...register('rate_type')}>
<option value="">{t('community.form.selectRateType')}</option>
{RATE_TYPE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</Select>
</FormField>
{watch('rate_type') &&
watch('rate_type') !== 'free' &&
watch('rate_type') !== 'trade' && (
<FormField label={t('community.form.rate')} error={errors.rate?.message}>
<Input
{...register('rate', { valueAsNumber: true })}
type="number"
step="0.01"
min="0"
placeholder="0.00"
className={errors.rate ? 'border-destructive' : ''}
/>
</FormField>
)}
</Stack>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MapPin className="h-5 w-5" />
{t('community.form.location')}
</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<div className="text-sm text-muted-foreground">{t('community.form.locationHelp')}</div>
<div className="h-64">
<MapPicker
onChange={handleLocationChange}
value={
watch('latitude') && watch('longitude')
? {
lat: watch('latitude')!,
lng: watch('longitude')!,
}
: undefined
}
/>
</div>
{errors.latitude && (
<Text variant="small" className="text-destructive">
{errors.latitude.message}
</Text>
)}
{errors.longitude && (
<Text variant="small" className="text-destructive">
{errors.longitude.message}
</Text>
)}
</Stack>
</CardContent>
</Card>
</Stack>
);
const renderStep3 = () => (
<Stack spacing="xl">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" />
{t('community.form.media')}
</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<FormField label={t('community.form.images')} error={errors.images?.message}>
<ImageGallery
images={watch('images') || []}
onChange={handleImagesChange}
maxImages={10}
editable={true}
title={t('community.form.uploadPhotos')}
/>
</FormField>
<FormField label={t('community.form.tags')} error={errors.tags?.message}>
<Input
placeholder={t('community.form.tagsPlaceholder')}
onChange={(e) => {
const tags = e.target.value
.split(',')
.map((tag) => tag.trim())
.filter(Boolean);
handleTagsChange(tags);
}}
defaultValue={watch('tags')?.join(', ')}
/>
</FormField>
</Stack>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('community.form.availability')}</CardTitle>
</CardHeader>
<CardContent>
<Stack spacing="md">
<Checkbox
{...register('pickup_available')}
label={t('community.form.pickupAvailable')}
defaultChecked={true}
/>
<Checkbox
{...register('delivery_available')}
label={t('community.form.deliveryAvailable')}
/>
{deliveryAvailable && (
<FormField
label={t('community.form.deliveryRadius')}
error={errors.delivery_radius_km?.message}
>
<Input
{...register('delivery_radius_km', { valueAsNumber: true })}
type="number"
min="0"
max="500"
placeholder="10"
className={errors.delivery_radius_km ? 'border-destructive' : ''}
/>
</FormField>
)}
</Stack>
</CardContent>
</Card>
</Stack>
);
const renderCurrentStep = () => {
switch (currentStep) {
case 1:
return renderStep1();
case 2:
return renderStep2();
case 3:
return renderStep3();
default:
return renderStep1();
}
};
return (
<div className="max-w-2xl mx-auto p-6">
<div className="mb-8">
<Heading level="h1" tKey="community.createListing" className="mb-2" />
<div className="flex items-center gap-2 mb-4">
{[1, 2, 3].map((step) => (
<div
key={step}
className={`h-2 flex-1 rounded ${step <= currentStep ? 'bg-primary' : 'bg-muted'}`}
/>
))}
</div>
<Text
variant="muted"
tKey="community.step"
replacements={{ step: currentStep, total: totalSteps }}
>
{t('community.step')} {currentStep} {t('community.of')} {totalSteps}
</Text>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
{renderCurrentStep()}
{createMutation.error && (
<Alert
variant="destructive"
description={
createMutation.error instanceof Error
? createMutation.error.message
: t('community.createError')
}
/>
)}
<div className="flex justify-between mt-8">
{currentStep > 1 ? (
<Button type="button" variant="outline" onClick={handlePrev}>
{t('common.previous')}
</Button>
) : (
<div />
)}
<div className="flex gap-2">
<Button type="button" variant="outline" onClick={onCancel || (() => navigate(-1))}>
{t('common.cancel')}
</Button>
{currentStep < totalSteps ? (
<Button type="button" onClick={handleNext}>
{t('common.next')}
</Button>
) : (
<Button type="submit" disabled={createMutation.isPending || !isValid}>
{createMutation.isPending ? (
<>
<Spinner className="h-4 w-4 mr-2" />
{t('community.creating')}
</>
) : (
t('community.createListing')
)}
</Button>
)}
</div>
</div>
</form>
</div>
);
};
export default CreateCommunityListingForm;