feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,244 @@
// Smart App City — AI Chat Screen
import React, { useState, useRef, useEffect } from 'react';
import {
View, Text, TextInput, TouchableOpacity,
ScrollView, StyleSheet, KeyboardAvoidingView, Platform,
} from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
type Props = { navigation: NativeStackNavigationProp<any> };
interface Message {
id: string;
text: string;
isBot: boolean;
timestamp: Date;
}
const QUICK_PROMPTS = [
{ id: '1', text: '🌤️ Météo aujourd\'hui', prompt: 'Quelle est la météo à Martinique aujourd\'hui ?' },
{ id: '2', text: '🚌 Prochain bus', prompt: 'Quel est le prochain bus pour Fort-de-France ?' },
{ id: '3', text: '⚡ Consommation énergie', prompt: 'Quelle est ma consommation d\'énergie cette semaine ?' },
{ id: '4', text: '🚨 Alertes en cours', prompt: 'Y a-t-il des alertes en cours à Martinique ?' },
];
const BOT_RESPONSES: Record<string, string> = {
'🌤️ Météo': '☀️ Aujourd\'hui à Fort-de-France : 28°C, ensoleillé avec quelques nuages. Humidité 72%, vent 12 km/h NE. Indice UV 8 — pensez à la crème solaire !',
'🚌 Prochain bus': '🚌 Le prochain bus L1 (Centre-Ville → Schoelcher) arrive dans 8 min. Le L3 (Aimé Césaires) dans 12 min. Source : CFTA temps réel.',
'⚡ Consommation': '⚡ Votre consommation cette semaine : 45 kWh, soit 12% de moins que la semaine dernière. Estimation mensuelle : 180 kWh. Bon travail !',
'🚨 Alertes': '🚨 1 alerte en cours : Qualité de l\'air dégradée à Schoelcher (AQI 85). Évitez les efforts prolongés en extérieur. Pas d\'alerte météo.',
};
export default function ChatScreen({ navigation }: Props) {
const [messages, setMessages] = useState<Message[]>([
{
id: '0',
text: 'Bonjour ! Je suis Smart City IA 🤖\n\nJe peux vous aider avec :\n• 🌤️ Météo en temps réel\n• 🚌 Transports\n• ⚡ Énergie\n• 🚨 Alertes\n\nComment puis-je vous aider ?',
isBot: true,
timestamp: new Date(),
},
]);
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const scrollRef = useRef<ScrollView>(null);
useEffect(() => {
scrollRef.current?.scrollToEnd({ animated: true });
}, [messages]);
const sendMessage = (text: string) => {
if (!text.trim()) return;
const userMsg: Message = {
id: Date.now().toString(),
text: text.trim(),
isBot: false,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMsg]);
setInput('');
setIsTyping(true);
// Simulate bot response
setTimeout(() => {
const response = getBotResponse(text);
const botMsg: Message = {
id: (Date.now() + 1).toString(),
text: response,
isBot: true,
timestamp: new Date(),
};
setMessages((prev) => [...prev, botMsg]);
setIsTyping(false);
}, 1000 + Math.random() * 1000);
};
const getBotResponse = (text: string): string => {
const lower = text.toLowerCase();
for (const [key, response] of Object.entries(BOT_RESPONSES)) {
if (lower.includes(key.slice(2).toLowerCase())) return response;
}
if (lower.includes('météo') || lower.includes('weather')) return BOT_RESPONSES['🌤️ Météo'];
if (lower.includes('bus') || lower.includes('transport')) return BOT_RESPONSES['🚌 Prochain bus'];
if (lower.includes('énergie') || lower.includes('energie') || lower.includes('consommation')) return BOT_RESPONSES['⚡ Consommation'];
if (lower.includes('alerte') || lower.includes('danger')) return BOT_RESPONSES['🚨 Alertes'];
return 'Je comprends votre question. Laissez-moi consulter les données en temps réel... 🔍\n\nPour l\'instant, voici les services disponibles : météo, transports, énergie, et alertes. Essayez l\'un des raccourcis ci-dessous !';
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={90}
>
{/* Header */}
<View style={styles.header}>
<View style={styles.botAvatar}>
<Text style={styles.botEmoji}>🤖</Text>
</View>
<View>
<Text style={styles.title}>Smart City IA</Text>
<View style={styles.statusRow}>
<View style={styles.statusDot} />
<Text style={styles.statusText}>En ligne</Text>
</View>
</View>
</View>
{/* Quick prompts */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.promptsScroll}>
{QUICK_PROMPTS.map((p) => (
<TouchableOpacity key={p.id} style={styles.promptChip} onPress={() => sendMessage(p.prompt)}>
<Text style={styles.promptText}>{p.text}</Text>
</TouchableOpacity>
))}
</ScrollView>
{/* Messages */}
<ScrollView ref={scrollRef} style={styles.messagesList} showsVerticalScrollIndicator={false}>
{messages.map((msg) => (
<View key={msg.id} style={[styles.messageRow, msg.isBot ? styles.botRow : styles.userRow]}>
{msg.isBot && <View style={styles.botAvatarSmall}><Text style={styles.botEmojiSmall}>🤖</Text></View>}
<View style={[styles.messageBubble, msg.isBot ? styles.botBubble : styles.userBubble]}>
<Text style={[styles.messageText, msg.isBot ? styles.botText : styles.userText]}>{msg.text}</Text>
<Text style={[styles.messageTime, msg.isBot ? styles.botTime : styles.userTime]}>
{msg.timestamp.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
</Text>
</View>
</View>
))}
{isTyping && (
<View style={[styles.messageRow, styles.botRow]}>
<View style={styles.botAvatarSmall}><Text style={styles.botEmojiSmall}>🤖</Text></View>
<View style={[styles.messageBubble, styles.botBubble]}>
<Text style={styles.typingText}>Écrit...</Text>
</View>
</View>
)}
<View style={{ height: 20 }} />
</ScrollView>
{/* Input */}
<View style={styles.inputBar}>
<TextInput
style={styles.input}
placeholder="Posez votre question..."
placeholderTextColor={Colors.neutral400}
value={input}
onChangeText={setInput}
multiline
maxLength={500}
/>
<TouchableOpacity
style={[styles.sendBtn, !input.trim() && { opacity: 0.4 }]}
onPress={() => sendMessage(input)}
disabled={!input.trim() || isTyping}
>
<Text style={styles.sendIcon}></Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.neutral50 },
header: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.primary[500],
paddingTop: 50, paddingBottom: Spacing.base,
paddingHorizontal: Spacing.base,
gap: Spacing.base,
},
botAvatar: {
width: 44, height: 44, borderRadius: BorderRadius.lg,
backgroundColor: 'rgba(255,255,255,0.2)',
justifyContent: 'center', alignItems: 'center',
},
botEmoji: { fontSize: 24 },
title: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.white },
statusRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 2 },
statusDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: Colors.success },
statusText: { fontSize: Typography.sizes.xs, color: 'rgba(255,255,255,0.8)' },
promptsScroll: { padding: Spacing.base },
promptChip: {
backgroundColor: Colors.white, borderRadius: BorderRadius.full,
paddingHorizontal: Spacing.base, paddingVertical: Spacing.sm,
marginRight: Spacing.sm, borderWidth: 1, borderColor: Colors.neutral200,
},
promptText: { fontSize: Typography.sizes.sm, color: Colors.neutral700 },
messagesList: { flex: 1, paddingHorizontal: Spacing.base },
messageRow: { flexDirection: 'row', marginBottom: Spacing.sm, gap: Spacing.sm },
botRow: { justifyContent: 'flex-start' },
userRow: { justifyContent: 'flex-end' },
botAvatarSmall: {
width: 28, height: 28, borderRadius: BorderRadius.md,
backgroundColor: Colors.primary[100],
justifyContent: 'center', alignItems: 'center',
},
botEmojiSmall: { fontSize: 14 },
messageBubble: {
maxWidth: '75%', borderRadius: BorderRadius.lg,
padding: Spacing.base, ...Shadows.sm,
},
botBubble: {
backgroundColor: Colors.white,
borderBottomLeftRadius: 4,
},
userBubble: {
backgroundColor: Colors.primary[500],
borderBottomRightRadius: 4,
},
messageText: { fontSize: Typography.sizes.base, lineHeight: 20 },
botText: { color: Colors.neutral900 },
userText: { color: Colors.white },
messageTime: { fontSize: 9, marginTop: 4 },
botTime: { color: Colors.neutral400 },
userTime: { color: 'rgba(255,255,255,0.6)' },
typingText: { color: Colors.neutral400, fontStyle: 'italic' },
inputBar: {
flexDirection: 'row', alignItems: 'flex-end',
backgroundColor: Colors.white,
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.sm,
borderTopWidth: 1, borderTopColor: Colors.neutral200,
gap: Spacing.sm,
},
input: {
flex: 1, backgroundColor: Colors.neutral50,
borderRadius: BorderRadius.full,
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.sm,
fontSize: Typography.sizes.base,
color: Colors.neutral900,
maxHeight: 100,
},
sendBtn: {
width: 40, height: 40, borderRadius: 20,
backgroundColor: Colors.primary[500],
justifyContent: 'center', alignItems: 'center',
},
sendIcon: { color: Colors.white, fontSize: 18 },
});