fitst commit
This commit is contained in:
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user