442 lines
13 KiB
TypeScript
442 lines
13 KiB
TypeScript
import { useTheme } from '@/components/ThemeContext';
|
|
import { formatPhone, normalizeDigits } from '@/constants/formatPhone';
|
|
import * as ImagePicker from 'expo-image-picker';
|
|
import { Image as ImageIcon, Play, Video, 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 StepProps = { formData: any; updateForm: (key: string, value: any) => void };
|
|
|
|
type Errors = {
|
|
title?: string;
|
|
description?: string;
|
|
phone?: string;
|
|
media?: string;
|
|
};
|
|
|
|
type MediaTabType = 'image' | 'video';
|
|
|
|
const MAX_MEDIA = 1;
|
|
|
|
const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
|
|
const [phone, setPhone] = useState(formData.phone || '');
|
|
const [focused, setFocused] = useState(false);
|
|
const [errors, setErrors] = useState<Errors>({});
|
|
const [selectedMediaTab, setSelectedMediaTab] = useState<MediaTabType>('image');
|
|
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 = '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 mediaType =
|
|
selectedMediaTab === 'image'
|
|
? ImagePicker.MediaTypeOptions.Images
|
|
: ImagePicker.MediaTypeOptions.Videos;
|
|
|
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
mediaTypes: mediaType,
|
|
allowsMultipleSelection: false,
|
|
quality: 0.8,
|
|
videoMaxDuration: 60, // 60 seconds max for video
|
|
});
|
|
|
|
if (!result.canceled) {
|
|
const asset = result.assets[0];
|
|
const mediaItem = {
|
|
uri: asset.uri,
|
|
type: selectedMediaTab,
|
|
};
|
|
|
|
updateForm('media', [mediaItem]);
|
|
}
|
|
};
|
|
|
|
const handlePhone = useCallback(
|
|
(text: string) => {
|
|
const n = normalizeDigits(text);
|
|
setPhone(n);
|
|
updateForm('phone', n);
|
|
},
|
|
[updateForm]
|
|
);
|
|
|
|
const removeMedia = () => {
|
|
updateForm('media', []);
|
|
};
|
|
|
|
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',
|
|
tabActive: isDark ? '#2563eb' : '#3b82f6',
|
|
tabInactive: isDark ? '#334155' : '#e2e8f0',
|
|
};
|
|
|
|
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 Type Tabs */}
|
|
<Text style={[styles.label, { color: theme.text }]}>{t('Media turi')}</Text>
|
|
<View style={styles.tabsContainer}>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.tab,
|
|
selectedMediaTab === 'image' && styles.tabActive,
|
|
{
|
|
backgroundColor: selectedMediaTab === 'image' ? theme.tabActive : theme.tabInactive,
|
|
borderColor: selectedMediaTab === 'image' ? theme.tabActive : theme.inputBorder,
|
|
},
|
|
]}
|
|
onPress={() => setSelectedMediaTab('image')}
|
|
activeOpacity={0.7}
|
|
>
|
|
<ImageIcon
|
|
size={20}
|
|
color={selectedMediaTab === 'image' ? '#ffffff' : theme.textSecondary}
|
|
/>
|
|
<Text
|
|
style={[
|
|
styles.tabText,
|
|
{ color: selectedMediaTab === 'image' ? '#ffffff' : theme.textSecondary },
|
|
]}
|
|
>
|
|
{t('Rasm')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.tab,
|
|
selectedMediaTab === 'video' && styles.tabActive,
|
|
{
|
|
backgroundColor: selectedMediaTab === 'video' ? theme.tabActive : theme.tabInactive,
|
|
borderColor: selectedMediaTab === 'video' ? theme.tabActive : theme.inputBorder,
|
|
},
|
|
]}
|
|
onPress={() => setSelectedMediaTab('video')}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Video size={20} color={selectedMediaTab === 'video' ? '#ffffff' : theme.textSecondary} />
|
|
<Text
|
|
style={[
|
|
styles.tabText,
|
|
{ color: selectedMediaTab === 'video' ? '#ffffff' : theme.textSecondary },
|
|
]}
|
|
>
|
|
{t('Video')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Media Upload/Preview */}
|
|
<View style={styles.mediaContainer}>
|
|
{formData.media.length === 0 ? (
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.uploadLarge,
|
|
{ borderColor: theme.primary, backgroundColor: theme.inputBg },
|
|
]}
|
|
onPress={pickMedia}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={[styles.uploadIconWrapper, { backgroundColor: theme.primary }]}>
|
|
{selectedMediaTab === 'image' ? (
|
|
<ImageIcon size={32} color="#ffffff" />
|
|
) : (
|
|
<Video size={32} color="#ffffff" />
|
|
)}
|
|
</View>
|
|
<Text style={[styles.uploadLargeText, { color: theme.text }]}>
|
|
{selectedMediaTab === 'image' ? t('Rasm yuklash') : t('Video yuklash')}
|
|
</Text>
|
|
<Text style={[styles.uploadLargeSubtext, { color: theme.textSecondary }]}>
|
|
{selectedMediaTab === 'image' ? t('Rasm tanlang') : t('Video tanlang')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
) : (
|
|
<View style={styles.previewLarge}>
|
|
<Image source={{ uri: formData.media[0].uri }} style={styles.imageLarge} />
|
|
{formData.media[0].type === 'video' && (
|
|
<View style={styles.playLarge}>
|
|
<Play size={24} color="#fff" fill="#fff" />
|
|
</View>
|
|
)}
|
|
<TouchableOpacity
|
|
style={[styles.removeLarge, { backgroundColor: theme.error }]}
|
|
onPress={removeMedia}
|
|
activeOpacity={0.8}
|
|
>
|
|
<X size={16} color="#fff" />
|
|
</TouchableOpacity>
|
|
<View style={[styles.mediaTypeBadge, { backgroundColor: 'rgba(0,0,0,0.7)' }]}>
|
|
{formData.media[0].type === 'image' ? (
|
|
<ImageIcon size={14} color="#fff" />
|
|
) : (
|
|
<Video size={14} color="#fff" />
|
|
)}
|
|
<Text style={styles.mediaTypeBadgeText}>
|
|
{formData.media[0].type === 'image' ? 'Rasm' : 'Video'}
|
|
</Text>
|
|
</View>
|
|
</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', fontSize: 15 },
|
|
error: { fontSize: 13, marginLeft: 6 },
|
|
inputBox: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
borderRadius: 16,
|
|
borderWidth: 1,
|
|
paddingHorizontal: 16,
|
|
height: 56,
|
|
},
|
|
textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 0 },
|
|
input: { flex: 1, fontSize: 16 },
|
|
prefixContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginRight: 12,
|
|
},
|
|
prefix: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
letterSpacing: 0.3,
|
|
},
|
|
prefixFocused: {},
|
|
divider: {
|
|
width: 1.5,
|
|
height: 24,
|
|
marginLeft: 12,
|
|
},
|
|
|
|
// Media Tabs
|
|
tabsContainer: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
},
|
|
tab: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingVertical: 14,
|
|
paddingHorizontal: 16,
|
|
borderRadius: 16,
|
|
borderWidth: 2,
|
|
gap: 8,
|
|
},
|
|
tabActive: {
|
|
shadowColor: '#2563eb',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 4,
|
|
elevation: 3,
|
|
},
|
|
tabText: {
|
|
fontSize: 15,
|
|
fontWeight: '700',
|
|
letterSpacing: 0.3,
|
|
},
|
|
|
|
// Media Container
|
|
mediaContainer: {
|
|
marginTop: 4,
|
|
},
|
|
uploadLarge: {
|
|
height: 240,
|
|
borderRadius: 20,
|
|
borderWidth: 2,
|
|
borderStyle: 'dashed',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
},
|
|
uploadIconWrapper: {
|
|
width: 72,
|
|
height: 72,
|
|
borderRadius: 36,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.2,
|
|
shadowRadius: 4,
|
|
elevation: 3,
|
|
},
|
|
uploadLargeText: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
marginTop: 4,
|
|
},
|
|
uploadLargeSubtext: {
|
|
fontSize: 14,
|
|
fontWeight: '500',
|
|
textAlign: 'center',
|
|
paddingHorizontal: 32,
|
|
},
|
|
previewLarge: {
|
|
height: 240,
|
|
borderRadius: 20,
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
},
|
|
imageLarge: {
|
|
width: '100%',
|
|
height: '100%',
|
|
borderRadius: 20,
|
|
},
|
|
playLarge: {
|
|
position: 'absolute',
|
|
top: '50%',
|
|
left: '50%',
|
|
transform: [{ translateX: -28 }, { translateY: -28 }],
|
|
backgroundColor: 'rgba(0,0,0,.6)',
|
|
padding: 14,
|
|
borderRadius: 32,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 4,
|
|
elevation: 3,
|
|
},
|
|
removeLarge: {
|
|
position: 'absolute',
|
|
top: 12,
|
|
right: 12,
|
|
padding: 8,
|
|
borderRadius: 12,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 4,
|
|
elevation: 3,
|
|
},
|
|
mediaTypeBadge: {
|
|
position: 'absolute',
|
|
bottom: 12,
|
|
left: 12,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
paddingVertical: 6,
|
|
paddingHorizontal: 12,
|
|
borderRadius: 10,
|
|
},
|
|
mediaTypeBadgeText: {
|
|
color: '#ffffff',
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
},
|
|
});
|