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