418 lines
15 KiB
TypeScript
418 lines
15 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
TouchableOpacity,
|
|
View,
|
|
Text,
|
|
Modal,
|
|
FlatList,
|
|
StyleSheet,
|
|
Pressable,
|
|
} from "react-native";
|
|
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
|
import { useNavigation } from "@react-navigation/native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useCart } from "../context/CartContext";
|
|
import { useNotifications } from "../context/NotificationContext";
|
|
import { useAuth } from "../auth/AuthContext";
|
|
import { useTheme } from "../context/ThemeContext";
|
|
import { logoutUser } from "../api/api";
|
|
import type { ClientNotification } from "../api/api";
|
|
import { fontSize, spacing, borderRadius } from "../theme";
|
|
import Toast from "../components/ui/Toast";
|
|
import type { ClientTabParamList, ClientStackParamList } from "./types";
|
|
|
|
import ProductsScreen from "../screens/client/ProductsScreen";
|
|
import CartScreen from "../screens/client/CartScreen";
|
|
import OrderTrackingScreen from "../screens/client/OrderTrackingScreen";
|
|
import OrderHistoryScreen from "../screens/client/OrderHistoryScreen";
|
|
import ProductDetailScreen from "../screens/client/ProductDetailScreen";
|
|
import CheckoutScreen from "../screens/client/CheckoutScreen";
|
|
import OrderDetailsScreen from "../screens/client/OrderDetailsScreen";
|
|
import ParrainageScreen from "../screens/client/ParrainageScreen";
|
|
|
|
const Tab = createBottomTabNavigator<ClientTabParamList>();
|
|
const Stack = createNativeStackNavigator<ClientStackParamList>();
|
|
|
|
function formatNotifDate(dateStr: string): string {
|
|
try {
|
|
const diffMs = Date.now() - new Date(dateStr).getTime();
|
|
const diffMin = Math.floor(diffMs / 60000);
|
|
if (diffMin < 1) return "A l'instant";
|
|
if (diffMin < 60) return `Il y a ${diffMin} min`;
|
|
const diffH = Math.floor(diffMin / 60);
|
|
if (diffH < 24) return `Il y a ${diffH}h`;
|
|
const diffD = Math.floor(diffH / 24);
|
|
if (diffD === 1) return "Hier";
|
|
return `Il y a ${diffD} jours`;
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function ClientTabs() {
|
|
const { cartCount } = useCart();
|
|
const {
|
|
notifications,
|
|
unreadCount,
|
|
markAllRead,
|
|
toast,
|
|
clearToast,
|
|
navigateToOrder,
|
|
clearNavigateToOrder,
|
|
} = useNotifications();
|
|
const { logout } = useAuth();
|
|
const { colors, isDark, toggleTheme } = useTheme();
|
|
const navigation = useNavigation<any>();
|
|
const [notifModalVisible, setNotifModalVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (navigateToOrder) {
|
|
setNotifModalVisible(false);
|
|
navigation.navigate("Tracking");
|
|
clearNavigateToOrder();
|
|
}
|
|
}, [navigateToOrder, navigation, clearNavigateToOrder]);
|
|
|
|
const handleLogout = async () => {
|
|
await logoutUser();
|
|
await logout();
|
|
};
|
|
|
|
const openNotifModal = async () => {
|
|
setNotifModalVisible(true);
|
|
if (unreadCount > 0) {
|
|
await markAllRead();
|
|
}
|
|
};
|
|
|
|
const renderNotifItem = ({ item }: { item: ClientNotification }) => {
|
|
if (!item) return null;
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.notifItem,
|
|
{
|
|
borderBottomColor: colors.border,
|
|
borderLeftColor: item.read
|
|
? "transparent"
|
|
: colors.accent,
|
|
backgroundColor: item.read
|
|
? "transparent"
|
|
: colors.accent + "10",
|
|
},
|
|
]}
|
|
>
|
|
<Text
|
|
style={[styles.notifMessage, { color: colors.textPrimary }]}
|
|
>
|
|
{item.message || "Notification"}
|
|
</Text>
|
|
<Text style={[styles.notifDate, { color: colors.textMuted }]}>
|
|
{item.created_at ? formatNotifDate(item.created_at) : ""}
|
|
</Text>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Tab.Navigator
|
|
screenOptions={{
|
|
headerStyle: { backgroundColor: colors.bgSecondary },
|
|
headerTintColor: colors.textWhite,
|
|
headerRight: () => (
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
marginRight: spacing.l,
|
|
gap: spacing.m,
|
|
}}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={openNotifModal}
|
|
style={{ position: "relative" }}
|
|
>
|
|
<Ionicons
|
|
name="notifications-outline"
|
|
size={24}
|
|
color={colors.textSecondary}
|
|
/>
|
|
{unreadCount > 0 && (
|
|
<View style={styles.badge}>
|
|
<Text style={styles.badgeText}>
|
|
{unreadCount > 9
|
|
? "9+"
|
|
: unreadCount}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={toggleTheme}>
|
|
<Ionicons
|
|
name={
|
|
isDark
|
|
? "sunny-outline"
|
|
: "moon-outline"
|
|
}
|
|
size={22}
|
|
color={colors.textSecondary}
|
|
/>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={handleLogout}>
|
|
<Ionicons
|
|
name="log-out-outline"
|
|
size={24}
|
|
color={colors.textSecondary}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
),
|
|
tabBarStyle: {
|
|
backgroundColor: colors.bgSecondary,
|
|
borderTopColor: colors.border,
|
|
borderTopWidth: 1,
|
|
},
|
|
tabBarActiveTintColor: colors.accent,
|
|
tabBarInactiveTintColor: colors.textMuted,
|
|
tabBarLabelStyle: { fontSize: fontSize.xs },
|
|
}}
|
|
>
|
|
<Tab.Screen
|
|
name="Products"
|
|
component={ProductsScreen}
|
|
options={{
|
|
title: "Produits",
|
|
tabBarIcon: ({ color, size }) => (
|
|
<Ionicons
|
|
name="leaf-outline"
|
|
size={size}
|
|
color={color}
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
<Tab.Screen
|
|
name="Cart"
|
|
component={CartScreen}
|
|
options={{
|
|
title: "Panier",
|
|
tabBarIcon: ({ color, size }) => (
|
|
<Ionicons
|
|
name="cart-outline"
|
|
size={size}
|
|
color={color}
|
|
/>
|
|
),
|
|
tabBarBadge: cartCount > 0 ? cartCount : undefined,
|
|
tabBarBadgeStyle: { backgroundColor: colors.accent },
|
|
}}
|
|
/>
|
|
<Tab.Screen
|
|
name="Tracking"
|
|
component={OrderTrackingScreen}
|
|
options={{
|
|
title: "Suivi",
|
|
tabBarIcon: ({ color, size }) => (
|
|
<Ionicons
|
|
name="navigate-outline"
|
|
size={size}
|
|
color={color}
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
<Tab.Screen
|
|
name="History"
|
|
component={OrderHistoryScreen}
|
|
options={{
|
|
title: "Historique",
|
|
tabBarIcon: ({ color, size }) => (
|
|
<Ionicons
|
|
name="time-outline"
|
|
size={size}
|
|
color={color}
|
|
/>
|
|
),
|
|
}}
|
|
/>
|
|
</Tab.Navigator>
|
|
|
|
<Modal
|
|
visible={notifModalVisible}
|
|
animationType="slide"
|
|
transparent={true}
|
|
onRequestClose={() => setNotifModalVisible(false)}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.modalOverlay,
|
|
{ backgroundColor: "rgba(0,0,0,0.5)" },
|
|
]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.modalContent,
|
|
{ backgroundColor: colors.bgPrimary },
|
|
]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.modalHeader,
|
|
{ borderBottomColor: colors.border },
|
|
]}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.modalTitle,
|
|
{ color: colors.textPrimary },
|
|
]}
|
|
>
|
|
Notifications
|
|
</Text>
|
|
<Pressable
|
|
onPress={() => setNotifModalVisible(false)}
|
|
>
|
|
<Ionicons
|
|
name="close"
|
|
size={24}
|
|
color={colors.textSecondary}
|
|
/>
|
|
</Pressable>
|
|
</View>
|
|
<FlatList
|
|
data={notifications || []}
|
|
keyExtractor={(item, index) => {
|
|
if (
|
|
item?.command_id &&
|
|
item?.type &&
|
|
item?.created_at
|
|
) {
|
|
return `${item.command_id}-${item.type}-${item.created_at}`;
|
|
}
|
|
return `notif-${index}`;
|
|
}}
|
|
renderItem={renderNotifItem}
|
|
ListEmptyComponent={
|
|
<Text
|
|
style={[
|
|
styles.emptyText,
|
|
{ color: colors.textMuted },
|
|
]}
|
|
>
|
|
Aucune notification
|
|
</Text>
|
|
}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
<Toast
|
|
message={toast?.message || ""}
|
|
type={toast?.type || "info"}
|
|
visible={!!toast}
|
|
onHide={clearToast}
|
|
duration={5000}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function ClientNavigator() {
|
|
const { colors } = useTheme();
|
|
|
|
return (
|
|
<Stack.Navigator
|
|
screenOptions={{
|
|
headerStyle: { backgroundColor: colors.bgSecondary },
|
|
headerTintColor: colors.textWhite,
|
|
}}
|
|
>
|
|
<Stack.Screen
|
|
name="ClientTabs"
|
|
component={ClientTabs}
|
|
options={{ headerShown: false }}
|
|
/>
|
|
<Stack.Screen
|
|
name="ProductDetail"
|
|
component={ProductDetailScreen}
|
|
options={{ title: "Detail produit" }}
|
|
/>
|
|
<Stack.Screen
|
|
name="Checkout"
|
|
component={CheckoutScreen}
|
|
options={{ title: "Validation commande" }}
|
|
/>
|
|
<Stack.Screen
|
|
name="OrderDetails"
|
|
component={OrderDetailsScreen}
|
|
options={{ title: "Detail commande" }}
|
|
/>
|
|
<Stack.Screen
|
|
name="Parrainage"
|
|
component={ParrainageScreen}
|
|
options={{ title: "Parrainage" }}
|
|
/>
|
|
</Stack.Navigator>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
notifItem: {
|
|
padding: spacing.m,
|
|
borderBottomWidth: 1,
|
|
borderLeftWidth: 3,
|
|
},
|
|
notifMessage: {
|
|
fontSize: fontSize.sm,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
notifDate: {
|
|
fontSize: fontSize.xs,
|
|
},
|
|
badge: {
|
|
position: "absolute",
|
|
top: -4,
|
|
right: -4,
|
|
backgroundColor: "#FF3B30",
|
|
borderRadius: 10,
|
|
minWidth: 18,
|
|
height: 18,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
badgeText: {
|
|
color: "#fff",
|
|
fontSize: 10,
|
|
fontWeight: "bold",
|
|
},
|
|
modalOverlay: {
|
|
flex: 1,
|
|
justifyContent: "flex-end",
|
|
},
|
|
modalContent: {
|
|
height: "70%",
|
|
borderTopLeftRadius: borderRadius.lg,
|
|
borderTopRightRadius: borderRadius.lg,
|
|
},
|
|
modalHeader: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
padding: spacing.l,
|
|
borderBottomWidth: 1,
|
|
},
|
|
modalTitle: {
|
|
fontSize: fontSize.lg,
|
|
fontWeight: "bold",
|
|
},
|
|
emptyText: {
|
|
textAlign: "center",
|
|
marginTop: spacing.xl,
|
|
fontSize: fontSize.sm,
|
|
},
|
|
});
|