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