turash/bugulma/frontend/components/community/CreateCommunityListingForm.tsx
Damir Mukimov 673e8d4361
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
fix: resolve all frontend lint errors (85 issues fixed)
- Replace all 'any' types with proper TypeScript interfaces
- Fix React hooks setState in useEffect issues with lazy initialization
- Remove unused variables and imports across all files
- Fix React Compiler memoization dependency issues
- Add comprehensive i18n translation keys for admin interfaces
- Apply consistent prettier formatting throughout codebase
- Clean up unused bulk editing functionality
- Improve type safety and code quality across frontend

Files changed: 39
- ImpactMetrics.tsx: Fixed any types and interfaces
- AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars
- LocalizationUIPage.tsx: Fixed memoization, added translations
- LocalizationDataPage.tsx: Added type safety and translations
- And 35+ other files with various lint fixes
2025-12-25 14:14:58 +01:00

513 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, useWatch } 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: unknown) => 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 {
control,
register,
handleSubmit,
setValue,
formState: { errors, isValid },
trigger,
} = useForm<CommunityListingFormData>({
resolver: zodResolver(communityListingFormSchema),
mode: 'onChange',
defaultValues: {
pickup_available: true,
delivery_available: false,
},
});
const listingType = useWatch({ control, name: 'listing_type' });
const priceType = useWatch({ control, name: 'price_type' });
const deliveryAvailable = useWatch({ control, name: 'delivery_available' });
const rateType = useWatch({ control, name: 'rate_type' });
const latitude = useWatch({ control, name: 'latitude' });
const longitude = useWatch({ control, name: 'longitude' });
const images = useWatch({ control, name: 'images' });
const tags = useWatch({ control, name: 'tags' });
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>
{rateType && rateType !== 'free' && rateType !== '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={
latitude && longitude
? {
lat: latitude!,
lng: 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={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={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;