fitst commit
This commit is contained in:
48
components/AuthProvider.tsx
Normal file
48
components/AuthProvider.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type AuthContextType = {
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
login: (token: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkToken = async () => {
|
||||
const token = await AsyncStorage.getItem('access_token');
|
||||
setIsAuthenticated(!!token);
|
||||
setIsLoading(false);
|
||||
};
|
||||
checkToken();
|
||||
}, []);
|
||||
|
||||
const login = async (token: string) => {
|
||||
await AsyncStorage.setItem('access_token', token);
|
||||
setIsAuthenticated(true);
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await AsyncStorage.removeItem('access_token');
|
||||
await AsyncStorage.removeItem('refresh_token');
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, isLoading, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) throw new Error('useAuth must be used within AuthProvider');
|
||||
return context;
|
||||
};
|
||||
25
components/QueryProvider.tsx
Normal file
25
components/QueryProvider.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactNode, useState } from 'react';
|
||||
|
||||
const QueryProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
refetchInterval: 1000 * 60 * 5,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
};
|
||||
|
||||
export default QueryProvider;
|
||||
59
components/ThemeContext.tsx
Normal file
59
components/ThemeContext.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { Appearance } from 'react-native';
|
||||
|
||||
type ThemeType = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextProps {
|
||||
theme: ThemeType;
|
||||
isDark: boolean;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextProps | null>(null);
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const systemTheme = Appearance.getColorScheme();
|
||||
|
||||
const [theme, setTheme] = useState<ThemeType>(systemTheme === 'dark' ? 'dark' : 'light');
|
||||
|
||||
// 🔹 Load saved theme
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const savedTheme = await AsyncStorage.getItem('APP_THEME');
|
||||
if (savedTheme === 'dark' || savedTheme === 'light') {
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// 🔹 Save theme
|
||||
useEffect(() => {
|
||||
AsyncStorage.setItem('APP_THEME', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
isDark: theme === 'dark',
|
||||
toggleTheme,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom hook
|
||||
export function useTheme() {
|
||||
const ctx = useContext(ThemeContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useTheme must be used inside ThemeProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
151
components/ui/AuthHeader.tsx
Normal file
151
components/ui/AuthHeader.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { languages } from '@/constants/languages';
|
||||
import { getLang, saveLang } from '@/hooks/storage.native';
|
||||
import { useLanguage } from '@/i18n/useLanguage';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { ArrowLeft, Check, ChevronDown, Globe } from 'lucide-react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
export default function AuthHeader({ back = true }: { back?: boolean }) {
|
||||
const router = useRouter();
|
||||
const { language, changeLanguage, getLanguageName } = useLanguage();
|
||||
const { i18n } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
const loadLanguage = async () => {
|
||||
const lang = await getLang();
|
||||
if (lang === 'uz' || lang === 'ru' || lang === 'en') {
|
||||
changeLanguage(lang);
|
||||
}
|
||||
};
|
||||
|
||||
loadLanguage();
|
||||
}, [language]);
|
||||
|
||||
const selectLanguage = async (lang: string) => {
|
||||
changeLanguage(lang as any);
|
||||
queryClient.invalidateQueries();
|
||||
await i18n.changeLanguage(lang);
|
||||
await saveLang(lang);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.header, justifyContent: back ? 'space-between' : 'flex-end' }}>
|
||||
{/* Back */}
|
||||
{back && (
|
||||
<TouchableOpacity style={styles.back} onPress={() => router.back()} activeOpacity={0.7}>
|
||||
<ArrowLeft size={22} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Language */}
|
||||
<View>
|
||||
<TouchableOpacity style={styles.langBtn} onPress={() => setOpen(!open)} activeOpacity={0.7}>
|
||||
<Globe size={18} color="#94a3b8" />
|
||||
<Text style={styles.langText}>{getLanguageName()}</Text>
|
||||
<ChevronDown size={16} color="#94a3b8" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{open && (
|
||||
<View style={styles.dropdown}>
|
||||
{languages.map((l) => {
|
||||
const active = language === l.code;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={l.code}
|
||||
style={[styles.option, active && styles.optionActive]}
|
||||
onPress={() => selectLanguage(l.code)}
|
||||
>
|
||||
<Text style={[styles.optionText, active && styles.optionTextActive]}>
|
||||
{l.name}
|
||||
</Text>
|
||||
{active && (
|
||||
<View style={styles.check}>
|
||||
<Check size={14} color="#fff" />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
zIndex: 50,
|
||||
},
|
||||
back: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
langBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
langText: {
|
||||
color: '#94a3b8',
|
||||
fontWeight: '600',
|
||||
fontSize: 14,
|
||||
},
|
||||
dropdown: {
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
right: 0,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
padding: 8,
|
||||
minWidth: 180,
|
||||
elevation: 20,
|
||||
},
|
||||
option: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
},
|
||||
optionActive: {
|
||||
backgroundColor: '#eff6ff',
|
||||
},
|
||||
optionText: {
|
||||
color: '#475569',
|
||||
fontWeight: '600',
|
||||
},
|
||||
optionTextActive: {
|
||||
color: '#3b82f6',
|
||||
},
|
||||
check: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#3b82f6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
255
components/ui/CategorySelect.tsx
Normal file
255
components/ui/CategorySelect.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react-native';
|
||||
import React, { Dispatch, SetStateAction, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
external_id: string | null;
|
||||
level: number;
|
||||
is_leaf: boolean;
|
||||
icon_name: string | null;
|
||||
}
|
||||
|
||||
interface CategoryResponse {
|
||||
data: {
|
||||
data: Category[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
selectedCategories: Category | null;
|
||||
setSelectedCategories: Dispatch<SetStateAction<Category | null>>;
|
||||
}
|
||||
|
||||
interface HistoryItem {
|
||||
parentId: number | null;
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
export default function CategorySelect({ selectedCategories, setSelectedCategories }: Props) {
|
||||
const { isDark } = useTheme();
|
||||
const [currentCategories, setCurrentCategories] = useState<Category[]>([]);
|
||||
const [currentParentId, setCurrentParentId] = useState<number | null>(null);
|
||||
const [history, setHistory] = useState<HistoryItem[]>([]);
|
||||
|
||||
// Root categories
|
||||
const { isLoading: rootLoading, error: rootError } = useQuery<CategoryResponse>({
|
||||
queryKey: ['categories'],
|
||||
queryFn: async () => products_api.getCategorys(),
|
||||
select(data) {
|
||||
setCurrentCategories(data.data.data);
|
||||
setCurrentParentId(null);
|
||||
setHistory([]);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
// Child categories
|
||||
const { mutate, isPending: mutatePending } = useMutation({
|
||||
mutationFn: (id: number) => products_api.getCategorys({ parent: id }),
|
||||
onSuccess: (response: CategoryResponse, id) => {
|
||||
const childCategories = response.data.data;
|
||||
setHistory((prev) => [...prev, { parentId: currentParentId, categories: currentCategories }]);
|
||||
setCurrentCategories(childCategories);
|
||||
setCurrentParentId(id);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
console.error('Child category loading error:', err);
|
||||
},
|
||||
});
|
||||
|
||||
const toggleCategory = (category: Category) => {
|
||||
if (category.is_leaf) {
|
||||
setSelectedCategories(category);
|
||||
} else {
|
||||
mutate(category.id);
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (history.length > 0) {
|
||||
const previous = history[history.length - 1];
|
||||
setCurrentCategories(previous.categories);
|
||||
setCurrentParentId(previous.parentId);
|
||||
setHistory((prev) => prev.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = rootLoading || mutatePending;
|
||||
const error = rootError;
|
||||
|
||||
const renderCategory = ({ item: category }: { item: Category }) => {
|
||||
const isSelected = selectedCategories?.id === category.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.chip,
|
||||
isDark ? styles.darkChip : styles.lightChip,
|
||||
isSelected && styles.chipSelected,
|
||||
]}
|
||||
onPress={() => toggleCategory(category)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.chipText,
|
||||
isDark ? styles.darkChipText : styles.lightChipText,
|
||||
isSelected && styles.chipTextSelected,
|
||||
]}
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
{!category.is_leaf && (
|
||||
<ChevronRight size={20} color={isSelected ? '#ffffff' : isDark ? '#64748b' : '#94a3b8'} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading && currentCategories.length === 0) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<ActivityIndicator size="small" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && currentCategories.length === 0) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<Text style={{ color: '#f87171' }}>Ma'lumot yuklashda xatolik yuz berdi</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentCategories.length === 0) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<Text style={isDark ? styles.darkText : styles.lightText}>Kategoriyalar topilmadi</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||
<View style={styles.header}>
|
||||
{history.length > 0 && (
|
||||
<TouchableOpacity
|
||||
onPress={goBack}
|
||||
style={[styles.backButton, isDark ? styles.darkBackButton : styles.lightBackButton]}
|
||||
>
|
||||
<ChevronLeft color={'#3b82f6'} size={20} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={currentCategories}
|
||||
renderItem={renderCategory}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={true}
|
||||
ItemSeparatorComponent={() => <View style={{ height: 10 }} />}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginBottom: 20,
|
||||
},
|
||||
darkBg: {
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
lightBg: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
backButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
},
|
||||
darkBackButton: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightBackButton: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
chip: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
darkChip: {
|
||||
backgroundColor: '#1e293b',
|
||||
borderColor: '#334155',
|
||||
},
|
||||
lightChip: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
chipSelected: {
|
||||
backgroundColor: '#3b82f6',
|
||||
borderColor: '#3b82f6',
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
chipText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
},
|
||||
darkChipText: {
|
||||
color: '#cbd5e1',
|
||||
},
|
||||
lightChipText: {
|
||||
color: '#64748b',
|
||||
},
|
||||
chipTextSelected: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
darkText: {
|
||||
color: '#f1f5f9',
|
||||
},
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
});
|
||||
164
components/ui/Combobox.tsx
Normal file
164
components/ui/Combobox.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Check, ChevronDown } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { FlatList, Modal, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
interface ComboboxProps {
|
||||
value: string;
|
||||
onChange: (value: string, label: string) => void;
|
||||
items: { label: string; value: string }[];
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function Combobox({ value, onChange, items, placeholder, disabled }: ComboboxProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const selectedItem = items.find((item) => item.value === value);
|
||||
const displayText = selectedItem ? selectedItem.label : placeholder || 'Select...';
|
||||
|
||||
const filteredItems = items.filter((item) =>
|
||||
item.label.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const handleSelect = (item: { label: string; value: string }) => {
|
||||
onChange(item.value, item.label);
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={[styles.trigger, disabled && styles.triggerDisabled]}
|
||||
onPress={() => !disabled && setIsOpen(true)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text style={[styles.triggerText, disabled && styles.triggerTextDisabled]}>
|
||||
{displayText}
|
||||
</Text>
|
||||
<ChevronDown size={16} color={disabled ? '#9ca3af' : '#6b7280'} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal
|
||||
visible={isOpen}
|
||||
transparent
|
||||
animationType="slide"
|
||||
onRequestClose={() => setIsOpen(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Select Option</Text>
|
||||
<TouchableOpacity onPress={() => setIsOpen(false)}>
|
||||
<Text style={styles.closeButton}>Done</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={filteredItems}
|
||||
keyExtractor={(item) => item.value}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity style={styles.option} onPress={() => handleSelect(item)}>
|
||||
<Text style={styles.optionText}>{item.label}</Text>
|
||||
{value === item.value && <Check size={18} color="#2563eb" />}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
style={styles.optionsList}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
trigger: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
triggerDisabled: {
|
||||
backgroundColor: '#f9fafb',
|
||||
borderColor: '#e5e7eb',
|
||||
},
|
||||
triggerText: {
|
||||
fontSize: 14,
|
||||
color: '#374151',
|
||||
flex: 1,
|
||||
},
|
||||
triggerTextDisabled: {
|
||||
color: '#9ca3af',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: '#fff',
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
maxHeight: '80%',
|
||||
paddingBottom: 20,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e5e7eb',
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
},
|
||||
closeButton: {
|
||||
fontSize: 16,
|
||||
color: '#2563eb',
|
||||
fontWeight: '600',
|
||||
},
|
||||
searchInput: {
|
||||
margin: 16,
|
||||
padding: 12,
|
||||
backgroundColor: '#f9fafb',
|
||||
borderRadius: 8,
|
||||
fontSize: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e7eb',
|
||||
},
|
||||
optionsList: {
|
||||
flex: 1,
|
||||
},
|
||||
option: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#f3f4f6',
|
||||
},
|
||||
optionText: {
|
||||
fontSize: 15,
|
||||
color: '#374151',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
315
components/ui/CompanyList.tsx
Normal file
315
components/ui/CompanyList.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import { BASE_URL } from '@/api/URLs';
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import { CompanyResponse } from '@/screens/home/lib/types';
|
||||
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { Building2, ChevronRight, MapPin, Package } from 'lucide-react-native';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
FlatList,
|
||||
Image,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
|
||||
export default function CompanyList({ query }: { query: string }) {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
const [selectedCompany, setSelectedCompany] = useState<CompanyResponse | null>(null);
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['company-list', query],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await products_api.getCompany({
|
||||
page: pageParam,
|
||||
page_size: PAGE_SIZE,
|
||||
search: query,
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||||
initialPageParam: 1,
|
||||
});
|
||||
|
||||
const allCompanies = data?.pages.flatMap((page) => page.results) ?? [];
|
||||
|
||||
const handlePresentModal = useCallback((company: CompanyResponse) => {
|
||||
setSelectedCompany(company);
|
||||
bottomSheetModalRef.current?.present();
|
||||
}, []);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: any) => (
|
||||
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.5} />
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<Text style={{ color: '#f87171' }}>{t('Xatolik yuz berdi')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlatList
|
||||
data={allCompanies}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.card, isDark ? styles.darkCard : styles.lightCard]}
|
||||
activeOpacity={0.8}
|
||||
onPress={() => handlePresentModal(item)}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<Building2 size={22} color="#3b82f6" />
|
||||
<Text style={[styles.cardTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{item.company_name}
|
||||
</Text>
|
||||
<ChevronRight size={20} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||
</View>
|
||||
<View style={styles.cardLocation}>
|
||||
<MapPin size={16} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||
<Text
|
||||
style={[styles.cardLocationText, isDark ? styles.darkSubText : styles.lightSubText]}
|
||||
>
|
||||
{item.country_name}, {item.region_name}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
||||
onEndReachedThreshold={0.4}
|
||||
ListFooterComponent={
|
||||
isFetchingNextPage ? <ActivityIndicator color="#3b82f6" style={{ margin: 20 }} /> : null
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
index={0}
|
||||
snapPoints={['60%', '90%']}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{ backgroundColor: '#94a3b8', width: 50 }}
|
||||
backgroundStyle={{
|
||||
backgroundColor: isDark ? '#0f172a' : '#ffffff',
|
||||
borderRadius: 24,
|
||||
}}
|
||||
enablePanDownToClose
|
||||
>
|
||||
<BottomSheetScrollView contentContainerStyle={styles.sheetContent}>
|
||||
{selectedCompany && (
|
||||
<>
|
||||
<View style={styles.sheetHeader}>
|
||||
<Building2 size={28} color="#3b82f6" />
|
||||
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{selectedCompany.company_name}
|
||||
</Text>
|
||||
<View style={styles.sheetLocation}>
|
||||
<MapPin size={16} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||
<Text
|
||||
style={[
|
||||
styles.sheetLocationText,
|
||||
isDark ? styles.darkSubText : styles.lightSubText,
|
||||
]}
|
||||
>
|
||||
{selectedCompany.country_name}, {selectedCompany.region_name},{' '}
|
||||
{selectedCompany.district_name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.divider, isDark ? styles.darkDivider : styles.lightDivider]} />
|
||||
|
||||
{selectedCompany.product_service_company &&
|
||||
selectedCompany.product_service_company.length > 0 && (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.sectionLabel,
|
||||
isDark ? styles.darkSubText : styles.lightSubText,
|
||||
]}
|
||||
>
|
||||
{t('Mahsulotlar')} ({selectedCompany.product_service_company.length})
|
||||
</Text>
|
||||
{selectedCompany.product_service_company.map((product) => (
|
||||
<View
|
||||
key={product.id}
|
||||
style={[
|
||||
styles.productCard,
|
||||
isDark ? styles.darkProductCard : styles.lightProductCard,
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.productImage,
|
||||
isDark ? styles.darkProductImage : styles.lightProductImage,
|
||||
]}
|
||||
>
|
||||
{product.files?.[0]?.file ? (
|
||||
<Image
|
||||
source={{ uri: BASE_URL + product.files[0].file }}
|
||||
style={styles.productImg}
|
||||
/>
|
||||
) : (
|
||||
<Package size={28} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.productInfo}>
|
||||
<Text
|
||||
style={[
|
||||
styles.productTitle,
|
||||
isDark ? styles.darkText : styles.lightText,
|
||||
]}
|
||||
>
|
||||
{product.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.productDesc,
|
||||
isDark ? styles.darkSubText : styles.lightSubText,
|
||||
]}
|
||||
>
|
||||
{product.description || 'Tavsif mavjud emas.'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const CARD_WIDTH = SCREEN_WIDTH - 32;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
|
||||
// --- Company Card ---
|
||||
card: {
|
||||
width: CARD_WIDTH - 4,
|
||||
marginLeft: 2,
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginBottom: 8,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
},
|
||||
darkCard: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
cardHeader: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 6 },
|
||||
cardTitle: { fontSize: 17, fontWeight: '700', flex: 1 },
|
||||
darkText: {
|
||||
color: '#f1f5f9',
|
||||
},
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
cardLocation: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
cardLocationText: { fontSize: 13 },
|
||||
darkSubText: {
|
||||
color: '#64748b',
|
||||
},
|
||||
lightSubText: {
|
||||
color: '#94a3b8',
|
||||
},
|
||||
|
||||
// --- Bottom Sheet ---
|
||||
sheetContent: { padding: 20, paddingBottom: 40 },
|
||||
sheetHeader: { alignItems: 'center', marginBottom: 20 },
|
||||
sheetTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: '800',
|
||||
marginTop: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
sheetLocation: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 6 },
|
||||
sheetLocationText: { fontSize: 14 },
|
||||
divider: { height: 1, marginBottom: 16 },
|
||||
darkDivider: {
|
||||
backgroundColor: '#334155',
|
||||
},
|
||||
lightDivider: {
|
||||
backgroundColor: '#e2e8f0',
|
||||
},
|
||||
sectionLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 12,
|
||||
},
|
||||
|
||||
// --- Product Card ---
|
||||
productCard: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
gap: 16,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
darkProductCard: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightProductCard: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
productImage: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
},
|
||||
darkProductImage: {
|
||||
backgroundColor: '#0f172a',
|
||||
borderColor: '#334155',
|
||||
},
|
||||
lightProductImage: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
productImg: { width: '100%', height: '100%', borderRadius: 8 },
|
||||
productInfo: { flex: 1 },
|
||||
productTitle: { fontSize: 15, fontWeight: '700' },
|
||||
productDesc: { fontSize: 13, marginTop: 2 },
|
||||
});
|
||||
221
components/ui/CountriesList.tsx
Normal file
221
components/ui/CountriesList.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { Building2, ChevronDown, ChevronUp } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export default function CountriesList({ search }: { search: string }) {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [openedCountryId, setOpenedCountryId] = useState<number | null>(null);
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['countries-list-infinite', search],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await products_api.getCountry({
|
||||
page: pageParam,
|
||||
page_size: PAGE_SIZE,
|
||||
search: search,
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||||
initialPageParam: 1,
|
||||
});
|
||||
|
||||
const allCountries = data?.pages.flatMap((page) => page.results) ?? [];
|
||||
|
||||
const toggleAccordion = (id: number) => setOpenedCountryId((prev) => (prev === id ? null : id));
|
||||
|
||||
const loadMore = () => hasNextPage && !isFetchingNextPage && fetchNextPage();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<Text style={styles.errorText}>{t('Xatolik yuz berdi')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (allCountries.length === 0) {
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={[styles.emptyText, isDark ? styles.darkSubText : styles.lightSubText]}>
|
||||
{t('Natija topilmadi')}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={allCountries}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={{ gap: 12 }}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.4}
|
||||
ListFooterComponent={
|
||||
isFetchingNextPage ? <ActivityIndicator color="#3b82f6" style={{ margin: 20 }} /> : null
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => {
|
||||
const isOpen = openedCountryId === item.id;
|
||||
|
||||
return (
|
||||
<View style={[styles.countryCard, isDark ? styles.darkCard : styles.lightCard]}>
|
||||
{/* Davlat sarlavhasi */}
|
||||
<TouchableOpacity
|
||||
style={styles.countryHeader}
|
||||
onPress={() => toggleAccordion(item.id)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[styles.countryName, isDark ? styles.darkText : styles.lightText]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<View style={styles.rightSide}>
|
||||
<Text
|
||||
style={[styles.companyCount, isDark ? styles.darkSubText : styles.lightSubText]}
|
||||
>
|
||||
{item.companies.length} {t('ta korxona')}
|
||||
</Text>
|
||||
{isOpen ? (
|
||||
<ChevronUp size={20} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||
) : (
|
||||
<ChevronDown size={20} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Ochiladigan qism */}
|
||||
{isOpen && (
|
||||
<View style={styles.companiesContainer}>
|
||||
{item.companies.map((company) => (
|
||||
<View
|
||||
key={company.id}
|
||||
style={[
|
||||
styles.companyItem,
|
||||
isDark ? styles.darkCompanyItem : styles.lightCompanyItem,
|
||||
]}
|
||||
>
|
||||
<Building2 size={18} color="#3b82f6" />
|
||||
<View style={styles.companyInfo}>
|
||||
<Text
|
||||
style={[styles.companyName, isDark ? styles.darkText : styles.lightText]}
|
||||
>
|
||||
{company.company_name}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.serviceCount,
|
||||
isDark ? styles.darkSubText : styles.lightSubText,
|
||||
]}
|
||||
>
|
||||
{company.service_count} {t('ta mahsulot/xizmat')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const CARD_WIDTH = SCREEN_WIDTH - 32;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 40 },
|
||||
loadingText: { fontSize: 16, marginTop: 10 },
|
||||
errorText: { fontSize: 16, color: '#ef4444', textAlign: 'center' },
|
||||
emptyContainer: { padding: 40, alignItems: 'center' },
|
||||
emptyText: { fontSize: 16 },
|
||||
|
||||
countryCard: {
|
||||
width: CARD_WIDTH - 4,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 6,
|
||||
elevation: 2,
|
||||
marginLeft: 2,
|
||||
},
|
||||
darkCard: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
countryHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
},
|
||||
countryName: { fontSize: 17, fontWeight: '700' },
|
||||
darkText: {
|
||||
color: '#f1f5f9',
|
||||
},
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
rightSide: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||
companyCount: { fontSize: 14 },
|
||||
darkSubText: {
|
||||
color: '#64748b',
|
||||
},
|
||||
lightSubText: {
|
||||
color: '#94a3b8',
|
||||
},
|
||||
|
||||
companiesContainer: { paddingHorizontal: 16, paddingBottom: 16, gap: 10 },
|
||||
companyItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
darkCompanyItem: {
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
lightCompanyItem: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
companyInfo: { flex: 1 },
|
||||
companyName: { fontSize: 15, fontWeight: '600' },
|
||||
serviceCount: { fontSize: 13, marginTop: 2 },
|
||||
});
|
||||
91
components/ui/FilterContext.tsx
Normal file
91
components/ui/FilterContext.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { FilterData } from '@/types';
|
||||
import { cities, companies, countries, industries, states } from '@/types/data';
|
||||
import createContextHook from '@nkzw/create-context-hook';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export const [FilterProvider, useFilter] = createContextHook(() => {
|
||||
const [filterData, setFilterDataState] = useState<FilterData>({
|
||||
country: { name: '', iso: 'all' },
|
||||
state: { name: '', iso: 'all' },
|
||||
city: { name: '', iso: 'all' },
|
||||
industries: [],
|
||||
});
|
||||
|
||||
const [step, setStep] = useState<'filter' | 'items'>('filter');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
|
||||
const setFilterData = (updates: Partial<FilterData>) => {
|
||||
setFilterDataState((prev) => ({ ...prev, ...updates }));
|
||||
};
|
||||
|
||||
const availableStates = useMemo(() => {
|
||||
if (filterData.country.iso === 'all' || !filterData.country.iso) {
|
||||
return states;
|
||||
}
|
||||
return states.filter((s) => s.country_iso === filterData.country.iso);
|
||||
}, [filterData.country.iso]);
|
||||
|
||||
const availableCities = useMemo(() => {
|
||||
if (filterData.state.iso === 'all' || !filterData.state.iso) {
|
||||
return cities;
|
||||
}
|
||||
return cities.filter((c) => c.state_iso === filterData.state.iso);
|
||||
}, [filterData.state.iso]);
|
||||
|
||||
const filteredCompanies = useMemo(() => {
|
||||
return companies.filter((company) => {
|
||||
const countryMatch =
|
||||
!filterData.country.iso ||
|
||||
filterData.country.iso === 'all' ||
|
||||
company.country === filterData.country.iso;
|
||||
const industryMatch =
|
||||
filterData.industries.length === 0 ||
|
||||
(company.industry && filterData.industries.includes(company.industry));
|
||||
return countryMatch && industryMatch;
|
||||
});
|
||||
}, [filterData]);
|
||||
|
||||
const totalItems = filteredCompanies.length;
|
||||
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||
|
||||
const paginatedData = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return filteredCompanies.slice(startIndex, endIndex);
|
||||
}, [filteredCompanies, currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filterData.country.iso !== 'all') {
|
||||
const hasState = availableStates.some((s) => s.iso === filterData.state.iso);
|
||||
if (!hasState) {
|
||||
setFilterData({ state: { name: '', iso: 'all' }, city: { name: '', iso: 'all' } });
|
||||
}
|
||||
}
|
||||
}, [filterData.country.iso, availableStates]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filterData.state.iso !== 'all') {
|
||||
const hasCity = availableCities.some((c) => c.iso === filterData.city.iso);
|
||||
if (!hasCity) {
|
||||
setFilterData({ city: { name: '', iso: 'all' } });
|
||||
}
|
||||
}
|
||||
}, [filterData.state.iso, availableCities]);
|
||||
|
||||
return {
|
||||
filterData,
|
||||
setFilterData,
|
||||
countries,
|
||||
states: availableStates,
|
||||
cities: availableCities,
|
||||
industries,
|
||||
totalItems,
|
||||
step,
|
||||
setStep,
|
||||
allData: paginatedData,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
};
|
||||
});
|
||||
350
components/ui/FilterUI.tsx
Normal file
350
components/ui/FilterUI.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { XIcon } from 'lucide-react-native';
|
||||
import React, { Dispatch, SetStateAction, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import CategorySelect from './CategorySelect';
|
||||
|
||||
interface FilterUIProps {
|
||||
back: () => void;
|
||||
onApply?: (data: any) => void;
|
||||
setStep: Dispatch<SetStateAction<'filter' | 'items'>>;
|
||||
setFiltered: Dispatch<SetStateAction<any[]>>;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
external_id: string | null;
|
||||
level: number;
|
||||
is_leaf: boolean;
|
||||
icon_name: string | null;
|
||||
}
|
||||
|
||||
export default function FilterUI({ back, onApply, setStep, setFiltered }: FilterUIProps) {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [selectedCategories, setSelectedCategories] = useState<Category | null>(null);
|
||||
const [selectedCountry, setSelectedCountry] = useState<string>('all');
|
||||
const [selectedRegion, setSelectedRegion] = useState<string>('all');
|
||||
const [selectedDistrict, setSelectedDistrict] = useState<string>('all');
|
||||
|
||||
const { data: countryResponse, isLoading } = useQuery({
|
||||
queryKey: ['country-detail'],
|
||||
queryFn: async () => products_api.getStates(),
|
||||
select: (res) => res.data?.data || [],
|
||||
});
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: (params: any) => products_api.businessAbout(params),
|
||||
onSuccess: (data) => {
|
||||
setStep('items');
|
||||
setFiltered(data.data.data.results);
|
||||
},
|
||||
onError: (error: AxiosError) => console.log(error),
|
||||
});
|
||||
|
||||
const handleApply = () => {
|
||||
const countryObj = countryResponse?.find((c) => c.id?.toString() === selectedCountry);
|
||||
const regionObj = countryObj?.region.find((r) => r.id?.toString() === selectedRegion);
|
||||
const districtObj = regionObj?.districts.find((d) => d.id?.toString() === selectedDistrict);
|
||||
|
||||
mutate({
|
||||
country: countryObj?.name || '',
|
||||
region: regionObj?.name || '',
|
||||
district: districtObj?.name || '',
|
||||
types: selectedCategories ? selectedCategories.id : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const regions = useMemo(() => {
|
||||
if (selectedCountry === 'all') return [];
|
||||
const country = countryResponse?.find((c) => c.id?.toString() === selectedCountry);
|
||||
return country?.region || [];
|
||||
}, [countryResponse, selectedCountry]);
|
||||
|
||||
const districts = useMemo(() => {
|
||||
if (selectedRegion === 'all') return [];
|
||||
const region = regions.find((r) => r.id?.toString() === selectedRegion);
|
||||
return region?.districts || [];
|
||||
}, [regions, selectedRegion]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
isDark ? styles.darkBg : styles.lightBg,
|
||||
{ justifyContent: 'center', alignItems: 'center' },
|
||||
]}
|
||||
>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Single Tag Component
|
||||
const Tag = ({
|
||||
label,
|
||||
selected,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
selected: boolean;
|
||||
onPress: () => void;
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tag,
|
||||
isDark ? styles.darkTag : styles.lightTag,
|
||||
selected && styles.tagSelected,
|
||||
]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tagText,
|
||||
isDark ? styles.darkTagText : styles.lightTagText,
|
||||
selected && styles.tagTextSelected,
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||
{/* Header */}
|
||||
<View style={styles.btn}>
|
||||
<TouchableOpacity
|
||||
onPress={back}
|
||||
style={[styles.backBtn, isDark ? styles.darkBackBtn : styles.lightBackBtn]}
|
||||
>
|
||||
<XIcon color={isDark ? 'white' : '#0f172a'} fontSize={24} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<ScrollView
|
||||
style={{ padding: 16 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 140 }}
|
||||
>
|
||||
{/* Country Filter */}
|
||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Davlat')}
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.scrollRow}>
|
||||
<Tag
|
||||
label={t('Barchasi')}
|
||||
selected={selectedCountry === 'all'}
|
||||
onPress={() => setSelectedCountry('all')}
|
||||
/>
|
||||
{countryResponse?.map((c) => (
|
||||
<Tag
|
||||
key={c.id}
|
||||
label={c.name}
|
||||
selected={selectedCountry === c.id?.toString()}
|
||||
onPress={() => {
|
||||
setSelectedCountry(c.id?.toString() || 'all');
|
||||
setSelectedRegion('all');
|
||||
setSelectedDistrict('all');
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* Region Filter */}
|
||||
{regions.length > 0 && (
|
||||
<>
|
||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Viloyat')}
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.scrollRow}>
|
||||
<Tag
|
||||
label={t('Barchasi')}
|
||||
selected={selectedRegion === 'all'}
|
||||
onPress={() => setSelectedRegion('all')}
|
||||
/>
|
||||
{regions.map((r) => (
|
||||
<Tag
|
||||
key={r.id}
|
||||
label={r.name}
|
||||
selected={selectedRegion === r.id?.toString()}
|
||||
onPress={() => {
|
||||
setSelectedRegion(r.id?.toString() || 'all');
|
||||
setSelectedDistrict('all');
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* District Filter */}
|
||||
{districts.length > 0 && (
|
||||
<>
|
||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Tuman')}
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.scrollRow}>
|
||||
<Tag
|
||||
label={t('Barchasi')}
|
||||
selected={selectedDistrict === 'all'}
|
||||
onPress={() => setSelectedDistrict('all')}
|
||||
/>
|
||||
{districts.map((d) => (
|
||||
<Tag
|
||||
key={d.id}
|
||||
label={d.name}
|
||||
selected={selectedDistrict === d.id?.toString()}
|
||||
onPress={() => setSelectedDistrict(d.id?.toString() || 'all')}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Industry Selection */}
|
||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Sohalar')}
|
||||
</Text>
|
||||
<CategorySelect
|
||||
selectedCategories={selectedCategories}
|
||||
setSelectedCategories={setSelectedCategories}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
{/* Fixed Apply Button */}
|
||||
<View style={styles.applyBtnWrapper}>
|
||||
<TouchableOpacity style={styles.applyBtn} onPress={handleApply}>
|
||||
<Text style={styles.applyBtnText}>{t("Natijalarni ko'rish")}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
darkBg: {
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
lightBg: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
backBtn: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 10,
|
||||
marginTop: 10,
|
||||
borderWidth: 1,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
darkBackBtn: {
|
||||
backgroundColor: '#1e293b',
|
||||
borderColor: '#334155',
|
||||
},
|
||||
lightBackBtn: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
btn: {
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-end',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginBottom: 10,
|
||||
},
|
||||
darkText: {
|
||||
color: '#f1f5f9',
|
||||
},
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
scrollRow: { flexDirection: 'row', marginBottom: 12, gap: 10 },
|
||||
|
||||
tag: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 12,
|
||||
marginRight: 10,
|
||||
borderWidth: 1,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
darkTag: {
|
||||
backgroundColor: '#1e293b',
|
||||
borderColor: '#334155',
|
||||
},
|
||||
lightTag: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
tagSelected: {
|
||||
backgroundColor: '#3b82f6',
|
||||
borderColor: '#3b82f6',
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
},
|
||||
tagText: {
|
||||
fontWeight: '500',
|
||||
},
|
||||
darkTagText: {
|
||||
color: '#cbd5e1',
|
||||
},
|
||||
lightTagText: {
|
||||
color: '#64748b',
|
||||
},
|
||||
tagTextSelected: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
|
||||
applyBtnWrapper: {
|
||||
position: 'absolute',
|
||||
bottom: 55,
|
||||
left: 16,
|
||||
right: 16,
|
||||
zIndex: 10,
|
||||
},
|
||||
applyBtn: {
|
||||
backgroundColor: '#3b82f6',
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
marginBottom: 20,
|
||||
},
|
||||
applyBtnText: { color: '#ffffff', fontWeight: '700', fontSize: 16 },
|
||||
});
|
||||
369
components/ui/FilteredItems.tsx
Normal file
369
components/ui/FilteredItems.tsx
Normal file
@@ -0,0 +1,369 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import { businessAboutDetailResData } from '@/screens/home/lib/types';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { ChevronLeft, FileText, Phone, User } from 'lucide-react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Image,
|
||||
Linking,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
interface FilteredItemsProps {
|
||||
data: { id: number; company_name: string }[];
|
||||
back: () => void;
|
||||
}
|
||||
|
||||
export default function FilteredItems({ data, back }: FilteredItemsProps) {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [selectedItem, setSelectedItem] = useState<businessAboutDetailResData | null>(null);
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (id: number) => products_api.businessAboutDetail(id),
|
||||
onSuccess: (data) => setSelectedItem(data.data.data),
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedItem) {
|
||||
return (
|
||||
<ScrollView
|
||||
style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}
|
||||
contentContainerStyle={{ paddingBottom: 70 }}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
{/* Back Button */}
|
||||
<TouchableOpacity
|
||||
onPress={() => setSelectedItem(null)}
|
||||
style={[styles.backButton, isDark ? styles.darkBackButton : styles.lightBackButton]}
|
||||
>
|
||||
<ChevronLeft size={24} color="#3b82f6" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Company Name */}
|
||||
<Text style={[styles.detailTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{selectedItem.company_name}
|
||||
</Text>
|
||||
|
||||
{/* Company Image */}
|
||||
{selectedItem.company_image && (
|
||||
<Image
|
||||
source={{ uri: selectedItem.company_image }}
|
||||
style={[styles.companyImage, isDark ? styles.darkImageBg : styles.lightImageBg]}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Info Card */}
|
||||
<View style={[styles.infoCard, isDark ? styles.darkCard : styles.lightCard]}>
|
||||
{selectedItem.director_full_name && (
|
||||
<View style={styles.infoRow}>
|
||||
<User size={20} color={isDark ? '#64748b' : '#94a3b8'} />
|
||||
<View style={styles.infoTextContainer}>
|
||||
<Text
|
||||
style={[styles.infoLabel, isDark ? styles.darkSubText : styles.lightSubText]}
|
||||
>
|
||||
{t('Rahbar')}
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, isDark ? styles.darkText : styles.lightText]}>
|
||||
{selectedItem.director_full_name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{selectedItem.phone && (
|
||||
<TouchableOpacity
|
||||
style={styles.infoRow}
|
||||
onPress={() => Linking.openURL(`tel:+${selectedItem.phone}`)}
|
||||
>
|
||||
<Phone size={20} color="#3b82f6" />
|
||||
<View style={styles.infoTextContainer}>
|
||||
<Text
|
||||
style={[styles.infoLabel, isDark ? styles.darkSubText : styles.lightSubText]}
|
||||
>
|
||||
{t('Telefon raqami')}
|
||||
</Text>
|
||||
<Text style={[styles.infoValue, { color: '#3b82f6' }]}>
|
||||
+{selectedItem.phone}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{selectedItem.product_service_company?.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<FileText size={20} color="#3b82f6" />
|
||||
<Text style={[styles.sectionTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('Mahsulot va xizmatlar')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{selectedItem.product_service_company.map((item, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.serviceItem,
|
||||
isDark ? styles.darkServiceItem : styles.lightServiceItem,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.serviceTitle, isDark ? styles.darkText : styles.lightText]}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.description && (
|
||||
<Text
|
||||
style={[
|
||||
styles.serviceDescription,
|
||||
isDark ? styles.darkSubText : styles.lightSubText,
|
||||
]}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{item.category?.length > 0 && (
|
||||
<View style={styles.tagsContainer}>
|
||||
{item.category.map((cat) => (
|
||||
<View
|
||||
key={cat.id}
|
||||
style={[styles.tag, isDark ? styles.darkTag : styles.lightTag]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tagText,
|
||||
isDark ? styles.darkSubText : styles.lightSubText,
|
||||
]}
|
||||
>
|
||||
{cat.name}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{item.files?.length > 0 && (
|
||||
<View style={styles.filesContainer}>
|
||||
<Text
|
||||
style={[styles.filesTitle, isDark ? styles.darkText : styles.lightText]}
|
||||
>
|
||||
{t('Fayllar')}:
|
||||
</Text>
|
||||
{item.files.map((file, idx) => (
|
||||
<TouchableOpacity
|
||||
key={idx}
|
||||
onPress={() => Linking.openURL(file.file)}
|
||||
style={styles.fileLink}
|
||||
>
|
||||
<Text style={styles.fileLinkText}>
|
||||
{t('Fayl')} {idx + 1} {t('ochish')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
// List view
|
||||
return (
|
||||
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||
<View style={styles.content}>
|
||||
<TouchableOpacity
|
||||
onPress={back}
|
||||
style={[styles.backButton, isDark ? styles.darkBackButton : styles.lightBackButton]}
|
||||
>
|
||||
<ChevronLeft size={24} color="#3b82f6" />
|
||||
</TouchableOpacity>
|
||||
|
||||
{data && data.length > 0 ? (
|
||||
<FlatList
|
||||
data={data}
|
||||
style={{ marginBottom: 120 }}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.itemCard, isDark ? styles.darkItemCard : styles.lightItemCard]}
|
||||
onPress={() => mutate(item.id)}
|
||||
>
|
||||
<Text style={[styles.itemTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{item.company_name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={[styles.emptyText, isDark ? styles.darkSubText : styles.lightSubText]}>
|
||||
{t('Natija topilmadi')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
darkBg: {
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
lightBg: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
maxWidth: 500,
|
||||
width: '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
backButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
padding: 10,
|
||||
borderRadius: 12,
|
||||
alignSelf: 'flex-start',
|
||||
gap: 8,
|
||||
},
|
||||
darkBackButton: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightBackButton: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
backText: { fontSize: 16, color: '#3b82f6', fontWeight: '600' },
|
||||
listContainer: { gap: 12 },
|
||||
itemCard: {
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
darkItemCard: {
|
||||
backgroundColor: '#1e293b',
|
||||
borderColor: '#334155',
|
||||
},
|
||||
lightItemCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
itemTitle: { fontSize: 17, fontWeight: '600' },
|
||||
darkText: {
|
||||
color: '#f1f5f9',
|
||||
},
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
emptyContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 40 },
|
||||
emptyText: { fontSize: 16, textAlign: 'center' },
|
||||
darkSubText: {
|
||||
color: '#64748b',
|
||||
},
|
||||
lightSubText: {
|
||||
color: '#94a3b8',
|
||||
},
|
||||
|
||||
// Detail
|
||||
detailTitle: { fontSize: 26, fontWeight: '700', marginBottom: 16 },
|
||||
companyImage: {
|
||||
width: '100%',
|
||||
height: 220,
|
||||
borderRadius: 16,
|
||||
marginBottom: 20,
|
||||
},
|
||||
darkImageBg: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightImageBg: {
|
||||
backgroundColor: '#f1f5f9',
|
||||
},
|
||||
infoCard: {
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
borderWidth: 1,
|
||||
gap: 16,
|
||||
},
|
||||
darkCard: {
|
||||
backgroundColor: '#1e293b',
|
||||
borderColor: '#334155',
|
||||
},
|
||||
lightCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
infoRow: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 8 },
|
||||
infoTextContainer: { flex: 1 },
|
||||
infoLabel: { fontSize: 14, marginBottom: 4 },
|
||||
infoValue: { fontSize: 16, fontWeight: '500' },
|
||||
|
||||
section: { marginTop: 8 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '700' },
|
||||
serviceItem: {
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
darkServiceItem: {
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
lightServiceItem: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
serviceTitle: { fontSize: 17, fontWeight: '600', marginBottom: 8 },
|
||||
serviceDescription: { fontSize: 15, lineHeight: 22, marginBottom: 12 },
|
||||
|
||||
tagsContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 8 },
|
||||
tag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
},
|
||||
darkTag: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightTag: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
tagText: { fontSize: 13, fontWeight: '500' },
|
||||
|
||||
filesContainer: { marginTop: 12 },
|
||||
filesTitle: { fontSize: 14, fontWeight: '600', marginBottom: 8 },
|
||||
fileLink: { paddingVertical: 6 },
|
||||
fileLinkText: { color: '#3b82f6', fontSize: 15, textDecorationLine: 'underline' },
|
||||
});
|
||||
92
components/ui/Header.tsx
Normal file
92
components/ui/Header.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import Logo from '@/assets/images/logo.png';
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { LogOut } from 'lucide-react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useAuth } from '../AuthProvider';
|
||||
|
||||
export const CustomHeader = ({ logoutbtn = false }: { logoutbtn?: boolean }) => {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { logout } = useAuth();
|
||||
|
||||
return (
|
||||
<View style={[styles.container, isDark ? styles.darkBg : styles.lightBg]}>
|
||||
<View style={styles.logoWrapper}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Image source={Logo} style={styles.logo} />
|
||||
</View>
|
||||
<Text style={[styles.title, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t('app.name', 'InfoTarget')}
|
||||
</Text>
|
||||
</View>
|
||||
{logoutbtn && (
|
||||
<TouchableOpacity onPress={logout}>
|
||||
<LogOut color={'red'} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
|
||||
darkBg: {
|
||||
backgroundColor: '#0f172a',
|
||||
borderBottomColor: '#1e293b',
|
||||
},
|
||||
|
||||
lightBg: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderBottomColor: '#e2e8f0',
|
||||
},
|
||||
|
||||
logoWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
|
||||
logoContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 4,
|
||||
},
|
||||
|
||||
logo: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
|
||||
darkText: {
|
||||
color: '#f8fafc',
|
||||
},
|
||||
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
});
|
||||
236
components/ui/IndustrySelection.tsx
Normal file
236
components/ui/IndustrySelection.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react-native';
|
||||
import React, { Dispatch, SetStateAction, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useTheme } from '../ThemeContext';
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
external_id: string | null;
|
||||
level: number;
|
||||
is_leaf: boolean;
|
||||
icon_name: string | null;
|
||||
}
|
||||
|
||||
interface CategoryResponse {
|
||||
data: {
|
||||
data: Category[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
selectedCategories: Category[];
|
||||
setSelectedCategories: Dispatch<SetStateAction<Category[]>>;
|
||||
}
|
||||
|
||||
export default function CategorySelection({ selectedCategories, setSelectedCategories }: Props) {
|
||||
const [currentCategories, setCurrentCategories] = useState<Category[]>([]);
|
||||
const [history, setHistory] = useState<{ parentId: number | null; categories: Category[] }[]>([]);
|
||||
const [currentParentId, setCurrentParentId] = useState<number | null>(null);
|
||||
const { isDark } = useTheme();
|
||||
|
||||
const theme = {
|
||||
cardBg: isDark ? '#1e293b' : '#f8fafc',
|
||||
cardBorder: isDark ? '#334155' : '#e2e8f0',
|
||||
text: isDark ? '#cbd5e1' : '#334155',
|
||||
textSelected: '#ffffff',
|
||||
primary: '#2563eb',
|
||||
primaryBg: '#3b82f6',
|
||||
error: '#ef4444',
|
||||
backButtonBg: isDark ? '#1e293b' : '#e2e8f0',
|
||||
chevronColor: isDark ? '#94a3b8' : '#64748b',
|
||||
shadow: isDark ? '#000' : '#94a3b8',
|
||||
};
|
||||
|
||||
const {
|
||||
data: rootData,
|
||||
isLoading: rootLoading,
|
||||
error: rootError,
|
||||
} = useQuery<CategoryResponse>({
|
||||
queryKey: ['categories'],
|
||||
queryFn: async () => products_api.getCategorys(),
|
||||
select(data) {
|
||||
setCurrentCategories(data.data.data);
|
||||
setCurrentParentId(null);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate, isPending: mutatePending } = useMutation({
|
||||
mutationFn: (id: number) => products_api.getCategorys({ parent: id }),
|
||||
onSuccess: (response: CategoryResponse, id) => {
|
||||
const childCategories = response.data.data;
|
||||
setHistory((prev) => [...prev, { parentId: currentParentId, categories: currentCategories }]);
|
||||
setCurrentCategories(childCategories);
|
||||
setCurrentParentId(id);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
console.error('Child yuklashda xato:', err);
|
||||
},
|
||||
});
|
||||
|
||||
const toggleCategory = (category: Category) => {
|
||||
if (category.is_leaf) {
|
||||
setSelectedCategories((prev) => {
|
||||
const exists = prev.find((c) => c.id === category.id);
|
||||
if (exists) return prev.filter((c) => c.id !== category.id);
|
||||
return [...prev, category];
|
||||
});
|
||||
} else {
|
||||
mutate(category.id);
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (history.length > 0) {
|
||||
const previous = history[history.length - 1];
|
||||
setCurrentCategories(previous.categories);
|
||||
setCurrentParentId(previous.parentId);
|
||||
setHistory((prev) => prev.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = rootLoading || mutatePending;
|
||||
const error = rootError;
|
||||
|
||||
const renderCategory = ({ item: category }: { item: Category }) => {
|
||||
const isSelected = selectedCategories.some((c) => c.id === category.id);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.chip,
|
||||
{
|
||||
backgroundColor: isSelected ? theme.primaryBg : theme.cardBg,
|
||||
borderColor: isSelected ? theme.primaryBg : theme.cardBorder,
|
||||
shadowColor: isSelected ? theme.primaryBg : theme.shadow,
|
||||
},
|
||||
isSelected && styles.chipSelected,
|
||||
]}
|
||||
onPress={() => toggleCategory(category)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.chipText,
|
||||
{ color: isSelected ? theme.textSelected : theme.text },
|
||||
isSelected && styles.chipTextSelected,
|
||||
]}
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
{!category.is_leaf && (
|
||||
<ChevronRight size={20} color={isSelected ? theme.textSelected : theme.chevronColor} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading && currentCategories.length === 0) {
|
||||
return (
|
||||
<View style={[styles.centerContainer]}>
|
||||
<ActivityIndicator size="small" color={theme.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && currentCategories.length === 0) {
|
||||
return (
|
||||
<View style={[styles.centerContainer]}>
|
||||
<Text style={{ color: theme.error }}>Ma'lumot yuklashda xatolik yuz berdi</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentCategories.length === 0) {
|
||||
return (
|
||||
<View style={[styles.centerContainer]}>
|
||||
<Text style={{ color: theme.text }}>Kategoriyalar topilmadi</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container]}>
|
||||
<View style={styles.header}>
|
||||
{history.length > 0 && (
|
||||
<TouchableOpacity
|
||||
onPress={goBack}
|
||||
style={[styles.backButton, { backgroundColor: theme.backButtonBg }]}
|
||||
>
|
||||
<ChevronLeft color={theme.primary} size={20} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={currentCategories}
|
||||
renderItem={renderCategory}
|
||||
scrollEnabled={false}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
showsVerticalScrollIndicator={true}
|
||||
ItemSeparatorComponent={() => <View style={{ height: 10 }} />}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
marginBottom: 20,
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
backButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 8,
|
||||
},
|
||||
chip: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
chipSelected: {
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
chipText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
},
|
||||
chipTextSelected: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
376
components/ui/ProductList.tsx
Normal file
376
components/ui/ProductList.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { products_api } from '@/screens/home/lib/api';
|
||||
import { ProductResponse } from '@/screens/home/lib/types';
|
||||
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { ResizeMode, Video } from 'expo-av';
|
||||
import { Info, Package, PlayCircle } from 'lucide-react-native';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Image,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
const IMAGE_WIDTH = SCREEN_WIDTH - 40;
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
type Props = { query: string };
|
||||
|
||||
export default function ProductList({ query }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { isDark } = useTheme();
|
||||
const trimmedQuery = query.trim();
|
||||
const [selectedProduct, setSelectedProduct] = useState<ProductResponse | null>(null);
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
|
||||
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['products-list', trimmedQuery],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await products_api.getProducts({
|
||||
page: pageParam,
|
||||
page_size: PAGE_SIZE,
|
||||
search: trimmedQuery,
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.current_page < lastPage.total_pages ? lastPage.current_page + 1 : undefined,
|
||||
initialPageParam: 1,
|
||||
});
|
||||
|
||||
const allProducts = data?.pages.flatMap((p) => p.results) ?? [];
|
||||
|
||||
const handlePresentModalPress = useCallback((product: ProductResponse) => {
|
||||
setSelectedProduct(product);
|
||||
setCurrentImageIndex(0);
|
||||
bottomSheetModalRef.current?.present();
|
||||
}, []);
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
(props: any) => (
|
||||
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.5} />
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const isVideoFile = (url?: string) => url?.toLowerCase().match(/\.(mp4|mov|avi|mkv|webm)$/i);
|
||||
|
||||
const renderItem = ({ item }: { item: ProductResponse }) => {
|
||||
const mainFile = item.files?.[0]?.file;
|
||||
const isVideo = isVideoFile(mainFile);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.card, isDark ? styles.darkCard : styles.lightCard]}
|
||||
activeOpacity={0.85}
|
||||
onPress={() => handlePresentModalPress(item)}
|
||||
>
|
||||
<View style={[styles.imageContainer, isDark ? styles.darkImageBg : styles.lightImageBg]}>
|
||||
{mainFile ? (
|
||||
<>
|
||||
<Image source={{ uri: mainFile }} style={styles.image} resizeMode="cover" />
|
||||
{isVideo && (
|
||||
<View style={styles.videoIconOverlay}>
|
||||
<PlayCircle size={36} color="white" fill="rgba(0,0,0,0.35)" />
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.placeholder}>
|
||||
<Package size={40} color={isDark ? '#64748b' : '#cbd5e1'} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.info}>
|
||||
<Text
|
||||
style={[styles.title, isDark ? styles.darkText : styles.lightText]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<View style={styles.companyRow}>
|
||||
<Text style={[styles.companyText, isDark ? styles.darkSubText : styles.lightSubText]}>
|
||||
{item.company}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCarouselItem = ({ item }: { item: { id: number; file: string } }) => {
|
||||
const isVideo = isVideoFile(item.file);
|
||||
return (
|
||||
<View style={styles.carouselImageContainer}>
|
||||
{isVideo ? (
|
||||
<Video
|
||||
source={{ uri: item.file }}
|
||||
style={styles.carouselImage}
|
||||
resizeMode={ResizeMode.COVER}
|
||||
useNativeControls
|
||||
/>
|
||||
) : (
|
||||
<Image source={{ uri: item.file }} style={styles.carouselImage} resizeMode="cover" />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading && !data)
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
</View>
|
||||
);
|
||||
|
||||
if (isError)
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<Text style={{ color: '#f87171' }}>{t('Xatolik yuz berdi')}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlatList
|
||||
data={allProducts}
|
||||
nestedScrollEnabled={true}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={renderItem}
|
||||
numColumns={2}
|
||||
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 8 }}
|
||||
contentContainerStyle={{}}
|
||||
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
|
||||
onEndReachedThreshold={0.4}
|
||||
ListFooterComponent={
|
||||
isFetchingNextPage ? (
|
||||
<View style={styles.footerLoader}>
|
||||
<ActivityIndicator size="small" color="#3b82f6" />
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
data ? (
|
||||
<View style={styles.center}>
|
||||
<Text style={{ color: '#94a3b8' }}>{t('Natija topilmadi')}</Text>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
<BottomSheetModal
|
||||
ref={bottomSheetModalRef}
|
||||
index={0}
|
||||
snapPoints={['70%', '95%']}
|
||||
backdropComponent={renderBackdrop}
|
||||
handleIndicatorStyle={{ backgroundColor: '#94a3b8', width: 50 }}
|
||||
backgroundStyle={{ backgroundColor: isDark ? '#0f172a' : '#ffffff' }}
|
||||
enablePanDownToClose
|
||||
>
|
||||
<BottomSheetScrollView
|
||||
style={styles.sheetContent}
|
||||
contentContainerStyle={styles.sheetContentContainer}
|
||||
>
|
||||
{selectedProduct && (
|
||||
<>
|
||||
<View style={styles.carouselWrapper}>
|
||||
<FlatList
|
||||
nestedScrollEnabled={true}
|
||||
data={selectedProduct.files || []}
|
||||
renderItem={renderCarouselItem}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onMomentumScrollEnd={(e) => {
|
||||
const index = Math.round(e.nativeEvent.contentOffset.x / IMAGE_WIDTH);
|
||||
setCurrentImageIndex(index);
|
||||
}}
|
||||
/>
|
||||
{selectedProduct.files.length > 1 && (
|
||||
<View style={styles.pagination}>
|
||||
{selectedProduct.files.map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
styles.paginationDot,
|
||||
currentImageIndex === i && styles.paginationDotActive,
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.sheetHeader}>
|
||||
<Text style={[styles.sheetTitle, isDark ? styles.darkText : styles.lightText]}>
|
||||
{selectedProduct.title}
|
||||
</Text>
|
||||
<View style={styles.sheetCompanyBadge}>
|
||||
<Text style={styles.sheetCompanyText}>{selectedProduct.company}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.divider, isDark ? styles.darkDivider : styles.lightDivider]} />
|
||||
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionTitleRow}>
|
||||
<Info size={18} color={isDark ? '#f1f5f9' : '#0f172a'} />
|
||||
<Text style={[styles.sectionLabel, isDark ? styles.darkText : styles.lightText]}>
|
||||
{t("Batafsil ma'lumot")}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={[styles.sheetDescription, isDark ? styles.darkText : styles.lightText]}
|
||||
>
|
||||
{selectedProduct.description || "Ma'lumot mavjud emas."}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listContainer: { gap: 0, paddingBottom: 20 },
|
||||
card: {
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
width: (SCREEN_WIDTH - 40) / 2,
|
||||
marginLeft: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.15,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowRadius: 50,
|
||||
elevation: 4,
|
||||
},
|
||||
darkCard: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
imageContainer: {
|
||||
width: '100%',
|
||||
height: 160,
|
||||
},
|
||||
darkImageBg: {
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
lightImageBg: {
|
||||
backgroundColor: '#f1f5f9',
|
||||
},
|
||||
image: { width: '100%', height: '100%' },
|
||||
videoIconOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.25)',
|
||||
},
|
||||
placeholder: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||
info: { padding: 12 },
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginBottom: 4,
|
||||
},
|
||||
darkText: {
|
||||
color: '#f1f5f9',
|
||||
},
|
||||
lightText: {
|
||||
color: '#0f172a',
|
||||
},
|
||||
companyRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
companyText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
darkSubText: {
|
||||
color: '#64748b',
|
||||
},
|
||||
lightSubText: {
|
||||
color: '#94a3b8',
|
||||
},
|
||||
|
||||
// Bottom Sheet
|
||||
sheetContent: { flex: 1 },
|
||||
sheetContentContainer: { paddingHorizontal: 20, paddingBottom: 40 },
|
||||
carouselWrapper: {
|
||||
width: IMAGE_WIDTH,
|
||||
height: 280,
|
||||
marginBottom: 20,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
carouselImageContainer: { width: IMAGE_WIDTH, height: 280, backgroundColor: '#e2e8f0' },
|
||||
carouselImage: { width: '100%', height: '100%' },
|
||||
pagination: {
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
paginationDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: 'rgba(255,255,255,0.4)' },
|
||||
paginationDotActive: { backgroundColor: '#3b82f6', width: 20 },
|
||||
|
||||
sheetHeader: { marginBottom: 16 },
|
||||
sheetTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
marginBottom: 8,
|
||||
},
|
||||
sheetCompanyBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
backgroundColor: '#3b82f6',
|
||||
alignSelf: 'flex-start',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 12,
|
||||
},
|
||||
sheetCompanyText: { fontSize: 13, color: '#ffffff', fontWeight: '700' },
|
||||
divider: {
|
||||
height: 1,
|
||||
marginBottom: 20,
|
||||
},
|
||||
darkDivider: {
|
||||
backgroundColor: '#334155',
|
||||
},
|
||||
lightDivider: {
|
||||
backgroundColor: '#e2e8f0',
|
||||
},
|
||||
section: { marginBottom: 20 },
|
||||
sectionTitleRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 8 },
|
||||
sectionLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
sheetDescription: {
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
|
||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
footerLoader: { paddingVertical: 20, alignItems: 'center' },
|
||||
});
|
||||
37
components/ui/RefreshContext.tsx
Normal file
37
components/ui/RefreshContext.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createContext, useCallback, useContext, useState } from 'react';
|
||||
|
||||
type RefreshContextType = {
|
||||
refreshing: boolean;
|
||||
onRefresh: () => Promise<void>;
|
||||
};
|
||||
|
||||
const RefreshContext = createContext<RefreshContextType | null>(null);
|
||||
|
||||
export function RefreshProvider({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
if (refreshing) return;
|
||||
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await queryClient.refetchQueries();
|
||||
} catch (err) {
|
||||
console.error('Global refresh error:', err);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [queryClient, refreshing]);
|
||||
|
||||
return (
|
||||
<RefreshContext.Provider value={{ refreshing, onRefresh }}>{children}</RefreshContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useGlobalRefresh() {
|
||||
const ctx = useContext(RefreshContext);
|
||||
if (!ctx) throw new Error('useGlobalRefresh must be used inside RefreshProvider');
|
||||
return ctx;
|
||||
}
|
||||
132
components/ui/SearchTabs.tsx
Normal file
132
components/ui/SearchTabs.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useTheme } from '@/components/ThemeContext';
|
||||
import { TabKey } from '@/types';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
|
||||
interface SearchTabsProps {
|
||||
value: TabKey;
|
||||
onChange: (tab: TabKey) => void;
|
||||
}
|
||||
|
||||
export default function SearchTabs({ value, onChange }: SearchTabsProps) {
|
||||
const { isDark } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tabs: { key: TabKey; label: string }[] = [
|
||||
{ key: 'products', label: 'Tovar va xizmatlar' },
|
||||
{ key: 'companies', label: 'Yuridik shaxslar' },
|
||||
{ key: 'countries', label: 'Davlatlar' },
|
||||
];
|
||||
|
||||
const renderTab = ({ item }: { item: { key: TabKey; label: string } }) => {
|
||||
const isActive = value === item.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
isDark ? styles.darkTab : styles.lightTab,
|
||||
isActive && styles.activeTab,
|
||||
]}
|
||||
onPress={() => onChange(item.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabText,
|
||||
isDark ? styles.darkTabText : styles.lightTabText,
|
||||
isActive && styles.activeTabText,
|
||||
]}
|
||||
>
|
||||
{t(item.label)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.shadowWrapper]}>
|
||||
<View style={[styles.wrapper, isDark ? styles.darkWrapper : styles.lightWrapper]}>
|
||||
<FlatList
|
||||
data={tabs}
|
||||
renderItem={renderTab}
|
||||
keyExtractor={(item) => item.key}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.container}
|
||||
ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
shadowWrapper: {
|
||||
borderRadius: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
|
||||
wrapper: {
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden', // ENDI ISHLAYDI
|
||||
},
|
||||
|
||||
darkWrapper: {
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
lightWrapper: {
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
container: {
|
||||
paddingHorizontal: 2,
|
||||
paddingVertical: 2,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
tab: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
darkTab: {
|
||||
backgroundColor: '#1e293b',
|
||||
},
|
||||
lightTab: {
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
activeTab: {
|
||||
backgroundColor: '#3b82f6',
|
||||
shadowColor: '#3b82f6',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
darkTabText: {
|
||||
color: '#cbd5e1',
|
||||
},
|
||||
lightTabText: {
|
||||
color: '#64748b',
|
||||
},
|
||||
activeTabText: {
|
||||
color: '#ffffff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
309
components/ui/gluestack-ui-provider/config.ts
Normal file
309
components/ui/gluestack-ui-provider/config.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
'use client';
|
||||
import { vars } from 'nativewind';
|
||||
|
||||
export const config = {
|
||||
light: vars({
|
||||
'--color-primary-0': '179 179 179',
|
||||
'--color-primary-50': '153 153 153',
|
||||
'--color-primary-100': '128 128 128',
|
||||
'--color-primary-200': '115 115 115',
|
||||
'--color-primary-300': '102 102 102',
|
||||
'--color-primary-400': '82 82 82',
|
||||
'--color-primary-500': '51 51 51',
|
||||
'--color-primary-600': '41 41 41',
|
||||
'--color-primary-700': '31 31 31',
|
||||
'--color-primary-800': '13 13 13',
|
||||
'--color-primary-900': '10 10 10',
|
||||
'--color-primary-950': '8 8 8',
|
||||
|
||||
/* Secondary */
|
||||
'--color-secondary-0': '253 253 253',
|
||||
'--color-secondary-50': '251 251 251',
|
||||
'--color-secondary-100': '246 246 246',
|
||||
'--color-secondary-200': '242 242 242',
|
||||
'--color-secondary-300': '237 237 237',
|
||||
'--color-secondary-400': '230 230 231',
|
||||
'--color-secondary-500': '217 217 219',
|
||||
'--color-secondary-600': '198 199 199',
|
||||
'--color-secondary-700': '189 189 189',
|
||||
'--color-secondary-800': '177 177 177',
|
||||
'--color-secondary-900': '165 164 164',
|
||||
'--color-secondary-950': '157 157 157',
|
||||
|
||||
/* Tertiary */
|
||||
'--color-tertiary-0': '255 250 245',
|
||||
'--color-tertiary-50': '255 242 229',
|
||||
'--color-tertiary-100': '255 233 213',
|
||||
'--color-tertiary-200': '254 209 170',
|
||||
'--color-tertiary-300': '253 180 116',
|
||||
'--color-tertiary-400': '251 157 75',
|
||||
'--color-tertiary-500': '231 129 40',
|
||||
'--color-tertiary-600': '215 117 31',
|
||||
'--color-tertiary-700': '180 98 26',
|
||||
'--color-tertiary-800': '130 73 23',
|
||||
'--color-tertiary-900': '108 61 19',
|
||||
'--color-tertiary-950': '84 49 18',
|
||||
|
||||
/* Error */
|
||||
'--color-error-0': '254 233 233',
|
||||
'--color-error-50': '254 226 226',
|
||||
'--color-error-100': '254 202 202',
|
||||
'--color-error-200': '252 165 165',
|
||||
'--color-error-300': '248 113 113',
|
||||
'--color-error-400': '239 68 68',
|
||||
'--color-error-500': '230 53 53',
|
||||
'--color-error-600': '220 38 38',
|
||||
'--color-error-700': '185 28 28',
|
||||
'--color-error-800': '153 27 27',
|
||||
'--color-error-900': '127 29 29',
|
||||
'--color-error-950': '83 19 19',
|
||||
|
||||
/* Success */
|
||||
'--color-success-0': '228 255 244',
|
||||
'--color-success-50': '202 255 232',
|
||||
'--color-success-100': '162 241 192',
|
||||
'--color-success-200': '132 211 162',
|
||||
'--color-success-300': '102 181 132',
|
||||
'--color-success-400': '72 151 102',
|
||||
'--color-success-500': '52 131 82',
|
||||
'--color-success-600': '42 121 72',
|
||||
'--color-success-700': '32 111 62',
|
||||
'--color-success-800': '22 101 52',
|
||||
'--color-success-900': '20 83 45',
|
||||
'--color-success-950': '27 50 36',
|
||||
|
||||
/* Warning */
|
||||
'--color-warning-0': '255 249 245',
|
||||
'--color-warning-50': '255 244 236',
|
||||
'--color-warning-100': '255 231 213',
|
||||
'--color-warning-200': '254 205 170',
|
||||
'--color-warning-300': '253 173 116',
|
||||
'--color-warning-400': '251 149 75',
|
||||
'--color-warning-500': '231 120 40',
|
||||
'--color-warning-600': '215 108 31',
|
||||
'--color-warning-700': '180 90 26',
|
||||
'--color-warning-800': '130 68 23',
|
||||
'--color-warning-900': '108 56 19',
|
||||
'--color-warning-950': '84 45 18',
|
||||
|
||||
/* Info */
|
||||
'--color-info-0': '236 248 254',
|
||||
'--color-info-50': '199 235 252',
|
||||
'--color-info-100': '162 221 250',
|
||||
'--color-info-200': '124 207 248',
|
||||
'--color-info-300': '87 194 246',
|
||||
'--color-info-400': '50 180 244',
|
||||
'--color-info-500': '13 166 242',
|
||||
'--color-info-600': '11 141 205',
|
||||
'--color-info-700': '9 115 168',
|
||||
'--color-info-800': '7 90 131',
|
||||
'--color-info-900': '5 64 93',
|
||||
'--color-info-950': '3 38 56',
|
||||
|
||||
/* Typography */
|
||||
'--color-typography-0': '254 254 255',
|
||||
'--color-typography-50': '245 245 245',
|
||||
'--color-typography-100': '229 229 229',
|
||||
'--color-typography-200': '219 219 220',
|
||||
'--color-typography-300': '212 212 212',
|
||||
'--color-typography-400': '163 163 163',
|
||||
'--color-typography-500': '140 140 140',
|
||||
'--color-typography-600': '115 115 115',
|
||||
'--color-typography-700': '82 82 82',
|
||||
'--color-typography-800': '64 64 64',
|
||||
'--color-typography-900': '38 38 39',
|
||||
'--color-typography-950': '23 23 23',
|
||||
|
||||
/* Outline */
|
||||
'--color-outline-0': '253 254 254',
|
||||
'--color-outline-50': '243 243 243',
|
||||
'--color-outline-100': '230 230 230',
|
||||
'--color-outline-200': '221 220 219',
|
||||
'--color-outline-300': '211 211 211',
|
||||
'--color-outline-400': '165 163 163',
|
||||
'--color-outline-500': '140 141 141',
|
||||
'--color-outline-600': '115 116 116',
|
||||
'--color-outline-700': '83 82 82',
|
||||
'--color-outline-800': '65 65 65',
|
||||
'--color-outline-900': '39 38 36',
|
||||
'--color-outline-950': '26 23 23',
|
||||
|
||||
/* Background */
|
||||
'--color-background-0': '255 255 255',
|
||||
'--color-background-50': '246 246 246',
|
||||
'--color-background-100': '242 241 241',
|
||||
'--color-background-200': '220 219 219',
|
||||
'--color-background-300': '213 212 212',
|
||||
'--color-background-400': '162 163 163',
|
||||
'--color-background-500': '142 142 142',
|
||||
'--color-background-600': '116 116 116',
|
||||
'--color-background-700': '83 82 82',
|
||||
'--color-background-800': '65 64 64',
|
||||
'--color-background-900': '39 38 37',
|
||||
'--color-background-950': '18 18 18',
|
||||
|
||||
/* Background Special */
|
||||
'--color-background-error': '254 241 241',
|
||||
'--color-background-warning': '255 243 234',
|
||||
'--color-background-success': '237 252 242',
|
||||
'--color-background-muted': '247 248 247',
|
||||
'--color-background-info': '235 248 254',
|
||||
|
||||
/* Focus Ring Indicator */
|
||||
'--color-indicator-primary': '55 55 55',
|
||||
'--color-indicator-info': '83 153 236',
|
||||
'--color-indicator-error': '185 28 28',
|
||||
}),
|
||||
dark: vars({
|
||||
'--color-primary-0': '166 166 166',
|
||||
'--color-primary-50': '175 175 175',
|
||||
'--color-primary-100': '186 186 186',
|
||||
'--color-primary-200': '197 197 197',
|
||||
'--color-primary-300': '212 212 212',
|
||||
'--color-primary-400': '221 221 221',
|
||||
'--color-primary-500': '230 230 230',
|
||||
'--color-primary-600': '240 240 240',
|
||||
'--color-primary-700': '250 250 250',
|
||||
'--color-primary-800': '253 253 253',
|
||||
'--color-primary-900': '254 249 249',
|
||||
'--color-primary-950': '253 252 252',
|
||||
|
||||
/* Secondary */
|
||||
'--color-secondary-0': '20 20 20',
|
||||
'--color-secondary-50': '23 23 23',
|
||||
'--color-secondary-100': '31 31 31',
|
||||
'--color-secondary-200': '39 39 39',
|
||||
'--color-secondary-300': '44 44 44',
|
||||
'--color-secondary-400': '56 57 57',
|
||||
'--color-secondary-500': '63 64 64',
|
||||
'--color-secondary-600': '86 86 86',
|
||||
'--color-secondary-700': '110 110 110',
|
||||
'--color-secondary-800': '135 135 135',
|
||||
'--color-secondary-900': '150 150 150',
|
||||
'--color-secondary-950': '164 164 164',
|
||||
|
||||
/* Tertiary */
|
||||
'--color-tertiary-0': '84 49 18',
|
||||
'--color-tertiary-50': '108 61 19',
|
||||
'--color-tertiary-100': '130 73 23',
|
||||
'--color-tertiary-200': '180 98 26',
|
||||
'--color-tertiary-300': '215 117 31',
|
||||
'--color-tertiary-400': '231 129 40',
|
||||
'--color-tertiary-500': '251 157 75',
|
||||
'--color-tertiary-600': '253 180 116',
|
||||
'--color-tertiary-700': '254 209 170',
|
||||
'--color-tertiary-800': '255 233 213',
|
||||
'--color-tertiary-900': '255 242 229',
|
||||
'--color-tertiary-950': '255 250 245',
|
||||
|
||||
/* Error */
|
||||
'--color-error-0': '83 19 19',
|
||||
'--color-error-50': '127 29 29',
|
||||
'--color-error-100': '153 27 27',
|
||||
'--color-error-200': '185 28 28',
|
||||
'--color-error-300': '220 38 38',
|
||||
'--color-error-400': '230 53 53',
|
||||
'--color-error-500': '239 68 68',
|
||||
'--color-error-600': '249 97 96',
|
||||
'--color-error-700': '229 91 90',
|
||||
'--color-error-800': '254 202 202',
|
||||
'--color-error-900': '254 226 226',
|
||||
'--color-error-950': '254 233 233',
|
||||
|
||||
/* Success */
|
||||
'--color-success-0': '27 50 36',
|
||||
'--color-success-50': '20 83 45',
|
||||
'--color-success-100': '22 101 52',
|
||||
'--color-success-200': '32 111 62',
|
||||
'--color-success-300': '42 121 72',
|
||||
'--color-success-400': '52 131 82',
|
||||
'--color-success-500': '72 151 102',
|
||||
'--color-success-600': '102 181 132',
|
||||
'--color-success-700': '132 211 162',
|
||||
'--color-success-800': '162 241 192',
|
||||
'--color-success-900': '202 255 232',
|
||||
'--color-success-950': '228 255 244',
|
||||
|
||||
/* Warning */
|
||||
'--color-warning-0': '84 45 18',
|
||||
'--color-warning-50': '108 56 19',
|
||||
'--color-warning-100': '130 68 23',
|
||||
'--color-warning-200': '180 90 26',
|
||||
'--color-warning-300': '215 108 31',
|
||||
'--color-warning-400': '231 120 40',
|
||||
'--color-warning-500': '251 149 75',
|
||||
'--color-warning-600': '253 173 116',
|
||||
'--color-warning-700': '254 205 170',
|
||||
'--color-warning-800': '255 231 213',
|
||||
'--color-warning-900': '255 244 237',
|
||||
'--color-warning-950': '255 249 245',
|
||||
|
||||
/* Info */
|
||||
'--color-info-0': '3 38 56',
|
||||
'--color-info-50': '5 64 93',
|
||||
'--color-info-100': '7 90 131',
|
||||
'--color-info-200': '9 115 168',
|
||||
'--color-info-300': '11 141 205',
|
||||
'--color-info-400': '13 166 242',
|
||||
'--color-info-500': '50 180 244',
|
||||
'--color-info-600': '87 194 246',
|
||||
'--color-info-700': '124 207 248',
|
||||
'--color-info-800': '162 221 250',
|
||||
'--color-info-900': '199 235 252',
|
||||
'--color-info-950': '236 248 254',
|
||||
|
||||
/* Typography */
|
||||
'--color-typography-0': '23 23 23',
|
||||
'--color-typography-50': '38 38 39',
|
||||
'--color-typography-100': '64 64 64',
|
||||
'--color-typography-200': '82 82 82',
|
||||
'--color-typography-300': '115 115 115',
|
||||
'--color-typography-400': '140 140 140',
|
||||
'--color-typography-500': '163 163 163',
|
||||
'--color-typography-600': '212 212 212',
|
||||
'--color-typography-700': '219 219 220',
|
||||
'--color-typography-800': '229 229 229',
|
||||
'--color-typography-900': '245 245 245',
|
||||
'--color-typography-950': '254 254 255',
|
||||
|
||||
/* Outline */
|
||||
'--color-outline-0': '26 23 23',
|
||||
'--color-outline-50': '39 38 36',
|
||||
'--color-outline-100': '65 65 65',
|
||||
'--color-outline-200': '83 82 82',
|
||||
'--color-outline-300': '115 116 116',
|
||||
'--color-outline-400': '140 141 141',
|
||||
'--color-outline-500': '165 163 163',
|
||||
'--color-outline-600': '211 211 211',
|
||||
'--color-outline-700': '221 220 219',
|
||||
'--color-outline-800': '230 230 230',
|
||||
'--color-outline-900': '243 243 243',
|
||||
'--color-outline-950': '253 254 254',
|
||||
|
||||
/* Background */
|
||||
'--color-background-0': '18 18 18',
|
||||
'--color-background-50': '39 38 37',
|
||||
'--color-background-100': '65 64 64',
|
||||
'--color-background-200': '83 82 82',
|
||||
'--color-background-300': '116 116 116',
|
||||
'--color-background-400': '142 142 142',
|
||||
'--color-background-500': '162 163 163',
|
||||
'--color-background-600': '213 212 212',
|
||||
'--color-background-700': '229 228 228',
|
||||
'--color-background-800': '242 241 241',
|
||||
'--color-background-900': '246 246 246',
|
||||
'--color-background-950': '255 255 255',
|
||||
|
||||
/* Background Special */
|
||||
'--color-background-error': '66 43 43',
|
||||
'--color-background-warning': '65 47 35',
|
||||
'--color-background-success': '28 43 33',
|
||||
'--color-background-muted': '51 51 51',
|
||||
'--color-background-info': '26 40 46',
|
||||
|
||||
/* Focus Ring Indicator */
|
||||
'--color-indicator-primary': '247 247 247',
|
||||
'--color-indicator-info': '161 199 245',
|
||||
'--color-indicator-error': '232 70 69',
|
||||
}),
|
||||
};
|
||||
87
components/ui/gluestack-ui-provider/index.next15.tsx
Normal file
87
components/ui/gluestack-ui-provider/index.next15.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
// This is a Next.js 15 compatible version of the GluestackUIProvider
|
||||
'use client';
|
||||
import React, { useEffect, useLayoutEffect } from 'react';
|
||||
import { config } from './config';
|
||||
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import { script } from './script';
|
||||
|
||||
const variableStyleTagId = 'nativewind-style';
|
||||
const createStyle = (styleTagId: string) => {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleTagId;
|
||||
style.appendChild(document.createTextNode(''));
|
||||
return style;
|
||||
};
|
||||
|
||||
export const useSafeLayoutEffect =
|
||||
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
export function GluestackUIProvider({
|
||||
mode = 'light',
|
||||
...props
|
||||
}: {
|
||||
mode?: 'light' | 'dark' | 'system';
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
let cssVariablesWithMode = ``;
|
||||
Object.keys(config).forEach((configKey) => {
|
||||
cssVariablesWithMode +=
|
||||
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
|
||||
const cssVariables = Object.keys(
|
||||
config[configKey as keyof typeof config]
|
||||
).reduce((acc: string, curr: string) => {
|
||||
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
|
||||
return acc;
|
||||
}, '');
|
||||
cssVariablesWithMode += `${cssVariables} \n}`;
|
||||
});
|
||||
|
||||
setFlushStyles(cssVariablesWithMode);
|
||||
|
||||
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
|
||||
script(e.matches ? 'dark' : 'light');
|
||||
}, []);
|
||||
|
||||
useSafeLayoutEffect(() => {
|
||||
if (mode !== 'system') {
|
||||
const documentElement = document.documentElement;
|
||||
if (documentElement) {
|
||||
documentElement.classList.add(mode);
|
||||
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
|
||||
documentElement.style.colorScheme = mode;
|
||||
}
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
useSafeLayoutEffect(() => {
|
||||
if (mode !== 'system') return;
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
media.addListener(handleMediaQuery);
|
||||
|
||||
return () => media.removeListener(handleMediaQuery);
|
||||
}, [handleMediaQuery]);
|
||||
|
||||
useSafeLayoutEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const documentElement = document.documentElement;
|
||||
if (documentElement) {
|
||||
const head = documentElement.querySelector('head');
|
||||
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
|
||||
if (!style) {
|
||||
style = createStyle(variableStyleTagId);
|
||||
style.innerHTML = cssVariablesWithMode;
|
||||
if (head) head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OverlayProvider>
|
||||
<ToastProvider>{props.children}</ToastProvider>
|
||||
</OverlayProvider>
|
||||
);
|
||||
}
|
||||
38
components/ui/gluestack-ui-provider/index.tsx
Normal file
38
components/ui/gluestack-ui-provider/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { config } from './config';
|
||||
import { View, ViewProps } from 'react-native';
|
||||
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||
import { useColorScheme } from 'nativewind';
|
||||
|
||||
export type ModeType = 'light' | 'dark' | 'system';
|
||||
|
||||
export function GluestackUIProvider({
|
||||
mode = 'light',
|
||||
...props
|
||||
}: {
|
||||
mode?: ModeType;
|
||||
children?: React.ReactNode;
|
||||
style?: ViewProps['style'];
|
||||
}) {
|
||||
const { colorScheme, setColorScheme } = useColorScheme();
|
||||
|
||||
useEffect(() => {
|
||||
setColorScheme(mode);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
config[colorScheme!],
|
||||
{ flex: 1, height: '100%', width: '100%' },
|
||||
props.style,
|
||||
]}
|
||||
>
|
||||
<OverlayProvider>
|
||||
<ToastProvider>{props.children}</ToastProvider>
|
||||
</OverlayProvider>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
96
components/ui/gluestack-ui-provider/index.web.tsx
Normal file
96
components/ui/gluestack-ui-provider/index.web.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
import React, { useEffect, useLayoutEffect } from 'react';
|
||||
import { config } from './config';
|
||||
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
|
||||
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
|
||||
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import { script } from './script';
|
||||
|
||||
export type ModeType = 'light' | 'dark' | 'system';
|
||||
|
||||
const variableStyleTagId = 'nativewind-style';
|
||||
const createStyle = (styleTagId: string) => {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleTagId;
|
||||
style.appendChild(document.createTextNode(''));
|
||||
return style;
|
||||
};
|
||||
|
||||
export const useSafeLayoutEffect =
|
||||
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
export function GluestackUIProvider({
|
||||
mode = 'light',
|
||||
...props
|
||||
}: {
|
||||
mode?: ModeType;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
let cssVariablesWithMode = ``;
|
||||
Object.keys(config).forEach((configKey) => {
|
||||
cssVariablesWithMode +=
|
||||
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
|
||||
const cssVariables = Object.keys(
|
||||
config[configKey as keyof typeof config]
|
||||
).reduce((acc: string, curr: string) => {
|
||||
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
|
||||
return acc;
|
||||
}, '');
|
||||
cssVariablesWithMode += `${cssVariables} \n}`;
|
||||
});
|
||||
|
||||
setFlushStyles(cssVariablesWithMode);
|
||||
|
||||
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
|
||||
script(e.matches ? 'dark' : 'light');
|
||||
}, []);
|
||||
|
||||
useSafeLayoutEffect(() => {
|
||||
if (mode !== 'system') {
|
||||
const documentElement = document.documentElement;
|
||||
if (documentElement) {
|
||||
documentElement.classList.add(mode);
|
||||
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
|
||||
documentElement.style.colorScheme = mode;
|
||||
}
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
useSafeLayoutEffect(() => {
|
||||
if (mode !== 'system') return;
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
media.addListener(handleMediaQuery);
|
||||
|
||||
return () => media.removeListener(handleMediaQuery);
|
||||
}, [handleMediaQuery]);
|
||||
|
||||
useSafeLayoutEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const documentElement = document.documentElement;
|
||||
if (documentElement) {
|
||||
const head = documentElement.querySelector('head');
|
||||
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
|
||||
if (!style) {
|
||||
style = createStyle(variableStyleTagId);
|
||||
style.innerHTML = cssVariablesWithMode;
|
||||
if (head) head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(${script.toString()})('${mode}')`,
|
||||
}}
|
||||
/>
|
||||
<OverlayProvider>
|
||||
<ToastProvider>{props.children}</ToastProvider>
|
||||
</OverlayProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
components/ui/gluestack-ui-provider/script.ts
Normal file
19
components/ui/gluestack-ui-provider/script.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const script = (mode: string) => {
|
||||
const documentElement = document.documentElement;
|
||||
|
||||
function getSystemColorMode() {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
}
|
||||
|
||||
try {
|
||||
const isSystem = mode === 'system';
|
||||
const theme = isSystem ? getSystemColorMode() : mode;
|
||||
documentElement.classList.remove(theme === 'light' ? 'dark' : 'light');
|
||||
documentElement.classList.add(theme);
|
||||
documentElement.style.colorScheme = theme;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
1588
components/ui/icon/index.tsx
Normal file
1588
components/ui/icon/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1573
components/ui/icon/index.web.tsx
Normal file
1573
components/ui/icon/index.web.tsx
Normal file
File diff suppressed because it is too large
Load Diff
277
components/ui/select/index.tsx
Normal file
277
components/ui/select/index.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||
import {
|
||||
withStyleContext,
|
||||
useStyleContext,
|
||||
} from '@gluestack-ui/utils/nativewind-utils';
|
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import { createSelect } from '@gluestack-ui/core/select/creator';
|
||||
import { cssInterop } from 'nativewind';
|
||||
import {
|
||||
Actionsheet,
|
||||
ActionsheetContent,
|
||||
ActionsheetItem,
|
||||
ActionsheetItemText,
|
||||
ActionsheetDragIndicator,
|
||||
ActionsheetDragIndicatorWrapper,
|
||||
ActionsheetBackdrop,
|
||||
ActionsheetScrollView,
|
||||
ActionsheetVirtualizedList,
|
||||
ActionsheetFlatList,
|
||||
ActionsheetSectionList,
|
||||
ActionsheetSectionHeaderText,
|
||||
} from './select-actionsheet';
|
||||
import { Pressable, View, TextInput } from 'react-native';
|
||||
|
||||
const SelectTriggerWrapper = React.forwardRef<
|
||||
React.ComponentRef<typeof Pressable>,
|
||||
React.ComponentProps<typeof Pressable>
|
||||
>(function SelectTriggerWrapper({ ...props }, ref) {
|
||||
return <Pressable {...props} ref={ref} />;
|
||||
});
|
||||
|
||||
const selectIconStyle = tva({
|
||||
base: 'text-background-500 fill-none',
|
||||
parentVariants: {
|
||||
size: {
|
||||
'2xs': 'h-3 w-3',
|
||||
'xs': 'h-3.5 w-3.5',
|
||||
'sm': 'h-4 w-4',
|
||||
'md': 'h-[18px] w-[18px]',
|
||||
'lg': 'h-5 w-5',
|
||||
'xl': 'h-6 w-6',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const selectStyle = tva({
|
||||
base: '',
|
||||
});
|
||||
|
||||
const selectTriggerStyle = tva({
|
||||
base: 'border border-background-300 rounded flex-row items-center overflow-hidden data-[hover=true]:border-outline-400 data-[focus=true]:border-primary-700 data-[disabled=true]:opacity-40 data-[disabled=true]:data-[hover=true]:border-background-300',
|
||||
variants: {
|
||||
size: {
|
||||
xl: 'h-12',
|
||||
lg: 'h-11',
|
||||
md: 'h-10',
|
||||
sm: 'h-9',
|
||||
},
|
||||
variant: {
|
||||
underlined:
|
||||
'border-0 border-b rounded-none data-[hover=true]:border-primary-700 data-[focus=true]:border-primary-700 data-[focus=true]:web:shadow-[inset_0_-1px_0_0] data-[focus=true]:web:shadow-primary-700 data-[invalid=true]:border-error-700 data-[invalid=true]:web:shadow-error-700',
|
||||
outline:
|
||||
'data-[focus=true]:border-primary-700 data-[focus=true]:web:shadow-[inset_0_0_0_1px] data-[focus=true]:data-[hover=true]:web:shadow-primary-600 data-[invalid=true]:web:shadow-[inset_0_0_0_1px] data-[invalid=true]:border-error-700 data-[invalid=true]:web:shadow-error-700 data-[invalid=true]:data-[hover=true]:border-error-700',
|
||||
rounded:
|
||||
'rounded-full data-[focus=true]:border-primary-700 data-[focus=true]:web:shadow-[inset_0_0_0_1px] data-[focus=true]:web:shadow-primary-700 data-[invalid=true]:border-error-700 data-[invalid=true]:web:shadow-error-700',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const selectInputStyle = tva({
|
||||
base: 'px-3 placeholder:text-typography-500 web:w-full h-full text-typography-900 pointer-events-none web:outline-none ios:leading-[0px] py-0',
|
||||
parentVariants: {
|
||||
size: {
|
||||
xl: 'text-xl',
|
||||
lg: 'text-lg',
|
||||
md: 'text-base',
|
||||
sm: 'text-sm',
|
||||
},
|
||||
variant: {
|
||||
underlined: 'px-0',
|
||||
outline: '',
|
||||
rounded: 'px-4',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const UISelect = createSelect(
|
||||
{
|
||||
Root: View,
|
||||
Trigger: withStyleContext(SelectTriggerWrapper),
|
||||
Input: TextInput,
|
||||
Icon: UIIcon,
|
||||
},
|
||||
{
|
||||
Portal: Actionsheet,
|
||||
Backdrop: ActionsheetBackdrop,
|
||||
Content: ActionsheetContent,
|
||||
DragIndicator: ActionsheetDragIndicator,
|
||||
DragIndicatorWrapper: ActionsheetDragIndicatorWrapper,
|
||||
Item: ActionsheetItem,
|
||||
ItemText: ActionsheetItemText,
|
||||
ScrollView: ActionsheetScrollView,
|
||||
VirtualizedList: ActionsheetVirtualizedList,
|
||||
FlatList: ActionsheetFlatList,
|
||||
SectionList: ActionsheetSectionList,
|
||||
SectionHeaderText: ActionsheetSectionHeaderText,
|
||||
}
|
||||
);
|
||||
|
||||
cssInterop(UISelect, { className: 'style' });
|
||||
cssInterop(UISelect.Input, {
|
||||
className: { target: 'style', nativeStyleToProp: { textAlign: true } },
|
||||
});
|
||||
cssInterop(SelectTriggerWrapper, { className: 'style' });
|
||||
|
||||
cssInterop(PrimitiveIcon, {
|
||||
className: {
|
||||
target: 'style',
|
||||
nativeStyleToProp: {
|
||||
height: true,
|
||||
width: true,
|
||||
fill: true,
|
||||
color: 'classNameColor',
|
||||
stroke: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type ISelectProps = VariantProps<typeof selectStyle> &
|
||||
React.ComponentProps<typeof UISelect> & { className?: string };
|
||||
|
||||
const Select = React.forwardRef<
|
||||
React.ComponentRef<typeof UISelect>,
|
||||
ISelectProps
|
||||
>(function Select({ className, ...props }, ref) {
|
||||
return (
|
||||
<UISelect
|
||||
className={selectStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
type ISelectTriggerProps = VariantProps<typeof selectTriggerStyle> &
|
||||
React.ComponentProps<typeof UISelect.Trigger> & { className?: string };
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof UISelect.Trigger>,
|
||||
ISelectTriggerProps
|
||||
>(function SelectTrigger(
|
||||
{ className, size = 'md', variant = 'outline', ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<UISelect.Trigger
|
||||
className={selectTriggerStyle({
|
||||
class: className,
|
||||
size,
|
||||
variant,
|
||||
})}
|
||||
ref={ref}
|
||||
context={{ size, variant }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
type ISelectInputProps = VariantProps<typeof selectInputStyle> &
|
||||
React.ComponentProps<typeof UISelect.Input> & { className?: string };
|
||||
|
||||
const SelectInput = React.forwardRef<
|
||||
React.ComponentRef<typeof UISelect.Input>,
|
||||
ISelectInputProps
|
||||
>(function SelectInput({ className, ...props }, ref) {
|
||||
const { size: parentSize, variant: parentVariant } = useStyleContext();
|
||||
return (
|
||||
<UISelect.Input
|
||||
className={selectInputStyle({
|
||||
class: className,
|
||||
parentVariants: {
|
||||
size: parentSize,
|
||||
variant: parentVariant,
|
||||
},
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
type ISelectIcon = VariantProps<typeof selectIconStyle> &
|
||||
React.ComponentProps<typeof UISelect.Icon> & { className?: string };
|
||||
|
||||
const SelectIcon = React.forwardRef<
|
||||
React.ComponentRef<typeof UISelect.Icon>,
|
||||
ISelectIcon
|
||||
>(function SelectIcon({ className, size, ...props }, ref) {
|
||||
const { size: parentSize } = useStyleContext();
|
||||
if (typeof size === 'number') {
|
||||
return (
|
||||
<UISelect.Icon
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={selectIconStyle({ class: className })}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
//@ts-expect-error : web only
|
||||
(props?.height !== undefined || props?.width !== undefined) &&
|
||||
size === undefined
|
||||
) {
|
||||
return (
|
||||
<UISelect.Icon
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={selectIconStyle({ class: className })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<UISelect.Icon
|
||||
className={selectIconStyle({
|
||||
class: className,
|
||||
size,
|
||||
parentVariants: {
|
||||
size: parentSize,
|
||||
},
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Select.displayName = 'Select';
|
||||
SelectTrigger.displayName = 'SelectTrigger';
|
||||
SelectInput.displayName = 'SelectInput';
|
||||
SelectIcon.displayName = 'SelectIcon';
|
||||
|
||||
// Actionsheet Components
|
||||
const SelectPortal = UISelect.Portal;
|
||||
const SelectBackdrop = UISelect.Backdrop;
|
||||
const SelectContent = UISelect.Content;
|
||||
const SelectDragIndicator = UISelect.DragIndicator;
|
||||
const SelectDragIndicatorWrapper = UISelect.DragIndicatorWrapper;
|
||||
const SelectItem = UISelect.Item;
|
||||
const SelectScrollView = UISelect.ScrollView;
|
||||
const SelectVirtualizedList = UISelect.VirtualizedList;
|
||||
const SelectFlatList = UISelect.FlatList;
|
||||
const SelectSectionList = UISelect.SectionList;
|
||||
const SelectSectionHeaderText = UISelect.SectionHeaderText;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectInput,
|
||||
SelectIcon,
|
||||
SelectPortal,
|
||||
SelectBackdrop,
|
||||
SelectContent,
|
||||
SelectDragIndicator,
|
||||
SelectDragIndicatorWrapper,
|
||||
SelectItem,
|
||||
SelectScrollView,
|
||||
SelectVirtualizedList,
|
||||
SelectFlatList,
|
||||
SelectSectionList,
|
||||
SelectSectionHeaderText,
|
||||
};
|
||||
562
components/ui/select/select-actionsheet.tsx
Normal file
562
components/ui/select/select-actionsheet.tsx
Normal file
@@ -0,0 +1,562 @@
|
||||
'use client';
|
||||
|
||||
import { H4 } from '@expo/html-elements';
|
||||
import { createActionsheet } from '@gluestack-ui/core/actionsheet/creator';
|
||||
import {
|
||||
Pressable,
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
VirtualizedList,
|
||||
FlatList,
|
||||
SectionList,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator';
|
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import { cssInterop } from 'nativewind';
|
||||
import {
|
||||
Motion,
|
||||
AnimatePresence,
|
||||
createMotionAnimatedComponent,
|
||||
MotionComponentProps,
|
||||
} from '@legendapp/motion';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable> &
|
||||
MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>;
|
||||
|
||||
const AnimatedPressable = createMotionAnimatedComponent(
|
||||
Pressable
|
||||
) as React.ComponentType<IAnimatedPressableProps>;
|
||||
|
||||
type IMotionViewProps = React.ComponentProps<typeof View> &
|
||||
MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>;
|
||||
|
||||
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>;
|
||||
|
||||
export const UIActionsheet = createActionsheet({
|
||||
Root: View,
|
||||
Content: withStyleContext(MotionView),
|
||||
Item: withStyleContext(Pressable),
|
||||
ItemText: Text,
|
||||
DragIndicator: View,
|
||||
IndicatorWrapper: View,
|
||||
Backdrop: AnimatedPressable,
|
||||
ScrollView: ScrollView,
|
||||
VirtualizedList: VirtualizedList,
|
||||
FlatList: FlatList,
|
||||
SectionList: SectionList,
|
||||
SectionHeaderText: H4,
|
||||
Icon: UIIcon,
|
||||
AnimatePresence: AnimatePresence,
|
||||
});
|
||||
|
||||
cssInterop(UIActionsheet, { className: 'style' });
|
||||
cssInterop(UIActionsheet.Content, { className: 'style' });
|
||||
cssInterop(UIActionsheet.Item, { className: 'style' });
|
||||
cssInterop(UIActionsheet.ItemText, { className: 'style' });
|
||||
cssInterop(UIActionsheet.DragIndicator, { className: 'style' });
|
||||
cssInterop(UIActionsheet.DragIndicatorWrapper, { className: 'style' });
|
||||
cssInterop(UIActionsheet.Backdrop, { className: 'style' });
|
||||
cssInterop(UIActionsheet.ScrollView, {
|
||||
className: 'style',
|
||||
contentContainerClassName: 'contentContainerStyle',
|
||||
indicatorClassName: 'indicatorStyle',
|
||||
});
|
||||
cssInterop(UIActionsheet.VirtualizedList, {
|
||||
className: 'style',
|
||||
ListFooterComponentClassName: 'ListFooterComponentStyle',
|
||||
ListHeaderComponentClassName: 'ListHeaderComponentStyle',
|
||||
contentContainerClassName: 'contentContainerStyle',
|
||||
indicatorClassName: 'indicatorStyle',
|
||||
});
|
||||
cssInterop(UIActionsheet.FlatList, {
|
||||
className: 'style',
|
||||
ListFooterComponentClassName: 'ListFooterComponentStyle',
|
||||
ListHeaderComponentClassName: 'ListHeaderComponentStyle',
|
||||
columnWrapperClassName: 'columnWrapperStyle',
|
||||
contentContainerClassName: 'contentContainerStyle',
|
||||
indicatorClassName: 'indicatorStyle',
|
||||
});
|
||||
cssInterop(UIActionsheet.SectionList, { className: 'style' });
|
||||
cssInterop(UIActionsheet.SectionHeaderText, { className: 'style' });
|
||||
cssInterop(PrimitiveIcon, {
|
||||
className: {
|
||||
target: 'style',
|
||||
nativeStyleToProp: {
|
||||
height: true,
|
||||
width: true,
|
||||
fill: true,
|
||||
color: 'classNameColor',
|
||||
stroke: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const actionsheetStyle = tva({ base: 'w-full h-full web:pointer-events-none' });
|
||||
|
||||
const actionsheetContentStyle = tva({
|
||||
base: 'items-center rounded-tl-3xl rounded-tr-3xl p-2 bg-background-0 web:pointer-events-auto web:select-none shadow-lg pb-safe',
|
||||
});
|
||||
|
||||
const actionsheetItemStyle = tva({
|
||||
base: 'w-full flex-row items-center p-3 rounded-sm data-[disabled=true]:opacity-40 data-[disabled=true]:web:pointer-events-auto data-[disabled=true]:web:cursor-not-allowed hover:bg-background-50 active:bg-background-100 data-[focus=true]:bg-background-100 web:data-[focus-visible=true]:bg-background-100 data-[checked=true]:bg-background-100',
|
||||
});
|
||||
|
||||
const actionsheetItemTextStyle = tva({
|
||||
base: 'text-typography-700 font-normal font-body tracking-md text-left mx-2',
|
||||
variants: {
|
||||
isTruncated: {
|
||||
true: '',
|
||||
},
|
||||
bold: {
|
||||
true: 'font-bold',
|
||||
},
|
||||
underline: {
|
||||
true: 'underline',
|
||||
},
|
||||
strikeThrough: {
|
||||
true: 'line-through',
|
||||
},
|
||||
size: {
|
||||
'2xs': 'text-2xs',
|
||||
'xs': 'text-xs',
|
||||
'sm': 'text-sm',
|
||||
'md': 'text-base',
|
||||
'lg': 'text-lg',
|
||||
'xl': 'text-xl',
|
||||
'2xl': 'text-2xl',
|
||||
'3xl': 'text-3xl',
|
||||
'4xl': 'text-4xl',
|
||||
'5xl': 'text-5xl',
|
||||
'6xl': 'text-6xl',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
},
|
||||
});
|
||||
|
||||
const actionsheetDragIndicatorStyle = tva({
|
||||
base: 'w-16 h-1 bg-background-400 rounded-full',
|
||||
});
|
||||
|
||||
const actionsheetDragIndicatorWrapperStyle = tva({
|
||||
base: 'w-full py-1 items-center',
|
||||
});
|
||||
|
||||
const actionsheetBackdropStyle = tva({
|
||||
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default web:pointer-events-auto',
|
||||
});
|
||||
|
||||
const actionsheetScrollViewStyle = tva({
|
||||
base: 'w-full h-auto',
|
||||
});
|
||||
|
||||
const actionsheetVirtualizedListStyle = tva({
|
||||
base: 'w-full h-auto',
|
||||
});
|
||||
|
||||
const actionsheetFlatListStyle = tva({
|
||||
base: 'w-full h-auto',
|
||||
});
|
||||
|
||||
const actionsheetSectionListStyle = tva({
|
||||
base: 'w-full h-auto',
|
||||
});
|
||||
|
||||
const actionsheetSectionHeaderTextStyle = tva({
|
||||
base: 'leading-5 font-bold font-heading my-0 text-typography-500 p-3 uppercase',
|
||||
variants: {
|
||||
isTruncated: {
|
||||
true: '',
|
||||
},
|
||||
bold: {
|
||||
true: 'font-bold',
|
||||
},
|
||||
underline: {
|
||||
true: 'underline',
|
||||
},
|
||||
strikeThrough: {
|
||||
true: 'line-through',
|
||||
},
|
||||
size: {
|
||||
'5xl': 'text-5xl',
|
||||
'4xl': 'text-4xl',
|
||||
'3xl': 'text-3xl',
|
||||
'2xl': 'text-2xl',
|
||||
'xl': 'text-xl',
|
||||
'lg': 'text-lg',
|
||||
'md': 'text-base',
|
||||
'sm': 'text-sm',
|
||||
'xs': 'text-xs',
|
||||
},
|
||||
|
||||
sub: {
|
||||
true: 'text-xs',
|
||||
},
|
||||
italic: {
|
||||
true: 'italic',
|
||||
},
|
||||
highlight: {
|
||||
true: 'bg-yellow500',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'xs',
|
||||
},
|
||||
});
|
||||
|
||||
const actionsheetIconStyle = tva({
|
||||
base: 'text-typography-900',
|
||||
variants: {
|
||||
size: {
|
||||
'2xs': 'h-3 w-3',
|
||||
'xs': 'h-3.5 w-3.5',
|
||||
'sm': 'h-4 w-4',
|
||||
'md': 'w-4 h-4',
|
||||
'lg': 'h-5 w-5',
|
||||
'xl': 'h-6 w-6',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type IActionsheetProps = VariantProps<typeof actionsheetStyle> &
|
||||
React.ComponentProps<typeof UIActionsheet> & { className?: string };
|
||||
|
||||
type IActionsheetContentProps = VariantProps<typeof actionsheetContentStyle> &
|
||||
React.ComponentProps<typeof UIActionsheet.Content> & { className?: string };
|
||||
|
||||
type IActionsheetItemProps = VariantProps<typeof actionsheetItemStyle> &
|
||||
React.ComponentProps<typeof UIActionsheet.Item> & { className?: string };
|
||||
|
||||
type IActionsheetItemTextProps = VariantProps<typeof actionsheetItemTextStyle> &
|
||||
React.ComponentProps<typeof UIActionsheet.ItemText> & { className?: string };
|
||||
|
||||
type IActionsheetDragIndicatorProps = VariantProps<
|
||||
typeof actionsheetDragIndicatorStyle
|
||||
> &
|
||||
React.ComponentProps<typeof UIActionsheet.DragIndicator> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type IActionsheetDragIndicatorWrapperProps = VariantProps<
|
||||
typeof actionsheetDragIndicatorWrapperStyle
|
||||
> &
|
||||
React.ComponentProps<typeof UIActionsheet.DragIndicatorWrapper> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type IActionsheetBackdropProps = VariantProps<typeof actionsheetBackdropStyle> &
|
||||
React.ComponentProps<typeof UIActionsheet.Backdrop> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type IActionsheetScrollViewProps = VariantProps<
|
||||
typeof actionsheetScrollViewStyle
|
||||
> &
|
||||
React.ComponentProps<typeof UIActionsheet.ScrollView> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type IActionsheetVirtualizedListProps = VariantProps<
|
||||
typeof actionsheetVirtualizedListStyle
|
||||
> &
|
||||
React.ComponentProps<typeof UIActionsheet.VirtualizedList> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type IActionsheetFlatListProps = VariantProps<typeof actionsheetFlatListStyle> &
|
||||
React.ComponentProps<typeof UIActionsheet.FlatList> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type IActionsheetSectionListProps = VariantProps<
|
||||
typeof actionsheetSectionListStyle
|
||||
> &
|
||||
React.ComponentProps<typeof UIActionsheet.SectionList> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type IActionsheetSectionHeaderTextProps = VariantProps<
|
||||
typeof actionsheetSectionHeaderTextStyle
|
||||
> &
|
||||
React.ComponentProps<typeof UIActionsheet.SectionHeaderText> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type IActionsheetIconProps = VariantProps<typeof actionsheetIconStyle> &
|
||||
React.ComponentProps<typeof UIActionsheet.Icon> & {
|
||||
className?: string;
|
||||
as?: React.ElementType;
|
||||
};
|
||||
|
||||
const Actionsheet = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet>,
|
||||
IActionsheetProps
|
||||
>(function Actionsheet({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet
|
||||
className={actionsheetStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetContent = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.Content>,
|
||||
IActionsheetContentProps & { className?: string }
|
||||
>(function ActionsheetContent({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.Content
|
||||
className={actionsheetContentStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetItem = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.Item>,
|
||||
IActionsheetItemProps
|
||||
>(function ActionsheetItem({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.Item
|
||||
className={actionsheetItemStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetItemText = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.ItemText>,
|
||||
IActionsheetItemTextProps
|
||||
>(function ActionsheetItemText(
|
||||
{ className, isTruncated, bold, underline, strikeThrough, size, ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<UIActionsheet.ItemText
|
||||
className={actionsheetItemTextStyle({
|
||||
class: className,
|
||||
isTruncated: isTruncated as boolean,
|
||||
bold: bold as boolean,
|
||||
underline: underline as boolean,
|
||||
strikeThrough: strikeThrough as boolean,
|
||||
size,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetDragIndicator = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.DragIndicator>,
|
||||
IActionsheetDragIndicatorProps
|
||||
>(function ActionsheetDragIndicator({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.DragIndicator
|
||||
className={actionsheetDragIndicatorStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetDragIndicatorWrapper = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.DragIndicatorWrapper>,
|
||||
IActionsheetDragIndicatorWrapperProps
|
||||
>(function ActionsheetDragIndicatorWrapper({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.DragIndicatorWrapper
|
||||
className={actionsheetDragIndicatorWrapperStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetBackdrop = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.Backdrop>,
|
||||
IActionsheetBackdropProps
|
||||
>(function ActionsheetBackdrop({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.Backdrop
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 0.5,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
}}
|
||||
{...props}
|
||||
className={actionsheetBackdropStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetScrollView = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.ScrollView>,
|
||||
IActionsheetScrollViewProps
|
||||
>(function ActionsheetScrollView({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.ScrollView
|
||||
className={actionsheetScrollViewStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetVirtualizedList = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.VirtualizedList>,
|
||||
IActionsheetVirtualizedListProps
|
||||
>(function ActionsheetVirtualizedList({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.VirtualizedList
|
||||
className={actionsheetVirtualizedListStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetFlatList = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.FlatList>,
|
||||
IActionsheetFlatListProps
|
||||
>(function ActionsheetFlatList({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.FlatList
|
||||
className={actionsheetFlatListStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetSectionList = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.SectionList>,
|
||||
IActionsheetSectionListProps
|
||||
>(function ActionsheetSectionList({ className, ...props }, ref) {
|
||||
return (
|
||||
<UIActionsheet.SectionList
|
||||
className={actionsheetSectionListStyle({
|
||||
class: className,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetSectionHeaderText = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.SectionHeaderText>,
|
||||
IActionsheetSectionHeaderTextProps
|
||||
>(function ActionsheetSectionHeaderText(
|
||||
{
|
||||
className,
|
||||
isTruncated,
|
||||
bold,
|
||||
underline,
|
||||
strikeThrough,
|
||||
size,
|
||||
sub,
|
||||
italic,
|
||||
highlight,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<UIActionsheet.SectionHeaderText
|
||||
className={actionsheetSectionHeaderTextStyle({
|
||||
class: className,
|
||||
isTruncated: isTruncated as boolean,
|
||||
bold: bold as boolean,
|
||||
underline: underline as boolean,
|
||||
strikeThrough: strikeThrough as boolean,
|
||||
size,
|
||||
sub: sub as boolean,
|
||||
italic: italic as boolean,
|
||||
highlight: highlight as boolean,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ActionsheetIcon = React.forwardRef<
|
||||
React.ComponentRef<typeof UIActionsheet.Icon>,
|
||||
IActionsheetIconProps
|
||||
>(function ActionsheetIcon(
|
||||
{ className, as: AsComp, size = 'sm', ...props },
|
||||
ref
|
||||
) {
|
||||
if (AsComp) {
|
||||
return (
|
||||
<AsComp
|
||||
className={actionsheetIconStyle({
|
||||
class: className,
|
||||
size,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<UIActionsheet.Icon
|
||||
className={actionsheetIconStyle({
|
||||
class: className,
|
||||
size,
|
||||
})}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export {
|
||||
Actionsheet,
|
||||
ActionsheetContent,
|
||||
ActionsheetItem,
|
||||
ActionsheetItemText,
|
||||
ActionsheetDragIndicator,
|
||||
ActionsheetDragIndicatorWrapper,
|
||||
ActionsheetBackdrop,
|
||||
ActionsheetScrollView,
|
||||
ActionsheetVirtualizedList,
|
||||
ActionsheetFlatList,
|
||||
ActionsheetSectionList,
|
||||
ActionsheetSectionHeaderText,
|
||||
ActionsheetIcon,
|
||||
};
|
||||
Reference in New Issue
Block a user