government ui complated

This commit is contained in:
Samandar Turgunboyev
2026-02-05 16:09:03 +05:00
parent 5d31fe8ff4
commit 754f11804a
76 changed files with 2459 additions and 672 deletions

View File

@@ -47,7 +47,7 @@ export default function AddEmployee() {
user_api.create_employee(body),
onSuccess: () => {
router.push('/profile/employees');
queryClient.refetchQueries({ queryKey: ['employees-list'] });
queryClient.refetchQueries({ queryKey: ['employees_list'] });
},
onError: (err: AxiosError) => {
const errMessage = (err.response?.data as { data: { phone: string[] } }).data.phone[0];

View File

@@ -1,190 +1,395 @@
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 { 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 { 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 = {
title: string;
text: string;
image?: any;
image: any;
description?: string;
};
type Language = {
code: 'uz' | 'ru' | 'en';
label: string;
flag: string;
};
const languages: Language[] = [
{ code: 'uz', label: "O'zbekcha", flag: '🇺🇿' },
{ code: 'ru', label: 'Русский', flag: '🇷🇺' },
{ code: 'en', label: 'English', flag: '🇬🇧' },
];
export function ManualTab() {
const { isDark } = useTheme();
const { t } = useTranslation();
const router = useRouter();
const [videoLang, setVideoLang] = useState<'uz' | 'ru' | 'en'>('uz');
const { t, i18n } = useTranslation();
/** 🔹 Modal states */
const [imageVisible, setImageVisible] = useState(false);
const [imageIndex, setImageIndex] = useState(0);
const [langPickerVisible, setLangPickerVisible] = useState(false);
/** 🔹 Video tili (SELECT uchun) */
const userLang = i18n.language.startsWith('ru')
? 'ru'
: i18n.language.startsWith('en')
? 'en'
: 'uz';
const [selectedLang, setSelectedLang] = useState<'uz' | 'ru' | 'en'>(userLang);
/** 🔹 RASM tili (faqat app tili boyicha) */
const imageLang: 'uz' | 'ru' | 'en' = userLang;
/** 🔹 Theme */
const theme = {
background: isDark ? '#0f172a' : '#f8fafc',
cardBg: isDark ? '#1e293b' : '#ffffff',
text: isDark ? '#ffffff' : '#0f172a',
textSecondary: isDark ? '#94a3b8' : '#64748b',
primary: '#3b82f6',
textMuted: isDark ? '#94a3b8' : '#64748b',
border: isDark ? '#334155' : '#e2e8f0',
};
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> = {
/** 🔹 Video manbalari (SELECT ga bogliq) */
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'),
};
const handleVideoChange = (lang: 'uz' | 'ru' | 'en') => {
setVideoLang(lang);
};
const player = useVideoPlayer(videos[selectedLang], (player) => {
player.loop = false;
});
/** 🔹 Rasmlar (FAqat imageLang) */
const steps: ManualStep[] = [
{
image:
imageLang === 'uz'
? require('@/assets/manual/image_uz/step1.jpg')
: imageLang === 'ru'
? require('@/assets/manual/image_ru/step1.jpg')
: require('@/assets/manual/image_en/step1.jpg'),
description: t("Ilovani oching va ro'yxatdan o'ting"),
},
{
image:
imageLang === 'uz'
? require('@/assets/manual/image_uz/step2.jpg')
: imageLang === 'ru'
? require('@/assets/manual/image_ru/step2.jpg')
: require('@/assets/manual/image_en/step2.jpg'),
description: t("Profilni to'ldiring"),
},
{
image:
imageLang === 'uz'
? require('@/assets/manual/image_uz/step3.jpg')
: imageLang === 'ru'
? require('@/assets/manual/image_ru/step3.jpg')
: require('@/assets/manual/image_en/step3.jpg'),
description: t("To'lov usulini qo'shing"),
},
{
image:
imageLang === 'uz'
? require('@/assets/manual/image_uz/step4.jpg')
: imageLang === 'ru'
? require('@/assets/manual/image_ru/step4.jpg')
: require('@/assets/manual/image_en/step4.jpg'),
description: t('Xaridni tanlang'),
},
{
image:
imageLang === 'uz'
? require('@/assets/manual/image_uz/step5.jpg')
: imageLang === 'ru'
? require('@/assets/manual/image_ru/step5.jpg')
: require('@/assets/manual/image_en/step5.jpg'),
description: t("To'lovni amalga oshiring"),
},
{
image:
imageLang === 'uz'
? require('@/assets/manual/image_uz/step6.jpg')
: imageLang === 'ru'
? require('@/assets/manual/image_ru/step6.jpg')
: require('@/assets/manual/image_en/step6.jpg'),
description: t("Natijani ko'ring"),
},
{
image:
imageLang === 'uz'
? require('@/assets/manual/image_uz/step7.jpg')
: imageLang === 'ru'
? require('@/assets/manual/image_ru/step7.jpg')
: require('@/assets/manual/image_en/step7.jpg'),
description: t("Natijani ko'ring"),
},
{
image:
imageLang === 'uz'
? require('@/assets/manual/image_uz/step8.jpg')
: imageLang === 'ru'
? require('@/assets/manual/image_ru/step8.jpg')
: require('@/assets/manual/image_en/step8.jpg'),
description: t("Natijani ko'ring"),
},
];
/** 🔹 Image viewer */
const images = useMemo(
() => steps.map((step) => ({ url: '', props: { source: step.image } })),
[imageLang]
);
const selectedLanguage = languages.find((l) => l.code === selectedLang);
const renderStep = ({ item, index }: { item: ManualStep; index: number }) => (
<Pressable
onPress={() => {
setImageIndex(index);
setImageVisible(true);
}}
>
<View style={[styles.stepCard, { backgroundColor: theme.cardBg }]}>
<Image source={item.image} style={styles.stepImage} />
</View>
</Pressable>
);
return (
<ScrollView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
</Pressable>
{/* HEADER */}
<View style={styles.hero}>
<View style={styles.topHeader}>
<Pressable onPress={() => router.back()}>
<ArrowLeft color={theme.text} size={24} />
</Pressable>
<Text style={[styles.headerTitle, { color: theme.text }]}>
{t("Foydalanish qo'llanmasi")}
</Text>
</View>
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
{t("Foydalanish qo'lanmasi")}
<Text style={[styles.subtitle, { color: theme.textMuted }]}>
{t("Quyidagi qisqa video yoki rasmlarni ko'rib chiqing")}
</Text>
</View>
{steps.map((step, index) => (
<View key={index} style={[styles.card, { backgroundColor: theme.cardBg }]}>
<Text style={[styles.headerTitle, { color: theme.text }]}>{t(step.title)}</Text>
<Text style={[styles.text, { color: theme.textSecondary, marginTop: 8 }]}>
{t(step.text)}
</Text>
{step.image && <Image source={step.image} style={styles.image} resizeMode="contain" />}
</View>
))}
{/* Video bo'limi */}
<View style={[styles.card, { backgroundColor: theme.cardBg }]}>
<Text style={[styles.headerTitle, { color: theme.text }]}>{t("Qo'llanma video")}</Text>
{/* Til tanlash tugmalari */}
<View style={styles.buttonRow}>
<TouchableOpacity
{/* VIDEO */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: theme.text }]}>
{t("Foydalanish video qo'llanma")}
</Text>
<View style={{ alignContent: 'flex-end', alignItems: 'flex-end', paddingHorizontal: 16 }}>
<Pressable
style={[
styles.langButton,
{
backgroundColor: videoLang === 'uz' ? theme.primary : theme.cardBg,
borderColor: theme.primary,
},
styles.langSelector,
{ backgroundColor: theme.cardBg, borderColor: theme.border },
]}
onPress={() => handleVideoChange('uz')}
onPress={() => setLangPickerVisible(true)}
>
<Text style={{ color: videoLang === 'uz' ? '#fff' : theme.text }}>O'zbek</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.langButton,
{
backgroundColor: videoLang === 'ru' ? theme.primary : theme.cardBg,
borderColor: theme.primary,
},
]}
onPress={() => handleVideoChange('ru')}
>
<Text style={{ color: videoLang === 'ru' ? '#fff' : theme.text }}>Русский</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.langButton,
{
backgroundColor: videoLang === 'en' ? theme.primary : theme.cardBg,
borderColor: theme.primary,
},
]}
onPress={() => handleVideoChange('en')}
>
<Text style={{ color: videoLang === 'en' ? '#fff' : theme.text }}>English</Text>
</TouchableOpacity>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ fontSize: 20 }}>{selectedLanguage?.flag}</Text>
</View>
<ChevronDown color={theme.textMuted} />
</Pressable>
</View>
<Video
source={videos[videoLang]}
style={styles.video}
useNativeControls
isLooping
resizeMode={ResizeMode.COVER}
<View style={[styles.videoCard, { backgroundColor: theme.cardBg }]}>
<VideoView
player={player}
style={styles.video}
allowsFullscreen
allowsPictureInPicture
contentFit="cover"
/>
</View>
</View>
{/* RASMLAR */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: theme.text }]}>
{t('Foydalanish rasm qollanma')}
</Text>
<FlatList
horizontal
data={steps}
renderItem={renderStep}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16, gap: 20 }}
/>
</View>
{/* IMAGE MODAL */}
<Modal isVisible={imageVisible} style={{ margin: 0 }}>
<ImageViewer
imageUrls={images}
index={imageIndex}
enableSwipeDown
onSwipeDown={() => setImageVisible(false)}
renderHeader={() => (
<Pressable style={styles.closeButton} onPress={() => setImageVisible(false)}>
<X size={32} color="#fff" />
</Pressable>
)}
/>
</Modal>
{/* LANGUAGE MODAL */}
<Modal isVisible={langPickerVisible} onBackdropPress={() => setLangPickerVisible(false)}>
<View style={[styles.langModalContent, { backgroundColor: theme.cardBg }]}>
<Text style={{ color: 'white', marginBottom: 10, fontSize: 18 }}>
{t('Video tilini tanlang')}
</Text>
{languages.map((lang) => {
const isSelected = selectedLang === lang.code;
return (
<Pressable
key={lang.code}
onPress={() => {
setSelectedLang(lang.code);
setLangPickerVisible(false);
}}
style={[
styles.langOption,
{
borderColor: isSelected ? theme.primary : theme.border,
backgroundColor: isSelected ? theme.primary + '15' : 'transparent',
},
]}
>
<View style={styles.langOptionLeft}>
<Text style={{ fontSize: 26 }}>{lang.flag}</Text>
<Text
style={{
color: isSelected ? theme.primary : theme.text,
fontWeight: isSelected ? '700' : '500',
fontSize: 16,
}}
>
{lang.label}
</Text>
</View>
{isSelected && (
<View style={[styles.checkmark, { backgroundColor: theme.primary }]}>
<Check size={20} color={'white'} />
</View>
)}
</Pressable>
);
})}
</View>
</Modal>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
card: { borderRadius: 16, padding: 16, marginBottom: 16 },
headerTitle: { fontSize: 18, fontWeight: '700', lineHeight: 24 },
text: { fontSize: 14, lineHeight: 20 },
image: { width: width - 64, height: 200, marginTop: 12, borderRadius: 12 },
video: { width: width - 64, height: 320, marginTop: 12, borderRadius: 12 },
buttonRow: { flexDirection: 'row', justifyContent: 'space-around', marginVertical: 12 },
langButton: {
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
container: { flex: 1 },
hero: { padding: 20 },
topHeader: { flexDirection: 'row', alignItems: 'center', gap: 12 },
headerTitle: { fontSize: 22, fontWeight: '700' },
subtitle: { marginTop: 8 },
section: { marginBottom: 28 },
sectionTitle: { fontSize: 20, fontWeight: '700', marginLeft: 16 },
langSelector: {
margin: 5,
width: 80,
gap: 5,
paddingVertical: 5,
borderRadius: 14,
borderWidth: 1.5,
alignContent: 'center',
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
},
header: {
padding: 16,
videoCard: {
marginHorizontal: 16,
borderRadius: 20,
overflow: 'hidden',
position: 'relative',
},
video: { width: '100%', aspectRatio: 9 / 13 },
stepCard: { borderRadius: 16, overflow: 'hidden' },
stepImage: { width: 300, height: 300 },
closeButton: {
position: 'absolute',
top: 50,
right: 20,
padding: 8,
zIndex: 50,
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 20,
},
langModalContent: {
padding: 20,
borderRadius: 20,
},
langOption: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: 10,
elevation: 3,
justifyContent: 'space-between',
paddingVertical: 14,
paddingHorizontal: 16,
borderRadius: 14,
borderWidth: 1.5,
marginBottom: 10,
},
langOptionLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
checkmark: {
width: 24,
height: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
checkmarkText: {
color: '#fff',
fontSize: 14,
fontWeight: 'bold',
},
playOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.25)',
},
});

View File

@@ -41,7 +41,14 @@ export function NotificationTab() {
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<View
style={[
styles.loadingContainer,
{
backgroundColor: isDark ? '#0f172a' : '#f8fafc',
},
]}
>
<View style={styles.loadingContent}>
<ActivityIndicator size="large" color="#3b82f6" />
</View>
@@ -51,10 +58,12 @@ export function NotificationTab() {
if (isError) {
return (
<View style={styles.errorContainer}>
<View style={styles.errorContent}>
<View style={[styles.errorContainer, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.errorContent, { backgroundColor: isDark ? '#1e293b' : '#ffffff' }]}>
<Text style={styles.errorTitle}>{t('Xatolik yuz berdi')}</Text>
<Text style={styles.errorMessage}>{t("Bildirishnomalarni yuklashda muammo bo'ldi")}</Text>
<Text style={[styles.errorMessage, { color: isDark ? '#cbd5e1' : '#64748b' }]}>
{t("Bildirishnomalarni yuklashda muammo bo'ldi")}
</Text>
<TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
<Text style={styles.retryButtonText}>{t('Qayta urinish')}</Text>
</TouchableOpacity>
@@ -64,13 +73,13 @@ export function NotificationTab() {
}
return (
<View style={styles.container}>
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#fff' }]}>
<View style={[styles.container, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<View style={[styles.header, { backgroundColor: isDark ? '#0f172a' : '#f8fafc' }]}>
<Pressable onPress={() => router.push('/profile')}>
<ArrowLeft color={isDark ? '#fff' : '#0f172a'} />
</Pressable>
<Text style={[styles.headerTitle, { color: isDark ? '#fff' : '#0f172a' }]}>
<Text style={[styles.headerTitle, { color: isDark ? '#f1f5f9' : '#0f172a' }]}>
{t('Bildirishnomalar')}
</Text>
</View>
@@ -102,12 +111,14 @@ export function NotificationTab() {
function NotificationCard({ item }: { item: NotificationListDataRes }) {
const queryClient = useQueryClient();
const { isDark } = useTheme();
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'] });
queryClient.refetchQueries({ queryKey: ['notifications-list'] });
},
});
@@ -145,21 +156,54 @@ function NotificationCard({ item }: { item: NotificationListDataRes }) {
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={() => handlePress(item.id)}
style={[styles.card, !item.is_read && styles.unreadCard]}
style={[
styles.card,
!item.is_read && styles.unreadCard,
{
backgroundColor: isDark ? '#283046' : '#e0f2fe',
borderColor: isDark ? '#3b82f6' : '#3b82f6',
},
]}
>
<View style={styles.cardContent}>
<View style={styles.cardHeader}>
<Text style={[styles.cardTitle, !item.is_read && styles.unreadTitle]} numberOfLines={1}>
<Text
style={[
styles.cardTitle,
!item.is_read && styles.unreadTitle,
{ color: isDark ? '#3b82f6' : '#1e40af' },
]}
numberOfLines={1}
>
{item.title}
</Text>
{!item.is_read && <View style={styles.unreadIndicator} />}
{!item.is_read && (
<View
style={[
styles.unreadIndicator,
{
backgroundColor: isDark ? '#3b82f6' : '#3b82f6',
},
]}
/>
)}
</View>
<Text style={styles.cardMessage} numberOfLines={2}>
<Text
style={[
styles.cardMessage,
{
color: isDark ? '#cbd5e1' : '#64748b',
},
]}
numberOfLines={2}
>
{item.description}
</Text>
<Text style={styles.cardTime}>{formatDate(item.created_at, t)}</Text>
<Text style={[styles.cardTime, { color: isDark ? '#94a3b8' : '#94a3b8' }]}>
{formatDate(item.created_at, t)}
</Text>
</View>
</TouchableOpacity>
</Animated.View>
@@ -187,12 +231,9 @@ function formatDate(date: string, t: any) {
});
}
/* ---------------- STYLES ---------------- */
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0a0c17',
},
listContent: {
padding: 16,
@@ -202,41 +243,23 @@ const styles = StyleSheet.create({
/* 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,
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 8,
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,
shadowOpacity: 0.3,
shadowRadius: 16,
elevation: 10,
},
cardContent: {
flex: 1,
justifyContent: 'center',
@@ -248,20 +271,20 @@ const styles = StyleSheet.create({
},
cardTitle: {
flex: 1,
color: '#d1d5db',
fontSize: 16.5,
fontWeight: '600',
letterSpacing: -0.1,
},
unreadTitle: {
color: '#f1f5f9',
fontWeight: '700',
// unread title rang
},
unreadIndicator: {
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: '#3b82f6',
marginLeft: 8,
shadowColor: '#3b82f6',
shadowOffset: { width: 0, height: 0 },
@@ -269,13 +292,11 @@ const styles = StyleSheet.create({
shadowRadius: 6,
},
cardMessage: {
color: '#9ca3af',
fontSize: 14.5,
lineHeight: 21,
marginBottom: 8,
},
cardTime: {
color: '#64748b',
fontSize: 12.5,
fontWeight: '500',
opacity: 0.9,
@@ -284,7 +305,7 @@ const styles = StyleSheet.create({
/* Loading State */
loadingContainer: {
flex: 1,
backgroundColor: '#0a0c17',
alignItems: 'center',
justifyContent: 'center',
},
@@ -292,40 +313,22 @@ const styles = StyleSheet.create({
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,
@@ -333,7 +336,6 @@ const styles = StyleSheet.create({
marginBottom: 8,
},
errorMessage: {
color: '#94a3b8',
fontSize: 14,
textAlign: 'center',
marginBottom: 24,
@@ -356,52 +358,19 @@ const styles = StyleSheet.create({
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 */
header: {
padding: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: 10,
elevation: 3,
},
headerTitle: { fontSize: 18, fontWeight: '700', lineHeight: 24 },
headerTitle: {
fontSize: 18,
fontWeight: '700',
lineHeight: 24,
},
});

View File

@@ -25,7 +25,13 @@ export default function Profile() {
const { isDark } = useTheme();
const { t } = useTranslation();
const { data: me, isLoading } = useQuery({
const { data } = useQuery({
queryKey: ['notification-list'],
queryFn: () => user_api.notification_list({ page: 1, page_size: 1 }),
});
const unreadCount = data?.data?.data.unread_count ?? 0;
const { data: me } = useQuery({
queryKey: ['get_me'],
queryFn: () => user_api.getMe(),
});
@@ -36,7 +42,12 @@ export default function Profile() {
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' },
{
icon: Bell,
label: 'Bildirishnomalar',
route: '/profile/notification',
badge: unreadCount,
},
],
},
{
@@ -60,7 +71,7 @@ export default function Profile() {
title: 'Sozlamalar',
items: [
{ icon: Settings, label: 'Sozlamalar', route: '/profile/settings' },
{ icon: BookAIcon, label: "Foydalanish qo'lanmasi", route: '/profile/manual' },
{ icon: BookAIcon, label: "Foydalanish qo'llanmasi", route: '/profile/manual' },
],
},
];
@@ -95,6 +106,12 @@ export default function Profile() {
style={[styles.iconContainer, isDark ? styles.darkIconBg : styles.lightIconBg]}
>
<item.icon size={24} color="#3b82f6" />
{item.badge && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{item.badge}</Text>
</View>
)}
</View>
<Text style={[styles.cardLabel, isDark ? styles.darkText : styles.lightText]}>
@@ -202,4 +219,23 @@ const styles = StyleSheet.create({
lightText: {
color: '#0f172a',
},
badge: {
position: 'absolute',
top: -5,
right: -5,
backgroundColor: '#ef4444', // qizil badge
borderRadius: 8,
minWidth: 16,
height: 16,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 3,
},
badgeText: {
color: '#ffffff',
fontSize: 10,
fontWeight: '700',
textAlign: 'center',
},
});

View File

@@ -1,6 +1,6 @@
import { useTheme } from '@/components/ThemeContext';
import * as ImagePicker from 'expo-image-picker';
import { Camera, Play, X } from 'lucide-react-native';
import { Image as ImageIcon, Play, Video, X } from 'lucide-react-native';
import React, { forwardRef, useImperativeHandle, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
@@ -18,12 +18,15 @@ type Errors = {
media?: string;
};
const MAX_MEDIA = 10;
type MediaTabType = 'image' | 'video';
const MAX_MEDIA = 1;
const StepOneServices = forwardRef(({ formData, updateForm, removeMedia }: StepProps, ref) => {
const { isDark } = useTheme();
const { t } = useTranslation();
const [errors, setErrors] = useState<Errors>({});
const [selectedMediaTab, setSelectedMediaTab] = useState<MediaTabType>('image');
const validate = () => {
const e: Errors = {};
@@ -34,8 +37,7 @@ const StepOneServices = forwardRef(({ formData, updateForm, removeMedia }: StepP
if (!formData.description || formData.description.trim().length < 10)
e.description = t("Tavsif kamida 10 ta belgidan iborat bo'lishi kerak");
if (!formData.media || formData.media.length === 0)
e.media = t('Kamida bitta rasm yoki video yuklang');
if (!formData.media || formData.media.length === 0) e.media = t('Rasm yoki video yuklang');
setErrors(e);
return Object.keys(e).length === 0;
@@ -46,101 +48,199 @@ const StepOneServices = forwardRef(({ formData, updateForm, removeMedia }: StepP
const pickMedia = async () => {
if (formData.media.length >= MAX_MEDIA) return;
const mediaType =
selectedMediaTab === 'image'
? ImagePicker.MediaTypeOptions.Images
: ImagePicker.MediaTypeOptions.Videos;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsMultipleSelection: true,
mediaTypes: mediaType,
allowsMultipleSelection: false,
quality: 0.8,
videoMaxDuration: 60, // 60 seconds max for video
});
if (!result.canceled) {
const assets = result.assets
.slice(0, MAX_MEDIA - formData.media.length)
.map((a) => ({ uri: a.uri, type: a.type as 'image' | 'video' }));
const asset = result.assets[0];
const mediaItem = {
uri: asset.uri,
type: selectedMediaTab,
};
updateForm('media', [...formData.media, ...assets]);
updateForm('media', [mediaItem]);
}
};
const theme = {
background: isDark ? '#0f172a' : '#ffffff',
inputBg: isDark ? '#1e293b' : '#f1f5f9',
inputBorder: isDark ? '#334155' : '#e2e8f0',
text: isDark ? '#f8fafc' : '#0f172a',
textSecondary: isDark ? '#cbd5e1' : '#475569',
placeholder: isDark ? '#94a3b8' : '#94a3b8',
error: '#ef4444',
primary: '#2563eb',
tabActive: isDark ? '#2563eb' : '#3b82f6',
tabInactive: isDark ? '#334155' : '#e2e8f0',
};
return (
<View style={styles.stepContainer}>
{/* Sarlavha */}
<Text style={[styles.label, { color: isDark ? '#f8fafc' : '#0f172a' }]}>{t('Sarlavha')}</Text>
<Text style={[styles.label, { color: theme.text }]}>{t('Sarlavha')}</Text>
<View
style={[
styles.inputBox,
{
backgroundColor: isDark ? '#1e293b' : '#ffffff',
borderColor: isDark ? '#334155' : '#e2e8f0',
backgroundColor: theme.inputBg,
borderColor: theme.inputBorder,
},
]}
>
<TextInput
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
style={[styles.input, { color: theme.text }]}
placeholder={t('Xizmat sarlavhasi')}
placeholderTextColor={isDark ? '#94a3b8' : '#94a3b8'}
placeholderTextColor={theme.placeholder}
value={formData.title}
onChangeText={(t) => updateForm('title', t)}
/>
</View>
{errors.title && <Text style={styles.error}>{errors.title}</Text>}
{errors.title && <Text style={[styles.error, { color: theme.error }]}>{errors.title}</Text>}
{/* Tavsif */}
<Text style={[styles.label, { color: isDark ? '#f8fafc' : '#0f172a' }]}>{t('Tavsif')}</Text>
<Text style={[styles.label, { color: theme.text }]}>{t('Tavsif')}</Text>
<View
style={[
styles.inputBox,
styles.textArea,
{
backgroundColor: isDark ? '#1e293b' : '#ffffff',
borderColor: isDark ? '#334155' : '#e2e8f0',
backgroundColor: theme.inputBg,
borderColor: theme.inputBorder,
},
]}
>
<TextInput
style={[styles.input, { color: isDark ? '#fff' : '#0f172a' }]}
style={[styles.input, { color: theme.text }]}
placeholder={t('Batafsil yozing...')}
placeholderTextColor={isDark ? '#94a3b8' : '#94a3b8'}
placeholderTextColor={theme.placeholder}
multiline
value={formData.description}
onChangeText={(t) => updateForm('description', t)}
/>
</View>
{errors.description && <Text style={styles.error}>{errors.description}</Text>}
{errors.description && (
<Text style={[styles.error, { color: theme.error }]}>{errors.description}</Text>
)}
{/* Media */}
<Text style={[styles.label, { color: isDark ? '#f8fafc' : '#0f172a' }]}>
{t('Media')} ({formData.media.length}/{MAX_MEDIA})
</Text>
<View style={styles.media}>
{/* Media Type Tabs */}
<Text style={[styles.label, { color: theme.text }]}>{t('Media turi')}</Text>
<View style={styles.tabsContainer}>
<TouchableOpacity
style={[
styles.upload,
styles.tab,
selectedMediaTab === 'image' && styles.tabActive,
{
borderColor: '#2563eb',
backgroundColor: isDark ? '#1e293b' : '#f8fafc',
backgroundColor: selectedMediaTab === 'image' ? theme.tabActive : theme.tabInactive,
borderColor: selectedMediaTab === 'image' ? theme.tabActive : theme.inputBorder,
},
]}
onPress={pickMedia}
onPress={() => setSelectedMediaTab('image')}
activeOpacity={0.7}
>
<Camera size={28} color="#2563eb" />
<Text style={styles.uploadText}>{t('Yuklash')}</Text>
<ImageIcon
size={20}
color={selectedMediaTab === 'image' ? '#ffffff' : theme.textSecondary}
/>
<Text
style={[
styles.tabText,
{ color: selectedMediaTab === 'image' ? '#ffffff' : theme.textSecondary },
]}
>
{t('Rasm')}
</Text>
</TouchableOpacity>
{formData.media.map((m: MediaType, i: number) => (
<View key={i} style={styles.preview}>
<Image source={{ uri: m.uri }} style={styles.image} />
{m.type === 'video' && (
<View style={styles.play}>
<Play size={14} color="#fff" fill="#fff" />
<TouchableOpacity
style={[
styles.tab,
selectedMediaTab === 'video' && styles.tabActive,
{
backgroundColor: selectedMediaTab === 'video' ? theme.tabActive : theme.tabInactive,
borderColor: selectedMediaTab === 'video' ? theme.tabActive : theme.inputBorder,
},
]}
onPress={() => setSelectedMediaTab('video')}
activeOpacity={0.7}
>
<Video size={20} color={selectedMediaTab === 'video' ? '#ffffff' : theme.textSecondary} />
<Text
style={[
styles.tabText,
{ color: selectedMediaTab === 'video' ? '#ffffff' : theme.textSecondary },
]}
>
{t('Video')}
</Text>
</TouchableOpacity>
</View>
{/* Media Upload/Preview */}
<View style={styles.mediaContainer}>
{formData.media.length === 0 ? (
<TouchableOpacity
style={[
styles.uploadLarge,
{ borderColor: theme.primary, backgroundColor: theme.inputBg },
]}
onPress={pickMedia}
activeOpacity={0.7}
>
<View style={[styles.uploadIconWrapper, { backgroundColor: theme.primary }]}>
{selectedMediaTab === 'image' ? (
<ImageIcon size={32} color="#ffffff" />
) : (
<Video size={32} color="#ffffff" />
)}
</View>
<Text style={[styles.uploadLargeText, { color: theme.text }]}>
{selectedMediaTab === 'image' ? t('Rasm yuklash') : t('Video yuklash')}
</Text>
<Text style={[styles.uploadLargeSubtext, { color: theme.textSecondary }]}>
{selectedMediaTab === 'image' ? t('Rasm tanlang') : t('Video tanlang')}
</Text>
</TouchableOpacity>
) : (
<View style={styles.previewLarge}>
<Image source={{ uri: formData.media[0].uri }} style={styles.imageLarge} />
{formData.media[0].type === 'video' && (
<View style={styles.playLarge}>
<Play size={24} color="#fff" fill="#fff" />
</View>
)}
<TouchableOpacity style={styles.remove} onPress={() => removeMedia(i)}>
<X size={12} color="#fff" />
<TouchableOpacity
style={[styles.removeLarge, { backgroundColor: theme.error }]}
onPress={() => removeMedia(0)}
activeOpacity={0.8}
>
<X size={16} color="#fff" />
</TouchableOpacity>
<View style={[styles.mediaTypeBadge, { backgroundColor: 'rgba(0,0,0,0.7)' }]}>
{formData.media[0].type === 'image' ? (
<ImageIcon size={14} color="#fff" />
) : (
<Video size={14} color="#fff" />
)}
<Text style={styles.mediaTypeBadgeText}>
{formData.media[0].type === 'image' ? 'Rasm' : 'Video'}
</Text>
</View>
</View>
))}
)}
</View>
{errors.media && <Text style={styles.error}>{errors.media}</Text>}
{errors.media && (
<Text style={[styles.error, { color: theme.error }]}>{t(errors.media)}</Text>
)}
</View>
);
});
@@ -150,7 +250,7 @@ export default StepOneServices;
const styles = StyleSheet.create({
stepContainer: { gap: 10 },
label: { fontWeight: '700', fontSize: 15 },
error: { color: '#ef4444', fontSize: 13, marginLeft: 6 },
error: { fontSize: 13, marginLeft: 6 },
inputBox: {
flexDirection: 'row',
alignItems: 'center',
@@ -159,53 +259,125 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
height: 56,
},
textArea: { height: 120, alignItems: 'flex-start', paddingTop: 16 },
textArea: { height: 120, alignItems: 'flex-start', paddingVertical: 12 },
input: { flex: 1, fontSize: 16 },
media: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
upload: {
width: 100,
height: 100,
// Media Tabs
tabsContainer: {
flexDirection: 'row',
gap: 12,
},
tab: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
paddingHorizontal: 16,
borderRadius: 16,
borderWidth: 2,
gap: 8,
},
tabActive: {
shadowColor: '#2563eb',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3,
},
tabText: {
fontSize: 15,
fontWeight: '700',
letterSpacing: 0.3,
},
// Media Container
mediaContainer: {
marginTop: 4,
},
uploadLarge: {
height: 240,
borderRadius: 20,
borderWidth: 2,
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
gap: 12,
},
uploadText: { color: '#2563eb', fontSize: 11, marginTop: 4, fontWeight: '600' },
preview: { width: 100, height: 100 },
image: { width: '100%', height: '100%', borderRadius: 16 },
play: {
position: 'absolute',
top: '40%',
left: '40%',
backgroundColor: 'rgba(0,0,0,.5)',
padding: 6,
uploadIconWrapper: {
width: 72,
height: 72,
borderRadius: 36,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 3,
},
uploadLargeText: {
fontSize: 18,
fontWeight: '700',
marginTop: 4,
},
uploadLargeSubtext: {
fontSize: 14,
fontWeight: '500',
textAlign: 'center',
paddingHorizontal: 32,
},
previewLarge: {
height: 240,
borderRadius: 20,
overflow: 'hidden',
position: 'relative',
},
imageLarge: {
width: '100%',
height: '100%',
borderRadius: 20,
},
remove: {
playLarge: {
position: 'absolute',
top: -6,
right: -6,
backgroundColor: '#ef4444',
padding: 4,
borderRadius: 10,
top: '50%',
left: '50%',
transform: [{ translateX: -28 }, { translateY: -28 }],
backgroundColor: 'rgba(0,0,0,.6)',
padding: 14,
borderRadius: 32,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3,
},
prefixContainer: {
removeLarge: {
position: 'absolute',
top: 12,
right: 12,
padding: 8,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3,
},
mediaTypeBadge: {
position: 'absolute',
bottom: 12,
left: 12,
flexDirection: 'row',
alignItems: 'center',
marginRight: 12,
gap: 6,
paddingVertical: 6,
paddingHorizontal: 12,
borderRadius: 10,
},
prefix: {
fontSize: 16,
mediaTypeBadgeText: {
color: '#ffffff',
fontSize: 13,
fontWeight: '600',
letterSpacing: 0.3,
},
prefixFocused: {
color: '#fff',
},
divider: {
width: 1.5,
height: 24,
marginLeft: 12,
},
});