government ui complated

This commit is contained in:
Samandar Turgunboyev
2026-02-05 16:09:03 +05:00
parent 5d31fe8ff4
commit 754f11804a
76 changed files with 2459 additions and 672 deletions

View File

@@ -1,7 +1,7 @@
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 { 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';
@@ -16,12 +16,15 @@ type Errors = {
media?: string;
};
const MAX_MEDIA = 10;
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();
@@ -37,8 +40,7 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
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';
if (!formData.media || formData.media.length === 0) e.media = 'Rasm yoki video yuklang';
setErrors(e);
return Object.keys(e).length === 0;
@@ -49,18 +51,26 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
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: ImagePicker.MediaTypeOptions.All,
allowsMultipleSelection: true,
mediaTypes: mediaType,
allowsMultipleSelection: false,
quality: 0.8,
videoMaxDuration: 60, // 60 seconds max for video
});
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' }));
const asset = result.assets[0];
const mediaItem = {
uri: asset.uri,
type: selectedMediaTab,
};
updateForm('media', [...formData.media, ...assets]);
updateForm('media', [mediaItem]);
}
};
@@ -73,11 +83,9 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
[updateForm]
);
const removeMedia = (i: number) =>
updateForm(
'media',
formData.media.filter((_: any, idx: number) => idx !== i)
);
const removeMedia = () => {
updateForm('media', []);
};
const theme = {
background: isDark ? '#0f172a' : '#ffffff',
@@ -89,6 +97,8 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
error: '#ef4444',
primary: '#2563eb',
divider: isDark ? '#475569' : '#cbd5e1',
tabActive: isDark ? '#2563eb' : '#3b82f6',
tabInactive: isDark ? '#334155' : '#e2e8f0',
};
return (
@@ -165,35 +175,111 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
<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}>
{/* Media Type Tabs */}
<Text style={[styles.label, { color: theme.text }]}>{t('Media turi')}</Text>
<View style={styles.tabsContainer}>
<TouchableOpacity
style={[styles.upload, { borderColor: theme.primary }]}
onPress={pickMedia}
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}
>
<Camera size={28} color={theme.primary} />
<Text style={[styles.uploadText, { color: theme.primary }]}>{t('Yuklash')}</Text>
<ImageIcon
size={20}
color={selectedMediaTab === 'image' ? '#ffffff' : theme.textSecondary}
/>
<Text
style={[
styles.tabText,
{ color: selectedMediaTab === 'image' ? '#ffffff' : theme.textSecondary },
]}
>
{t('Rasm')}
</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" />
<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.remove, { backgroundColor: theme.error }]}
onPress={() => removeMedia(i)}
style={[styles.removeLarge, { backgroundColor: theme.error }]}
onPress={removeMedia}
activeOpacity={0.8}
>
<X size={12} color="#fff" />
<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>
@@ -206,7 +292,7 @@ export default StepOne;
const styles = StyleSheet.create({
stepContainer: { gap: 10 },
label: { fontWeight: '700' },
label: { fontWeight: '700', fontSize: 15 },
error: { fontSize: 13, marginLeft: 6 },
inputBox: {
flexDirection: 'row',
@@ -216,36 +302,8 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
height: 56,
},
textArea: { height: 120, alignItems: 'flex-start' },
textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 12 },
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',
@@ -262,4 +320,123 @@ const styles = StyleSheet.create({
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',
},
});