mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
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
- 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
513 lines
16 KiB
TypeScript
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;
|