government ui complated

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

View File

@@ -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/`,
};

View File

@@ -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,

View File

@@ -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 kok
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',
},

View 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>
);
}

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 />

View File

@@ -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} />

View File

@@ -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}
/>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

View File

@@ -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: {

View File

@@ -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 qollanma": "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"
}

View File

@@ -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 qollanma": "Инструкция по использованию изображений",
"Video tilini tanlang": "Выберите язык видео",
"Davlat xizmatlari kategoriyalari": "Категории государственных услуг",
"Kerakli xizmat turini tanlang": "Выберите нужный тип услуги",
"Bu kategoriya bo'yicha xizmat topilmadi": "Сервис в этой категории не найден.",
"Tez orada xizmat qo'shiladi": "Сервис скоро будет добавлен"
}

View File

@@ -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 qollanma": "Foydalanish rasm qollanma",
"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
View File

@@ -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",

View File

@@ -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",

View File

@@ -1,7 +1,7 @@
import { useTheme } from '@/components/ThemeContext';
import { formatPhone, normalizeDigits } from '@/constants/formatPhone';
import * as ImagePicker from 'expo-image-picker';
import { Camera, Play, X } from 'lucide-react-native';
import { Image as ImageIcon, Play, Video, X } from 'lucide-react-native';
import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
@@ -16,12 +16,15 @@ type Errors = {
media?: string;
};
const MAX_MEDIA = 10;
type MediaTabType = 'image' | 'video';
const MAX_MEDIA = 1;
const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
const [phone, setPhone] = useState(formData.phone || '');
const [focused, setFocused] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const [selectedMediaTab, setSelectedMediaTab] = useState<MediaTabType>('image');
const { isDark } = useTheme();
const { t } = useTranslation();
@@ -37,8 +40,7 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
if (!formData.phone || formData.phone.length !== 9)
e.phone = "Telefon raqam to'liq kiritilmadi";
if (!formData.media || formData.media.length === 0)
e.media = 'Kamida bitta rasm yoki video yuklang';
if (!formData.media || formData.media.length === 0) e.media = 'Rasm yoki video yuklang';
setErrors(e);
return Object.keys(e).length === 0;
@@ -49,18 +51,26 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
const pickMedia = async () => {
if (formData.media.length >= MAX_MEDIA) return;
const mediaType =
selectedMediaTab === 'image'
? ImagePicker.MediaTypeOptions.Images
: ImagePicker.MediaTypeOptions.Videos;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsMultipleSelection: true,
mediaTypes: mediaType,
allowsMultipleSelection: false,
quality: 0.8,
videoMaxDuration: 60, // 60 seconds max for video
});
if (!result.canceled) {
const assets = result.assets
.slice(0, MAX_MEDIA - formData.media.length)
.map((a) => ({ uri: a.uri, type: a.type as 'image' | 'video' }));
const asset = result.assets[0];
const mediaItem = {
uri: asset.uri,
type: selectedMediaTab,
};
updateForm('media', [...formData.media, ...assets]);
updateForm('media', [mediaItem]);
}
};
@@ -73,11 +83,9 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
[updateForm]
);
const removeMedia = (i: number) =>
updateForm(
'media',
formData.media.filter((_: any, idx: number) => idx !== i)
);
const removeMedia = () => {
updateForm('media', []);
};
const theme = {
background: isDark ? '#0f172a' : '#ffffff',
@@ -89,6 +97,8 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
error: '#ef4444',
primary: '#2563eb',
divider: isDark ? '#475569' : '#cbd5e1',
tabActive: isDark ? '#2563eb' : '#3b82f6',
tabInactive: isDark ? '#334155' : '#e2e8f0',
};
return (
@@ -165,35 +175,111 @@ const StepOne = forwardRef(({ formData, updateForm }: StepProps, ref) => {
<Text style={[styles.error, { color: theme.error }]}>{t(errors.phone)}</Text>
)}
{/* Media */}
<Text style={[styles.label, { color: theme.text }]}>
{t('Media')} ({formData.media.length}/{MAX_MEDIA})
</Text>
<View style={styles.media}>
{/* Media Type Tabs */}
<Text style={[styles.label, { color: theme.text }]}>{t('Media turi')}</Text>
<View style={styles.tabsContainer}>
<TouchableOpacity
style={[styles.upload, { borderColor: theme.primary }]}
onPress={pickMedia}
style={[
styles.tab,
selectedMediaTab === 'image' && styles.tabActive,
{
backgroundColor: selectedMediaTab === 'image' ? theme.tabActive : theme.tabInactive,
borderColor: selectedMediaTab === 'image' ? theme.tabActive : theme.inputBorder,
},
]}
onPress={() => setSelectedMediaTab('image')}
activeOpacity={0.7}
>
<Camera size={28} color={theme.primary} />
<Text style={[styles.uploadText, { color: theme.primary }]}>{t('Yuklash')}</Text>
<ImageIcon
size={20}
color={selectedMediaTab === 'image' ? '#ffffff' : theme.textSecondary}
/>
<Text
style={[
styles.tabText,
{ color: selectedMediaTab === 'image' ? '#ffffff' : theme.textSecondary },
]}
>
{t('Rasm')}
</Text>
</TouchableOpacity>
{formData.media.map((m: MediaType, i: number) => (
<View key={i} style={styles.preview}>
<Image source={{ uri: m.uri }} style={styles.image} />
{m.type === 'video' && (
<View style={styles.play}>
<Play size={14} color="#fff" fill="#fff" />
<TouchableOpacity
style={[
styles.tab,
selectedMediaTab === 'video' && styles.tabActive,
{
backgroundColor: selectedMediaTab === 'video' ? theme.tabActive : theme.tabInactive,
borderColor: selectedMediaTab === 'video' ? theme.tabActive : theme.inputBorder,
},
]}
onPress={() => setSelectedMediaTab('video')}
activeOpacity={0.7}
>
<Video size={20} color={selectedMediaTab === 'video' ? '#ffffff' : theme.textSecondary} />
<Text
style={[
styles.tabText,
{ color: selectedMediaTab === 'video' ? '#ffffff' : theme.textSecondary },
]}
>
{t('Video')}
</Text>
</TouchableOpacity>
</View>
{/* Media Upload/Preview */}
<View style={styles.mediaContainer}>
{formData.media.length === 0 ? (
<TouchableOpacity
style={[
styles.uploadLarge,
{ borderColor: theme.primary, backgroundColor: theme.inputBg },
]}
onPress={pickMedia}
activeOpacity={0.7}
>
<View style={[styles.uploadIconWrapper, { backgroundColor: theme.primary }]}>
{selectedMediaTab === 'image' ? (
<ImageIcon size={32} color="#ffffff" />
) : (
<Video size={32} color="#ffffff" />
)}
</View>
<Text style={[styles.uploadLargeText, { color: theme.text }]}>
{selectedMediaTab === 'image' ? t('Rasm yuklash') : t('Video yuklash')}
</Text>
<Text style={[styles.uploadLargeSubtext, { color: theme.textSecondary }]}>
{selectedMediaTab === 'image' ? t('Rasm tanlang') : t('Video tanlang')}
</Text>
</TouchableOpacity>
) : (
<View style={styles.previewLarge}>
<Image source={{ uri: formData.media[0].uri }} style={styles.imageLarge} />
{formData.media[0].type === 'video' && (
<View style={styles.playLarge}>
<Play size={24} color="#fff" fill="#fff" />
</View>
)}
<TouchableOpacity
style={[styles.remove, { backgroundColor: theme.error }]}
onPress={() => removeMedia(i)}
style={[styles.removeLarge, { backgroundColor: theme.error }]}
onPress={removeMedia}
activeOpacity={0.8}
>
<X size={12} color="#fff" />
<X size={16} color="#fff" />
</TouchableOpacity>
<View style={[styles.mediaTypeBadge, { backgroundColor: 'rgba(0,0,0,0.7)' }]}>
{formData.media[0].type === 'image' ? (
<ImageIcon size={14} color="#fff" />
) : (
<Video size={14} color="#fff" />
)}
<Text style={styles.mediaTypeBadgeText}>
{formData.media[0].type === 'image' ? 'Rasm' : 'Video'}
</Text>
</View>
</View>
))}
)}
</View>
{errors.media && (
<Text style={[styles.error, { color: theme.error }]}>{t(errors.media)}</Text>
@@ -206,7 +292,7 @@ export default StepOne;
const styles = StyleSheet.create({
stepContainer: { gap: 10 },
label: { fontWeight: '700' },
label: { fontWeight: '700', fontSize: 15 },
error: { fontSize: 13, marginLeft: 6 },
inputBox: {
flexDirection: 'row',
@@ -216,36 +302,8 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
height: 56,
},
textArea: { height: 120, alignItems: 'flex-start' },
textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 12 },
input: { flex: 1, fontSize: 16 },
media: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
upload: {
width: 100,
height: 100,
borderRadius: 16,
borderWidth: 2,
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
},
uploadText: { fontSize: 11, marginTop: 4 },
preview: { width: 100, height: 100 },
image: { width: '100%', height: '100%', borderRadius: 16 },
play: {
position: 'absolute',
top: '40%',
left: '40%',
backgroundColor: 'rgba(0,0,0,.5)',
padding: 6,
borderRadius: 20,
},
remove: {
position: 'absolute',
top: -6,
right: -6,
padding: 4,
borderRadius: 10,
},
prefixContainer: {
flexDirection: 'row',
alignItems: 'center',
@@ -262,4 +320,123 @@ const styles = StyleSheet.create({
height: 24,
marginLeft: 12,
},
// Media Tabs
tabsContainer: {
flexDirection: 'row',
gap: 12,
},
tab: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
paddingHorizontal: 16,
borderRadius: 16,
borderWidth: 2,
gap: 8,
},
tabActive: {
shadowColor: '#2563eb',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3,
},
tabText: {
fontSize: 15,
fontWeight: '700',
letterSpacing: 0.3,
},
// Media Container
mediaContainer: {
marginTop: 4,
},
uploadLarge: {
height: 240,
borderRadius: 20,
borderWidth: 2,
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
gap: 12,
},
uploadIconWrapper: {
width: 72,
height: 72,
borderRadius: 36,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 3,
},
uploadLargeText: {
fontSize: 18,
fontWeight: '700',
marginTop: 4,
},
uploadLargeSubtext: {
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
paddingHorizontal: 32,
},
previewLarge: {
height: 240,
borderRadius: 20,
overflow: 'hidden',
position: 'relative',
},
imageLarge: {
width: '100%',
height: '100%',
borderRadius: 20,
},
playLarge: {
position: 'absolute',
top: '50%',
left: '50%',
transform: [{ translateX: -28 }, { translateY: -28 }],
backgroundColor: 'rgba(0,0,0,.6)',
padding: 14,
borderRadius: 32,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3,
},
removeLarge: {
position: 'absolute',
top: 12,
right: 12,
padding: 8,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3,
},
mediaTypeBadge: {
position: 'absolute',
bottom: 12,
left: 12,
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingVertical: 6,
paddingHorizontal: 12,
borderRadius: 10,
},
mediaTypeBadgeText: {
color: '#ffffff',
fontSize: 13,
fontWeight: '600',
},
});

View File

@@ -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();
},
};

View File

@@ -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;
}

View File

@@ -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,
},
});

View 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>
);
}

View 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 (alogifa, faqat korsatish) */}
<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,
},
});

View 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,
},
});

View File

@@ -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];

View File

@@ -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 boyicha) */
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 bogliq) */
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 qollanma')}
</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)',
},
});

View File

@@ -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,
},
});

View File

@@ -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',
},
});

View File

@@ -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,
},
});