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,144 @@
// Smart App City — Forgot Password Screen
import React, { useState } from 'react';
import {
View, Text, TextInput, TouchableOpacity,
StyleSheet, Alert, ActivityIndicator,
} 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> };
export default function ForgotPasswordScreen({ navigation }: Props) {
const [email, setEmail] = useState('');
const [sent, setSent] = useState(false);
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
if (!email.trim()) {
Alert.alert('Erreur', 'Veuillez entrer votre email');
return;
}
setLoading(true);
// Simulate API call
await new Promise((r) => setTimeout(r, 1500));
setLoading(false);
setSent(true);
};
return (
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backText}> Retour</Text>
</TouchableOpacity>
<View style={styles.iconContainer}>
<Text style={styles.iconEmoji}>🔑</Text>
</View>
<Text style={styles.title}>Mot de passe oublié</Text>
<Text style={styles.subtitle}>
{sent
? `Un email de réinitialisation a été envoyé à ${email}`
: 'Entrez votre email pour recevoir un lien de réinitialisation'}
</Text>
</View>
<View style={styles.form}>
{!sent ? (
<>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<View style={styles.inputWrapper}>
<Text style={styles.inputIcon}>📧</Text>
<TextInput
style={styles.input}
placeholder="votre@email.com"
placeholderTextColor={Colors.neutral400}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
editable={!loading}
/>
</View>
</View>
<TouchableOpacity
style={[styles.submitBtn, loading && { opacity: 0.6 }]}
onPress={handleSubmit}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={Colors.white} />
) : (
<Text style={styles.submitText}>Envoyer le lien</Text>
)}
</TouchableOpacity>
</>
) : (
<View style={styles.successContainer}>
<Text style={styles.successIcon}></Text>
<Text style={styles.successTitle}>Email envoyé !</Text>
<Text style={styles.successText}>
Vérifiez votre boîte de réception et suivez les instructions.
</Text>
<TouchableOpacity
style={styles.backLoginBtn}
onPress={() => navigation.navigate('Login')}
>
<Text style={styles.backLoginText}>Retour à la connexion</Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
header: {
paddingTop: 50, paddingBottom: 40, paddingHorizontal: Spacing.xl,
backgroundColor: Colors.primary[500],
borderBottomLeftRadius: BorderRadius.xxl,
borderBottomRightRadius: BorderRadius.xxl,
alignItems: 'center',
},
backBtn: { alignSelf: 'flex-start', marginBottom: Spacing.lg },
backText: { color: Colors.white, fontSize: Typography.sizes.base },
iconContainer: {
width: 72, height: 72, borderRadius: BorderRadius.xl,
backgroundColor: 'rgba(255,255,255,0.15)',
justifyContent: 'center', alignItems: 'center',
marginBottom: Spacing.base,
},
iconEmoji: { fontSize: 36 },
title: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.white, marginBottom: Spacing.sm },
subtitle: { fontSize: Typography.sizes.base, color: 'rgba(255,255,255,0.8)', textAlign: 'center', lineHeight: 22 },
form: { padding: Spacing.xl, flex: 1 },
inputGroup: { marginBottom: Spacing.lg },
label: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.semibold, color: Colors.neutral700, marginBottom: Spacing.xs },
inputWrapper: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.neutral50, borderRadius: BorderRadius.md,
borderWidth: 1, borderColor: Colors.neutral200,
paddingHorizontal: Spacing.base, height: 48,
},
inputIcon: { fontSize: 16, marginRight: Spacing.sm },
input: { flex: 1, fontSize: Typography.sizes.base, color: Colors.neutral900, height: 48 },
submitBtn: {
backgroundColor: Colors.primary[500], borderRadius: BorderRadius.md,
height: 50, justifyContent: 'center', alignItems: 'center', ...Shadows.md,
},
submitText: { color: Colors.white, fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold },
successContainer: { alignItems: 'center', paddingTop: Spacing.xl },
successIcon: { fontSize: 48, marginBottom: Spacing.base },
successTitle: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.neutral900, marginBottom: Spacing.sm },
successText: { fontSize: Typography.sizes.base, color: Colors.neutral600, textAlign: 'center', marginBottom: Spacing.xl },
backLoginBtn: {
backgroundColor: Colors.primary[50], borderRadius: BorderRadius.md,
paddingVertical: Spacing.base, paddingHorizontal: Spacing.xl,
borderWidth: 1, borderColor: Colors.primary[200],
},
backLoginText: { color: Colors.primary[500], fontWeight: Typography.weights.bold },
});

View File

@@ -0,0 +1,340 @@
// Smart App City — Login Screen
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
StyleSheet,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
Alert,
} from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useAuthStore } from '../../stores/authStore';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
type LoginScreenProps = {
navigation: NativeStackNavigationProp<any>;
};
export default function LoginScreen({ navigation }: LoginScreenProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const login = useAuthStore((state) => state.login);
const isLoading = useAuthStore((state) => state.isLoading);
const error = useAuthStore((state) => state.error);
const clearError = useAuthStore((state) => state.clearError);
const handleLogin = async () => {
if (!email.trim() || !password.trim()) {
Alert.alert('Erreur', 'Veuillez remplir tous les champs');
return;
}
try {
clearError();
await login(email.trim(), password);
} catch (err: any) {
Alert.alert('Erreur de connexion', error ?? 'Identifiants incorrects');
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
{/* Header gradient */}
<View style={styles.header}>
<View style={styles.logoContainer}>
<Text style={styles.logoEmoji}>🏙</Text>
</View>
<Text style={styles.title}>Smart App City</Text>
<Text style={styles.subtitle}>Martinique Digital Twin</Text>
</View>
{/* Form */}
<View style={styles.formContainer}>
<Text style={styles.formTitle}>Connexion</Text>
{/* Email */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<View style={styles.inputWrapper}>
<Text style={styles.inputIcon}>📧</Text>
<TextInput
style={styles.input}
placeholder="votre@email.com"
placeholderTextColor={Colors.neutral400}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
</View>
</View>
{/* Password */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Mot de passe</Text>
<View style={styles.inputWrapper}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
style={[styles.input, { flex: 1 }]}
placeholder="••••••••"
placeholderTextColor={Colors.neutral400}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoCapitalize="none"
editable={!isLoading}
/>
<TouchableOpacity onPress={() => setShowPassword(!showPassword)}>
<Text style={styles.toggleIcon}>{showPassword ? '🙈' : '👁️'}</Text>
</TouchableOpacity>
</View>
</View>
{/* Forgot password */}
<TouchableOpacity
onPress={() => navigation.navigate('ForgotPassword')}
style={styles.forgotRow}
>
<Text style={styles.forgotText}>Mot de passe oublié ?</Text>
</TouchableOpacity>
{/* Error */}
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}> {error}</Text>
</View>
)}
{/* Login button */}
<TouchableOpacity
style={[styles.loginButton, isLoading && styles.loginButtonDisabled]}
onPress={handleLogin}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color={Colors.white} />
) : (
<Text style={styles.loginButtonText}>Se connecter</Text>
)}
</TouchableOpacity>
{/* Divider */}
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>ou</Text>
<View style={styles.dividerLine} />
</View>
{/* Social buttons */}
<View style={styles.socialRow}>
<TouchableOpacity style={styles.socialButton}>
<Text style={styles.socialIcon}>🔵</Text>
<Text style={styles.socialText}>Google</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.socialButton}>
<Text style={styles.socialIcon}>🍎</Text>
<Text style={styles.socialText}>Apple</Text>
</TouchableOpacity>
</View>
{/* Register link */}
<View style={styles.registerRow}>
<Text style={styles.registerText}>Pas encore de compte ? </Text>
<TouchableOpacity onPress={() => navigation.navigate('Register')}>
<Text style={styles.registerLink}>S'inscrire</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.white,
},
scrollContent: {
flexGrow: 1,
},
header: {
paddingTop: 60,
paddingBottom: 40,
paddingHorizontal: Spacing.xl,
backgroundColor: Colors.primary[500],
borderBottomLeftRadius: BorderRadius.xxl,
borderBottomRightRadius: BorderRadius.xxl,
alignItems: 'center',
...Shadows.lg,
},
logoContainer: {
width: 80,
height: 80,
borderRadius: BorderRadius.xl,
backgroundColor: 'rgba(255,255,255,0.15)',
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
logoEmoji: {
fontSize: 40,
},
title: {
fontSize: Typography.sizes.xxl,
fontWeight: Typography.weights.bold,
color: Colors.white,
marginBottom: Spacing.xs,
},
subtitle: {
fontSize: Typography.sizes.base,
color: 'rgba(255,255,255,0.8)',
},
formContainer: {
flex: 1,
padding: Spacing.xl,
marginTop: Spacing.lg,
},
formTitle: {
fontSize: Typography.sizes.xl,
fontWeight: Typography.weights.bold,
color: Colors.neutral900,
marginBottom: Spacing.xl,
},
inputGroup: {
marginBottom: Spacing.base,
},
label: {
fontSize: Typography.sizes.sm,
fontWeight: Typography.weights.semibold,
color: Colors.neutral700,
marginBottom: Spacing.xs,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.neutral50,
borderRadius: BorderRadius.md,
borderWidth: 1,
borderColor: Colors.neutral200,
paddingHorizontal: Spacing.base,
height: 48,
},
inputIcon: {
fontSize: 16,
marginRight: Spacing.sm,
},
input: {
flex: 1,
fontSize: Typography.sizes.base,
color: Colors.neutral900,
height: 48,
},
toggleIcon: {
fontSize: 16,
padding: Spacing.xs,
},
forgotRow: {
alignSelf: 'flex-end',
marginBottom: Spacing.lg,
},
forgotText: {
fontSize: Typography.sizes.sm,
color: Colors.primary[500],
fontWeight: Typography.weights.medium,
},
errorContainer: {
backgroundColor: '#FFEBEE',
borderRadius: BorderRadius.md,
padding: Spacing.base,
marginBottom: Spacing.base,
},
errorText: {
fontSize: Typography.sizes.sm,
color: Colors.danger,
},
loginButton: {
backgroundColor: Colors.primary[500],
borderRadius: BorderRadius.md,
height: 50,
justifyContent: 'center',
alignItems: 'center',
...Shadows.md,
},
loginButtonDisabled: {
opacity: 0.6,
},
loginButtonText: {
color: Colors.white,
fontSize: Typography.sizes.md,
fontWeight: Typography.weights.bold,
},
divider: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: Spacing.xl,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: Colors.neutral200,
},
dividerText: {
fontSize: Typography.sizes.sm,
color: Colors.neutral500,
paddingHorizontal: Spacing.base,
},
socialRow: {
flexDirection: 'row',
gap: Spacing.base,
marginBottom: Spacing.xl,
},
socialButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Colors.neutral50,
borderRadius: BorderRadius.md,
borderWidth: 1,
borderColor: Colors.neutral200,
height: 48,
gap: Spacing.sm,
},
socialIcon: {
fontSize: 16,
},
socialText: {
fontSize: Typography.sizes.base,
fontWeight: Typography.weights.medium,
color: Colors.neutral700,
},
registerRow: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
registerText: {
fontSize: Typography.sizes.base,
color: Colors.neutral600,
},
registerLink: {
fontSize: Typography.sizes.base,
fontWeight: Typography.weights.bold,
color: Colors.primary[500],
},
});

View File

@@ -0,0 +1,217 @@
// Smart App City — Register Screen
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
StyleSheet,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
Alert,
} from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useAuthStore } from '../../stores/authStore';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
type Props = { navigation: NativeStackNavigationProp<any> };
export default function RegisterScreen({ navigation }: Props) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const register = useAuthStore((s) => s.register);
const isLoading = useAuthStore((s) => s.isLoading);
const error = useAuthStore((s) => s.error);
const clearError = useAuthStore((s) => s.clearError);
const handleRegister = async () => {
if (!email.trim() || !password.trim() || !firstName.trim() || !lastName.trim()) {
Alert.alert('Erreur', 'Veuillez remplir tous les champs');
return;
}
if (password !== confirmPassword) {
Alert.alert('Erreur', 'Les mots de passe ne correspondent pas');
return;
}
if (password.length < 8) {
Alert.alert('Erreur', 'Le mot de passe doit contenir au moins 8 caractères');
return;
}
try {
clearError();
await register({ email: email.trim(), password, firstName: firstName.trim(), lastName: lastName.trim() });
} catch (err: any) {
Alert.alert('Erreur', error ?? 'Inscription échouée');
}
};
return (
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backBtn}>
<Text style={styles.backText}> Retour</Text>
</TouchableOpacity>
<Text style={styles.title}>Créer un compte</Text>
<Text style={styles.subtitle}>Rejoignez Smart App City</Text>
</View>
{/* Form */}
<View style={styles.form}>
<View style={styles.row}>
<View style={[styles.inputGroup, { flex: 1, marginRight: Spacing.sm }]}>
<Text style={styles.label}>Prénom</Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="Jean"
placeholderTextColor={Colors.neutral400}
value={firstName}
onChangeText={setFirstName}
autoCapitalize="words"
editable={!isLoading}
/>
</View>
</View>
<View style={[styles.inputGroup, { flex: 1, marginLeft: Spacing.sm }]}>
<Text style={styles.label}>Nom</Text>
<View style={styles.inputWrapper}>
<TextInput
style={styles.input}
placeholder="Dupont"
placeholderTextColor={Colors.neutral400}
value={lastName}
onChangeText={setLastName}
autoCapitalize="words"
editable={!isLoading}
/>
</View>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<View style={styles.inputWrapper}>
<Text style={styles.inputIcon}>📧</Text>
<TextInput
style={styles.input}
placeholder="votre@email.com"
placeholderTextColor={Colors.neutral400}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
editable={!isLoading}
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Mot de passe</Text>
<View style={styles.inputWrapper}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
style={styles.input}
placeholder="Min. 8 caractères"
placeholderTextColor={Colors.neutral400}
value={password}
onChangeText={setPassword}
secureTextEntry
autoCapitalize="none"
editable={!isLoading}
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Confirmer le mot de passe</Text>
<View style={styles.inputWrapper}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
style={styles.input}
placeholder="Retapez le mot de passe"
placeholderTextColor={Colors.neutral400}
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
autoCapitalize="none"
editable={!isLoading}
/>
</View>
</View>
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}> {error}</Text>
</View>
)}
<TouchableOpacity
style={[styles.submitBtn, isLoading && { opacity: 0.6 }]}
onPress={handleRegister}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color={Colors.white} />
) : (
<Text style={styles.submitText}>S'inscrire</Text>
)}
</TouchableOpacity>
<View style={styles.loginRow}>
<Text style={styles.loginText}>Déjà un compte ? </Text>
<TouchableOpacity onPress={() => navigation.navigate('Login')}>
<Text style={styles.loginLink}>Se connecter</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
scrollContent: { flexGrow: 1 },
header: {
paddingTop: 50,
paddingBottom: 30,
paddingHorizontal: Spacing.xl,
backgroundColor: Colors.primary[500],
borderBottomLeftRadius: BorderRadius.xxl,
borderBottomRightRadius: BorderRadius.xxl,
},
backBtn: { marginBottom: Spacing.base },
backText: { color: Colors.white, fontSize: Typography.sizes.base },
title: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.white, marginBottom: Spacing.xs },
subtitle: { fontSize: Typography.sizes.base, color: 'rgba(255,255,255,0.8)' },
form: { padding: Spacing.xl },
row: { flexDirection: 'row' },
inputGroup: { marginBottom: Spacing.base },
label: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.semibold, color: Colors.neutral700, marginBottom: Spacing.xs },
inputWrapper: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.neutral50, borderRadius: BorderRadius.md,
borderWidth: 1, borderColor: Colors.neutral200,
paddingHorizontal: Spacing.base, height: 48,
},
inputIcon: { fontSize: 16, marginRight: Spacing.sm },
input: { flex: 1, fontSize: Typography.sizes.base, color: Colors.neutral900, height: 48 },
errorContainer: { backgroundColor: '#FFEBEE', borderRadius: BorderRadius.md, padding: Spacing.base, marginBottom: Spacing.base },
errorText: { fontSize: Typography.sizes.sm, color: Colors.danger },
submitBtn: {
backgroundColor: Colors.primary[500], borderRadius: BorderRadius.md,
height: 50, justifyContent: 'center', alignItems: 'center',
marginTop: Spacing.sm, ...Shadows.md,
},
submitText: { color: Colors.white, fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold },
loginRow: { flexDirection: 'row', justifyContent: 'center', marginTop: Spacing.xl },
loginText: { fontSize: Typography.sizes.base, color: Colors.neutral600 },
loginLink: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.bold, color: Colors.primary[500] },
});

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 },
});

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { View, Text } from 'react-native';
const DashboardScreen = () => {
return (
<View>
<Text>DashboardScreen</Text>
</View>
);
};
export default DashboardScreen;

View File

@@ -0,0 +1,334 @@
// Smart App City — Home Dashboard Screen
// Météo, capteurs IoT, quick actions, alertes
import React, { useState, useEffect } from 'react';
import {
View, Text, ScrollView, TouchableOpacity,
StyleSheet, RefreshControl, Dimensions,
} from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
const { width } = Dimensions.get('window');
type Props = { navigation: NativeStackNavigationProp<any> };
// ─── Mock data ───────────────────────────────────────────────────────────────
const MOCK_METRICS = [
{ id: 'temp', label: 'Température', value: '28°', unit: 'C', trend: 'up', color: Colors.ocean[500], icon: '🌡️' },
{ id: 'humidity', label: 'Humidité', value: '72', unit: '%', trend: 'down', color: Colors.primary[500], icon: '💧' },
{ id: 'aqi', label: 'Qualité Air', value: '42', unit: 'AQI', trend: 'up', color: Colors.success, icon: '🌬️' },
{ id: 'noise', label: 'Bruit', value: '55', unit: 'dB', trend: 'down', color: Colors.indigo[500], icon: '🔊' },
];
const MOCK_SENSORS = [
{ id: 's1', name: 'Capteur Centre-Ville', location: 'Fort-de-France', value: 28.3, unit: '°C', type: 'Température', icon: '🌡️', status: 'ok' },
{ id: 's2', name: 'Capteur Port', location: 'Baie de Fort-de-France', value: 72, unit: '%', type: 'Humidité', icon: '💧', status: 'ok' },
{ id: 's3', name: 'Capteur Qualité Air', location: 'Schoelcher', value: 42, unit: 'AQI', type: 'Air', icon: '🌬️', status: 'warning' },
{ id: 's4', name: 'Capteur Bruit', location: 'Centre-ville', value: 68, unit: 'dB', type: 'Son', icon: '🔊', status: 'alert' },
];
const QUICK_ACTIONS = [
{ id: 'signal', label: 'Signaler', icon: '🚨', color: Colors.danger, screen: 'Alerts' },
{ id: 'transport', label: 'Transport', icon: '🚌', color: Colors.primary[500], screen: 'Map' },
{ id: 'energie', label: 'Énergie', icon: '⚡', color: Colors.warning, screen: 'Sensors' },
{ id: 'sante', label: 'Santé', icon: '🏥', color: Colors.success, screen: 'Marketplace' },
];
const SERVICES = [
{ id: 'meteo', label: 'Météo', icon: '🌤️', color: Colors.ocean[500] },
{ id: 'events', label: 'Événements', icon: '🎉', color: Colors.indigo[500] },
{ id: 'wallet', label: 'Wallet', icon: '💳', color: Colors.primary[500] },
{ id: 'bus', label: 'Bus', icon: '🚌', color: Colors.ocean[600] },
];
// ─── Component ───────────────────────────────────────────────────────────────
export default function HomeScreen({ navigation }: Props) {
const [refreshing, setRefreshing] = useState(false);
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => setCurrentTime(new Date()), 60000);
return () => clearInterval(timer);
}, []);
const onRefresh = async () => {
setRefreshing(true);
await new Promise((r) => setTimeout(r, 1500));
setRefreshing(false);
};
const greeting = () => {
const h = currentTime.getHours();
if (h < 12) return 'Bonjour';
if (h < 18) return 'Bon après-midi';
return 'Bonsoir';
};
return (
<ScrollView
style={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.primary[500]} />}
showsVerticalScrollIndicator={false}
>
{/* ── Header ── */}
<View style={styles.header}>
<View style={styles.headerTop}>
<View>
<Text style={styles.greeting}>{greeting()} 👋</Text>
<Text style={styles.userName}>Eric</Text>
</View>
<TouchableOpacity style={styles.notificationBtn}>
<Text style={styles.notificationIcon}>🔔</Text>
<View style={styles.notificationBadge}>
<Text style={styles.badgeText}>3</Text>
</View>
</TouchableOpacity>
</View>
{/* Search bar */}
<View style={styles.searchBar}>
<Text style={styles.searchIcon}>🔍</Text>
<Text style={styles.searchPlaceholder}>Rechercher un service, un lieu...</Text>
</View>
{/* Live indicator */}
<View style={styles.liveRow}>
<View style={styles.liveIndicator}>
<View style={styles.liveDot} />
<Text style={styles.liveText}>EN DIRECT</Text>
</View>
<Text style={styles.lastUpdate}>
{currentTime.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
</Text>
</View>
</View>
{/* ── Metrics Row ── */}
<View style={styles.metricsRow}>
{MOCK_METRICS.map((m) => (
<View key={m.id} style={styles.metricCard}>
<Text style={styles.metricIcon}>{m.icon}</Text>
<Text style={styles.metricValue}>{m.value}</Text>
<Text style={styles.metricUnit}>{m.unit}</Text>
<Text style={styles.metricLabel}>{m.label}</Text>
<Text style={[styles.metricTrend, { color: m.trend === 'up' ? Colors.success : Colors.danger }]}>
{m.trend === 'up' ? '↑' : '↓'}
</Text>
</View>
))}
</View>
{/* ── Weather Widget ── */}
<View style={styles.weatherWidget}>
<View>
<Text style={styles.weatherTemp}>28°C</Text>
<Text style={styles.weatherDetails}>Fort-de-France</Text>
<Text style={styles.weatherDetails}>Ensoleillé Humidité 72%</Text>
<Text style={styles.weatherDetails}>Vent 12 km/h NE</Text>
</View>
<Text style={styles.weatherEmoji}></Text>
</View>
{/* ── Alert Banner ── */}
<TouchableOpacity style={styles.alertBanner}>
<Text style={styles.alertIcon}></Text>
<View style={styles.alertContent}>
<Text style={styles.alertText}>Alerte qualité air Schoelcher</Text>
<Text style={styles.alertSub}>AQI élevé détecté. Évitez les efforts prolongés en extérieur.</Text>
</View>
<Text style={styles.alertArrow}></Text>
</TouchableOpacity>
{/* ── Quick Actions ── */}
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Actions rapides</Text>
</View>
<View style={styles.quickActions}>
{QUICK_ACTIONS.map((a) => (
<TouchableOpacity key={a.id} style={styles.quickAction} onPress={() => navigation.navigate(a.screen)}>
<View style={[styles.quickActionIcon, { backgroundColor: a.color }]}>
<Text style={styles.quickActionEmoji}>{a.icon}</Text>
</View>
<Text style={styles.quickActionLabel}>{a.label}</Text>
</TouchableOpacity>
))}
</View>
{/* ── Sensors ── */}
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Capteurs en direct</Text>
<TouchableOpacity>
<Text style={styles.seeAll}>Voir tout </Text>
</TouchableOpacity>
</View>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.sensorsScroll}>
{MOCK_SENSORS.map((s) => (
<TouchableOpacity key={s.id} style={styles.sensorCard}>
<View style={styles.sensorHeader}>
<View style={[styles.sensorIcon, { backgroundColor: s.status === 'ok' ? Colors.primary[50] : s.status === 'warning' ? '#FFF3E0' : '#FFEBEE' }]}>
<Text style={styles.sensorEmoji}>{s.icon}</Text>
</View>
<View style={[styles.sensorStatus, {
backgroundColor: s.status === 'ok' ? Colors.success : s.status === 'warning' ? Colors.warning : Colors.danger,
}]} />
</View>
<Text style={styles.sensorValue}>{s.value}<Text style={styles.sensorUnit}> {s.unit}</Text></Text>
<Text style={styles.sensorName}>{s.name}</Text>
<Text style={styles.sensorLocation}>📍 {s.location}</Text>
</TouchableOpacity>
))}
</ScrollView>
{/* ── Services ── */}
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Services</Text>
</View>
<View style={styles.servicesGrid}>
{SERVICES.map((s) => (
<TouchableOpacity key={s.id} style={styles.serviceShortcut}>
<View style={[styles.serviceIcon, { backgroundColor: s.color + '15' }]}>
<Text style={styles.serviceEmoji}>{s.icon}</Text>
</View>
<Text style={styles.serviceLabel}>{s.label}</Text>
</TouchableOpacity>
))}
</View>
<View style={{ height: 80 }} />
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.neutral50 },
header: {
backgroundColor: Colors.primary[500],
paddingTop: 50,
paddingHorizontal: Spacing.base,
paddingBottom: Spacing.lg,
borderBottomLeftRadius: BorderRadius.xxl,
borderBottomRightRadius: BorderRadius.xxl,
},
headerTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: Spacing.base },
greeting: { fontSize: Typography.sizes.sm, color: 'rgba(255,255,255,0.85)' },
userName: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white },
notificationBtn: { position: 'relative' },
notificationIcon: { fontSize: 24 },
notificationBadge: {
position: 'absolute', top: -4, right: -4,
backgroundColor: Colors.danger, borderRadius: 10,
width: 18, height: 18, justifyContent: 'center', alignItems: 'center',
},
badgeText: { color: Colors.white, fontSize: 10, fontWeight: '700' },
searchBar: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: BorderRadius.full,
paddingHorizontal: Spacing.base,
height: 40,
marginBottom: Spacing.sm,
},
searchIcon: { fontSize: 14, marginRight: Spacing.sm },
searchPlaceholder: { color: 'rgba(255,255,255,0.6)', fontSize: Typography.sizes.sm },
liveRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
liveIndicator: { flexDirection: 'row', alignItems: 'center', gap: 6 },
liveDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: Colors.success },
liveText: { color: Colors.success, fontSize: Typography.sizes.xs, fontWeight: Typography.weights.semibold },
lastUpdate: { color: 'rgba(255,255,255,0.6)', fontSize: Typography.sizes.xs },
// Metrics
metricsRow: {
flexDirection: 'row', gap: 8,
paddingHorizontal: Spacing.base,
marginTop: -20, marginBottom: Spacing.base,
},
metricCard: {
flex: 1, backgroundColor: Colors.white,
borderRadius: BorderRadius.lg, padding: Spacing.md,
alignItems: 'center', ...Shadows.md,
borderWidth: 1, borderColor: Colors.neutral100,
},
metricIcon: { fontSize: 18, marginBottom: 4 },
metricValue: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
metricUnit: { fontSize: 9, color: Colors.neutral500 },
metricLabel: { fontSize: 9, color: Colors.neutral500, marginTop: 2, textAlign: 'center' },
metricTrend: { fontSize: 12, marginTop: 2 },
// Weather
weatherWidget: {
marginHorizontal: Spacing.base, marginBottom: Spacing.base,
backgroundColor: Colors.primary[500],
borderRadius: BorderRadius.xl, padding: Spacing.base,
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
},
weatherTemp: { fontSize: Typography.sizes.xxxl, fontWeight: Typography.weights.bold, color: Colors.white },
weatherDetails: { fontSize: Typography.sizes.sm, color: 'rgba(255,255,255,0.8)', marginTop: 2 },
weatherEmoji: { fontSize: 48 },
// Alert
alertBanner: {
marginHorizontal: Spacing.base, marginBottom: Spacing.base,
backgroundColor: '#FFEBEE', borderRadius: BorderRadius.lg,
borderWidth: 1, borderColor: 'rgba(211,47,47,0.2)',
padding: Spacing.base, flexDirection: 'row', alignItems: 'center', gap: 10,
},
alertIcon: { fontSize: 20 },
alertContent: { flex: 1 },
alertText: { fontSize: Typography.sizes.sm, color: Colors.danger, fontWeight: Typography.weights.medium },
alertSub: { fontSize: Typography.sizes.xs, color: Colors.neutral600, marginTop: 2 },
alertArrow: { fontSize: 18, color: Colors.danger },
// Section
sectionHeader: {
flexDirection: 'row', justifyContent: 'space-between',
alignItems: 'center', paddingHorizontal: Spacing.base, paddingBottom: Spacing.sm,
},
sectionTitle: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
seeAll: { fontSize: Typography.sizes.sm, color: Colors.primary[500], fontWeight: Typography.weights.medium },
// Quick Actions
quickActions: {
flexDirection: 'row', paddingHorizontal: Spacing.base,
gap: 12, marginBottom: Spacing.base,
},
quickAction: { flex: 1, alignItems: 'center', gap: 6 },
quickActionIcon: {
width: 52, height: 52, borderRadius: BorderRadius.lg,
justifyContent: 'center', alignItems: 'center', ...Shadows.sm,
},
quickActionEmoji: { fontSize: 22 },
quickActionLabel: { fontSize: 10, color: Colors.neutral600, textAlign: 'center' },
// Sensors
sensorsScroll: { paddingLeft: Spacing.base, marginBottom: Spacing.base },
sensorCard: {
width: 150, backgroundColor: Colors.white,
borderRadius: BorderRadius.lg, padding: Spacing.base,
marginRight: Spacing.sm, ...Shadows.sm,
borderWidth: 1, borderColor: Colors.neutral100,
},
sensorHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: Spacing.sm },
sensorIcon: { width: 36, height: 36, borderRadius: BorderRadius.md, justifyContent: 'center', alignItems: 'center' },
sensorEmoji: { fontSize: 18 },
sensorStatus: { width: 8, height: 8, borderRadius: 4 },
sensorValue: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
sensorUnit: { fontSize: Typography.sizes.xs, color: Colors.neutral500 },
sensorName: { fontSize: Typography.sizes.xs, color: Colors.neutral600, marginTop: 4 },
sensorLocation: { fontSize: 10, color: Colors.neutral400, marginTop: 2 },
// Services
servicesGrid: {
flexDirection: 'row', paddingHorizontal: Spacing.base,
gap: 10, marginBottom: Spacing.base,
},
serviceShortcut: { flex: 1, alignItems: 'center', gap: 6 },
serviceIcon: {
width: 48, height: 48, borderRadius: BorderRadius.lg,
justifyContent: 'center', alignItems: 'center',
},
serviceEmoji: { fontSize: 20 },
serviceLabel: { fontSize: 9, color: Colors.neutral600, textAlign: 'center', lineHeight: 14 },
});

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { View, Text } from 'react-native';
const LayerDetailScreen = () => {
return (
<View>
<Text>LayerDetailScreen</Text>
</View>
);
};
export default LayerDetailScreen;

View File

@@ -0,0 +1,133 @@
// Smart App City — Map Screen (Carte Interactive)
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } 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> };
const LAYERS = [
{ id: 'sensors', label: 'Capteurs IoT', icon: '📡', active: true },
{ id: 'air', label: 'Qualité Air', icon: '🌬️', active: false },
{ id: 'noise', label: 'Bruit', icon: '🔊', active: false },
{ id: 'traffic', label: 'Trafic', icon: '🚗', active: false },
{ id: 'weather', label: 'Météo', icon: '🌤️', active: true },
{ id: 'events', label: 'Événements', icon: '🎉', active: false },
];
export default function MapScreen({ navigation }: Props) {
const [activeLayers, setActiveLayers] = useState<string[]>(['sensors', 'weather']);
const toggleLayer = (id: string) => {
setActiveLayers((prev) =>
prev.includes(id) ? prev.filter((l) => l !== id) : [...prev, id]
);
};
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Carte Interactive</Text>
<Text style={styles.subtitle}>Martinique Temps réel</Text>
</View>
{/* Map placeholder */}
<View style={styles.mapContainer}>
<View style={styles.mapPlaceholder}>
<Text style={styles.mapEmoji}>🗺</Text>
<Text style={styles.mapText}>Carte OpenStreetMap</Text>
<Text style={styles.mapSubtext}>react-native-maps Martinique</Text>
<Text style={styles.mapCoords}>14.6°N, 61.0°W</Text>
</View>
{/* Sensor markers mock */}
<View style={[styles.marker, { top: '30%', left: '40%' }]}>
<View style={[styles.markerDot, { backgroundColor: Colors.success }]} />
<Text style={styles.markerLabel}>28°C</Text>
</View>
<View style={[styles.marker, { top: '50%', left: '60%' }]}>
<View style={[styles.markerDot, { backgroundColor: Colors.warning }]} />
<Text style={styles.markerLabel}>AQI 42</Text>
</View>
<View style={[styles.marker, { top: '70%', left: '30%' }]}>
<View style={[styles.markerDot, { backgroundColor: Colors.danger }]} />
<Text style={styles.markerLabel}>68dB</Text>
</View>
</View>
{/* Layer toggles */}
<View style={styles.layerPanel}>
<Text style={styles.layerTitle}>Couches</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.layerScroll}>
{LAYERS.map((layer) => (
<TouchableOpacity
key={layer.id}
style={[
styles.layerChip,
activeLayers.includes(layer.id) && styles.layerChipActive,
]}
onPress={() => toggleLayer(layer.id)}
>
<Text style={styles.layerIcon}>{layer.icon}</Text>
<Text style={[
styles.layerLabel,
activeLayers.includes(layer.id) && styles.layerLabelActive,
]}>
{layer.label}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.neutral50 },
header: {
backgroundColor: Colors.primary[500],
paddingTop: 50,
paddingBottom: Spacing.base,
paddingHorizontal: Spacing.base,
},
title: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white },
subtitle: { fontSize: Typography.sizes.sm, color: 'rgba(255,255,255,0.8)' },
mapContainer: { flex: 1, position: 'relative' },
mapPlaceholder: {
flex: 1, justifyContent: 'center', alignItems: 'center',
backgroundColor: '#E8F5E9',
},
mapEmoji: { fontSize: 64, marginBottom: Spacing.base },
mapText: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.neutral700 },
mapSubtext: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: Spacing.xs },
mapCoords: { fontSize: Typography.sizes.xs, color: Colors.neutral400, marginTop: Spacing.xs },
marker: { position: 'absolute', alignItems: 'center' },
markerDot: { width: 12, height: 12, borderRadius: 6, borderWidth: 2, borderColor: Colors.white },
markerLabel: {
fontSize: 10, fontWeight: '600', color: Colors.neutral700,
backgroundColor: 'rgba(255,255,255,0.9)',
paddingHorizontal: 4, paddingVertical: 1,
borderRadius: 4, marginTop: 2,
},
layerPanel: {
backgroundColor: Colors.white,
borderTopLeftRadius: BorderRadius.xl,
borderTopRightRadius: BorderRadius.xl,
padding: Spacing.base,
...Shadows.lg,
},
layerTitle: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.neutral900, marginBottom: Spacing.sm },
layerScroll: { flexDirection: 'row' },
layerChip: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.neutral100, borderRadius: BorderRadius.full,
paddingHorizontal: Spacing.base, paddingVertical: Spacing.xs,
marginRight: Spacing.sm, gap: 4,
},
layerChipActive: { backgroundColor: Colors.primary[500] },
layerIcon: { fontSize: 14 },
layerLabel: { fontSize: Typography.sizes.sm, color: Colors.neutral600 },
layerLabelActive: { color: Colors.white },
});

View File

@@ -0,0 +1,170 @@
// Smart App City — Alerts Screen
import React, { useState } from 'react';
import {
View, Text, ScrollView, TouchableOpacity,
StyleSheet, RefreshControl,
} from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors';
import { useAlerts } from '../../../hooks/useAlerts';
import SectionHeader from '../../common/Header';
type Props = { navigation: NativeStackNavigationProp<any> };
const SEVERITY_CONFIG = {
critical: { color: Colors.danger, icon: '🔴', label: 'Critique' },
high: { color: Colors.warning, icon: '🟠', label: 'Haute' },
medium: { color: '#FF9800', icon: '🟡', label: 'Moyenne' },
low: { color: Colors.info, icon: '🔵', label: 'Basse' },
};
export default function AlertsScreen({ navigation }: Props) {
const { alerts, activeAlerts, acknowledgeAlert, refresh } = useAlerts();
const [refreshing, setRefreshing] = useState(false);
const [filter, setFilter] = useState<'all' | 'active' | 'acknowledged'>('active');
const filtered = filter === 'all' ? alerts : filter === 'active' ? activeAlerts : alerts.filter((a) => a.acknowledged);
const onRefresh = async () => {
setRefreshing(true);
await refresh();
setRefreshing(false);
};
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text style={styles.backText}> Retour</Text>
</TouchableOpacity>
<Text style={styles.title}>Alertes</Text>
<View style={styles.badge}>
<Text style={styles.badgeText}>{activeAlerts.length}</Text>
</View>
</View>
{/* Filter tabs */}
<View style={styles.tabs}>
{[
{ key: 'active', label: 'Actives' },
{ key: 'all', label: 'Toutes' },
{ key: 'acknowledged', label: 'Traitées' },
].map((tab) => (
<TouchableOpacity
key={tab.key}
style={[styles.tab, filter === tab.key && styles.tabActive]}
onPress={() => setFilter(tab.key as any)}
>
<Text style={[styles.tabText, filter === tab.key && styles.tabTextActive]}>
{tab.label}
</Text>
</TouchableOpacity>
))}
</View>
{/* Alerts list */}
<ScrollView
style={styles.list}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.primary[500]} />}
showsVerticalScrollIndicator={false}
>
{filtered.length === 0 ? (
<View style={styles.empty}>
<Text style={styles.emptyEmoji}></Text>
<Text style={styles.emptyText}>Aucune alerte {filter === 'active' ? 'active' : ''}</Text>
</View>
) : (
filtered.map((alert) => {
const config = SEVERITY_CONFIG[alert.severity];
return (
<TouchableOpacity
key={alert.id}
style={[styles.alertCard, alert.acknowledged && { opacity: 0.6 }]}
onPress={() => !alert.acknowledged && acknowledgeAlert(alert.id)}
>
<View style={[styles.severityBar, { backgroundColor: config.color }]} />
<View style={styles.alertContent}>
<View style={styles.alertHeader}>
<Text style={styles.alertTitle}>
{config.icon} {alert.sensorName}
</Text>
<Text style={[styles.severityBadge, { color: config.color }]}>
{config.label}
</Text>
</View>
<Text style={styles.alertMessage}>{alert.message}</Text>
<View style={styles.alertFooter}>
<Text style={styles.alertTime}>
{new Date(alert.createdAt).toLocaleString('fr-FR', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
})}
</Text>
{alert.value !== undefined && (
<Text style={styles.alertValue}>
Valeur: {alert.value}{alert.threshold ? ` (${alert.threshold})` : ''}
</Text>
)}
</View>
</View>
{!alert.acknowledged && <Text style={styles.checkIcon}></Text>}
{alert.acknowledged && <Text style={styles.checkIcon}></Text>}
</TouchableOpacity>
);
})
)}
<View style={{ height: 40 }} />
</ScrollView>
</View>
);
}
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,
},
backText: { color: Colors.white, fontSize: Typography.sizes.base },
title: { flex: 1, fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white },
badge: {
backgroundColor: Colors.danger, borderRadius: 12,
paddingHorizontal: Spacing.sm, paddingVertical: 2,
minWidth: 24, alignItems: 'center',
},
badgeText: { color: Colors.white, fontSize: Typography.sizes.sm, fontWeight: '700' },
tabs: {
flexDirection: 'row', backgroundColor: Colors.white,
paddingHorizontal: Spacing.base, paddingTop: Spacing.base,
gap: Spacing.sm,
},
tab: {
paddingHorizontal: Spacing.base, paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full, backgroundColor: Colors.neutral100,
},
tabActive: { backgroundColor: Colors.primary[500] },
tabText: { fontSize: Typography.sizes.sm, color: Colors.neutral600 },
tabTextActive: { color: Colors.white, fontWeight: '600' },
list: { flex: 1, padding: Spacing.base },
empty: { alignItems: 'center', paddingTop: Spacing.xxxl },
emptyEmoji: { fontSize: 48, marginBottom: Spacing.base },
emptyText: { fontSize: Typography.sizes.base, color: Colors.neutral500 },
alertCard: {
flexDirection: 'row', backgroundColor: Colors.white,
borderRadius: BorderRadius.lg, marginBottom: Spacing.sm,
...Shadows.sm, overflow: 'hidden',
},
severityBar: { width: 4 },
alertContent: { flex: 1, padding: Spacing.base },
alertHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: Spacing.xs },
alertTitle: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.bold, color: Colors.neutral900, flex: 1 },
severityBadge: { fontSize: Typography.sizes.xs, fontWeight: '600' },
alertMessage: { fontSize: Typography.sizes.sm, color: Colors.neutral600, marginBottom: Spacing.sm, lineHeight: 18 },
alertFooter: { flexDirection: 'row', justifyContent: 'space-between' },
alertTime: { fontSize: Typography.sizes.xs, color: Colors.neutral400 },
alertValue: { fontSize: Typography.sizes.xs, color: Colors.neutral500 },
checkIcon: { fontSize: 18, color: Colors.neutral400, padding: Spacing.base, alignSelf: 'center' },
});

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { View, Text } from 'react-native';
const SensorDetailScreen = () => {
return (
<View>
<Text>SensorDetailScreen</Text>
</View>
);
};
export default SensorDetailScreen;

View File

@@ -0,0 +1,157 @@
// Smart App City — Sensors Screen
import React, { useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, TextInput } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors';
import { useSensors } from '../../../hooks/useSensors';
type Props = { navigation: NativeStackNavigationProp<any> };
const TYPE_FILTERS = [
{ key: null, label: 'Tous', icon: '📡' },
{ key: 'temperature', label: 'Temp', icon: '🌡️' },
{ key: 'humidity', label: 'Humidité', icon: '💧' },
{ key: 'air_quality', label: 'Air', icon: '🌬️' },
{ key: 'noise', label: 'Bruit', icon: '🔊' },
{ key: 'traffic', label: 'Trafic', icon: '🚗' },
{ key: 'energy', label: 'Énergie', icon: '⚡' },
];
const STATUS_DOT = { ok: Colors.success, warning: Colors.warning, alert: Colors.danger, offline: Colors.neutral400 };
export default function SensorsScreen({ navigation }: Props) {
const { sensors, isLoading, refresh } = useSensors();
const [search, setSearch] = useState('');
const [selectedType, setSelectedType] = useState<string | null>(null);
const filtered = sensors.filter((s) => {
if (selectedType && s.type !== selectedType) return false;
if (search && !s.name.toLowerCase().includes(search.toLowerCase()) && !s.location.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
return (
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text style={styles.backText}> Retour</Text>
</TouchableOpacity>
<Text style={styles.title}>Capteurs IoT</Text>
<Text style={styles.count}>{sensors.length}</Text>
</View>
{/* Search */}
<View style={styles.searchBar}>
<Text style={styles.searchIcon}>🔍</Text>
<TextInput
style={styles.searchInput}
placeholder="Rechercher un capteur..."
placeholderTextColor={Colors.neutral400}
value={search}
onChangeText={setSearch}
/>
</View>
{/* Type filters */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.filtersScroll}>
{TYPE_FILTERS.map((f) => (
<TouchableOpacity
key={f.key ?? 'all'}
style={[styles.filterChip, selectedType === f.key && styles.filterChipActive]}
onPress={() => setSelectedType(f.key)}
>
<Text style={styles.filterIcon}>{f.icon}</Text>
<Text style={[styles.filterLabel, selectedType === f.key && styles.filterLabelActive]}>{f.label}</Text>
</TouchableOpacity>
))}
</ScrollView>
{/* Sensors list */}
<ScrollView style={styles.list} showsVerticalScrollIndicator={false}>
{filtered.map((sensor) => (
<TouchableOpacity key={sensor.id} style={styles.sensorCard}>
<View style={[styles.sensorIconBg, { backgroundColor: Colors.primary[50] }]}>
<Text style={styles.sensorEmoji}>
{sensor.type === 'temperature' ? '🌡️' :
sensor.type === 'humidity' ? '💧' :
sensor.type === 'air_quality' ? '🌬️' :
sensor.type === 'noise' ? '🔊' :
sensor.type === 'traffic' ? '🚗' : '⚡'}
</Text>
</View>
<View style={styles.sensorInfo}>
<View style={styles.sensorHeader}>
<Text style={styles.sensorName}>{sensor.name}</Text>
<View style={[styles.statusDot, { backgroundColor: STATUS_DOT[sensor.status] }]} />
</View>
<Text style={styles.sensorLocation}>📍 {sensor.location}</Text>
<View style={styles.sensorRow}>
<Text style={styles.sensorValue}>
{sensor.value} <Text style={styles.sensorUnit}>{sensor.unit}</Text>
</Text>
<Text style={styles.sensorUpdate}>
{new Date(sensor.lastUpdate).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
</Text>
</View>
</View>
</TouchableOpacity>
))}
<View style={{ height: 40 }} />
</ScrollView>
</View>
);
}
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,
},
backText: { color: Colors.white, fontSize: Typography.sizes.base },
title: { flex: 1, fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white },
count: { color: 'rgba(255,255,255,0.8)', fontSize: Typography.sizes.base },
searchBar: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.white, marginHorizontal: Spacing.base,
marginTop: Spacing.base, borderRadius: BorderRadius.full,
paddingHorizontal: Spacing.base, height: 40,
...Shadows.sm,
},
searchIcon: { fontSize: 14, marginRight: Spacing.sm },
searchInput: { flex: 1, fontSize: Typography.sizes.sm, color: Colors.neutral900 },
filtersScroll: { padding: Spacing.base, paddingBottom: Spacing.sm },
filterChip: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.white, borderRadius: BorderRadius.full,
paddingHorizontal: Spacing.base, paddingVertical: Spacing.xs,
marginRight: Spacing.sm, gap: 4, ...Shadows.sm,
},
filterChipActive: { backgroundColor: Colors.primary[500] },
filterIcon: { fontSize: 14 },
filterLabel: { fontSize: Typography.sizes.sm, color: Colors.neutral600 },
filterLabelActive: { color: Colors.white },
list: { flex: 1, paddingHorizontal: Spacing.base },
sensorCard: {
flexDirection: 'row', backgroundColor: Colors.white,
borderRadius: BorderRadius.lg, padding: Spacing.base,
marginBottom: Spacing.sm, alignItems: 'center', gap: Spacing.base,
...Shadows.sm,
},
sensorIconBg: {
width: 48, height: 48, borderRadius: BorderRadius.lg,
justifyContent: 'center', alignItems: 'center',
},
sensorEmoji: { fontSize: 20 },
sensorInfo: { flex: 1 },
sensorHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
sensorName: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.bold, color: Colors.neutral900, flex: 1 },
statusDot: { width: 8, height: 8, borderRadius: 4 },
sensorLocation: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: 2 },
sensorRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 4 },
sensorValue: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.primary[500] },
sensorUnit: { fontSize: Typography.sizes.xs, fontWeight: '400', color: Colors.neutral500 },
sensorUpdate: { fontSize: Typography.sizes.xs, color: Colors.neutral400 },
});

View File

@@ -0,0 +1,84 @@
// Smart App City — Zones Screen
import React from 'react';
import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors';
import { useIoTStore } from '../../../stores/iotStore';
type Props = { navigation: NativeStackNavigationProp<any> };
export default function ZonesScreen({ navigation }: Props) {
const zones = useIoTStore((s) => s.zones);
const sensors = useIoTStore((s) => s.sensors);
return (
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text style={styles.backText}> Retour</Text>
</TouchableOpacity>
<Text style={styles.title}>Zones</Text>
<Text style={styles.count}>{zones.length}</Text>
</View>
<ScrollView style={styles.list} showsVerticalScrollIndicator={false}>
{zones.map((zone) => {
const zoneSensors = sensors.filter((s) => s.zoneId === zone.id);
return (
<TouchableOpacity key={zone.id} style={styles.zoneCard}>
<View style={[styles.zoneColorBar, { backgroundColor: zone.color }]} />
<View style={styles.zoneContent}>
<Text style={styles.zoneName}>{zone.name}</Text>
<Text style={styles.zoneDesc}>{zone.description}</Text>
<View style={styles.zoneStats}>
<View style={styles.zoneStat}>
<Text style={styles.zoneStatValue}>{zoneSensors.length}</Text>
<Text style={styles.zoneStatLabel}>Capteurs</Text>
</View>
<View style={styles.zoneStat}>
<Text style={[styles.zoneStatValue, { color: zone.alertCount > 0 ? Colors.danger : Colors.success }]}>
{zone.alertCount}
</Text>
<Text style={styles.zoneStatLabel}>Alertes</Text>
</View>
<View style={styles.zoneStat}>
<Text style={styles.zoneStatValue}>{(zone.radius / 1000).toFixed(1)}km</Text>
<Text style={styles.zoneStatLabel}>Rayon</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
})}
<View style={{ height: 40 }} />
</ScrollView>
</View>
);
}
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,
},
backText: { color: Colors.white, fontSize: Typography.sizes.base },
title: { flex: 1, fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white },
count: { color: 'rgba(255,255,255,0.8)', fontSize: Typography.sizes.base },
list: { flex: 1, padding: Spacing.base },
zoneCard: {
flexDirection: 'row', backgroundColor: Colors.white,
borderRadius: BorderRadius.lg, marginBottom: Spacing.sm,
...Shadows.sm, overflow: 'hidden',
},
zoneColorBar: { width: 4 },
zoneContent: { flex: 1, padding: Spacing.base },
zoneName: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
zoneDesc: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: 2, marginBottom: Spacing.sm },
zoneStats: { flexDirection: 'row', gap: Spacing.base },
zoneStat: { alignItems: 'center' },
zoneStatValue: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.primary[500] },
zoneStatLabel: { fontSize: Typography.sizes.xs, color: Colors.neutral400, marginTop: 2 },
});

View File

@@ -0,0 +1,155 @@
// Smart App City — Marketplace Screen
import React, { useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, TextInput } 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> };
const CATEGORIES = [
{ id: 'all', label: 'Tous', icon: '🏪' },
{ id: 'food', label: 'Alimentation', icon: '🍎' },
{ id: 'transport', label: 'Transport', icon: '🚌' },
{ id: 'energy', label: 'Énergie', icon: '⚡' },
{ id: 'health', label: 'Santé', icon: '🏥' },
{ id: 'culture', label: 'Culture', icon: '🎭' },
];
const PRODUCTS = [
{ id: 'p1', name: 'Panier Bio Local', provider: 'AMAP Martinique', price: '15€', unit: '/semaine', category: 'food', icon: '🥬', rating: 4.8 },
{ id: 'p2', name: 'Pass Transport', provider: 'CFTA', price: '45€', unit: '/mois', category: 'transport', icon: '🎫', rating: 4.2 },
{ id: 'p3', name: 'Kit Solaire', provider: 'EDF DOM', price: '299€', unit: '', category: 'energy', icon: '☀️', rating: 4.6 },
{ id: 'p4', name: 'Consultation Télémédecine', provider: 'Santé+', price: '25€', unit: '/consult', category: 'health', icon: '🩺', rating: 4.5 },
{ id: 'p5', name: 'Billet Musée', provider: 'Musée de la Pagerie', price: '8€', unit: '', category: 'culture', icon: '🏛️', rating: 4.7 },
];
export default function MarketplaceScreen({ navigation }: Props) {
const [search, setSearch] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const filtered = PRODUCTS.filter(
(p) =>
(selectedCategory === 'all' || p.category === selectedCategory) &&
(search === '' || p.name.toLowerCase().includes(search.toLowerCase()) || p.provider.toLowerCase().includes(search.toLowerCase()))
);
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Marketplace</Text>
<Text style={styles.subtitle}>Services & Produits locaux</Text>
<View style={styles.searchBar}>
<Text style={styles.searchIcon}>🔍</Text>
<TextInput
style={styles.searchInput}
placeholder="Rechercher un service..."
placeholderTextColor={Colors.neutral400}
value={search}
onChangeText={setSearch}
/>
</View>
</View>
{/* Categories */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.categoriesScroll}>
{CATEGORIES.map((cat) => (
<TouchableOpacity
key={cat.id}
style={[styles.categoryChip, selectedCategory === cat.id && styles.categoryChipActive]}
onPress={() => setSelectedCategory(cat.id)}
>
<Text style={styles.categoryIcon}>{cat.icon}</Text>
<Text style={[styles.categoryLabel, selectedCategory === cat.id && styles.categoryLabelActive]}>
{cat.label}
</Text>
</TouchableOpacity>
))}
</ScrollView>
{/* Products */}
<ScrollView style={styles.productsList} showsVerticalScrollIndicator={false}>
{filtered.map((product) => (
<TouchableOpacity key={product.id} style={styles.productCard}>
<View style={styles.productIcon}>
<Text style={styles.productEmoji}>{product.icon}</Text>
</View>
<View style={styles.productInfo}>
<Text style={styles.productName}>{product.name}</Text>
<Text style={styles.productProvider}>{product.provider}</Text>
<View style={styles.productRow}>
<Text style={styles.productPrice}>{product.price}<Text style={styles.productUnit}>{product.unit}</Text></Text>
<Text style={styles.productRating}> {product.rating}</Text>
</View>
</View>
<TouchableOpacity style={styles.addBtn}>
<Text style={styles.addBtnText}>+</Text>
</TouchableOpacity>
</TouchableOpacity>
))}
<View style={{ height: 80 }} />
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.neutral50 },
header: {
backgroundColor: Colors.primary[500],
paddingTop: 50,
paddingBottom: Spacing.base,
paddingHorizontal: Spacing.base,
borderBottomLeftRadius: BorderRadius.xl,
borderBottomRightRadius: BorderRadius.xl,
},
title: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white },
subtitle: { fontSize: Typography.sizes.sm, color: 'rgba(255,255,255,0.8)', marginBottom: Spacing.sm },
searchBar: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: BorderRadius.full,
paddingHorizontal: Spacing.base,
height: 36,
},
searchIcon: { fontSize: 14, marginRight: Spacing.sm },
searchInput: { flex: 1, color: Colors.white, fontSize: Typography.sizes.sm, height: 36 },
categoriesScroll: { padding: Spacing.base, paddingBottom: Spacing.sm },
categoryChip: {
alignItems: 'center', backgroundColor: Colors.white,
borderRadius: BorderRadius.lg, paddingHorizontal: Spacing.base,
paddingVertical: Spacing.sm, marginRight: Spacing.sm,
borderWidth: 1, borderColor: Colors.neutral200,
},
categoryChipActive: { backgroundColor: Colors.primary[500], borderColor: Colors.primary[500] },
categoryIcon: { fontSize: 20 },
categoryLabel: { fontSize: 10, color: Colors.neutral600, marginTop: 2 },
categoryLabelActive: { color: Colors.white },
productsList: { flex: 1, paddingHorizontal: Spacing.base },
productCard: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.white, borderRadius: BorderRadius.lg,
padding: Spacing.base, marginBottom: Spacing.sm,
...Shadows.sm, borderWidth: 1, borderColor: Colors.neutral100,
},
productIcon: {
width: 52, height: 52, borderRadius: BorderRadius.lg,
backgroundColor: Colors.primary[50],
justifyContent: 'center', alignItems: 'center',
marginRight: Spacing.base,
},
productEmoji: { fontSize: 24 },
productInfo: { flex: 1 },
productName: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
productProvider: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: 2 },
productRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 4 },
productPrice: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.primary[500] },
productUnit: { fontSize: Typography.sizes.xs, fontWeight: '400', color: Colors.neutral500 },
productRating: { fontSize: Typography.sizes.sm, color: Colors.neutral600 },
addBtn: {
width: 36, height: 36, borderRadius: BorderRadius.md,
backgroundColor: Colors.primary[500],
justifyContent: 'center', alignItems: 'center',
},
addBtnText: { color: Colors.white, fontSize: 20, fontWeight: '700' },
});

View File

@@ -0,0 +1,12 @@
import React from 'react';
import { View, Text } from 'react-native';
const NotificationPrefsScreen = () => {
return (
<View>
<Text>NotificationPrefsScreen</Text>
</View>
);
};
export default NotificationPrefsScreen;

View File

@@ -0,0 +1,166 @@
// Smart App City — Notifications Screen
import React, { useState } from 'react';
import {
View, Text, ScrollView, TouchableOpacity,
StyleSheet, RefreshControl,
} from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors';
import { useNotifications } from '../../../hooks/useAuth';
type Props = { navigation: NativeStackNavigationProp<any> };
const TYPE_CONFIG = {
alert: { icon: '🚨', color: Colors.danger },
info: { icon: '', color: Colors.info },
event: { icon: '🎉', color: Colors.indigo[500] },
system: { icon: '⚙️', color: Colors.neutral500 },
};
export default function NotificationsScreen({ navigation }: Props) {
const { notifications, unreadCount, markAsRead, markAllAsRead, removeNotification, clearAll } = useNotifications();
const [refreshing, setRefreshing] = useState(false);
const [filter, setFilter] = useState<'all' | 'unread'>('all');
const filtered = filter === 'unread' ? notifications.filter((n) => !n.read) : notifications;
const onRefresh = async () => {
setRefreshing(true);
await new Promise((r) => setTimeout(r, 1000));
setRefreshing(false);
};
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text style={styles.backText}> Retour</Text>
</TouchableOpacity>
<Text style={styles.title}>Notifications</Text>
{unreadCount > 0 && (
<TouchableOpacity onPress={markAllAsRead}>
<Text style={styles.markAll}>Tout lu</Text>
</TouchableOpacity>
)}
</View>
{/* Actions */}
<View style={styles.actions}>
<View style={styles.tabs}>
<TouchableOpacity
style={[styles.tab, filter === 'all' && styles.tabActive]}
onPress={() => setFilter('all')}
>
<Text style={[styles.tabText, filter === 'all' && styles.tabTextActive]}>
Toutes ({notifications.length})
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, filter === 'unread' && styles.tabActive]}
onPress={() => setFilter('unread')}
>
<Text style={[styles.tabText, filter === 'unread' && styles.tabTextActive]}>
Non lues ({unreadCount})
</Text>
</TouchableOpacity>
</View>
{notifications.length > 0 && (
<TouchableOpacity onPress={clearAll}>
<Text style={styles.clearAll}>Supprimer tout</Text>
</TouchableOpacity>
)}
</View>
{/* List */}
<ScrollView
style={styles.list}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.primary[500]} />}
showsVerticalScrollIndicator={false}
>
{filtered.length === 0 ? (
<View style={styles.empty}>
<Text style={styles.emptyEmoji}>🔔</Text>
<Text style={styles.emptyText}>Aucune notification</Text>
</View>
) : (
filtered.map((notif) => {
const config = TYPE_CONFIG[notif.type];
return (
<TouchableOpacity
key={notif.id}
style={[styles.notifCard, !notif.read && styles.notifUnread]}
onPress={() => markAsRead(notif.id)}
>
<Text style={styles.notifIcon}>{config.icon}</Text>
<View style={styles.notifContent}>
<View style={styles.notifHeader}>
<Text style={[styles.notifTitle, !notif.read && { fontWeight: '700' }]}>{notif.title}</Text>
<TouchableOpacity onPress={() => removeNotification(notif.id)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
<Text style={styles.removeIcon}></Text>
</TouchableOpacity>
</View>
<Text style={styles.notifBody} numberOfLines={2}>{notif.body}</Text>
<Text style={styles.notifTime}>
{new Date(notif.createdAt).toLocaleString('fr-FR', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
})}
</Text>
</View>
{!notif.read && <View style={styles.unreadDot} />}
</TouchableOpacity>
);
})
)}
<View style={{ height: 40 }} />
</ScrollView>
</View>
);
}
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,
},
backText: { color: Colors.white, fontSize: Typography.sizes.base },
title: { flex: 1, fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white },
markAll: { color: Colors.white, fontSize: Typography.sizes.sm, fontWeight: '600' },
actions: {
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
backgroundColor: Colors.white,
paddingHorizontal: Spacing.base, paddingTop: Spacing.base,
},
tabs: { flexDirection: 'row', gap: Spacing.sm },
tab: {
paddingHorizontal: Spacing.base, paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full, backgroundColor: Colors.neutral100,
},
tabActive: { backgroundColor: Colors.primary[500] },
tabText: { fontSize: Typography.sizes.sm, color: Colors.neutral600 },
tabTextActive: { color: Colors.white, fontWeight: '600' },
clearAll: { fontSize: Typography.sizes.sm, color: Colors.danger },
list: { flex: 1, padding: Spacing.base },
empty: { alignItems: 'center', paddingTop: Spacing.xxxl },
emptyEmoji: { fontSize: 48, marginBottom: Spacing.base },
emptyText: { fontSize: Typography.sizes.base, color: Colors.neutral500 },
notifCard: {
flexDirection: 'row', backgroundColor: Colors.white,
borderRadius: BorderRadius.lg, padding: Spacing.base,
marginBottom: Spacing.sm, ...Shadows.sm,
alignItems: 'flex-start', gap: Spacing.base,
},
notifUnread: { borderLeftWidth: 3, borderLeftColor: Colors.primary[500] },
notifIcon: { fontSize: 24, marginTop: 2 },
notifContent: { flex: 1 },
notifHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: Spacing.xs },
notifTitle: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.semibold, color: Colors.neutral900, flex: 1 },
removeIcon: { fontSize: 14, color: Colors.neutral400, padding: Spacing.xs },
notifBody: { fontSize: Typography.sizes.sm, color: Colors.neutral600, lineHeight: 18, marginBottom: Spacing.xs },
notifTime: { fontSize: Typography.sizes.xs, color: Colors.neutral400 },
unreadDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: Colors.primary[500], marginTop: 4 },
});

View File

@@ -0,0 +1,187 @@
// Smart App City — Profile Screen
import React from 'react';
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Switch } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useAuthStore } from '../../stores/authStore';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
type Props = { navigation: NativeStackNavigationProp<any> };
const MENU_ITEMS = [
{ id: 'notifications', label: 'Notifications', icon: '🔔', type: 'toggle' },
{ id: 'darkmode', label: 'Mode sombre', icon: '🌙', type: 'toggle' },
{ id: 'language', label: 'Langue', icon: '🌍', value: 'Français', type: 'nav' },
{ id: 'zones', label: 'Mes zones', icon: '📍', type: 'nav' },
{ id: 'sensors', label: 'Mes capteurs', icon: '📡', type: 'nav' },
{ id: 'history', label: 'Historique', icon: '📊', type: 'nav' },
{ id: 'privacy', label: 'Confidentialité', icon: '🔒', type: 'nav' },
{ id: 'help', label: 'Aide & Support', icon: '❓', type: 'nav' },
{ id: 'about', label: 'À propos', icon: '', type: 'nav' },
];
export default function ProfileScreen({ navigation }: Props) {
const [toggles, setToggles] = React.useState<Record<string, boolean>>({
notifications: true,
darkmode: false,
});
const logout = useAuthStore((s) => s.logout);
const user = useAuthStore((s) => s.user);
const handleToggle = (id: string) => {
setToggles((prev) => ({ ...prev, [id]: !prev[id] }));
};
return (
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
{/* Header */}
<View style={styles.header}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>👤</Text>
</View>
<Text style={styles.name}>{user?.firstName ? `${user.firstName} ${user.lastName}` : 'Eric F.'}</Text>
<Text style={styles.email}>{user?.email ?? 'eric@digitribe.fr'}</Text>
<View style={styles.badgeRow}>
<View style={styles.badge}>
<Text style={styles.badgeText}>🏙 Citoyen</Text>
</View>
<View style={styles.badge}>
<Text style={styles.badgeText}> Vérifié</Text>
</View>
</View>
</View>
{/* Stats */}
<View style={styles.statsRow}>
<View style={styles.statCard}>
<Text style={styles.statValue}>12</Text>
<Text style={styles.statLabel}>Signalements</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>45</Text>
<Text style={styles.statLabel}>Points</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>5</Text>
<Text style={styles.statLabel}>Abonnements</Text>
</View>
</View>
{/* Menu */}
<View style={styles.menuSection}>
{MENU_ITEMS.map((item) => (
<TouchableOpacity key={item.id} style={styles.menuItem} disabled={item.type === 'toggle'}>
<View style={styles.menuLeft}>
<Text style={styles.menuIcon}>{item.icon}</Text>
<Text style={styles.menuLabel}>{item.label}</Text>
</View>
{item.type === 'toggle' ? (
<Switch
value={toggles[item.id] ?? false}
onValueChange={() => handleToggle(item.id)}
trackColor={{ false: Colors.neutral200, true: Colors.primary[300] }}
thumbColor={toggles[item.id] ? Colors.primary[500] : Colors.neutral400}
/>
) : (
<View style={styles.menuRight}>
{item.value && <Text style={styles.menuValue}>{item.value}</Text>}
<Text style={styles.menuArrow}></Text>
</View>
)}
</TouchableOpacity>
))}
</View>
{/* Logout */}
<TouchableOpacity style={styles.logoutBtn} onPress={logout}>
<Text style={styles.logoutIcon}>🚪</Text>
<Text style={styles.logoutText}>Se déconnecter</Text>
</TouchableOpacity>
<Text style={styles.version}>Smart App City v0.1.0</Text>
<View style={{ height: 40 }} />
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.neutral50 },
header: {
backgroundColor: Colors.primary[500],
paddingTop: 50,
paddingBottom: Spacing.xxl,
paddingHorizontal: Spacing.xl,
alignItems: 'center',
borderBottomLeftRadius: BorderRadius.xxl,
borderBottomRightRadius: BorderRadius.xxl,
},
avatar: {
width: 80, height: 80, borderRadius: BorderRadius.xl,
backgroundColor: 'rgba(255,255,255,0.2)',
justifyContent: 'center', alignItems: 'center',
marginBottom: Spacing.base,
},
avatarText: { fontSize: 40 },
name: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.white },
email: { fontSize: Typography.sizes.sm, color: 'rgba(255,255,255,0.7)', marginTop: Spacing.xs },
badgeRow: { flexDirection: 'row', gap: Spacing.sm, marginTop: Spacing.base },
badge: {
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: BorderRadius.full,
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.xs,
},
badgeText: { fontSize: Typography.sizes.sm, color: Colors.white },
statsRow: {
flexDirection: 'row',
paddingHorizontal: Spacing.base,
marginTop: -20,
marginBottom: Spacing.base,
gap: Spacing.sm,
},
statCard: {
flex: 1, backgroundColor: Colors.white,
borderRadius: BorderRadius.lg, padding: Spacing.base,
alignItems: 'center', ...Shadows.md,
},
statValue: { fontSize: Typography.sizes.xxl, fontWeight: Typography.weights.bold, color: Colors.primary[500] },
statLabel: { fontSize: Typography.sizes.xs, color: Colors.neutral500, marginTop: 2 },
menuSection: {
backgroundColor: Colors.white,
marginHorizontal: Spacing.base,
borderRadius: BorderRadius.xl,
...Shadows.sm,
overflow: 'hidden',
},
menuItem: {
flexDirection: 'row', justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.base,
borderBottomWidth: 1, borderBottomColor: Colors.neutral100,
},
menuLeft: { flexDirection: 'row', alignItems: 'center', gap: Spacing.base },
menuIcon: { fontSize: 20 },
menuLabel: { fontSize: Typography.sizes.base, color: Colors.neutral900 },
menuRight: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm },
menuValue: { fontSize: Typography.sizes.sm, color: Colors.neutral500 },
menuArrow: { fontSize: 22, color: Colors.neutral300 },
logoutBtn: {
flexDirection: 'row', alignItems: 'center',
justifyContent: 'center',
backgroundColor: Colors.white,
marginHorizontal: Spacing.base,
marginTop: Spacing.base,
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.base,
gap: Spacing.sm,
borderWidth: 1, borderColor: '#FFCDD2',
},
logoutIcon: { fontSize: 18 },
logoutText: { fontSize: Typography.sizes.base, color: Colors.danger, fontWeight: Typography.weights.medium },
version: {
textAlign: 'center',
fontSize: Typography.sizes.xs,
color: Colors.neutral400,
marginTop: Spacing.base,
},
});

View File

@@ -0,0 +1,121 @@
// Smart App City — Settings Screen
import React, { useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, Switch, StyleSheet } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors';
import { useUIStore, Language } from '../../../stores/uiStore';
type Props = { navigation: NativeStackNavigationProp<any> };
export default function SettingsScreen({ navigation }: Props) {
const language = useUIStore((s) => s.language);
const setLanguage = useUIStore((s) => s.setLanguage);
const [showLangPicker, setShowLangPicker] = useState(false);
const languages: { key: Language; label: string; flag: string }[] = [
{ key: 'fr', label: 'Français', flag: '🇫🇷' },
{ key: 'en', label: 'English', flag: '🇬🇧' },
{ key: 'es', label: 'Español', flag: '🇪🇸' },
{ key: 'de', label: 'Deutsch', flag: '🇩🇪' },
];
return (
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text style={styles.backText}> Retour</Text>
</TouchableOpacity>
<Text style={styles.title}>Paramètres</Text>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Language */}
<TouchableOpacity style={styles.row} onPress={() => setShowLangPicker(!showLangPicker)}>
<Text style={styles.rowIcon}>🌍</Text>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>Langue</Text>
<Text style={styles.rowValue}>{languages.find((l) => l.key === language)?.flag} {languages.find((l) => l.key === language)?.label}</Text>
</View>
<Text style={styles.rowArrow}></Text>
</TouchableOpacity>
{showLangPicker && (
<View style={styles.langPicker}>
{languages.map((lang) => (
<TouchableOpacity
key={lang.key}
style={[styles.langOption, language === lang.key && styles.langOptionActive]}
onPress={() => { setLanguage(lang.key); setShowLangPicker(false); }}
>
<Text style={styles.langFlag}>{lang.flag}</Text>
<Text style={[styles.langLabel, language === lang.key && styles.langLabelActive]}>{lang.label}</Text>
{language === lang.key && <Text style={styles.langCheck}></Text>}
</TouchableOpacity>
))}
</View>
)}
<View style={styles.divider} />
{/* Privacy */}
<TouchableOpacity style={styles.row}>
<Text style={styles.rowIcon}>🔒</Text>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>Confidentialité</Text>
<Text style={styles.rowValue}>Gérer vos données</Text>
</View>
<Text style={styles.rowArrow}></Text>
</TouchableOpacity>
{/* About */}
<TouchableOpacity style={styles.row}>
<Text style={styles.rowIcon}></Text>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>À propos</Text>
<Text style={styles.rowValue}>v0.1.0</Text>
</View>
<Text style={styles.rowArrow}></Text>
</TouchableOpacity>
<View style={{ height: 40 }} />
</ScrollView>
</View>
);
}
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,
},
backText: { color: Colors.white, fontSize: Typography.sizes.base },
title: { flex: 1, fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white },
row: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.white,
paddingHorizontal: Spacing.base,
paddingVertical: Spacing.base,
borderBottomWidth: 1, borderBottomColor: Colors.neutral100,
gap: Spacing.base,
},
rowIcon: { fontSize: 20 },
rowContent: { flex: 1 },
rowLabel: { fontSize: Typography.sizes.base, color: Colors.neutral900 },
rowValue: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: 2 },
rowArrow: { fontSize: 22, color: Colors.neutral300 },
divider: { height: Spacing.base, backgroundColor: Colors.neutral100 },
langPicker: { backgroundColor: Colors.white, paddingHorizontal: Spacing.base },
langOption: {
flexDirection: 'row', alignItems: 'center',
paddingVertical: Spacing.base, gap: Spacing.base,
borderBottomWidth: 1, borderBottomColor: Colors.neutral100,
},
langOptionActive: { backgroundColor: Colors.primary[50] },
langFlag: { fontSize: 20 },
langLabel: { flex: 1, fontSize: Typography.sizes.base, color: Colors.neutral900 },
langLabelActive: { fontWeight: '700', color: Colors.primary[500] },
langCheck: { fontSize: 16, color: Colors.primary[500], fontWeight: '700' },
});