fitst commit
This commit is contained in:
32
screens/create-ads/lib/api.ts
Normal file
32
screens/create-ads/lib/api.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import httpClient from '@/api/httpClient';
|
||||
import { API_URLS } from '@/api/URLs';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { CreateAdsResponse, PriceCalculationRes } from './types';
|
||||
|
||||
export const price_calculation = {
|
||||
async calculation(params: {
|
||||
country: string;
|
||||
region: string;
|
||||
district: string | 'all';
|
||||
letters: string | any;
|
||||
types: string;
|
||||
}): Promise<AxiosResponse<PriceCalculationRes>> {
|
||||
const res = await httpClient.get(API_URLS.Price_Calculation, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async ad(body: FormData): Promise<AxiosResponse<CreateAdsResponse>> {
|
||||
const res = await httpClient.post(API_URLS.Add_Ads, body, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return res;
|
||||
},
|
||||
async payment(body: { return_url: string; adId: number; paymentType: 'payme' | 'referral' }) {
|
||||
const res = await httpClient.post(API_URLS.Payment_Ads(body.paymentType, body.adId), {
|
||||
return_url: body.return_url,
|
||||
});
|
||||
return res;
|
||||
},
|
||||
};
|
||||
21
screens/create-ads/lib/types.ts
Normal file
21
screens/create-ads/lib/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface PriceCalculationRes {
|
||||
country: string;
|
||||
region: string;
|
||||
district: string;
|
||||
letters: string[];
|
||||
one_person_price: number;
|
||||
user_count: number;
|
||||
total_price: number;
|
||||
user_ids: number[];
|
||||
}
|
||||
|
||||
export interface CreateAdsResponse {
|
||||
status: boolean;
|
||||
data: {
|
||||
description: string;
|
||||
id: number;
|
||||
phone_number: string;
|
||||
title: string;
|
||||
total_price: number;
|
||||
};
|
||||
}
|
||||
139
screens/create-ads/ui/CategorySelectorBottomSheet.tsx
Normal file
139
screens/create-ads/ui/CategorySelectorBottomSheet.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetScrollView,
|
||||
} from '@gorhom/bottom-sheet';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
export type Option = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type CategorySelectorProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectedValue: string;
|
||||
onSelect: (value: string) => void;
|
||||
data: Option[];
|
||||
};
|
||||
|
||||
export default function CategorySelectorBottomSheet({
|
||||
isOpen,
|
||||
onClose,
|
||||
selectedValue,
|
||||
onSelect,
|
||||
data = [],
|
||||
}: CategorySelectorProps) {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const theme = {
|
||||
background: isDark ? '#1e293b' : '#ffffff',
|
||||
text: isDark ? '#f8fafc' : '#0f172a',
|
||||
border: isDark ? '#334155' : '#e2e8f0',
|
||||
selectedBg: '#2563eb',
|
||||
selectedText: '#ffffff',
|
||||
indicator: isDark ? '#cbd5e1' : '#94a3b8',
|
||||
};
|
||||
|
||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
||||
const snapPoints = useMemo(() => ['60%', '85%'], []);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: BottomSheetBackdropProps) => (
|
||||
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.5} />
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
bottomSheetRef.current?.present();
|
||||
} else {
|
||||
bottomSheetRef.current?.dismiss();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetRef}
|
||||
index={0}
|
||||
snapPoints={snapPoints}
|
||||
backdropComponent={renderBackdrop}
|
||||
enablePanDownToClose
|
||||
onDismiss={onClose}
|
||||
handleIndicatorStyle={{ backgroundColor: theme.indicator, width: 50 }}
|
||||
backgroundStyle={{
|
||||
backgroundColor: theme.background,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
}}
|
||||
>
|
||||
<View style={[styles.header, { borderBottomColor: theme.border }]}>
|
||||
<Text style={[styles.title, { color: theme.text }]}>{t('Tanlang')}</Text>
|
||||
</View>
|
||||
|
||||
<BottomSheetScrollView style={styles.content} contentContainerStyle={styles.contentContainer}>
|
||||
{data.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.value}
|
||||
style={[
|
||||
styles.optionRow,
|
||||
{ borderBottomColor: theme.border },
|
||||
selectedValue === item.value && { backgroundColor: theme.selectedBg },
|
||||
]}
|
||||
onPress={() => {
|
||||
onSelect(item.value);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.optionText,
|
||||
{ color: selectedValue === item.value ? theme.selectedText : theme.text },
|
||||
selectedValue === item.value && styles.optionTextSelected,
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 20,
|
||||
borderBottomWidth: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingHorizontal: 8,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
optionRow: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 20,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
optionTextSelected: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
404
screens/create-ads/ui/CreateAdsScreens.tsx
Normal file
404
screens/create-ads/ui/CreateAdsScreens.tsx
Normal file
@@ -0,0 +1,404 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { router, useFocusEffect } from 'expo-router';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
Linking,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import PAYME from '@/assets/images/Payme_NEW.png';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { price_calculation } from '../lib/api';
|
||||
import { CreateAdsResponse } from '../lib/types';
|
||||
import StepFour from './StepFour';
|
||||
import StepOne from './StepOne';
|
||||
import StepThree from './StepThree';
|
||||
import StepTwo from './StepTwo';
|
||||
|
||||
type MediaFile = {
|
||||
uri: string;
|
||||
type: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
interface formDataType {
|
||||
title: string;
|
||||
description: string;
|
||||
phone: string;
|
||||
media: MediaFile[];
|
||||
category: any[];
|
||||
country: string;
|
||||
region: string;
|
||||
district: string;
|
||||
company: any[];
|
||||
}
|
||||
|
||||
const getMimeType = (uri: string) => {
|
||||
const ext = uri.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'webp':
|
||||
return 'image/webp';
|
||||
case 'heic':
|
||||
return 'image/heic';
|
||||
default:
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
};
|
||||
|
||||
export default function CreateAdsScreens() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [paymentType, setPaymentType] = useState<'PAYME' | 'REFFERAL'>('PAYME');
|
||||
const [ads, setAds] = useState<CreateAdsResponse | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
|
||||
const stepOneRef = useRef<{ validate: () => boolean } | null>(null);
|
||||
const stepTwoRef = useRef<{ validate: () => boolean } | null>(null);
|
||||
const stepThreeRef = useRef<{ validate: () => boolean } | null>(null);
|
||||
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const renderBackdrop = useCallback(
|
||||
(props: any) => (
|
||||
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.5} />
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const [formData, setFormData] = useState<formDataType>({
|
||||
title: '',
|
||||
description: '',
|
||||
phone: '',
|
||||
media: [],
|
||||
category: [],
|
||||
country: '',
|
||||
region: '',
|
||||
district: 'all',
|
||||
company: [],
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
return () => {
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
phone: '',
|
||||
media: [],
|
||||
category: [],
|
||||
country: '',
|
||||
region: '',
|
||||
district: 'all',
|
||||
company: [],
|
||||
});
|
||||
setCurrentStep(1);
|
||||
};
|
||||
}, [])
|
||||
);
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['price-calculation', formData],
|
||||
queryFn: () =>
|
||||
price_calculation.calculation({
|
||||
country: formData.country,
|
||||
district: formData.district,
|
||||
region: formData.region,
|
||||
types: formData.category.map((c: any) => c.id).join(','),
|
||||
letters: formData.company.map((c: any) => c.latter).join(','),
|
||||
}),
|
||||
enabled: formData.company.length > 0,
|
||||
});
|
||||
|
||||
const updateForm = (key: string, value: any) => setFormData((p) => ({ ...p, [key]: value }));
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: FormData) => price_calculation.ad(body),
|
||||
onSuccess: (res) => {
|
||||
setAds(res.data);
|
||||
setCurrentStep(4);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
Alert.alert('Xatolik', err.message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
const form = new FormData();
|
||||
|
||||
form.append('title', formData.title);
|
||||
form.append('description', formData.description);
|
||||
|
||||
formData.media.forEach((file, index) => {
|
||||
form.append(`files[${index}]`, {
|
||||
uri: file.uri,
|
||||
type: getMimeType(file.uri),
|
||||
name: file.uri.split('/').pop(),
|
||||
} as any);
|
||||
});
|
||||
|
||||
formData.category.forEach((e, index) => {
|
||||
form.append(`types[${index}]`, e.id);
|
||||
});
|
||||
|
||||
if (data) {
|
||||
form.append('total_price', data.data.total_price.toString());
|
||||
}
|
||||
|
||||
form.append('phone_number', `998${formData.phone}`);
|
||||
|
||||
const letters = formData.company.map((c: any) => c.latter).join(',');
|
||||
|
||||
form.append('company_selections[0]continent', 'Asia');
|
||||
form.append('company_selections[0]country', formData.country);
|
||||
form.append('company_selections[0]region', formData.region);
|
||||
form.append('company_selections[0]district', formData.district);
|
||||
form.append('company_selections[0]letters', letters);
|
||||
form.append('company_selections[0]total_companies', data?.data.user_count.toString() || '0');
|
||||
form.append('company_selections[0]ad_price', data?.data.one_person_price.toString() || '0');
|
||||
form.append('company_selections[0]total_price', data?.data.total_price.toString() || '0');
|
||||
|
||||
mutate(form);
|
||||
};
|
||||
|
||||
const handlePresentModalPress = useCallback(() => {
|
||||
bottomSheetModalRef.current?.present();
|
||||
}, []);
|
||||
|
||||
const { mutate: payment } = useMutation({
|
||||
mutationFn: (body: { return_url: string; adId: number; paymentType: 'payme' | 'referral' }) =>
|
||||
price_calculation.payment(body),
|
||||
onSuccess: async (res, variables) => {
|
||||
if (variables.paymentType === 'payme') {
|
||||
await Linking.openURL(res.data.url);
|
||||
router.push('/(dashboard)/announcements');
|
||||
} else {
|
||||
router.push('/(dashboard)/announcements');
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
Alert.alert('Xatolik yuz berdi', err.message);
|
||||
},
|
||||
});
|
||||
|
||||
const sendPayment = (type: 'payme' | 'referral') => {
|
||||
if (ads) {
|
||||
payment({
|
||||
adId: ads.data.id,
|
||||
paymentType: type,
|
||||
return_url: 'https://infotarget.uz/en/main/dashboard',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
|
||||
{currentStep === 1
|
||||
? t("E'lon ma'lumotlari")
|
||||
: currentStep === 2
|
||||
? t('Sohalar')
|
||||
: currentStep === 3
|
||||
? t('Manzil')
|
||||
: t("To'lov")}
|
||||
</Text>
|
||||
|
||||
{currentStep === 1 && (
|
||||
<StepOne ref={stepOneRef} formData={formData} updateForm={updateForm} />
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<StepThree
|
||||
ref={stepThreeRef}
|
||||
formData={formData}
|
||||
updateForm={updateForm}
|
||||
data={data?.data}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 4 && <StepFour data={ads} setPayment={setPaymentType} />}
|
||||
</ScrollView>
|
||||
|
||||
{/* FOOTER */}
|
||||
<View style={styles.footer}>
|
||||
{currentStep > 1 && currentStep !== 4 && (
|
||||
<TouchableOpacity
|
||||
style={[styles.back, isDark ? styles.darkBack : styles.lightBack]}
|
||||
onPress={() => setCurrentStep((s) => s - 1)}
|
||||
>
|
||||
<Text style={[styles.btnText, isDark ? styles.darkBtnText : styles.lightBtnText]}>
|
||||
{t('Orqaga')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.next}
|
||||
disabled={isPending}
|
||||
onPress={() => {
|
||||
let isValid = true;
|
||||
|
||||
if (currentStep === 1) isValid = stepOneRef.current?.validate() ?? false;
|
||||
if (currentStep === 2) isValid = stepTwoRef.current?.validate() ?? false;
|
||||
if (currentStep === 3) isValid = stepThreeRef.current?.validate() ?? false;
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
if (currentStep < 3) setCurrentStep((s) => s + 1);
|
||||
if (currentStep === 3) handleSubmit();
|
||||
if (currentStep === 4) handlePresentModalPress();
|
||||
}}
|
||||
>
|
||||
<Text style={styles.btnText}>
|
||||
{currentStep === 3 ? t('Yaratish') : currentStep === 4 ? t("To'lash") : t('Keyingisi')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* PAYMENT BOTTOM SHEET */}
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
index={0}
|
||||
snapPoints={['70%', '95%']}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{ backgroundColor: '#94a3b8', width: 50 }}
|
||||
backgroundStyle={{ backgroundColor: isDark ? '#0f172a' : '#ffffff' }}
|
||||
enablePanDownToClose
|
||||
>
|
||||
<BottomSheetScrollView
|
||||
style={styles.sheetContent}
|
||||
contentContainerStyle={styles.sheetContentContainer}
|
||||
>
|
||||
<View style={{ padding: 20 }}>
|
||||
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t("To'lov turini tanlang")}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.paymentItem,
|
||||
isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
|
||||
{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center' },
|
||||
]}
|
||||
onPress={() => sendPayment('payme')}
|
||||
>
|
||||
<Image source={PAYME} style={{ width: 80, height: 80 }} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.paymentItem,
|
||||
isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
|
||||
]}
|
||||
onPress={() => sendPayment('referral')}
|
||||
>
|
||||
<Text style={[styles.paymentText, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Referal orqali')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1 },
|
||||
darkBg: {
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
lightBg: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
container: { padding: 20, paddingBottom: 140 },
|
||||
title: { fontSize: 22, fontWeight: '800', marginBottom: 20 },
|
||||
darkText: {
|
||||
color: '#f1f5f9',
|
||||
},
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 80,
|
||||
left: 20,
|
||||
right: 20,
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
},
|
||||
back: {
|
||||
flex: 1,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
darkBack: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightBack: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
sheetContent: { flex: 1 },
|
||||
sheetContentContainer: { paddingBottom: 40 },
|
||||
next: {
|
||||
flex: 2,
|
||||
height: 56,
|
||||
backgroundColor: '#3b82f6',
|
||||
borderRadius: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
btnText: { color: '#ffffff', fontWeight: '700', fontSize: 16 },
|
||||
darkBtnText: {
|
||||
color: '#f1f5f9',
|
||||
},
|
||||
lightBtnText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
|
||||
sheetTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
marginBottom: 16,
|
||||
},
|
||||
paymentItem: {
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
darkPaymentItem: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightPaymentItem: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
paymentText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
73
screens/create-ads/ui/StepFour.tsx
Normal file
73
screens/create-ads/ui/StepFour.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import React, { Dispatch, forwardRef, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { CreateAdsResponse } from '../lib/types';
|
||||
|
||||
type StepFourProps = {
|
||||
setPayment: Dispatch<SetStateAction<'PAYME' | 'REFFERAL'>>;
|
||||
data: CreateAdsResponse | null;
|
||||
};
|
||||
|
||||
const StepFour = forwardRef(({ data }: StepFourProps) => {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = {
|
||||
background: isDark ? '#0f172a' : '#ffffff',
|
||||
cardBg: isDark ? '#1e293b' : '#f8fafc',
|
||||
cardBorder: isDark ? '#334155' : '#e2e8f0',
|
||||
text: isDark ? '#f8fafc' : '#0f172a',
|
||||
textSecondary: isDark ? '#cbd5e1' : '#64748b',
|
||||
totalPrice: isDark ? '#f87171' : '#ef4444',
|
||||
};
|
||||
|
||||
const totalPrice = data?.data.total_price || 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.container, { backgroundColor: theme.background }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
||||
{t("To'lov uchun ma'lumotlar")}
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[styles.card, { backgroundColor: theme.cardBg, borderColor: theme.cardBorder }]}
|
||||
>
|
||||
<Text style={[styles.label, { color: theme.text }]}>
|
||||
{t("E'lon nomi")}: <Text style={styles.value}>{data?.data.title}</Text>
|
||||
</Text>
|
||||
<Text style={[styles.label, { color: theme.text }]}>
|
||||
{t("E'lon tavsifi")}: <Text style={styles.value}>{data?.data.description}</Text>
|
||||
</Text>
|
||||
<Text style={[styles.label, styles.total, { color: theme.totalPrice }]}>
|
||||
{t('Umumiy narx')}:{' '}
|
||||
<Text style={styles.value}>
|
||||
{totalPrice} {t("so'm")}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default StepFour;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flexGrow: 1 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '700', marginVertical: 12 },
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 20,
|
||||
borderWidth: 1,
|
||||
gap: 8,
|
||||
},
|
||||
label: { fontSize: 15, fontWeight: '600' },
|
||||
value: { fontWeight: '800' },
|
||||
total: { marginTop: 8, fontSize: 16 },
|
||||
});
|
||||
265
screens/create-ads/ui/StepOne.tsx
Normal file
265
screens/create-ads/ui/StepOne.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { formatPhone, normalizeDigits } from '@/constants/formatPhone';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { Camera, Play, X } from 'lucide-react-native';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
type MediaType = { uri: string; type: 'image' | 'video' };
|
||||
type StepProps = { formData: any; updateForm: (key: string, value: any) => void };
|
||||
|
||||
type Errors = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
phone?: string;
|
||||
media?: string;
|
||||
};
|
||||
|
||||
const MAX_MEDIA = 10;
|
||||
|
||||
const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
|
||||
const [phone, setPhone] = useState(formData.phone || '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const validate = () => {
|
||||
const e: Errors = {};
|
||||
|
||||
if (!formData.title || formData.title.trim().length < 5)
|
||||
e.title = "Sarlavha kamida 5 ta belgidan iborat bo'lishi kerak";
|
||||
|
||||
if (!formData.description || formData.description.trim().length < 10)
|
||||
e.description = "Tavsif kamida 10 ta belgidan iborat bo'lishi kerak";
|
||||
|
||||
if (!formData.phone || formData.phone.length !== 9)
|
||||
e.phone = "Telefon raqam to'liq kiritilmadi";
|
||||
|
||||
if (!formData.media || formData.media.length === 0)
|
||||
e.media = 'Kamida bitta rasm yoki video yuklang';
|
||||
|
||||
setErrors(e);
|
||||
return Object.keys(e).length === 0;
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({ validate }));
|
||||
|
||||
const pickMedia = async () => {
|
||||
if (formData.media.length >= MAX_MEDIA) return;
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.All,
|
||||
allowsMultipleSelection: true,
|
||||
quality: 0.8,
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
const assets = result.assets
|
||||
.slice(0, MAX_MEDIA - formData.media.length)
|
||||
.map((a) => ({ uri: a.uri, type: a.type as 'image' | 'video' }));
|
||||
|
||||
updateForm('media', [...formData.media, ...assets]);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePhone = useCallback(
|
||||
(text: string) => {
|
||||
const n = normalizeDigits(text);
|
||||
setPhone(n);
|
||||
updateForm('phone', n);
|
||||
},
|
||||
[updateForm]
|
||||
);
|
||||
|
||||
const removeMedia = (i: number) =>
|
||||
updateForm(
|
||||
'media',
|
||||
formData.media.filter((_: any, idx: number) => idx !== i)
|
||||
);
|
||||
|
||||
const theme = {
|
||||
background: isDark ? '#0f172a' : '#ffffff',
|
||||
inputBg: isDark ? '#1e293b' : '#f1f5f9',
|
||||
inputBorder: isDark ? '#334155' : '#e2e8f0',
|
||||
text: isDark ? '#f8fafc' : '#0f172a',
|
||||
textSecondary: isDark ? '#cbd5e1' : '#475569',
|
||||
placeholder: isDark ? '#94a3b8' : '#94a3b8',
|
||||
error: '#ef4444',
|
||||
primary: '#2563eb',
|
||||
divider: isDark ? '#475569' : '#cbd5e1',
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.stepContainer}>
|
||||
{/* Sarlavha */}
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Sarlavha')}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputBox,
|
||||
{ backgroundColor: theme.inputBg, borderColor: theme.inputBorder },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
style={[styles.input, { color: theme.text }]}
|
||||
placeholder={t("E'lon sarlavhasi")}
|
||||
placeholderTextColor={theme.placeholder}
|
||||
value={formData.title}
|
||||
onChangeText={(t) => updateForm('title', t)}
|
||||
/>
|
||||
</View>
|
||||
{errors.title && (
|
||||
<Text style={[styles.error, { color: theme.error }]}>{t(errors.title)}</Text>
|
||||
)}
|
||||
|
||||
{/* Tavsif */}
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Tavsif')}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputBox,
|
||||
styles.textArea,
|
||||
{ backgroundColor: theme.inputBg, borderColor: theme.inputBorder },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
style={[styles.input, { color: theme.text }]}
|
||||
placeholder={t('Batafsil yozing...')}
|
||||
placeholderTextColor={theme.placeholder}
|
||||
multiline
|
||||
value={formData.description}
|
||||
onChangeText={(t) => updateForm('description', t)}
|
||||
/>
|
||||
</View>
|
||||
{errors.description && (
|
||||
<Text style={[styles.error, { color: theme.error }]}>{t(errors.description)}</Text>
|
||||
)}
|
||||
|
||||
{/* Telefon */}
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Telefon raqami')}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputBox,
|
||||
{ backgroundColor: theme.inputBg, borderColor: theme.inputBorder },
|
||||
]}
|
||||
>
|
||||
<View style={styles.prefixContainer}>
|
||||
<Text style={[styles.prefix, { color: theme.text }, focused && styles.prefixFocused]}>
|
||||
+998
|
||||
</Text>
|
||||
<View style={[styles.divider, { backgroundColor: theme.divider }]} />
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.input, { color: theme.text }]}
|
||||
value={formatPhone(phone)}
|
||||
onChangeText={handlePhone}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
keyboardType="phone-pad"
|
||||
placeholder="90 123 45 67"
|
||||
maxLength={12}
|
||||
placeholderTextColor={theme.placeholder}
|
||||
/>
|
||||
</View>
|
||||
{errors.phone && (
|
||||
<Text style={[styles.error, { color: theme.error }]}>{t(errors.phone)}</Text>
|
||||
)}
|
||||
|
||||
{/* Media */}
|
||||
<Text style={[styles.label, { color: theme.text }]}>
|
||||
{t('Media')} ({formData.media.length}/{MAX_MEDIA})
|
||||
</Text>
|
||||
<View style={styles.media}>
|
||||
<TouchableOpacity
|
||||
style={[styles.upload, { borderColor: theme.primary }]}
|
||||
onPress={pickMedia}
|
||||
>
|
||||
<Camera size={28} color={theme.primary} />
|
||||
<Text style={[styles.uploadText, { color: theme.primary }]}>{t('Yuklash')}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{formData.media.map((m: MediaType, i: number) => (
|
||||
<View key={i} style={styles.preview}>
|
||||
<Image source={{ uri: m.uri }} style={styles.image} />
|
||||
{m.type === 'video' && (
|
||||
<View style={styles.play}>
|
||||
<Play size={14} color="#fff" fill="#fff" />
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.remove, { backgroundColor: theme.error }]}
|
||||
onPress={() => removeMedia(i)}
|
||||
>
|
||||
<X size={12} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{errors.media && (
|
||||
<Text style={[styles.error, { color: theme.error }]}>{t(errors.media)}</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
export default StepOne;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
stepContainer: { gap: 10 },
|
||||
label: { fontWeight: '700' },
|
||||
error: { fontSize: 13, marginLeft: 6 },
|
||||
inputBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 16,
|
||||
height: 56,
|
||||
},
|
||||
textArea: { height: 120, alignItems: 'flex-start' },
|
||||
input: { flex: 1, fontSize: 16 },
|
||||
media: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
|
||||
upload: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 16,
|
||||
borderWidth: 2,
|
||||
borderStyle: 'dashed',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
uploadText: { fontSize: 11, marginTop: 4 },
|
||||
preview: { width: 100, height: 100 },
|
||||
image: { width: '100%', height: '100%', borderRadius: 16 },
|
||||
play: {
|
||||
position: 'absolute',
|
||||
top: '40%',
|
||||
left: '40%',
|
||||
backgroundColor: 'rgba(0,0,0,.5)',
|
||||
padding: 6,
|
||||
borderRadius: 20,
|
||||
},
|
||||
remove: {
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
right: -6,
|
||||
padding: 4,
|
||||
borderRadius: 10,
|
||||
},
|
||||
prefixContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
prefix: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
prefixFocused: {},
|
||||
divider: {
|
||||
width: 1.5,
|
||||
height: 24,
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
289
screens/create-ads/ui/StepThree.tsx
Normal file
289
screens/create-ads/ui/StepThree.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dimensions,
|
||||
FlatList,
|
||||
ListRenderItemInfo,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { PriceCalculationRes } from '../lib/types';
|
||||
import CategorySelectorBottomSheet from './CategorySelectorBottomSheet';
|
||||
|
||||
type StepProps = {
|
||||
formData: any;
|
||||
data: PriceCalculationRes | undefined;
|
||||
updateForm: (key: string, value: any) => void;
|
||||
};
|
||||
|
||||
const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||
const GAP = 8;
|
||||
const NUM_COLUMNS = 6;
|
||||
const ITEM_SIZE = (SCREEN_WIDTH - GAP * (NUM_COLUMNS + 1)) / NUM_COLUMNS;
|
||||
|
||||
const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) => {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = {
|
||||
background: isDark ? '#0f172a' : '#ffffff',
|
||||
cardBg: isDark ? '#1e293b' : '#f8fafc',
|
||||
cardBorder: isDark ? '#334155' : '#e2e8f0',
|
||||
text: isDark ? '#f8fafc' : '#0f172a',
|
||||
textSecondary: isDark ? '#cbd5e1' : '#64748b',
|
||||
primary: '#2563eb',
|
||||
error: '#ef4444',
|
||||
priceText: isDark ? '#dc2626' : '#ef4444',
|
||||
companyBg: isDark ? '#1e293b' : '#f1f5f9',
|
||||
companyBorder: isDark ? '#334155' : '#cbd5e1',
|
||||
};
|
||||
|
||||
const { data: statesData } = useQuery({
|
||||
queryKey: ['country-detail'],
|
||||
queryFn: async () => products_api.getStates(),
|
||||
select: (res) => res.data.data || [],
|
||||
});
|
||||
|
||||
const [showCountry, setShowCountry] = useState(false);
|
||||
const [showRegion, setShowRegion] = useState(false);
|
||||
const [showDistrict, setShowDistrict] = useState(false);
|
||||
const [regions, setRegions] = useState<any[]>([]);
|
||||
const [districts, setDistricts] = useState<any[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const corporations = Array.from({ length: 26 }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
latter: String.fromCharCode(65 + i),
|
||||
}));
|
||||
|
||||
const onCompanyPress = (item: { id: number; latter: string }) => {
|
||||
const selected = formData.company || [];
|
||||
const exists = selected.some((c: any) => c.id === item.id);
|
||||
|
||||
if (exists) {
|
||||
updateForm(
|
||||
'company',
|
||||
selected.filter((c: any) => c.id !== item.id)
|
||||
);
|
||||
} else {
|
||||
updateForm('company', [...selected, item]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const country = statesData?.find((c) => c.code === formData.country);
|
||||
setRegions(country?.region || []);
|
||||
if (!country?.region.some((r) => r.code === formData.region)) {
|
||||
updateForm('region', '');
|
||||
updateForm('district', '');
|
||||
setDistricts([]);
|
||||
}
|
||||
}, [formData.country, statesData]);
|
||||
|
||||
useEffect(() => {
|
||||
const country = statesData?.find((c) => c.code === formData.country);
|
||||
const region = country?.region.find((r) => r.code === formData.region);
|
||||
setDistricts(region?.districts || []);
|
||||
if (!region?.districts.some((d) => d.code === formData.district)) {
|
||||
updateForm('district', '');
|
||||
}
|
||||
}, [formData.region, formData.country, statesData]);
|
||||
|
||||
const getLabel = (arr: { name: string; code: string }[], val: string) =>
|
||||
arr.find((item) => item.code === val)?.name || t('— Tanlang —');
|
||||
|
||||
const renderCompanyItem = ({ item }: ListRenderItemInfo<{ id: number; latter: string }>) => {
|
||||
const isSelected = formData.company?.some((c: any) => c.id === item.id);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.companyItem,
|
||||
{
|
||||
backgroundColor: isSelected ? theme.primary : theme.companyBg,
|
||||
borderColor: isSelected ? theme.primary : theme.companyBorder,
|
||||
},
|
||||
]}
|
||||
onPress={() => onCompanyPress(item)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.companyText,
|
||||
{ color: isSelected ? '#fff' : theme.text },
|
||||
isSelected && styles.companyTextActive,
|
||||
]}
|
||||
>
|
||||
{item.latter}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
if (!formData.country) {
|
||||
setError('Iltimos, davlat tanlang');
|
||||
return false;
|
||||
}
|
||||
if (!formData.region) {
|
||||
setError('Iltimos, viloyat tanlang');
|
||||
return false;
|
||||
}
|
||||
if (!formData.district) {
|
||||
setError('Iltimos, tuman/shahar tanlang');
|
||||
return false;
|
||||
}
|
||||
setError(null);
|
||||
return true;
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
validate,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.container, { backgroundColor: theme.background }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{error && <Text style={[styles.error, { color: theme.error }]}>{t(error)}</Text>}
|
||||
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Davlat')}</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.pickerButton,
|
||||
{ backgroundColor: theme.cardBg, borderColor: theme.cardBorder },
|
||||
]}
|
||||
onPress={() => setShowCountry(true)}
|
||||
>
|
||||
<Text style={[styles.pickerText, { color: theme.text }]}>
|
||||
{statesData &&
|
||||
getLabel(
|
||||
statesData.map((c) => ({ name: c.name, code: c.code })),
|
||||
formData.country
|
||||
)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Viloyat')}</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.pickerButton,
|
||||
{ backgroundColor: theme.cardBg, borderColor: theme.cardBorder },
|
||||
]}
|
||||
onPress={() => setShowRegion(true)}
|
||||
>
|
||||
<Text style={[styles.pickerText, { color: theme.text }]}>
|
||||
{getLabel(
|
||||
regions.map((r) => ({ name: r.name, code: r.code })),
|
||||
formData.region
|
||||
)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Tuman / Shahar')}</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.pickerButton,
|
||||
{ backgroundColor: theme.cardBg, borderColor: theme.cardBorder },
|
||||
]}
|
||||
onPress={() => setShowDistrict(true)}
|
||||
>
|
||||
<Text style={[styles.pickerText, { color: theme.text }]}>
|
||||
{getLabel(
|
||||
districts.map((d) => ({ name: d.name, code: d.code })),
|
||||
formData.district
|
||||
)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
||||
{t('Reklama joylashtirish kompaniyasi')}
|
||||
</Text>
|
||||
<FlatList
|
||||
data={corporations}
|
||||
renderItem={renderCompanyItem}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
numColumns={6}
|
||||
columnWrapperStyle={{ gap: 2, marginBottom: GAP, justifyContent: 'flex-start' }}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[styles.priceCard, { backgroundColor: theme.cardBg, borderColor: theme.cardBorder }]}
|
||||
>
|
||||
<Text style={[styles.priceLine, { color: theme.text }]}>
|
||||
{t('Jami kampaniyalar soni')}: {data ? data.user_count : '0'}
|
||||
</Text>
|
||||
<Text style={[styles.priceLine, { color: theme.text }]}>
|
||||
{t('Reklama narxi')}: {data ? data.one_person_price : '0'}
|
||||
</Text>
|
||||
<Text style={[styles.totalPrice, { color: theme.priceText }]}>
|
||||
{t('Umumiy narx')}: {data ? data.total_price : '0'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<CategorySelectorBottomSheet
|
||||
isOpen={showCountry}
|
||||
onClose={() => setShowCountry(false)}
|
||||
selectedValue={formData.country}
|
||||
data={statesData ? statesData.map((c) => ({ label: c.name, value: c.code })) : []}
|
||||
onSelect={(v) => updateForm('country', v)}
|
||||
/>
|
||||
<CategorySelectorBottomSheet
|
||||
isOpen={showRegion}
|
||||
onClose={() => setShowRegion(false)}
|
||||
selectedValue={formData.region}
|
||||
data={regions.map((r) => ({ label: r.name, value: r.code }))}
|
||||
onSelect={(v) => updateForm('region', v)}
|
||||
/>
|
||||
<CategorySelectorBottomSheet
|
||||
isOpen={showDistrict}
|
||||
onClose={() => setShowDistrict(false)}
|
||||
selectedValue={formData.district}
|
||||
data={districts.map((d) => ({ label: d.name, value: d.code }))}
|
||||
onSelect={(v) => updateForm('district', v)}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
});
|
||||
|
||||
export default StepThree;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flexGrow: 1 },
|
||||
label: { fontSize: 14, fontWeight: '700', marginBottom: 6, marginTop: 10 },
|
||||
pickerButton: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
pickerText: { fontSize: 16 },
|
||||
sectionTitle: { fontSize: 16, fontWeight: '700', marginVertical: 12 },
|
||||
companyItem: {
|
||||
width: 55,
|
||||
height: 55,
|
||||
borderRadius: ITEM_SIZE / 2,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
companyText: { fontSize: 14 },
|
||||
companyTextActive: { fontWeight: '600' },
|
||||
priceCard: {
|
||||
marginTop: 24,
|
||||
padding: 20,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
gap: 8,
|
||||
},
|
||||
priceLine: { fontSize: 15 },
|
||||
totalPrice: { fontSize: 18, fontWeight: '700', marginTop: 6 },
|
||||
error: { fontWeight: '600', marginBottom: 10 },
|
||||
});
|
||||
143
screens/create-ads/ui/StepTwo.tsx
Normal file
143
screens/create-ads/ui/StepTwo.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import CategorySelection from '@/components/ui/IndustrySelection';
|
||||
import { XIcon } from 'lucide-react-native';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FlatList, StyleSheet, Text, ToastAndroid, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
type StepProps = {
|
||||
formData: any;
|
||||
updateForm: (key: string, value: any) => void;
|
||||
};
|
||||
|
||||
const StepTwo = forwardRef(({ formData, updateForm }: StepProps, ref) => {
|
||||
const [selectedCategories, setSelectedCategories] = useState<any[]>(formData.category || []);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = {
|
||||
text: isDark ? '#f8fafc' : '#0f172a',
|
||||
tabBg: isDark ? '#1e293b' : '#f1f5f9',
|
||||
tabText: isDark ? '#ffffff' : '#1e293b',
|
||||
deleteBg: isDark ? '#394e73' : '#cbd5e1',
|
||||
deleteIcon: isDark ? '#f8fafc' : '#475569',
|
||||
error: '#ef4444',
|
||||
shadow: isDark ? '#000' : '#64748b',
|
||||
};
|
||||
|
||||
// FormData-ni yangilash
|
||||
useEffect(() => {
|
||||
updateForm('category', selectedCategories);
|
||||
if (selectedCategories.length > 0) setError(null);
|
||||
}, [selectedCategories]);
|
||||
|
||||
// Validatsiya
|
||||
const validate = () => {
|
||||
if (selectedCategories.length === 0) {
|
||||
setError('Iltimos, kompaniyalarni tanlang');
|
||||
ToastAndroid.show(t('Iltimos, kompaniyalarni tanlang'), ToastAndroid.TOP);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
validate,
|
||||
}));
|
||||
|
||||
// O'chirish funksiyasi
|
||||
const removeCategory = (id: string | number) => {
|
||||
setSelectedCategories((prev) => prev.filter((c) => c.id !== id));
|
||||
};
|
||||
|
||||
// SearchTabs uchun render funksiyasi
|
||||
const renderTab = ({ item }: { item: any }) => (
|
||||
<View style={[styles.tabWrapper, { backgroundColor: theme.tabBg, shadowColor: theme.shadow }]}>
|
||||
<Text
|
||||
style={[styles.tabText, { color: theme.tabText }]}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => removeCategory(item.id)}
|
||||
style={[styles.deleteTab, { backgroundColor: theme.deleteBg }]}
|
||||
>
|
||||
<XIcon size={15} color={theme.deleteIcon} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container]}>
|
||||
{/* Tanlangan kategoriya tablari */}
|
||||
{selectedCategories.length > 0 && (
|
||||
<>
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Tanlangan sohalar')}:</Text>
|
||||
<FlatList
|
||||
data={selectedCategories}
|
||||
renderItem={renderTab}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.tabsContainer}
|
||||
ItemSeparatorComponent={() => <View style={{ width: 8 }} />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Kategoriya tanlash */}
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t("Sohalar ro'yxati")}:</Text>
|
||||
<CategorySelection
|
||||
selectedCategories={selectedCategories}
|
||||
setSelectedCategories={setSelectedCategories}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
export default StepTwo;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flexGrow: 1 },
|
||||
label: {
|
||||
fontSize: 16,
|
||||
marginBottom: 5,
|
||||
fontWeight: '600',
|
||||
},
|
||||
error: {
|
||||
marginTop: 10,
|
||||
fontWeight: '600',
|
||||
fontSize: 14,
|
||||
},
|
||||
tabsContainer: {
|
||||
marginBottom: 16,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
tabWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 20,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 3,
|
||||
elevation: 3,
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginRight: 6,
|
||||
maxWidth: 200,
|
||||
flexShrink: 1,
|
||||
},
|
||||
deleteTab: {
|
||||
padding: 4,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user