government ui complated
@@ -32,6 +32,7 @@ export const API_URLS = {
|
||||
My_Bonuses: 'api/cashback/',
|
||||
My_Refferals: 'api/referral/',
|
||||
Goverment_Service: '/api/goverment-service/',
|
||||
Goverment_Category: '/api/goverment-category/',
|
||||
Notification_List: '/api/notifications/',
|
||||
Notification_Ready: (id: number) => `/api/notifications/${id}/read/`,
|
||||
};
|
||||
|
||||
7
app.json
@@ -11,7 +11,9 @@
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"infoPlist": {
|
||||
"UIBackgroundModes": ["remote-notification"]
|
||||
"UIBackgroundModes": [
|
||||
"remote-notification"
|
||||
]
|
||||
},
|
||||
"bundleIdentifier": "com.felix.infotarget"
|
||||
},
|
||||
@@ -63,7 +65,8 @@
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-video"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
|
||||
@@ -2,17 +2,33 @@ import Logo from '@/assets/images/logo.png';
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { RefreshProvider } from '@/components/ui/RefreshContext';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
import { Image } from 'expo-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Tabs } from 'expo-router';
|
||||
import { router, Tabs } from 'expo-router';
|
||||
import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform, StyleSheet, Text, View } from 'react-native';
|
||||
import { Animated, Easing, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
export default function TabsLayout() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
Animated.loop(
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
duration: 4000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
}, []);
|
||||
|
||||
const rotate = rotateAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
});
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
@@ -77,7 +93,7 @@ export default function TabsLayout() {
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
{
|
||||
marginLeft: 10,
|
||||
marginLeft: 0,
|
||||
color,
|
||||
fontWeight: focused ? '700' : '600',
|
||||
},
|
||||
@@ -92,7 +108,7 @@ export default function TabsLayout() {
|
||||
styles.iconContainer,
|
||||
focused && styles.iconContainerActive,
|
||||
{
|
||||
marginLeft: 10,
|
||||
marginLeft: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -112,7 +128,8 @@ export default function TabsLayout() {
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
{
|
||||
marginRight: 10,
|
||||
// marginRight: 10,
|
||||
marginLeft: 0,
|
||||
color,
|
||||
fontWeight: focused ? '700' : '600',
|
||||
},
|
||||
@@ -127,7 +144,8 @@ export default function TabsLayout() {
|
||||
styles.iconContainer,
|
||||
focused && styles.iconContainerActive,
|
||||
{
|
||||
marginRight: 10,
|
||||
// marginRight: 10,
|
||||
marginLeft: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -138,28 +156,41 @@ export default function TabsLayout() {
|
||||
/>
|
||||
|
||||
<Tabs.Screen
|
||||
name="e-services"
|
||||
key="e-service-tab"
|
||||
name="e-service"
|
||||
options={{
|
||||
title: 'Davlat xizmatlari',
|
||||
title: '',
|
||||
tabBarLabel: () => null,
|
||||
tabBarItemStyle: {
|
||||
flex: 1.2,
|
||||
top: -10,
|
||||
top: 8,
|
||||
},
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<View style={styles.centerTabContainer}>
|
||||
<LinearGradient
|
||||
colors={focused ? ['#3b82f6', '#2563eb'] : ['#1e40af', '#1e3a8a']} // dark mod uchun quyuq ko‘k
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.centerTabGradient}
|
||||
tabBarIcon: () => (
|
||||
<TouchableOpacity
|
||||
style={styles.centerTabContainer}
|
||||
onPress={() => router.push('/(dashboard)/e-service/e-services')}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.centerTabInner,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? 'rgba(255, 255, 255, 0.1)'
|
||||
: 'rgba(15, 23, 42, 0.1)',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.centerTabInner}>
|
||||
<Image source={Logo} style={{ width: 60, height: 60 }} />
|
||||
</View>
|
||||
</LinearGradient>
|
||||
<Text
|
||||
numberOfLines={1} // 1 qatorda
|
||||
<Animated.Image
|
||||
source={Logo}
|
||||
style={{
|
||||
width: 55,
|
||||
height: 55,
|
||||
transform: [{ rotate }],
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{/* <Text
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.85}
|
||||
style={[
|
||||
@@ -167,15 +198,15 @@ export default function TabsLayout() {
|
||||
{
|
||||
color: focused ? '#3b82f6' : isDark ? '#94a3b8' : '#64748b',
|
||||
fontWeight: focused ? '700' : '600',
|
||||
fontSize: 13, // kattalashtirildi
|
||||
width: 100, // kenglik oshirildi
|
||||
fontSize: 13,
|
||||
width: 100,
|
||||
textAlign: 'center',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{t('Davlat xizmatlari')}
|
||||
</Text>
|
||||
</View>
|
||||
</Text> */}
|
||||
</TouchableOpacity>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -190,7 +221,8 @@ export default function TabsLayout() {
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
{
|
||||
marginLeft: 20,
|
||||
// marginLeft: 10,
|
||||
marginRight: 0,
|
||||
color,
|
||||
fontWeight: focused ? '700' : '600',
|
||||
},
|
||||
@@ -205,7 +237,8 @@ export default function TabsLayout() {
|
||||
styles.iconContainer,
|
||||
focused && styles.iconContainerActive,
|
||||
{
|
||||
marginLeft: 20,
|
||||
// marginLeft: 10,
|
||||
marginRight: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -225,7 +258,7 @@ export default function TabsLayout() {
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
{
|
||||
marginRight: 20,
|
||||
marginRight: 0,
|
||||
color,
|
||||
fontWeight: focused ? '700' : '600',
|
||||
},
|
||||
@@ -239,7 +272,7 @@ export default function TabsLayout() {
|
||||
style={[
|
||||
styles.iconContainer,
|
||||
focused && styles.iconContainerActive,
|
||||
{ marginRight: 20 },
|
||||
{ marginRight: 0 },
|
||||
]}
|
||||
>
|
||||
<User color={color} size={30} strokeWidth={2} />
|
||||
@@ -270,29 +303,10 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
centerTabGradient: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
...Platform.select({
|
||||
ios: {
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 12,
|
||||
},
|
||||
android: {
|
||||
elevation: 12,
|
||||
},
|
||||
}),
|
||||
},
|
||||
centerTabInner: {
|
||||
width: 68,
|
||||
height: 68,
|
||||
width: 65,
|
||||
height: 65,
|
||||
borderRadius: 34,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
10
app/(dashboard)/e-service/_layout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
export default function EServiceLayout() {
|
||||
return (
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="e-services.tsx" />
|
||||
<Stack.Screen name="e-services-category.tsx" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
17
app/(dashboard)/e-service/e-services-category.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { CustomHeader } from '@/components/ui/Header';
|
||||
import EServicesScreen from '@/screens/e-services/ui/EServicesScreen';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function EServicesCategory() {
|
||||
const { isDark } = useTheme();
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 50 }}
|
||||
edges={['top']}
|
||||
>
|
||||
<CustomHeader />
|
||||
<EServicesScreen />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { CustomHeader } from '@/components/ui/Header';
|
||||
import EServicesScreen from '@/screens/e-services/ui/EServices';
|
||||
import EServicesCategoryScreen from '@/screens/e-services/ui/EServicesCategoryScreen';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function EServices() {
|
||||
const { isDark } = useTheme();
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 30 }}
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 80 }}
|
||||
edges={['top']}
|
||||
>
|
||||
<CustomHeader />
|
||||
<EServicesScreen />
|
||||
<EServicesCategoryScreen />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export default function Index() {
|
||||
return (
|
||||
<FilterProvider>
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 85 }}
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 55 }}
|
||||
>
|
||||
<CustomHeader />
|
||||
<HomeScreen />
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function ProfileScreen() {
|
||||
const { isDark } = useTheme();
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 70 }}
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 40 }}
|
||||
edges={['top']}
|
||||
>
|
||||
<CustomHeader logoutbtn={true} notif={false} />
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { AuthProvider } from '@/components/AuthProvider';
|
||||
import QueryProvider from '@/components/QueryProvider';
|
||||
import { ThemeProvider, useTheme } from '@/components/ThemeContext';
|
||||
import { ThemeProvider } from '@/components/ThemeContext';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import i18n from '@/i18n/i18n';
|
||||
import { ProfileDataProvider } from '@/screens/profile/lib/ProfileDataContext';
|
||||
import { Stack } from 'expo-router';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { StatusBar } from 'react-native';
|
||||
import 'react-native-reanimated';
|
||||
|
||||
function AppContent() {
|
||||
const { isDark } = useTheme();
|
||||
useNotifications();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
<StatusBar
|
||||
barStyle={isDark ? 'light-content' : 'dark-content'}
|
||||
backgroundColor={isDark ? '#0f172a' : '#ffffff'}
|
||||
translucent={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
assets/images/Express_diagnistika.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |
BIN
assets/manual/image_en/step1.jpg
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
assets/manual/image_en/step2.jpg
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
assets/manual/image_en/step3.jpg
Normal file
|
After Width: | Height: | Size: 261 KiB |
BIN
assets/manual/image_en/step4.jpg
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
assets/manual/image_en/step5.jpg
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
assets/manual/image_en/step6.jpg
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
assets/manual/image_en/step7.jpg
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
assets/manual/image_en/step8.jpg
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
assets/manual/image_ru/step1.jpg
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
assets/manual/image_ru/step2.jpg
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
assets/manual/image_ru/step3.jpg
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
assets/manual/image_ru/step4.jpg
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
assets/manual/image_ru/step5.jpg
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
assets/manual/image_ru/step6.jpg
Normal file
|
After Width: | Height: | Size: 260 KiB |
BIN
assets/manual/image_ru/step7.jpg
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
assets/manual/image_ru/step8.jpg
Normal file
|
After Width: | Height: | Size: 273 KiB |
BIN
assets/manual/image_uz/step1.jpg
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
assets/manual/image_uz/step2.jpg
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
assets/manual/image_uz/step3.jpg
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
assets/manual/image_uz/step4.jpg
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
assets/manual/image_uz/step5.jpg
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
assets/manual/image_uz/step6.jpg
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
assets/manual/image_uz/step7.jpg
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
assets/manual/image_uz/step8.jpg
Normal file
|
After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 181 KiB |
@@ -27,10 +27,8 @@ export const CustomHeader = ({
|
||||
return (
|
||||
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||
<View style={styles.logoWrapper}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Image source={Logo} style={styles.logo} />
|
||||
</View>
|
||||
<Image source={LogoText} style={{ width: 160, height: 40, objectFit: 'cover' }} />
|
||||
<Image source={Logo} style={styles.logo} />
|
||||
<Image source={LogoText} style={{ width: 160, height: 30, objectFit: 'contain' }} />
|
||||
</View>
|
||||
{logoutbtn && (
|
||||
<TouchableOpacity onPress={logout}>
|
||||
@@ -46,7 +44,7 @@ export const CustomHeader = ({
|
||||
|
||||
{unreadCount > 0 && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{unreadCount > 99 ? '99+' : unreadCount}</Text>
|
||||
<Text style={styles.badgeText}>{unreadCount > 9 ? '9+' : unreadCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -87,7 +85,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
gap: 5,
|
||||
gap: 8,
|
||||
},
|
||||
|
||||
logoContainer: {
|
||||
@@ -101,8 +99,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
logo: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
|
||||
@@ -130,11 +128,10 @@ const styles = StyleSheet.create({
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
backgroundColor: '#ef4444', // qizil badge
|
||||
backgroundColor: '#ef4444',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 4,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
|
||||
badgeText: {
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
"Jinsi": "Gender",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"Foydalanish qo'lanmasi": "User Manual",
|
||||
"Foydalanish qo'llanmasi": "User Manual",
|
||||
"Ro'yxatdan o'tish (Registratsiya) – 1 daqiqa ichida": "Registration – in 1 minute",
|
||||
"Platformaga kirish uchun avval ro'yxatdan o'ting.": "First, register to access the platform.",
|
||||
"Profilni to'ldirish va tasdiqlash": "Complete and verify your profile",
|
||||
@@ -183,5 +183,21 @@
|
||||
"Hozir": "Just now",
|
||||
"daqiqa oldin": "minute ago",
|
||||
"soat oldin": "hours ago",
|
||||
"kun oldin": "days ago"
|
||||
"kun oldin": "days ago",
|
||||
"Rasm yoki video yuklang": "Upload image or video",
|
||||
"Media turi": "Media type",
|
||||
"Rasm": "Picture",
|
||||
"Video": "Video",
|
||||
"Rasm yuklash": "Upload image",
|
||||
"Rasm tanlang": "Select image",
|
||||
"Video yuklash": "Upload video",
|
||||
"Video tanlang": "Select a video",
|
||||
"Quyidagi qisqa video yoki rasmlarni ko'rib chiqing": "Check out the short videos or images below",
|
||||
"Foydalanish video qo'llanma": "Use video tutorial",
|
||||
"Foydalanish rasm qo‘llanma": "Use Image Guide",
|
||||
"Video tilini tanlang": "Select a video language",
|
||||
"Davlat xizmatlari kategoriyalari": "Categories of government services",
|
||||
"Kerakli xizmat turini tanlang": "Select the desired service type",
|
||||
"Bu kategoriya bo'yicha xizmat topilmadi": "No service in this category",
|
||||
"Tez orada xizmat qo'shiladi": "Service will be added soon"
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"Qayta urinish": "Повторить попытку",
|
||||
"Bosh sahifa": "Главная",
|
||||
"E'lon joylashtirish": "Разместить объявление",
|
||||
"E'lonlar": "Объявления",
|
||||
"E'lonlar": "Реклама",
|
||||
"Profil": "Профиль",
|
||||
"Davlat": "Страна",
|
||||
"Barchasi": "Все",
|
||||
@@ -146,7 +146,7 @@
|
||||
"Rejimni tanlang": "Выберите режим",
|
||||
"Tungi rejim": "Ночной режим",
|
||||
"Yorug' rejim": "Дневной режим",
|
||||
"Qo'shish": "Добавить",
|
||||
"Qo'shish": "Рассылка",
|
||||
"Refferallarim": "Мои рефералы",
|
||||
"Davlat xizmatlari": "Государственные услуги",
|
||||
"foydalanuvchi": "пользователь",
|
||||
@@ -163,7 +163,7 @@
|
||||
"Jinsi": "Пол",
|
||||
"male": "Мужской",
|
||||
"female": "Женщина",
|
||||
"Foydalanish qo'lanmasi": "Инструкция по использованию",
|
||||
"Foydalanish qo'llanmasi": "Инструкция по использованию",
|
||||
"Ro'yxatdan o'tish (Registratsiya) – 1 daqiqa ichida": "Регистрация – за 1 минуту",
|
||||
"Platformaga kirish uchun avval ro'yxatdan o'ting.": "Сначала войдите в систему, зарегистрировавшись.",
|
||||
"Profilni to'ldirish va tasdiqlash": "Заполнение и подтверждение профиля",
|
||||
@@ -182,5 +182,21 @@
|
||||
"Hozir": "Сейчас",
|
||||
"daqiqa oldin": "минут назад",
|
||||
"soat oldin": "час назад",
|
||||
"kun oldin": "дней назад"
|
||||
"kun oldin": "дней назад",
|
||||
"Rasm yoki video yuklang": "Добавьте фото или видео",
|
||||
"Media turi": "Тип медиа",
|
||||
"Rasm": "Изображение",
|
||||
"Video": "Видео",
|
||||
"Rasm yuklash": "Добавить изображение",
|
||||
"Rasm tanlang": "Выберите изображение",
|
||||
"Video yuklash": "Добавить видео",
|
||||
"Video tanlang": "Выберите видео",
|
||||
"Quyidagi qisqa video yoki rasmlarni ko'rib chiqing": "Посмотрите следующие короткие видео или изображения",
|
||||
"Foydalanish video qo'llanma": "Видеоинструкция по использованию",
|
||||
"Foydalanish rasm qo‘llanma": "Инструкция по использованию изображений",
|
||||
"Video tilini tanlang": "Выберите язык видео",
|
||||
"Davlat xizmatlari kategoriyalari": "Категории государственных услуг",
|
||||
"Kerakli xizmat turini tanlang": "Выберите нужный тип услуги",
|
||||
"Bu kategoriya bo'yicha xizmat topilmadi": "Сервис в этой категории не найден.",
|
||||
"Tez orada xizmat qo'shiladi": "Сервис скоро будет добавлен"
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
"Jinsi": "Jinsi",
|
||||
"male": "Erkak",
|
||||
"female": "Ayol",
|
||||
"Foydalanish qo'lanmasi": "Foydalanish qo'lanmasi",
|
||||
"Foydalanish qo'llanmasi": "Foydalanish qo'llanmasi",
|
||||
"Ro'yxatdan o'tish (Registratsiya) – 1 daqiqa ichida": "Ro'yxatdan o'tish (Registratsiya) – 1 daqiqa ichida",
|
||||
"Platformaga kirish uchun avval ro'yxatdan o'ting.": "Platformaga kirish uchun avval ro'yxatdan o'ting.",
|
||||
"Profilni to'ldirish va tasdiqlash": "Profilni to'ldirish va tasdiqlash",
|
||||
@@ -182,5 +182,21 @@
|
||||
"Hozir": "Hozir",
|
||||
"daqiqa oldin": "daqiqa oldin",
|
||||
"soat oldin": "soat oldin",
|
||||
"kun oldin": "kun oldin"
|
||||
"kun oldin": "kun oldin",
|
||||
"Rasm yoki video yuklang": "Rasm yoki video yuklang",
|
||||
"Media turi": "Media turi",
|
||||
"Rasm": "Rasm",
|
||||
"Video": "Video",
|
||||
"Rasm yuklash": "Rasm yuklash",
|
||||
"Rasm tanlang": "Rasm tanlang",
|
||||
"Video yuklash": "Video yuklash",
|
||||
"Video tanlang": "Video tanlang",
|
||||
"Quyidagi qisqa video yoki rasmlarni ko'rib chiqing": "Quyidagi qisqa video yoki rasmlarni ko'rib chiqing",
|
||||
"Foydalanish video qo'llanma": "Foydalanish video qo'llanma",
|
||||
"Foydalanish rasm qo‘llanma": "Foydalanish rasm qo‘llanma",
|
||||
"Video tilini tanlang": "Video tilini tanlang",
|
||||
"Davlat xizmatlari kategoriyalari": "Davlat xizmatlari kategoriyalari",
|
||||
"Kerakli xizmat turini tanlang": "Kerakli xizmat turini tanlang",
|
||||
"Bu kategoriya bo'yicha xizmat topilmadi": "Bu kategoriya bo'yicha xizmat topilmadi",
|
||||
"Tez orada xizmat qo'shiladi": "Tez orada xizmat qo'shiladi"
|
||||
}
|
||||
|
||||
73
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@legendapp/motion": "^2.4.0",
|
||||
"@nkzw/create-context-hook": "^1.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-picker/picker": "^2.11.4",
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/material-top-tabs": "^7.4.13",
|
||||
@@ -42,6 +43,7 @@
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"expo-symbols": "~1.0.8",
|
||||
"expo-system-ui": "~6.0.9",
|
||||
"expo-video": "~3.0.15",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"i18next": "^25.7.4",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
@@ -55,7 +57,9 @@
|
||||
"react-i18next": "^16.5.3",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
"react-native-keyboard-aware-scroll-view": "^0.9.5",
|
||||
"react-native-modal": "^14.0.0-rc.1",
|
||||
"react-native-pager-view": "^8.0.0",
|
||||
"react-native-reanimated": "~4.1.0",
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
@@ -5187,6 +5191,19 @@
|
||||
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-picker/picker": {
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.4.tgz",
|
||||
"integrity": "sha512-Kf8h1AMnBo54b1fdiVylP2P/iFcZqzpMYcglC28EEFB1DEnOjsNr6Ucqc+3R9e91vHxEDnhZFbYDmAe79P2gjA==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"example"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets-registry": {
|
||||
"version": "0.81.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
|
||||
@@ -10280,6 +10297,17 @@
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-video": {
|
||||
"version": "3.0.15",
|
||||
"resolved": "https://registry.npmjs.org/expo-video/-/expo-video-3.0.15.tgz",
|
||||
"integrity": "sha512-KmxHVCtOBb1fnxXL6DpgLbEe7Qlv/vHNGTLfz0u/eY8fBC9s5cncD2BhPunEffrGvNMftBzYMYDaO86x+IYpnA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-web-browser": {
|
||||
"version": "15.0.10",
|
||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.10.tgz",
|
||||
@@ -14903,6 +14931,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-animatable": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.4.0.tgz",
|
||||
"integrity": "sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-css-interop": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.2.1.tgz",
|
||||
@@ -15189,6 +15226,29 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-image-pan-zoom": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz",
|
||||
"integrity": "sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-image-zoom-viewer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-zoom-viewer/-/react-native-image-zoom-viewer-3.0.1.tgz",
|
||||
"integrity": "sha512-la6s5DNSuq4GCRLsi5CZ29FPjgTpdCuGIRdO5T9rUrAtxrlpBPhhSnHrbmPVxsdtOUvxHacTh2Gfa9+RraMZQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-native-image-pan-zoom": "^2.1.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-iphone-x-helper": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
|
||||
@@ -15221,6 +15281,19 @@
|
||||
"react-native": ">=0.48.4"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-modal": {
|
||||
"version": "14.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-v5pvGyx1FlmBzdHyPqBsYQyS2mIJhVmuXyNo5EarIzxicKhuoul6XasXMviGcXboEUT0dTYWs88/VendojPiVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-native-animatable": "1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": ">=0.70.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-pager-view": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-8.0.0.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@legendapp/motion": "^2.4.0",
|
||||
"@nkzw/create-context-hook": "^1.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-picker/picker": "^2.11.4",
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/material-top-tabs": "^7.4.13",
|
||||
@@ -46,6 +47,7 @@
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"expo-symbols": "~1.0.8",
|
||||
"expo-system-ui": "~6.0.9",
|
||||
"expo-video": "~3.0.15",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"i18next": "^25.7.4",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
@@ -59,7 +61,9 @@
|
||||
"react-i18next": "^16.5.3",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
"react-native-keyboard-aware-scroll-view": "^0.9.5",
|
||||
"react-native-modal": "^14.0.0-rc.1",
|
||||
"react-native-pager-view": "^8.0.0",
|
||||
"react-native-reanimated": "~4.1.0",
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import httpClient from '@/api/httpClient';
|
||||
import { API_URLS } from '@/api/URLs';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { GovermentServiceData } from './types';
|
||||
import { GovermentCategoryData, GovermentServiceData } from './types';
|
||||
|
||||
export const eservices_api = {
|
||||
async list(params: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
category?: number;
|
||||
}): Promise<AxiosResponse<GovermentServiceData>> {
|
||||
const res = await httpClient.get(API_URLS.Goverment_Service, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async category(): Promise<AxiosResponse<GovermentCategoryData>> {
|
||||
const res = await httpClient.get(API_URLS.Goverment_Category);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
export const search_api = {
|
||||
search: async (query: string) => {
|
||||
const res = await fetch(`https://server.myorg.uz/search?query=${encodeURIComponent(query)}`, {
|
||||
headers: { Access: 'Bearer 12opNgslS4S2DDllcsnPKD789D2ek2HJhH23C' },
|
||||
});
|
||||
if (!res.ok) throw new Error('Search error');
|
||||
return res.json();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,3 +19,24 @@ export interface GovermentServiceDataRes {
|
||||
url: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export interface GovermentCategoryData {
|
||||
status: boolean;
|
||||
data: {
|
||||
links: {
|
||||
previous: null | string;
|
||||
next: null | string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
results: GovermentCategoryDataRes[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GovermentCategoryDataRes {
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
// EServicesScreen.tsx
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { Image } from 'expo-image';
|
||||
import { ChevronLeft, XIcon } from 'lucide-react-native';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
FlatList,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { eservices_api } from '../lib/api';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export interface GovermentServiceDataRes {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export default function EServicesScreen() {
|
||||
const { isDark } = useTheme();
|
||||
const [webUrl, setWebUrl] = useState<string | null>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const webviewRef = useRef<WebView>(null); // WebView ref for goBack
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['goverment_service'],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await eservices_api.list({
|
||||
page: pageParam,
|
||||
page_size: PAGE_SIZE,
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||||
initialPageParam: 1,
|
||||
});
|
||||
|
||||
const services: GovermentServiceDataRes[] = data?.pages.flatMap((p) => p.results) ?? [];
|
||||
|
||||
const openWebView = (url: string) => {
|
||||
setWebUrl(url);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: GovermentServiceDataRes }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDark ? '#1e293b' : '#f8fafc' },
|
||||
isDark ? styles.darkShadow : styles.lightShadow,
|
||||
]}
|
||||
onPress={() => openWebView(item.url)}
|
||||
>
|
||||
<Image source={{ uri: item.logo }} style={styles.logo} resizeMode="contain" />
|
||||
<Text style={[styles.name, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>{item.name}</Text>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
[isDark]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<Text style={{ color: 'red' }}>Xatolik yuz berdi</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||
<FlatList
|
||||
data={services}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={renderItem}
|
||||
numColumns={2}
|
||||
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }}
|
||||
contentContainerStyle={{ padding: 16 }}
|
||||
onEndReached={() => hasNextPage && fetchNextPage()}
|
||||
onEndReachedThreshold={0.4}
|
||||
ListFooterComponent={
|
||||
isFetchingNextPage ? <ActivityIndicator color="#3b82f6" style={{ margin: 20 }} /> : null
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
|
||||
{/* WebView Modal */}
|
||||
{/* WebView Modal */}
|
||||
<Modal visible={modalVisible} animationType="slide">
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: isDark ? '#0f172a' : '#f8fafc', // modal background
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
padding: 12,
|
||||
backgroundColor: isDark ? '#1e293b' : '#f8fafc', // header background
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Back tugmasi */}
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (webviewRef.current) webviewRef.current.goBack();
|
||||
}}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<ChevronLeft color={isDark ? '#f1f5f9' : '#0f172a'} size={24} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Close tugmasi */}
|
||||
<TouchableOpacity
|
||||
onPress={() => setModalVisible(false)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<XIcon color={isDark ? '#f1f5f9' : '#0f172a'} size={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* WebView */}
|
||||
{webUrl && (
|
||||
<WebView
|
||||
ref={webviewRef}
|
||||
source={{ uri: webUrl }}
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }} // webview background
|
||||
startInLoadingState
|
||||
renderLoading={() => (
|
||||
<ActivityIndicator color="#3b82f6" size="large" style={{ flex: 1 }} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const CARD_WIDTH = (SCREEN_WIDTH - 48) / 2;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
card: {
|
||||
width: CARD_WIDTH,
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
logo: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
marginBottom: 8,
|
||||
},
|
||||
name: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
darkShadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 6,
|
||||
elevation: 3,
|
||||
},
|
||||
lightShadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
258
screens/e-services/ui/EServicesCategoryScreen.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import Express_diagnistika from '@/assets/images/Express_diagnistika.png';
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Image } from 'expo-image';
|
||||
import { router } from 'expo-router';
|
||||
import { ChevronLeft, XIcon } from 'lucide-react-native';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActivityIndicator, FlatList, Modal, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { RefreshControl } from 'react-native-gesture-handler';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { eservices_api } from '../lib/api';
|
||||
|
||||
const dark = {
|
||||
bg: '#0f172a',
|
||||
card: '#1f2937',
|
||||
border: '#1e293b',
|
||||
muted: '#334155',
|
||||
text: '#f8fafc',
|
||||
subText: '#cbd5f5',
|
||||
};
|
||||
|
||||
export default function EServicesCategoryScreen() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const webviewRef = useRef<WebView>(null);
|
||||
const [webUrl, setWebUrl] = React.useState<string | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['goverment_category'],
|
||||
queryFn: () => eservices_api.category(),
|
||||
});
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await queryClient.refetchQueries({ queryKey: ['goverment_category'] });
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const staticCategory = {
|
||||
id: 0,
|
||||
name: 'Express Diagnostika',
|
||||
image: Express_diagnistika,
|
||||
url: 'https://myorg.uz/ru',
|
||||
};
|
||||
|
||||
const categories = [staticCategory, ...(data?.data?.data?.results ?? [])];
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
if (item.id === 0 && item.url) {
|
||||
setWebUrl(item.url);
|
||||
setModalVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({
|
||||
pathname: '/(dashboard)/e-service/e-services-category',
|
||||
params: {
|
||||
categoryId: item.id,
|
||||
categoryName: item.name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: isDark ? dark.bg : '#f8fafc',
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
<FlatList
|
||||
data={categories}
|
||||
keyExtractor={(item) => `${item.id}-${item.name}`}
|
||||
contentContainerStyle={{ gap: 12 }}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={isDark ? '#f8fafc' : '#020617'}
|
||||
colors={['#3b82f6']}
|
||||
/>
|
||||
}
|
||||
ListHeaderComponent={() => (
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
fontWeight: '800',
|
||||
color: isDark ? dark.text : '#020617',
|
||||
}}
|
||||
>
|
||||
{t('Davlat xizmatlari kategoriyalari')}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
color: isDark ? dark.subText : '#64748b',
|
||||
}}
|
||||
>
|
||||
{t('Kerakli xizmat turini tanlang')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => handlePress(item)}
|
||||
style={{
|
||||
backgroundColor: isDark ? dark.card : '#ffffff',
|
||||
padding: 14,
|
||||
borderRadius: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: isDark ? 1 : 0,
|
||||
borderColor: isDark ? dark.border : 'transparent',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 120,
|
||||
height: 70,
|
||||
borderRadius: 12,
|
||||
backgroundColor: isDark ? dark.muted : '#e2e8f0',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 14,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{item.image ? (
|
||||
<Image
|
||||
source={item.image}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
contentFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<Text style={{ fontWeight: '700', color: dark.text }}>{item.name[0]}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: item.id === 0 ? '700' : '600',
|
||||
color: isDark ? dark.text : '#020617',
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
|
||||
{item.id === 0 && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
marginTop: 4,
|
||||
color: dark.subText,
|
||||
}}
|
||||
>
|
||||
Tezkor va qulay xizmat
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
<Modal visible={modalVisible} animationType="slide">
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: isDark ? '#0f172a' : '#f8fafc',
|
||||
}}
|
||||
>
|
||||
{/* WebView Header */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: isDark ? '#1e293b' : '#f8fafc',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? '#334155' : '#e2e8f0',
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => webviewRef.current?.goBack()}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<ChevronLeft size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: isDark ? '#f1f5f9' : '#0f172a',
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{webUrl}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => setModalVisible(false)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<XIcon size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* WebView */}
|
||||
{webUrl && (
|
||||
<WebView
|
||||
ref={webviewRef}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
domStorageEnabled
|
||||
onShouldStartLoadWithRequest={(request) => {
|
||||
// iOS va Android uchun barcha URLlarni WebView ichida ochish
|
||||
return true;
|
||||
}}
|
||||
source={{ uri: webUrl }}
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}
|
||||
startInLoadingState
|
||||
renderLoading={() => (
|
||||
<ActivityIndicator color="#3b82f6" size="large" style={{ flex: 1 }} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
313
screens/e-services/ui/EServicesScreen.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
// EServicesScreen.tsx
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Image } from 'expo-image';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import { ChevronLeft, XIcon } from 'lucide-react-native';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
FlatList,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { WebView } from 'react-native-webview';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshControl } from 'react-native-gesture-handler';
|
||||
import { eservices_api } from '../lib/api';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export interface GovermentServiceDataRes {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export default function EServicesScreen() {
|
||||
const { isDark } = useTheme();
|
||||
const [webUrl, setWebUrl] = useState<string | null>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const webviewRef = useRef<WebView>(null);
|
||||
const params = useLocalSearchParams();
|
||||
const { t } = useTranslation();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['goverment_service', params.categoryId],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await eservices_api.list({
|
||||
page: pageParam,
|
||||
page_size: PAGE_SIZE,
|
||||
category: Number(params.categoryId),
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||||
initialPageParam: 1,
|
||||
});
|
||||
|
||||
const services: GovermentServiceDataRes[] = data?.pages.flatMap((p) => p.results) ?? [];
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await queryClient.refetchQueries({ queryKey: ['goverment_service'] });
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openWebView = (url: string) => {
|
||||
setWebUrl(url);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: GovermentServiceDataRes }) => (
|
||||
<View style={{ alignItems: 'center', marginTop: 12, width: CARD_WIDTH }}>
|
||||
{/* Logo (bosilganda WebView ochiladi) */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.card,
|
||||
{ backgroundColor: isDark ? '#1e293b' : '#f8fafc' },
|
||||
isDark ? styles.darkShadow : styles.lightShadow,
|
||||
]}
|
||||
onPress={() => openWebView(item.url)}
|
||||
>
|
||||
<Image source={{ uri: item.logo }} style={styles.logo} resizeMode="contain" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Name (alog‘ifa, faqat ko‘rsatish) */}
|
||||
<Text
|
||||
style={[
|
||||
styles.name,
|
||||
{ color: isDark ? '#f1f5f9' : '#0f172a', marginTop: 4, paddingHorizontal: 5 },
|
||||
]}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
),
|
||||
[isDark]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={[styles.center, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<Text style={{ color: 'red' }}>Xatolik yuz berdi</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||
{/* Header */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
gap: 10,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: isDark ? '#1e293b' : '#f8fafc',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? '#334155' : '#e2e8f0',
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<ChevronLeft size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: isDark ? '#f1f5f9' : '#0f172a',
|
||||
}}
|
||||
>
|
||||
{params.categoryName}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Empty / List */}
|
||||
{services.length === 0 && !isLoading ? (
|
||||
<View style={[styles.center, { flex: 1, padding: 16, gap: 5 }]}>
|
||||
<Text
|
||||
style={{
|
||||
color: isDark ? '#f1f5f9' : '#0f172a',
|
||||
fontSize: 18,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{t("Bu kategoriya bo'yicha xizmat topilmadi")}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: isDark ? '#f1f5f9' : '#0f172a',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{t("Tez orada xizmat qo'shiladi")}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={services}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={isDark ? '#f8fafc' : '#020617'}
|
||||
colors={['#3b82f6']}
|
||||
/>
|
||||
}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={renderItem}
|
||||
numColumns={3}
|
||||
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }}
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
|
||||
onEndReached={() => hasNextPage && fetchNextPage()}
|
||||
onEndReachedThreshold={0.4}
|
||||
ListFooterComponent={
|
||||
isFetchingNextPage ? (
|
||||
<ActivityIndicator color="#3b82f6" size="large" style={{ marginVertical: 20 }} />
|
||||
) : null
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* WebView Modal */}
|
||||
<Modal visible={modalVisible} animationType="slide">
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: isDark ? '#0f172a' : '#f8fafc',
|
||||
}}
|
||||
>
|
||||
{/* WebView Header */}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: isDark ? '#1e293b' : '#f8fafc',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? '#334155' : '#e2e8f0',
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => webviewRef.current?.goBack()}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<ChevronLeft size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: isDark ? '#f1f5f9' : '#0f172a',
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{webUrl}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => setModalVisible(false)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<XIcon size={28} color={isDark ? '#f1f5f9' : '#0f172a'} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* WebView */}
|
||||
{webUrl && (
|
||||
<WebView
|
||||
ref={webviewRef}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
domStorageEnabled
|
||||
onShouldStartLoadWithRequest={(request) => {
|
||||
// iOS va Android uchun barcha URLlarni WebView ichida ochish
|
||||
return true;
|
||||
}}
|
||||
source={{ uri: webUrl }}
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}
|
||||
startInLoadingState
|
||||
renderLoading={() => (
|
||||
<ActivityIndicator color="#3b82f6" size="large" style={{ flex: 1 }} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const CARD_WIDTH = (SCREEN_WIDTH - 50) / 3;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
card: {
|
||||
width: CARD_WIDTH,
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
logo: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
marginBottom: 8,
|
||||
},
|
||||
name: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
darkShadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 6,
|
||||
elevation: 3,
|
||||
},
|
||||
lightShadow: {
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
658
screens/e-services/ui/SearchResultsScreen.tsx
Normal file
@@ -0,0 +1,658 @@
|
||||
// SearchResultsScreen.tsx
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import {
|
||||
Award,
|
||||
Building2,
|
||||
Calendar,
|
||||
ChevronLeft,
|
||||
DollarSign,
|
||||
Info,
|
||||
Mail,
|
||||
MapPin,
|
||||
Phone,
|
||||
User,
|
||||
} from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FlatList, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
type TabType = 'entity' | 'entrepreneur' | 'trademark';
|
||||
|
||||
interface SearchResult {
|
||||
entity: any;
|
||||
entrepreneur: any;
|
||||
trademark: any;
|
||||
}
|
||||
|
||||
interface SearchResultsScreenProps {
|
||||
searchData: SearchResult;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export default function SearchResultsScreen({ searchData, onBack }: SearchResultsScreenProps) {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('entity');
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: 'entity' as TabType,
|
||||
label: 'Korxonalar',
|
||||
icon: Building2,
|
||||
count:
|
||||
searchData.entity.name.total +
|
||||
searchData.entity.director.total +
|
||||
searchData.entity.founder.total,
|
||||
},
|
||||
{
|
||||
key: 'entrepreneur' as TabType,
|
||||
label: 'Tadbirkorlar',
|
||||
icon: User,
|
||||
count: searchData.entrepreneur.total,
|
||||
},
|
||||
{
|
||||
key: 'trademark' as TabType,
|
||||
label: 'Tovar belgilari',
|
||||
icon: Award,
|
||||
count: searchData.trademark.total,
|
||||
},
|
||||
];
|
||||
|
||||
const renderTabs = () => (
|
||||
<View style={styles.tabsContainer}>
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={tab.key}
|
||||
style={[
|
||||
styles.tab,
|
||||
isActive && styles.activeTab,
|
||||
{
|
||||
backgroundColor: isActive
|
||||
? isDark
|
||||
? '#3b82f6'
|
||||
: '#3b82f6'
|
||||
: isDark
|
||||
? '#1e293b'
|
||||
: '#f1f5f9',
|
||||
},
|
||||
]}
|
||||
onPress={() => setActiveTab(tab.key)}
|
||||
>
|
||||
<Icon size={18} color={isActive ? '#ffffff' : isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text
|
||||
style={[
|
||||
styles.tabText,
|
||||
{ color: isActive ? '#ffffff' : isDark ? '#94a3b8' : '#64748b' },
|
||||
]}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
{tab.count > 0 && (
|
||||
<View style={[styles.badge, { backgroundColor: isActive ? '#60a5fa' : '#3b82f6' }]}>
|
||||
<Text style={styles.badgeText}>{tab.count > 9999 ? '9999+' : tab.count}</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderEntityCard = (item: any, variant: string) => (
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: isDark ? '#1e293b' : '#ffffff',
|
||||
borderColor: isDark ? '#334155' : '#e2e8f0',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.cardHeader}>
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
{ backgroundColor: item.activity_state === 1 ? '#22c55e' : '#ef4444' },
|
||||
]}
|
||||
>
|
||||
<Text style={styles.statusText}>{item.activity_state === 1 ? 'Faol' : 'Faol emas'}</Text>
|
||||
</View>
|
||||
{variant && (
|
||||
<View style={[styles.variantBadge, { backgroundColor: isDark ? '#334155' : '#f1f5f9' }]}>
|
||||
<Text style={[styles.variantText, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
{variant === 'director' ? 'Direktor' : variant === 'founder' ? "Ta'sischi" : 'Nomi'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Company Name */}
|
||||
<Text style={[styles.cardTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>{item.name}</Text>
|
||||
|
||||
{/* INN */}
|
||||
<View style={styles.infoRow}>
|
||||
<Info size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.infoLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>INN:</Text>
|
||||
<Text style={[styles.infoValue, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{item.inn}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Director */}
|
||||
{item.director && (
|
||||
<View style={styles.infoRow}>
|
||||
<User size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.infoLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Direktor:
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{item.director}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Address */}
|
||||
{item.address && (
|
||||
<View style={styles.infoRow}>
|
||||
<MapPin size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.infoLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>Manzil:</Text>
|
||||
<Text
|
||||
style={[styles.infoValue, { color: isDark ? '#f1f5f9' : '#0f172a' }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.address}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Registration Date */}
|
||||
{item.registration_date && (
|
||||
<View style={styles.infoRow}>
|
||||
<Calendar size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.infoLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Ro'yxatga olingan:
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{new Date(item.registration_date).toLocaleDateString('uz-UZ')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Activity */}
|
||||
<View style={[styles.activityBox, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<Text style={[styles.activityCode, { color: isDark ? '#60a5fa' : '#3b82f6' }]}>
|
||||
{item.oked_code}
|
||||
</Text>
|
||||
<Text style={[styles.activityName, { color: isDark ? '#cbd5e1' : '#475569' }]}>
|
||||
{item.oked_name}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Contact Info */}
|
||||
<View style={styles.contactRow}>
|
||||
{item.email && (
|
||||
<View style={styles.contactItem}>
|
||||
<Mail size={14} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.contactText, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
{item.email}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{item.phones?.length > 0 && (
|
||||
<View style={styles.contactItem}>
|
||||
<Phone size={14} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.contactText, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
{item.phones[0]}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Statutory Fund */}
|
||||
{item.statutory_fund && (
|
||||
<View style={styles.fundRow}>
|
||||
<DollarSign size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.fundLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Ustav fondi:
|
||||
</Text>
|
||||
<Text style={[styles.fundValue, { color: isDark ? '#22c55e' : '#16a34a' }]}>
|
||||
{parseFloat(item.statutory_fund).toLocaleString('uz-UZ')} so'm
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderEntrepreneurCard = (item: any) => (
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: isDark ? '#1e293b' : '#ffffff',
|
||||
borderColor: isDark ? '#334155' : '#e2e8f0',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.entrepreneurHeader}>
|
||||
<View style={[styles.avatarCircle, { backgroundColor: isDark ? '#334155' : '#e2e8f0' }]}>
|
||||
<User size={32} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.entrepreneurName, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{item.entrepreneur}
|
||||
</Text>
|
||||
<Text style={[styles.pinfl, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
PINFL: {item.pinfl}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{item.registration_date && (
|
||||
<View style={styles.infoRow}>
|
||||
<Calendar size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.infoLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Ro'yxatga olingan:
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{new Date(item.registration_date).toLocaleDateString('uz-UZ')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{item.email && (
|
||||
<View style={styles.contactItem}>
|
||||
<Mail size={14} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.contactText, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
{item.email}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{item.phone && (
|
||||
<View style={styles.contactItem}>
|
||||
<Phone size={14} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.contactText, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
{item.phone}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderTrademarkCard = (item: any) => (
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
{
|
||||
backgroundColor: isDark ? '#1e293b' : '#ffffff',
|
||||
borderColor: isDark ? '#334155' : '#e2e8f0',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.trademarkHeader}>
|
||||
{item.logo && (
|
||||
<Image source={{ uri: item.logo }} style={styles.trademarkLogo} resizeMode="contain" />
|
||||
)}
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.trademarkName, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{item.transliteration}
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
{
|
||||
backgroundColor:
|
||||
item.status_name === 'COMPLETED'
|
||||
? '#22c55e'
|
||||
: item.status_name === 'WAITING'
|
||||
? '#f59e0b'
|
||||
: '#ef4444',
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={styles.statusText}>{item.status_name}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<User size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.infoLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Ariza beruvchi:
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{item.applicant}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{item.registration_date && (
|
||||
<View style={styles.infoRow}>
|
||||
<Calendar size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.infoLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Ro'yxatga olingan:
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{new Date(item.registration_date).toLocaleDateString('uz-UZ')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{item.relevance_date && (
|
||||
<View style={styles.infoRow}>
|
||||
<Calendar size={16} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||
<Text style={[styles.infoLabel, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Amal qilish muddati:
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{new Date(item.relevance_date).toLocaleDateString('uz-UZ')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'entity':
|
||||
const allEntities = [
|
||||
...searchData.entity.name.rows.map((r: any) => ({ ...r, variant: 'name' })),
|
||||
...searchData.entity.director.rows.map((r: any) => ({ ...r, variant: 'director' })),
|
||||
...searchData.entity.founder.rows.map((r: any) => ({ ...r, variant: 'founder' })),
|
||||
];
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={allEntities}
|
||||
keyExtractor={(item, index) => `entity-${item.id}-${index}`}
|
||||
renderItem={({ item }) => renderEntityCard(item, item.variant)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.emptyState}>
|
||||
<Building2 size={48} color={isDark ? '#475569' : '#94a3b8'} />
|
||||
<Text style={[styles.emptyText, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Korxonalar topilmadi
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'entrepreneur':
|
||||
return (
|
||||
<FlatList
|
||||
data={searchData.entrepreneur.rows}
|
||||
keyExtractor={(item) => `entrepreneur-${item.id}`}
|
||||
renderItem={({ item }) => renderEntrepreneurCard(item)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.emptyState}>
|
||||
<User size={48} color={isDark ? '#475569' : '#94a3b8'} />
|
||||
<Text style={[styles.emptyText, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Tadbirkorlar topilmadi
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'trademark':
|
||||
return (
|
||||
<FlatList
|
||||
data={searchData.trademark.rows}
|
||||
keyExtractor={(item) => `trademark-${item.id}`}
|
||||
renderItem={({ item }) => renderTrademarkCard(item)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.emptyState}>
|
||||
<Award size={48} color={isDark ? '#475569' : '#94a3b8'} />
|
||||
<Text style={[styles.emptyText, { color: isDark ? '#94a3b8' : '#64748b' }]}>
|
||||
Tovar belgilari topilmadi
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={onBack} style={styles.backButton}>
|
||||
<ChevronLeft size={24} color={isDark ? '#f1f5f9' : '#0f172a'} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
Qidiruv natijalari
|
||||
</Text>
|
||||
<View style={{ width: 24 }} />
|
||||
</View>
|
||||
|
||||
{/* Tabs */}
|
||||
{renderTabs()}
|
||||
|
||||
{/* Content */}
|
||||
{renderContent()}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
backButton: {
|
||||
padding: 4,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
tabsContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 16,
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 12,
|
||||
gap: 6,
|
||||
},
|
||||
activeTab: {
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
badge: {
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 10,
|
||||
minWidth: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
badgeText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 24,
|
||||
},
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
borderWidth: 1,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
statusText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
variantBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
variantText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
cardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginBottom: 12,
|
||||
},
|
||||
infoRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
flex: 1,
|
||||
},
|
||||
activityBox: {
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginTop: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
activityCode: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
marginBottom: 4,
|
||||
},
|
||||
activityName: {
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
},
|
||||
contactRow: {
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
},
|
||||
contactItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
contactText: {
|
||||
fontSize: 12,
|
||||
},
|
||||
fundRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#334155',
|
||||
},
|
||||
fundLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
},
|
||||
fundValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
},
|
||||
entrepreneurHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
avatarCircle: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
entrepreneurName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginBottom: 4,
|
||||
},
|
||||
pinfl: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
trademarkHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
trademarkLogo: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 8,
|
||||
},
|
||||
trademarkName: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyState: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginTop: 12,
|
||||
},
|
||||
});
|
||||
@@ -47,7 +47,7 @@ export default function AddEmployee() {
|
||||
user_api.create_employee(body),
|
||||
onSuccess: () => {
|
||||
router.push('/profile/employees');
|
||||
queryClient.refetchQueries({ queryKey: ['employees-list'] });
|
||||
queryClient.refetchQueries({ queryKey: ['employees_list'] });
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = (err.response?.data as { data: { phone: string[] } }).data.phone[0];
|
||||
|
||||
@@ -1,190 +1,395 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { ResizeMode, Video } from 'expo-av';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { ArrowLeft } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { router } from 'expo-router';
|
||||
import { VideoView, useVideoPlayer } from 'expo-video';
|
||||
import { ArrowLeft, Check, ChevronDown, X } from 'lucide-react-native';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dimensions,
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import Modal from 'react-native-modal';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
type ManualStep = {
|
||||
title: string;
|
||||
text: string;
|
||||
image?: any;
|
||||
image: any;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
type Language = {
|
||||
code: 'uz' | 'ru' | 'en';
|
||||
label: string;
|
||||
flag: string;
|
||||
};
|
||||
|
||||
const languages: Language[] = [
|
||||
{ code: 'uz', label: "O'zbekcha", flag: '🇺🇿' },
|
||||
{ code: 'ru', label: 'Русский', flag: '🇷🇺' },
|
||||
{ code: 'en', label: 'English', flag: '🇬🇧' },
|
||||
];
|
||||
|
||||
export function ManualTab() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [videoLang, setVideoLang] = useState<'uz' | 'ru' | 'en'>('uz');
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
/** 🔹 Modal states */
|
||||
const [imageVisible, setImageVisible] = useState(false);
|
||||
const [imageIndex, setImageIndex] = useState(0);
|
||||
const [langPickerVisible, setLangPickerVisible] = useState(false);
|
||||
|
||||
/** 🔹 Video tili (SELECT uchun) */
|
||||
const userLang = i18n.language.startsWith('ru')
|
||||
? 'ru'
|
||||
: i18n.language.startsWith('en')
|
||||
? 'en'
|
||||
: 'uz';
|
||||
|
||||
const [selectedLang, setSelectedLang] = useState<'uz' | 'ru' | 'en'>(userLang);
|
||||
|
||||
/** 🔹 RASM tili (faqat app tili bo‘yicha) */
|
||||
const imageLang: 'uz' | 'ru' | 'en' = userLang;
|
||||
|
||||
/** 🔹 Theme */
|
||||
const theme = {
|
||||
background: isDark ? '#0f172a' : '#f8fafc',
|
||||
cardBg: isDark ? '#1e293b' : '#ffffff',
|
||||
text: isDark ? '#ffffff' : '#0f172a',
|
||||
textSecondary: isDark ? '#94a3b8' : '#64748b',
|
||||
primary: '#3b82f6',
|
||||
textMuted: isDark ? '#94a3b8' : '#64748b',
|
||||
border: isDark ? '#334155' : '#e2e8f0',
|
||||
};
|
||||
|
||||
const steps: ManualStep[] = [
|
||||
{
|
||||
title: "Foydalanish qo'lanmasi",
|
||||
text: "Foydalanish qo'lanmasi",
|
||||
image: require('@/assets/manual/step1.jpg'),
|
||||
},
|
||||
{
|
||||
title: "Ro'yxatdan o'tish (Registratsiya) – 1 daqiqa ichida",
|
||||
text: "Platformaga kirish uchun avval ro'yxatdan o'ting.",
|
||||
image: require('@/assets/manual/step2.jpg'),
|
||||
},
|
||||
{
|
||||
title: "Profilni to'ldirish va tasdiqlash",
|
||||
text: "Muhim: Ro'yxatdan o'tgandan keyin profil to'liq bo'lishi kerak — aks holda platforma cheklangan rejimda ishlaydi.",
|
||||
image: require('@/assets/manual/step3.jpg'),
|
||||
},
|
||||
{
|
||||
title: "Xodimlarni qo'shish",
|
||||
text: "Profil ichida Xodimlar bo'limiga o'ting va + tugmasi orqali xodim qo'shing.",
|
||||
image: require('@/assets/manual/step4.jpg'),
|
||||
},
|
||||
{
|
||||
title: "E'lon berish va o'z mahsulot/xizmatlaringizni ko'rsatish",
|
||||
text: "Pastki menyudan E'lonlar bo'limiga o'ting va yangi e'lon yarating.",
|
||||
image: require('@/assets/manual/step5.jpg'),
|
||||
},
|
||||
{
|
||||
title: 'Mijozlarni qidirish va topish',
|
||||
text: 'Filtrdan foydalanib kerakli kompaniyalarni toping va profiliga kirib xabar yuboring.',
|
||||
image: require('@/assets/manual/step6.jpg'),
|
||||
},
|
||||
{
|
||||
title: 'Muhim maslahatlar va xavfsizlik',
|
||||
text: 'Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.',
|
||||
image: require('@/assets/manual/step7.jpg'),
|
||||
},
|
||||
];
|
||||
|
||||
const videos: Record<'uz' | 'ru' | 'en', any> = {
|
||||
/** 🔹 Video manbalari (SELECT ga bog‘liq) */
|
||||
const videos = {
|
||||
uz: require('@/assets/manual/manual_video_uz.mp4'),
|
||||
ru: require('@/assets/manual/manual_video_ru.mp4'),
|
||||
en: require('@/assets/manual/manual_video_en.mp4'),
|
||||
};
|
||||
|
||||
const handleVideoChange = (lang: 'uz' | 'ru' | 'en') => {
|
||||
setVideoLang(lang);
|
||||
};
|
||||
const player = useVideoPlayer(videos[selectedLang], (player) => {
|
||||
player.loop = false;
|
||||
});
|
||||
/** 🔹 Rasmlar (FAqat imageLang) */
|
||||
const steps: ManualStep[] = [
|
||||
{
|
||||
image:
|
||||
imageLang === 'uz'
|
||||
? require('@/assets/manual/image_uz/step1.jpg')
|
||||
: imageLang === 'ru'
|
||||
? require('@/assets/manual/image_ru/step1.jpg')
|
||||
: require('@/assets/manual/image_en/step1.jpg'),
|
||||
description: t("Ilovani oching va ro'yxatdan o'ting"),
|
||||
},
|
||||
{
|
||||
image:
|
||||
imageLang === 'uz'
|
||||
? require('@/assets/manual/image_uz/step2.jpg')
|
||||
: imageLang === 'ru'
|
||||
? require('@/assets/manual/image_ru/step2.jpg')
|
||||
: require('@/assets/manual/image_en/step2.jpg'),
|
||||
description: t("Profilni to'ldiring"),
|
||||
},
|
||||
{
|
||||
image:
|
||||
imageLang === 'uz'
|
||||
? require('@/assets/manual/image_uz/step3.jpg')
|
||||
: imageLang === 'ru'
|
||||
? require('@/assets/manual/image_ru/step3.jpg')
|
||||
: require('@/assets/manual/image_en/step3.jpg'),
|
||||
description: t("To'lov usulini qo'shing"),
|
||||
},
|
||||
{
|
||||
image:
|
||||
imageLang === 'uz'
|
||||
? require('@/assets/manual/image_uz/step4.jpg')
|
||||
: imageLang === 'ru'
|
||||
? require('@/assets/manual/image_ru/step4.jpg')
|
||||
: require('@/assets/manual/image_en/step4.jpg'),
|
||||
description: t('Xaridni tanlang'),
|
||||
},
|
||||
{
|
||||
image:
|
||||
imageLang === 'uz'
|
||||
? require('@/assets/manual/image_uz/step5.jpg')
|
||||
: imageLang === 'ru'
|
||||
? require('@/assets/manual/image_ru/step5.jpg')
|
||||
: require('@/assets/manual/image_en/step5.jpg'),
|
||||
description: t("To'lovni amalga oshiring"),
|
||||
},
|
||||
{
|
||||
image:
|
||||
imageLang === 'uz'
|
||||
? require('@/assets/manual/image_uz/step6.jpg')
|
||||
: imageLang === 'ru'
|
||||
? require('@/assets/manual/image_ru/step6.jpg')
|
||||
: require('@/assets/manual/image_en/step6.jpg'),
|
||||
description: t("Natijani ko'ring"),
|
||||
},
|
||||
{
|
||||
image:
|
||||
imageLang === 'uz'
|
||||
? require('@/assets/manual/image_uz/step7.jpg')
|
||||
: imageLang === 'ru'
|
||||
? require('@/assets/manual/image_ru/step7.jpg')
|
||||
: require('@/assets/manual/image_en/step7.jpg'),
|
||||
description: t("Natijani ko'ring"),
|
||||
},
|
||||
{
|
||||
image:
|
||||
imageLang === 'uz'
|
||||
? require('@/assets/manual/image_uz/step8.jpg')
|
||||
: imageLang === 'ru'
|
||||
? require('@/assets/manual/image_ru/step8.jpg')
|
||||
: require('@/assets/manual/image_en/step8.jpg'),
|
||||
description: t("Natijani ko'ring"),
|
||||
},
|
||||
];
|
||||
|
||||
/** 🔹 Image viewer */
|
||||
const images = useMemo(
|
||||
() => steps.map((step) => ({ url: '', props: { source: step.image } })),
|
||||
[imageLang]
|
||||
);
|
||||
|
||||
const selectedLanguage = languages.find((l) => l.code === selectedLang);
|
||||
|
||||
const renderStep = ({ item, index }: { item: ManualStep; index: number }) => (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setImageIndex(index);
|
||||
setImageVisible(true);
|
||||
}}
|
||||
>
|
||||
<View style={[styles.stepCard, { backgroundColor: theme.cardBg }]}>
|
||||
<Image source={item.image} style={styles.stepImage} />
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
||||
<Pressable onPress={() => router.push('/profile')}>
|
||||
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||
</Pressable>
|
||||
{/* HEADER */}
|
||||
<View style={styles.hero}>
|
||||
<View style={styles.topHeader}>
|
||||
<Pressable onPress={() => router.back()}>
|
||||
<ArrowLeft color={theme.text} size={24} />
|
||||
</Pressable>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>
|
||||
{t("Foydalanish qo'llanmasi")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||
{t("Foydalanish qo'lanmasi")}
|
||||
<Text style={[styles.subtitle, { color: theme.textMuted }]}>
|
||||
{t("Quyidagi qisqa video yoki rasmlarni ko'rib chiqing")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{steps.map((step, index) => (
|
||||
<View key={index} style={[styles.card, { backgroundColor: theme.cardBg }]}>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t(step.title)}</Text>
|
||||
<Text style={[styles.text, { color: theme.textSecondary, marginTop: 8 }]}>
|
||||
{t(step.text)}
|
||||
</Text>
|
||||
{step.image && <Image source={step.image} style={styles.image} resizeMode="contain" />}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Video bo'limi */}
|
||||
<View style={[styles.card, { backgroundColor: theme.cardBg }]}>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t("Qo'llanma video")}</Text>
|
||||
|
||||
{/* Til tanlash tugmalari */}
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
{/* VIDEO */}
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
||||
{t("Foydalanish video qo'llanma")}
|
||||
</Text>
|
||||
<View style={{ alignContent: 'flex-end', alignItems: 'flex-end', paddingHorizontal: 16 }}>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.langButton,
|
||||
{
|
||||
backgroundColor: videoLang === 'uz' ? theme.primary : theme.cardBg,
|
||||
borderColor: theme.primary,
|
||||
},
|
||||
styles.langSelector,
|
||||
{ backgroundColor: theme.cardBg, borderColor: theme.border },
|
||||
]}
|
||||
onPress={() => handleVideoChange('uz')}
|
||||
onPress={() => setLangPickerVisible(true)}
|
||||
>
|
||||
<Text style={{ color: videoLang === 'uz' ? '#fff' : theme.text }}>O'zbek</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.langButton,
|
||||
{
|
||||
backgroundColor: videoLang === 'ru' ? theme.primary : theme.cardBg,
|
||||
borderColor: theme.primary,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleVideoChange('ru')}
|
||||
>
|
||||
<Text style={{ color: videoLang === 'ru' ? '#fff' : theme.text }}>Русский</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.langButton,
|
||||
{
|
||||
backgroundColor: videoLang === 'en' ? theme.primary : theme.cardBg,
|
||||
borderColor: theme.primary,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleVideoChange('en')}
|
||||
>
|
||||
<Text style={{ color: videoLang === 'en' ? '#fff' : theme.text }}>English</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ fontSize: 20 }}>{selectedLanguage?.flag}</Text>
|
||||
</View>
|
||||
<ChevronDown color={theme.textMuted} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<Video
|
||||
source={videos[videoLang]}
|
||||
style={styles.video}
|
||||
useNativeControls
|
||||
isLooping
|
||||
resizeMode={ResizeMode.COVER}
|
||||
<View style={[styles.videoCard, { backgroundColor: theme.cardBg }]}>
|
||||
<VideoView
|
||||
player={player}
|
||||
style={styles.video}
|
||||
allowsFullscreen
|
||||
allowsPictureInPicture
|
||||
contentFit="cover"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* RASMLAR */}
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
||||
{t('Foydalanish rasm qo‘llanma')}
|
||||
</Text>
|
||||
|
||||
<FlatList
|
||||
horizontal
|
||||
data={steps}
|
||||
renderItem={renderStep}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingHorizontal: 16, gap: 20 }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* IMAGE MODAL */}
|
||||
<Modal isVisible={imageVisible} style={{ margin: 0 }}>
|
||||
<ImageViewer
|
||||
imageUrls={images}
|
||||
index={imageIndex}
|
||||
enableSwipeDown
|
||||
onSwipeDown={() => setImageVisible(false)}
|
||||
renderHeader={() => (
|
||||
<Pressable style={styles.closeButton} onPress={() => setImageVisible(false)}>
|
||||
<X size={32} color="#fff" />
|
||||
</Pressable>
|
||||
)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* LANGUAGE MODAL */}
|
||||
<Modal isVisible={langPickerVisible} onBackdropPress={() => setLangPickerVisible(false)}>
|
||||
<View style={[styles.langModalContent, { backgroundColor: theme.cardBg }]}>
|
||||
<Text style={{ color: 'white', marginBottom: 10, fontSize: 18 }}>
|
||||
{t('Video tilini tanlang')}
|
||||
</Text>
|
||||
{languages.map((lang) => {
|
||||
const isSelected = selectedLang === lang.code;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={lang.code}
|
||||
onPress={() => {
|
||||
setSelectedLang(lang.code);
|
||||
setLangPickerVisible(false);
|
||||
}}
|
||||
style={[
|
||||
styles.langOption,
|
||||
{
|
||||
borderColor: isSelected ? theme.primary : theme.border,
|
||||
backgroundColor: isSelected ? theme.primary + '15' : 'transparent',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.langOptionLeft}>
|
||||
<Text style={{ fontSize: 26 }}>{lang.flag}</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: isSelected ? theme.primary : theme.text,
|
||||
fontWeight: isSelected ? '700' : '500',
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{lang.label}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{isSelected && (
|
||||
<View style={[styles.checkmark, { backgroundColor: theme.primary }]}>
|
||||
<Check size={20} color={'white'} />
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</Modal>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 16 },
|
||||
card: { borderRadius: 16, padding: 16, marginBottom: 16 },
|
||||
headerTitle: { fontSize: 18, fontWeight: '700', lineHeight: 24 },
|
||||
text: { fontSize: 14, lineHeight: 20 },
|
||||
image: { width: width - 64, height: 200, marginTop: 12, borderRadius: 12 },
|
||||
video: { width: width - 64, height: 320, marginTop: 12, borderRadius: 12 },
|
||||
buttonRow: { flexDirection: 'row', justifyContent: 'space-around', marginVertical: 12 },
|
||||
langButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
container: { flex: 1 },
|
||||
hero: { padding: 20 },
|
||||
topHeader: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||||
headerTitle: { fontSize: 22, fontWeight: '700' },
|
||||
subtitle: { marginTop: 8 },
|
||||
|
||||
section: { marginBottom: 28 },
|
||||
sectionTitle: { fontSize: 20, fontWeight: '700', marginLeft: 16 },
|
||||
|
||||
langSelector: {
|
||||
margin: 5,
|
||||
width: 80,
|
||||
gap: 5,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
header: {
|
||||
padding: 16,
|
||||
|
||||
videoCard: {
|
||||
marginHorizontal: 16,
|
||||
borderRadius: 20,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
video: { width: '100%', aspectRatio: 9 / 13 },
|
||||
|
||||
stepCard: { borderRadius: 16, overflow: 'hidden' },
|
||||
stepImage: { width: 300, height: 300 },
|
||||
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
right: 20,
|
||||
padding: 8,
|
||||
zIndex: 50,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
borderRadius: 20,
|
||||
},
|
||||
|
||||
langModalContent: {
|
||||
padding: 20,
|
||||
borderRadius: 20,
|
||||
},
|
||||
langOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 10,
|
||||
elevation: 3,
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
marginBottom: 10,
|
||||
},
|
||||
|
||||
langOptionLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
|
||||
checkmark: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
checkmarkText: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
playOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.25)',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,7 +41,14 @@ export function NotificationTab() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.loadingContainer,
|
||||
{
|
||||
backgroundColor: isDark ? '#0f172a' : '#f8fafc',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.loadingContent}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
@@ -51,10 +58,12 @@ export function NotificationTab() {
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={styles.errorContainer}>
|
||||
<View style={styles.errorContent}>
|
||||
<View style={[styles.errorContainer, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<View style={[styles.errorContent, { backgroundColor: isDark ? '#1e293b' : '#ffffff' }]}>
|
||||
<Text style={styles.errorTitle}>{t('Xatolik yuz berdi')}</Text>
|
||||
<Text style={styles.errorMessage}>{t("Bildirishnomalarni yuklashda muammo bo'ldi")}</Text>
|
||||
<Text style={[styles.errorMessage, { color: isDark ? '#cbd5e1' : '#64748b' }]}>
|
||||
{t("Bildirishnomalarni yuklashda muammo bo'ldi")}
|
||||
</Text>
|
||||
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
|
||||
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -64,13 +73,13 @@ export function NotificationTab() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
||||
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||
<Pressable onPress={() => router.push('/profile')}>
|
||||
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||
</Pressable>
|
||||
|
||||
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||
{t('Bildirishnomalar')}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -102,12 +111,14 @@ export function NotificationTab() {
|
||||
|
||||
function NotificationCard({ item }: { item: NotificationListDataRes }) {
|
||||
const queryClient = useQueryClient();
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [scaleAnim] = useState(new Animated.Value(1));
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: (id: number) => user_api.is_ready_id(id),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ['notification-list'] });
|
||||
queryClient.refetchQueries({ queryKey: ['notifications-list'] });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -145,21 +156,54 @@ function NotificationCard({ item }: { item: NotificationListDataRes }) {
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
onPress={() => handlePress(item.id)}
|
||||
style={[styles.card, !item.is_read && styles.unreadCard]}
|
||||
style={[
|
||||
styles.card,
|
||||
!item.is_read && styles.unreadCard,
|
||||
{
|
||||
backgroundColor: isDark ? '#283046' : '#e0f2fe',
|
||||
borderColor: isDark ? '#3b82f6' : '#3b82f6',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.cardContent}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={[styles.cardTitle, !item.is_read && styles.unreadTitle]} numberOfLines={1}>
|
||||
<Text
|
||||
style={[
|
||||
styles.cardTitle,
|
||||
!item.is_read && styles.unreadTitle,
|
||||
{ color: isDark ? '#3b82f6' : '#1e40af' },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
{!item.is_read && <View style={styles.unreadIndicator} />}
|
||||
{!item.is_read && (
|
||||
<View
|
||||
style={[
|
||||
styles.unreadIndicator,
|
||||
{
|
||||
backgroundColor: isDark ? '#3b82f6' : '#3b82f6',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text style={styles.cardMessage} numberOfLines={2}>
|
||||
<Text
|
||||
style={[
|
||||
styles.cardMessage,
|
||||
{
|
||||
color: isDark ? '#cbd5e1' : '#64748b',
|
||||
},
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.cardTime}>{formatDate(item.created_at, t)}</Text>
|
||||
<Text style={[styles.cardTime, { color: isDark ? '#94a3b8' : '#94a3b8' }]}>
|
||||
{formatDate(item.created_at, t)}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
@@ -187,12 +231,9 @@ function formatDate(date: string, t: any) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- STYLES ---------------- */
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0a0c17',
|
||||
},
|
||||
listContent: {
|
||||
padding: 16,
|
||||
@@ -202,41 +243,23 @@ const styles = StyleSheet.create({
|
||||
/* Card Styles */
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#121826',
|
||||
padding: 16,
|
||||
borderRadius: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(60, 70, 90, 0.18)',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 16,
|
||||
elevation: 10,
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
unreadCard: {
|
||||
backgroundColor: '#1a2236',
|
||||
borderColor: 'rgba(59, 130, 246, 0.4)',
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOpacity: 0.28,
|
||||
shadowRadius: 20,
|
||||
elevation: 12,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(30, 38, 56, 0.7)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
unreadIconContainer: {
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.22)',
|
||||
},
|
||||
iconText: {
|
||||
fontSize: 32,
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 16,
|
||||
elevation: 10,
|
||||
},
|
||||
|
||||
cardContent: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
@@ -248,20 +271,20 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
cardTitle: {
|
||||
flex: 1,
|
||||
color: '#d1d5db',
|
||||
|
||||
fontSize: 16.5,
|
||||
fontWeight: '600',
|
||||
letterSpacing: -0.1,
|
||||
},
|
||||
unreadTitle: {
|
||||
color: '#f1f5f9',
|
||||
fontWeight: '700',
|
||||
// unread title rang
|
||||
},
|
||||
unreadIndicator: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: '#3b82f6',
|
||||
|
||||
marginLeft: 8,
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
@@ -269,13 +292,11 @@ const styles = StyleSheet.create({
|
||||
shadowRadius: 6,
|
||||
},
|
||||
cardMessage: {
|
||||
color: '#9ca3af',
|
||||
fontSize: 14.5,
|
||||
lineHeight: 21,
|
||||
marginBottom: 8,
|
||||
},
|
||||
cardTime: {
|
||||
color: '#64748b',
|
||||
fontSize: 12.5,
|
||||
fontWeight: '500',
|
||||
opacity: 0.9,
|
||||
@@ -284,7 +305,7 @@ const styles = StyleSheet.create({
|
||||
/* Loading State */
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0a0c17',
|
||||
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
@@ -292,40 +313,22 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
padding: 32,
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 16,
|
||||
color: '#94a3b8',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
|
||||
/* Error State */
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0a0c17',
|
||||
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
errorContent: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#151b2e',
|
||||
|
||||
padding: 32,
|
||||
borderRadius: 24,
|
||||
maxWidth: 320,
|
||||
},
|
||||
errorIconContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: '#1e2638',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
errorIcon: {
|
||||
fontSize: 40,
|
||||
},
|
||||
errorTitle: {
|
||||
color: '#ef4444',
|
||||
fontSize: 20,
|
||||
@@ -333,7 +336,6 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
errorMessage: {
|
||||
color: '#94a3b8',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
marginBottom: 24,
|
||||
@@ -356,52 +358,19 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
/* Empty State */
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0a0c17',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
emptyContent: {
|
||||
alignItems: 'center',
|
||||
maxWidth: 300,
|
||||
},
|
||||
emptyIconContainer: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: '#151b2e',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 24,
|
||||
borderWidth: 2,
|
||||
borderColor: '#1e2638',
|
||||
},
|
||||
emptyIcon: {
|
||||
fontSize: 50,
|
||||
},
|
||||
emptyTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
marginBottom: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
emptyMessage: {
|
||||
color: '#94a3b8',
|
||||
fontSize: 15,
|
||||
textAlign: 'center',
|
||||
lineHeight: 22,
|
||||
},
|
||||
/* Header */
|
||||
header: {
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 10,
|
||||
|
||||
elevation: 3,
|
||||
},
|
||||
headerTitle: { fontSize: 18, fontWeight: '700', lineHeight: 24 },
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
lineHeight: 24,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,7 +25,13 @@ export default function Profile() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: me, isLoading } = useQuery({
|
||||
const { data } = useQuery({
|
||||
queryKey: ['notification-list'],
|
||||
queryFn: () => user_api.notification_list({ page: 1, page_size: 1 }),
|
||||
});
|
||||
const unreadCount = data?.data?.data.unread_count ?? 0;
|
||||
|
||||
const { data: me } = useQuery({
|
||||
queryKey: ['get_me'],
|
||||
queryFn: () => user_api.getMe(),
|
||||
});
|
||||
@@ -36,7 +42,12 @@ export default function Profile() {
|
||||
items: [
|
||||
{ icon: User, label: "Shaxsiy ma'lumotlar", route: '/profile/personal-info' },
|
||||
{ icon: Users, label: 'Xodimlar', route: '/profile/employees' },
|
||||
{ icon: Bell, label: 'Bildirishnomalar', route: '/profile/notification' },
|
||||
{
|
||||
icon: Bell,
|
||||
label: 'Bildirishnomalar',
|
||||
route: '/profile/notification',
|
||||
badge: unreadCount,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -60,7 +71,7 @@ export default function Profile() {
|
||||
title: 'Sozlamalar',
|
||||
items: [
|
||||
{ icon: Settings, label: 'Sozlamalar', route: '/profile/settings' },
|
||||
{ icon: BookAIcon, label: "Foydalanish qo'lanmasi", route: '/profile/manual' },
|
||||
{ icon: BookAIcon, label: "Foydalanish qo'llanmasi", route: '/profile/manual' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -95,6 +106,12 @@ export default function Profile() {
|
||||
style={[styles.iconContainer, isDark ? styles.darkIconBg : styles.lightIconBg]}
|
||||
>
|
||||
<item.icon size={24} color="#3b82f6" />
|
||||
|
||||
{item.badge && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{item.badge}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text style={[styles.cardLabel, isDark ? styles.darkText : styles.lightText]}>
|
||||
@@ -202,4 +219,23 @@ const styles = StyleSheet.create({
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
top: -5,
|
||||
right: -5,
|
||||
backgroundColor: '#ef4444', // qizil badge
|
||||
borderRadius: 8,
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 3,
|
||||
},
|
||||
|
||||
badgeText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
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, useImperativeHandle, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
@@ -18,12 +18,15 @@ type Errors = {
|
||||
media?: string;
|
||||
};
|
||||
|
||||
const MAX_MEDIA = 10;
|
||||
type MediaTabType = 'image' | 'video';
|
||||
|
||||
const MAX_MEDIA = 1;
|
||||
|
||||
const StepOneServices = forwardRef(({ formData, updateForm, removeMedia }: StepProps, ref) => {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
const [selectedMediaTab, setSelectedMediaTab] = useState<MediaTabType>('image');
|
||||
|
||||
const validate = () => {
|
||||
const e: Errors = {};
|
||||
@@ -34,8 +37,7 @@ const StepOneServices = forwardRef(({ formData, updateForm, removeMedia }: StepP
|
||||
if (!formData.description || formData.description.trim().length < 10)
|
||||
e.description = t("Tavsif kamida 10 ta belgidan iborat bo'lishi kerak");
|
||||
|
||||
if (!formData.media || formData.media.length === 0)
|
||||
e.media = t('Kamida bitta rasm yoki video yuklang');
|
||||
if (!formData.media || formData.media.length === 0) e.media = t('Rasm yoki video yuklang');
|
||||
|
||||
setErrors(e);
|
||||
return Object.keys(e).length === 0;
|
||||
@@ -46,101 +48,199 @@ const StepOneServices = forwardRef(({ formData, updateForm, removeMedia }: StepP
|
||||
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]);
|
||||
}
|
||||
};
|
||||
|
||||
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',
|
||||
tabActive: isDark ? '#2563eb' : '#3b82f6',
|
||||
tabInactive: isDark ? '#334155' : '#e2e8f0',
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.stepContainer}>
|
||||
{/* Sarlavha */}
|
||||
<Text style={[styles.label, { color: isDark ? '#f8fafc' : '#0f172a' }]}>{t('Sarlavha')}</Text>
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Sarlavha')}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputBox,
|
||||
{
|
||||
backgroundColor: isDark ? '#1e293b' : '#ffffff',
|
||||
borderColor: isDark ? '#334155' : '#e2e8f0',
|
||||
backgroundColor: theme.inputBg,
|
||||
borderColor: theme.inputBorder,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
|
||||
style={[styles.input, { color: theme.text }]}
|
||||
placeholder={t('Xizmat sarlavhasi')}
|
||||
placeholderTextColor={isDark ? '#94a3b8' : '#94a3b8'}
|
||||
placeholderTextColor={theme.placeholder}
|
||||
value={formData.title}
|
||||
onChangeText={(t) => updateForm('title', t)}
|
||||
/>
|
||||
</View>
|
||||
{errors.title && <Text style={styles.error}>{errors.title}</Text>}
|
||||
{errors.title && <Text style={[styles.error, { color: theme.error }]}>{errors.title}</Text>}
|
||||
|
||||
{/* Tavsif */}
|
||||
<Text style={[styles.label, { color: isDark ? '#f8fafc' : '#0f172a' }]}>{t('Tavsif')}</Text>
|
||||
<Text style={[styles.label, { color: theme.text }]}>{t('Tavsif')}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.inputBox,
|
||||
styles.textArea,
|
||||
{
|
||||
backgroundColor: isDark ? '#1e293b' : '#ffffff',
|
||||
borderColor: isDark ? '#334155' : '#e2e8f0',
|
||||
backgroundColor: theme.inputBg,
|
||||
borderColor: theme.inputBorder,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
|
||||
style={[styles.input, { color: theme.text }]}
|
||||
placeholder={t('Batafsil yozing...')}
|
||||
placeholderTextColor={isDark ? '#94a3b8' : '#94a3b8'}
|
||||
placeholderTextColor={theme.placeholder}
|
||||
multiline
|
||||
value={formData.description}
|
||||
onChangeText={(t) => updateForm('description', t)}
|
||||
/>
|
||||
</View>
|
||||
{errors.description && <Text style={styles.error}>{errors.description}</Text>}
|
||||
{errors.description && (
|
||||
<Text style={[styles.error, { color: theme.error }]}>{errors.description}</Text>
|
||||
)}
|
||||
|
||||
{/* Media */}
|
||||
<Text style={[styles.label, { color: isDark ? '#f8fafc' : '#0f172a' }]}>
|
||||
{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,
|
||||
styles.tab,
|
||||
selectedMediaTab === 'image' && styles.tabActive,
|
||||
{
|
||||
borderColor: '#2563eb',
|
||||
backgroundColor: isDark ? '#1e293b' : '#f8fafc',
|
||||
backgroundColor: selectedMediaTab === 'image' ? theme.tabActive : theme.tabInactive,
|
||||
borderColor: selectedMediaTab === 'image' ? theme.tabActive : theme.inputBorder,
|
||||
},
|
||||
]}
|
||||
onPress={pickMedia}
|
||||
onPress={() => setSelectedMediaTab('image')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Camera size={28} color="#2563eb" />
|
||||
<Text style={styles.uploadText}>{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} onPress={() => removeMedia(i)}>
|
||||
<X size={12} color="#fff" />
|
||||
<TouchableOpacity
|
||||
style={[styles.removeLarge, { backgroundColor: theme.error }]}
|
||||
onPress={() => removeMedia(0)}
|
||||
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}>{errors.media}</Text>}
|
||||
{errors.media && (
|
||||
<Text style={[styles.error, { color: theme.error }]}>{t(errors.media)}</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
@@ -150,7 +250,7 @@ export default StepOneServices;
|
||||
const styles = StyleSheet.create({
|
||||
stepContainer: { gap: 10 },
|
||||
label: { fontWeight: '700', fontSize: 15 },
|
||||
error: { color: '#ef4444', fontSize: 13, marginLeft: 6 },
|
||||
error: { fontSize: 13, marginLeft: 6 },
|
||||
inputBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -159,53 +259,125 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 16,
|
||||
height: 56,
|
||||
},
|
||||
textArea: { height: 120, alignItems: 'flex-start', paddingTop: 16 },
|
||||
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,
|
||||
|
||||
// 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,
|
||||
},
|
||||
uploadText: { color: '#2563eb', fontSize: 11, marginTop: 4, fontWeight: '600' },
|
||||
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,
|
||||
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,
|
||||
},
|
||||
remove: {
|
||||
playLarge: {
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
right: -6,
|
||||
backgroundColor: '#ef4444',
|
||||
padding: 4,
|
||||
borderRadius: 10,
|
||||
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,
|
||||
},
|
||||
prefixContainer: {
|
||||
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',
|
||||
marginRight: 12,
|
||||
gap: 6,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 10,
|
||||
},
|
||||
prefix: {
|
||||
fontSize: 16,
|
||||
mediaTypeBadgeText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
prefixFocused: {
|
||||
color: '#fff',
|
||||
},
|
||||
divider: {
|
||||
width: 1.5,
|
||||
height: 24,
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
|
||||