fitst commit
This commit is contained in:
6
app/(auth)/_layout.tsx
Normal file
6
app/(auth)/_layout.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
// app/(auth)/_layout.tsx
|
||||
import { Slot } from 'expo-router';
|
||||
|
||||
export default function AuthLayout() {
|
||||
return <Slot />;
|
||||
}
|
||||
18
app/(auth)/confirm.tsx
Normal file
18
app/(auth)/confirm.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import ConfirmScreen from '@/screens/auth/confirm/ConfirmScreen';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function Confirm() {
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: '#0f172a',
|
||||
}}
|
||||
>
|
||||
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
|
||||
<ConfirmScreen />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
39
app/(auth)/index.tsx
Normal file
39
app/(auth)/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useAuth } from '@/components/AuthProvider';
|
||||
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();
|
||||
|
||||
// Loading spinner
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Token bo‘lsa dashboard-ga yo‘naltir
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
router.replace('/(dashboard)');
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Token yo‘q → login screen
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#0f172a' }}>
|
||||
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
|
||||
<LoginScreen />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
18
app/(auth)/register-confirm.tsx
Normal file
18
app/(auth)/register-confirm.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import RegisterConfirmScreen from '@/screens/auth/register-confirm/ConfirmScreen';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function RegisterConfirm() {
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: '#0f172a',
|
||||
}}
|
||||
>
|
||||
<ScrollView contentContainerStyle={{ flexGrow: 1 }} keyboardShouldPersistTaps="handled">
|
||||
<RegisterConfirmScreen />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
22
app/(auth)/register.tsx
Normal file
22
app/(auth)/register.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
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';
|
||||
|
||||
export default function Index() {
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<RegisterScreen />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: { flex: 1, backgroundColor: '#0f172a' },
|
||||
});
|
||||
263
app/(auth)/select-category.tsx
Normal file
263
app/(auth)/select-category.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import AuthHeader from '@/components/ui/AuthHeader';
|
||||
import { auth_api } from '@/screens/auth/login/lib/api';
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { ChevronLeft } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
is_leaf: boolean;
|
||||
}
|
||||
|
||||
export default function CategorySelectScreen() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { phone, stir, person_type } = useLocalSearchParams<{
|
||||
phone: string;
|
||||
stir: string;
|
||||
person_type: 'band' | 'ytt';
|
||||
}>();
|
||||
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [history, setHistory] = useState<Category[][]>([]);
|
||||
|
||||
/** ROOT categories */
|
||||
const { isLoading } = useQuery({
|
||||
queryKey: ['categories-root'],
|
||||
queryFn: () => products_api.getCategorys(),
|
||||
select: (res) => {
|
||||
setCategories(res.data.data);
|
||||
},
|
||||
});
|
||||
|
||||
/** CHILD categories */
|
||||
const childMutation = useMutation({
|
||||
mutationFn: (id: number) => products_api.getCategorys({ parent: id }),
|
||||
onSuccess: (res) => {
|
||||
setHistory((prev) => [...prev, categories]);
|
||||
setCategories(res.data.data);
|
||||
},
|
||||
});
|
||||
|
||||
/** REGISTER */
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: (body: {
|
||||
phone: string;
|
||||
stir: string;
|
||||
person_type: string;
|
||||
activate_types: number[];
|
||||
}) => auth_api.register(body),
|
||||
onSuccess: async () => {
|
||||
router.replace('/(auth)/register-confirm');
|
||||
await AsyncStorage.setItem('phone', phone);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = (err.response?.data as { data: { stir: string[] } }).data.stir[0];
|
||||
const errMessageDetail = (err.response?.data as { data: { detail: string } }).data.detail;
|
||||
|
||||
const errrAlert = errMessage ? errMessage : errMessageDetail;
|
||||
|
||||
Alert.alert(t('Xatolik yuz berdi'), errMessage || errrAlert || t('erroXatolik yuz berdi'));
|
||||
},
|
||||
});
|
||||
|
||||
const onCategoryPress = (cat: Category) => {
|
||||
if (cat.is_leaf) {
|
||||
setSelected(cat.id);
|
||||
} else {
|
||||
childMutation.mutate(cat.id);
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (history.length === 0) return;
|
||||
const prev = history[history.length - 1];
|
||||
setCategories(prev);
|
||||
setHistory((h) => h.slice(0, -1));
|
||||
setSelected(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<AuthHeader />
|
||||
<Stack.Screen options={{ title: t('Yo‘nalishni tanlang') }} />
|
||||
|
||||
<LinearGradient colors={['#0f172a', '#1e293b']} style={StyleSheet.absoluteFill} />
|
||||
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
{history.length > 0 && (
|
||||
<TouchableOpacity onPress={goBack} style={styles.backBtn}>
|
||||
<ChevronLeft size={20} color="#3b82f6" />
|
||||
<Text style={styles.backText}>{t('Orqaga')}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text style={styles.title}>{t("Yo'nalishni tanlang")}</Text>
|
||||
|
||||
{isLoading || childMutation.isPending ? (
|
||||
<ActivityIndicator color="#3b82f6" />
|
||||
) : (
|
||||
categories.map((c) => {
|
||||
const active = selected === c.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={c.id}
|
||||
style={[styles.item, active && styles.itemActive]}
|
||||
onPress={() => onCategoryPress(c)}
|
||||
>
|
||||
<Text style={[styles.text, active && styles.textActive]}>{c.name}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity
|
||||
disabled={!selected || registerMutation.isPending}
|
||||
style={[styles.bottom, (!selected || registerMutation.isPending) && styles.bottomDisabled]}
|
||||
onPress={() => {
|
||||
if (!selected) return;
|
||||
registerMutation.mutate({
|
||||
activate_types: [selected],
|
||||
person_type,
|
||||
phone: `998${phone}`,
|
||||
stir,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text style={styles.bottomText} disabled={registerMutation.isPending}>
|
||||
{t('Tadiqlash')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#ffffff',
|
||||
marginBottom: 12,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
|
||||
container: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 70,
|
||||
gap: 12,
|
||||
},
|
||||
|
||||
backBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
marginBottom: 12,
|
||||
},
|
||||
|
||||
backText: {
|
||||
fontSize: 14,
|
||||
color: '#3b82f6',
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
item: {
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 18,
|
||||
borderRadius: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.12)',
|
||||
},
|
||||
|
||||
itemActive: {
|
||||
backgroundColor: 'rgba(59,130,246,0.15)',
|
||||
borderColor: 'rgba(59,130,246,0.6)',
|
||||
},
|
||||
|
||||
text: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#cbd5f5',
|
||||
},
|
||||
|
||||
textActive: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '800',
|
||||
},
|
||||
|
||||
arrow: {
|
||||
fontSize: 18,
|
||||
color: '#94a3b8',
|
||||
},
|
||||
|
||||
bottom: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 16,
|
||||
right: 16,
|
||||
height: 54,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#3b82f6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 12,
|
||||
elevation: 10,
|
||||
},
|
||||
|
||||
bottomDisabled: {
|
||||
backgroundColor: '#64748b',
|
||||
},
|
||||
|
||||
bottomText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '800',
|
||||
fontSize: 16,
|
||||
},
|
||||
|
||||
decorCircle1: {
|
||||
position: 'absolute',
|
||||
top: -120,
|
||||
right: -80,
|
||||
width: 300,
|
||||
height: 300,
|
||||
borderRadius: 150,
|
||||
backgroundColor: 'rgba(59,130,246,0.12)',
|
||||
},
|
||||
|
||||
decorCircle2: {
|
||||
position: 'absolute',
|
||||
bottom: -120,
|
||||
left: -100,
|
||||
width: 280,
|
||||
height: 280,
|
||||
borderRadius: 140,
|
||||
backgroundColor: 'rgba(16,185,129,0.1)',
|
||||
},
|
||||
});
|
||||
143
app/(dashboard)/_layout.tsx
Normal file
143
app/(dashboard)/_layout.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { RefreshProvider } from '@/components/ui/RefreshContext';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
import { Tabs } from 'expo-router';
|
||||
import { Home, Megaphone, PlusCircle, User } from 'lucide-react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text } from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
export default function TabsLayout() {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<RefreshProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarStyle: {
|
||||
position: 'absolute',
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 0,
|
||||
height: 70,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
backgroundColor: isDark ? '#0f172a' : '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: isDark ? '#334155' : '#e2e8f0',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: isDark ? 0.5 : 0.1,
|
||||
shadowOffset: { width: 0, height: -3 },
|
||||
shadowRadius: isDark ? 12 : 10,
|
||||
elevation: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
tabBarActiveTintColor: '#3b82f6',
|
||||
tabBarInactiveTintColor: isDark ? '#64748b' : '#94a3b8',
|
||||
tabBarLabelStyle: { fontSize: 12, fontWeight: '600' },
|
||||
tabBarItemStyle: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
alignContent: 'center',
|
||||
},
|
||||
tabBarLabelPosition: 'below-icon',
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
tabBarLabel: ({ color }) => (
|
||||
<Text
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
flexWrap: 'wrap',
|
||||
width: 80, // yetarli joy berish
|
||||
}}
|
||||
numberOfLines={2} // 2 qatorga sig‘adi
|
||||
>
|
||||
{t('Bosh sahifa')}
|
||||
</Text>
|
||||
),
|
||||
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tabs.Screen
|
||||
name="create-announcements"
|
||||
options={{
|
||||
tabBarLabel: ({ focused, color }) => (
|
||||
<Text
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
flexWrap: 'wrap',
|
||||
width: 80, // yetarli joy berish
|
||||
}}
|
||||
numberOfLines={2} // 2 qatorga sig‘adi
|
||||
>
|
||||
{t("E'lon joylashtirish")}
|
||||
</Text>
|
||||
),
|
||||
tabBarIcon: ({ color, size }) => <PlusCircle color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tabs.Screen
|
||||
name="announcements"
|
||||
options={{
|
||||
tabBarLabel: ({ focused, color }) => (
|
||||
<Text
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
flexWrap: 'wrap',
|
||||
width: 80, // yetarli joy berish
|
||||
}}
|
||||
numberOfLines={2} // 2 qatorga sig‘adi
|
||||
>
|
||||
{t("E'lonlar")}
|
||||
</Text>
|
||||
),
|
||||
tabBarIcon: ({ color, size }) => <Megaphone color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
tabBarLabel: ({ focused, color }) => (
|
||||
<Text
|
||||
style={{
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
flexWrap: 'wrap',
|
||||
width: 80, // yetarli joy berish
|
||||
}}
|
||||
numberOfLines={2} // 2 qatorga sig‘adi
|
||||
>
|
||||
{t('Profil')}
|
||||
</Text>
|
||||
),
|
||||
tabBarIcon: ({ color, size }) => <User color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</BottomSheetModalProvider>
|
||||
</RefreshProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
21
app/(dashboard)/announcements.tsx
Normal file
21
app/(dashboard)/announcements.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { FilterProvider } from '@/components/ui/FilterContext';
|
||||
import { CustomHeader } from '@/components/ui/Header';
|
||||
import DashboardScreen from '@/screens/announcements/ui/AnnouncementsList';
|
||||
import { Stack } from 'expo-router';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function Announcements() {
|
||||
const { isDark } = useTheme();
|
||||
return (
|
||||
<FilterProvider>
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 50 }}
|
||||
>
|
||||
<CustomHeader />
|
||||
<Stack.Screen options={{ title: "E'lonlar" }} />
|
||||
<DashboardScreen />
|
||||
</SafeAreaView>
|
||||
</FilterProvider>
|
||||
);
|
||||
}
|
||||
17
app/(dashboard)/create-announcements.tsx
Normal file
17
app/(dashboard)/create-announcements.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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 (
|
||||
<FilterProvider>
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff' }}>
|
||||
<CustomHeader />
|
||||
<CreateAdsScreens />
|
||||
</SafeAreaView>
|
||||
</FilterProvider>
|
||||
);
|
||||
}
|
||||
33
app/(dashboard)/index.tsx
Normal file
33
app/(dashboard)/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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(() => {
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
router.replace('/(auth)');
|
||||
}
|
||||
}, [isAuthenticated, isLoading]);
|
||||
|
||||
if (isLoading || !isAuthenticated) {
|
||||
return null; // Loading vaqtida yoki auth yo‘q bo‘lsa hech narsa ko‘rmasin
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterProvider>
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff' }}>
|
||||
<CustomHeader />
|
||||
<HomeScreen />
|
||||
</SafeAreaView>
|
||||
</FilterProvider>
|
||||
);
|
||||
}
|
||||
17
app/(dashboard)/profile.tsx
Normal file
17
app/(dashboard)/profile.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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 (
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#ffffff', paddingBottom: 30 }}
|
||||
edges={['top']}
|
||||
>
|
||||
<CustomHeader logoutbtn={true} />
|
||||
<Profile />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
|
||||
import { HapticTab } from '@/components/haptic-tab';
|
||||
import { IconSymbol } from '@/components/ui/icon-symbol';
|
||||
import { Colors } from '@/constants/theme';
|
||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
||||
headerShown: false,
|
||||
tabBarButton: HapticTab,
|
||||
}}>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Home',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="explore"
|
||||
options={{
|
||||
title: 'Explore',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Image } from 'expo-image';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
|
||||
import { Collapsible } from '@/components/ui/collapsible';
|
||||
import { ExternalLink } from '@/components/external-link';
|
||||
import ParallaxScrollView from '@/components/parallax-scroll-view';
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { IconSymbol } from '@/components/ui/icon-symbol';
|
||||
import { Fonts } from '@/constants/theme';
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
|
||||
headerImage={
|
||||
<IconSymbol
|
||||
size={310}
|
||||
color="#808080"
|
||||
name="chevron.left.forwardslash.chevron.right"
|
||||
style={styles.headerImage}
|
||||
/>
|
||||
}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText
|
||||
type="title"
|
||||
style={{
|
||||
fontFamily: Fonts.rounded,
|
||||
}}>
|
||||
Explore
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedText>This app includes example code to help you get started.</ThemedText>
|
||||
<Collapsible title="File-based routing">
|
||||
<ThemedText>
|
||||
This app has two screens:{' '}
|
||||
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
|
||||
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
|
||||
</ThemedText>
|
||||
<ThemedText>
|
||||
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
|
||||
sets up the tab navigator.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Android, iOS, and web support">
|
||||
<ThemedText>
|
||||
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
|
||||
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
|
||||
</ThemedText>
|
||||
</Collapsible>
|
||||
<Collapsible title="Images">
|
||||
<ThemedText>
|
||||
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
|
||||
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
||||
different screen densities
|
||||
</ThemedText>
|
||||
<Image
|
||||
source={require('@/assets/images/react-logo.png')}
|
||||
style={{ width: 100, height: 100, alignSelf: 'center' }}
|
||||
/>
|
||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Light and dark mode components">
|
||||
<ThemedText>
|
||||
This template has light and dark mode support. The{' '}
|
||||
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
|
||||
what the user's current color scheme is, and so you can adjust UI colors accordingly.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Animations">
|
||||
<ThemedText>
|
||||
This template includes an example of an animated component. The{' '}
|
||||
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
|
||||
the powerful{' '}
|
||||
<ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
|
||||
react-native-reanimated
|
||||
</ThemedText>{' '}
|
||||
library to create a waving hand animation.
|
||||
</ThemedText>
|
||||
{Platform.select({
|
||||
ios: (
|
||||
<ThemedText>
|
||||
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
|
||||
component provides a parallax effect for the header image.
|
||||
</ThemedText>
|
||||
),
|
||||
})}
|
||||
</Collapsible>
|
||||
</ParallaxScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerImage: {
|
||||
color: '#808080',
|
||||
bottom: -90,
|
||||
left: -35,
|
||||
position: 'absolute',
|
||||
},
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Image } from 'expo-image';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
|
||||
import { HelloWave } from '@/components/hello-wave';
|
||||
import ParallaxScrollView from '@/components/parallax-scroll-view';
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { Link } from 'expo-router';
|
||||
|
||||
export default function HomeScreen() {
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
|
||||
headerImage={
|
||||
<Image
|
||||
source={require('@/assets/images/partial-react-logo.png')}
|
||||
style={styles.reactLogo}
|
||||
/>
|
||||
}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="title">Welcome!</ThemedText>
|
||||
<HelloWave />
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
|
||||
<ThemedText>
|
||||
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
|
||||
Press{' '}
|
||||
<ThemedText type="defaultSemiBold">
|
||||
{Platform.select({
|
||||
ios: 'cmd + d',
|
||||
android: 'cmd + m',
|
||||
web: 'F12',
|
||||
})}
|
||||
</ThemedText>{' '}
|
||||
to open developer tools.
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<Link href="/modal">
|
||||
<Link.Trigger>
|
||||
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
|
||||
</Link.Trigger>
|
||||
<Link.Preview />
|
||||
<Link.Menu>
|
||||
<Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} />
|
||||
<Link.MenuAction
|
||||
title="Share"
|
||||
icon="square.and.arrow.up"
|
||||
onPress={() => alert('Share pressed')}
|
||||
/>
|
||||
<Link.Menu title="More" icon="ellipsis">
|
||||
<Link.MenuAction
|
||||
title="Delete"
|
||||
icon="trash"
|
||||
destructive
|
||||
onPress={() => alert('Delete pressed')}
|
||||
/>
|
||||
</Link.Menu>
|
||||
</Link.Menu>
|
||||
</Link>
|
||||
|
||||
<ThemedText>
|
||||
{`Tap the Explore tab to learn more about what's included in this starter app.`}
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
|
||||
<ThemedText>
|
||||
{`When you're ready, run `}
|
||||
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
|
||||
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
|
||||
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
|
||||
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
</ParallaxScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
stepContainer: {
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
reactLogo: {
|
||||
height: 178,
|
||||
width: 290,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
},
|
||||
});
|
||||
@@ -1,24 +1,34 @@
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { AuthProvider } from '@/components/AuthProvider';
|
||||
import QueryProvider from '@/components/QueryProvider';
|
||||
import { ThemeProvider } from '@/components/ThemeContext';
|
||||
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 'react-native-reanimated';
|
||||
|
||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||
|
||||
export const unstable_settings = {
|
||||
anchor: '(tabs)',
|
||||
};
|
||||
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
function AppContent() {
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
<>
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
<StatusBar style={'light'} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<QueryProvider>
|
||||
<ThemeProvider>
|
||||
<ProfileDataProvider>
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</ProfileDataProvider>
|
||||
</ThemeProvider>
|
||||
</QueryProvider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
21
app/index.tsx
Normal file
21
app/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useAuth } from '@/components/AuthProvider';
|
||||
import { Redirect } from 'expo-router';
|
||||
import { ActivityIndicator, View } from 'react-native';
|
||||
|
||||
export default function Index() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Redirect href="/(dashboard)" />;
|
||||
}
|
||||
|
||||
return <Redirect href="/(auth)" />;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Link } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
|
||||
export default function ModalScreen() {
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">This is a modal</ThemedText>
|
||||
<Link href="/" dismissTo style={styles.link}>
|
||||
<ThemedText type="link">Go to home screen</ThemedText>
|
||||
</Link>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
});
|
||||
16
app/profile/_layout.tsx
Normal file
16
app/profile/_layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { RefreshProvider } from '@/components/ui/RefreshContext';
|
||||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||||
import { Stack } from 'expo-router';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
export default function TabsLayout() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<RefreshProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
</BottomSheetModalProvider>
|
||||
</RefreshProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
5
app/profile/bonuses.tsx
Normal file
5
app/profile/bonuses.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import BonusesScreen from '@/screens/profile/ui/BonusesScreen';
|
||||
|
||||
export default function Bonuses() {
|
||||
return <BonusesScreen />;
|
||||
}
|
||||
227
app/profile/categories.tsx
Normal file
227
app/profile/categories.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import CategorySelection from '@/components/ui/IndustrySelection';
|
||||
import { user_api } from '@/screens/profile/lib/api';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { ArrowLeft, XIcon } from 'lucide-react-native';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
FlatList,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
ToastAndroid,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function PersonalInfoScreen() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = {
|
||||
background: isDark ? '#0f172a' : '#f8fafc',
|
||||
text: isDark ? '#ffffff' : '#0f172a',
|
||||
textSecondary: isDark ? '#64748b' : '#94a3b8',
|
||||
primary: '#3b82f6',
|
||||
tabBg: isDark ? '#1e293b' : '#e0e7ff',
|
||||
tabText: isDark ? '#ffffff' : '#4338ca',
|
||||
deleteBg: isDark ? '#394e73' : '#cbd5e1',
|
||||
deleteIcon: isDark ? '#f8fafc' : '#475569',
|
||||
shadow: isDark ? '#000' : '#64748b',
|
||||
};
|
||||
|
||||
const [selectedCategories, setSelectedCategories] = useState<any[]>([]);
|
||||
|
||||
const { data: me, isLoading } = useQuery({
|
||||
queryKey: ['get_me'],
|
||||
queryFn: () => user_api.getMe(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (me?.data.data?.activate_types) {
|
||||
setSelectedCategories(me.data.data.activate_types.map((a: any) => a));
|
||||
}
|
||||
}, [me]);
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (body: {
|
||||
first_name: string;
|
||||
industries: {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
external_id: null | number;
|
||||
level: number;
|
||||
is_leaf: boolean;
|
||||
icon_name: null | string;
|
||||
parent: {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
};
|
||||
}[];
|
||||
phone: string;
|
||||
person_type: 'employee' | 'legal_entity' | 'ytt' | 'band';
|
||||
activate_types: number[];
|
||||
}) => user_api.updateMe(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['get_me'] });
|
||||
router.push('/profile/personal-info');
|
||||
ToastAndroid.show(t("Ma'lumotlar yangilandi"), ToastAndroid.TOP);
|
||||
},
|
||||
onError: () => {
|
||||
Alert.alert(t('Xatolik yzu berdi'), t("Ma'lumotlarni yangilashda xatolik yuz berdi"));
|
||||
},
|
||||
});
|
||||
|
||||
const removeCategory = (id: string | number) => {
|
||||
setSelectedCategories((prev) => prev.filter((c) => c.id !== id));
|
||||
};
|
||||
|
||||
const renderTab = ({ item }: { item: any }) => (
|
||||
<View style={[styles.tabWrapper, { backgroundColor: theme.tabBg, shadowColor: theme.shadow }]}>
|
||||
<Text
|
||||
style={[styles.tabText, { color: theme.tabText }]}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => removeCategory(item.id)}
|
||||
style={[styles.deleteTab, { backgroundColor: theme.deleteBg }]}
|
||||
>
|
||||
<XIcon size={15} color={theme.deleteIcon} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<View style={styles.topHeader}>
|
||||
<Pressable onPress={() => router.push('/profile/personal-info')}>
|
||||
<ArrowLeft color={theme.text} />
|
||||
</Pressable>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t('Faoliyat sohalari')}</Text>
|
||||
<Pressable>
|
||||
<Text style={{ color: theme.primary, fontSize: 16 }}>{t('Tayyor')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<ActivityIndicator size={'large'} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<View style={styles.topHeader}>
|
||||
<Pressable onPress={() => router.push('/profile/personal-info')}>
|
||||
<ArrowLeft color={theme.text} />
|
||||
</Pressable>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t('Faoliyat sohalari')}</Text>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (me) {
|
||||
const activate_types = selectedCategories.map((e) => e.id) ?? [];
|
||||
updateMutation.mutate({
|
||||
person_type: me?.data.data.person_type,
|
||||
first_name: me?.data.data.first_name,
|
||||
phone: me.data.data.phone,
|
||||
industries: selectedCategories,
|
||||
activate_types,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{updateMutation.isPending ? (
|
||||
<ActivityIndicator size={'small'} />
|
||||
) : (
|
||||
<Text style={{ color: theme.primary, fontSize: 16 }}>{t('Tayyor')}</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
{selectedCategories.length > 0 && (
|
||||
<FlatList
|
||||
data={selectedCategories}
|
||||
keyExtractor={(item) => String(item.id)}
|
||||
renderItem={renderTab}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
contentContainerStyle={styles.tabsContainer}
|
||||
style={styles.tabsList}
|
||||
ItemSeparatorComponent={() => <View style={{ width: 8 }} />}
|
||||
/>
|
||||
)}
|
||||
<CategorySelection
|
||||
selectedCategories={selectedCategories}
|
||||
setSelectedCategories={setSelectedCategories}
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
tabsList: {
|
||||
maxHeight: 56,
|
||||
},
|
||||
tabsContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
tabWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 20,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 3,
|
||||
elevation: 3,
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginRight: 6,
|
||||
maxWidth: 200,
|
||||
flexShrink: 1,
|
||||
},
|
||||
deleteTab: {
|
||||
padding: 4,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
marginTop: 40,
|
||||
},
|
||||
topHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||
});
|
||||
5
app/profile/employees.tsx
Normal file
5
app/profile/employees.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EmployeesTab } from '@/screens/profile/ui/EmployeesTab';
|
||||
|
||||
export default function EmployeesScreen() {
|
||||
return <EmployeesTab />;
|
||||
}
|
||||
5
app/profile/employees/[id].tsx
Normal file
5
app/profile/employees/[id].tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EmployeesTab } from '@/screens/profile/ui/EmployeesTab';
|
||||
|
||||
export default function EmployeeDetailScreen() {
|
||||
return <EmployeesTab />;
|
||||
}
|
||||
9
app/profile/employees/add.tsx
Normal file
9
app/profile/employees/add.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import AddEmployee from '@/screens/profile/ui/AddEmployee';
|
||||
|
||||
export default function AddEmployeeScreen() {
|
||||
return (
|
||||
<>
|
||||
<AddEmployee />
|
||||
</>
|
||||
);
|
||||
}
|
||||
12
app/profile/my-ads.tsx
Normal file
12
app/profile/my-ads.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { AnnouncementsTab } from '@/screens/profile/ui/AnnouncementsTab';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function MyAds() {
|
||||
const { isDark } = useTheme();
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: isDark ? '#0f172a' : '#f8fafc' }}>
|
||||
<AnnouncementsTab />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
346
app/profile/personal-info.tsx
Normal file
346
app/profile/personal-info.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { formatNumber, formatPhone, normalizeDigits } from '@/constants/formatPhone';
|
||||
import { user_api } from '@/screens/profile/lib/api';
|
||||
import { UserInfoResponseData } from '@/screens/profile/lib/type';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { ArrowLeft, Edit2, Plus } from 'lucide-react-native';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Image,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
ToastAndroid,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function PersonalInfoScreen() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const theme = {
|
||||
background: isDark ? '#0f172a' : '#f8fafc',
|
||||
cardBg: isDark ? '#1e293b' : '#ffffff',
|
||||
text: isDark ? '#f8fafc' : '#0f172a',
|
||||
textSecondary: isDark ? '#94a3b8' : '#64748b',
|
||||
textTertiary: isDark ? '#64748b' : '#94a3b8',
|
||||
inputBg: isDark ? '#1e293b' : '#f1f5f9',
|
||||
inputBorder: isDark ? '#334155' : '#e2e8f0',
|
||||
primary: '#3b82f6',
|
||||
chipBg: isDark ? '#1e293b' : '#e0e7ff',
|
||||
chipText: isDark ? '#f8fafc' : '#4338ca',
|
||||
divider: isDark ? '#334155' : '#cbd5e1',
|
||||
placeholder: isDark ? '#64748b' : '#94a3b8',
|
||||
};
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editData, setEditData] = useState<UserInfoResponseData | null>(null);
|
||||
const [phone, setPhone] = useState('');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [showCategories, setShowCategories] = useState(false);
|
||||
const [selectedCategories, setSelectedCategories] = useState<any[]>([]);
|
||||
|
||||
const { data: me, isLoading } = useQuery({
|
||||
queryKey: ['get_me'],
|
||||
queryFn: () => user_api.getMe(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (me?.data.data) {
|
||||
setEditData(me.data);
|
||||
|
||||
const rawPhone = normalizeDigits(me.data.data.phone || '');
|
||||
setPhone(rawPhone.startsWith('998') ? rawPhone.slice(3) : rawPhone);
|
||||
|
||||
setSelectedCategories(me.data.data.activate_types ?? []);
|
||||
}
|
||||
}, [me]);
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (body: any) => user_api.updateMe(body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['get_me'] });
|
||||
setIsEditing(false);
|
||||
setShowCategories(false);
|
||||
ToastAndroid.show(t("Ma'lumotlar yangilandi"), ToastAndroid.TOP);
|
||||
},
|
||||
onError: () => {
|
||||
Alert.alert(t('Xatolik yuz berdi'), t('Yangilashda xatolik yuz berdi'));
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
if (!editData) return;
|
||||
|
||||
updateMutation.mutate({
|
||||
first_name: editData.data.first_name,
|
||||
phone: normalizeDigits(phone),
|
||||
person_type: editData.data.person_type,
|
||||
industries: editData.data.activate_types,
|
||||
activate_types: editData.data.activate_types.map((e) => e.id),
|
||||
|
||||
company_name: editData.data.company_name,
|
||||
stir: editData.data.stir,
|
||||
director_full_name: editData.data.director_full_name,
|
||||
address: editData.data.address,
|
||||
});
|
||||
};
|
||||
|
||||
const removeCategory = (id: number) => {
|
||||
setSelectedCategories((prev) => prev.filter((c) => c.id !== id));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<View style={styles.topHeader}>
|
||||
<Pressable onPress={() => setIsEditing(false)}>
|
||||
<ArrowLeft color={theme.text} />
|
||||
</Pressable>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t('Tahrirlash')}</Text>
|
||||
<Pressable onPress={handleSave}>
|
||||
<Text style={[styles.saveButton, { color: theme.primary }]}>{t('Saqlash')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<ActivityIndicator size={'large'} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
/* ===================== EDIT MODE ===================== */
|
||||
if (isEditing && editData) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<View style={styles.topHeader}>
|
||||
<Pressable onPress={() => setIsEditing(false)}>
|
||||
<ArrowLeft color={theme.text} />
|
||||
</Pressable>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t('Tahrirlash')}</Text>
|
||||
<Pressable onPress={handleSave}>
|
||||
{updateMutation.isPending ? (
|
||||
<ActivityIndicator size={'small'} />
|
||||
) : (
|
||||
<Text style={[styles.saveButton, { color: theme.primary }]}>{t('Saqlash')}</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content}>
|
||||
<View style={styles.editSection}>
|
||||
<Text style={[styles.label, { color: theme.textSecondary }]}>{t('Ism')}</Text>
|
||||
<TextInput
|
||||
style={[styles.input, { backgroundColor: theme.inputBg, color: theme.text }]}
|
||||
value={editData?.data.first_name}
|
||||
onChangeText={(text) => setEditData((prev) => prev && { ...prev, first_name: text })}
|
||||
placeholderTextColor={theme.placeholder}
|
||||
/>
|
||||
<Text style={[styles.label, { color: theme.textSecondary }]}>
|
||||
{t('Telefon raqami')}
|
||||
</Text>
|
||||
<View style={[styles.phoneInputContainer, { backgroundColor: theme.inputBg }]}>
|
||||
<View style={styles.prefixContainer}>
|
||||
<Text
|
||||
style={[styles.prefix, { color: theme.text }, focused && styles.prefixFocused]}
|
||||
>
|
||||
+998
|
||||
</Text>
|
||||
<View style={[styles.divider, { backgroundColor: theme.divider }]} />
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.phoneInput, { color: theme.text }]}
|
||||
value={formatPhone(phone)}
|
||||
onChangeText={(text) => setPhone(normalizeDigits(text))}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
keyboardType="phone-pad"
|
||||
placeholder="90 123 45 67"
|
||||
maxLength={12}
|
||||
placeholderTextColor={theme.placeholder}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
/* ===================== VIEW MODE ===================== */
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<View style={styles.topHeader}>
|
||||
<Pressable onPress={() => router.push('/profile')}>
|
||||
<ArrowLeft color={theme.text} />
|
||||
</Pressable>
|
||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{t("Shaxsiy ma'lumotlar")}</Text>
|
||||
<Pressable onPress={() => setIsEditing(true)}>
|
||||
<Edit2 color={theme.primary} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content}>
|
||||
<View style={[styles.infoCard, { backgroundColor: theme.cardBg }]}>
|
||||
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>{t('Ism')}</Text>
|
||||
<Text style={[styles.infoValue, { color: theme.text }]}>{me?.data.data.first_name}</Text>
|
||||
|
||||
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>
|
||||
{t('Telefon raqami')}
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: theme.text }]}>
|
||||
{me && formatNumber(me?.data.data.phone)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{me?.data.data.person_type !== 'employee' && (
|
||||
<View style={[styles.infoCard, { backgroundColor: theme.cardBg }]}>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textTertiary }]}>
|
||||
{t('Kompaniya')}
|
||||
</Text>
|
||||
|
||||
{me?.data.data.company_image && (
|
||||
<Image source={{ uri: me.data.data.company_image }} style={styles.companyImage} />
|
||||
)}
|
||||
|
||||
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>{t('Nomi')}</Text>
|
||||
<Text style={[styles.infoValue, { color: theme.text }]}>
|
||||
{me?.data.data.company_name}
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>{t('STIR')}</Text>
|
||||
<Text style={[styles.infoValue, { color: theme.text }]}>{me?.data.data.stir}</Text>
|
||||
|
||||
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>{t('Direktor')}</Text>
|
||||
<Text style={[styles.infoValue, { color: theme.text }]}>
|
||||
{me?.data.data.director_full_name}
|
||||
</Text>
|
||||
{me?.data.data.address && (
|
||||
<>
|
||||
<Text style={[styles.infoLabel, { color: theme.textSecondary }]}>
|
||||
{t('Manzil')}
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: theme.text }]}>
|
||||
{me?.data.data.address}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.section}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.sectionTitle, { color: theme.textTertiary }]}>
|
||||
{t('Faoliyat sohalari')}
|
||||
</Text>
|
||||
<Plus
|
||||
color={theme.primary}
|
||||
size={24}
|
||||
onPress={() => router.push('/profile/categories')}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.fieldsContainer}>
|
||||
{me?.data.data.activate_types.map((field: any) => (
|
||||
<View key={field.id} style={[styles.fieldChip, { backgroundColor: theme.chipBg }]}>
|
||||
<Text style={[styles.fieldText, { color: theme.chipText }]}>{field.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
fieldsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||
fieldChip: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 16,
|
||||
},
|
||||
fieldText: { fontSize: 14, fontWeight: '600' },
|
||||
section: {
|
||||
padding: 10,
|
||||
marginTop: 0,
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
loadingText: {
|
||||
textAlign: 'center',
|
||||
marginTop: 40,
|
||||
},
|
||||
infoCard: {
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
infoLabel: {
|
||||
fontSize: 13,
|
||||
marginTop: 8,
|
||||
},
|
||||
infoValue: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontWeight: '700',
|
||||
fontSize: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
editSection: {
|
||||
gap: 12,
|
||||
},
|
||||
label: {
|
||||
fontSize: 13,
|
||||
},
|
||||
input: {
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
},
|
||||
phoneInputContainer: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
},
|
||||
phoneInput: {
|
||||
flex: 1,
|
||||
},
|
||||
saveButton: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
companyImage: {
|
||||
width: 90,
|
||||
height: 90,
|
||||
borderRadius: 45,
|
||||
alignSelf: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
topHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerTitle: { fontSize: 18, fontWeight: '700' },
|
||||
prefixContainer: { flexDirection: 'row', alignItems: 'center', marginRight: 12 },
|
||||
prefix: { fontSize: 17, fontWeight: '600' },
|
||||
prefixFocused: {},
|
||||
divider: { width: 1, height: 24, marginLeft: 12 },
|
||||
});
|
||||
5
app/profile/products.tsx
Normal file
5
app/profile/products.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import MyServicesScreen from '@/screens/profile/ui/MyServices';
|
||||
|
||||
export default function ProductsScreen() {
|
||||
return <MyServicesScreen />;
|
||||
}
|
||||
5
app/profile/products/add.tsx
Normal file
5
app/profile/products/add.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import AddService from '@/screens/profile/ui/AddService';
|
||||
|
||||
export default function AddProductScreen() {
|
||||
return <AddService />;
|
||||
}
|
||||
5
app/profile/products/edit/[id].tsx
Normal file
5
app/profile/products/edit/[id].tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import EditServices from '@/screens/profile/ui/EditServices';
|
||||
|
||||
export default function EditProductScreen() {
|
||||
return <EditServices />;
|
||||
}
|
||||
166
app/profile/settings.tsx
Normal file
166
app/profile/settings.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import GB from '@/assets/images/GB.png';
|
||||
import RU from '@/assets/images/RU.png';
|
||||
import UZ from '@/assets/images/UZ.png';
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { saveLang } from '@/hooks/storage.native';
|
||||
import { useLanguage } from '@/i18n/useLanguage';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Image } from 'expo-image';
|
||||
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();
|
||||
const { language, changeLanguage } = useLanguage();
|
||||
const { t, i18n } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const selectLanguage = async (lang: string) => {
|
||||
changeLanguage(lang as 'uz' | 'ru' | 'en');
|
||||
await i18n.changeLanguage(lang);
|
||||
queryClient.invalidateQueries();
|
||||
await saveLang(lang);
|
||||
};
|
||||
const { isDark, toggleTheme } = useTheme();
|
||||
|
||||
const languages = [
|
||||
{ code: 'uz', label: "O'zbek", icon: UZ },
|
||||
{ code: 'ru', label: 'Русский', icon: RU },
|
||||
{ code: 'en', label: 'English', icon: GB },
|
||||
];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: 32 }}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Pressable onPress={() => router.back()}>
|
||||
<ChevronLeft size={26} color={isDark ? '#f8fafc' : '#0f172a'} />
|
||||
</Pressable>
|
||||
|
||||
<Text style={[styles.headerTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Sozlamalar')}
|
||||
</Text>
|
||||
|
||||
<View style={{ width: 26 }} />
|
||||
</View>
|
||||
|
||||
{/* Language Cards */}
|
||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Tilni tanlang')}
|
||||
</Text>
|
||||
{languages.map((lang) => {
|
||||
const active = language === lang.code;
|
||||
return (
|
||||
<Pressable
|
||||
key={lang.code}
|
||||
onPress={() => selectLanguage(lang.code)}
|
||||
style={[
|
||||
styles.langCard,
|
||||
isDark ? styles.darkCard : styles.lightCard,
|
||||
active && styles.langActiveCard,
|
||||
]}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<Image source={lang.icon} style={{ width: 32, height: 24 }} />
|
||||
<Text
|
||||
style={[
|
||||
styles.label,
|
||||
isDark ? styles.darkText : styles.lightText,
|
||||
active && { color: '#fff' },
|
||||
]}
|
||||
>
|
||||
{lang.label}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Theme */}
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Rejimni tanlang')}
|
||||
</Text>
|
||||
<View style={[styles.card, isDark ? styles.darkCard : styles.lightCard]}>
|
||||
<View style={styles.row}>
|
||||
{isDark ? <Moon size={22} color="#818cf8" /> : <Sun size={22} color="#f59e0b" />}
|
||||
<Text style={[styles.label, isDark ? styles.darkText : styles.lightText]}>
|
||||
{isDark ? t('Tungi rejim') : t("Yorug' rejim")}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Switch
|
||||
value={isDark}
|
||||
onValueChange={toggleTheme}
|
||||
trackColor={{ false: '#d1d5db', true: '#6366f1' }}
|
||||
thumbColor={isDark ? '#e5e7eb' : '#ffffff'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
/* Backgrounds */
|
||||
lightBg: { backgroundColor: '#f9fafb' },
|
||||
darkBg: { backgroundColor: '#0f172a' },
|
||||
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
},
|
||||
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
card: {
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 8,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
lightCard: { backgroundColor: '#ffffff' },
|
||||
darkCard: { backgroundColor: '#1e293b', borderWidth: 1, borderColor: '#334155' },
|
||||
|
||||
row: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
|
||||
label: { fontSize: 15, fontWeight: '500' },
|
||||
darkText: { color: '#f8fafc' },
|
||||
lightText: { color: '#0f172a' },
|
||||
|
||||
/* Language Cards */
|
||||
langCard: {
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
langActiveCard: { backgroundColor: '#3b82f6' },
|
||||
});
|
||||
Reference in New Issue
Block a user