diff --git a/api/URLs.ts b/api/URLs.ts index 88f8736..369727d 100644 --- a/api/URLs.ts +++ b/api/URLs.ts @@ -35,4 +35,5 @@ export const API_URLS = { Goverment_Category: '/api/goverment-category/', Notification_List: '/api/notifications/', Notification_Ready: (id: number) => `/api/notifications/${id}/read/`, + Notification_Mark_All_Read: '/api/notifications/read-all/', }; diff --git a/app.json b/app.json index 764390d..4f27c9f 100644 --- a/app.json +++ b/app.json @@ -11,9 +11,7 @@ "ios": { "supportsTablet": true, "infoPlist": { - "UIBackgroundModes": [ - "remote-notification" - ] + "UIBackgroundModes": ["remote-notification"] }, "bundleIdentifier": "com.felix.infotarget" }, @@ -75,7 +73,7 @@ "extra": { "router": {}, "eas": { - "projectId": "9a281404-9d04-4493-b630-66c35af03ace" + "projectId": "4d7c2011-4ca0-4944-b540-34740b82470f" } } } diff --git a/app/(auth)/confirm.tsx b/app/(auth)/confirm.tsx index 2aebe68..7ea1553 100644 --- a/app/(auth)/confirm.tsx +++ b/app/(auth)/confirm.tsx @@ -1,10 +1,9 @@ import ConfirmScreen from '@/screens/auth/confirm/ConfirmScreen'; -import { ScrollView } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { ScrollView, View } from 'react-native'; export default function Confirm() { return ( - - + ); } diff --git a/app/(auth)/index.tsx b/app/(auth)/index.tsx index 998c212..3ee3afb 100644 --- a/app/(auth)/index.tsx +++ b/app/(auth)/index.tsx @@ -3,7 +3,6 @@ import LoginScreen from '@/screens/auth/login/ui/LoginScreens'; import { router } from 'expo-router'; import { useEffect } from 'react'; import { ActivityIndicator, ScrollView, View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function Index() { const { isAuthenticated, isLoading } = useAuth(); @@ -27,11 +26,11 @@ export default function Index() { // Token yo‘q → login screen if (!isAuthenticated) { return ( - + - + ); } diff --git a/app/(auth)/register-confirm.tsx b/app/(auth)/register-confirm.tsx index f75f7c9..d4df76c 100644 --- a/app/(auth)/register-confirm.tsx +++ b/app/(auth)/register-confirm.tsx @@ -1,10 +1,9 @@ import RegisterConfirmScreen from '@/screens/auth/register-confirm/ConfirmScreen'; -import { ScrollView } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { ScrollView, View } from 'react-native'; export default function RegisterConfirm() { return ( - - + ); } diff --git a/app/(auth)/register.tsx b/app/(auth)/register.tsx index 7592042..7faee25 100644 --- a/app/(auth)/register.tsx +++ b/app/(auth)/register.tsx @@ -1,11 +1,10 @@ import RegisterScreen from '@/screens/auth/register/RegisterScreen'; import React from 'react'; -import { ScrollView, StyleSheet } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { ScrollView, StyleSheet, View } from 'react-native'; export default function Index() { return ( - + - + ); } diff --git a/app/(auth)/select-category.tsx b/app/(auth)/select-category.tsx index de5671a..2e81208 100644 --- a/app/(auth)/select-category.tsx +++ b/app/(auth)/select-category.tsx @@ -16,8 +16,8 @@ import { StyleSheet, Text, TouchableOpacity, + View, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; interface Category { id: number; @@ -95,7 +95,7 @@ export default function CategorySelectScreen() { }; return ( - + @@ -145,7 +145,7 @@ export default function CategorySelectScreen() { {t('Tadiqlash')} - + ); } diff --git a/app/(dashboard)/_layout.tsx b/app/(dashboard)/_layout.tsx index da35fc4..2f6fbbb 100644 --- a/app/(dashboard)/_layout.tsx +++ b/app/(dashboard)/_layout.tsx @@ -1,6 +1,7 @@ import Logo from '@/assets/images/logo.png'; import { useTheme } from '@/components/ThemeContext'; import { RefreshProvider } from '@/components/ui/RefreshContext'; +import { useHomeStore } from '@/screens/home/lib/hook'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import { router, Tabs } from 'expo-router'; import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native'; @@ -12,6 +13,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; export default function TabsLayout() { const { isDark } = useTheme(); const { t } = useTranslation(); + const { setShowFilter, setStep } = useHomeStore(); const rotateAnim = useRef(new Animated.Value(0)).current; useEffect(() => { @@ -58,9 +60,9 @@ export default function TabsLayout() { paddingBottom: 12, borderTopLeftRadius: 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, - 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({ ios: { shadowColor: isDark ? '#0f172a' : '#0f172a', // shadow qora emas #0f172a bilan uyg‘un @@ -85,6 +87,7 @@ export default function TabsLayout() { > ( @@ -103,7 +106,12 @@ export default function TabsLayout() { ), tabBarIcon: ({ color, focused }) => ( - { + router.push('/(dashboard)'); + setShowFilter(false); + setStep('filter'); + }} style={[ styles.iconContainer, focused && styles.iconContainerActive, @@ -113,7 +121,7 @@ export default function TabsLayout() { ]} > - + ), }} /> @@ -135,7 +143,7 @@ export default function TabsLayout() { }, ]} > - {t("Qo'shish")} + {t("Jo'natish")} ), tabBarIcon: ({ color, focused }) => ( diff --git a/app/(dashboard)/announcements.tsx b/app/(dashboard)/announcements.tsx index 52e8995..3b31d48 100644 --- a/app/(dashboard)/announcements.tsx +++ b/app/(dashboard)/announcements.tsx @@ -1,19 +1,12 @@ -import { useTheme } from '@/components/ThemeContext'; import { FilterProvider } from '@/components/ui/FilterContext'; import { CustomHeader } from '@/components/ui/Header'; import DashboardScreen from '@/screens/announcements/ui/AnnouncementsList'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function Announcements() { - const { isDark } = useTheme(); return ( - - - - + + ); } diff --git a/app/(dashboard)/create-announcements.tsx b/app/(dashboard)/create-announcements.tsx index 4899820..f0af7c1 100644 --- a/app/(dashboard)/create-announcements.tsx +++ b/app/(dashboard)/create-announcements.tsx @@ -2,18 +2,13 @@ import { useTheme } from '@/components/ThemeContext'; import { FilterProvider } from '@/components/ui/FilterContext'; import { CustomHeader } from '@/components/ui/Header'; import CreateAdsScreens from '@/screens/create-ads/ui/CreateAdsScreens'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function CreateAnnouncements() { const { isDark } = useTheme(); return ( - - - - + + ); } diff --git a/app/(dashboard)/e-service/e-services-category.tsx b/app/(dashboard)/e-service/e-services-category.tsx index dfdbdcb..ca29bf7 100644 --- a/app/(dashboard)/e-service/e-services-category.tsx +++ b/app/(dashboard)/e-service/e-services-category.tsx @@ -1,17 +1,11 @@ -import { useTheme } from '@/components/ThemeContext'; import { CustomHeader } from '@/components/ui/Header'; import EServicesScreen from '@/screens/e-services/ui/EServicesScreen'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function EServicesCategory() { - const { isDark } = useTheme(); return ( - + <> - + ); } diff --git a/app/(dashboard)/e-service/e-services.tsx b/app/(dashboard)/e-service/e-services.tsx index 67950a8..0ce4889 100644 --- a/app/(dashboard)/e-service/e-services.tsx +++ b/app/(dashboard)/e-service/e-services.tsx @@ -1,17 +1,13 @@ import { useTheme } from '@/components/ThemeContext'; import { CustomHeader } from '@/components/ui/Header'; import EServicesCategoryScreen from '@/screens/e-services/ui/EServicesCategoryScreen'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function EServices() { const { isDark } = useTheme(); return ( - + <> - + ); } diff --git a/app/(dashboard)/index.tsx b/app/(dashboard)/index.tsx index a228710..22ef3fd 100644 --- a/app/(dashboard)/index.tsx +++ b/app/(dashboard)/index.tsx @@ -1,15 +1,12 @@ // pages/home/index.tsx import { useAuth } from '@/components/AuthProvider'; -import { useTheme } from '@/components/ThemeContext'; import { FilterProvider } from '@/components/ui/FilterContext'; import { CustomHeader } from '@/components/ui/Header'; import HomeScreen from '@/screens/home/ui/HomeScreen'; import { router } from 'expo-router'; import { useEffect } from 'react'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function Index() { - const { isDark } = useTheme(); const { isAuthenticated, isLoading } = useAuth(); useEffect(() => { @@ -19,17 +16,13 @@ export default function Index() { }, [isAuthenticated, isLoading]); if (isLoading || !isAuthenticated) { - return null; // Loading vaqtida yoki auth yo‘q bo‘lsa hech narsa ko‘rmasin + return null; } return ( - - - - + + ); } diff --git a/app/(dashboard)/profile.tsx b/app/(dashboard)/profile.tsx index 18012ed..ade9fc4 100644 --- a/app/(dashboard)/profile.tsx +++ b/app/(dashboard)/profile.tsx @@ -1,17 +1,11 @@ -import { useTheme } from '@/components/ThemeContext'; import { CustomHeader } from '@/components/ui/Header'; import Profile from '@/screens/profile/ui/ProfileScreen'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function ProfileScreen() { - const { isDark } = useTheme(); return ( - + <> - + ); } diff --git a/app/_layout.tsx b/app/_layout.tsx index aed6b6d..a50eb1f 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -5,19 +5,36 @@ import { useNotifications } from '@/hooks/useNotifications'; import i18n from '@/i18n/i18n'; import { ProfileDataProvider } from '@/screens/profile/lib/ProfileDataContext'; import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; import { I18nextProvider } from 'react-i18next'; +import { View } from 'react-native'; import 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; function AppContent() { useNotifications(); + const insets = useSafeAreaInsets(); return ( <> + {/* iOS status bar fon */} + + + {/* StatusBar */} + + ); } - export default function RootLayout() { return ( diff --git a/app/profile/categories.tsx b/app/profile/categories.tsx index f6daa31..c011c6a 100644 --- a/app/profile/categories.tsx +++ b/app/profile/categories.tsx @@ -18,7 +18,6 @@ import { TouchableOpacity, View, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function PersonalInfoScreen() { const router = useRouter(); @@ -71,6 +70,8 @@ export default function PersonalInfoScreen() { phone: string; person_type: 'employee' | 'legal_entity' | 'ytt' | 'band'; activate_types: number[]; + age: number | null; + gender: 'male' | 'female' | null; }) => user_api.updateMe(body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['get_me'] }); @@ -106,7 +107,7 @@ export default function PersonalInfoScreen() { if (isLoading) { return ( - + router.push('/profile/personal-info')}> @@ -117,12 +118,12 @@ export default function PersonalInfoScreen() { - + ); } return ( - + router.push('/profile/personal-info')}> @@ -138,6 +139,8 @@ export default function PersonalInfoScreen() { phone: me.data.data.phone, industries: selectedCategories, activate_types, + age: me.data.data.age, + gender: me.data.data.gender, }); } }} @@ -173,20 +176,21 @@ export default function PersonalInfoScreen() { setSelectedCategories={setSelectedCategories} /> - + ); } const styles = StyleSheet.create({ container: { flex: 1, + paddingBottom: 30, }, tabsList: { maxHeight: 56, }, tabsContainer: { alignItems: 'center', - marginBottom: 20, + marginBottom: 10, }, tabWrapper: { flexDirection: 'row', diff --git a/app/profile/manual.tsx b/app/profile/manual.tsx index 665928f..4f886e3 100644 --- a/app/profile/manual.tsx +++ b/app/profile/manual.tsx @@ -1,12 +1,12 @@ import { useTheme } from '@/components/ThemeContext'; import { ManualTab } from '@/screens/profile/ui/ManualTab'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { View } from 'react-native'; export default function MyAds() { const { isDark } = useTheme(); return ( - + - + ); } diff --git a/app/profile/my-ads.tsx b/app/profile/my-ads.tsx index b525b8a..879e400 100644 --- a/app/profile/my-ads.tsx +++ b/app/profile/my-ads.tsx @@ -1,12 +1,12 @@ import { useTheme } from '@/components/ThemeContext'; import { AnnouncementsTab } from '@/screens/profile/ui/AnnouncementsTab'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { View } from 'react-native'; export default function MyAds() { const { isDark } = useTheme(); return ( - + - + ); } diff --git a/app/profile/my-referrals.tsx b/app/profile/my-referrals.tsx index 40a0a15..bfff9dc 100644 --- a/app/profile/my-referrals.tsx +++ b/app/profile/my-referrals.tsx @@ -1,12 +1,12 @@ import { useTheme } from '@/components/ThemeContext'; import { ReferralsTab } from '@/screens/profile/ui/RefferallsTab'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { View } from 'react-native'; export default function MyReffrals() { const { isDark } = useTheme(); return ( - + - + ); } diff --git a/app/profile/notification.tsx b/app/profile/notification.tsx index dd5bcad..2566bdd 100644 --- a/app/profile/notification.tsx +++ b/app/profile/notification.tsx @@ -1,12 +1,12 @@ import { useTheme } from '@/components/ThemeContext'; import { NotificationTab } from '@/screens/profile/ui/NotificationTab'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { View } from 'react-native'; export default function MyAds() { const { isDark } = useTheme(); return ( - + - + ); } diff --git a/app/profile/personal-info.tsx b/app/profile/personal-info.tsx index 08ca659..d3e78b5 100644 --- a/app/profile/personal-info.tsx +++ b/app/profile/personal-info.tsx @@ -20,7 +20,6 @@ import { TouchableOpacity, View, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function PersonalInfoScreen() { const router = useRouter(); @@ -104,7 +103,7 @@ export default function PersonalInfoScreen() { if (isLoading) { return ( - + setIsEditing(false)}> @@ -115,12 +114,10 @@ export default function PersonalInfoScreen() { - + ); } - /* ===================== EDIT MODE ===================== */ - const [showGenderOptions, setShowGenderOptions] = useState(false); if (isEditing && editData) { const genderOptions: { label: string; value: 'male' | 'female' }[] = [ { label: t('Erkak'), value: 'male' }, @@ -128,7 +125,7 @@ export default function PersonalInfoScreen() { ]; return ( - + setIsEditing(false)}> @@ -225,12 +222,12 @@ export default function PersonalInfoScreen() { - + ); } /* ===================== VIEW MODE ===================== */ return ( - + router.push('/profile')}> @@ -324,13 +321,14 @@ export default function PersonalInfoScreen() { - + ); } const styles = StyleSheet.create({ container: { flex: 1, + paddingBottom: 30, }, fieldsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 20 }, fieldChip: { diff --git a/app/profile/settings.tsx b/app/profile/settings.tsx index 7223170..8331111 100644 --- a/app/profile/settings.tsx +++ b/app/profile/settings.tsx @@ -10,7 +10,6 @@ import { useRouter } from 'expo-router'; import { ChevronLeft, Moon, Sun } from 'lucide-react-native'; import { useTranslation } from 'react-i18next'; import { Pressable, ScrollView, StyleSheet, Switch, Text, View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; export default function SettingsScreen() { const router = useRouter(); @@ -21,7 +20,7 @@ export default function SettingsScreen() { const selectLanguage = async (lang: string) => { changeLanguage(lang as 'uz' | 'ru' | 'en'); await i18n.changeLanguage(lang); - queryClient.invalidateQueries(); + queryClient.resetQueries(); await saveLang(lang); }; const { isDark, toggleTheme } = useTheme(); @@ -33,7 +32,7 @@ export default function SettingsScreen() { ]; return ( - + {/* Header */} @@ -102,7 +101,7 @@ export default function SettingsScreen() { - + ); } diff --git a/assets/announcements-video/video_en.webm b/assets/announcements-video/video_en.webm new file mode 100644 index 0000000..dd38173 Binary files /dev/null and b/assets/announcements-video/video_en.webm differ diff --git a/assets/announcements-video/video_ru.webm b/assets/announcements-video/video_ru.webm new file mode 100644 index 0000000..558baf5 Binary files /dev/null and b/assets/announcements-video/video_ru.webm differ diff --git a/assets/announcements-video/video_uz.webm b/assets/announcements-video/video_uz.webm new file mode 100644 index 0000000..5244549 Binary files /dev/null and b/assets/announcements-video/video_uz.webm differ diff --git a/assets/goverment/video_ru.webm b/assets/goverment/video_ru.webm new file mode 100644 index 0000000..b5a62fb Binary files /dev/null and b/assets/goverment/video_ru.webm differ diff --git a/assets/images/one_click.png b/assets/images/one_click.png new file mode 100644 index 0000000..58af623 Binary files /dev/null and b/assets/images/one_click.png differ diff --git a/assets/manual/manual_video_en.mp4 b/assets/manual/manual_video_en.mp4 deleted file mode 100644 index ec03703..0000000 Binary files a/assets/manual/manual_video_en.mp4 and /dev/null differ diff --git a/assets/manual/manual_video_en.webm b/assets/manual/manual_video_en.webm new file mode 100644 index 0000000..4015cf7 Binary files /dev/null and b/assets/manual/manual_video_en.webm differ diff --git a/assets/manual/manual_video_ru.mp4 b/assets/manual/manual_video_ru.mp4 deleted file mode 100644 index 4e1fce5..0000000 Binary files a/assets/manual/manual_video_ru.mp4 and /dev/null differ diff --git a/assets/manual/manual_video_ru.webm b/assets/manual/manual_video_ru.webm new file mode 100644 index 0000000..e710499 Binary files /dev/null and b/assets/manual/manual_video_ru.webm differ diff --git a/assets/manual/manual_video_uz.mp4 b/assets/manual/manual_video_uz.mp4 deleted file mode 100644 index 83e3b56..0000000 Binary files a/assets/manual/manual_video_uz.mp4 and /dev/null differ diff --git a/assets/manual/manual_video_uz.webm b/assets/manual/manual_video_uz.webm new file mode 100644 index 0000000..d35c99d Binary files /dev/null and b/assets/manual/manual_video_uz.webm differ diff --git a/components/ui/CompanyList.tsx b/components/ui/CompanyList.tsx index b387f66..79c2ed1 100644 --- a/components/ui/CompanyList.tsx +++ b/components/ui/CompanyList.tsx @@ -44,7 +44,9 @@ export default function CompanyList({ query }: { query: string }) { 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) => { setSelectedCompany(company); @@ -79,6 +81,7 @@ export default function CompanyList({ query }: { query: string }) { item.id.toString()} + contentContainerStyle={{ paddingBottom: 70 }} renderItem={({ item }) => ( - - - - {item.country_name}, {item.region_name} - - + {item.country_name && ( + + + + {item.country_name}, {item.region_name} + + + )} )} onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()} diff --git a/components/ui/CountriesList.tsx b/components/ui/CountriesList.tsx index 986f86d..374241c 100644 --- a/components/ui/CountriesList.tsx +++ b/components/ui/CountriesList.tsx @@ -75,7 +75,7 @@ export default function CountriesList({ search }: { search: string }) { item.id.toString()} - contentContainerStyle={{ gap: 5 }} + contentContainerStyle={{ gap: 5, paddingBottom: 80 }} onEndReached={loadMore} onEndReachedThreshold={0.4} ListFooterComponent={ diff --git a/components/ui/FilterUI.tsx b/components/ui/FilterUI.tsx index 54aa5a5..cd5f6b6 100644 --- a/components/ui/FilterUI.tsx +++ b/components/ui/FilterUI.tsx @@ -18,7 +18,7 @@ import CategorySelect from './CategorySelect'; interface FilterUIProps { back: () => void; onApply?: (data: any) => void; - setStep: Dispatch>; + setStep: (value: 'filter' | 'items') => void; setFiltered: Dispatch>; } @@ -280,7 +280,7 @@ export default function FilterUI({ back, onApply, setStep, setFiltered }: Filter } let data: any[] = []; - let onSelect: (id: string) => void = () => {}; + let onSelect: (id: string) => void = () => { }; let selectedId = ''; switch (activeSheet) { diff --git a/components/ui/FilteredItems.tsx b/components/ui/FilteredItems.tsx index fa4fc07..fe67354 100644 --- a/components/ui/FilteredItems.tsx +++ b/components/ui/FilteredItems.tsx @@ -244,7 +244,7 @@ const styles = StyleSheet.create({ backButton: { flexDirection: 'row', alignItems: 'center', - marginBottom: 16, + marginBottom: 5, padding: 10, borderRadius: 12, alignSelf: 'flex-start', @@ -257,7 +257,7 @@ const styles = StyleSheet.create({ backgroundColor: '#ffffff', }, backText: { fontSize: 16, color: '#3b82f6', fontWeight: '600' }, - listContainer: { gap: 12 }, + listContainer: { gap: 12, paddingBottom: 50 }, itemCard: { borderRadius: 12, padding: 16, @@ -293,7 +293,7 @@ const styles = StyleSheet.create({ }, // Detail - detailTitle: { fontSize: 26, fontWeight: '700', marginBottom: 16 }, + detailTitle: { fontSize: 26, fontWeight: '700', marginBottom: 10 }, companyImage: { width: '100%', height: 220, diff --git a/components/ui/ProductList.tsx b/components/ui/ProductList.tsx index 248af86..b52d98a 100644 --- a/components/ui/ProductList.tsx +++ b/components/ui/ProductList.tsx @@ -148,8 +148,8 @@ export default function ProductList({ query }: Props) { keyExtractor={(item) => item.id.toString()} renderItem={renderItem} numColumns={2} - columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 8 }} - contentContainerStyle={{}} + columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 20 }} + contentContainerStyle={{ paddingBottom: 60 }} onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()} onEndReachedThreshold={0.4} ListFooterComponent={ diff --git a/hooks/useNotifications.ts b/hooks/useNotifications.ts index 0700757..bb5677d 100644 --- a/hooks/useNotifications.ts +++ b/hooks/useNotifications.ts @@ -12,7 +12,7 @@ export interface IRegisterDeviceBody { platform: string; } -const commonRequests = { +export const commonRequests = { /** * Register device for notification * @param body token @@ -27,35 +27,39 @@ const commonRequests = { return response; }, }; - export function useNotifications() { const notificationListener = useRef(null); const responseListener = useRef(null); - const queryClinet = useQueryClient(); - useEffect(() => { - registerForPushNotificationsAsync().then((token) => { - if (!token) return null; + const queryClient = useQueryClient(); - const body: IRegisterDeviceBody = { - token: token, - platform: Platform.OS, - }; - commonRequests.registerDevice(body); + useEffect(() => { + // Android channel + if (Platform.OS === 'android') { + Notifications.setNotificationChannelAsync('default', { + 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( - (notification) => {} - ); - + // User response listener responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => { const data = response.notification.request.content.data; - queryClinet.refetchQueries({ queryKey: ['notification-list'] }); - queryClinet.refetchQueries({ queryKey: ['notifications-list'] }); - if (data?.screen === '/profile/notification') { - return; - } else { - router.push('/profile/notification'); - } + queryClient.refetchQueries({ queryKey: ['notification-list'] }); + queryClient.refetchQueries({ queryKey: ['notifications-list'] }); + router.push('/profile/notification'); + }); + + // Token olish va serverga yuborish + registerForPushNotificationsAsync().then((token) => { + if (!token) return; + commonRequests.registerDevice({ token, platform: Platform.OS }); }); return () => { diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 925f2aa..73c372d 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -59,7 +59,7 @@ "E'lon ma'lumotlari": "Announcement details", "Manzil": "Address", "To'lov": "Payment", - "Yaratish": "Create", + "Yaratish": "Send", "To'lash": "Pay", "Keyingisi": "Next", "To'lov turini tanlang": "Select payment method", @@ -134,7 +134,6 @@ "Media yo'q": "No media", "Hozircha xizmatlar yo'q": "No services yet", "Xizmat qo'shish": "Add service", - "Bekor qilish": "Cancel", "Yangi xizmat (1/2)": "New service (1/2)", "Yangi xizmat (2/2)": "New service (2/2)", "Keyingi": "Next", @@ -199,5 +198,13 @@ "Davlat xizmatlari kategoriyalari": "Categories of government services", "Kerakli xizmat turini tanlang": "Select the desired service type", "Bu kategoriya bo'yicha xizmat topilmadi": "No service in this category", - "Tez orada xizmat qo'shiladi": "Service will be added soon" + "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" } diff --git a/i18n/locales/ru.json b/i18n/locales/ru.json index b511717..e5ef3bd 100644 --- a/i18n/locales/ru.json +++ b/i18n/locales/ru.json @@ -59,7 +59,7 @@ "E'lon ma'lumotlari": "Информация об объявлении", "Manzil": "Адрес", "To'lov": "Оплата", - "Yaratish": "Создать", + "Yaratish": "Отправить", "To'lash": "Оплатить", "Keyingisi": "Далее", "To'lov turini tanlang": "Выберите способ оплаты", @@ -134,7 +134,6 @@ "Media yo'q": "Нет медиа", "Hozircha xizmatlar yo'q": "Пока нет услуг", "Xizmat qo'shish": "Добавить услугу", - "Bekor qilish": "Отмена", "Yangi xizmat (1/2)": "Новая услуга (1/2)", "Yangi xizmat (2/2)": "Новая услуга (2/2)", "Keyingi": "Далее", @@ -198,5 +197,13 @@ "Davlat xizmatlari kategoriyalari": "Категории государственных услуг", "Kerakli xizmat turini tanlang": "Выберите нужный тип услуги", "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": "Отметить все как прочитанное" } diff --git a/i18n/locales/uz.json b/i18n/locales/uz.json index 764becc..be70d69 100644 --- a/i18n/locales/uz.json +++ b/i18n/locales/uz.json @@ -59,7 +59,7 @@ "E'lon ma'lumotlari": "E'lon ma'lumotlari", "Manzil": "Manzil", "To'lov": "To'lov", - "Yaratish": "Yaratish", + "Yaratish": "Yuborish", "To'lash": "To'lash", "Keyingisi": "Keyingisi", "To'lov turini tanlang": "To'lov turini tanlang", @@ -135,7 +135,6 @@ "Yangilashda xato yuz berdi": "Yangilashda xato yuz berdi", "Hozircha xizmatlar yo'q": "Hozircha xizmatlar yo'q", "Xizmat qo'shish": "Xizmat qo'shish", - "Bekor qilish": "Bekor qilish", "Yangi xizmat (1/2)": "Yangi xizmat (1/2)", "Yangi xizmat (2/2)": "Yangi xizmat (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" + "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" } diff --git a/package-lock.json b/package-lock.json index be7b4a9..a28d5b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "expo-navigation-bar": "~5.0.10", "expo-notifications": "~0.32.16", "expo-router": "~6.0.21", + "expo-sharing": "~14.0.8", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "expo-symbols": "~1.0.8", @@ -10230,6 +10231,15 @@ "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": { "version": "31.0.13", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.13.tgz", diff --git a/package.json b/package.json index 636a6a7..a1e7bf2 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "expo-navigation-bar": "~5.0.10", "expo-notifications": "~0.32.16", "expo-router": "~6.0.21", + "expo-sharing": "~14.0.8", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "expo-symbols": "~1.0.8", diff --git a/screens/announcements/lib/type.ts b/screens/announcements/lib/type.ts index 6efc540..2303187 100644 --- a/screens/announcements/lib/type.ts +++ b/screens/announcements/lib/type.ts @@ -18,16 +18,15 @@ export interface AnnouncementListBodyRes { title: string; description: string; total_view_count: number; - files: [ - { - file: string; - } - ]; + files: { + id: number; + file: string; + }[]; status: 'pending' | 'paid' | 'verified' | 'canceled'; types: { id: number; name: string; - icon_name: string; + icon_name: string | null; }[]; created_at: string; } diff --git a/screens/announcements/ui/AnnouncementsList.tsx b/screens/announcements/ui/AnnouncementsList.tsx index 30460d1..61524f0 100644 --- a/screens/announcements/ui/AnnouncementsList.tsx +++ b/screens/announcements/ui/AnnouncementsList.tsx @@ -1,31 +1,86 @@ import { useTheme } from '@/components/ThemeContext'; 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 { 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 { announcement_api } from '../lib/api'; -import { AnnouncementListBodyRes } from '../lib/type'; import AnnouncementCard from './AnnouncementCard'; 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 ( + + + + {!isPlaying && ( + + { + player.play(); + setIsPlaying(true); + }} + > + + + + )} + + ); +} + export default function DashboardScreen() { - const [announcements, setAnnouncements] = useState([]); const queryClient = useQueryClient(); const fadeAnim = useRef(new Animated.Value(0)).current; 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 = { background: isDark ? '#0f172a' : '#f8fafc', primary: '#2563eb', + text: isDark ? '#f8fafc' : '#0f172a', loaderBg: isDark ? '#0f172a' : '#ffffff', }; + // Announcements query const { data, isLoading, isRefetching, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ['announcements_list'], 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; }, getNextPageParam: (lastPage) => @@ -36,8 +91,6 @@ export default function DashboardScreen() { const allAnnouncements = data?.pages.flatMap((p) => p.results) ?? []; useEffect(() => { - setAnnouncements(allAnnouncements); - fadeAnim.setValue(0); Animated.timing(fadeAnim, { toValue: 1, @@ -46,6 +99,44 @@ export default function DashboardScreen() { }).start(); }, [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> = { + 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 = () => { queryClient.refetchQueries({ queryKey: ['announcements_list'] }); }; @@ -54,6 +145,21 @@ export default function DashboardScreen() { if (hasNextPage) fetchNextPage(); }; + const videoItems = [ + { id: '1', player }, + govermentVideoSource && { id: '2', player: player2 }, + ].filter(Boolean) as { id: string; player: VideoPlayer }[]; + + const renderVideoHeader = () => ( + + {videoItems.map((item) => ( + + + + ))} + + ); + if (isLoading) { return ( @@ -66,33 +172,33 @@ export default function DashboardScreen() { return ( - + {t("E'lonlar ro'yxati")} - {announcements.length > 0 ? ( - item.id.toString()} - numColumns={2} - columnWrapperStyle={styles.columnWrapper} - renderItem={({ item }) => } - refreshControl={ - - } - onEndReached={loadMore} - onEndReachedThreshold={0.3} - showsVerticalScrollIndicator={false} - /> - ) : ( - - )} + + item.id.toString()} + numColumns={2} + columnWrapperStyle={styles.columnWrapper} + renderItem={({ item }) => } + ListHeaderComponent={renderVideoHeader} + refreshControl={ + + } + ListEmptyComponent={} + onEndReached={loadMore} + onEndReachedThreshold={0.3} + contentContainerStyle={{ paddingBottom: 80 }} + showsVerticalScrollIndicator={false} + /> ); } @@ -101,7 +207,6 @@ const styles = StyleSheet.create({ container: { flex: 1, paddingHorizontal: 16, - marginTop: 20, }, loaderBox: { flex: 1, @@ -112,4 +217,33 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', 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', + }, }); diff --git a/screens/announcements/ui/EmptyState.tsx b/screens/announcements/ui/EmptyState.tsx index 75db5cd..948f504 100644 --- a/screens/announcements/ui/EmptyState.tsx +++ b/screens/announcements/ui/EmptyState.tsx @@ -2,6 +2,7 @@ import { useTheme } from '@/components/ThemeContext'; import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; type Props = { @@ -18,7 +19,7 @@ export default function EmptyState({ isRefreshing = false, }: Props) { const { isDark } = useTheme(); - + const { t } = useTranslation(); const theme = { gradientColors: isDark ? (['#1e293b', '#334155'] as [string, string]) @@ -37,8 +38,8 @@ export default function EmptyState({ - {title} - {description} + {t(title)} + {t(description)} {onRefresh && ( - Yangilash + + {t('Yangilash')} + )} diff --git a/screens/auth/confirm/ConfirmScreen.tsx b/screens/auth/confirm/ConfirmScreen.tsx index 8176765..976ed65 100644 --- a/screens/auth/confirm/ConfirmScreen.tsx +++ b/screens/auth/confirm/ConfirmScreen.tsx @@ -1,7 +1,9 @@ import { useAuth } from '@/components/AuthProvider'; +import { registerForPushNotificationsAsync } from '@/components/NotificationProvider'; import AuthHeader from '@/components/ui/AuthHeader'; +import { commonRequests } from '@/hooks/useNotifications'; 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 { LinearGradient } from 'expo-linear-gradient'; import { Redirect, useRouter } from 'expo-router'; @@ -26,6 +28,7 @@ import ConfirmForm from './ConfirmForm'; const ConfirmScreen = () => { const router = useRouter(); + const queryClient = useQueryClient(); const [phoneOTP, setPhone] = useState(''); const [error, setError] = useState(''); const { login } = useAuth(); @@ -59,11 +62,27 @@ const ConfirmScreen = () => { const { mutate, isPending } = useMutation({ mutationFn: (body: { code: string; phone: string }) => auth_api.verify_otp(body), onSuccess: async (res) => { + // Tokenlarni saqlash await AsyncStorage.removeItem('phone'); await AsyncStorage.setItem('access_token', 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 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)'); }, onError: (err: any) => { diff --git a/screens/auth/register-confirm/ConfirmScreen.tsx b/screens/auth/register-confirm/ConfirmScreen.tsx index 91ba39c..50c7779 100644 --- a/screens/auth/register-confirm/ConfirmScreen.tsx +++ b/screens/auth/register-confirm/ConfirmScreen.tsx @@ -1,7 +1,9 @@ import { useAuth } from '@/components/AuthProvider'; +import { registerForPushNotificationsAsync } from '@/components/NotificationProvider'; import AuthHeader from '@/components/ui/AuthHeader'; +import { commonRequests } from '@/hooks/useNotifications'; 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 { LinearGradient } from 'expo-linear-gradient'; import { Redirect, useRouter } from 'expo-router'; @@ -29,6 +31,7 @@ const RegisterConfirmScreen = () => { const [phoneOTP, setPhone] = useState(''); const [error, setError] = useState(''); const { login } = useAuth(); + const queryClient = useQueryClient(); const { savedToken } = useTokenStore(); const [resendTimer, setResendTimer] = useState(60); @@ -64,6 +67,17 @@ const RegisterConfirmScreen = () => { savedToken(res.data.data.token.access); await AsyncStorage.setItem('refresh_token', res.data.data.token.refresh); 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)'); }, onError: (err: any) => { diff --git a/screens/create-ads/ui/CategorySelectorBottomSheet.tsx b/screens/create-ads/ui/CategorySelectorBottomSheet.tsx index a94ef8d..f59728b 100644 --- a/screens/create-ads/ui/CategorySelectorBottomSheet.tsx +++ b/screens/create-ads/ui/CategorySelectorBottomSheet.tsx @@ -5,6 +5,7 @@ import { BottomSheetModal, BottomSheetScrollView, } from '@gorhom/bottom-sheet'; +import { Image } from 'expo-image'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -12,6 +13,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; export type Option = { label: string; value: string; + flag?: string; }; type CategorySelectorProps = { @@ -91,6 +93,12 @@ export default function CategorySelectorBottomSheet({ onClose(); }} > + {item.flag ? ( + + ) : null} + - {currentStep === 1 - ? t("E'lon ma'lumotlari") - : currentStep === 2 - ? t('Sohalar') - : currentStep === 3 - ? t('Manzil') - : t("To'lov")} + {t("Bir Zumda Jonatish")} {currentStep === 1 && ( @@ -334,7 +334,7 @@ const styles = StyleSheet.create({ backgroundColor: '#f8fafc', }, container: { padding: 20 }, - title: { fontSize: 22, fontWeight: '800', marginBottom: 20 }, + title: { fontSize: 18, fontWeight: '800', marginBottom: 20 }, darkText: { color: '#f1f5f9', }, diff --git a/screens/create-ads/ui/StepOne.tsx b/screens/create-ads/ui/StepOne.tsx index 3fc8c45..0262317 100644 --- a/screens/create-ads/ui/StepOne.tsx +++ b/screens/create-ads/ui/StepOne.tsx @@ -6,7 +6,6 @@ import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'r import { useTranslation } from 'react-i18next'; 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 Errors = { @@ -302,7 +301,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, height: 56, }, - textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 12 }, + textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 0 }, input: { flex: 1, fontSize: 16 }, prefixContainer: { flexDirection: 'row', diff --git a/screens/create-ads/ui/StepThree.tsx b/screens/create-ads/ui/StepThree.tsx index bd91474..5db2ff8 100644 --- a/screens/create-ads/ui/StepThree.tsx +++ b/screens/create-ads/ui/StepThree.tsx @@ -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(() => { const country = statesData?.find((c) => c.code === formData.country); 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 region = country?.region.find((r) => r.code === formData.region); 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', ''); } }, [formData.region, formData.country, statesData]); - const getLabel = (arr: { name: string; code: string }[], val: string) => - arr.find((item) => item.code === val)?.name || t('— Tanlang —'); + const getLabel = (arr: { name: string; code: string }[], val: string) => { + 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 isSelected = formData.company?.some((c: any) => c.id === item.id); @@ -157,7 +174,13 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) => setShowCountry(true)} > @@ -190,9 +213,18 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) => setShowDistrict(true)} + onPress={() => { + if (formData.region !== 'all') { + setShowDistrict(true); + } + }} + disabled={formData.region === 'all'} > {getLabel( @@ -202,9 +234,34 @@ const StepThree = forwardRef(({ formData, updateForm, data }: StepProps, ref) => - - {t('Reklama joylashtirish kompaniyasi')} - + + + {t('Reklama joylashtirish kompaniyasi')} + + + + {formData.company?.length === corporations.length ? t('Bekor qilish') : t('Hammasi')} + + + isOpen={showCountry} onClose={() => setShowCountry(false)} 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)} /> setShowRegion(false)} 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)} /> setShowDistrict(false)} 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)} /> @@ -286,4 +351,14 @@ const styles = StyleSheet.create({ priceLine: { fontSize: 15 }, totalPrice: { fontSize: 18, fontWeight: '700', marginTop: 6 }, error: { fontWeight: '600', marginBottom: 10 }, + selectAllButton: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + borderWidth: 1, + }, + selectAllText: { + fontSize: 14, + fontWeight: '600', + }, }); diff --git a/screens/e-services/ui/EServicesCategoryScreen.tsx b/screens/e-services/ui/EServicesCategoryScreen.tsx index 8150dba..2c50b72 100644 --- a/screens/e-services/ui/EServicesCategoryScreen.tsx +++ b/screens/e-services/ui/EServicesCategoryScreen.tsx @@ -5,7 +5,6 @@ import { Image } from 'expo-image'; import { router } from 'expo-router'; import { ChevronLeft, XIcon } from 'lucide-react-native'; import React, { useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import { ActivityIndicator, FlatList, Modal, Text, TouchableOpacity, View } from 'react-native'; import { RefreshControl } from 'react-native-gesture-handler'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -14,16 +13,15 @@ import { eservices_api } from '../lib/api'; const dark = { bg: '#0f172a', - card: '#1f2937', + card: '#334155', border: '#1e293b', muted: '#334155', - text: '#f8fafc', - subText: '#cbd5f5', + text: '#E5B037', + subText: '#0B0F2C', }; export default function EServicesCategoryScreen() { const { isDark } = useTheme(); - const { t } = useTranslation(); const [modalVisible, setModalVisible] = useState(false); const webviewRef = useRef(null); const [webUrl, setWebUrl] = React.useState(null); @@ -88,7 +86,7 @@ export default function EServicesCategoryScreen() { `${item.id}-${item.name}`} - contentContainerStyle={{ gap: 12 }} + contentContainerStyle={{ gap: 12, paddingBottom: 80 }} refreshControl={ } - ListHeaderComponent={() => ( - - - {t('Davlat xizmatlari kategoriyalari')} - - - - {t('Kerakli xizmat turini tanlang')} - - - )} renderItem={({ item }) => ( handlePress(item)} style={{ - backgroundColor: isDark ? dark.card : '#ffffff', - padding: 14, + marginHorizontal: 1, + backgroundColor: isDark ? '#FDFDFD' : '#ffffff', borderRadius: 16, flexDirection: 'row', alignItems: 'center', borderWidth: isDark ? 1 : 0, borderColor: isDark ? dark.border : 'transparent', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 4, + elevation: 2, }} > @@ -156,31 +136,6 @@ export default function EServicesCategoryScreen() { {item.name[0]} )} - - - - {item.name} - - - {item.id === 0 && ( - - Tezkor va qulay xizmat - - )} - )} /> diff --git a/screens/e-services/ui/EServicesScreen.tsx b/screens/e-services/ui/EServicesScreen.tsx index 0d6fc08..71dfe39 100644 --- a/screens/e-services/ui/EServicesScreen.tsx +++ b/screens/e-services/ui/EServicesScreen.tsx @@ -190,8 +190,8 @@ export default function EServicesScreen() { renderItem={renderItem} numColumns={3} columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }} - contentContainerStyle={{ padding: 16, paddingBottom: 32 }} - onEndReached={() => hasNextPage && fetchNextPage()} + contentContainerStyle={{ padding: 16, paddingBottom: 80 }} + onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()} onEndReachedThreshold={0.4} ListFooterComponent={ isFetchingNextPage ? ( diff --git a/screens/home/lib/hook.ts b/screens/home/lib/hook.ts new file mode 100644 index 0000000..d1f7207 --- /dev/null +++ b/screens/home/lib/hook.ts @@ -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((set) => ({ + showFilter: false, + setShowFilter: (value: boolean) => set({ showFilter: value }), + step: 'filter', + setStep: (value: 'filter' | 'items') => set({ step: value }), +})) \ No newline at end of file diff --git a/screens/home/lib/types.ts b/screens/home/lib/types.ts index b61dfe0..80983c4 100644 --- a/screens/home/lib/types.ts +++ b/screens/home/lib/types.ts @@ -22,7 +22,7 @@ export interface ProductResponse { id: number; file: string; }[]; - category: { id: number; name: string; icon: string }[]; + category: { id: number; name: string; icon_name: string | null }[]; } export interface CompanyBody { @@ -42,7 +42,7 @@ export interface CompanyBody { export interface CompanyResponse { id: number; - company_name: string; + company_name: string | null; country_name: string; region_name: string; district_name: string; @@ -99,6 +99,7 @@ export interface States { districts: { id: number; name: string; code: string }[]; }[]; code: string; + flag: string; }[]; } diff --git a/screens/home/ui/HomeScreen.tsx b/screens/home/ui/HomeScreen.tsx index 45f386f..b7a3b27 100644 --- a/screens/home/ui/HomeScreen.tsx +++ b/screens/home/ui/HomeScreen.tsx @@ -9,7 +9,7 @@ import SearchTabs from '@/components/ui/SearchTabs'; import { useTabSearch } from '@/hooks/useSearch'; import { TabKey } from '@/types'; 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 React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -25,6 +25,7 @@ import { } from 'react-native'; import { GestureHandlerRootView, RefreshControl } from 'react-native-gesture-handler'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +import { useHomeStore } from '../lib/hook'; function Loading() { return ( @@ -37,12 +38,12 @@ function Loading() { export default function HomeScreen() { const { isDark } = useTheme(); const [activeTab, setActiveTab] = useState('products'); - const [step, setStep] = useState<'filter' | 'items'>('filter'); + const [query, setQuery] = useState(''); - const [showFilter, setShowFilter] = useState(false); const [refreshing, setRefreshing] = useState(false); const [filtered, setFiltered] = useState<{ id: number; company_name: string }[]>([]); const { t } = useTranslation(); + const { showFilter, setShowFilter, step, setStep } = useHomeStore(); const queryClient = useQueryClient(); @@ -115,10 +116,7 @@ export default function HomeScreen() { /> } > - - - {/* Qidiruv va filter */} - - + + + diff --git a/screens/profile/lib/api.ts b/screens/profile/lib/api.ts index 2d36ea0..5618afe 100644 --- a/screens/profile/lib/api.ts +++ b/screens/profile/lib/api.ts @@ -3,12 +3,12 @@ import { API_URLS } from '@/api/URLs'; import { ProductBody, ProductResponse } from '@/screens/home/lib/types'; import { AxiosResponse } from 'axios'; import { - ExployeesResponse, - MyAdsData, - MyAdsDataRes, - MyBonusesData, - NotificationListRes, - UserInfoResponseData, + ExployeesResponse, + MyAdsData, + MyAdsDataRes, + MyBonusesData, + NotificationListRes, + UserInfoResponseData, } from './type'; export const user_api = { @@ -36,8 +36,8 @@ export const user_api = { person_type: 'employee' | 'legal_entity' | 'ytt' | 'band'; phone: string; activate_types: number[]; - age: number; - gender: 'male' | 'female'; + age: number | null; + gender: 'male' | 'female' | null; }) { const res = await httpClient.patch(API_URLS.User_Update, body); return res; @@ -136,4 +136,9 @@ export const user_api = { const res = await httpClient.post(API_URLS.Notification_Ready(id)); return res; }, + + async mark_all_as_read() { + const res = await httpClient.post(API_URLS.Notification_Mark_All_Read); + return res; + }, }; diff --git a/screens/profile/ui/AddEmployee.tsx b/screens/profile/ui/AddEmployee.tsx index c2075c5..3e862d2 100644 --- a/screens/profile/ui/AddEmployee.tsx +++ b/screens/profile/ui/AddEmployee.tsx @@ -17,7 +17,6 @@ import { ToastAndroid, View, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { user_api } from '../lib/api'; export default function AddEmployee() { @@ -73,7 +72,7 @@ export default function AddEmployee() { }; return ( - + router.push('/profile/employees')}> @@ -140,7 +139,7 @@ export default function AddEmployee() { - + ); } diff --git a/screens/profile/ui/AddService.tsx b/screens/profile/ui/AddService.tsx index 4440c95..245703c 100644 --- a/screens/profile/ui/AddService.tsx +++ b/screens/profile/ui/AddService.tsx @@ -14,7 +14,6 @@ import { Text, View, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { user_api } from '../lib/api'; import StepOneServices from './StepOneService'; @@ -127,7 +126,7 @@ export default function AddService() { ); return ( - + } - + ); } diff --git a/screens/profile/ui/AnnouncementsTab.tsx b/screens/profile/ui/AnnouncementsTab.tsx index 2689e98..93dd0ab 100644 --- a/screens/profile/ui/AnnouncementsTab.tsx +++ b/screens/profile/ui/AnnouncementsTab.tsx @@ -1,7 +1,9 @@ +import PAYME from '@/assets/images/Payme_NEW.png'; import { useTheme } from '@/components/ThemeContext'; +import { price_calculation } from '@/screens/create-ads/lib/api'; import { Ionicons } from '@expo/vector-icons'; 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 { useRouter } from 'expo-router'; 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 { ActivityIndicator, + Alert, Dimensions, FlatList, Image, + Linking, Pressable, RefreshControl, ScrollView, StyleSheet, Text, + TouchableOpacity, View, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { user_api } from '../lib/api'; +import { MyAdsDataRes } from '../lib/type'; const PAGE_SIZE = 10; const { width } = Dimensions.get('window'); @@ -47,8 +52,8 @@ export function AnnouncementsTab() { }; const [refreshing, setRefreshing] = useState(false); - const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); - const [sheetOpen, setSheetOpen] = useState(false); + const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); + const [sheetOpen, setSheetOpen] = useState(false); const bottomSheetModalRef = useRef(null); const { data, isLoading, isError, fetchNextPage, hasNextPage, refetch } = useInfiniteQuery({ queryKey: ['my_ads'], @@ -78,12 +83,12 @@ export function AnnouncementsTab() { isError: detailError, } = useQuery({ 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, enabled: !!selectedAnnouncement && sheetOpen, }); - const openSheet = (item: any) => { + const openSheet = (item: MyAdsDataRes) => { setSelectedAnnouncement(item); setSheetOpen(true); 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 { 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) { return ( - + router.push('/profile')}> @@ -138,7 +169,7 @@ export function AnnouncementsTab() { - + ); } @@ -304,6 +335,63 @@ export function AnnouncementsTab() { )} + + {detail?.status === 'pending' && ( + + { + bottomSheetModalRef.current?.present(); + }} + > + {t("To'lov qilish")} + + + )} + + + + + + + {t("To'lov turini tanlang")} + + + sendPayment({ id: selectedAnnouncement?.id!, type: 'payme' })} + > + + + + sendPayment({ id: selectedAnnouncement?.id!, type: 'referral' })} + > + + {t('Referal orqali')} + + + + ); @@ -321,7 +409,17 @@ const styles = StyleSheet.create({ }, 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 }, cardImage: { width: '100%', height: 160, borderRadius: 12 }, @@ -329,6 +427,24 @@ const styles = StyleSheet.create({ title: { fontSize: 16, fontWeight: '700' }, 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' }, metaText: {}, date: {}, @@ -363,6 +479,25 @@ const styles = StyleSheet.create({ value: { flex: 1 }, 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: {}, error: {}, }); diff --git a/screens/profile/ui/BonusesScreen.tsx b/screens/profile/ui/BonusesScreen.tsx index c99c28a..0e21cac 100644 --- a/screens/profile/ui/BonusesScreen.tsx +++ b/screens/profile/ui/BonusesScreen.tsx @@ -6,7 +6,6 @@ import { ArrowLeft, Award, Percent } from 'lucide-react-native'; import { useTranslation } from 'react-i18next'; import { ActivityIndicator, FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; import { RefreshControl } from 'react-native-gesture-handler'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { user_api } from '../lib/api'; const PAGE_SIZE = 10; @@ -60,7 +59,7 @@ export default function BonusesScreen() { } return ( - + router.push('/profile')}> @@ -153,7 +152,7 @@ export default function BonusesScreen() { } /> - + ); } @@ -164,6 +163,7 @@ const styles = StyleSheet.create({ list: { padding: 16, gap: 16, + paddingBottom: 30, }, card: { borderRadius: 20, diff --git a/screens/profile/ui/CreateReferrals.tsx b/screens/profile/ui/CreateReferrals.tsx index 57b8456..0fc1cde 100644 --- a/screens/profile/ui/CreateReferrals.tsx +++ b/screens/profile/ui/CreateReferrals.tsx @@ -15,7 +15,6 @@ import { ToastAndroid, View, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { user_api } from '../lib/api'; type FormType = { @@ -89,7 +88,7 @@ export default function CreateReferrals() { }; return ( - + {/* HEADER */} router.back()}> @@ -187,7 +186,7 @@ export default function CreateReferrals() { )} - + ); } diff --git a/screens/profile/ui/EditServices.tsx b/screens/profile/ui/EditServices.tsx index 6c8c827..fa53d94 100644 --- a/screens/profile/ui/EditServices.tsx +++ b/screens/profile/ui/EditServices.tsx @@ -6,7 +6,6 @@ import { ArrowLeft, Loader } from 'lucide-react-native'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; 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 StepOneServices from './StepOneService'; @@ -145,7 +144,7 @@ export default function EditService() { }; return ( - + @@ -179,7 +178,7 @@ export default function EditService() { )} {step === 2 && } - + ); } diff --git a/screens/profile/ui/EmployeesTab.tsx b/screens/profile/ui/EmployeesTab.tsx index 6a98579..c521bff 100644 --- a/screens/profile/ui/EmployeesTab.tsx +++ b/screens/profile/ui/EmployeesTab.tsx @@ -14,7 +14,6 @@ import { Text, View, } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { user_api } from '../lib/api'; import { ExployeesDataResponse } from '../lib/type'; @@ -84,7 +83,7 @@ export function EmployeesTab() { if (isLoading) { return ( - + router.push('/profile')}> @@ -95,7 +94,7 @@ export function EmployeesTab() { - + ); } @@ -110,7 +109,7 @@ export function EmployeesTab() { } return ( - + router.push('/profile')}> @@ -152,14 +151,14 @@ export function EmployeesTab() { } /> - + ); } const styles = StyleSheet.create({ container: { flex: 1 }, addButton: { padding: 8 }, - list: { padding: 16, gap: 12 }, + list: { padding: 16, gap: 12, paddingBottom: 30 }, card: { flexDirection: 'row', alignItems: 'center', diff --git a/screens/profile/ui/ManualTab.tsx b/screens/profile/ui/ManualTab.tsx index 958c93f..10f1801 100644 --- a/screens/profile/ui/ManualTab.tsx +++ b/screens/profile/ui/ManualTab.tsx @@ -1,24 +1,22 @@ import { useTheme } from '@/components/ThemeContext'; import { router } from 'expo-router'; import { VideoView, useVideoPlayer } from 'expo-video'; -import { ArrowLeft, Check, ChevronDown, X } from 'lucide-react-native'; -import React, { useMemo, useState } from 'react'; +import { ArrowLeft, Play, X } from 'lucide-react-native'; +import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { - Dimensions, FlatList, Image, Pressable, ScrollView, StyleSheet, Text, + TouchableOpacity, View, } from 'react-native'; import ImageViewer from 'react-native-image-zoom-viewer'; import Modal from 'react-native-modal'; -const { width } = Dimensions.get('window'); - type ManualStep = { image: any; description?: string; @@ -39,6 +37,7 @@ const languages: Language[] = [ export function ManualTab() { const { isDark } = useTheme(); const { t, i18n } = useTranslation(); + const [isPlaying, setIsPlaying] = useState(false); /** 🔹 Modal states */ const [imageVisible, setImageVisible] = useState(false); @@ -69,9 +68,9 @@ export function ManualTab() { /** 🔹 Video manbalari (SELECT ga bog‘liq) */ const videos = { - uz: require('@/assets/manual/manual_video_uz.mp4'), - ru: require('@/assets/manual/manual_video_ru.mp4'), - en: require('@/assets/manual/manual_video_en.mp4'), + uz: require('@/assets/manual/manual_video_uz.webm'), + ru: require('@/assets/manual/manual_video_ru.webm'), + en: require('@/assets/manual/manual_video_en.webm'), }; const player = useVideoPlayer(videos[selectedLang], (player) => { @@ -161,6 +160,17 @@ export function ManualTab() { 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 }) => ( { @@ -186,18 +196,17 @@ export function ManualTab() { {t("Foydalanish qo'llanmasi")} - - - {t("Quyidagi qisqa video yoki rasmlarni ko'rib chiqing")} - {/* VIDEO */} - {t("Foydalanish video qo'llanma")} + {t("Foydalanish qo'llanmasi")} - + + {t("Quyidagi qisqa video yoki rasmlarni ko'rib chiqing")} + + {/* - + */} + + {!isPlaying && ( + + { + player.play(); + setIsPlaying(true); + }} + > + + + + )} {/* RASMLAR */} - + {/* {t('Foydalanish rasm qo‘llanma')} - + */} {/* LANGUAGE MODAL */} - setLangPickerVisible(false)}> + {/* setLangPickerVisible(false)}> {t('Video tilini tanlang')} @@ -299,7 +341,7 @@ export function ManualTab() { ); })} - + */} ); } @@ -308,8 +350,8 @@ const styles = StyleSheet.create({ container: { flex: 1 }, hero: { padding: 20 }, topHeader: { flexDirection: 'row', alignItems: 'center', gap: 12 }, - headerTitle: { fontSize: 22, fontWeight: '700' }, - subtitle: { marginTop: 8 }, + headerTitle: { fontSize: 22, fontWeight: '700', marginHorizontal: 16 }, + subtitle: { fontSize: 16, marginTop: 5, fontWeight: '500', marginHorizontal: 16 }, section: { marginBottom: 28 }, sectionTitle: { fontSize: 20, fontWeight: '700', marginLeft: 16 }, @@ -329,6 +371,7 @@ const styles = StyleSheet.create({ videoCard: { marginHorizontal: 16, + marginTop: 10, borderRadius: 20, overflow: 'hidden', position: 'relative', diff --git a/screens/profile/ui/MyServices.tsx b/screens/profile/ui/MyServices.tsx index 6ae60c5..78c7256 100644 --- a/screens/profile/ui/MyServices.tsx +++ b/screens/profile/ui/MyServices.tsx @@ -1,5 +1,4 @@ import { useTheme } from '@/components/ThemeContext'; -import { useGlobalRefresh } from '@/components/ui/RefreshContext'; import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Image as ExpoImage } from 'expo-image'; import { useRouter } from 'expo-router'; @@ -15,20 +14,18 @@ import { View, } from 'react-native'; import { RefreshControl } from 'react-native-gesture-handler'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { user_api } from '../lib/api'; -const PAGE_SIZE = 5; +const PAGE_SIZE = 10; export default function MyServicesScreen() { const router = useRouter(); - const { onRefresh, refreshing } = useGlobalRefresh(); const queryClient = useQueryClient(); const { isDark } = useTheme(); const { t } = useTranslation(); /* ================= QUERY ================= */ - const { data, isLoading, isError, fetchNextPage, hasNextPage } = useInfiniteQuery({ + const { data, isLoading, isError, fetchNextPage, hasNextPage, isRefetching } = useInfiniteQuery({ queryKey: ['my_services'], queryFn: async ({ pageParam = 1 }) => { const res = await user_api.my_sevices({ @@ -69,9 +66,13 @@ export default function MyServicesScreen() { ]); }; + const onRefresh = () => { + queryClient.refetchQueries({ queryKey: ['my_services'] }); + }; + if (isLoading) { return ( - + router.push('/profile')}> @@ -84,7 +85,7 @@ export default function MyServicesScreen() { - + ); } @@ -97,7 +98,7 @@ export default function MyServicesScreen() { } return ( - + {/* HEADER */} router.push('/profile')}> @@ -119,7 +120,7 @@ export default function MyServicesScreen() { onEndReached={() => hasNextPage && fetchNextPage()} refreshControl={ } /> - + ); } @@ -253,7 +254,7 @@ const styles = StyleSheet.create({ elevation: 3, }, 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' }, mediaContainer: { width: '100%', height: 200 }, media: { width: '100%', height: '100%' }, diff --git a/screens/profile/ui/NotificationTab.tsx b/screens/profile/ui/NotificationTab.tsx index 3b68aed..9135352 100644 --- a/screens/profile/ui/NotificationTab.tsx +++ b/screens/profile/ui/NotificationTab.tsx @@ -38,6 +38,15 @@ export function NotificationTab() { initialPageParam: 1, }); 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) { return ( @@ -82,6 +91,25 @@ export function NotificationTab() { {t('Bildirishnomalar')} + + {notifications.some((n) => !n.is_read) && ( + markAllAsRead()} + disabled={isMarkingAllRead} + > + {isMarkingAllRead ? ( + + ) : ( + + {t("Barchasi o'qildi")} + + )} + + )} user_api.getMe(), }); - const sections = [ + const sections: SectionType[] = [ { title: 'Shaxsiy', items: [ @@ -53,17 +61,17 @@ export default function Profile() { { title: 'Faoliyat', 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: Package, label: 'Xizmatlar', route: '/profile/products' }, ...(me?.data.data.can_create_referral ? [ - { - icon: HandCoins, - label: 'Refferallarim', - route: '/profile/my-referrals', - }, - ] + { + icon: HandCoins, + label: 'Refferallarim', + route: '/profile/my-referrals', + }, + ] : []), ], }, @@ -79,6 +87,7 @@ export default function Profile() { return ( - {item.badge && ( + {item?.badge && ( {item.badge} )} - - - {t(item.label)} - - + + {item.image ? ( + + + + {t("Bir Zumda Jonatish")} + + + ) : ( + + + {t(item.label)} + + + )} + ))} @@ -131,7 +155,7 @@ export default function Profile() { const styles = StyleSheet.create({ content: { flex: 1, - marginBottom: 50, + paddingBottom: 120, }, darkBg: { diff --git a/screens/profile/ui/RefferallsTab.tsx b/screens/profile/ui/RefferallsTab.tsx index f5ba03d..0f71d57 100644 --- a/screens/profile/ui/RefferallsTab.tsx +++ b/screens/profile/ui/RefferallsTab.tsx @@ -8,8 +8,10 @@ import { useTranslation } from 'react-i18next'; import { ActivityIndicator, FlatList, + Platform, Pressable, RefreshControl, + Share, StyleSheet, Text, ToastAndroid, @@ -64,9 +66,31 @@ export function ReferralsTab() { 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) { return ( - + ); @@ -103,26 +127,12 @@ export function ReferralsTab() { onEndReached={() => hasNextPage && fetchNextPage()} renderItem={({ item }) => ( - + {item.code} - { - await Clipboard.setStringAsync( - `https://t.me/infotargetbot/join?startapp=${item.code}` - ); - ToastAndroid.show('Refferal kopiya qilindi', ToastAndroid.SHORT); - }} - > + handleCopyAndShare(item.code)}> @@ -165,7 +175,7 @@ const styles = StyleSheet.create({ }, headerTitle: { fontSize: 18, fontWeight: '700' }, - list: { padding: 16, gap: 12 }, + list: { padding: 16, gap: 12, paddingBottom: 30 }, card: { borderRadius: 16, @@ -173,6 +183,13 @@ const styles = StyleSheet.create({ gap: 10, }, + cardRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignContent: 'center', + alignItems: 'center', + }, + cardHeader: { flexDirection: 'row', gap: 8,