complated
This commit is contained in:
@@ -35,4 +35,5 @@ export const API_URLS = {
|
|||||||
Goverment_Category: '/api/goverment-category/',
|
Goverment_Category: '/api/goverment-category/',
|
||||||
Notification_List: '/api/notifications/',
|
Notification_List: '/api/notifications/',
|
||||||
Notification_Ready: (id: number) => `/api/notifications/${id}/read/`,
|
Notification_Ready: (id: number) => `/api/notifications/${id}/read/`,
|
||||||
|
Notification_Mark_All_Read: '/api/notifications/read-all/',
|
||||||
};
|
};
|
||||||
|
|||||||
6
app.json
6
app.json
@@ -11,9 +11,7 @@
|
|||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"UIBackgroundModes": [
|
"UIBackgroundModes": ["remote-notification"]
|
||||||
"remote-notification"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"bundleIdentifier": "com.felix.infotarget"
|
"bundleIdentifier": "com.felix.infotarget"
|
||||||
},
|
},
|
||||||
@@ -75,7 +73,7 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
"router": {},
|
"router": {},
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "9a281404-9d04-4493-b630-66c35af03ace"
|
"projectId": "4d7c2011-4ca0-4944-b540-34740b82470f"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import ConfirmScreen from '@/screens/auth/confirm/ConfirmScreen';
|
import ConfirmScreen from '@/screens/auth/confirm/ConfirmScreen';
|
||||||
import { ScrollView } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function Confirm() {
|
export default function Confirm() {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#0f172a',
|
backgroundColor: '#0f172a',
|
||||||
@@ -13,6 +12,6 @@ export default function Confirm() {
|
|||||||
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
|
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
|
||||||
<ConfirmScreen />
|
<ConfirmScreen />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import LoginScreen from '@/screens/auth/login/ui/LoginScreens';
|
|||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ActivityIndicator, ScrollView, View } from 'react-native';
|
import { ActivityIndicator, ScrollView, View } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { isAuthenticated, isLoading } = useAuth();
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
@@ -27,11 +26,11 @@ export default function Index() {
|
|||||||
// Token yo‘q → login screen
|
// Token yo‘q → login screen
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#0f172a' }}>
|
<View style={{ flex: 1, backgroundColor: '#0f172a' }}>
|
||||||
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
|
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
|
||||||
<LoginScreen />
|
<LoginScreen />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import RegisterConfirmScreen from '@/screens/auth/register-confirm/ConfirmScreen';
|
import RegisterConfirmScreen from '@/screens/auth/register-confirm/ConfirmScreen';
|
||||||
import { ScrollView } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function RegisterConfirm() {
|
export default function RegisterConfirm() {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#0f172a',
|
backgroundColor: '#0f172a',
|
||||||
@@ -13,6 +12,6 @@ export default function RegisterConfirm() {
|
|||||||
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
|
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
|
||||||
<RegisterConfirmScreen />
|
<RegisterConfirmScreen />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import RegisterScreen from '@/screens/auth/register/RegisterScreen';
|
import RegisterScreen from '@/screens/auth/register/RegisterScreen';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ScrollView, StyleSheet } from 'react-native';
|
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<View style={styles.safeArea}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{ flexGrow: 1 }}
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
@@ -13,7 +12,7 @@ export default function Index() {
|
|||||||
>
|
>
|
||||||
<RegisterScreen />
|
<RegisterScreen />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -95,7 +95,7 @@ export default function CategorySelectScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<View style={styles.safeArea}>
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<Stack.Screen options={{ title: t('Yo‘nalishni tanlang') }} />
|
<Stack.Screen options={{ title: t('Yo‘nalishni tanlang') }} />
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ export default function CategorySelectScreen() {
|
|||||||
{t('Tadiqlash')}
|
{t('Tadiqlash')}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Logo from '@/assets/images/logo.png';
|
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 { useHomeStore } from '@/screens/home/lib/hook';
|
||||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||||
import { router, Tabs } from 'expo-router';
|
import { router, Tabs } from 'expo-router';
|
||||||
import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native';
|
import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native';
|
||||||
@@ -12,6 +13,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|||||||
export default function TabsLayout() {
|
export default function TabsLayout() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { setShowFilter, setStep } = useHomeStore();
|
||||||
const rotateAnim = useRef(new Animated.Value(0)).current;
|
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -58,9 +60,9 @@ export default function TabsLayout() {
|
|||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
borderTopLeftRadius: 24,
|
borderTopLeftRadius: 24,
|
||||||
borderTopRightRadius: 24,
|
borderTopRightRadius: 24,
|
||||||
backgroundColor: isDark ? 'rgba(15, 23, 42, 0.95)' : 'rgba(255, 255, 255, 0.95)', // #0f172a mos fon
|
backgroundColor: isDark ? 'rgba(15, 23, 42, 1)' : 'rgba(255, 255, 255, 1)', // #0f172a mos fon
|
||||||
borderWidth: 0.5,
|
borderWidth: 0.5,
|
||||||
borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(226, 232, 240, 0.8)', // quyuq fon uchun yengil oq
|
borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(226, 232, 240, 1)', // quyuq fon uchun yengil oq
|
||||||
...Platform.select({
|
...Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
shadowColor: isDark ? '#0f172a' : '#0f172a', // shadow qora emas #0f172a bilan uyg‘un
|
shadowColor: isDark ? '#0f172a' : '#0f172a', // shadow qora emas #0f172a bilan uyg‘un
|
||||||
@@ -85,6 +87,7 @@ export default function TabsLayout() {
|
|||||||
>
|
>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
|
key={"index"}
|
||||||
options={{
|
options={{
|
||||||
title: 'Bosh sahifa',
|
title: 'Bosh sahifa',
|
||||||
tabBarLabel: ({ color, focused }) => (
|
tabBarLabel: ({ color, focused }) => (
|
||||||
@@ -103,7 +106,12 @@ export default function TabsLayout() {
|
|||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, focused }) => (
|
tabBarIcon: ({ color, focused }) => (
|
||||||
<View
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
router.push('/(dashboard)');
|
||||||
|
setShowFilter(false);
|
||||||
|
setStep('filter');
|
||||||
|
}}
|
||||||
style={[
|
style={[
|
||||||
styles.iconContainer,
|
styles.iconContainer,
|
||||||
focused && styles.iconContainerActive,
|
focused && styles.iconContainerActive,
|
||||||
@@ -113,7 +121,7 @@ export default function TabsLayout() {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Home color={color} size={30} strokeWidth={2} />
|
<Home color={color} size={30} strokeWidth={2} />
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -135,7 +143,7 @@ export default function TabsLayout() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{t("Qo'shish")}
|
{t("Jo'natish")}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
tabBarIcon: ({ color, focused }) => (
|
tabBarIcon: ({ color, focused }) => (
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
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 { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function Announcements() {
|
export default function Announcements() {
|
||||||
const { isDark } = useTheme();
|
|
||||||
return (
|
return (
|
||||||
<FilterProvider>
|
<FilterProvider>
|
||||||
<SafeAreaView
|
<CustomHeader />
|
||||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 80 }}
|
<DashboardScreen />
|
||||||
>
|
|
||||||
<CustomHeader />
|
|
||||||
<DashboardScreen />
|
|
||||||
</SafeAreaView>
|
|
||||||
</FilterProvider>
|
</FilterProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,13 @@ 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 CreateAdsScreens from '@/screens/create-ads/ui/CreateAdsScreens';
|
import CreateAdsScreens from '@/screens/create-ads/ui/CreateAdsScreens';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function CreateAnnouncements() {
|
export default function CreateAnnouncements() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
return (
|
return (
|
||||||
<FilterProvider>
|
<FilterProvider>
|
||||||
<SafeAreaView
|
<CustomHeader />
|
||||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 30 }}
|
<CreateAdsScreens />
|
||||||
>
|
|
||||||
<CustomHeader />
|
|
||||||
<CreateAdsScreens />
|
|
||||||
</SafeAreaView>
|
|
||||||
</FilterProvider>
|
</FilterProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
|
||||||
import { CustomHeader } from '@/components/ui/Header';
|
import { CustomHeader } from '@/components/ui/Header';
|
||||||
import EServicesScreen from '@/screens/e-services/ui/EServicesScreen';
|
import EServicesScreen from '@/screens/e-services/ui/EServicesScreen';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function EServicesCategory() {
|
export default function EServicesCategory() {
|
||||||
const { isDark } = useTheme();
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<>
|
||||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 50 }}
|
|
||||||
edges={['top']}
|
|
||||||
>
|
|
||||||
<CustomHeader />
|
<CustomHeader />
|
||||||
<EServicesScreen />
|
<EServicesScreen />
|
||||||
</SafeAreaView>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { CustomHeader } from '@/components/ui/Header';
|
import { CustomHeader } from '@/components/ui/Header';
|
||||||
import EServicesCategoryScreen from '@/screens/e-services/ui/EServicesCategoryScreen';
|
import EServicesCategoryScreen from '@/screens/e-services/ui/EServicesCategoryScreen';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function EServices() {
|
export default function EServices() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<>
|
||||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 80 }}
|
|
||||||
edges={['top']}
|
|
||||||
>
|
|
||||||
<CustomHeader />
|
<CustomHeader />
|
||||||
<EServicesCategoryScreen />
|
<EServicesCategoryScreen />
|
||||||
</SafeAreaView>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
// pages/home/index.tsx
|
// pages/home/index.tsx
|
||||||
import { useAuth } from '@/components/AuthProvider';
|
import { useAuth } from '@/components/AuthProvider';
|
||||||
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 HomeScreen from '@/screens/home/ui/HomeScreen';
|
import HomeScreen from '@/screens/home/ui/HomeScreen';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { isDark } = useTheme();
|
|
||||||
const { isAuthenticated, isLoading } = useAuth();
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -19,17 +16,13 @@ export default function Index() {
|
|||||||
}, [isAuthenticated, isLoading]);
|
}, [isAuthenticated, isLoading]);
|
||||||
|
|
||||||
if (isLoading || !isAuthenticated) {
|
if (isLoading || !isAuthenticated) {
|
||||||
return null; // Loading vaqtida yoki auth yo‘q bo‘lsa hech narsa ko‘rmasin
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterProvider>
|
<FilterProvider>
|
||||||
<SafeAreaView
|
<CustomHeader />
|
||||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 55 }}
|
<HomeScreen />
|
||||||
>
|
|
||||||
<CustomHeader />
|
|
||||||
<HomeScreen />
|
|
||||||
</SafeAreaView>
|
|
||||||
</FilterProvider>
|
</FilterProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
|
||||||
import { CustomHeader } from '@/components/ui/Header';
|
import { CustomHeader } from '@/components/ui/Header';
|
||||||
import Profile from '@/screens/profile/ui/ProfileScreen';
|
import Profile from '@/screens/profile/ui/ProfileScreen';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function ProfileScreen() {
|
export default function ProfileScreen() {
|
||||||
const { isDark } = useTheme();
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<>
|
||||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 40 }}
|
|
||||||
edges={['top']}
|
|
||||||
>
|
|
||||||
<CustomHeader logoutbtn={true} notif={false} />
|
<CustomHeader logoutbtn={true} notif={false} />
|
||||||
<Profile />
|
<Profile />
|
||||||
</SafeAreaView>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,36 @@ 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 { View } from 'react-native';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
useNotifications();
|
useNotifications();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* iOS status bar fon */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
height: insets.top,
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* StatusBar */}
|
||||||
|
<StatusBar
|
||||||
|
style="light"
|
||||||
|
backgroundColor="#000" // Android
|
||||||
|
/>
|
||||||
|
|
||||||
<Stack screenOptions={{ headerShown: false }} />
|
<Stack screenOptions={{ headerShown: false }} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return (
|
return (
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function PersonalInfoScreen() {
|
export default function PersonalInfoScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -71,6 +70,8 @@ export default function PersonalInfoScreen() {
|
|||||||
phone: string;
|
phone: string;
|
||||||
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
||||||
activate_types: number[];
|
activate_types: number[];
|
||||||
|
age: number | null;
|
||||||
|
gender: 'male' | 'female' | null;
|
||||||
}) => user_api.updateMe(body),
|
}) => user_api.updateMe(body),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['get_me'] });
|
queryClient.invalidateQueries({ queryKey: ['get_me'] });
|
||||||
@@ -106,7 +107,7 @@ export default function PersonalInfoScreen() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => router.push('/profile/personal-info')}>
|
<Pressable onPress={() => router.push('/profile/personal-info')}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -117,12 +118,12 @@ export default function PersonalInfoScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<ActivityIndicator size={'large'} />
|
<ActivityIndicator size={'large'} />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => router.push('/profile/personal-info')}>
|
<Pressable onPress={() => router.push('/profile/personal-info')}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -138,6 +139,8 @@ export default function PersonalInfoScreen() {
|
|||||||
phone: me.data.data.phone,
|
phone: me.data.data.phone,
|
||||||
industries: selectedCategories,
|
industries: selectedCategories,
|
||||||
activate_types,
|
activate_types,
|
||||||
|
age: me.data.data.age,
|
||||||
|
gender: me.data.data.gender,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -173,20 +176,21 @@ export default function PersonalInfoScreen() {
|
|||||||
setSelectedCategories={setSelectedCategories}
|
setSelectedCategories={setSelectedCategories}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
paddingBottom: 30,
|
||||||
},
|
},
|
||||||
tabsList: {
|
tabsList: {
|
||||||
maxHeight: 56,
|
maxHeight: 56,
|
||||||
},
|
},
|
||||||
tabsContainer: {
|
tabsContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 20,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
tabWrapper: {
|
tabWrapper: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { ManualTab } from '@/screens/profile/ui/ManualTab';
|
import { ManualTab } from '@/screens/profile/ui/ManualTab';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { View } from 'react-native';
|
||||||
|
|
||||||
export default function MyAds() {
|
export default function MyAds() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
<ManualTab />
|
<ManualTab />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { AnnouncementsTab } from '@/screens/profile/ui/AnnouncementsTab';
|
import { AnnouncementsTab } from '@/screens/profile/ui/AnnouncementsTab';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { View } from 'react-native';
|
||||||
|
|
||||||
export default function MyAds() {
|
export default function MyAds() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
<AnnouncementsTab />
|
<AnnouncementsTab />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { ReferralsTab } from '@/screens/profile/ui/RefferallsTab';
|
import { ReferralsTab } from '@/screens/profile/ui/RefferallsTab';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { View } from 'react-native';
|
||||||
|
|
||||||
export default function MyReffrals() {
|
export default function MyReffrals() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
<ReferralsTab />
|
<ReferralsTab />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { NotificationTab } from '@/screens/profile/ui/NotificationTab';
|
import { NotificationTab } from '@/screens/profile/ui/NotificationTab';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { View } from 'react-native';
|
||||||
|
|
||||||
export default function MyAds() {
|
export default function MyAds() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
<NotificationTab />
|
<NotificationTab />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function PersonalInfoScreen() {
|
export default function PersonalInfoScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -104,7 +103,7 @@ export default function PersonalInfoScreen() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => setIsEditing(false)}>
|
<Pressable onPress={() => setIsEditing(false)}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -115,12 +114,10 @@ export default function PersonalInfoScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<ActivityIndicator size={'large'} />
|
<ActivityIndicator size={'large'} />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===================== EDIT MODE ===================== */
|
|
||||||
const [showGenderOptions, setShowGenderOptions] = useState(false);
|
|
||||||
if (isEditing && editData) {
|
if (isEditing && editData) {
|
||||||
const genderOptions: { label: string; value: 'male' | 'female' }[] = [
|
const genderOptions: { label: string; value: 'male' | 'female' }[] = [
|
||||||
{ label: t('Erkak'), value: 'male' },
|
{ label: t('Erkak'), value: 'male' },
|
||||||
@@ -128,7 +125,7 @@ export default function PersonalInfoScreen() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => setIsEditing(false)}>
|
<Pressable onPress={() => setIsEditing(false)}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -225,12 +222,12 @@ export default function PersonalInfoScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/* ===================== VIEW MODE ===================== */
|
/* ===================== VIEW MODE ===================== */
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => router.push('/profile')}>
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -324,13 +321,14 @@ export default function PersonalInfoScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
paddingBottom: 30,
|
||||||
},
|
},
|
||||||
fieldsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 20 },
|
fieldsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 20 },
|
||||||
fieldChip: {
|
fieldChip: {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { useRouter } from 'expo-router';
|
|||||||
import { ChevronLeft, Moon, Sun } from 'lucide-react-native';
|
import { ChevronLeft, Moon, Sun } from 'lucide-react-native';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Pressable, ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
|
import { Pressable, ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -21,7 +20,7 @@ export default function SettingsScreen() {
|
|||||||
const selectLanguage = async (lang: string) => {
|
const selectLanguage = async (lang: string) => {
|
||||||
changeLanguage(lang as 'uz' | 'ru' | 'en');
|
changeLanguage(lang as 'uz' | 'ru' | 'en');
|
||||||
await i18n.changeLanguage(lang);
|
await i18n.changeLanguage(lang);
|
||||||
queryClient.invalidateQueries();
|
queryClient.resetQueries();
|
||||||
await saveLang(lang);
|
await saveLang(lang);
|
||||||
};
|
};
|
||||||
const { isDark, toggleTheme } = useTheme();
|
const { isDark, toggleTheme } = useTheme();
|
||||||
@@ -33,7 +32,7 @@ export default function SettingsScreen() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||||
<ScrollView contentContainerStyle={{ paddingBottom: 32 }}>
|
<ScrollView contentContainerStyle={{ paddingBottom: 32 }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@@ -102,7 +101,7 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
assets/announcements-video/video_en.webm
Normal file
BIN
assets/announcements-video/video_en.webm
Normal file
Binary file not shown.
BIN
assets/announcements-video/video_ru.webm
Normal file
BIN
assets/announcements-video/video_ru.webm
Normal file
Binary file not shown.
BIN
assets/announcements-video/video_uz.webm
Normal file
BIN
assets/announcements-video/video_uz.webm
Normal file
Binary file not shown.
BIN
assets/goverment/video_ru.webm
Normal file
BIN
assets/goverment/video_ru.webm
Normal file
Binary file not shown.
BIN
assets/images/one_click.png
Normal file
BIN
assets/images/one_click.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 435 KiB |
Binary file not shown.
BIN
assets/manual/manual_video_en.webm
Normal file
BIN
assets/manual/manual_video_en.webm
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/manual/manual_video_ru.webm
Normal file
BIN
assets/manual/manual_video_ru.webm
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/manual/manual_video_uz.webm
Normal file
BIN
assets/manual/manual_video_uz.webm
Normal file
Binary file not shown.
@@ -44,7 +44,9 @@ export default function CompanyList({ query }: { query: string }) {
|
|||||||
initialPageParam: 1,
|
initialPageParam: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const allCompanies = data?.pages.flatMap((page) => page.results) ?? [];
|
const allCompanies = data?.pages
|
||||||
|
.flatMap((page) => page.results)
|
||||||
|
.filter((company) => company.company_name);
|
||||||
|
|
||||||
const handlePresentModal = useCallback((company: CompanyResponse) => {
|
const handlePresentModal = useCallback((company: CompanyResponse) => {
|
||||||
setSelectedCompany(company);
|
setSelectedCompany(company);
|
||||||
@@ -79,6 +81,7 @@ export default function CompanyList({ query }: { query: string }) {
|
|||||||
<FlatList
|
<FlatList
|
||||||
data={allCompanies}
|
data={allCompanies}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
contentContainerStyle={{ paddingBottom: 70 }}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.card, isDark ? styles.darkCard : styles.lightCard]}
|
style={[styles.card, isDark ? styles.darkCard : styles.lightCard]}
|
||||||
@@ -92,14 +95,19 @@ export default function CompanyList({ query }: { query: string }) {
|
|||||||
</Text>
|
</Text>
|
||||||
<ChevronRight size={20} color={isDark ? '#64748b' : '#94a3b8'} />
|
<ChevronRight size={20} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.cardLocation}>
|
{item.country_name && (
|
||||||
<MapPin size={16} color={isDark ? '#64748b' : '#94a3b8'} />
|
<View style={styles.cardLocation}>
|
||||||
<Text
|
<MapPin size={16} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||||
style={[styles.cardLocationText, isDark ? styles.darkSubText : styles.lightSubText]}
|
<Text
|
||||||
>
|
style={[
|
||||||
{item.country_name}, {item.region_name}
|
styles.cardLocationText,
|
||||||
</Text>
|
isDark ? styles.darkSubText : styles.lightSubText,
|
||||||
</View>
|
]}
|
||||||
|
>
|
||||||
|
{item.country_name}, {item.region_name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
||||||
|
|||||||
@@ -75,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: 5 }}
|
contentContainerStyle={{ gap: 5, paddingBottom: 80 }}
|
||||||
onEndReached={loadMore}
|
onEndReached={loadMore}
|
||||||
onEndReachedThreshold={0.4}
|
onEndReachedThreshold={0.4}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import CategorySelect from './CategorySelect';
|
|||||||
interface FilterUIProps {
|
interface FilterUIProps {
|
||||||
back: () => void;
|
back: () => void;
|
||||||
onApply?: (data: any) => void;
|
onApply?: (data: any) => void;
|
||||||
setStep: Dispatch<SetStateAction<'filter' | 'items'>>;
|
setStep: (value: 'filter' | 'items') => void;
|
||||||
setFiltered: Dispatch<SetStateAction<any[]>>;
|
setFiltered: Dispatch<SetStateAction<any[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ export default function FilterUI({ back, onApply, setStep, setFiltered }: Filter
|
|||||||
}
|
}
|
||||||
|
|
||||||
let data: any[] = [];
|
let data: any[] = [];
|
||||||
let onSelect: (id: string) => void = () => {};
|
let onSelect: (id: string) => void = () => { };
|
||||||
let selectedId = '';
|
let selectedId = '';
|
||||||
|
|
||||||
switch (activeSheet) {
|
switch (activeSheet) {
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ const styles = StyleSheet.create({
|
|||||||
backButton: {
|
backButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 16,
|
marginBottom: 5,
|
||||||
padding: 10,
|
padding: 10,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
alignSelf: 'flex-start',
|
alignSelf: 'flex-start',
|
||||||
@@ -257,7 +257,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
},
|
},
|
||||||
backText: { fontSize: 16, color: '#3b82f6', fontWeight: '600' },
|
backText: { fontSize: 16, color: '#3b82f6', fontWeight: '600' },
|
||||||
listContainer: { gap: 12 },
|
listContainer: { gap: 12, paddingBottom: 50 },
|
||||||
itemCard: {
|
itemCard: {
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
@@ -293,7 +293,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Detail
|
// Detail
|
||||||
detailTitle: { fontSize: 26, fontWeight: '700', marginBottom: 16 },
|
detailTitle: { fontSize: 26, fontWeight: '700', marginBottom: 10 },
|
||||||
companyImage: {
|
companyImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 220,
|
height: 220,
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ export default function ProductList({ query }: Props) {
|
|||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
numColumns={2}
|
numColumns={2}
|
||||||
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 8 }}
|
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 20 }}
|
||||||
contentContainerStyle={{}}
|
contentContainerStyle={{ paddingBottom: 60 }}
|
||||||
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
||||||
onEndReachedThreshold={0.4}
|
onEndReachedThreshold={0.4}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface IRegisterDeviceBody {
|
|||||||
platform: string;
|
platform: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commonRequests = {
|
export const commonRequests = {
|
||||||
/**
|
/**
|
||||||
* Register device for notification
|
* Register device for notification
|
||||||
* @param body token
|
* @param body token
|
||||||
@@ -27,35 +27,39 @@ const commonRequests = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useNotifications() {
|
export function useNotifications() {
|
||||||
const notificationListener = useRef<any>(null);
|
const notificationListener = useRef<any>(null);
|
||||||
const responseListener = useRef<any>(null);
|
const responseListener = useRef<any>(null);
|
||||||
const queryClinet = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
useEffect(() => {
|
|
||||||
registerForPushNotificationsAsync().then((token) => {
|
|
||||||
if (!token) return null;
|
|
||||||
|
|
||||||
const body: IRegisterDeviceBody = {
|
useEffect(() => {
|
||||||
token: token,
|
// Android channel
|
||||||
platform: Platform.OS,
|
if (Platform.OS === 'android') {
|
||||||
};
|
Notifications.setNotificationChannelAsync('default', {
|
||||||
commonRequests.registerDevice(body);
|
name: 'default',
|
||||||
|
importance: Notifications.AndroidImportance.MAX,
|
||||||
|
vibrationPattern: [0, 250, 250, 250],
|
||||||
|
lightColor: '#FF231F7C',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foreground listener
|
||||||
|
notificationListener.current = Notifications.addNotificationReceivedListener((notification) => {
|
||||||
|
console.log('Notification received:', notification);
|
||||||
});
|
});
|
||||||
|
|
||||||
notificationListener.current = Notifications.addNotificationReceivedListener(
|
// User response listener
|
||||||
(notification) => {}
|
|
||||||
);
|
|
||||||
|
|
||||||
responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => {
|
responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => {
|
||||||
const data = response.notification.request.content.data;
|
const data = response.notification.request.content.data;
|
||||||
queryClinet.refetchQueries({ queryKey: ['notification-list'] });
|
queryClient.refetchQueries({ queryKey: ['notification-list'] });
|
||||||
queryClinet.refetchQueries({ queryKey: ['notifications-list'] });
|
queryClient.refetchQueries({ queryKey: ['notifications-list'] });
|
||||||
if (data?.screen === '/profile/notification') {
|
router.push('/profile/notification');
|
||||||
return;
|
});
|
||||||
} else {
|
|
||||||
router.push('/profile/notification');
|
// Token olish va serverga yuborish
|
||||||
}
|
registerForPushNotificationsAsync().then((token) => {
|
||||||
|
if (!token) return;
|
||||||
|
commonRequests.registerDevice({ token, platform: Platform.OS });
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"E'lon ma'lumotlari": "Announcement details",
|
"E'lon ma'lumotlari": "Announcement details",
|
||||||
"Manzil": "Address",
|
"Manzil": "Address",
|
||||||
"To'lov": "Payment",
|
"To'lov": "Payment",
|
||||||
"Yaratish": "Create",
|
"Yaratish": "Send",
|
||||||
"To'lash": "Pay",
|
"To'lash": "Pay",
|
||||||
"Keyingisi": "Next",
|
"Keyingisi": "Next",
|
||||||
"To'lov turini tanlang": "Select payment method",
|
"To'lov turini tanlang": "Select payment method",
|
||||||
@@ -134,7 +134,6 @@
|
|||||||
"Media yo'q": "No media",
|
"Media yo'q": "No media",
|
||||||
"Hozircha xizmatlar yo'q": "No services yet",
|
"Hozircha xizmatlar yo'q": "No services yet",
|
||||||
"Xizmat qo'shish": "Add service",
|
"Xizmat qo'shish": "Add service",
|
||||||
"Bekor qilish": "Cancel",
|
|
||||||
"Yangi xizmat (1/2)": "New service (1/2)",
|
"Yangi xizmat (1/2)": "New service (1/2)",
|
||||||
"Yangi xizmat (2/2)": "New service (2/2)",
|
"Yangi xizmat (2/2)": "New service (2/2)",
|
||||||
"Keyingi": "Next",
|
"Keyingi": "Next",
|
||||||
@@ -199,5 +198,13 @@
|
|||||||
"Davlat xizmatlari kategoriyalari": "Categories of government services",
|
"Davlat xizmatlari kategoriyalari": "Categories of government services",
|
||||||
"Kerakli xizmat turini tanlang": "Select the desired service type",
|
"Kerakli xizmat turini tanlang": "Select the desired service type",
|
||||||
"Bu kategoriya bo'yicha xizmat topilmadi": "No service in this category",
|
"Bu kategoriya bo'yicha xizmat topilmadi": "No service in this category",
|
||||||
"Tez orada xizmat qo'shiladi": "Service will be added soon"
|
"Tez orada xizmat qo'shiladi": "Service will be added soon",
|
||||||
|
"Maʼlumot topilmadi": "Data not found",
|
||||||
|
"Hozircha hech qanday eʼlon mavjud emas": "No announcements yet",
|
||||||
|
"Yangilash": "Update",
|
||||||
|
"Jo'natish": "Send",
|
||||||
|
"Bir Zumda Jonatish": "Express Send Info",
|
||||||
|
"Hammasi": "All",
|
||||||
|
"Bekor qilish": "Cancel",
|
||||||
|
"Barchasi o'qildi": "Mark All as Read"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"E'lon ma'lumotlari": "Информация об объявлении",
|
"E'lon ma'lumotlari": "Информация об объявлении",
|
||||||
"Manzil": "Адрес",
|
"Manzil": "Адрес",
|
||||||
"To'lov": "Оплата",
|
"To'lov": "Оплата",
|
||||||
"Yaratish": "Создать",
|
"Yaratish": "Отправить",
|
||||||
"To'lash": "Оплатить",
|
"To'lash": "Оплатить",
|
||||||
"Keyingisi": "Далее",
|
"Keyingisi": "Далее",
|
||||||
"To'lov turini tanlang": "Выберите способ оплаты",
|
"To'lov turini tanlang": "Выберите способ оплаты",
|
||||||
@@ -134,7 +134,6 @@
|
|||||||
"Media yo'q": "Нет медиа",
|
"Media yo'q": "Нет медиа",
|
||||||
"Hozircha xizmatlar yo'q": "Пока нет услуг",
|
"Hozircha xizmatlar yo'q": "Пока нет услуг",
|
||||||
"Xizmat qo'shish": "Добавить услугу",
|
"Xizmat qo'shish": "Добавить услугу",
|
||||||
"Bekor qilish": "Отмена",
|
|
||||||
"Yangi xizmat (1/2)": "Новая услуга (1/2)",
|
"Yangi xizmat (1/2)": "Новая услуга (1/2)",
|
||||||
"Yangi xizmat (2/2)": "Новая услуга (2/2)",
|
"Yangi xizmat (2/2)": "Новая услуга (2/2)",
|
||||||
"Keyingi": "Далее",
|
"Keyingi": "Далее",
|
||||||
@@ -198,5 +197,13 @@
|
|||||||
"Davlat xizmatlari kategoriyalari": "Категории государственных услуг",
|
"Davlat xizmatlari kategoriyalari": "Категории государственных услуг",
|
||||||
"Kerakli xizmat turini tanlang": "Выберите нужный тип услуги",
|
"Kerakli xizmat turini tanlang": "Выберите нужный тип услуги",
|
||||||
"Bu kategoriya bo'yicha xizmat topilmadi": "Сервис в этой категории не найден.",
|
"Bu kategoriya bo'yicha xizmat topilmadi": "Сервис в этой категории не найден.",
|
||||||
"Tez orada xizmat qo'shiladi": "Сервис скоро будет добавлен"
|
"Tez orada xizmat qo'shiladi": "Сервис скоро будет добавлен",
|
||||||
|
"Maʼlumot topilmadi": "Информация не найдена",
|
||||||
|
"Hozircha hech qanday eʼlon mavjud emas": "Пока нет объявлений",
|
||||||
|
"Yangilash": "Обновление",
|
||||||
|
"Jo'natish": "Отправить",
|
||||||
|
"Bir Zumda Jonatish": "Экспресс Рассылки Инфо",
|
||||||
|
"Hammasi": "Все",
|
||||||
|
"Bekor qilish": "Отменить",
|
||||||
|
"Barchasi o'qildi": "Отметить все как прочитанное"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
"E'lon ma'lumotlari": "E'lon ma'lumotlari",
|
"E'lon ma'lumotlari": "E'lon ma'lumotlari",
|
||||||
"Manzil": "Manzil",
|
"Manzil": "Manzil",
|
||||||
"To'lov": "To'lov",
|
"To'lov": "To'lov",
|
||||||
"Yaratish": "Yaratish",
|
"Yaratish": "Yuborish",
|
||||||
"To'lash": "To'lash",
|
"To'lash": "To'lash",
|
||||||
"Keyingisi": "Keyingisi",
|
"Keyingisi": "Keyingisi",
|
||||||
"To'lov turini tanlang": "To'lov turini tanlang",
|
"To'lov turini tanlang": "To'lov turini tanlang",
|
||||||
@@ -135,7 +135,6 @@
|
|||||||
"Yangilashda xato yuz berdi": "Yangilashda xato yuz berdi",
|
"Yangilashda xato yuz berdi": "Yangilashda xato yuz berdi",
|
||||||
"Hozircha xizmatlar yo'q": "Hozircha xizmatlar yo'q",
|
"Hozircha xizmatlar yo'q": "Hozircha xizmatlar yo'q",
|
||||||
"Xizmat qo'shish": "Xizmat qo'shish",
|
"Xizmat qo'shish": "Xizmat qo'shish",
|
||||||
"Bekor qilish": "Bekor qilish",
|
|
||||||
"Yangi xizmat (1/2)": "Yangi xizmat (1/2)",
|
"Yangi xizmat (1/2)": "Yangi xizmat (1/2)",
|
||||||
"Yangi xizmat (2/2)": "Yangi xizmat (2/2)",
|
"Yangi xizmat (2/2)": "Yangi xizmat (2/2)",
|
||||||
"Keyingi": "Keyingi",
|
"Keyingi": "Keyingi",
|
||||||
@@ -198,5 +197,13 @@
|
|||||||
"Davlat xizmatlari kategoriyalari": "Davlat xizmatlari kategoriyalari",
|
"Davlat xizmatlari kategoriyalari": "Davlat xizmatlari kategoriyalari",
|
||||||
"Kerakli xizmat turini tanlang": "Kerakli xizmat turini tanlang",
|
"Kerakli xizmat turini tanlang": "Kerakli xizmat turini tanlang",
|
||||||
"Bu kategoriya bo'yicha xizmat topilmadi": "Bu kategoriya bo'yicha xizmat topilmadi",
|
"Bu kategoriya bo'yicha xizmat topilmadi": "Bu kategoriya bo'yicha xizmat topilmadi",
|
||||||
"Tez orada xizmat qo'shiladi": "Tez orada xizmat qo'shiladi"
|
"Tez orada xizmat qo'shiladi": "Tez orada xizmat qo'shiladi",
|
||||||
|
"Maʼlumot topilmadi": "Maʼlumot topilmadi",
|
||||||
|
"Hozircha hech qanday eʼlon mavjud emas": "Hozircha hech qanday eʼlon mavjud emas",
|
||||||
|
"Yangilash": "Yangilash",
|
||||||
|
"Jo'natish": "Jo'natish",
|
||||||
|
"Bir Zumda Jonatish": "Bir Zumda Jonatish",
|
||||||
|
"Hammasi": "Hammasi",
|
||||||
|
"Bekor qilish": "Bekor qilish",
|
||||||
|
"Barchasi o'qildi": "Barchasi o'qildi"
|
||||||
}
|
}
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -39,6 +39,7 @@
|
|||||||
"expo-navigation-bar": "~5.0.10",
|
"expo-navigation-bar": "~5.0.10",
|
||||||
"expo-notifications": "~0.32.16",
|
"expo-notifications": "~0.32.16",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
|
"expo-sharing": "~14.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-symbols": "~1.0.8",
|
"expo-symbols": "~1.0.8",
|
||||||
@@ -10230,6 +10231,15 @@
|
|||||||
"node": ">=20.16.0"
|
"node": ">=20.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-sharing": {
|
||||||
|
"version": "14.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-14.0.8.tgz",
|
||||||
|
"integrity": "sha512-A1pPr2iBrxypFDCWVAESk532HK+db7MFXbvO2sCV9ienaFXAk7lIBm6bkqgE6vzRd9O3RGdEGzYx80cYlc089Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-splash-screen": {
|
"node_modules/expo-splash-screen": {
|
||||||
"version": "31.0.13",
|
"version": "31.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.13.tgz",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"expo-navigation-bar": "~5.0.10",
|
"expo-navigation-bar": "~5.0.10",
|
||||||
"expo-notifications": "~0.32.16",
|
"expo-notifications": "~0.32.16",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
|
"expo-sharing": "~14.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-symbols": "~1.0.8",
|
"expo-symbols": "~1.0.8",
|
||||||
|
|||||||
@@ -18,16 +18,15 @@ export interface AnnouncementListBodyRes {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
total_view_count: number;
|
total_view_count: number;
|
||||||
files: [
|
files: {
|
||||||
{
|
id: number;
|
||||||
file: string;
|
file: string;
|
||||||
}
|
}[];
|
||||||
];
|
|
||||||
status: 'pending' | 'paid' | 'verified' | 'canceled';
|
status: 'pending' | 'paid' | 'verified' | 'canceled';
|
||||||
types: {
|
types: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
icon_name: string;
|
icon_name: string | null;
|
||||||
}[];
|
}[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,86 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useVideoPlayer, VideoPlayer, VideoView } from 'expo-video';
|
||||||
|
import { Play } from 'lucide-react-native';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ActivityIndicator, Animated, RefreshControl, StyleSheet, Text, View } from 'react-native';
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Animated,
|
||||||
|
RefreshControl,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { announcement_api } from '../lib/api';
|
import { announcement_api } from '../lib/api';
|
||||||
import { AnnouncementListBodyRes } from '../lib/type';
|
|
||||||
import AnnouncementCard from './AnnouncementCard';
|
import AnnouncementCard from './AnnouncementCard';
|
||||||
import EmptyState from './EmptyState';
|
import EmptyState from './EmptyState';
|
||||||
|
|
||||||
|
function VideoCard({ player }: { player: VideoPlayer }) {
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = player.addListener('playingChange', (state) => {
|
||||||
|
setIsPlaying(state.isPlaying);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.remove();
|
||||||
|
};
|
||||||
|
}, [player]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<VideoView player={player} style={styles.video} contentFit="contain" />
|
||||||
|
|
||||||
|
{!isPlaying && (
|
||||||
|
<View style={styles.playOverlay}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.playButton}
|
||||||
|
onPress={() => {
|
||||||
|
player.play();
|
||||||
|
setIsPlaying(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play color="white" size={26} fill="black" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function DashboardScreen() {
|
export default function DashboardScreen() {
|
||||||
const [announcements, setAnnouncements] = useState<AnnouncementListBodyRes[]>([]);
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const userLang = i18n.language.startsWith('ru')
|
||||||
|
? 'ru'
|
||||||
|
: i18n.language.startsWith('en')
|
||||||
|
? 'en'
|
||||||
|
: 'uz';
|
||||||
|
|
||||||
|
const [selectedLang, setSelectedLang] = useState<'uz' | 'ru' | 'en'>(userLang);
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
background: isDark ? '#0f172a' : '#f8fafc',
|
background: isDark ? '#0f172a' : '#f8fafc',
|
||||||
primary: '#2563eb',
|
primary: '#2563eb',
|
||||||
|
text: isDark ? '#f8fafc' : '#0f172a',
|
||||||
loaderBg: isDark ? '#0f172a' : '#ffffff',
|
loaderBg: isDark ? '#0f172a' : '#ffffff',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Announcements query
|
||||||
const { data, isLoading, isRefetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
const { data, isLoading, isRefetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
||||||
queryKey: ['announcements_list'],
|
queryKey: ['announcements_list'],
|
||||||
queryFn: async ({ pageParam = 1 }) => {
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const res = await announcement_api.list({ page: pageParam, page_size: 10 });
|
const res = await announcement_api.list({
|
||||||
|
page: pageParam,
|
||||||
|
page_size: 10,
|
||||||
|
});
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) =>
|
getNextPageParam: (lastPage) =>
|
||||||
@@ -36,8 +91,6 @@ export default function DashboardScreen() {
|
|||||||
const allAnnouncements = data?.pages.flatMap((p) => p.results) ?? [];
|
const allAnnouncements = data?.pages.flatMap((p) => p.results) ?? [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAnnouncements(allAnnouncements);
|
|
||||||
|
|
||||||
fadeAnim.setValue(0);
|
fadeAnim.setValue(0);
|
||||||
Animated.timing(fadeAnim, {
|
Animated.timing(fadeAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
@@ -46,6 +99,44 @@ export default function DashboardScreen() {
|
|||||||
}).start();
|
}).start();
|
||||||
}, [allAnnouncements]);
|
}, [allAnnouncements]);
|
||||||
|
|
||||||
|
// Announcement videos
|
||||||
|
const videos = {
|
||||||
|
uz: require('@/assets/announcements-video/video_uz.webm'),
|
||||||
|
ru: require('@/assets/announcements-video/video_ru.webm'),
|
||||||
|
en: require('@/assets/announcements-video/video_en.webm'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Government videos: faqat RU mavjud
|
||||||
|
const govermentVideos: Partial<Record<'uz' | 'ru' | 'en', any>> = {
|
||||||
|
ru: require('@/assets/goverment/video_ru.webm'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update selected language
|
||||||
|
useEffect(() => {
|
||||||
|
const lang = i18n.language.startsWith('ru')
|
||||||
|
? 'ru'
|
||||||
|
: i18n.language.startsWith('en')
|
||||||
|
? 'en'
|
||||||
|
: 'uz';
|
||||||
|
setSelectedLang(lang);
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
|
// 🔹 Hooks: conditional emas, har doim chaqiriladi
|
||||||
|
const player = useVideoPlayer(videos[selectedLang], (player) => {
|
||||||
|
player.loop = false;
|
||||||
|
player.volume = 1;
|
||||||
|
player.muted = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const govermentVideoSource = govermentVideos[selectedLang] ?? null;
|
||||||
|
|
||||||
|
const player2 = useVideoPlayer(govermentVideoSource, (player) => {
|
||||||
|
if (!govermentVideoSource) return; // no video, do nothing
|
||||||
|
player.loop = false;
|
||||||
|
player.volume = 1;
|
||||||
|
player.muted = false;
|
||||||
|
});
|
||||||
|
|
||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
queryClient.refetchQueries({ queryKey: ['announcements_list'] });
|
queryClient.refetchQueries({ queryKey: ['announcements_list'] });
|
||||||
};
|
};
|
||||||
@@ -54,6 +145,21 @@ export default function DashboardScreen() {
|
|||||||
if (hasNextPage) fetchNextPage();
|
if (hasNextPage) fetchNextPage();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const videoItems = [
|
||||||
|
{ id: '1', player },
|
||||||
|
govermentVideoSource && { id: '2', player: player2 },
|
||||||
|
].filter(Boolean) as { id: string; player: VideoPlayer }[];
|
||||||
|
|
||||||
|
const renderVideoHeader = () => (
|
||||||
|
<View style={{ marginBottom: 8 }}>
|
||||||
|
{videoItems.map((item) => (
|
||||||
|
<View key={item.id} style={styles.videoContainer}>
|
||||||
|
<VideoCard player={item.player} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
@@ -66,33 +172,33 @@ export default function DashboardScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<Text style={{ color: 'white', fontSize: 20, marginBottom: 10 }}>
|
<Text style={{ color: theme.text, fontSize: 20, marginVertical: 16 }}>
|
||||||
{t("E'lonlar ro'yxati")}
|
{t("E'lonlar ro'yxati")}
|
||||||
</Text>
|
</Text>
|
||||||
{announcements.length > 0 ? (
|
|
||||||
<Animated.FlatList
|
<Animated.FlatList
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1, opacity: fadeAnim }}
|
||||||
data={announcements}
|
data={allAnnouncements}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
numColumns={2}
|
numColumns={2}
|
||||||
columnWrapperStyle={styles.columnWrapper}
|
columnWrapperStyle={styles.columnWrapper}
|
||||||
renderItem={({ item }) => <AnnouncementCard announcement={item} />}
|
renderItem={({ item }) => <AnnouncementCard announcement={item} />}
|
||||||
refreshControl={
|
ListHeaderComponent={renderVideoHeader}
|
||||||
<RefreshControl
|
refreshControl={
|
||||||
refreshing={isRefetching}
|
<RefreshControl
|
||||||
onRefresh={onRefresh}
|
refreshing={isRefetching}
|
||||||
colors={[theme.primary]}
|
onRefresh={onRefresh}
|
||||||
tintColor={theme.primary}
|
colors={[theme.primary]}
|
||||||
progressBackgroundColor={theme.background}
|
tintColor={theme.primary}
|
||||||
/>
|
progressBackgroundColor={theme.background}
|
||||||
}
|
/>
|
||||||
onEndReached={loadMore}
|
}
|
||||||
onEndReachedThreshold={0.3}
|
ListEmptyComponent={<EmptyState onRefresh={onRefresh} isRefreshing={isRefetching} />}
|
||||||
showsVerticalScrollIndicator={false}
|
onEndReached={loadMore}
|
||||||
/>
|
onEndReachedThreshold={0.3}
|
||||||
) : (
|
contentContainerStyle={{ paddingBottom: 80 }}
|
||||||
<EmptyState onRefresh={onRefresh} isRefreshing={isRefetching} />
|
showsVerticalScrollIndicator={false}
|
||||||
)}
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,7 +207,6 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
marginTop: 20,
|
|
||||||
},
|
},
|
||||||
loaderBox: {
|
loaderBox: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -112,4 +217,33 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
|
videoContainer: {
|
||||||
|
width: '100%',
|
||||||
|
height: 250,
|
||||||
|
marginBottom: 8,
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
playOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
playButton: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: 50,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useTheme } from '@/components/ThemeContext';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -18,7 +19,7 @@ export default function EmptyState({
|
|||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
const theme = {
|
const theme = {
|
||||||
gradientColors: isDark
|
gradientColors: isDark
|
||||||
? (['#1e293b', '#334155'] as [string, string])
|
? (['#1e293b', '#334155'] as [string, string])
|
||||||
@@ -37,8 +38,8 @@ export default function EmptyState({
|
|||||||
<Ionicons name="megaphone-outline" size={64} color={theme.iconColor} />
|
<Ionicons name="megaphone-outline" size={64} color={theme.iconColor} />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
||||||
<Text style={[emptyStyles.title, { color: theme.title }]}>{title}</Text>
|
<Text style={[emptyStyles.title, { color: theme.title }]}>{t(title)}</Text>
|
||||||
<Text style={[emptyStyles.description, { color: theme.description }]}>{description}</Text>
|
<Text style={[emptyStyles.description, { color: theme.description }]}>{t(description)}</Text>
|
||||||
|
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -52,7 +53,9 @@ export default function EmptyState({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Ionicons name="refresh" size={20} color={theme.buttonText} />
|
<Ionicons name="refresh" size={20} color={theme.buttonText} />
|
||||||
<Text style={[emptyStyles.refreshText, { color: theme.buttonText }]}>Yangilash</Text>
|
<Text style={[emptyStyles.refreshText, { color: theme.buttonText }]}>
|
||||||
|
{t('Yangilash')}
|
||||||
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useAuth } from '@/components/AuthProvider';
|
import { useAuth } from '@/components/AuthProvider';
|
||||||
|
import { registerForPushNotificationsAsync } from '@/components/NotificationProvider';
|
||||||
import AuthHeader from '@/components/ui/AuthHeader';
|
import AuthHeader from '@/components/ui/AuthHeader';
|
||||||
|
import { commonRequests } from '@/hooks/useNotifications';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Redirect, useRouter } from 'expo-router';
|
import { Redirect, useRouter } from 'expo-router';
|
||||||
@@ -26,6 +28,7 @@ import ConfirmForm from './ConfirmForm';
|
|||||||
|
|
||||||
const ConfirmScreen = () => {
|
const ConfirmScreen = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
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();
|
||||||
@@ -59,11 +62,27 @@ const ConfirmScreen = () => {
|
|||||||
const { mutate, isPending } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: (body: { code: string; phone: string }) => auth_api.verify_otp(body),
|
mutationFn: (body: { code: string; phone: string }) => auth_api.verify_otp(body),
|
||||||
onSuccess: async (res) => {
|
onSuccess: async (res) => {
|
||||||
|
// Tokenlarni saqlash
|
||||||
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);
|
savedToken(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);
|
||||||
|
await login(res.data.data.token.access);
|
||||||
|
|
||||||
|
// **Push tokenni qayta serverga yuborish**
|
||||||
|
const pushToken = await registerForPushNotificationsAsync();
|
||||||
|
if (pushToken) {
|
||||||
|
await commonRequests.registerDevice({
|
||||||
|
token: pushToken,
|
||||||
|
platform: Platform.OS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification querylarni refetch
|
||||||
|
queryClient.refetchQueries({ queryKey: ['notification-list'] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ['notifications-list'] });
|
||||||
|
|
||||||
|
// Dashboardga yo‘naltirish
|
||||||
router.replace('/(dashboard)');
|
router.replace('/(dashboard)');
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: any) => {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useAuth } from '@/components/AuthProvider';
|
import { useAuth } from '@/components/AuthProvider';
|
||||||
|
import { registerForPushNotificationsAsync } from '@/components/NotificationProvider';
|
||||||
import AuthHeader from '@/components/ui/AuthHeader';
|
import AuthHeader from '@/components/ui/AuthHeader';
|
||||||
|
import { commonRequests } from '@/hooks/useNotifications';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Redirect, useRouter } from 'expo-router';
|
import { Redirect, useRouter } from 'expo-router';
|
||||||
@@ -29,6 +31,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 queryClient = useQueryClient();
|
||||||
const { savedToken } = useTokenStore();
|
const { savedToken } = useTokenStore();
|
||||||
|
|
||||||
const [resendTimer, setResendTimer] = useState<number>(60);
|
const [resendTimer, setResendTimer] = useState<number>(60);
|
||||||
@@ -64,6 +67,17 @@ const RegisterConfirmScreen = () => {
|
|||||||
savedToken(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);
|
||||||
|
const pushToken = await registerForPushNotificationsAsync();
|
||||||
|
if (pushToken) {
|
||||||
|
await commonRequests.registerDevice({
|
||||||
|
token: pushToken,
|
||||||
|
platform: Platform.OS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification querylarni refetch
|
||||||
|
queryClient.refetchQueries({ queryKey: ['notification-list'] });
|
||||||
|
queryClient.refetchQueries({ queryKey: ['notifications-list'] });
|
||||||
router.replace('/(dashboard)');
|
router.replace('/(dashboard)');
|
||||||
},
|
},
|
||||||
onError: (err: any) => {
|
onError: (err: any) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
BottomSheetModal,
|
BottomSheetModal,
|
||||||
BottomSheetScrollView,
|
BottomSheetScrollView,
|
||||||
} from '@gorhom/bottom-sheet';
|
} from '@gorhom/bottom-sheet';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
@@ -12,6 +13,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|||||||
export type Option = {
|
export type Option = {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
flag?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CategorySelectorProps = {
|
type CategorySelectorProps = {
|
||||||
@@ -91,6 +93,12 @@ export default function CategorySelectorBottomSheet({
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{item.flag ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: `https://flagcdn.com/24x18/${item.flag.toLowerCase()}.png` }}
|
||||||
|
style={{ width: 24, height: 18, borderRadius: 2 }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.optionText,
|
styles.optionText,
|
||||||
@@ -129,6 +137,10 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
},
|
},
|
||||||
optionText: {
|
optionText: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
|
import OneClick from '@/assets/images/one_click.png';
|
||||||
import PAYME from '@/assets/images/Payme_NEW.png';
|
import PAYME from '@/assets/images/Payme_NEW.png';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { price_calculation } from '../lib/api';
|
import { price_calculation } from '../lib/api';
|
||||||
@@ -210,14 +211,13 @@ export default function CreateAdsScreens() {
|
|||||||
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
|
style={[styles.safeArea, isDark ? styles.darkBg : styles.lightBg]}
|
||||||
>
|
>
|
||||||
<ScrollView contentContainerStyle={[styles.container, { paddingBottom: 90 }]}>
|
<ScrollView contentContainerStyle={[styles.container, { paddingBottom: 90 }]}>
|
||||||
|
<Image
|
||||||
|
source={OneClick}
|
||||||
|
style={{ width: 180, height: 56, marginBottom: 10 }}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
|
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
|
||||||
{currentStep === 1
|
{t("Bir Zumda Jonatish")}
|
||||||
? t("E'lon ma'lumotlari")
|
|
||||||
: currentStep === 2
|
|
||||||
? t('Sohalar')
|
|
||||||
: currentStep === 3
|
|
||||||
? t('Manzil')
|
|
||||||
: t("To'lov")}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
@@ -334,7 +334,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#f8fafc',
|
backgroundColor: '#f8fafc',
|
||||||
},
|
},
|
||||||
container: { padding: 20 },
|
container: { padding: 20 },
|
||||||
title: { fontSize: 22, fontWeight: '800', marginBottom: 20 },
|
title: { fontSize: 18, fontWeight: '800', marginBottom: 20 },
|
||||||
darkText: {
|
darkText: {
|
||||||
color: '#f1f5f9',
|
color: '#f1f5f9',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'r
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
import { Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
type MediaType = { uri: string; type: 'image' | 'video' };
|
|
||||||
type StepProps = { formData: any; updateForm: (key: string, value: any) => void };
|
type StepProps = { formData: any; updateForm: (key: string, value: any) => void };
|
||||||
|
|
||||||
type Errors = {
|
type Errors = {
|
||||||
@@ -302,7 +301,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
height: 56,
|
height: 56,
|
||||||
},
|
},
|
||||||
textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 12 },
|
textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 0 },
|
||||||
input: { flex: 1, fontSize: 16 },
|
input: { flex: 1, fontSize: 16 },
|
||||||
prefixContainer: {
|
prefixContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -76,6 +76,17 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleSelectAllCompanies = () => {
|
||||||
|
const selected = formData.company || [];
|
||||||
|
if (selected.length === corporations.length) {
|
||||||
|
// Deselect all
|
||||||
|
updateForm('company', []);
|
||||||
|
} else {
|
||||||
|
// Select all
|
||||||
|
updateForm('company', corporations);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const country = statesData?.find((c) => c.code === formData.country);
|
const country = statesData?.find((c) => c.code === formData.country);
|
||||||
setRegions(country?.region || []);
|
setRegions(country?.region || []);
|
||||||
@@ -90,13 +101,19 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
|
|||||||
const country = statesData?.find((c) => c.code === formData.country);
|
const country = statesData?.find((c) => c.code === formData.country);
|
||||||
const region = country?.region.find((r) => r.code === formData.region);
|
const region = country?.region.find((r) => r.code === formData.region);
|
||||||
setDistricts(region?.districts || []);
|
setDistricts(region?.districts || []);
|
||||||
if (!region?.districts.some((d) => d.code === formData.district)) {
|
|
||||||
|
// If region is 'all', automatically set district to 'all'
|
||||||
|
if (formData.region === 'all') {
|
||||||
|
updateForm('district', 'all');
|
||||||
|
} else if (!region?.districts.some((d) => d.code === formData.district)) {
|
||||||
updateForm('district', '');
|
updateForm('district', '');
|
||||||
}
|
}
|
||||||
}, [formData.region, formData.country, statesData]);
|
}, [formData.region, formData.country, statesData]);
|
||||||
|
|
||||||
const getLabel = (arr: { name: string; code: string }[], val: string) =>
|
const getLabel = (arr: { name: string; code: string }[], val: string) => {
|
||||||
arr.find((item) => item.code === val)?.name || t('— Tanlang —');
|
if (val === 'all') return t('Hammasi');
|
||||||
|
return arr.find((item) => item.code === val)?.name || t('— Tanlang —');
|
||||||
|
};
|
||||||
|
|
||||||
const renderCompanyItem = ({ item }: ListRenderItemInfo<{ id: number; latter: string }>) => {
|
const renderCompanyItem = ({ item }: ListRenderItemInfo<{ id: number; latter: string }>) => {
|
||||||
const isSelected = formData.company?.some((c: any) => c.id === item.id);
|
const isSelected = formData.company?.some((c: any) => c.id === item.id);
|
||||||
@@ -157,7 +174,13 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.pickerButton,
|
styles.pickerButton,
|
||||||
{ backgroundColor: theme.cardBg, borderColor: theme.cardBorder },
|
{
|
||||||
|
backgroundColor: theme.cardBg,
|
||||||
|
borderColor: theme.cardBorder,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => setShowCountry(true)}
|
onPress={() => setShowCountry(true)}
|
||||||
>
|
>
|
||||||
@@ -190,9 +213,18 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.pickerButton,
|
styles.pickerButton,
|
||||||
{ backgroundColor: theme.cardBg, borderColor: theme.cardBorder },
|
{
|
||||||
|
backgroundColor: theme.cardBg,
|
||||||
|
borderColor: theme.cardBorder,
|
||||||
|
opacity: formData.region === 'all' ? 0.5 : 1,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => setShowDistrict(true)}
|
onPress={() => {
|
||||||
|
if (formData.region !== 'all') {
|
||||||
|
setShowDistrict(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={formData.region === 'all'}
|
||||||
>
|
>
|
||||||
<Text style={[styles.pickerText, { color: theme.text }]}>
|
<Text style={[styles.pickerText, { color: theme.text }]}>
|
||||||
{getLabel(
|
{getLabel(
|
||||||
@@ -202,9 +234,34 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
{t('Reklama joylashtirish kompaniyasi')}
|
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
||||||
</Text>
|
{t('Reklama joylashtirish kompaniyasi')}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.selectAllButton,
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
formData.company?.length === corporations.length ? theme.primary : theme.cardBg,
|
||||||
|
borderColor: theme.primary,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPress={toggleSelectAllCompanies}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.selectAllText,
|
||||||
|
{
|
||||||
|
color:
|
||||||
|
formData.company?.length === corporations.length ? '#fff' : theme.primary,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{formData.company?.length === corporations.length ? t('Bekor qilish') : t('Hammasi')}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={corporations}
|
data={corporations}
|
||||||
renderItem={renderCompanyItem}
|
renderItem={renderCompanyItem}
|
||||||
@@ -232,21 +289,29 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) =>
|
|||||||
isOpen={showCountry}
|
isOpen={showCountry}
|
||||||
onClose={() => setShowCountry(false)}
|
onClose={() => setShowCountry(false)}
|
||||||
selectedValue={formData.country}
|
selectedValue={formData.country}
|
||||||
data={statesData ? statesData.map((c) => ({ label: c.name, value: c.code })) : []}
|
data={
|
||||||
|
statesData ? statesData.map((c) => ({ label: c.name, value: c.code, flag: c.flag })) : []
|
||||||
|
}
|
||||||
onSelect={(v) => updateForm('country', v)}
|
onSelect={(v) => updateForm('country', v)}
|
||||||
/>
|
/>
|
||||||
<CategorySelectorBottomSheet
|
<CategorySelectorBottomSheet
|
||||||
isOpen={showRegion}
|
isOpen={showRegion}
|
||||||
onClose={() => setShowRegion(false)}
|
onClose={() => setShowRegion(false)}
|
||||||
selectedValue={formData.region}
|
selectedValue={formData.region}
|
||||||
data={regions.map((r) => ({ label: r.name, value: r.code }))}
|
data={[
|
||||||
|
{ label: t('Hammasi'), value: 'all' },
|
||||||
|
...regions.map((r) => ({ label: r.name, value: r.code })),
|
||||||
|
]}
|
||||||
onSelect={(v) => updateForm('region', v)}
|
onSelect={(v) => updateForm('region', v)}
|
||||||
/>
|
/>
|
||||||
<CategorySelectorBottomSheet
|
<CategorySelectorBottomSheet
|
||||||
isOpen={showDistrict}
|
isOpen={showDistrict}
|
||||||
onClose={() => setShowDistrict(false)}
|
onClose={() => setShowDistrict(false)}
|
||||||
selectedValue={formData.district}
|
selectedValue={formData.district}
|
||||||
data={districts.map((d) => ({ label: d.name, value: d.code }))}
|
data={[
|
||||||
|
{ label: t('Hammasi'), value: 'all' },
|
||||||
|
...districts.map((d) => ({ label: d.name, value: d.code })),
|
||||||
|
]}
|
||||||
onSelect={(v) => updateForm('district', v)}
|
onSelect={(v) => updateForm('district', v)}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -286,4 +351,14 @@ const styles = StyleSheet.create({
|
|||||||
priceLine: { fontSize: 15 },
|
priceLine: { fontSize: 15 },
|
||||||
totalPrice: { fontSize: 18, fontWeight: '700', marginTop: 6 },
|
totalPrice: { fontSize: 18, fontWeight: '700', marginTop: 6 },
|
||||||
error: { fontWeight: '600', marginBottom: 10 },
|
error: { fontWeight: '600', marginBottom: 10 },
|
||||||
|
selectAllButton: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
selectAllText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Image } from 'expo-image';
|
|||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { ChevronLeft, XIcon } from 'lucide-react-native';
|
import { ChevronLeft, XIcon } from 'lucide-react-native';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { ActivityIndicator, FlatList, Modal, Text, TouchableOpacity, View } from 'react-native';
|
import { ActivityIndicator, FlatList, Modal, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { RefreshControl } from 'react-native-gesture-handler';
|
import { RefreshControl } from 'react-native-gesture-handler';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
@@ -14,16 +13,15 @@ import { eservices_api } from '../lib/api';
|
|||||||
|
|
||||||
const dark = {
|
const dark = {
|
||||||
bg: '#0f172a',
|
bg: '#0f172a',
|
||||||
card: '#1f2937',
|
card: '#334155',
|
||||||
border: '#1e293b',
|
border: '#1e293b',
|
||||||
muted: '#334155',
|
muted: '#334155',
|
||||||
text: '#f8fafc',
|
text: '#E5B037',
|
||||||
subText: '#cbd5f5',
|
subText: '#0B0F2C',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EServicesCategoryScreen() {
|
export default function EServicesCategoryScreen() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { t } = useTranslation();
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const webviewRef = useRef<WebView>(null);
|
const webviewRef = useRef<WebView>(null);
|
||||||
const [webUrl, setWebUrl] = React.useState<string | null>(null);
|
const [webUrl, setWebUrl] = React.useState<string | null>(null);
|
||||||
@@ -88,7 +86,7 @@ export default function EServicesCategoryScreen() {
|
|||||||
<FlatList
|
<FlatList
|
||||||
data={categories}
|
data={categories}
|
||||||
keyExtractor={(item) => `${item.id}-${item.name}`}
|
keyExtractor={(item) => `${item.id}-${item.name}`}
|
||||||
contentContainerStyle={{ gap: 12 }}
|
contentContainerStyle={{ gap: 12, paddingBottom: 80 }}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
@@ -97,52 +95,34 @@ export default function EServicesCategoryScreen() {
|
|||||||
colors={['#3b82f6']}
|
colors={['#3b82f6']}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
ListHeaderComponent={() => (
|
|
||||||
<View>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: '800',
|
|
||||||
color: isDark ? dark.text : '#020617',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Davlat xizmatlari kategoriyalari')}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
marginTop: 4,
|
|
||||||
color: isDark ? dark.subText : '#64748b',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Kerakli xizmat turini tanlang')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
onPress={() => handlePress(item)}
|
onPress={() => handlePress(item)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isDark ? dark.card : '#ffffff',
|
marginHorizontal: 1,
|
||||||
padding: 14,
|
backgroundColor: isDark ? '#FDFDFD' : '#ffffff',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: isDark ? 1 : 0,
|
borderWidth: isDark ? 1 : 0,
|
||||||
borderColor: isDark ? dark.border : 'transparent',
|
borderColor: isDark ? dark.border : 'transparent',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: 120,
|
width: '100%',
|
||||||
height: 70,
|
height: 100,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
backgroundColor: isDark ? dark.muted : '#e2e8f0',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginRight: 14,
|
marginRight: 14,
|
||||||
|
padding: 2,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -156,31 +136,6 @@ export default function EServicesCategoryScreen() {
|
|||||||
<Text style={{ fontWeight: '700', color: dark.text }}>{item.name[0]}</Text>
|
<Text style={{ fontWeight: '700', color: dark.text }}>{item.name[0]}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text
|
|
||||||
numberOfLines={2}
|
|
||||||
style={{
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: item.id === 0 ? '700' : '600',
|
|
||||||
color: isDark ? dark.text : '#020617',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{item.id === 0 && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
marginTop: 4,
|
|
||||||
color: dark.subText,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Tezkor va qulay xizmat
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -190,8 +190,8 @@ export default function EServicesScreen() {
|
|||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
numColumns={3}
|
numColumns={3}
|
||||||
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }}
|
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }}
|
||||||
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
|
contentContainerStyle={{ padding: 16, paddingBottom: 80 }}
|
||||||
onEndReached={() => hasNextPage && fetchNextPage()}
|
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
||||||
onEndReachedThreshold={0.4}
|
onEndReachedThreshold={0.4}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
isFetchingNextPage ? (
|
isFetchingNextPage ? (
|
||||||
|
|||||||
15
screens/home/lib/hook.ts
Normal file
15
screens/home/lib/hook.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
type HomeStore = {
|
||||||
|
showFilter: boolean;
|
||||||
|
setShowFilter: (value: boolean) => void;
|
||||||
|
step: 'filter' | 'items';
|
||||||
|
setStep: (value: 'filter' | 'items') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHomeStore = create<HomeStore>((set) => ({
|
||||||
|
showFilter: false,
|
||||||
|
setShowFilter: (value: boolean) => set({ showFilter: value }),
|
||||||
|
step: 'filter',
|
||||||
|
setStep: (value: 'filter' | 'items') => set({ step: value }),
|
||||||
|
}))
|
||||||
@@ -22,7 +22,7 @@ export interface ProductResponse {
|
|||||||
id: number;
|
id: number;
|
||||||
file: string;
|
file: string;
|
||||||
}[];
|
}[];
|
||||||
category: { id: number; name: string; icon: string }[];
|
category: { id: number; name: string; icon_name: string | null }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompanyBody {
|
export interface CompanyBody {
|
||||||
@@ -42,7 +42,7 @@ export interface CompanyBody {
|
|||||||
|
|
||||||
export interface CompanyResponse {
|
export interface CompanyResponse {
|
||||||
id: number;
|
id: number;
|
||||||
company_name: string;
|
company_name: string | null;
|
||||||
country_name: string;
|
country_name: string;
|
||||||
region_name: string;
|
region_name: string;
|
||||||
district_name: string;
|
district_name: string;
|
||||||
@@ -99,6 +99,7 @@ export interface States {
|
|||||||
districts: { id: number; name: string; code: string }[];
|
districts: { id: number; name: string; code: string }[];
|
||||||
}[];
|
}[];
|
||||||
code: string;
|
code: string;
|
||||||
|
flag: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import SearchTabs from '@/components/ui/SearchTabs';
|
|||||||
import { useTabSearch } from '@/hooks/useSearch';
|
import { useTabSearch } from '@/hooks/useSearch';
|
||||||
import { TabKey } from '@/types';
|
import { TabKey } from '@/types';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { Stack } from 'expo-router';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { Filter, Search } from 'lucide-react-native';
|
import { Filter, Search } from 'lucide-react-native';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { GestureHandlerRootView, RefreshControl } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView, RefreshControl } from 'react-native-gesture-handler';
|
||||||
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { useHomeStore } from '../lib/hook';
|
||||||
|
|
||||||
function Loading() {
|
function Loading() {
|
||||||
return (
|
return (
|
||||||
@@ -37,12 +38,12 @@ function Loading() {
|
|||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>('products');
|
const [activeTab, setActiveTab] = useState<TabKey>('products');
|
||||||
const [step, setStep] = useState<'filter' | 'items'>('filter');
|
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [filtered, setFiltered] = useState<{ id: number; company_name: string }[]>([]);
|
const [filtered, setFiltered] = useState<{ id: number; company_name: string }[]>([]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { showFilter, setShowFilter, step, setStep } = useHomeStore();
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -115,10 +116,7 @@ export default function HomeScreen() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack.Screen options={{ headerShown: false }} />
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
{/* Qidiruv va filter */}
|
|
||||||
<View style={styles.searchSection}>
|
<View style={styles.searchSection}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -169,8 +167,12 @@ export default function HomeScreen() {
|
|||||||
presentationStyle="pageSheet"
|
presentationStyle="pageSheet"
|
||||||
onRequestClose={handleCloseFilter}
|
onRequestClose={handleCloseFilter}
|
||||||
>
|
>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider style={{ flex: 1 }}>
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#fff' }}>
|
||||||
|
<StatusBar
|
||||||
|
style={isDark ? 'light' : 'dark'}
|
||||||
|
backgroundColor={isDark ? '#0f172a' : '#fff'}
|
||||||
|
/>
|
||||||
<FilterUI back={handleCloseFilter} setStep={setStep} setFiltered={setFiltered} />
|
<FilterUI back={handleCloseFilter} setStep={setStep} setFiltered={setFiltered} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { API_URLS } from '@/api/URLs';
|
|||||||
import { ProductBody, ProductResponse } from '@/screens/home/lib/types';
|
import { ProductBody, ProductResponse } from '@/screens/home/lib/types';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import {
|
import {
|
||||||
ExployeesResponse,
|
ExployeesResponse,
|
||||||
MyAdsData,
|
MyAdsData,
|
||||||
MyAdsDataRes,
|
MyAdsDataRes,
|
||||||
MyBonusesData,
|
MyBonusesData,
|
||||||
NotificationListRes,
|
NotificationListRes,
|
||||||
UserInfoResponseData,
|
UserInfoResponseData,
|
||||||
} from './type';
|
} from './type';
|
||||||
|
|
||||||
export const user_api = {
|
export const user_api = {
|
||||||
@@ -36,8 +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;
|
age: number | null;
|
||||||
gender: 'male' | 'female';
|
gender: 'male' | 'female' | null;
|
||||||
}) {
|
}) {
|
||||||
const res = await httpClient.patch(API_URLS.User_Update, body);
|
const res = await httpClient.patch(API_URLS.User_Update, body);
|
||||||
return res;
|
return res;
|
||||||
@@ -136,4 +136,9 @@ export const user_api = {
|
|||||||
const res = await httpClient.post(API_URLS.Notification_Ready(id));
|
const res = await httpClient.post(API_URLS.Notification_Ready(id));
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async mark_all_as_read() {
|
||||||
|
const res = await httpClient.post(API_URLS.Notification_Mark_All_Read);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
|
|
||||||
export default function AddEmployee() {
|
export default function AddEmployee() {
|
||||||
@@ -73,7 +72,7 @@ export default function AddEmployee() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => router.push('/profile/employees')}>
|
<Pressable onPress={() => router.push('/profile/employees')}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -140,7 +139,7 @@ export default function AddEmployee() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
import StepOneServices from './StepOneService';
|
import StepOneServices from './StepOneService';
|
||||||
|
|
||||||
@@ -127,7 +126,7 @@ export default function AddService() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
||||||
<View
|
<View
|
||||||
style={{ flexDirection: 'row', gap: 10, alignContent: 'center', alignItems: 'center' }}
|
style={{ flexDirection: 'row', gap: 10, alignContent: 'center', alignItems: 'center' }}
|
||||||
@@ -176,7 +175,7 @@ export default function AddService() {
|
|||||||
)}
|
)}
|
||||||
{step === 2 && <StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />}
|
{step === 2 && <StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import PAYME from '@/assets/images/Payme_NEW.png';
|
||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
|
import { price_calculation } from '@/screens/create-ads/lib/api';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
||||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { ResizeMode, Video } from 'expo-av';
|
import { ResizeMode, Video } from 'expo-av';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { ArrowLeft, EyeIcon, Megaphone, Plus } from 'lucide-react-native';
|
import { ArrowLeft, EyeIcon, Megaphone, Plus } from 'lucide-react-native';
|
||||||
@@ -9,18 +11,21 @@ import React, { useCallback, useRef, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
Image,
|
||||||
|
Linking,
|
||||||
Pressable,
|
Pressable,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
|
import { MyAdsDataRes } from '../lib/type';
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
@@ -47,8 +52,8 @@ export function AnnouncementsTab() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<any>(null);
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<MyAdsDataRes | null>(null);
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
const [sheetOpen, setSheetOpen] = useState(false); const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
|
|
||||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({
|
const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({
|
||||||
queryKey: ['my_ads'],
|
queryKey: ['my_ads'],
|
||||||
@@ -78,12 +83,12 @@ export function AnnouncementsTab() {
|
|||||||
isError: detailError,
|
isError: detailError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['my_ads_id', selectedAnnouncement?.id],
|
queryKey: ['my_ads_id', selectedAnnouncement?.id],
|
||||||
queryFn: () => user_api.my_ads_detail(selectedAnnouncement.id),
|
queryFn: () => user_api.my_ads_detail(selectedAnnouncement?.id!),
|
||||||
select: (res) => res.data.data,
|
select: (res) => res.data.data,
|
||||||
enabled: !!selectedAnnouncement && sheetOpen,
|
enabled: !!selectedAnnouncement && sheetOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const openSheet = (item: any) => {
|
const openSheet = (item: MyAdsDataRes) => {
|
||||||
setSelectedAnnouncement(item);
|
setSelectedAnnouncement(item);
|
||||||
setSheetOpen(true);
|
setSheetOpen(true);
|
||||||
requestAnimationFrame(() => bottomSheetRef.current?.present());
|
requestAnimationFrame(() => bottomSheetRef.current?.present());
|
||||||
@@ -125,9 +130,35 @@ export function AnnouncementsTab() {
|
|||||||
|
|
||||||
const formatAmount = (amount: number) => new Intl.NumberFormat('uz-UZ').format(amount) + " so'm";
|
const formatAmount = (amount: number) => new Intl.NumberFormat('uz-UZ').format(amount) + " so'm";
|
||||||
|
|
||||||
|
const { mutate: payment } = useMutation({
|
||||||
|
mutationFn: (body: { return_url: string; adId: number; paymentType: 'payme' | 'referral' }) =>
|
||||||
|
price_calculation.payment(body),
|
||||||
|
onSuccess: async (res, variables) => {
|
||||||
|
if (variables.paymentType === 'payme') {
|
||||||
|
await Linking.openURL(res.data.url);
|
||||||
|
router.push('/profile/my-ads');
|
||||||
|
} else {
|
||||||
|
router.push('/profile/my-ads');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
Alert.alert('Xatolik yuz berdi', err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendPayment = ({ type, id }: { id: number, type: 'payme' | 'referral' }) => {
|
||||||
|
payment({
|
||||||
|
adId: id,
|
||||||
|
paymentType: type,
|
||||||
|
return_url: 'https://infotarget.uz/en/main/dashboard',
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<View>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => router.push('/profile')}>
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -138,7 +169,7 @@ export function AnnouncementsTab() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<ActivityIndicator size={'large'} />
|
<ActivityIndicator size={'large'} />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +335,63 @@ export function AnnouncementsTab() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
|
|
||||||
|
{detail?.status === 'pending' && (
|
||||||
|
<View style={[styles.footerContainer, { backgroundColor: theme.sheetBg }]}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.paymentButton, { backgroundColor: theme.primary }]}
|
||||||
|
onPress={() => {
|
||||||
|
bottomSheetModalRef.current?.present();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.paymentButtonText}>{t("To'lov qilish")}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</BottomSheetModal>
|
||||||
|
|
||||||
|
<BottomSheetModal
|
||||||
|
ref={bottomSheetModalRef}
|
||||||
|
index={0}
|
||||||
|
snapPoints={['70%', '95%']}
|
||||||
|
backdropComponent={renderBackdrop}
|
||||||
|
handleIndicatorStyle={{ backgroundColor: '#94a3b8', width: 50 }}
|
||||||
|
backgroundStyle={{ backgroundColor: isDark ? '#0f172a' : '#ffffff' }}
|
||||||
|
enablePanDownToClose
|
||||||
|
>
|
||||||
|
<BottomSheetScrollView
|
||||||
|
style={styles.sheetContent}
|
||||||
|
contentContainerStyle={styles.sheetContentContainer}
|
||||||
|
>
|
||||||
|
<View style={{ padding: 20 }}>
|
||||||
|
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||||
|
{t("To'lov turini tanlang")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.paymentItem,
|
||||||
|
isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
|
||||||
|
{ flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center' },
|
||||||
|
]}
|
||||||
|
onPress={() => sendPayment({ id: selectedAnnouncement?.id!, type: 'payme' })}
|
||||||
|
>
|
||||||
|
<Image source={PAYME} style={{ width: 80, height: 80 }} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.paymentItem,
|
||||||
|
isDark ? styles.darkPaymentItem : styles.lightPaymentItem,
|
||||||
|
]}
|
||||||
|
onPress={() => sendPayment({ id: selectedAnnouncement?.id!, type: 'referral' })}
|
||||||
|
>
|
||||||
|
<Text style={[styles.paymentText, isDark ? styles.darkText : styles.lightText]}>
|
||||||
|
{t('Referal orqali')}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</BottomSheetScrollView>
|
||||||
</BottomSheetModal>
|
</BottomSheetModal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -321,7 +409,17 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
headerTitle: { fontSize: 18, fontWeight: '700' },
|
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||||
|
|
||||||
list: { padding: 16, gap: 12 },
|
sheetContent: { flex: 1 },
|
||||||
|
sheetContentContainer: { paddingBottom: 40 },
|
||||||
|
|
||||||
|
darkText: {
|
||||||
|
color: '#f1f5f9',
|
||||||
|
},
|
||||||
|
lightText: {
|
||||||
|
color: '#0f172a',
|
||||||
|
},
|
||||||
|
|
||||||
|
list: { padding: 16, paddingBottom: 30, gap: 12 },
|
||||||
card: { borderRadius: 16, padding: 16, gap: 8 },
|
card: { borderRadius: 16, padding: 16, gap: 8 },
|
||||||
cardImage: { width: '100%', height: 160, borderRadius: 12 },
|
cardImage: { width: '100%', height: 160, borderRadius: 12 },
|
||||||
|
|
||||||
@@ -329,6 +427,24 @@ const styles = StyleSheet.create({
|
|||||||
title: { fontSize: 16, fontWeight: '700' },
|
title: { fontSize: 16, fontWeight: '700' },
|
||||||
desc: { lineHeight: 20 },
|
desc: { lineHeight: 20 },
|
||||||
|
|
||||||
|
paymentItem: {
|
||||||
|
height: 56,
|
||||||
|
borderRadius: 14,
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
darkPaymentItem: {
|
||||||
|
backgroundColor: '#1e293b',
|
||||||
|
},
|
||||||
|
lightPaymentItem: {
|
||||||
|
backgroundColor: '#f8fafc',
|
||||||
|
},
|
||||||
|
paymentText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
|
||||||
footer: { flexDirection: 'row', justifyContent: 'space-between' },
|
footer: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||||
metaText: {},
|
metaText: {},
|
||||||
date: {},
|
date: {},
|
||||||
@@ -363,6 +479,25 @@ const styles = StyleSheet.create({
|
|||||||
value: { flex: 1 },
|
value: { flex: 1 },
|
||||||
price: { fontWeight: '700' },
|
price: { fontWeight: '700' },
|
||||||
|
|
||||||
|
footerContainer: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 20,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#e2e8f0',
|
||||||
|
},
|
||||||
|
paymentButton: {
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
paymentButtonText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
},
|
||||||
|
|
||||||
loading: {},
|
loading: {},
|
||||||
error: {},
|
error: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { ArrowLeft, Award, Percent } from 'lucide-react-native';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||||
import { RefreshControl } from 'react-native-gesture-handler';
|
import { RefreshControl } from 'react-native-gesture-handler';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
@@ -60,7 +59,7 @@ export default function BonusesScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||||
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
||||||
<Pressable onPress={() => router.push('/profile')}>
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||||
@@ -153,7 +152,7 @@ export default function BonusesScreen() {
|
|||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +163,7 @@ const styles = StyleSheet.create({
|
|||||||
list: {
|
list: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
gap: 16,
|
gap: 16,
|
||||||
|
paddingBottom: 30,
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
|
|
||||||
type FormType = {
|
type FormType = {
|
||||||
@@ -89,7 +88,7 @@ export default function CreateReferrals() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
|
||||||
<Pressable onPress={() => router.back()}>
|
<Pressable onPress={() => router.back()}>
|
||||||
@@ -187,7 +186,7 @@ export default function CreateReferrals() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { ArrowLeft, Loader } from 'lucide-react-native';
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
import StepOneServices from './StepOneService';
|
import StepOneServices from './StepOneService';
|
||||||
|
|
||||||
@@ -145,7 +144,7 @@ export default function EditService() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
<View style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||||
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
||||||
<View style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }}>
|
||||||
<Pressable onPress={handleBack}>
|
<Pressable onPress={handleBack}>
|
||||||
@@ -179,7 +178,7 @@ export default function EditService() {
|
|||||||
)}
|
)}
|
||||||
{step === 2 && <StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />}
|
{step === 2 && <StepTwo ref={stepTwoRef} formData={formData} updateForm={updateForm} />}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
import { ExployeesDataResponse } from '../lib/type';
|
import { ExployeesDataResponse } from '../lib/type';
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ export function EmployeesTab() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => router.push('/profile')}>
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -95,7 +94,7 @@ export function EmployeesTab() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<ActivityIndicator size={'large'} />
|
<ActivityIndicator size={'large'} />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ export function EmployeesTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||||
<View style={styles.topHeader}>
|
<View style={styles.topHeader}>
|
||||||
<Pressable onPress={() => router.push('/profile')}>
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
<ArrowLeft color={theme.text} />
|
<ArrowLeft color={theme.text} />
|
||||||
@@ -152,14 +151,14 @@ export function EmployeesTab() {
|
|||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1 },
|
container: { flex: 1 },
|
||||||
addButton: { padding: 8 },
|
addButton: { padding: 8 },
|
||||||
list: { padding: 16, gap: 12 },
|
list: { padding: 16, gap: 12, paddingBottom: 30 },
|
||||||
card: {
|
card: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { VideoView, useVideoPlayer } from 'expo-video';
|
import { VideoView, useVideoPlayer } from 'expo-video';
|
||||||
import { ArrowLeft, Check, ChevronDown, X } from 'lucide-react-native';
|
import { ArrowLeft, Play, X } from 'lucide-react-native';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
|
||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
Image,
|
||||||
Pressable,
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||||
import Modal from 'react-native-modal';
|
import Modal from 'react-native-modal';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
|
||||||
|
|
||||||
type ManualStep = {
|
type ManualStep = {
|
||||||
image: any;
|
image: any;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -39,6 +37,7 @@ const languages: Language[] = [
|
|||||||
export function ManualTab() {
|
export function ManualTab() {
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
|
||||||
/** 🔹 Modal states */
|
/** 🔹 Modal states */
|
||||||
const [imageVisible, setImageVisible] = useState(false);
|
const [imageVisible, setImageVisible] = useState(false);
|
||||||
@@ -69,9 +68,9 @@ export function ManualTab() {
|
|||||||
|
|
||||||
/** 🔹 Video manbalari (SELECT ga bog‘liq) */
|
/** 🔹 Video manbalari (SELECT ga bog‘liq) */
|
||||||
const videos = {
|
const videos = {
|
||||||
uz: require('@/assets/manual/manual_video_uz.mp4'),
|
uz: require('@/assets/manual/manual_video_uz.webm'),
|
||||||
ru: require('@/assets/manual/manual_video_ru.mp4'),
|
ru: require('@/assets/manual/manual_video_ru.webm'),
|
||||||
en: require('@/assets/manual/manual_video_en.mp4'),
|
en: require('@/assets/manual/manual_video_en.webm'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const player = useVideoPlayer(videos[selectedLang], (player) => {
|
const player = useVideoPlayer(videos[selectedLang], (player) => {
|
||||||
@@ -161,6 +160,17 @@ export function ManualTab() {
|
|||||||
|
|
||||||
const selectedLanguage = languages.find((l) => l.code === selectedLang);
|
const selectedLanguage = languages.find((l) => l.code === selectedLang);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// listener qo'shish
|
||||||
|
const subscription = player.addListener('playingChange', (state) => {
|
||||||
|
setIsPlaying(state.isPlaying);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.remove();
|
||||||
|
};
|
||||||
|
}, [player]);
|
||||||
|
|
||||||
const renderStep = ({ item, index }: { item: ManualStep; index: number }) => (
|
const renderStep = ({ item, index }: { item: ManualStep; index: number }) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -186,18 +196,17 @@ export function ManualTab() {
|
|||||||
{t("Foydalanish qo'llanmasi")}
|
{t("Foydalanish qo'llanmasi")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={[styles.subtitle, { color: theme.textMuted }]}>
|
|
||||||
{t("Quyidagi qisqa video yoki rasmlarni ko'rib chiqing")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* VIDEO */}
|
{/* VIDEO */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
||||||
{t("Foydalanish video qo'llanma")}
|
{t("Foydalanish qo'llanmasi")}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ alignContent: 'flex-end', alignItems: 'flex-end', paddingHorizontal: 16 }}>
|
<Text style={[styles.subtitle, { color: theme.textMuted }]}>
|
||||||
|
{t("Quyidagi qisqa video yoki rasmlarni ko'rib chiqing")}
|
||||||
|
</Text>
|
||||||
|
{/* <View style={{ alignContent: 'flex-end', alignItems: 'flex-end', paddingHorizontal: 16 }}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
styles.langSelector,
|
styles.langSelector,
|
||||||
@@ -210,7 +219,7 @@ export function ManualTab() {
|
|||||||
</View>
|
</View>
|
||||||
<ChevronDown color={theme.textMuted} />
|
<ChevronDown color={theme.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View> */}
|
||||||
|
|
||||||
<View style={[styles.videoCard, { backgroundColor: theme.cardBg }]}>
|
<View style={[styles.videoCard, { backgroundColor: theme.cardBg }]}>
|
||||||
<VideoView
|
<VideoView
|
||||||
@@ -218,16 +227,49 @@ export function ManualTab() {
|
|||||||
style={styles.video}
|
style={styles.video}
|
||||||
allowsFullscreen
|
allowsFullscreen
|
||||||
allowsPictureInPicture
|
allowsPictureInPicture
|
||||||
|
nativeControls={true}
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!isPlaying && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: 50,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
player.play();
|
||||||
|
setIsPlaying(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play color={'white'} size={26} fill={'black'} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* RASMLAR */}
|
{/* RASMLAR */}
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={[styles.sectionTitle, { color: theme.text }]}>
|
{/* <Text style={[styles.sectionTitle, { color: theme.text }]}>
|
||||||
{t('Foydalanish rasm qo‘llanma')}
|
{t('Foydalanish rasm qo‘llanma')}
|
||||||
</Text>
|
</Text> */}
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
horizontal
|
horizontal
|
||||||
@@ -254,7 +296,7 @@ export function ManualTab() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* LANGUAGE MODAL */}
|
{/* LANGUAGE MODAL */}
|
||||||
<Modal isVisible={langPickerVisible} onBackdropPress={() => setLangPickerVisible(false)}>
|
{/* <Modal isVisible={langPickerVisible} onBackdropPress={() => setLangPickerVisible(false)}>
|
||||||
<View style={[styles.langModalContent, { backgroundColor: theme.cardBg }]}>
|
<View style={[styles.langModalContent, { backgroundColor: theme.cardBg }]}>
|
||||||
<Text style={{ color: 'white', marginBottom: 10, fontSize: 18 }}>
|
<Text style={{ color: 'white', marginBottom: 10, fontSize: 18 }}>
|
||||||
{t('Video tilini tanlang')}
|
{t('Video tilini tanlang')}
|
||||||
@@ -299,7 +341,7 @@ export function ManualTab() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal> */}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -308,8 +350,8 @@ const styles = StyleSheet.create({
|
|||||||
container: { flex: 1 },
|
container: { flex: 1 },
|
||||||
hero: { padding: 20 },
|
hero: { padding: 20 },
|
||||||
topHeader: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
topHeader: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||||||
headerTitle: { fontSize: 22, fontWeight: '700' },
|
headerTitle: { fontSize: 22, fontWeight: '700', marginHorizontal: 16 },
|
||||||
subtitle: { marginTop: 8 },
|
subtitle: { fontSize: 16, marginTop: 5, fontWeight: '500', marginHorizontal: 16 },
|
||||||
|
|
||||||
section: { marginBottom: 28 },
|
section: { marginBottom: 28 },
|
||||||
sectionTitle: { fontSize: 20, fontWeight: '700', marginLeft: 16 },
|
sectionTitle: { fontSize: 20, fontWeight: '700', marginLeft: 16 },
|
||||||
@@ -329,6 +371,7 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
videoCard: {
|
videoCard: {
|
||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
|
marginTop: 10,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useTheme } from '@/components/ThemeContext';
|
import { useTheme } from '@/components/ThemeContext';
|
||||||
import { useGlobalRefresh } from '@/components/ui/RefreshContext';
|
|
||||||
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import { Image as ExpoImage } from 'expo-image';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
@@ -15,20 +14,18 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { RefreshControl } from 'react-native-gesture-handler';
|
import { RefreshControl } from 'react-native-gesture-handler';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
|
|
||||||
const PAGE_SIZE = 5;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
export default function MyServicesScreen() {
|
export default function MyServicesScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { onRefresh, refreshing } = useGlobalRefresh();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
/* ================= QUERY ================= */
|
/* ================= QUERY ================= */
|
||||||
const { data, isLoading, isError, fetchNextPage, hasNextPage } = useInfiniteQuery({
|
const { data, isLoading, isError, fetchNextPage, hasNextPage, isRefetching } = useInfiniteQuery({
|
||||||
queryKey: ['my_services'],
|
queryKey: ['my_services'],
|
||||||
queryFn: async ({ pageParam = 1 }) => {
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const res = await user_api.my_sevices({
|
const res = await user_api.my_sevices({
|
||||||
@@ -69,9 +66,13 @@ export default function MyServicesScreen() {
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
queryClient.refetchQueries({ queryKey: ['my_services'] });
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||||
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
||||||
<Pressable onPress={() => router.push('/profile')}>
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
|
||||||
@@ -84,7 +85,7 @@ export default function MyServicesScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<ActivityIndicator size={'large'} />
|
<ActivityIndicator size={'large'} />
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ export default function MyServicesScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
<View style={[styles.topHeader, { backgroundColor: isDark ? '#0f172a' : '#ffffff' }]}>
|
||||||
<Pressable onPress={() => router.push('/profile')}>
|
<Pressable onPress={() => router.push('/profile')}>
|
||||||
@@ -119,7 +120,7 @@ export default function MyServicesScreen() {
|
|||||||
onEndReached={() => hasNextPage && fetchNextPage()}
|
onEndReached={() => hasNextPage && fetchNextPage()}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={isRefetching}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
colors={['#2563eb']}
|
colors={['#2563eb']}
|
||||||
tintColor="#2563eb"
|
tintColor="#2563eb"
|
||||||
@@ -235,7 +236,7 @@ export default function MyServicesScreen() {
|
|||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +254,7 @@ const styles = StyleSheet.create({
|
|||||||
elevation: 3,
|
elevation: 3,
|
||||||
},
|
},
|
||||||
headerTitle: { fontSize: 18, fontWeight: '700', flex: 1, marginLeft: 10 },
|
headerTitle: { fontSize: 18, fontWeight: '700', flex: 1, marginLeft: 10 },
|
||||||
list: { padding: 16, gap: 16 },
|
list: { padding: 16, gap: 16, paddingBottom: 30 },
|
||||||
card: { borderRadius: 20, overflow: 'hidden' },
|
card: { borderRadius: 20, overflow: 'hidden' },
|
||||||
mediaContainer: { width: '100%', height: 200 },
|
mediaContainer: { width: '100%', height: 200 },
|
||||||
media: { width: '100%', height: '100%' },
|
media: { width: '100%', height: '100%' },
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ export function NotificationTab() {
|
|||||||
initialPageParam: 1,
|
initialPageParam: 1,
|
||||||
});
|
});
|
||||||
const notifications = data?.pages.flatMap((p) => p.results) ?? [];
|
const notifications = data?.pages.flatMap((p) => p.results) ?? [];
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { mutate: markAllAsRead, isPending: isMarkingAllRead } = useMutation({
|
||||||
|
mutationFn: () => user_api.mark_all_as_read(),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['notifications-list'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['notification-list'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -82,6 +91,25 @@ export function NotificationTab() {
|
|||||||
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
|
||||||
{t('Bildirishnomalar')}
|
{t('Bildirishnomalar')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{notifications.some((n) => !n.is_read) && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.markAllButton,
|
||||||
|
{ backgroundColor: isDark ? '#1e293b' : '#e0f2fe', borderColor: '#3b82f6' },
|
||||||
|
]}
|
||||||
|
onPress={() => markAllAsRead()}
|
||||||
|
disabled={isMarkingAllRead}
|
||||||
|
>
|
||||||
|
{isMarkingAllRead ? (
|
||||||
|
<ActivityIndicator size="small" color="#3b82f6" />
|
||||||
|
) : (
|
||||||
|
<Text style={[styles.markAllText, { color: '#3b82f6' }]}>
|
||||||
|
{t("Barchasi o'qildi")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={notifications}
|
data={notifications}
|
||||||
@@ -372,5 +400,19 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
markAllButton: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
minWidth: 80,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
markAllText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import AdsLogo from '@/assets/images/one_click.png';
|
||||||
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 { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import {
|
import {
|
||||||
Award,
|
Award,
|
||||||
@@ -8,6 +10,7 @@ import {
|
|||||||
BookAIcon,
|
BookAIcon,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
HandCoins,
|
HandCoins,
|
||||||
|
LucideIcon,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
Package,
|
Package,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -19,6 +22,11 @@ import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-nati
|
|||||||
import { RefreshControl } from 'react-native-gesture-handler';
|
import { RefreshControl } from 'react-native-gesture-handler';
|
||||||
import { user_api } from '../lib/api';
|
import { user_api } from '../lib/api';
|
||||||
|
|
||||||
|
interface SectionType {
|
||||||
|
title: string;
|
||||||
|
items: { icon: LucideIcon; label: string; route: string; badge?: number; image?: any }[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function Profile() {
|
export default function Profile() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { onRefresh, refreshing } = useGlobalRefresh();
|
const { onRefresh, refreshing } = useGlobalRefresh();
|
||||||
@@ -36,7 +44,7 @@ export default function Profile() {
|
|||||||
queryFn: () => user_api.getMe(),
|
queryFn: () => user_api.getMe(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const sections = [
|
const sections: SectionType[] = [
|
||||||
{
|
{
|
||||||
title: 'Shaxsiy',
|
title: 'Shaxsiy',
|
||||||
items: [
|
items: [
|
||||||
@@ -53,17 +61,17 @@ export default function Profile() {
|
|||||||
{
|
{
|
||||||
title: 'Faoliyat',
|
title: 'Faoliyat',
|
||||||
items: [
|
items: [
|
||||||
{ icon: Megaphone, label: "E'lonlar", route: '/profile/my-ads' },
|
{ icon: Megaphone, label: "E'lonlar", image: AdsLogo, 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
|
...(me?.data.data.can_create_referral
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
icon: HandCoins,
|
icon: HandCoins,
|
||||||
label: 'Refferallarim',
|
label: 'Refferallarim',
|
||||||
route: '/profile/my-referrals',
|
route: '/profile/my-referrals',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -79,6 +87,7 @@ export default function Profile() {
|
|||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={[styles.content, isDark ? styles.darkBg : styles.lightBg]}
|
style={[styles.content, isDark ? styles.darkBg : styles.lightBg]}
|
||||||
|
contentContainerStyle={{ paddingBottom: 90 }}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
@@ -107,17 +116,32 @@ export default function Profile() {
|
|||||||
>
|
>
|
||||||
<item.icon size={24} color="#3b82f6" />
|
<item.icon size={24} color="#3b82f6" />
|
||||||
|
|
||||||
{item.badge && (
|
{item?.badge && (
|
||||||
<View style={styles.badge}>
|
<View style={styles.badge}>
|
||||||
<Text style={styles.badgeText}>{item.badge}</Text>
|
<Text style={styles.badgeText}>{item.badge}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={[styles.cardLabel, isDark ? styles.darkText : styles.lightText]}>
|
{item.image ? (
|
||||||
{t(item.label)}
|
<View style={{ flex: 1 }}>
|
||||||
</Text>
|
<Image
|
||||||
|
source={item.image}
|
||||||
|
style={{ width: '60%', height: 40 }}
|
||||||
|
contentFit="contain"
|
||||||
|
/>
|
||||||
|
<Text style={[styles.cardLabel, isDark ? styles.darkText : styles.lightText, { fontSize: 14, marginTop: 4, fontWeight: '500' }]}>
|
||||||
|
{t("Bir Zumda Jonatish")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View>
|
||||||
|
<Text style={[styles.cardLabel, isDark ? styles.darkText : styles.lightText]}>
|
||||||
|
{t(item.label)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
<ChevronRight size={20} color={isDark ? '#64748b' : '#94a3b8'} />
|
<ChevronRight size={20} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
@@ -131,7 +155,7 @@ export default function Profile() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginBottom: 50,
|
paddingBottom: 120,
|
||||||
},
|
},
|
||||||
|
|
||||||
darkBg: {
|
darkBg: {
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
FlatList,
|
||||||
|
Platform,
|
||||||
Pressable,
|
Pressable,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
|
Share,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
@@ -64,9 +66,31 @@ export function ReferralsTab() {
|
|||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clipboard + Share funksiyasi
|
||||||
|
const handleCopyAndShare = async (code: string) => {
|
||||||
|
const referralLink = `https://t.me/infotargetbot/join?startapp=${code}`;
|
||||||
|
|
||||||
|
// Clipboard-ga nusxa olish
|
||||||
|
await Clipboard.setStringAsync(referralLink);
|
||||||
|
|
||||||
|
// Share qilish
|
||||||
|
try {
|
||||||
|
await Share.share({
|
||||||
|
message: referralLink,
|
||||||
|
title: t('Referal linkni ulashish'),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Share error:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
ToastAndroid.show(t('Refferal kopiya qilindi'), ToastAndroid.SHORT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
@@ -103,26 +127,12 @@ export function ReferralsTab() {
|
|||||||
onEndReached={() => hasNextPage && fetchNextPage()}
|
onEndReached={() => hasNextPage && fetchNextPage()}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<View style={[styles.card, { backgroundColor: theme.cardBg }]}>
|
<View style={[styles.card, { backgroundColor: theme.cardBg }]}>
|
||||||
<View
|
<View style={styles.cardRow}>
|
||||||
style={{
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<HandCoins size={20} color={theme.primary} />
|
<HandCoins size={20} color={theme.primary} />
|
||||||
<Text style={[styles.code, { color: theme.text }]}>{item.code}</Text>
|
<Text style={[styles.code, { color: theme.text }]}>{item.code}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity onPress={() => handleCopyAndShare(item.code)}>
|
||||||
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} />
|
<CopyIcon size={20} color={theme.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -165,7 +175,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
headerTitle: { fontSize: 18, fontWeight: '700' },
|
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||||
|
|
||||||
list: { padding: 16, gap: 12 },
|
list: { padding: 16, gap: 12, paddingBottom: 30 },
|
||||||
|
|
||||||
card: {
|
card: {
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
@@ -173,6 +183,13 @@ const styles = StyleSheet.create({
|
|||||||
gap: 10,
|
gap: 10,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cardRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
cardHeader: {
|
cardHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
|||||||
Reference in New Issue
Block a user