Initial commit

This commit is contained in:
Jonathan Atta
2026-03-03 10:33:56 +01:00
commit da373199e0
139 changed files with 26421 additions and 0 deletions

View File

@@ -0,0 +1,278 @@
import React, { useEffect, useRef, useState } from 'react';
import {
View,
Text,
TouchableOpacity,
FlatList,
StyleSheet,
Animated,
Dimensions,
Pressable,
} from 'react-native';
import type { Conversation } from '../../store/chatTypes';
import { colors, spacing, typography, borderRadius } from '../../theme/lightTheme';
const DRAWER_WIDTH = Math.min(Dimensions.get('window').width * 0.80, 310);
interface Props {
visible: boolean;
conversations: Conversation[];
activeConversationId: string | null;
onClose: () => void;
onNewChat: () => void;
onSelectConversation: (id: string) => void;
onDeleteConversation: (id: string) => void;
}
function formatDate(ts: number): string {
const diffDays = Math.floor((Date.now() - ts) / 86400000);
if (diffDays === 0) return "Aujourd'hui";
if (diffDays === 1) return 'Hier';
if (diffDays < 7) return `Il y a ${diffDays} j`;
return new Date(ts).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
}
export default function ChatDrawer({
visible,
conversations,
activeConversationId,
onClose,
onNewChat,
onSelectConversation,
onDeleteConversation,
}: Props) {
const translateX = useRef(new Animated.Value(-DRAWER_WIDTH)).current;
const backdropOpacity = useRef(new Animated.Value(0)).current;
// Keep mounted during the close animation
const [mounted, setMounted] = useState(false);
useEffect(() => {
if (visible) {
setMounted(true);
Animated.parallel([
Animated.timing(translateX, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}),
Animated.timing(backdropOpacity, {
toValue: 0.5,
duration: 250,
useNativeDriver: true,
}),
]).start();
} else {
Animated.parallel([
Animated.timing(translateX, {
toValue: -DRAWER_WIDTH,
duration: 210,
useNativeDriver: true,
}),
Animated.timing(backdropOpacity, {
toValue: 0,
duration: 210,
useNativeDriver: true,
}),
]).start(() => setMounted(false));
}
}, [visible, translateX, backdropOpacity]);
if (!mounted) return null;
const sorted = [...conversations].sort((a, b) => b.updatedAt - a.updatedAt);
const renderItem = ({ item }: { item: Conversation }) => {
const isActive = item.id === activeConversationId;
const lastMsg = item.messages[item.messages.length - 1];
return (
<TouchableOpacity
style={[styles.convItem, isActive && styles.convItemActive]}
onPress={() => { onSelectConversation(item.id); onClose(); }}
activeOpacity={0.75}
>
<View style={styles.convItemContent}>
<Text
style={[styles.convTitle, isActive && styles.convTitleActive]}
numberOfLines={1}
>
{item.title}
</Text>
<Text style={styles.convMeta}>
{item.messages.length} msg · {formatDate(item.updatedAt)}
</Text>
{lastMsg ? (
<Text style={styles.convPreview} numberOfLines={1}>
{lastMsg.role === 'user' ? '👤 ' : '🤖 '}
{lastMsg.content}
</Text>
) : (
<Text style={styles.convPreviewEmpty}>Vide</Text>
)}
</View>
<TouchableOpacity
style={styles.deleteBtn}
onPress={() => onDeleteConversation(item.id)}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Text style={styles.deleteBtnText}></Text>
</TouchableOpacity>
</TouchableOpacity>
);
};
return (
<View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
{/* Backdrop */}
<Animated.View
style={[styles.backdrop, { opacity: backdropOpacity }]}
pointerEvents={visible ? 'auto' : 'none'}
>
<Pressable style={StyleSheet.absoluteFillObject} onPress={onClose} />
</Animated.View>
{/* Drawer panel */}
<Animated.View style={[styles.drawer, { transform: [{ translateX }] }]}>
{/* Header */}
<View style={styles.drawerHeader}>
<Text style={styles.drawerTitle}>Conversations</Text>
<TouchableOpacity
onPress={onClose}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Text style={styles.closeBtnText}></Text>
</TouchableOpacity>
</View>
{/* New chat button */}
<TouchableOpacity
style={styles.newChatBtn}
onPress={() => { onNewChat(); onClose(); }}
activeOpacity={0.85}
>
<Text style={styles.newChatBtnText}> Nouveau chat</Text>
</TouchableOpacity>
{/* Conversations list */}
<FlatList
data={sorted}
keyExtractor={item => item.id}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ListEmptyComponent={
<Text style={styles.emptyText}>Aucune conversation</Text>
}
/>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: colors.textPrimary,
},
drawer: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: DRAWER_WIDTH,
backgroundColor: colors.surface,
shadowColor: colors.textPrimary,
shadowOffset: { width: 4, height: 0 },
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 16,
},
drawerHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.lg,
paddingTop: spacing.xxl,
paddingBottom: spacing.md,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.border,
},
drawerTitle: {
fontSize: typography.sizes.lg,
fontWeight: typography.weights.semibold,
color: colors.textPrimary,
},
closeBtnText: {
fontSize: 18,
color: colors.textSecondary,
},
newChatBtn: {
margin: spacing.md,
paddingVertical: spacing.md,
backgroundColor: colors.primary,
borderRadius: borderRadius.lg,
alignItems: 'center',
},
newChatBtnText: {
color: colors.surface,
fontSize: typography.sizes.md,
fontWeight: typography.weights.semibold,
},
listContent: {
paddingHorizontal: spacing.sm,
paddingBottom: spacing.xxl,
},
convItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
borderRadius: borderRadius.lg,
marginBottom: 2,
},
convItemActive: {
backgroundColor: colors.surfaceSecondary,
borderLeftWidth: 3,
borderLeftColor: colors.primary,
},
convItemContent: {
flex: 1,
marginRight: spacing.sm,
},
convTitle: {
fontSize: typography.sizes.sm,
fontWeight: typography.weights.medium,
color: colors.textPrimary,
marginBottom: 2,
},
convTitleActive: {
color: colors.primary,
fontWeight: typography.weights.semibold,
},
convMeta: {
fontSize: 11,
color: colors.textTertiary,
marginBottom: 2,
},
convPreview: {
fontSize: 12,
color: colors.textSecondary,
},
convPreviewEmpty: {
fontSize: 12,
color: colors.textTertiary,
fontStyle: 'italic',
},
deleteBtn: {
padding: 4,
},
deleteBtnText: {
fontSize: 14,
color: colors.textTertiary,
},
emptyText: {
textAlign: 'center',
color: colors.textTertiary,
marginTop: spacing.xl,
fontSize: typography.sizes.sm,
},
});