complated project
@@ -30,4 +30,8 @@ export const API_URLS = {
|
|||||||
My_Ads: 'api/my-ads/',
|
My_Ads: 'api/my-ads/',
|
||||||
My_Ads_Detail: (id: number) => `api/my-ads/${id}`,
|
My_Ads_Detail: (id: number) => `api/my-ads/${id}`,
|
||||||
My_Bonuses: 'api/cashback/',
|
My_Bonuses: 'api/cashback/',
|
||||||
|
My_Refferals: 'api/referral/',
|
||||||
|
Goverment_Service: '/api/goverment-service/',
|
||||||
|
Notification_List: '/api/notifications/',
|
||||||
|
Notification_Ready: (id: number) => `/api/notifications/${id}/read/`,
|
||||||
};
|
};
|
||||||
|
|||||||
30
app.json
@@ -9,19 +9,23 @@
|
|||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"supportsTablet": true,
|
||||||
|
"infoPlist": {
|
||||||
|
"UIBackgroundModes": ["remote-notification"]
|
||||||
|
},
|
||||||
|
"bundleIdentifier": "com.felix.infotarget"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
"useNextNotificationsApi": true,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#E6F4FE",
|
"backgroundColor": "#E6F4FE",
|
||||||
"foregroundImage": "./assets/images/logo.png",
|
"foregroundImage": "./assets/images/logo.png"
|
||||||
"backgroundImage": "./assets/images/logo.png",
|
|
||||||
"monochromeImage": "./assets/images/logo.png"
|
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"predictiveBackGestureEnabled": false,
|
"predictiveBackGestureEnabled": false,
|
||||||
"package": "com.felix.infotarget",
|
"package": "com.felix.infotarget",
|
||||||
"versionCode": 1
|
"versionCode": 1,
|
||||||
|
"googleServicesFile": "./google-services.json"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"output": "static",
|
"output": "static",
|
||||||
@@ -29,6 +33,14 @@
|
|||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
|
[
|
||||||
|
"expo-notifications",
|
||||||
|
{
|
||||||
|
"icon": "./assets/images/notification-icon.png",
|
||||||
|
"color": "#ffffff",
|
||||||
|
"sounds": ["./assets/sounds/notification.wav"]
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"expo-navigation-bar",
|
"expo-navigation-bar",
|
||||||
{
|
{
|
||||||
@@ -59,14 +71,6 @@
|
|||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"router": {},
|
"router": {},
|
||||||
"expo-navigation-bar": {
|
|
||||||
"backgroundColor": "#0f172a",
|
|
||||||
"barStyle": "light",
|
|
||||||
"borderColor": "#1f2937",
|
|
||||||
"visibility": "visible",
|
|
||||||
"behavior": "inset-swipe",
|
|
||||||
"position": "relative"
|
|
||||||
},
|
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "9a281404-9d04-4493-b630-66c35af03ace"
|
"projectId": "9a281404-9d04-4493-b630-66c35af03ace"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import Logo from '@/assets/images/logo.png';
|
||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { RefreshProvider } from '@/components/ui/RefreshContext';
|
import { RefreshProvider } from '@/components/ui/RefreshContext';
|
||||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Tabs } from 'expo-router';
|
import { Tabs } from 'expo-router';
|
||||||
import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native';
|
import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Text } from 'react-native';
|
import { Platform, StyleSheet, Text, View } from 'react-native';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
export default function TabsLayout() {
|
export default function TabsLayout() {
|
||||||
@@ -13,37 +16,53 @@ export default function TabsLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<RefreshProvider>
|
<BottomSheetModalProvider>
|
||||||
<BottomSheetModalProvider>
|
<RefreshProvider>
|
||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
|
headerStyle: {
|
||||||
|
backgroundColor: isDark ? '#0f172a' : '#ffffff',
|
||||||
|
borderBottomWidth: 0.5,
|
||||||
|
borderBottomColor: isDark ? '#334155' : '#e2e8f0',
|
||||||
|
},
|
||||||
|
headerTitleStyle: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: isDark ? '#f1f5f9' : '#0f172a',
|
||||||
|
},
|
||||||
|
headerShadowVisible: false,
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom: 0,
|
bottom: 4,
|
||||||
height: 70,
|
height: 70,
|
||||||
borderTopLeftRadius: 16,
|
paddingTop: 8,
|
||||||
borderTopRightRadius: 16,
|
paddingBottom: 12,
|
||||||
backgroundColor: isDark ? '#0f172a' : '#fff',
|
borderTopLeftRadius: 24,
|
||||||
borderWidth: 1,
|
borderTopRightRadius: 24,
|
||||||
borderColor: isDark ? '#334155' : '#e2e8f0',
|
backgroundColor: isDark ? 'rgba(15, 23, 42, 0.95)' : 'rgba(255, 255, 255, 0.95)', // #0f172a mos fon
|
||||||
shadowColor: '#000',
|
borderWidth: 0.5,
|
||||||
shadowOpacity: isDark ? 0.5 : 0.1,
|
borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(226, 232, 240, 0.8)', // quyuq fon uchun yengil oq
|
||||||
shadowOffset: { width: 0, height: -3 },
|
...Platform.select({
|
||||||
shadowRadius: isDark ? 12 : 10,
|
ios: {
|
||||||
elevation: 8,
|
shadowColor: isDark ? '#0f172a' : '#0f172a', // shadow qora emas #0f172a bilan uyg‘un
|
||||||
overflow: 'hidden',
|
shadowOffset: { width: 0, height: 8 },
|
||||||
|
shadowOpacity: isDark ? 0.5 : 0.15,
|
||||||
|
shadowRadius: 24,
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
elevation: 10,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
overflow: 'visible',
|
||||||
},
|
},
|
||||||
tabBarActiveTintColor: '#3b82f6',
|
tabBarActiveTintColor: isDark ? '#3b82f6' : '#3b82f6', // active ko‘k
|
||||||
tabBarInactiveTintColor: isDark ? '#64748b' : '#94a3b8',
|
tabBarInactiveTintColor: isDark ? '#94a3b8' : '#64748b', // inactive ochiq kulrang
|
||||||
tabBarLabelStyle: { fontSize: 12, fontWeight: '600' },
|
|
||||||
tabBarItemStyle: {
|
tabBarItemStyle: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
justifyContent: 'center',
|
paddingVertical: 4,
|
||||||
alignItems: 'flex-start',
|
|
||||||
alignContent: 'center',
|
|
||||||
},
|
},
|
||||||
tabBarLabelPosition: 'below-icon',
|
tabBarLabelPosition: 'below-icon',
|
||||||
}}
|
}}
|
||||||
@@ -51,93 +70,236 @@ export default function TabsLayout() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: ({ color }) => (
|
title: 'Bosh sahifa',
|
||||||
|
tabBarLabel: ({ color, focused }) => (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
numberOfLines={1}
|
||||||
color: color,
|
style={[
|
||||||
fontSize: 12,
|
styles.tabLabel,
|
||||||
fontWeight: '600',
|
{
|
||||||
textAlign: 'center',
|
marginLeft: 10,
|
||||||
flexWrap: 'wrap',
|
color,
|
||||||
width: 80, // yetarli joy berish
|
fontWeight: focused ? '700' : '600',
|
||||||
}}
|
},
|
||||||
numberOfLines={2} // 2 qatorga sig‘adi
|
]}
|
||||||
>
|
>
|
||||||
{t('Bosh sahifa')}
|
{t('Bosh sahifa')}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
|
tabBarIcon: ({ color, focused }) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.iconContainer,
|
||||||
|
focused && styles.iconContainerActive,
|
||||||
|
{
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Home color={color} size={30} strokeWidth={2} />
|
||||||
|
</View>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="create-announcements"
|
name="create-announcements"
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: ({ focused, color }) => (
|
title: "Qo'shish",
|
||||||
|
tabBarLabel: ({ color, focused }) => (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
numberOfLines={1}
|
||||||
color: color,
|
style={[
|
||||||
fontSize: 12,
|
styles.tabLabel,
|
||||||
fontWeight: '600',
|
{
|
||||||
textAlign: 'center',
|
marginRight: 10,
|
||||||
flexWrap: 'wrap',
|
color,
|
||||||
width: 80, // yetarli joy berish
|
fontWeight: focused ? '700' : '600',
|
||||||
}}
|
},
|
||||||
numberOfLines={2} // 2 qatorga sig‘adi
|
]}
|
||||||
>
|
>
|
||||||
{t("E'lon joylashtirish")}
|
{t("Qo'shish")}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, size }) => <PlusCircle color={color} size={size} />,
|
tabBarIcon: ({ color, focused }) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.iconContainer,
|
||||||
|
focused && styles.iconContainerActive,
|
||||||
|
{
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<PlusCircle color={color} size={30} strokeWidth={2} />
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabs.Screen
|
||||||
|
name="e-services"
|
||||||
|
options={{
|
||||||
|
title: 'Davlat xizmatlari',
|
||||||
|
tabBarLabel: () => null,
|
||||||
|
tabBarItemStyle: {
|
||||||
|
flex: 1.2,
|
||||||
|
top: -10,
|
||||||
|
},
|
||||||
|
tabBarIcon: ({ focused }) => (
|
||||||
|
<View style={styles.centerTabContainer}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={focused ? ['#3b82f6', '#2563eb'] : ['#1e40af', '#1e3a8a']} // dark mod uchun quyuq ko‘k
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 1 }}
|
||||||
|
style={styles.centerTabGradient}
|
||||||
|
>
|
||||||
|
<View style={styles.centerTabInner}>
|
||||||
|
<Image source={Logo} style={{ width: 60, height: 60 }} />
|
||||||
|
</View>
|
||||||
|
</LinearGradient>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1} // 1 qatorda
|
||||||
|
adjustsFontSizeToFit
|
||||||
|
minimumFontScale={0.85}
|
||||||
|
style={[
|
||||||
|
styles.centerTabLabel,
|
||||||
|
{
|
||||||
|
color: focused ? '#3b82f6' : isDark ? '#94a3b8' : '#64748b',
|
||||||
|
fontWeight: focused ? '700' : '600',
|
||||||
|
fontSize: 13, // kattalashtirildi
|
||||||
|
width: 100, // kenglik oshirildi
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t('Davlat xizmatlari')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="announcements"
|
name="announcements"
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: ({ focused, color }) => (
|
title: "E'lonlar",
|
||||||
|
tabBarLabel: ({ color, focused }) => (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
numberOfLines={1}
|
||||||
color: color,
|
style={[
|
||||||
fontSize: 12,
|
styles.tabLabel,
|
||||||
fontWeight: '600',
|
{
|
||||||
textAlign: 'center',
|
marginLeft: 20,
|
||||||
flexWrap: 'wrap',
|
color,
|
||||||
width: 80, // yetarli joy berish
|
fontWeight: focused ? '700' : '600',
|
||||||
}}
|
},
|
||||||
numberOfLines={2} // 2 qatorga sig‘adi
|
]}
|
||||||
>
|
>
|
||||||
{t("E'lonlar")}
|
{t("E'lonlar")}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, size }) => <Megaphone color={color} size={size} />,
|
tabBarIcon: ({ color, focused }) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.iconContainer,
|
||||||
|
focused && styles.iconContainerActive,
|
||||||
|
{
|
||||||
|
marginLeft: 20,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Megaphone color={color} size={30} strokeWidth={2} />
|
||||||
|
</View>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="profile"
|
name="profile"
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: ({ focused, color }) => (
|
title: 'Profil',
|
||||||
|
tabBarLabel: ({ color, focused }) => (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
numberOfLines={1}
|
||||||
color: color,
|
style={[
|
||||||
fontSize: 12,
|
styles.tabLabel,
|
||||||
fontWeight: '600',
|
{
|
||||||
textAlign: 'center',
|
marginRight: 20,
|
||||||
flexWrap: 'wrap',
|
color,
|
||||||
width: 80, // yetarli joy berish
|
fontWeight: focused ? '700' : '600',
|
||||||
}}
|
},
|
||||||
numberOfLines={2} // 2 qatorga sig‘adi
|
]}
|
||||||
>
|
>
|
||||||
{t('Profil')}
|
{t('Profil')}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, size }) => <User color={color} size={size} />,
|
tabBarIcon: ({ color, focused }) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.iconContainer,
|
||||||
|
focused && styles.iconContainerActive,
|
||||||
|
{ marginRight: 20 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<User color={color} size={30} strokeWidth={2} />
|
||||||
|
</View>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</BottomSheetModalProvider>
|
</RefreshProvider>
|
||||||
</RefreshProvider>
|
</BottomSheetModalProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
tabLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
paddingVertical: 4,
|
||||||
|
},
|
||||||
|
iconContainerActive: {
|
||||||
|
transform: [{ scale: 1.05 }],
|
||||||
|
},
|
||||||
|
centerTabContainer: {
|
||||||
|
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,
|
||||||
|
borderRadius: 34,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
centerTabLabel: {
|
||||||
|
marginTop: 8,
|
||||||
|
fontSize: 11,
|
||||||
|
textAlign: 'center',
|
||||||
|
maxWidth: 110,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useTheme } from '@/components/ThemeContext';
|
|||||||
import { FilterProvider } from '@/components/ui/FilterContext';
|
import { FilterProvider } from '@/components/ui/FilterContext';
|
||||||
import { CustomHeader } from '@/components/ui/Header';
|
import { CustomHeader } from '@/components/ui/Header';
|
||||||
import DashboardScreen from '@/screens/announcements/ui/AnnouncementsList';
|
import DashboardScreen from '@/screens/announcements/ui/AnnouncementsList';
|
||||||
import { Stack } from 'expo-router';
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
export default function Announcements() {
|
export default function Announcements() {
|
||||||
@@ -10,10 +9,9 @@ export default function Announcements() {
|
|||||||
return (
|
return (
|
||||||
<FilterProvider>
|
<FilterProvider>
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 50 }}
|
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 80 }}
|
||||||
>
|
>
|
||||||
<CustomHeader />
|
<CustomHeader />
|
||||||
<Stack.Screen options={{ title: "E'lonlar" }} />
|
|
||||||
<DashboardScreen />
|
<DashboardScreen />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</FilterProvider>
|
</FilterProvider>
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export default function CreateAnnouncements() {
|
|||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
return (
|
return (
|
||||||
<FilterProvider>
|
<FilterProvider>
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff' }}>
|
<SafeAreaView
|
||||||
|
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 30 }}
|
||||||
|
>
|
||||||
<CustomHeader />
|
<CustomHeader />
|
||||||
<CreateAdsScreens />
|
<CreateAdsScreens />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
17
app/(dashboard)/e-services.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
|
import { CustomHeader } from '@/components/ui/Header';
|
||||||
|
import EServicesScreen from '@/screens/e-services/ui/EServices';
|
||||||
|
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 }}
|
||||||
|
edges={['top']}
|
||||||
|
>
|
||||||
|
<CustomHeader />
|
||||||
|
<EServicesScreen />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,9 @@ export default function Index() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterProvider>
|
<FilterProvider>
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff' }}>
|
<SafeAreaView
|
||||||
|
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 85 }}
|
||||||
|
>
|
||||||
<CustomHeader />
|
<CustomHeader />
|
||||||
<HomeScreen />
|
<HomeScreen />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ export default function ProfileScreen() {
|
|||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 30 }}
|
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 70 }}
|
||||||
edges={['top']}
|
edges={['top']}
|
||||||
>
|
>
|
||||||
<CustomHeader logoutbtn={true} />
|
<CustomHeader logoutbtn={true} notif={false} />
|
||||||
<Profile />
|
<Profile />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import { AuthProvider } from '@/components/AuthProvider';
|
import { AuthProvider } from '@/components/AuthProvider';
|
||||||
import QueryProvider from '@/components/QueryProvider';
|
import QueryProvider from '@/components/QueryProvider';
|
||||||
import { ThemeProvider } from '@/components/ThemeContext';
|
import { ThemeProvider, useTheme } from '@/components/ThemeContext';
|
||||||
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
import i18n from '@/i18n/i18n';
|
import i18n from '@/i18n/i18n';
|
||||||
import { ProfileDataProvider } from '@/screens/profile/lib/ProfileDataContext';
|
import { ProfileDataProvider } from '@/screens/profile/lib/ProfileDataContext';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import { StatusBar } from 'react-native';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
useNotifications();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack screenOptions={{ headerShown: false }} />
|
<Stack screenOptions={{ headerShown: false }} />
|
||||||
<StatusBar style={'light'} />
|
<StatusBar
|
||||||
|
barStyle={isDark ? 'light-content' : 'dark-content'}
|
||||||
|
backgroundColor={isDark ? '#0f172a' : '#ffffff'}
|
||||||
|
translucent={false}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
9
app/profile/added-referalls.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import CreateReferrals from '@/screens/profile/ui/CreateReferrals';
|
||||||
|
|
||||||
|
export default function AddEmployeeScreen() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateReferrals />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/profile/manual.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
|
import { ManualTab } from '@/screens/profile/ui/ManualTab';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
export default function MyAds() {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
|
<ManualTab />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/profile/my-referrals.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
|
import { ReferralsTab } from '@/screens/profile/ui/RefferallsTab';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
export default function MyReffrals() {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
|
<ReferralsTab />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/profile/notification.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
|
import { NotificationTab } from '@/screens/profile/ui/NotificationTab';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
|
export default function MyAds() {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
|
<NotificationTab />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
@@ -92,6 +93,8 @@ export default function PersonalInfoScreen() {
|
|||||||
stir: editData.data.stir,
|
stir: editData.data.stir,
|
||||||
director_full_name: editData.data.director_full_name,
|
director_full_name: editData.data.director_full_name,
|
||||||
address: editData.data.address,
|
address: editData.data.address,
|
||||||
|
gender: editData.data.gender,
|
||||||
|
age: editData.data.age,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,7 +120,13 @@ export default function PersonalInfoScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== EDIT MODE ===================== */
|
/* ===================== EDIT MODE ===================== */
|
||||||
|
const [showGenderOptions, setShowGenderOptions] = useState(false);
|
||||||
if (isEditing && editData) {
|
if (isEditing && editData) {
|
||||||
|
const genderOptions: { label: string; value: 'male' | 'female' }[] = [
|
||||||
|
{ label: t('Erkak'), value: 'male' },
|
||||||
|
{ label: t('Ayol'), value: 'female' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
@@ -136,13 +145,18 @@ export default function PersonalInfoScreen() {
|
|||||||
|
|
||||||
<ScrollView style={styles.content}>
|
<ScrollView style={styles.content}>
|
||||||
<View style={styles.editSection}>
|
<View style={styles.editSection}>
|
||||||
|
{/* First Name */}
|
||||||
<Text style={[styles.label, { color: theme.textSecondary }]}>{t('Ism')}</Text>
|
<Text style={[styles.label, { color: theme.textSecondary }]}>{t('Ism')}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, { backgroundColor: theme.inputBg, color: theme.text }]}
|
style={[styles.input, { backgroundColor: theme.inputBg, color: theme.text }]}
|
||||||
value={editData?.data.first_name}
|
value={editData?.data.first_name}
|
||||||
onChangeText={(text) => setEditData((prev) => prev && { ...prev, first_name: text })}
|
onChangeText={(text) =>
|
||||||
|
setEditData((prev) => prev && { ...prev, data: { ...prev.data, first_name: text } })
|
||||||
|
}
|
||||||
placeholderTextColor={theme.placeholder}
|
placeholderTextColor={theme.placeholder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Phone */}
|
||||||
<Text style={[styles.label, { color: theme.textSecondary }]}>
|
<Text style={[styles.label, { color: theme.textSecondary }]}>
|
||||||
{t('Telefon raqami')}
|
{t('Telefon raqami')}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -161,18 +175,59 @@ export default function PersonalInfoScreen() {
|
|||||||
onChangeText={(text) => setPhone(normalizeDigits(text))}
|
onChangeText={(text) => setPhone(normalizeDigits(text))}
|
||||||
onFocus={() => setFocused(true)}
|
onFocus={() => setFocused(true)}
|
||||||
onBlur={() => setFocused(false)}
|
onBlur={() => setFocused(false)}
|
||||||
keyboardType="phone-pad"
|
keyboardType="numeric"
|
||||||
placeholder="90 123 45 67"
|
placeholder="90 123 45 67"
|
||||||
maxLength={12}
|
maxLength={12}
|
||||||
placeholderTextColor={theme.placeholder}
|
placeholderTextColor={theme.placeholder}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Age */}
|
||||||
|
<Text style={[styles.label, { color: theme.textSecondary }]}>{t('Yoshi')}</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, { backgroundColor: theme.inputBg, color: theme.text }]}
|
||||||
|
value={editData?.data.age?.toString() || ''}
|
||||||
|
placeholder="18"
|
||||||
|
keyboardType="numeric"
|
||||||
|
onChangeText={(text) =>
|
||||||
|
setEditData(
|
||||||
|
(prev) => prev && { ...prev, data: { ...prev.data, age: Number(text) } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholderTextColor={theme.placeholder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Gender as buttons */}
|
||||||
|
<Text style={[styles.label, { color: theme.textSecondary }]}>{t('Jinsi')}</Text>
|
||||||
|
<View style={{ flexDirection: 'row', gap: 12, marginTop: 4 }}>
|
||||||
|
{genderOptions.map((option) => {
|
||||||
|
const isSelected = editData.data.gender === option.value;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={option.value}
|
||||||
|
onPress={() =>
|
||||||
|
setEditData((prev) =>
|
||||||
|
prev ? { ...prev, data: { ...prev.data, gender: option.value } } : prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: isSelected ? theme.primary : theme.inputBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: isSelected ? '#fff' : theme.text }}>{option.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== VIEW MODE ===================== */
|
/* ===================== VIEW MODE ===================== */
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
@@ -191,6 +246,16 @@ export default function PersonalInfoScreen() {
|
|||||||
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>{t('Ism')}</Text>
|
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>{t('Ism')}</Text>
|
||||||
<Text style={[styles.infoValue, { color: theme.text }]}>{me?.data.data.first_name}</Text>
|
<Text style={[styles.infoValue, { color: theme.text }]}>{me?.data.data.first_name}</Text>
|
||||||
|
|
||||||
|
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>{t('Yoshi')}</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: theme.text }]}>
|
||||||
|
{me?.data.data.age ? me?.data.data.age : t("Noma'lum")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>{t('Jinsi')}</Text>
|
||||||
|
<Text style={[styles.infoValue, { color: theme.text }]}>
|
||||||
|
{me?.data.data.gender ? t(me?.data.data.gender) : t("Noma'lum")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>
|
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>
|
||||||
{t('Telefon raqami')}
|
{t('Telefon raqami')}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -267,7 +332,7 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
fieldsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
fieldsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 20 },
|
||||||
fieldChip: {
|
fieldChip: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
|
|||||||
BIN
assets/images/navbar.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/manual/manual_video_en.mp4
Normal file
BIN
assets/manual/manual_video_ru.mp4
Normal file
BIN
assets/manual/manual_video_uz.mp4
Normal file
BIN
assets/manual/photo_8_2026-01-30_17-15-31.jpg
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
assets/manual/photo_9_2026-01-30_17-15-31.jpg
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
assets/manual/step1.jpg
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
assets/manual/step2.jpg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
assets/manual/step3.jpg
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
assets/manual/step4.jpg
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
assets/manual/step5.jpg
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
assets/manual/step6.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
assets/manual/step7.jpg
Normal file
|
After Width: | Height: | Size: 181 KiB |
32
components/AuthContext.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// components/AuthContext.tsx
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
accessToken: string | null;
|
||||||
|
setAccessToken: (token: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType>({
|
||||||
|
accessToken: null,
|
||||||
|
setAccessToken: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// AsyncStorage dan tokenni yuklash
|
||||||
|
useEffect(() => {
|
||||||
|
const loadToken = async () => {
|
||||||
|
const token = await AsyncStorage.getItem('access_token');
|
||||||
|
if (token) setAccessToken(token);
|
||||||
|
};
|
||||||
|
loadToken();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ accessToken, setAccessToken }}>{children}</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => useContext(AuthContext);
|
||||||
67
components/NotificationProvider.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import * as Device from 'expo-device';
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
Notifications.setNotificationHandler({
|
||||||
|
handleNotification: async () => ({
|
||||||
|
shouldPlaySound: true,
|
||||||
|
shouldShowAlert: true,
|
||||||
|
shouldSetBadge: true,
|
||||||
|
shouldShowBanner: true,
|
||||||
|
shouldShowList: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function registerForPushNotificationsAsync() {
|
||||||
|
let token;
|
||||||
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
await Notifications.setNotificationChannelAsync('default', {
|
||||||
|
name: 'default',
|
||||||
|
importance: Notifications.AndroidImportance.MAX,
|
||||||
|
vibrationPattern: [0, 250, 250, 250],
|
||||||
|
lightColor: '#FF231F7C',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Device.isDevice) {
|
||||||
|
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||||
|
let finalStatus = existingStatus;
|
||||||
|
|
||||||
|
if (existingStatus !== 'granted') {
|
||||||
|
const { status } = await Notifications.requestPermissionsAsync();
|
||||||
|
finalStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalStatus !== 'granted') {
|
||||||
|
console.log('Notification uchun ruxsat berilmadi!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
token = (
|
||||||
|
await Notifications.getExpoPushTokenAsync({
|
||||||
|
projectId: '9a281404-9d04-4493-b630-66c35af03ace',
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
console.log('Push Token:', token);
|
||||||
|
} else {
|
||||||
|
console.log('Push notification faqat real qurilmalarda ishlaydi!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendTestNotification() {
|
||||||
|
await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: 'Test xabar 📬',
|
||||||
|
body: 'Bu test notification! Notification tizimi muvaffaqiyatli ishlayapti.',
|
||||||
|
data: { screen: 'monitoring', type: 'test' },
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
|
||||||
|
seconds: 3, // 2 o'rniga 3
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { products_api } from '@/screens/home/lib/api';
|
import { products_api } from '@/screens/home/lib/api';
|
||||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
import { Building2, ChevronDown, ChevronUp } from 'lucide-react-native';
|
import { Building2, ChevronDown, ChevronUp } from 'lucide-react-native';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -74,7 +75,7 @@ export default function CountriesList({ search }: { search: string }) {
|
|||||||
<FlatList
|
<FlatList
|
||||||
data={allCountries}
|
data={allCountries}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
contentContainerStyle={{ gap: 12 }}
|
contentContainerStyle={{ gap: 5 }}
|
||||||
onEndReached={loadMore}
|
onEndReached={loadMore}
|
||||||
onEndReachedThreshold={0.4}
|
onEndReachedThreshold={0.4}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
@@ -83,7 +84,7 @@ export default function CountriesList({ search }: { search: string }) {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
renderItem={({ item }) => {
|
renderItem={({ item }) => {
|
||||||
const isOpen = openedCountryId === item.id;
|
const isOpen = openedCountryId === item.id;
|
||||||
|
const flagCode = item.flag ? item.flag.toLowerCase() : ''; // "uz"
|
||||||
return (
|
return (
|
||||||
<View style={[styles.countryCard, isDark ? styles.darkCard : styles.lightCard]}>
|
<View style={[styles.countryCard, isDark ? styles.darkCard : styles.lightCard]}>
|
||||||
{/* Davlat sarlavhasi */}
|
{/* Davlat sarlavhasi */}
|
||||||
@@ -92,9 +93,24 @@ export default function CountriesList({ search }: { search: string }) {
|
|||||||
onPress={() => toggleAccordion(item.id)}
|
onPress={() => toggleAccordion(item.id)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text style={[styles.countryName, isDark ? styles.darkText : styles.lightText]}>
|
<View
|
||||||
{item.name}
|
style={{
|
||||||
</Text>
|
flexDirection: 'row',
|
||||||
|
gap: 10,
|
||||||
|
alignContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: `https://flagcdn.com/w320/${flagCode}.png` }}
|
||||||
|
style={{ width: 40, height: 20 }}
|
||||||
|
resizeMode="cover" // objectFit o'rniga resizeMode ishlatildi
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={[styles.countryName, isDark ? styles.darkText : styles.lightText]}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
<View style={styles.rightSide}>
|
<View style={styles.rightSide}>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.companyCount, isDark ? styles.darkSubText : styles.lightSubText]}
|
style={[styles.companyCount, isDark ? styles.darkSubText : styles.lightSubText]}
|
||||||
@@ -166,6 +182,7 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 6,
|
shadowRadius: 6,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
marginLeft: 2,
|
marginLeft: 2,
|
||||||
|
marginBottom: 5,
|
||||||
},
|
},
|
||||||
darkCard: {
|
darkCard: {
|
||||||
backgroundColor: '#1e293b',
|
backgroundColor: '#1e293b',
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { products_api } from '@/screens/home/lib/api';
|
import { products_api } from '@/screens/home/lib/api';
|
||||||
|
import BottomSheet, {
|
||||||
|
BottomSheetBackdrop,
|
||||||
|
BottomSheetFlatList,
|
||||||
|
BottomSheetScrollView,
|
||||||
|
} from '@gorhom/bottom-sheet';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { XIcon } from 'lucide-react-native';
|
import { Image } from 'expo-image';
|
||||||
import React, { Dispatch, SetStateAction, useMemo, useState } from 'react';
|
import { CheckIcon, ChevronRight, XIcon } from 'lucide-react-native';
|
||||||
|
import React, { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
ActivityIndicator,
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native';
|
|
||||||
import CategorySelect from './CategorySelect';
|
import CategorySelect from './CategorySelect';
|
||||||
|
|
||||||
interface FilterUIProps {
|
interface FilterUIProps {
|
||||||
@@ -32,9 +32,32 @@ interface Category {
|
|||||||
icon_name: string | null;
|
icon_name: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Country {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
region: Region[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Region {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
districts: District[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface District {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SheetType = 'country' | 'region' | 'district' | 'category' | null;
|
||||||
|
|
||||||
export default function FilterUI({ back, onApply, setStep, setFiltered }: FilterUIProps) {
|
export default function FilterUI({ back, onApply, setStep, setFiltered }: FilterUIProps) {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const bottomSheetRef = useRef<BottomSheet>(null);
|
||||||
|
const snapPoints = useMemo(() => ['60%', '85%'], []);
|
||||||
|
|
||||||
|
const [activeSheet, setActiveSheet] = useState<SheetType>(null);
|
||||||
const [selectedCategories, setSelectedCategories] = useState<Category | null>(null);
|
const [selectedCategories, setSelectedCategories] = useState<Category | null>(null);
|
||||||
const [selectedCountry, setSelectedCountry] = useState<string>('all');
|
const [selectedCountry, setSelectedCountry] = useState<string>('all');
|
||||||
const [selectedRegion, setSelectedRegion] = useState<string>('all');
|
const [selectedRegion, setSelectedRegion] = useState<string>('all');
|
||||||
@@ -46,7 +69,7 @@ export default function FilterUI({ back, onApply, setStep, setFiltered }: Filter
|
|||||||
select: (res) => res.data?.data || [],
|
select: (res) => res.data?.data || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: (params: any) => products_api.businessAbout(params),
|
mutationFn: (params: any) => products_api.businessAbout(params),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setStep('items');
|
setStep('items');
|
||||||
@@ -80,177 +103,409 @@ export default function FilterUI({ back, onApply, setStep, setFiltered }: Filter
|
|||||||
return region?.districts || [];
|
return region?.districts || [];
|
||||||
}, [regions, selectedRegion]);
|
}, [regions, selectedRegion]);
|
||||||
|
|
||||||
|
const openSheet = useCallback((type: SheetType) => {
|
||||||
|
setActiveSheet(type);
|
||||||
|
setTimeout(() => {
|
||||||
|
bottomSheetRef.current?.snapToIndex(0);
|
||||||
|
}, 100);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeSheet = useCallback(() => {
|
||||||
|
bottomSheetRef.current?.close();
|
||||||
|
setTimeout(() => setActiveSheet(null), 300);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderBackdrop = useCallback(
|
||||||
|
(props: any) => (
|
||||||
|
<BottomSheetBackdrop
|
||||||
|
{...props}
|
||||||
|
appearsOnIndex={0}
|
||||||
|
disappearsOnIndex={-1}
|
||||||
|
opacity={0.6}
|
||||||
|
pressBehavior="close"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSelectedLabel = useCallback(
|
||||||
|
(type: SheetType) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'country':
|
||||||
|
if (selectedCountry === 'all') return t('Barchasi');
|
||||||
|
return (
|
||||||
|
countryResponse?.find((c) => c.id?.toString() === selectedCountry)?.name || t('Tanlang')
|
||||||
|
);
|
||||||
|
case 'region':
|
||||||
|
if (selectedRegion === 'all') return t('Barchasi');
|
||||||
|
return regions.find((r) => r.id?.toString() === selectedRegion)?.name || t('Tanlang');
|
||||||
|
case 'district':
|
||||||
|
if (selectedDistrict === 'all') return t('Barchasi');
|
||||||
|
return districts.find((d) => d.id?.toString() === selectedDistrict)?.name || t('Tanlang');
|
||||||
|
case 'category':
|
||||||
|
return selectedCategories?.name || t('Tanlang');
|
||||||
|
default:
|
||||||
|
return t('Tanlang');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
selectedCountry,
|
||||||
|
selectedRegion,
|
||||||
|
selectedDistrict,
|
||||||
|
selectedCategories,
|
||||||
|
countryResponse,
|
||||||
|
regions,
|
||||||
|
districts,
|
||||||
|
t,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const FilterButton = useCallback(
|
||||||
|
({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onPress,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onPress: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.filterBtn,
|
||||||
|
isDark ? styles.darkFilterBtn : styles.lightFilterBtn,
|
||||||
|
disabled && styles.disabledBtn,
|
||||||
|
]}
|
||||||
|
onPress={onPress}
|
||||||
|
disabled={disabled}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.filterBtnContent}>
|
||||||
|
<Text style={[styles.filterLabel, isDark ? styles.darkText : styles.lightText]}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.filterValueContainer}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.filterValue,
|
||||||
|
isDark ? styles.darkValueText : styles.lightValueText,
|
||||||
|
disabled && styles.disabledText,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
<ChevronRight size={20} color={isDark ? '#94a3b8' : '#64748b'} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
),
|
||||||
|
[isDark]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderListItem = useCallback(
|
||||||
|
({
|
||||||
|
item,
|
||||||
|
onSelect,
|
||||||
|
selectedId,
|
||||||
|
}: {
|
||||||
|
item: any;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
selectedId: string;
|
||||||
|
}) => {
|
||||||
|
const isSelected = selectedId === (item.id?.toString() || 'all');
|
||||||
|
const flagCode = item.flag ? item.flag.toLowerCase() : ''; // "uz"
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.listItem,
|
||||||
|
isDark ? styles.darkListItem : styles.lightListItem,
|
||||||
|
isSelected && styles.selectedListItem,
|
||||||
|
]}
|
||||||
|
onPress={() => onSelect(item.id?.toString() || 'all')}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 10,
|
||||||
|
alignContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.flag && (
|
||||||
|
<Image
|
||||||
|
source={{ uri: `https://flagcdn.com/w320/${flagCode}.png` }}
|
||||||
|
style={{ width: 34, height: 20 }}
|
||||||
|
resizeMode="cover" // objectFit o'rniga resizeMode ishlatildi
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.listItemText,
|
||||||
|
isDark ? styles.darkText : styles.lightText,
|
||||||
|
isSelected && styles.selectedListItemText,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{isSelected && (
|
||||||
|
<View style={styles.checkmark}>
|
||||||
|
<CheckIcon color={'#3b82f6'} strokeWidth={'2.5'} size={18} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[isDark]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSheetContent = useCallback(() => {
|
||||||
|
if (activeSheet === 'category') {
|
||||||
|
return (
|
||||||
|
<BottomSheetScrollView
|
||||||
|
contentContainerStyle={styles.scrollViewContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={true}
|
||||||
|
>
|
||||||
|
<CategorySelect
|
||||||
|
selectedCategories={selectedCategories}
|
||||||
|
setSelectedCategories={setSelectedCategories}
|
||||||
|
/>
|
||||||
|
</BottomSheetScrollView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: any[] = [];
|
||||||
|
let onSelect: (id: string) => void = () => {};
|
||||||
|
let selectedId = '';
|
||||||
|
|
||||||
|
switch (activeSheet) {
|
||||||
|
case 'country':
|
||||||
|
data = [{ id: 'all', name: t('Barchasi') }, ...(countryResponse || [])];
|
||||||
|
onSelect = (id) => {
|
||||||
|
setSelectedCountry(id);
|
||||||
|
setSelectedRegion('all');
|
||||||
|
setSelectedDistrict('all');
|
||||||
|
closeSheet();
|
||||||
|
};
|
||||||
|
selectedId = selectedCountry;
|
||||||
|
break;
|
||||||
|
case 'region':
|
||||||
|
data = [{ id: 'all', name: t('Barchasi') }, ...regions];
|
||||||
|
onSelect = (id) => {
|
||||||
|
setSelectedRegion(id);
|
||||||
|
setSelectedDistrict('all');
|
||||||
|
closeSheet();
|
||||||
|
};
|
||||||
|
selectedId = selectedRegion;
|
||||||
|
break;
|
||||||
|
case 'district':
|
||||||
|
data = [{ id: 'all', name: t('Barchasi') }, ...districts];
|
||||||
|
onSelect = (id) => {
|
||||||
|
setSelectedDistrict(id);
|
||||||
|
closeSheet();
|
||||||
|
};
|
||||||
|
selectedId = selectedDistrict;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BottomSheetFlatList
|
||||||
|
data={data}
|
||||||
|
keyExtractor={(item: any) => item.id?.toString() || 'all'}
|
||||||
|
contentContainerStyle={styles.listContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={true}
|
||||||
|
overScrollMode="always"
|
||||||
|
renderItem={({ item }: { item: any }) => renderListItem({ item, onSelect, selectedId })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
activeSheet,
|
||||||
|
selectedCategories,
|
||||||
|
countryResponse,
|
||||||
|
regions,
|
||||||
|
districts,
|
||||||
|
selectedCountry,
|
||||||
|
selectedRegion,
|
||||||
|
selectedDistrict,
|
||||||
|
t,
|
||||||
|
closeSheet,
|
||||||
|
renderListItem,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getSheetTitle = useCallback(() => {
|
||||||
|
switch (activeSheet) {
|
||||||
|
case 'country':
|
||||||
|
return t('Davlat');
|
||||||
|
case 'region':
|
||||||
|
return t('Viloyat');
|
||||||
|
case 'district':
|
||||||
|
return t('Tuman');
|
||||||
|
case 'category':
|
||||||
|
return t('Sohalar');
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [activeSheet, t]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||||
style={[
|
<View style={styles.loadingContainer}>
|
||||||
styles.container,
|
<ActivityIndicator size="large" color="#3b82f6" />
|
||||||
isDark ? styles.darkBg : styles.lightBg,
|
</View>
|
||||||
{ justifyContent: 'center', alignItems: 'center' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<ActivityIndicator size="large" color="#3b82f6" />
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single Tag Component
|
|
||||||
const Tag = ({
|
|
||||||
label,
|
|
||||||
selected,
|
|
||||||
onPress,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
selected: boolean;
|
|
||||||
onPress: () => void;
|
|
||||||
}) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[
|
|
||||||
styles.tag,
|
|
||||||
isDark ? styles.darkTag : styles.lightTag,
|
|
||||||
selected && styles.tagSelected,
|
|
||||||
]}
|
|
||||||
onPress={onPress}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.tagText,
|
|
||||||
isDark ? styles.darkTagText : styles.lightTagText,
|
|
||||||
selected && styles.tagTextSelected,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
{/* Header */}
|
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||||
<View style={styles.btn}>
|
{/* Header */}
|
||||||
<TouchableOpacity
|
<View style={[styles.header, { borderBottomColor: isDark ? '#334155' : '#e2e8f0' }]}>
|
||||||
onPress={back}
|
<Text style={[styles.headerTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||||
style={[styles.backBtn, isDark ? styles.darkBackBtn : styles.lightBackBtn]}
|
{t('Filter')}
|
||||||
>
|
</Text>
|
||||||
<XIcon color={isDark ? 'white' : '#0f172a'} fontSize={24} />
|
<TouchableOpacity
|
||||||
</TouchableOpacity>
|
onPress={back}
|
||||||
</View>
|
style={[styles.closeBtn, isDark ? styles.darkCloseBtn : styles.lightCloseBtn]}
|
||||||
|
>
|
||||||
|
<XIcon color={isDark ? '#f1f5f9' : '#0f172a'} size={20} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Filter Options */}
|
||||||
<ScrollView
|
<View style={styles.content}>
|
||||||
style={{ padding: 16 }}
|
<FilterButton
|
||||||
showsVerticalScrollIndicator={false}
|
label={t('Davlat')}
|
||||||
contentContainerStyle={{ paddingBottom: 140 }}
|
value={getSelectedLabel('country')}
|
||||||
>
|
onPress={() => openSheet('country')}
|
||||||
{/* Country Filter */}
|
|
||||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
|
||||||
{t('Davlat')}
|
|
||||||
</Text>
|
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.scrollRow}>
|
|
||||||
<Tag
|
|
||||||
label={t('Barchasi')}
|
|
||||||
selected={selectedCountry === 'all'}
|
|
||||||
onPress={() => setSelectedCountry('all')}
|
|
||||||
/>
|
/>
|
||||||
{countryResponse?.map((c) => (
|
|
||||||
<Tag
|
|
||||||
key={c.id}
|
|
||||||
label={c.name}
|
|
||||||
selected={selectedCountry === c.id?.toString()}
|
|
||||||
onPress={() => {
|
|
||||||
setSelectedCountry(c.id?.toString() || 'all');
|
|
||||||
setSelectedRegion('all');
|
|
||||||
setSelectedDistrict('all');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{/* Region Filter */}
|
<FilterButton
|
||||||
{regions.length > 0 && (
|
label={t('Viloyat')}
|
||||||
<>
|
value={getSelectedLabel('region')}
|
||||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
onPress={() => openSheet('region')}
|
||||||
{t('Viloyat')}
|
disabled={selectedCountry === 'all' || regions.length === 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FilterButton
|
||||||
|
label={t('Tuman')}
|
||||||
|
value={getSelectedLabel('district')}
|
||||||
|
onPress={() => openSheet('district')}
|
||||||
|
disabled={selectedRegion === 'all' || districts.length === 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FilterButton
|
||||||
|
label={t('Sohalar')}
|
||||||
|
value={getSelectedLabel('category')}
|
||||||
|
onPress={() => openSheet('category')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Apply Button */}
|
||||||
|
<View style={[styles.applyBtnWrapper, { borderTopColor: isDark ? '#334155' : '#e2e8f0' }]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.applyBtn, isPending && styles.applyBtnDisabled]}
|
||||||
|
onPress={handleApply}
|
||||||
|
disabled={isPending}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<ActivityIndicator color="#ffffff" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.applyBtnText}>{t("Natijalarni ko'rish")}</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom Sheet */}
|
||||||
|
<BottomSheet
|
||||||
|
ref={bottomSheetRef}
|
||||||
|
index={-1}
|
||||||
|
snapPoints={snapPoints}
|
||||||
|
enablePanDownToClose={true}
|
||||||
|
enableDynamicSizing={false}
|
||||||
|
enableOverDrag={false}
|
||||||
|
onClose={() => setActiveSheet(null)}
|
||||||
|
backdropComponent={renderBackdrop}
|
||||||
|
backgroundStyle={[styles.bottomSheetBackground, isDark ? styles.darkBg : styles.lightBg]}
|
||||||
|
handleIndicatorStyle={[
|
||||||
|
styles.handleIndicator,
|
||||||
|
isDark ? styles.darkHandleIndicator : styles.lightHandleIndicator,
|
||||||
|
]}
|
||||||
|
android_keyboardInputMode="adjustResize"
|
||||||
|
keyboardBehavior="interactive"
|
||||||
|
keyboardBlurBehavior="restore"
|
||||||
|
>
|
||||||
|
<View style={[styles.sheetHeader, { borderBottomColor: isDark ? '#334155' : '#e2e8f0' }]}>
|
||||||
|
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||||
|
{getSheetTitle()}
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.scrollRow}>
|
<TouchableOpacity onPress={closeSheet} style={styles.sheetCloseBtn}>
|
||||||
<Tag
|
<XIcon color={isDark ? '#94a3b8' : '#64748b'} size={20} />
|
||||||
label={t('Barchasi')}
|
</TouchableOpacity>
|
||||||
selected={selectedRegion === 'all'}
|
</View>
|
||||||
onPress={() => setSelectedRegion('all')}
|
{renderSheetContent()}
|
||||||
/>
|
</BottomSheet>
|
||||||
{regions.map((r) => (
|
|
||||||
<Tag
|
|
||||||
key={r.id}
|
|
||||||
label={r.name}
|
|
||||||
selected={selectedRegion === r.id?.toString()}
|
|
||||||
onPress={() => {
|
|
||||||
setSelectedRegion(r.id?.toString() || 'all');
|
|
||||||
setSelectedDistrict('all');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* District Filter */}
|
|
||||||
{districts.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
|
||||||
{t('Tuman')}
|
|
||||||
</Text>
|
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.scrollRow}>
|
|
||||||
<Tag
|
|
||||||
label={t('Barchasi')}
|
|
||||||
selected={selectedDistrict === 'all'}
|
|
||||||
onPress={() => setSelectedDistrict('all')}
|
|
||||||
/>
|
|
||||||
{districts.map((d) => (
|
|
||||||
<Tag
|
|
||||||
key={d.id}
|
|
||||||
label={d.name}
|
|
||||||
selected={selectedDistrict === d.id?.toString()}
|
|
||||||
onPress={() => setSelectedDistrict(d.id?.toString() || 'all')}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Industry Selection */}
|
|
||||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
|
||||||
{t('Sohalar')}
|
|
||||||
</Text>
|
|
||||||
<CategorySelect
|
|
||||||
selectedCategories={selectedCategories}
|
|
||||||
setSelectedCategories={setSelectedCategories}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
{/* Fixed Apply Button */}
|
|
||||||
<View style={styles.applyBtnWrapper}>
|
|
||||||
<TouchableOpacity style={styles.applyBtn} onPress={handleApply}>
|
|
||||||
<Text style={styles.applyBtnText}>{t("Natijalarni ko'rish")}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1 },
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
darkBg: {
|
darkBg: {
|
||||||
backgroundColor: '#0f172a',
|
backgroundColor: '#0f172a',
|
||||||
},
|
},
|
||||||
lightBg: {
|
lightBg: {
|
||||||
backgroundColor: '#f8fafc',
|
backgroundColor: '#f8fafc',
|
||||||
},
|
},
|
||||||
backBtn: {
|
header: {
|
||||||
paddingHorizontal: 10,
|
flexDirection: 'row',
|
||||||
paddingVertical: 10,
|
justifyContent: 'space-between',
|
||||||
borderRadius: 10,
|
alignItems: 'center',
|
||||||
marginTop: 10,
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
closeBtn: {
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
darkCloseBtn: {
|
||||||
|
backgroundColor: '#1e293b',
|
||||||
|
borderColor: '#334155',
|
||||||
|
},
|
||||||
|
lightCloseBtn: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderColor: '#e2e8f0',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 20,
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
filterBtn: {
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
@@ -258,81 +513,56 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
},
|
},
|
||||||
darkBackBtn: {
|
darkFilterBtn: {
|
||||||
backgroundColor: '#1e293b',
|
backgroundColor: '#1e293b',
|
||||||
borderColor: '#334155',
|
borderColor: '#334155',
|
||||||
},
|
},
|
||||||
lightBackBtn: {
|
lightFilterBtn: {
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
borderColor: '#e2e8f0',
|
borderColor: '#e2e8f0',
|
||||||
},
|
},
|
||||||
btn: {
|
disabledBtn: {
|
||||||
justifyContent: 'flex-end',
|
opacity: 0.5,
|
||||||
alignItems: 'flex-end',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
filterBtnContent: {
|
||||||
fontSize: 16,
|
flexDirection: 'column',
|
||||||
fontWeight: '700',
|
gap: 8, // 4 o'rniga 8 qildim
|
||||||
marginBottom: 10,
|
|
||||||
},
|
},
|
||||||
|
filterLabel: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 2, // 4 o'rniga 2 qildim
|
||||||
|
},
|
||||||
|
filterValueContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8, // ChevronRight va text orasida gap
|
||||||
|
},
|
||||||
|
filterValue: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '500',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
|
||||||
darkText: {
|
darkText: {
|
||||||
color: '#f1f5f9',
|
color: '#f1f5f9',
|
||||||
},
|
},
|
||||||
lightText: {
|
lightText: {
|
||||||
color: '#0f172a',
|
color: '#0f172a',
|
||||||
},
|
},
|
||||||
scrollRow: { flexDirection: 'row', marginBottom: 12, gap: 10 },
|
darkValueText: {
|
||||||
|
|
||||||
tag: {
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 10,
|
|
||||||
borderRadius: 12,
|
|
||||||
marginRight: 10,
|
|
||||||
borderWidth: 1,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 1 },
|
|
||||||
shadowOpacity: 0.05,
|
|
||||||
shadowRadius: 2,
|
|
||||||
elevation: 1,
|
|
||||||
},
|
|
||||||
darkTag: {
|
|
||||||
backgroundColor: '#1e293b',
|
|
||||||
borderColor: '#334155',
|
|
||||||
},
|
|
||||||
lightTag: {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
borderColor: '#e2e8f0',
|
|
||||||
},
|
|
||||||
tagSelected: {
|
|
||||||
backgroundColor: '#3b82f6',
|
|
||||||
borderColor: '#3b82f6',
|
|
||||||
shadowColor: '#3b82f6',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 4,
|
|
||||||
elevation: 5,
|
|
||||||
},
|
|
||||||
tagText: {
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
darkTagText: {
|
|
||||||
color: '#cbd5e1',
|
color: '#cbd5e1',
|
||||||
},
|
},
|
||||||
lightTagText: {
|
lightValueText: {
|
||||||
color: '#64748b',
|
color: '#64748b',
|
||||||
},
|
},
|
||||||
tagTextSelected: {
|
disabledText: {
|
||||||
color: '#ffffff',
|
opacity: 0.5,
|
||||||
fontWeight: '600',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
applyBtnWrapper: {
|
applyBtnWrapper: {
|
||||||
position: 'absolute',
|
padding: 20,
|
||||||
bottom: 55,
|
borderTopWidth: 1,
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
},
|
||||||
applyBtn: {
|
applyBtn: {
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: '#3b82f6',
|
||||||
@@ -344,7 +574,98 @@ const styles = StyleSheet.create({
|
|||||||
shadowOpacity: 0.3,
|
shadowOpacity: 0.3,
|
||||||
shadowRadius: 4,
|
shadowRadius: 4,
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
marginBottom: 20,
|
|
||||||
},
|
},
|
||||||
applyBtnText: { color: '#ffffff', fontWeight: '700', fontSize: 16 },
|
applyBtnDisabled: {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
applyBtnText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
bottomSheetBackground: {
|
||||||
|
borderTopLeftRadius: 24,
|
||||||
|
borderTopRightRadius: 24,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: -2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
handleIndicator: {
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
borderRadius: 2,
|
||||||
|
},
|
||||||
|
darkHandleIndicator: {
|
||||||
|
backgroundColor: '#475569',
|
||||||
|
},
|
||||||
|
lightHandleIndicator: {
|
||||||
|
backgroundColor: '#cbd5e1',
|
||||||
|
},
|
||||||
|
sheetHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
},
|
||||||
|
sheetTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
sheetCloseBtn: {
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
listContainer: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 40,
|
||||||
|
},
|
||||||
|
scrollViewContent: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 40,
|
||||||
|
},
|
||||||
|
listItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
darkListItem: {
|
||||||
|
backgroundColor: '#1e293b',
|
||||||
|
borderColor: '#334155',
|
||||||
|
},
|
||||||
|
lightListItem: {
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderColor: '#e2e8f0',
|
||||||
|
},
|
||||||
|
selectedListItem: {
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
},
|
||||||
|
listItemText: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
selectedListItemText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
checkmark: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
checkmarkText: {
|
||||||
|
color: '#3b82f6',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ export default function FilteredItems({ data, back }: FilteredItemsProps) {
|
|||||||
|
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||||
style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}
|
|
||||||
contentContainerStyle={{ paddingBottom: 70 }}
|
|
||||||
>
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -203,7 +200,7 @@ export default function FilteredItems({ data, back }: FilteredItemsProps) {
|
|||||||
{data && data.length > 0 ? (
|
{data && data.length > 0 ? (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data}
|
data={data}
|
||||||
style={{ marginBottom: 120 }}
|
style={{ marginBottom: 70 }}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|||||||
@@ -1,30 +1,59 @@
|
|||||||
import Logo from '@/assets/images/logo.png';
|
import Logo from '@/assets/images/logo.png';
|
||||||
|
import LogoText from '@/assets/images/navbar.png';
|
||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { LogOut } from 'lucide-react-native';
|
import { user_api } from '@/screens/profile/lib/api';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import { Bell, LogOut } from 'lucide-react-native';
|
||||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { useAuth } from '../AuthProvider';
|
import { useAuth } from '../AuthProvider';
|
||||||
|
|
||||||
export const CustomHeader = ({ logoutbtn = false }: { logoutbtn?: boolean }) => {
|
export const CustomHeader = ({
|
||||||
|
logoutbtn = false,
|
||||||
|
notif = true,
|
||||||
|
}: {
|
||||||
|
logoutbtn?: boolean;
|
||||||
|
notif?: boolean;
|
||||||
|
}) => {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { t } = useTranslation();
|
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['notification-list'],
|
||||||
|
queryFn: () => user_api.notification_list({ page: 1, page_size: 1 }),
|
||||||
|
});
|
||||||
|
const unreadCount = data?.data?.data.unread_count ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||||
<View style={styles.logoWrapper}>
|
<View style={styles.logoWrapper}>
|
||||||
<View style={styles.logoContainer}>
|
<View style={styles.logoContainer}>
|
||||||
<Image source={Logo} style={styles.logo} />
|
<Image source={Logo} style={styles.logo} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
|
<Image source={LogoText} style={{ width: 160, height: 40, objectFit: 'cover' }} />
|
||||||
{t('app.name', 'InfoTarget')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
{logoutbtn && (
|
{logoutbtn && (
|
||||||
<TouchableOpacity onPress={logout}>
|
<TouchableOpacity onPress={logout}>
|
||||||
<LogOut color={'red'} />
|
<LogOut color={'red'} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
{notif && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.push('/profile/notification')} // yoki '/notifications' sahifasiga
|
||||||
|
style={styles.bellContainer}
|
||||||
|
>
|
||||||
|
<Bell color={isDark ? '#e2e8f0' : '#334155'} size={24} />
|
||||||
|
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<View style={styles.badge}>
|
||||||
|
<Text style={styles.badgeText}>{unreadCount > 99 ? '99+' : unreadCount}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Agar yuklanayotgan bo'lsa kichik indikator (ixtiyoriy) */}
|
||||||
|
{isLoading && unreadCount === 0 && <View style={styles.loadingDot} />}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -57,7 +86,8 @@ const styles = StyleSheet.create({
|
|||||||
logoWrapper: {
|
logoWrapper: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 12,
|
alignContent: 'center',
|
||||||
|
gap: 5,
|
||||||
},
|
},
|
||||||
|
|
||||||
logoContainer: {
|
logoContainer: {
|
||||||
@@ -89,4 +119,38 @@ const styles = StyleSheet.create({
|
|||||||
lightText: {
|
lightText: {
|
||||||
color: '#0f172a',
|
color: '#0f172a',
|
||||||
},
|
},
|
||||||
|
bellContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
|
||||||
|
badge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -6,
|
||||||
|
right: -6,
|
||||||
|
minWidth: 18,
|
||||||
|
height: 18,
|
||||||
|
borderRadius: 9,
|
||||||
|
backgroundColor: '#ef4444', // qizil badge
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
borderWidth: 1.5,
|
||||||
|
},
|
||||||
|
|
||||||
|
badgeText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
|
||||||
|
loadingDot: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
29
google-services.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "1082838592913",
|
||||||
|
"project_id": "infotarget-b4d7b",
|
||||||
|
"storage_bucket": "infotarget-b4d7b.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:1082838592913:android:862235507e6fa20aa23e30",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.felix.infotarget"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyAKFx1WLRSdT4ykyTadtPXlb6WMOm4FNCE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
66
hooks/useNotifications.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import httpClient from '@/api/httpClient';
|
||||||
|
import { registerForPushNotificationsAsync } from '@/components/NotificationProvider';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
import { getToken } from './storage.native';
|
||||||
|
|
||||||
|
export interface IRegisterDeviceBody {
|
||||||
|
token: string;
|
||||||
|
platform: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonRequests = {
|
||||||
|
/**
|
||||||
|
* Register device for notification
|
||||||
|
* @param body token
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async registerDevice(body: IRegisterDeviceBody) {
|
||||||
|
const response = await httpClient.post('https://api.infotarget.uz/api/push-token/', body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${getToken()}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useNotifications() {
|
||||||
|
const notificationListener = useRef<any>(null);
|
||||||
|
const responseListener = useRef<any>(null);
|
||||||
|
const queryClinet = useQueryClient();
|
||||||
|
useEffect(() => {
|
||||||
|
registerForPushNotificationsAsync().then((token) => {
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const body: IRegisterDeviceBody = {
|
||||||
|
token: token,
|
||||||
|
platform: Platform.OS,
|
||||||
|
};
|
||||||
|
commonRequests.registerDevice(body);
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationListener.current = Notifications.addNotificationReceivedListener(
|
||||||
|
(notification) => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => {
|
||||||
|
const data = response.notification.request.content.data;
|
||||||
|
queryClinet.refetchQueries({ queryKey: ['notification-list'] });
|
||||||
|
queryClinet.refetchQueries({ queryKey: ['notifications-list'] });
|
||||||
|
if (data?.screen === '/profile/notification') {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
router.push('/profile/notification');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
notificationListener.current?.remove();
|
||||||
|
responseListener.current?.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"Qayta urinish": "Retry",
|
"Qayta urinish": "Retry",
|
||||||
"Bosh sahifa": "Home",
|
"Bosh sahifa": "Home",
|
||||||
"E'lon joylashtirish": "Post Announcement",
|
"E'lon joylashtirish": "Post Announcement",
|
||||||
"E'lonlar": "Announcements",
|
"E'lonlar": "Ads",
|
||||||
"Profil": "Profile",
|
"Profil": "Profile",
|
||||||
"Davlat": "Country",
|
"Davlat": "Country",
|
||||||
"Barchasi": "All",
|
"Barchasi": "All",
|
||||||
@@ -145,5 +145,43 @@
|
|||||||
"Tilni tanlang": "Select language",
|
"Tilni tanlang": "Select language",
|
||||||
"Rejimni tanlang": "Select mode",
|
"Rejimni tanlang": "Select mode",
|
||||||
"Tungi rejim": "Dark mode",
|
"Tungi rejim": "Dark mode",
|
||||||
"Yorug' rejim": "Light mode"
|
"Yorug' rejim": "Light mode",
|
||||||
|
"E'lon qo'shish": "Add announcement",
|
||||||
|
"Qo'shish": "Add",
|
||||||
|
"Refferallarim": "My referrals",
|
||||||
|
"Davlat xizmatlari": "Public services",
|
||||||
|
"foydalanuvchi": "customer",
|
||||||
|
"Referral yaratildi": "Referral created",
|
||||||
|
"Kod aynan 9 ta belgidan iborat bo‘lishi kerak": "Code must be exactly 9 characters",
|
||||||
|
"Tavsif kamida 5 ta belgidan iborat bo‘lishi kerak": "Description must be at least 5 characters",
|
||||||
|
"Agent uchun foiz majburiy": "Agent percentage is required",
|
||||||
|
"Referral yaratish": "Create Referral",
|
||||||
|
"Referral nomi": "Referral Name",
|
||||||
|
"Agentmi?": "Is Agent?",
|
||||||
|
"Referral foizi (%)": "Referral Percentage (%)",
|
||||||
|
"Yoshi": "Age",
|
||||||
|
"Noma'lum": "Unknown",
|
||||||
|
"Jinsi": "Gender",
|
||||||
|
"male": "Male",
|
||||||
|
"female": "Female",
|
||||||
|
"Foydalanish qo'lanmasi": "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",
|
||||||
|
"Muhim: Ro'yxatdan o'tgandan keyin profil to'liq bo'lishi kerak — aks holda platforma cheklangan rejimda ishlaydi.": "Important: After registration, the profile must be complete; otherwise the platform works in limited mode.",
|
||||||
|
"Xodimlarni qo'shish": "Add employees",
|
||||||
|
"Profil ichida Xodimlar bo'limiga o'ting va + tugmasi orqali xodim qo'shing.": "Go to the Employees section in the profile and add an employee using the + button.",
|
||||||
|
"E'lon berish va o'z mahsulot/xizmatlaringizni ko'rsatish": "Post announcements and showcase your products/services",
|
||||||
|
"Pastki menyudan E'lonlar bo'limiga o'ting va yangi e'lon yarating.": "Go to the Announcements section in the bottom menu and create a new announcement.",
|
||||||
|
"Mijozlarni qidirish va topish": "Search and find clients",
|
||||||
|
"Filtrdan foydalanib kerakli kompaniyalarni toping va profiliga kirib xabar yuboring.": "Use the filter to find the desired companies and send a message to their profile.",
|
||||||
|
"Muhim maslahatlar va xavfsizlik": "Important tips and safety",
|
||||||
|
"Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.": "Only contact verified profiles.",
|
||||||
|
"Qo'llanma video": "Manual Video",
|
||||||
|
"Bildirishnomalar": "Notifications",
|
||||||
|
"Bildirishnomalarni yuklashda muammo bo'ldi": "There was a problem loading notifications",
|
||||||
|
"Hozir": "Just now",
|
||||||
|
"daqiqa oldin": "minute ago",
|
||||||
|
"soat oldin": "hours ago",
|
||||||
|
"kun oldin": "days ago"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,5 +145,42 @@
|
|||||||
"Tilni tanlang": "Выберите язык",
|
"Tilni tanlang": "Выберите язык",
|
||||||
"Rejimni tanlang": "Выберите режим",
|
"Rejimni tanlang": "Выберите режим",
|
||||||
"Tungi rejim": "Ночной режим",
|
"Tungi rejim": "Ночной режим",
|
||||||
"Yorug' rejim": "Дневной режим"
|
"Yorug' rejim": "Дневной режим",
|
||||||
|
"Qo'shish": "Добавить",
|
||||||
|
"Refferallarim": "Мои рефералы",
|
||||||
|
"Davlat xizmatlari": "Государственные услуги",
|
||||||
|
"foydalanuvchi": "пользователь",
|
||||||
|
"Referral yaratildi": "Рефераль создан",
|
||||||
|
"Kod aynan 9 ta belgidan iborat bo‘lishi kerak": "Код должен содержать ровно 9 символов",
|
||||||
|
"Tavsif kamida 5 ta belgidan iborat bo‘lishi kerak": "Описание должно содержать не менее 5 символов",
|
||||||
|
"Agent uchun foiz majburiy": "Процент для агента обязателен",
|
||||||
|
"Referral yaratish": "Создать реферал",
|
||||||
|
"Referral nomi": "Название реферала",
|
||||||
|
"Agentmi?": "Является агентом?",
|
||||||
|
"Referral foizi (%)": "Процент реферала (%)",
|
||||||
|
"Yoshi": "Возраст",
|
||||||
|
"Noma'lum": "Неизвестный",
|
||||||
|
"Jinsi": "Пол",
|
||||||
|
"male": "Мужской",
|
||||||
|
"female": "Женщина",
|
||||||
|
"Foydalanish qo'lanmasi": "Инструкция по использованию",
|
||||||
|
"Ro'yxatdan o'tish (Registratsiya) – 1 daqiqa ichida": "Регистрация – за 1 минуту",
|
||||||
|
"Platformaga kirish uchun avval ro'yxatdan o'ting.": "Сначала войдите в систему, зарегистрировавшись.",
|
||||||
|
"Profilni to'ldirish va tasdiqlash": "Заполнение и подтверждение профиля",
|
||||||
|
"Muhim: Ro'yxatdan o'tgandan keyin profil to'liq bo'lishi kerak — aks holda platforma cheklangan rejimda ishlaydi.": "Важно: после регистрации профиль должен быть заполнен полностью, иначе платформа работает в ограниченном режиме.",
|
||||||
|
"Xodimlarni qo'shish": "Добавление сотрудников",
|
||||||
|
"Profil ichida Xodimlar bo'limiga o'ting va + tugmasi orqali xodim qo'shing.": "Перейдите в раздел сотрудников в профиле и добавьте сотрудника через кнопку +.",
|
||||||
|
"E'lon berish va o'z mahsulot/xizmatlaringizni ko'rsatish": "Создание объявлений и показ своих продуктов/услуг",
|
||||||
|
"Pastki menyudan E'lonlar bo'limiga o'ting va yangi e'lon yarating.": "Перейдите в раздел объявлений в нижнем меню и создайте новое объявление.",
|
||||||
|
"Mijozlarni qidirish va topish": "Поиск и нахождение клиентов",
|
||||||
|
"Filtrdan foydalanib kerakli kompaniyalarni toping va profiliga kirib xabar yuboring.": "Используйте фильтр, чтобы найти нужные компании, и отправьте сообщение в их профиль.",
|
||||||
|
"Muhim maslahatlar va xavfsizlik": "Важные советы и безопасность",
|
||||||
|
"Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.": "Обращайтесь только к подтвержденным профилям.",
|
||||||
|
"Qo'llanma video": "Видео-инструкция",
|
||||||
|
"Bildirishnomalar": "Уведомления",
|
||||||
|
"Bildirishnomalarni yuklashda muammo bo'ldi": "Возникла проблема с загрузкой уведомлений",
|
||||||
|
"Hozir": "Сейчас",
|
||||||
|
"daqiqa oldin": "минут назад",
|
||||||
|
"soat oldin": "час назад",
|
||||||
|
"kun oldin": "дней назад"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
"Davlatlar": "Davlatlar",
|
"Davlatlar": "Davlatlar",
|
||||||
"Ma'lumot yuklashda xatolik": "Ma'lumot yuklashda xatolik",
|
"Ma'lumot yuklashda xatolik": "Ma'lumot yuklashda xatolik",
|
||||||
"Qayta urinish": "Qayta urinish",
|
"Qayta urinish": "Qayta urinish",
|
||||||
"Bosh sahifa": "Bosh sahifa",
|
"Bosh sahifa": "Asosiy",
|
||||||
"E'lon joylashtirish": "E'lon joylashtirish",
|
"E'lon joylashtirish": "E'lon joylashtirish",
|
||||||
"E'lonlar": "E'lonlar",
|
"E'lonlar": "E'lonlar",
|
||||||
"Profil": "Profil",
|
"Profil": "Profil",
|
||||||
@@ -145,5 +145,42 @@
|
|||||||
"Tilni tanlang": "Tilni tanlang",
|
"Tilni tanlang": "Tilni tanlang",
|
||||||
"Rejimni tanlang": "Rejimni tanlang",
|
"Rejimni tanlang": "Rejimni tanlang",
|
||||||
"Tungi rejim": "Tungi rejim",
|
"Tungi rejim": "Tungi rejim",
|
||||||
"Yorug' rejim": "Yorug' rejim"
|
"Yorug' rejim": "Yorug' rejim",
|
||||||
|
"E'lon qo'shish": "E'lon qo'shish",
|
||||||
|
"Refferallarim": "Refferallarim",
|
||||||
|
"Davlat xizmatlari": "Davlat xizmatlari",
|
||||||
|
"foydalanuvchi": "foydalanuvchi",
|
||||||
|
"Referral yaratildi": "Referral yaratildi",
|
||||||
|
"Kod aynan 9 ta belgidan iborat bo‘lishi kerak": "Kod aynan 9 ta belgidan iborat bo‘lishi kerak",
|
||||||
|
"Tavsif kamida 5 ta belgidan iborat bo‘lishi kerak": "Tavsif kamida 5 ta belgidan iborat bo‘lishi kerak",
|
||||||
|
"Agent uchun foiz majburiy": "Agent uchun foiz majburiy",
|
||||||
|
"Referral yaratish": "Referral yaratish",
|
||||||
|
"Referral nomi": "Referral nomi",
|
||||||
|
"Agentmi?": "Agentmi?",
|
||||||
|
"Referral foizi (%)": "Referral foizi (%)",
|
||||||
|
"Yoshi": "Yoshi",
|
||||||
|
"Noma'lum": "Noma'lum",
|
||||||
|
"Jinsi": "Jinsi",
|
||||||
|
"male": "Erkak",
|
||||||
|
"female": "Ayol",
|
||||||
|
"Foydalanish qo'lanmasi": "Foydalanish qo'lanmasi",
|
||||||
|
"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",
|
||||||
|
"Muhim: Ro'yxatdan o'tgandan keyin profil to'liq bo'lishi kerak — aks holda platforma cheklangan rejimda ishlaydi.": "Muhim: Ro'yxatdan o'tgandan keyin profil to'liq bo'lishi kerak — aks holda platforma cheklangan rejimda ishlaydi.",
|
||||||
|
"Xodimlarni qo'shish": "Xodimlarni qo'shish",
|
||||||
|
"Profil ichida Xodimlar bo'limiga o'ting va + tugmasi orqali xodim qo'shing.": "Profil ichida Xodimlar bo'limiga o'ting va + tugmasi orqali xodim qo'shing.",
|
||||||
|
"E'lon berish va o'z mahsulot/xizmatlaringizni ko'rsatish": "E'lon berish va o'z mahsulot/xizmatlaringizni ko'rsatish",
|
||||||
|
"Pastki menyudan E'lonlar bo'limiga o'ting va yangi e'lon yarating.": "Pastki menyudan E'lonlar bo'limiga o'ting va yangi e'lon yarating.",
|
||||||
|
"Mijozlarni qidirish va topish": "Mijozlarni qidirish va topish",
|
||||||
|
"Filtrdan foydalanib kerakli kompaniyalarni toping va profiliga kirib xabar yuboring.": "Filtrdan foydalanib kerakli kompaniyalarni toping va profiliga kirib xabar yuboring.",
|
||||||
|
"Muhim maslahatlar va xavfsizlik": "Muhim maslahatlar va xavfsizlik",
|
||||||
|
"Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.": "Faqat tasdiqlangan profillarga ishonch bilan murojaat qiling.",
|
||||||
|
"Qo'llanma video": "Qo'llanma video",
|
||||||
|
"Bildirishnomalar": "Bildirishnomalar",
|
||||||
|
"Bildirishnomalarni yuklashda muammo bo'ldi": "Bildirishnomalarni yuklashda muammo bo'ldi",
|
||||||
|
"Hozir": "Hozir",
|
||||||
|
"daqiqa oldin": "daqiqa oldin",
|
||||||
|
"soat oldin": "soat oldin",
|
||||||
|
"kun oldin": "kun oldin"
|
||||||
}
|
}
|
||||||
|
|||||||
200
package-lock.json
generated
@@ -25,8 +25,10 @@
|
|||||||
"expo": "~54.0.31",
|
"expo": "~54.0.31",
|
||||||
"expo-av": "~16.0.8",
|
"expo-av": "~16.0.8",
|
||||||
"expo-blur": "~15.0.8",
|
"expo-blur": "~15.0.8",
|
||||||
|
"expo-clipboard": "~8.0.8",
|
||||||
"expo-constants": "~18.0.13",
|
"expo-constants": "~18.0.13",
|
||||||
"expo-dev-client": "~6.0.20",
|
"expo-dev-client": "~6.0.20",
|
||||||
|
"expo-device": "~8.0.10",
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
"expo-linear-gradient": "~15.0.8",
|
"expo-linear-gradient": "~15.0.8",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-navigation-bar": "~5.0.10",
|
"expo-navigation-bar": "~5.0.10",
|
||||||
|
"expo-notifications": "~0.32.16",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
@@ -59,6 +62,7 @@
|
|||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-svg": "^15.15.1",
|
"react-native-svg": "^15.15.1",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
|
"react-native-webview": "13.15.0",
|
||||||
"react-native-worklets": "^0.5.1",
|
"react-native-worklets": "^0.5.1",
|
||||||
"react-stately": "^3.39.0",
|
"react-stately": "^3.39.0",
|
||||||
"tailwind-variants": "^0.1.20",
|
"tailwind-variants": "^0.1.20",
|
||||||
@@ -2917,6 +2921,12 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ide/backoff": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@internationalized/date": {
|
"node_modules/@internationalized/date": {
|
||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz",
|
||||||
@@ -7528,6 +7538,19 @@
|
|||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/assert": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"is-nan": "^1.3.2",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object.assign": "^4.1.4",
|
||||||
|
"util": "^0.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async-function": {
|
"node_modules/async-function": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||||
@@ -7554,7 +7577,6 @@
|
|||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"possible-typed-array-names": "^1.0.0"
|
"possible-typed-array-names": "^1.0.0"
|
||||||
@@ -7809,6 +7831,12 @@
|
|||||||
"@babel/core": "^7.0.0"
|
"@babel/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/badgin": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -8029,7 +8057,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.0",
|
"call-bind-apply-helpers": "^1.0.0",
|
||||||
@@ -8061,7 +8088,6 @@
|
|||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@@ -8738,7 +8764,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-define-property": "^1.0.0",
|
"es-define-property": "^1.0.0",
|
||||||
@@ -8765,7 +8790,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.0.1",
|
"define-data-property": "^1.0.1",
|
||||||
@@ -9719,6 +9743,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-application": {
|
||||||
|
"version": "7.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
|
||||||
|
"integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-asset": {
|
"node_modules/expo-asset": {
|
||||||
"version": "12.0.12",
|
"version": "12.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
|
||||||
@@ -9762,6 +9795,17 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-clipboard": {
|
||||||
|
"version": "8.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-8.0.8.tgz",
|
||||||
|
"integrity": "sha512-VKoBkHIpZZDJTB0jRO4/PZskHdMNOEz3P/41tmM6fDuODMpqhvyWK053X0ebspkxiawJX9lX33JXHBCvVsTTOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
"version": "18.0.13",
|
"version": "18.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
|
||||||
@@ -9849,6 +9893,44 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-device": {
|
||||||
|
"version": "8.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-device/-/expo-device-8.0.10.tgz",
|
||||||
|
"integrity": "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ua-parser-js": "^0.7.33"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-device/node_modules/ua-parser-js": {
|
||||||
|
"version": "0.7.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz",
|
||||||
|
"integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/ua-parser-js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/faisalman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"ua-parser-js": "script/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-file-system": {
|
"node_modules/expo-file-system": {
|
||||||
"version": "19.0.21",
|
"version": "19.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz",
|
||||||
@@ -10019,6 +10101,26 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-notifications": {
|
||||||
|
"version": "0.32.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz",
|
||||||
|
"integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/image-utils": "^0.8.8",
|
||||||
|
"@ide/backoff": "^1.0.0",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"assert": "^2.0.0",
|
||||||
|
"badgin": "^1.1.5",
|
||||||
|
"expo-application": "~7.0.8",
|
||||||
|
"expo-constants": "~18.0.13"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-router": {
|
"node_modules/expo-router": {
|
||||||
"version": "6.0.21",
|
"version": "6.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.21.tgz",
|
||||||
@@ -10481,7 +10583,6 @@
|
|||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-callable": "^1.2.7"
|
"is-callable": "^1.2.7"
|
||||||
@@ -10591,7 +10692,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
|
||||||
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
|
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -10854,7 +10954,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-define-property": "^1.0.0"
|
"es-define-property": "^1.0.0"
|
||||||
@@ -11230,6 +11329,22 @@
|
|||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -11334,7 +11449,6 @@
|
|||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -11447,7 +11561,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||||
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
|
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.4",
|
"call-bound": "^1.0.4",
|
||||||
@@ -11489,6 +11602,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-nan": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"define-properties": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-negative-zero": {
|
"node_modules/is-negative-zero": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
||||||
@@ -11541,7 +11670,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
@@ -11624,7 +11752,6 @@
|
|||||||
"version": "1.1.15",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
||||||
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"which-typed-array": "^1.1.16"
|
"which-typed-array": "^1.1.16"
|
||||||
@@ -13426,11 +13553,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-keys": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -13440,7 +13582,6 @@
|
|||||||
"version": "4.1.7",
|
"version": "4.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
|
||||||
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
|
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.8",
|
"call-bind": "^1.0.8",
|
||||||
@@ -13991,7 +14132,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -15214,6 +15354,20 @@
|
|||||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-webview": {
|
||||||
|
"version": "13.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz",
|
||||||
|
"integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"invariant": "2.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-worklets": {
|
"node_modules/react-native-worklets": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.2.tgz",
|
||||||
@@ -15923,7 +16077,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||||
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
@@ -16076,7 +16229,6 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.1.4",
|
"define-data-property": "^1.1.4",
|
||||||
@@ -17413,6 +17565,19 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"is-arguments": "^1.0.4",
|
||||||
|
"is-generator-function": "^1.0.7",
|
||||||
|
"is-typed-array": "^1.1.3",
|
||||||
|
"which-typed-array": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -17639,7 +17804,6 @@
|
|||||||
"version": "1.1.20",
|
"version": "1.1.20",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
||||||
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
|
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"available-typed-arrays": "^1.0.7",
|
"available-typed-arrays": "^1.0.7",
|
||||||
|
|||||||
@@ -29,8 +29,10 @@
|
|||||||
"expo": "~54.0.31",
|
"expo": "~54.0.31",
|
||||||
"expo-av": "~16.0.8",
|
"expo-av": "~16.0.8",
|
||||||
"expo-blur": "~15.0.8",
|
"expo-blur": "~15.0.8",
|
||||||
|
"expo-clipboard": "~8.0.8",
|
||||||
"expo-constants": "~18.0.13",
|
"expo-constants": "~18.0.13",
|
||||||
"expo-dev-client": "~6.0.20",
|
"expo-dev-client": "~6.0.20",
|
||||||
|
"expo-device": "~8.0.10",
|
||||||
"expo-font": "~14.0.10",
|
"expo-font": "~14.0.10",
|
||||||
"expo-haptics": "~15.0.8",
|
"expo-haptics": "~15.0.8",
|
||||||
"expo-image": "~3.0.11",
|
"expo-image": "~3.0.11",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"expo-linear-gradient": "~15.0.8",
|
"expo-linear-gradient": "~15.0.8",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-navigation-bar": "~5.0.10",
|
"expo-navigation-bar": "~5.0.10",
|
||||||
|
"expo-notifications": "~0.32.16",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-svg": "^15.15.1",
|
"react-native-svg": "^15.15.1",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
|
"react-native-webview": "13.15.0",
|
||||||
"react-native-worklets": "^0.5.1",
|
"react-native-worklets": "^0.5.1",
|
||||||
"react-stately": "^3.39.0",
|
"react-stately": "^3.39.0",
|
||||||
"tailwind-variants": "^0.1.20",
|
"tailwind-variants": "^0.1.20",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { auth_api } from '../login/lib/api';
|
import { auth_api } from '../login/lib/api';
|
||||||
|
import useTokenStore from '../login/lib/hook';
|
||||||
import ConfirmForm from './ConfirmForm';
|
import ConfirmForm from './ConfirmForm';
|
||||||
|
|
||||||
const ConfirmScreen = () => {
|
const ConfirmScreen = () => {
|
||||||
@@ -28,6 +29,7 @@ const ConfirmScreen = () => {
|
|||||||
const [phoneOTP, setPhone] = useState<string | null>('');
|
const [phoneOTP, setPhone] = useState<string | null>('');
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
|
const { savedToken } = useTokenStore();
|
||||||
|
|
||||||
const [resendTimer, setResendTimer] = useState<number>(60);
|
const [resendTimer, setResendTimer] = useState<number>(60);
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ const ConfirmScreen = () => {
|
|||||||
onSuccess: async (res) => {
|
onSuccess: async (res) => {
|
||||||
await AsyncStorage.removeItem('phone');
|
await AsyncStorage.removeItem('phone');
|
||||||
await AsyncStorage.setItem('access_token', res.data.data.token.access);
|
await AsyncStorage.setItem('access_token', res.data.data.token.access);
|
||||||
|
savedToken(res.data.data.token.access);
|
||||||
await login(res.data.data.token.access);
|
await login(res.data.data.token.access);
|
||||||
await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh);
|
await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh);
|
||||||
router.replace('/(dashboard)');
|
router.replace('/(dashboard)');
|
||||||
|
|||||||
16
screens/auth/login/lib/hook.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
token: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Actions = {
|
||||||
|
savedToken: (token: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTokenStore = create<State & Actions>((set) => ({
|
||||||
|
token: null,
|
||||||
|
savedToken: (token: string | null) => set(() => ({ token })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useTokenStore;
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { auth_api } from '../login/lib/api';
|
import { auth_api } from '../login/lib/api';
|
||||||
|
import useTokenStore from '../login/lib/hook';
|
||||||
import ConfirmForm from './ConfirmForm';
|
import ConfirmForm from './ConfirmForm';
|
||||||
|
|
||||||
const RegisterConfirmScreen = () => {
|
const RegisterConfirmScreen = () => {
|
||||||
@@ -28,6 +29,7 @@ const RegisterConfirmScreen = () => {
|
|||||||
const [phoneOTP, setPhone] = useState<string | null>('');
|
const [phoneOTP, setPhone] = useState<string | null>('');
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
|
const { savedToken } = useTokenStore();
|
||||||
|
|
||||||
const [resendTimer, setResendTimer] = useState<number>(60);
|
const [resendTimer, setResendTimer] = useState<number>(60);
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ const RegisterConfirmScreen = () => {
|
|||||||
onSuccess: async (res) => {
|
onSuccess: async (res) => {
|
||||||
await AsyncStorage.removeItem('phone');
|
await AsyncStorage.removeItem('phone');
|
||||||
await AsyncStorage.setItem('access_token', res.data.data.token.access);
|
await AsyncStorage.setItem('access_token', res.data.data.token.access);
|
||||||
|
savedToken(res.data.data.token.access);
|
||||||
await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh);
|
await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh);
|
||||||
await login(res.data.data.token.access);
|
await login(res.data.data.token.access);
|
||||||
router.replace('/(dashboard)');
|
router.replace('/(dashboard)');
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export default function CreateAdsScreens() {
|
|||||||
behavior="padding"
|
behavior="padding"
|
||||||
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
|
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
|
||||||
>
|
>
|
||||||
<ScrollView contentContainerStyle={styles.container}>
|
<ScrollView contentContainerStyle={[styles.container, { paddingBottom: 90 }]}>
|
||||||
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
|
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
|
||||||
{currentStep === 1
|
{currentStep === 1
|
||||||
? t("E'lon ma'lumotlari")
|
? t("E'lon ma'lumotlari")
|
||||||
@@ -235,43 +235,47 @@ export default function CreateAdsScreens() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentStep === 4 && <StepFour data={ads} setPayment={setPaymentType} />}
|
{currentStep === 4 && <StepFour data={ads} setPayment={setPaymentType} />}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
{currentStep > 1 && currentStep !== 4 && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.back, isDark ? styles.darkBack : styles.lightBack]}
|
||||||
|
onPress={() => setCurrentStep((s) => s - 1)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.btnText, isDark ? styles.darkBtnText : styles.lightBtnText]}>
|
||||||
|
{t('Orqaga')}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.next}
|
||||||
|
disabled={isPending}
|
||||||
|
onPress={() => {
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
if (currentStep === 1) isValid = stepOneRef.current?.validate() ?? false;
|
||||||
|
if (currentStep === 2) isValid = stepTwoRef.current?.validate() ?? false;
|
||||||
|
if (currentStep === 3) isValid = stepThreeRef.current?.validate() ?? false;
|
||||||
|
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
if (currentStep < 3) setCurrentStep((s) => s + 1);
|
||||||
|
if (currentStep === 3) handleSubmit();
|
||||||
|
if (currentStep === 4) handlePresentModalPress();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.btnText}>
|
||||||
|
{currentStep === 3
|
||||||
|
? t('Yaratish')
|
||||||
|
: currentStep === 4
|
||||||
|
? t("To'lash")
|
||||||
|
: t('Keyingisi')}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
{/* FOOTER */}
|
{/* FOOTER */}
|
||||||
<View style={styles.footer}>
|
|
||||||
{currentStep > 1 && currentStep !== 4 && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.back, isDark ? styles.darkBack : styles.lightBack]}
|
|
||||||
onPress={() => setCurrentStep((s) => s - 1)}
|
|
||||||
>
|
|
||||||
<Text style={[styles.btnText, isDark ? styles.darkBtnText : styles.lightBtnText]}>
|
|
||||||
{t('Orqaga')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.next}
|
|
||||||
disabled={isPending}
|
|
||||||
onPress={() => {
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
if (currentStep === 1) isValid = stepOneRef.current?.validate() ?? false;
|
|
||||||
if (currentStep === 2) isValid = stepTwoRef.current?.validate() ?? false;
|
|
||||||
if (currentStep === 3) isValid = stepThreeRef.current?.validate() ?? false;
|
|
||||||
|
|
||||||
if (!isValid) return;
|
|
||||||
|
|
||||||
if (currentStep < 3) setCurrentStep((s) => s + 1);
|
|
||||||
if (currentStep === 3) handleSubmit();
|
|
||||||
if (currentStep === 4) handlePresentModalPress();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={styles.btnText}>
|
|
||||||
{currentStep === 3 ? t('Yaratish') : currentStep === 4 ? t("To'lash") : t('Keyingisi')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* PAYMENT BOTTOM SHEET */}
|
{/* PAYMENT BOTTOM SHEET */}
|
||||||
<BottomSheetModal
|
<BottomSheetModal
|
||||||
@@ -329,7 +333,7 @@ const styles = StyleSheet.create({
|
|||||||
lightBg: {
|
lightBg: {
|
||||||
backgroundColor: '#f8fafc',
|
backgroundColor: '#f8fafc',
|
||||||
},
|
},
|
||||||
container: { padding: 20, paddingBottom: 140 },
|
container: { padding: 20 },
|
||||||
title: { fontSize: 22, fontWeight: '800', marginBottom: 20 },
|
title: { fontSize: 22, fontWeight: '800', marginBottom: 20 },
|
||||||
darkText: {
|
darkText: {
|
||||||
color: '#f1f5f9',
|
color: '#f1f5f9',
|
||||||
@@ -339,12 +343,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
footer: {
|
footer: {
|
||||||
position: 'absolute',
|
marginTop: 10,
|
||||||
bottom: 80,
|
gap: 5,
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 10,
|
|
||||||
},
|
},
|
||||||
back: {
|
back: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
14
screens/e-services/lib/api.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import httpClient from '@/api/httpClient';
|
||||||
|
import { API_URLS } from '@/api/URLs';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { GovermentServiceData } from './types';
|
||||||
|
|
||||||
|
export const eservices_api = {
|
||||||
|
async list(params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
}): Promise<AxiosResponse<GovermentServiceData>> {
|
||||||
|
const res = await httpClient.get(API_URLS.Goverment_Service, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
21
screens/e-services/lib/types.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export interface GovermentServiceData {
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
links: {
|
||||||
|
previous: null | string;
|
||||||
|
next: null | string;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
results: GovermentServiceDataRes[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GovermentServiceDataRes {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
logo: string;
|
||||||
|
}
|
||||||
199
screens/e-services/ui/EServices.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
// 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -79,6 +79,7 @@ export interface CountryBody {
|
|||||||
export interface CountryResponse {
|
export interface CountryResponse {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
flag: string;
|
||||||
companies: {
|
companies: {
|
||||||
id: number;
|
id: number;
|
||||||
company_name: string;
|
company_name: string;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Modal,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
@@ -22,7 +23,8 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { RefreshControl } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView, RefreshControl } from 'react-native-gesture-handler';
|
||||||
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
function Loading() {
|
function Loading() {
|
||||||
return (
|
return (
|
||||||
@@ -84,83 +86,96 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
}, [activeTab, query]);
|
}, [activeTab, query]);
|
||||||
|
|
||||||
if (showFilter && step === 'filter') {
|
const handleCloseFilter = () => {
|
||||||
return (
|
setShowFilter(false);
|
||||||
<FilterUI back={() => setShowFilter(false)} setStep={setStep} setFiltered={setFiltered} />
|
setStep('filter');
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
// Show filtered items if filter was applied
|
||||||
if (showFilter && step === 'items') {
|
if (showFilter && step === 'items') {
|
||||||
return (
|
return (
|
||||||
<FilteredItems
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
data={filtered}
|
<FilteredItems data={filtered} back={handleCloseFilter} />
|
||||||
back={() => {
|
</GestureHandlerRootView>
|
||||||
setShowFilter(false);
|
|
||||||
setStep('filter');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
style={[isDark ? styles.darkBg : styles.lightBg]}
|
<ScrollView
|
||||||
contentContainerStyle={{ flexGrow: 1, paddingBottom: 60 }}
|
style={[isDark ? styles.darkBg : styles.lightBg]}
|
||||||
keyboardShouldPersistTaps="handled"
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
refreshControl={
|
keyboardShouldPersistTaps="handled"
|
||||||
<RefreshControl
|
refreshControl={
|
||||||
refreshing={refreshing}
|
<RefreshControl
|
||||||
onRefresh={onRefresh}
|
refreshing={refreshing}
|
||||||
colors={['#3b82f6']}
|
onRefresh={onRefresh}
|
||||||
tintColor="#3b82f6"
|
colors={['#3b82f6']}
|
||||||
/>
|
tintColor="#3b82f6"
|
||||||
}
|
/>
|
||||||
>
|
}
|
||||||
<Stack.Screen options={{ headerShown: false }} />
|
>
|
||||||
|
<Stack.Screen options={{ headerShown: false }} />
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* Qidiruv va filter */}
|
{/* Qidiruv va filter */}
|
||||||
<View style={styles.searchSection}>
|
<View style={styles.searchSection}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.searchInputContainer,
|
styles.searchInputContainer,
|
||||||
isDark ? styles.darkSearchInput : styles.lightSearchInput,
|
isDark ? styles.darkSearchInput : styles.lightSearchInput,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Search size={20} color={isDark ? '#64748b' : '#94a3b8'} style={styles.searchIcon} />
|
<Search size={20} color={isDark ? '#64748b' : '#94a3b8'} style={styles.searchIcon} />
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.searchInput, isDark ? styles.darkInputText : styles.lightInputText]}
|
style={[styles.searchInput, isDark ? styles.darkInputText : styles.lightInputText]}
|
||||||
placeholder={t(placeholderText)}
|
placeholder={t(placeholderText)}
|
||||||
value={query}
|
value={query}
|
||||||
onChangeText={setQuery}
|
onChangeText={setQuery}
|
||||||
placeholderTextColor={isDark ? '#64748b' : '#94a3b8'}
|
placeholderTextColor={isDark ? '#64748b' : '#94a3b8'}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.filterButton}
|
style={styles.filterButton}
|
||||||
onPress={() => setShowFilter(true)}
|
onPress={() => setShowFilter(true)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Filter size={20} color="#fff" />
|
<Filter size={20} color="#fff" />
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<SearchTabs value={activeTab} onChange={setActiveTab} />
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<View style={[styles.errorContainer, isDark ? styles.darkError : styles.lightError]}>
|
|
||||||
<Text style={styles.errorText}>{t("Ma'lumot yuklashda xatolik")}</Text>
|
|
||||||
<TouchableOpacity onPress={onRefresh} style={styles.retryButton}>
|
|
||||||
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoading && !refreshing && <Loading />}
|
<SearchTabs value={activeTab} onChange={setActiveTab} />
|
||||||
|
|
||||||
{!isLoading && RenderedView}
|
{error && (
|
||||||
</View>
|
<View style={[styles.errorContainer, isDark ? styles.darkError : styles.lightError]}>
|
||||||
</ScrollView>
|
<Text style={styles.errorText}>{t("Ma'lumot yuklashda xatolik")}</Text>
|
||||||
|
<TouchableOpacity onPress={onRefresh} style={styles.retryButton}>
|
||||||
|
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && !refreshing && <Loading />}
|
||||||
|
|
||||||
|
{!isLoading && RenderedView}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Filter Modal */}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={showFilter && step === 'filter'}
|
||||||
|
animationType="slide"
|
||||||
|
presentationStyle="pageSheet"
|
||||||
|
onRequestClose={handleCloseFilter}
|
||||||
|
>
|
||||||
|
<SafeAreaProvider>
|
||||||
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
|
<FilterUI back={handleCloseFilter} setStep={setStep} setFiltered={setFiltered} />
|
||||||
|
</SafeAreaView>
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</Modal>
|
||||||
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
MyAdsData,
|
MyAdsData,
|
||||||
MyAdsDataRes,
|
MyAdsDataRes,
|
||||||
MyBonusesData,
|
MyBonusesData,
|
||||||
|
NotificationListRes,
|
||||||
UserInfoResponseData,
|
UserInfoResponseData,
|
||||||
} from './type';
|
} from './type';
|
||||||
|
|
||||||
@@ -35,6 +36,8 @@ export const user_api = {
|
|||||||
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
||||||
phone: string;
|
phone: string;
|
||||||
activate_types: number[];
|
activate_types: number[];
|
||||||
|
age: number;
|
||||||
|
gender: 'male' | 'female';
|
||||||
}) {
|
}) {
|
||||||
const res = await httpClient.patch(API_URLS.User_Update, body);
|
const res = await httpClient.patch(API_URLS.User_Update, body);
|
||||||
return res;
|
return res;
|
||||||
@@ -105,4 +108,32 @@ export const user_api = {
|
|||||||
const res = await httpClient.get(API_URLS.Detail_Products(id));
|
const res = await httpClient.get(API_URLS.Detail_Products(id));
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async my_referrals(params: { page: number; page_size: number }) {
|
||||||
|
const res = await httpClient.get(API_URLS.My_Refferals, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async create_referral(body: {
|
||||||
|
code: string;
|
||||||
|
referral_share: number;
|
||||||
|
description: string;
|
||||||
|
is_agent: boolean;
|
||||||
|
}) {
|
||||||
|
const res = await httpClient.post(API_URLS.My_Refferals, body);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async notification_list(params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
}): Promise<AxiosResponse<NotificationListRes>> {
|
||||||
|
const res = await httpClient.get(API_URLS.Notification_List, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async is_ready_id(id: number) {
|
||||||
|
const res = await httpClient.post(API_URLS.Notification_Ready(id));
|
||||||
|
return res;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ export interface UserInfoResponseData {
|
|||||||
company_image: null | string;
|
company_image: null | string;
|
||||||
address: null | string;
|
address: null | string;
|
||||||
district: number;
|
district: number;
|
||||||
|
age: null | number;
|
||||||
|
gender: 'male' | 'female' | null;
|
||||||
parent: null | string;
|
parent: null | string;
|
||||||
user_tg_ids: number[];
|
user_tg_ids: number[];
|
||||||
};
|
};
|
||||||
@@ -166,3 +168,30 @@ export interface MyBonusesDataRes {
|
|||||||
percent: number;
|
percent: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotificationListRes {
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
links: {
|
||||||
|
previous: null | string;
|
||||||
|
next: null | string;
|
||||||
|
};
|
||||||
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
|
page_size: number;
|
||||||
|
current_page: number;
|
||||||
|
unread_count: number;
|
||||||
|
|
||||||
|
results: NotificationListDataRes[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationListDataRes {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
is_send: boolean;
|
||||||
|
is_read: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|||||||
233
screens/profile/ui/CreateReferrals.tsx
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
|
import { ArrowLeft } from 'lucide-react-native';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Pressable,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
ToastAndroid,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { user_api } from '../lib/api';
|
||||||
|
|
||||||
|
type FormType = {
|
||||||
|
code: string;
|
||||||
|
referral_share: string;
|
||||||
|
description: string;
|
||||||
|
is_agent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CreateReferrals() {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [form, setForm] = useState<FormType>({
|
||||||
|
code: '',
|
||||||
|
referral_share: '',
|
||||||
|
description: '',
|
||||||
|
is_agent: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate, isPending } = useMutation({
|
||||||
|
mutationFn: (body: {
|
||||||
|
code: string;
|
||||||
|
referral_share: number;
|
||||||
|
description: string;
|
||||||
|
is_agent: boolean;
|
||||||
|
}) => user_api.create_referral(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
ToastAndroid.show(t('Referral yaratildi'), ToastAndroid.SHORT);
|
||||||
|
queryClient.refetchQueries({ queryKey: ['my_referrals'] });
|
||||||
|
router.back();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
ToastAndroid.show(t('Xatolik yuz berdi'), ToastAndroid.SHORT);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<any>({});
|
||||||
|
|
||||||
|
const update = (key: keyof FormType, value: any) => setForm((p) => ({ ...p, [key]: value }));
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
const e: any = {};
|
||||||
|
|
||||||
|
if (!form.code || form.code.length !== 9)
|
||||||
|
e.code = 'Kod aynan 9 ta belgidan iborat bo‘lishi kerak';
|
||||||
|
if (!form.description || form.description.length < 5)
|
||||||
|
e.description = 'Tavsif kamida 5 ta belgidan iborat bo‘lishi kerak';
|
||||||
|
|
||||||
|
if (form.is_agent) {
|
||||||
|
if (!form.referral_share || Number(form.referral_share) <= 0)
|
||||||
|
e.referral_share = 'Agent uchun foiz majburiy';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(e);
|
||||||
|
return Object.keys(e).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!validate()) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
code: form.code,
|
||||||
|
referral_share: form.is_agent ? Number(form.referral_share) : 0,
|
||||||
|
description: form.description,
|
||||||
|
is_agent: form.is_agent,
|
||||||
|
};
|
||||||
|
|
||||||
|
mutate(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
|
{/* HEADER */}
|
||||||
|
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
||||||
|
<Pressable onPress={() => router.back()}>
|
||||||
|
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||||
|
{t('Referral yaratish')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Pressable onPress={handleSave}>
|
||||||
|
{isPending ? (
|
||||||
|
<ActivityIndicator size={'small'} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.save}>{t('Saqlash')}</Text>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView contentContainerStyle={styles.container}>
|
||||||
|
{/* NOM */}
|
||||||
|
<Text style={[styles.label, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||||
|
{t('Referral nomi')}
|
||||||
|
</Text>
|
||||||
|
<View style={[styles.inputBox, theme(isDark)]}>
|
||||||
|
<TextInput
|
||||||
|
maxLength={9}
|
||||||
|
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
|
||||||
|
placeholder="ABC123"
|
||||||
|
placeholderTextColor="#94a3b8"
|
||||||
|
value={form.code}
|
||||||
|
onChangeText={(v) => update('code', v)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{errors.code && <Text style={styles.error}>{t(errors.code)}</Text>}
|
||||||
|
|
||||||
|
{/* TAVSIF */}
|
||||||
|
<Text style={[styles.label, { color: isDark ? '#fff' : '#0f172a' }]}>{t('Tavsif')}</Text>
|
||||||
|
<View style={[styles.inputBox, styles.textArea, theme(isDark)]}>
|
||||||
|
<TextInput
|
||||||
|
multiline
|
||||||
|
textAlignVertical="top"
|
||||||
|
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
|
||||||
|
placeholder={t('Batafsil yozing...')}
|
||||||
|
placeholderTextColor="#94a3b8"
|
||||||
|
value={form.description}
|
||||||
|
onChangeText={(v) => update('description', v)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{errors.description && <Text style={styles.error}>{t(errors.description)}</Text>}
|
||||||
|
|
||||||
|
{/* AGENT SWITCH */}
|
||||||
|
<View style={styles.switchRow}>
|
||||||
|
<Text style={[styles.label, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||||
|
{t('Agentmi?')}
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
value={form.is_agent}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
update('is_agent', v);
|
||||||
|
if (!v) update('referral_share', '');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 👉 FOIZ FAQAT AGENT YOQILGANDA */}
|
||||||
|
{form.is_agent && (
|
||||||
|
<>
|
||||||
|
<Text style={[styles.label, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||||
|
{t('Referral foizi (%)')}
|
||||||
|
</Text>
|
||||||
|
<View style={[styles.inputBox, theme(isDark)]}>
|
||||||
|
<TextInput
|
||||||
|
maxLength={1}
|
||||||
|
keyboardType="numeric"
|
||||||
|
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
|
||||||
|
placeholder="5"
|
||||||
|
placeholderTextColor="#94a3b8"
|
||||||
|
value={form.referral_share}
|
||||||
|
onChangeText={(v) => {
|
||||||
|
// faqat 1–5 oralig‘ini qabul qiladi
|
||||||
|
if (v === '') {
|
||||||
|
update('referral_share', '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(v);
|
||||||
|
if (num >= 1 && num <= 5) {
|
||||||
|
update('referral_share', v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{errors.referral_share && <Text style={styles.error}>{t(errors.referral_share)}</Text>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = (isDark: boolean) => ({
|
||||||
|
backgroundColor: isDark ? '#1e293b' : '#fff',
|
||||||
|
borderColor: isDark ? '#334155' : '#e2e8f0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
header: {
|
||||||
|
padding: 16,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||||
|
save: { color: '#3b82f6', fontSize: 16, fontWeight: '600' },
|
||||||
|
|
||||||
|
container: { padding: 16, gap: 10 },
|
||||||
|
label: { fontSize: 15, fontWeight: '700' },
|
||||||
|
error: { color: '#ef4444', fontSize: 13, marginLeft: 6 },
|
||||||
|
|
||||||
|
inputBox: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderRadius: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
height: 56,
|
||||||
|
},
|
||||||
|
textArea: { height: 120, alignItems: 'flex-start' },
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
switchRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
190
screens/profile/ui/ManualTab.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Dimensions,
|
||||||
|
Image,
|
||||||
|
Pressable,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
type ManualStep = {
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
image?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ManualTab() {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const [videoLang, setVideoLang] = useState<'uz' | 'ru' | 'en'>('uz');
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
background: isDark ? '#0f172a' : '#f8fafc',
|
||||||
|
cardBg: isDark ? '#1e293b' : '#ffffff',
|
||||||
|
text: isDark ? '#ffffff' : '#0f172a',
|
||||||
|
textSecondary: isDark ? '#94a3b8' : '#64748b',
|
||||||
|
primary: '#3b82f6',
|
||||||
|
};
|
||||||
|
|
||||||
|
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> = {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||||
|
{t("Foydalanish qo'lanmasi")}
|
||||||
|
</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
|
||||||
|
style={[
|
||||||
|
styles.langButton,
|
||||||
|
{
|
||||||
|
backgroundColor: videoLang === 'uz' ? theme.primary : theme.cardBg,
|
||||||
|
borderColor: theme.primary,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPress={() => handleVideoChange('uz')}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<Video
|
||||||
|
source={videos[videoLang]}
|
||||||
|
style={styles.video}
|
||||||
|
useNativeControls
|
||||||
|
isLooping
|
||||||
|
resizeMode={ResizeMode.COVER}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</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,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
padding: 16,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
gap: 10,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
407
screens/profile/ui/NotificationTab.tsx
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
|
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import { ArrowLeft } from 'lucide-react-native';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Animated,
|
||||||
|
FlatList,
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
import { user_api } from '../lib/api';
|
||||||
|
import { NotificationListDataRes } from '../lib/type';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
export function NotificationTab() {
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } =
|
||||||
|
useInfiniteQuery({
|
||||||
|
queryKey: ['notifications-list'],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
|
const response = await user_api.notification_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 notifications = data?.pages.flatMap((p) => p.results) ?? [];
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<View style={styles.loadingContent}>
|
||||||
|
<ActivityIndicator size="large" color="#3b82f6" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<View style={styles.errorContent}>
|
||||||
|
<Text style={styles.errorTitle}>{t('Xatolik yuz berdi')}</Text>
|
||||||
|
<Text style={styles.errorMessage}>{t("Bildirishnomalarni yuklashda muammo bo'ldi")}</Text>
|
||||||
|
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
|
||||||
|
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
||||||
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
|
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
|
||||||
|
{t('Bildirishnomalar')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
|
data={notifications}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
contentContainerStyle={styles.listContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
renderItem={({ item, index }) => <NotificationCard item={item} />}
|
||||||
|
onEndReached={() => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
ListFooterComponent={
|
||||||
|
isFetchingNextPage ? (
|
||||||
|
<ActivityIndicator size="small" color="#3b82f6" style={{ marginVertical: 20 }} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
refreshing={isLoading}
|
||||||
|
onRefresh={refetch}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- CARD ---------------- */
|
||||||
|
|
||||||
|
function NotificationCard({ item }: { item: NotificationListDataRes }) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
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'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePressIn = () => {
|
||||||
|
Animated.spring(scaleAnim, {
|
||||||
|
toValue: 0.96,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePressOut = () => {
|
||||||
|
Animated.spring(scaleAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
friction: 4,
|
||||||
|
tension: 50,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePress = (id: number) => {
|
||||||
|
if (!item.is_read) {
|
||||||
|
mutate(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={{
|
||||||
|
transform: [{ scale: scaleAnim }],
|
||||||
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
activeOpacity={0.85}
|
||||||
|
onPressIn={handlePressIn}
|
||||||
|
onPressOut={handlePressOut}
|
||||||
|
onPress={() => handlePress(item.id)}
|
||||||
|
style={[styles.card, !item.is_read && styles.unreadCard]}
|
||||||
|
>
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<Text style={[styles.cardTitle, !item.is_read && styles.unreadTitle]} numberOfLines={1}>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
{!item.is_read && <View style={styles.unreadIndicator} />}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.cardMessage} numberOfLines={2}>
|
||||||
|
{item.description}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={styles.cardTime}>{formatDate(item.created_at, t)}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- HELPERS ---------------- */
|
||||||
|
|
||||||
|
function formatDate(date: string, t: any) {
|
||||||
|
const now = new Date();
|
||||||
|
const notifDate = new Date(date);
|
||||||
|
const diffMs = now.getTime() - notifDate.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
const diffHours = Math.floor(diffMs / 3600000);
|
||||||
|
const diffDays = Math.floor(diffMs / 86400000);
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'Hozir';
|
||||||
|
if (diffMins < 60) return `${diffMins} ${t('daqiqa oldin')}`;
|
||||||
|
if (diffHours < 24) return `${diffHours} ${t('soat oldin')}`;
|
||||||
|
if (diffDays < 7) return `${diffDays} ${t('kun oldin')}`;
|
||||||
|
|
||||||
|
return notifDate.toLocaleDateString('uz-UZ', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- STYLES ---------------- */
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#0a0c17',
|
||||||
|
},
|
||||||
|
listContent: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 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,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
cardContent: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
cardHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
cardTitle: {
|
||||||
|
flex: 1,
|
||||||
|
color: '#d1d5db',
|
||||||
|
fontSize: 16.5,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: -0.1,
|
||||||
|
},
|
||||||
|
unreadTitle: {
|
||||||
|
color: '#f1f5f9',
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
unreadIndicator: {
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
marginLeft: 8,
|
||||||
|
shadowColor: '#3b82f6',
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
shadowOpacity: 0.7,
|
||||||
|
shadowRadius: 6,
|
||||||
|
},
|
||||||
|
cardMessage: {
|
||||||
|
color: '#9ca3af',
|
||||||
|
fontSize: 14.5,
|
||||||
|
lineHeight: 21,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
cardTime: {
|
||||||
|
color: '#64748b',
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: '500',
|
||||||
|
opacity: 0.9,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#0a0c17',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
loadingContent: {
|
||||||
|
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,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
color: '#94a3b8',
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
retryButton: {
|
||||||
|
backgroundColor: '#3b82f6',
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 12,
|
||||||
|
shadowColor: '#3b82f6',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
retryButtonText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 15,
|
||||||
|
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: {
|
||||||
|
padding: 16,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
gap: 10,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
headerTitle: { fontSize: 18, fontWeight: '700', lineHeight: 24 },
|
||||||
|
});
|
||||||
@@ -4,11 +4,9 @@ import { Edit2, Plus } from 'lucide-react-native';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Alert, Modal, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
import { Alert, Modal, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
import { useProfileData } from '../lib/ProfileDataContext';
|
|
||||||
import { UserInfoResponseData } from '../lib/type';
|
import { UserInfoResponseData } from '../lib/type';
|
||||||
|
|
||||||
export function PersonalInfoTab() {
|
export function PersonalInfoTab() {
|
||||||
const { personalInfo, updatePersonalInfo } = useProfileData();
|
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
const [addFieldModalVisible, setAddFieldModalVisible] = useState(false);
|
const [addFieldModalVisible, setAddFieldModalVisible] = useState(false);
|
||||||
const [newField, setNewField] = useState('');
|
const [newField, setNewField] = useState('');
|
||||||
@@ -27,7 +25,6 @@ export function PersonalInfoTab() {
|
|||||||
queryKey: ['get_me'],
|
queryKey: ['get_me'],
|
||||||
queryFn: () => user_api.getMe(),
|
queryFn: () => user_api.getMe(),
|
||||||
select: (res) => {
|
select: (res) => {
|
||||||
setEditData(res.data.data);
|
|
||||||
setPhone(res.data.data.phone || '');
|
setPhone(res.data.data.phone || '');
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
@@ -51,8 +48,11 @@ export function PersonalInfoTab() {
|
|||||||
code: string;
|
code: string;
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
phone: string;
|
|
||||||
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
||||||
|
phone: string;
|
||||||
|
activate_types: number[];
|
||||||
|
age: number;
|
||||||
|
gender: 'male' | 'female';
|
||||||
}) => user_api.updateMe(body),
|
}) => user_api.updateMe(body),
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['get_me'] });
|
queryClient.invalidateQueries({ queryKey: ['get_me'] });
|
||||||
@@ -102,9 +102,6 @@ export function PersonalInfoTab() {
|
|||||||
|
|
||||||
const handleAddField = () => {
|
const handleAddField = () => {
|
||||||
if (newField.trim()) {
|
if (newField.trim()) {
|
||||||
updatePersonalInfo({
|
|
||||||
activityFields: [...personalInfo.activityFields, newField.trim()],
|
|
||||||
});
|
|
||||||
setNewField('');
|
setNewField('');
|
||||||
setAddFieldModalVisible(false);
|
setAddFieldModalVisible(false);
|
||||||
}
|
}
|
||||||
@@ -116,18 +113,12 @@ export function PersonalInfoTab() {
|
|||||||
{
|
{
|
||||||
text: 'Olib tashlash',
|
text: 'Olib tashlash',
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: () => {
|
|
||||||
updatePersonalInfo({
|
|
||||||
activityFields: personalInfo.activityFields.filter((f) => f !== field),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (me?.data.data) {
|
if (me?.data.data) {
|
||||||
setEditData(me.data.data);
|
|
||||||
setPhone(me.data.data.phone || '');
|
setPhone(me.data.data.phone || '');
|
||||||
}
|
}
|
||||||
}, [me]);
|
}, [me]);
|
||||||
@@ -201,7 +192,7 @@ export function PersonalInfoTab() {
|
|||||||
<Text style={styles.inputLabel}>Ism</Text>
|
<Text style={styles.inputLabel}>Ism</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
value={editData?.director_full_name}
|
value={editData?.data.first_name}
|
||||||
onChangeText={(text) =>
|
onChangeText={(text) =>
|
||||||
setEditData((prev) => prev && { ...prev, director_full_name: text })
|
setEditData((prev) => prev && { ...prev, director_full_name: text })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { useGlobalRefresh } from '@/components/ui/RefreshContext';
|
import { useGlobalRefresh } from '@/components/ui/RefreshContext';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import {
|
import {
|
||||||
Award,
|
Award,
|
||||||
|
Bell,
|
||||||
|
BookAIcon,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
HandCoins,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
Package,
|
Package,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -13,6 +17,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { RefreshControl } from 'react-native-gesture-handler';
|
import { RefreshControl } from 'react-native-gesture-handler';
|
||||||
|
import { user_api } from '../lib/api';
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -20,12 +25,18 @@ export default function Profile() {
|
|||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data: me, isLoading } = useQuery({
|
||||||
|
queryKey: ['get_me'],
|
||||||
|
queryFn: () => user_api.getMe(),
|
||||||
|
});
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{
|
{
|
||||||
title: 'Shaxsiy',
|
title: 'Shaxsiy',
|
||||||
items: [
|
items: [
|
||||||
{ icon: User, label: "Shaxsiy ma'lumotlar", route: '/profile/personal-info' },
|
{ icon: User, label: "Shaxsiy ma'lumotlar", route: '/profile/personal-info' },
|
||||||
{ icon: Users, label: 'Xodimlar', route: '/profile/employees' },
|
{ icon: Users, label: 'Xodimlar', route: '/profile/employees' },
|
||||||
|
{ icon: Bell, label: 'Bildirishnomalar', route: '/profile/notification' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,11 +45,23 @@ export default function Profile() {
|
|||||||
{ icon: Megaphone, label: "E'lonlar", route: '/profile/my-ads' },
|
{ icon: Megaphone, label: "E'lonlar", route: '/profile/my-ads' },
|
||||||
{ icon: Award, label: 'Bonuslar', route: '/profile/bonuses' },
|
{ icon: Award, label: 'Bonuslar', route: '/profile/bonuses' },
|
||||||
{ icon: Package, label: 'Xizmatlar', route: '/profile/products' },
|
{ icon: Package, label: 'Xizmatlar', route: '/profile/products' },
|
||||||
|
...(me?.data.data.can_create_referral
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
icon: HandCoins,
|
||||||
|
label: 'Refferallarim',
|
||||||
|
route: '/profile/my-referrals',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Sozlamalar',
|
title: 'Sozlamalar',
|
||||||
items: [{ icon: Settings, label: 'Sozlamalar', route: '/profile/settings' }],
|
items: [
|
||||||
|
{ icon: Settings, label: 'Sozlamalar', route: '/profile/settings' },
|
||||||
|
{ icon: BookAIcon, label: "Foydalanish qo'lanmasi", route: '/profile/manual' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
203
screens/profile/ui/RefferallsTab.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
|
import * as Clipboard from 'expo-clipboard';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
|
import { ArrowLeft, CopyIcon, HandCoins, Plus, Users } from 'lucide-react-native';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
FlatList,
|
||||||
|
Pressable,
|
||||||
|
RefreshControl,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
ToastAndroid,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { user_api } from '../lib/api';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
export function ReferralsTab() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { isDark } = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
background: isDark ? '#0f172a' : '#f8fafc',
|
||||||
|
cardBg: isDark ? '#1e293b' : '#ffffff',
|
||||||
|
text: isDark ? '#ffffff' : '#0f172a',
|
||||||
|
subText: isDark ? '#94a3b8' : '#64748b',
|
||||||
|
primary: '#3b82f6',
|
||||||
|
success: '#10b981',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({
|
||||||
|
queryKey: ['my_referrals'],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
|
const res = await user_api.my_referrals({
|
||||||
|
page: pageParam,
|
||||||
|
page_size: PAGE_SIZE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const d = res.data.data;
|
||||||
|
return {
|
||||||
|
results: d.results ?? [],
|
||||||
|
current_page: d.current_page,
|
||||||
|
total_pages: d.total_pages,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) =>
|
||||||
|
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||||||
|
initialPageParam: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const referrals = data?.pages.flatMap((p) => p.results) ?? [];
|
||||||
|
|
||||||
|
const onRefresh = async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
await refetch();
|
||||||
|
setRefreshing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.center, { backgroundColor: theme.background }]}>
|
||||||
|
<Text style={{ color: 'red' }}>{t('Xatolik yuz berdi')}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.topHeader}>
|
||||||
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
|
<ArrowLeft color={theme.text} />
|
||||||
|
</Pressable>
|
||||||
|
<Text style={[styles.headerTitle, { color: theme.text }]}>{t('Refferallarim')}</Text>
|
||||||
|
<Pressable onPress={() => router.push('/profile/added-referalls')}>
|
||||||
|
<Plus color={theme.primary} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={referrals}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
contentContainerStyle={styles.list}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.primary} />
|
||||||
|
}
|
||||||
|
onEndReached={() => hasNextPage && fetchNextPage()}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View style={[styles.card, { backgroundColor: theme.cardBg }]}>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<HandCoins size={20} color={theme.primary} />
|
||||||
|
<Text style={[styles.code, { color: theme.text }]}>{item.code}</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={async () => {
|
||||||
|
await Clipboard.setStringAsync(
|
||||||
|
`https://t.me/infotargetbot/join?startapp=${item.code}`
|
||||||
|
);
|
||||||
|
ToastAndroid.show('Refferal kopiya qilindi', ToastAndroid.SHORT);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon size={20} color={theme.primary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={{ color: theme.subText }}>{item.description}</Text>
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Users size={16} color={theme.subText} />
|
||||||
|
<Text style={[styles.meta, { color: theme.subText }]}>
|
||||||
|
{item.referral_registered_count} {t('foydalanuvchi')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={[styles.amount, { color: theme.success }]}>
|
||||||
|
{item.referral_income_amount} {t("so'm")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Text style={{ textAlign: 'center', color: theme.subText }}>
|
||||||
|
{t('Refferallar topilmadi')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1 },
|
||||||
|
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||||
|
|
||||||
|
topHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||||
|
|
||||||
|
list: { padding: 16, gap: 12 },
|
||||||
|
|
||||||
|
card: {
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 16,
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
cardHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
code: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginTop: 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
row: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 6,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
meta: {},
|
||||||
|
amount: {
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
});
|
||||||