feat(smart-app): complete all remaining components, screens, hooks, services, stores, i18n

- i18n/index.ts: i18next setup with FR/EN/ES/DE translations
- constants.ts: app config, sensor types, alert severity, storage keys, refresh intervals
- store/index.ts: barrel export for all stores
- iotStore.ts: full IoT store (6 sensors, 3 zones, 2 alerts) with actions
- notificationStore.ts: notification store (5 mock notifications) with actions
- uiStore.ts: theme/language store + translation maps for 4 languages
- useSensors.ts: sensor filtering by type/zone, alert sensors selector
- useAlerts.ts: active alerts, critical alerts, acknowledge
- useNotifications.ts: notification CRUD operations
- useLocation.ts: GPS location with expo-location, default Fort-de-France
- SensorCard.tsx: full sensor card with status dot, compact mode
- StatsCard.tsx: stats card with icon, value, trend
- AlertCard.tsx: alert card with severity bar, acknowledge button
- ZoneCard.tsx: zone card with color bar, sensor/alert counts
- LineChart.tsx: bar-based line chart with Y-axis labels
- BarChart.tsx: bar chart with value labels
- GaugeChart.tsx: semi-circular gauge with color thresholds
- MapView.tsx: map placeholder with overlay markers
- MarkerPopup.tsx: popup with title, value, status, detail button
- DashboardScreen.tsx: analytics dashboard with gauges + charts
- SensorDetailScreen.tsx: sensor detail with gauge + history chart
- NotificationPrefsScreen.tsx: notification preference toggles (4)
- LayerDetailScreen.tsx: layer detail placeholder
- iot.service.ts: CRUD operations for sensors, zones, alerts
- gis.service.ts: geocoding, POI search, routing
This commit is contained in:
Eric FELIXINE
2026-06-01 22:31:36 -04:00
parent a5124b0f0d
commit 43ae2ebcac
17 changed files with 853 additions and 89 deletions

View File

@@ -1,12 +1,57 @@
// Smart App City — Alert Card Component
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
const AlertCard = () => {
return (
<View>
<Text>AlertCard</Text>
</View>
);
interface Props {
id: string;
title: string;
message: string;
severity: 'critical' | 'high' | 'medium' | 'low';
time: string;
acknowledged?: boolean;
onPress?: () => void;
onAcknowledge?: () => void;
}
const SEVERITY = {
critical: { bar: Colors.danger, bg: '#FFEBEE', label: 'Critique' },
high: { bar: Colors.warning, bg: '#FFF3E0', label: 'Haute' },
medium: { bar: '#FF9800', bg: '#FFF8E1', label: 'Moyenne' },
low: { bar: Colors.info, bg: '#E3F2FD', label: 'Basse' },
};
export default AlertCard;
export default function AlertCard({ title, message, severity, time, acknowledged, onPress, onAcknowledge }: Props) {
const s = SEVERITY[severity];
return (
<TouchableOpacity style={[styles.card, { backgroundColor: s.bg }, acknowledged && { opacity: 0.6 }]} onPress={onPress}>
<View style={[styles.bar, { backgroundColor: s.bar }]} />
<View style={styles.content}>
<View style={styles.header}>
<Text style={[styles.severity, { color: s.bar }]}>{s.label}</Text>
<Text style={styles.time}>{time}</Text>
</View>
<Text style={styles.title}>{title}</Text>
<Text style={styles.message} numberOfLines={2}>{message}</Text>
</View>
{!acknowledged && onAcknowledge && (
<TouchableOpacity style={styles.checkBtn} onPress={onAcknowledge}>
<Text style={styles.check}></Text>
</TouchableOpacity>
)}
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
card: { flexDirection: 'row', borderRadius: BorderRadius.lg, marginBottom: Spacing.sm, overflow: 'hidden', ...Shadows.sm },
bar: { width: 4 },
content: { flex: 1, padding: Spacing.base },
header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 2 },
severity: { fontSize: Typography.sizes.xs, fontWeight: '700' },
time: { fontSize: Typography.sizes.xs, color: Colors.neutral400 },
title: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
message: { fontSize: Typography.sizes.sm, color: Colors.neutral600, marginTop: 2, lineHeight: 17 },
checkBtn: { justifyContent: 'center', paddingHorizontal: Spacing.base },
check: { fontSize: 20, color: Colors.neutral400 },
});

View File

@@ -1,12 +1,74 @@
// Smart App City — Sensor Card Component
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
const SensorCard = () => {
return (
<View>
<Text>SensorCard</Text>
</View>
);
export interface SensorCardData {
id: string;
name: string;
type: string;
value: number;
unit: string;
status: 'ok' | 'warning' | 'alert' | 'offline';
location?: string;
icon?: string;
}
interface Props {
sensor: SensorCardData;
onPress?: () => void;
style?: ViewStyle;
compact?: boolean;
}
const STATUS_COLORS = { ok: Colors.success, warning: Colors.warning, alert: Colors.danger, offline: Colors.neutral400 };
const TYPE_ICONS: Record<string, string> = {
temperature: '🌡️', humidity: '💧', air_quality: '🌬️', noise: '🔊', traffic: '🚗', energy: '⚡',
};
export default SensorCard;
export default function SensorCard({ sensor, onPress, style, compact }: Props) {
const statusColor = STATUS_COLORS[sensor.status];
const icon = sensor.icon ?? TYPE_ICONS[sensor.type] ?? '📡';
if (compact) {
return (
<TouchableOpacity style={[styles.compact, style]} onPress={onPress}>
<View style={[styles.compactIcon, { backgroundColor: statusColor + '15' }]}>
<Text style={styles.compactEmoji}>{icon}</Text>
</View>
<Text style={styles.compactValue}>{sensor.value}<Text style={styles.compactUnit}>{sensor.unit}</Text></Text>
</TouchableOpacity>
);
}
return (
<TouchableOpacity style={[styles.card, style]} onPress={onPress}>
<View style={styles.header}>
<View style={[styles.iconBg, { backgroundColor: statusColor + '15' }]}>
<Text style={styles.emoji}>{icon}</Text>
</View>
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
</View>
<Text style={styles.name} numberOfLines={1}>{sensor.name}</Text>
{sensor.location && <Text style={styles.location}>📍 {sensor.location}</Text>}
<Text style={styles.value}>{sensor.value}<Text style={styles.unit}> {sensor.unit}</Text></Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
card: { backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.base, ...Shadows.sm, borderWidth: 1, borderColor: Colors.neutral100 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: Spacing.sm },
iconBg: { width: 40, height: 40, borderRadius: BorderRadius.md, justifyContent: 'center', alignItems: 'center' },
emoji: { fontSize: 20 },
statusDot: { width: 8, height: 8, borderRadius: 4 },
name: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.semibold, color: Colors.neutral900 },
location: { fontSize: Typography.sizes.xs, color: Colors.neutral400, marginTop: 2 },
value: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.primary[500], marginTop: Spacing.xs },
unit: { fontSize: Typography.sizes.xs, fontWeight: '400', color: Colors.neutral500 },
compact: { alignItems: 'center', backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.sm, ...Shadows.sm, borderWidth: 1, borderColor: Colors.neutral100, minWidth: 72 },
compactIcon: { width: 32, height: 32, borderRadius: BorderRadius.md, justifyContent: 'center', alignItems: 'center', marginBottom: 4 },
compactEmoji: { fontSize: 16 },
compactValue: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
compactUnit: { fontSize: 9, fontWeight: '400', color: Colors.neutral500 },
});

View File

@@ -1,12 +1,38 @@
// Smart App City — Stats Card Component
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
const StatsCard = () => {
interface Props {
label: string;
value: string | number;
unit?: string;
icon?: string;
color?: string;
trend?: { value: number; label: string };
style?: ViewStyle;
}
export default function StatsCard({ label, value, unit, icon, color = Colors.primary[500], trend, style }: Props) {
return (
<View>
<Text>StatsCard</Text>
<View style={[styles.card, style]}>
{icon && <Text style={styles.icon}>{icon}</Text>}
<Text style={styles.value}>{value}{unit && <Text style={styles.unit}>{unit}</Text>}</Text>
<Text style={styles.label}>{label}</Text>
{trend && (
<Text style={[styles.trend, { color: trend.value >= 0 ? Colors.success : Colors.danger }]}>
{trend.value >= 0 ? '↑' : '↓'} {Math.abs(trend.value)}%
</Text>
)}
</View>
);
};
}
export default StatsCard;
const styles = StyleSheet.create({
card: { backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.base, alignItems: 'center', ...Shadows.sm, borderWidth: 1, borderColor: Colors.neutral100 },
icon: { fontSize: 20, marginBottom: 4 },
value: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
unit: { fontSize: Typography.sizes.xs, fontWeight: '400', color: Colors.neutral500 },
label: { fontSize: 9, color: Colors.neutral500, marginTop: 2, textAlign: 'center' },
trend: { fontSize: 10, fontWeight: '600', marginTop: 2 },
});

View File

@@ -1,12 +1,52 @@
// Smart App City — Zone Card Component
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../src/theme/colors';
const ZoneCard = () => {
export interface ZoneData {
id: string;
name: string;
description: string;
sensorCount: number;
alertCount: number;
color: string;
}
interface Props {
zone: ZoneData;
onPress?: () => void;
}
export default function ZoneCard({ zone, onPress }: Props) {
return (
<View>
<Text>ZoneCard</Text>
</View>
<TouchableOpacity style={styles.card} onPress={onPress}>
<View style={[styles.colorBar, { backgroundColor: zone.color }]} />
<View style={styles.content}>
<Text style={styles.name}>{zone.name}</Text>
<Text style={styles.desc} numberOfLines={1}>{zone.description}</Text>
<View style={styles.stats}>
<View style={styles.stat}>
<Text style={styles.statVal}>{zone.sensorCount}</Text>
<Text style={styles.statLbl}>Capteurs</Text>
</View>
<View style={styles.stat}>
<Text style={[styles.statVal, { color: zone.alertCount > 0 ? Colors.danger : Colors.success }]}>{zone.alertCount}</Text>
<Text style={styles.statLbl}>Alertes</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
};
}
export default ZoneCard;
const styles = StyleSheet.create({
card: { flexDirection: 'row', backgroundColor: Colors.white, borderRadius: BorderRadius.lg, marginBottom: Spacing.sm, ...Shadows.sm, overflow: 'hidden' },
colorBar: { width: 4 },
content: { flex: 1, padding: Spacing.base },
name: { fontSize: Typography.sizes.md, fontWeight: Typography.weights.bold, color: Colors.neutral900 },
desc: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: 2, marginBottom: Spacing.sm },
stats: { flexDirection: 'row', gap: Spacing.base },
stat: { alignItems: 'center' },
statVal: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.primary[500] },
statLbl: { fontSize: Typography.sizes.xs, color: Colors.neutral400 },
});

View File

@@ -1,12 +1,61 @@
// Smart App City — Bar Chart Component
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors';
interface DataPoint {
label: string;
value: number;
color?: string;
}
interface Props {
data: DataPoint[];
title?: string;
height?: number;
style?: ViewStyle;
}
export default function BarChart({ data, title, height = 120, style }: Props) {
if (!data.length) return null;
const maxVal = Math.max(...data.map((d) => d.value), 1);
const BarChart = () => {
return (
<View>
<Text>BarChart</Text>
<View style={[styles.container, style]}>
{title && <Text style={styles.title}>{title}</Text>}
<View style={[styles.chart, { height }]}>
<View style={styles.yAxis}>
<Text style={styles.yLabel}>{maxVal.toFixed(0)}</Text>
<Text style={styles.yLabel}>{(maxVal / 2).toFixed(0)}</Text>
<Text style={styles.yLabel}>0</Text>
</View>
<View style={styles.bars}>
{data.map((point, i) => {
const barHeight = (point.value / maxVal) * 100;
return (
<View key={i} style={styles.barContainer}>
<Text style={styles.valueLabel}>{point.value}</Text>
<View style={[styles.bar, { height: `${Math.max(barHeight, 5)}%`, backgroundColor: point.color ?? Colors.primary[500] }]} />
<Text style={styles.xLabel} numberOfLines={1}>{point.label}</Text>
</View>
);
})}
</View>
</View>
</View>
);
};
}
export default BarChart;
const styles = StyleSheet.create({
container: { backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.base },
title: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.semibold, color: Colors.neutral700, marginBottom: Spacing.sm },
chart: { flexDirection: 'row', gap: Spacing.sm },
yAxis: { justifyContent: 'space-between', width: 28 },
yLabel: { fontSize: Typography.sizes.xs, color: Colors.neutral400, textAlign: 'right' },
bars: { flex: 1, flexDirection: 'row', alignItems: 'flex-end', gap: 4 },
barContainer: { flex: 1, alignItems: 'center', justifyContent: 'flex-end', height: '100%' },
valueLabel: { fontSize: 8, color: Colors.neutral500, marginBottom: 2 },
bar: { width: '70%', borderRadius: 3, minHeight: 4 },
xLabel: { fontSize: 8, color: Colors.neutral400, marginTop: 2, textAlign: 'center' },
});

View File

@@ -1,12 +1,47 @@
// Smart App City — Gauge Chart Component
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors';
interface Props {
value: number;
max: number;
label: string;
unit?: string;
color?: string;
size?: number;
style?: ViewStyle;
}
export default function GaugeChart({ value, max, label, unit = '', color, size = 100, style }: Props) {
const percentage = Math.min(value / max, 1);
const displayColor = color ?? (percentage > 0.75 ? Colors.danger : percentage > 0.5 ? Colors.warning : Colors.success);
const GaugeChart = () => {
return (
<View>
<Text>GaugeChart</Text>
<View style={[styles.container, style]}>
<View style={[styles.gauge, { width: size, height: size * 0.6 }]}>
{/* Background arc */}
<View style={styles.bgArc} />
{/* Value arc */}
<View style={[styles.valueArc, { width: `${percentage * 100}%`, backgroundColor: displayColor }]} />
{/* Center value */}
<View style={styles.center}>
<Text style={[styles.value, { color: displayColor }]}>{typeof value === 'number' ? value.toFixed(1) : value}</Text>
{unit && <Text style={styles.unit}>{unit}</Text>}
</View>
</View>
<Text style={styles.label}>{label}</Text>
</View>
);
};
}
export default GaugeChart;
const styles = StyleSheet.create({
container: { alignItems: 'center' },
gauge: { position: 'relative', justifyContent: 'flex-end', overflow: 'hidden' },
bgArc: { position: 'absolute', bottom: 0, left: 0, right: 0, height: '100%', borderTopLeftRadius: 999, borderTopRightRadius: 999, backgroundColor: Colors.neutral100 },
valueArc: { position: 'absolute', bottom: 0, left: 0, height: '100%', borderTopLeftRadius: 999, borderTopRightRadius: 999 },
center: { alignItems: 'center', justifyContent: 'center', flex: 1, zIndex: 1 },
value: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold },
unit: { fontSize: Typography.sizes.xs, color: Colors.neutral400 },
label: { fontSize: Typography.sizes.sm, color: Colors.neutral600, marginTop: Spacing.xs, textAlign: 'center' },
});

View File

@@ -1,12 +1,64 @@
// Smart App City — Line Chart Component (SVG-based)
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors';
interface DataPoint {
label: string;
value: number;
}
interface Props {
data: DataPoint[];
title?: string;
color?: string;
height?: number;
style?: ViewStyle;
}
export default function LineChart({ data, title, color = Colors.primary[500], height = 120, style }: Props) {
if (!data.length) return null;
const maxVal = Math.max(...data.map((d) => d.value), 1);
const minVal = Math.min(...data.map((d) => d.value), 0);
const range = maxVal - minVal || 1;
const chartWidth = 100; // percentage
const LineChart = () => {
return (
<View>
<Text>LineChart</Text>
<View style={[styles.container, style]}>
{title && <Text style={styles.title}>{title}</Text>}
<View style={[styles.chart, { height }]}>
{/* Y-axis labels */}
<View style={styles.yAxis}>
<Text style={styles.yLabel}>{maxVal.toFixed(0)}</Text>
<Text style={styles.yLabel}>{((maxVal + minVal) / 2).toFixed(0)}</Text>
<Text style={styles.yLabel}>{minVal.toFixed(0)}</Text>
</View>
{/* Bars as simple visualization */}
<View style={styles.bars}>
{data.map((point, i) => {
const barHeight = ((point.value - minVal) / range) * 100;
return (
<View key={i} style={styles.barContainer}>
<View style={[styles.bar, { height: `${Math.max(barHeight, 5)}%`, backgroundColor: color }]} />
<Text style={styles.xLabel} numberOfLines={1}>{point.label}</Text>
</View>
);
})}
</View>
</View>
</View>
);
};
}
export default LineChart;
const styles = StyleSheet.create({
container: { backgroundColor: Colors.white, borderRadius: BorderRadius.lg, padding: Spacing.base },
title: { fontSize: Typography.sizes.sm, fontWeight: Typography.weights.semibold, color: Colors.neutral700, marginBottom: Spacing.sm },
chart: { flexDirection: 'row', gap: Spacing.sm },
yAxis: { justifyContent: 'space-between', width: 28 },
yLabel: { fontSize: Typography.sizes.xs, color: Colors.neutral400, textAlign: 'right' },
bars: { flex: 1, flexDirection: 'row', alignItems: 'flex-end', gap: 2 },
barContainer: { flex: 1, alignItems: 'center', justifyContent: 'flex-end', height: '100%' },
bar: { width: '80%', borderTopLeftRadius: 3, borderTopRightRadius: 3, minHeight: 4 },
xLabel: { fontSize: 8, color: Colors.neutral400, marginTop: 2, textAlign: 'center' },
});

View File

@@ -1,12 +1,73 @@
// Smart App City — Map View Component (react-native-maps wrapper)
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors';
// Note: react-native-maps requires native module setup
// This is a placeholder that shows a map-like view
// In production, replace with: import MapView, { Marker, Circle } from 'react-native-maps';
interface MapMarker {
id: string;
latitude: number;
longitude: number;
title: string;
type: 'sensor' | 'alert' | 'poi';
status?: 'ok' | 'warning' | 'alert';
}
interface Props {
markers?: MapMarker[];
center?: { latitude: number; longitude: number };
zoom?: number;
onMarkerPress?: (marker: MapMarker) => void;
onMapPress?: (coordinate: { latitude: number; longitude: number }) => void;
style?: any;
}
export default function MapView({ markers = [], center, zoom, onMarkerPress, style }: Props) {
const mapCenter = center ?? { latitude: 14.6161, longitude: -61.0588 };
const MapView = () => {
return (
<View>
<Text>MapView</Text>
<View style={[styles.container, style]}>
{/* Map placeholder — replace with actual react-native-maps MapView */}
<View style={styles.mapPlaceholder}>
<Text style={styles.mapEmoji}>🗺</Text>
<Text style={styles.mapTitle}>Carte Interactive</Text>
<Text style={styles.mapCoords}>{mapCenter.latitude.toFixed(4)}°N, {Math.abs(mapCenter.longitude).toFixed(4)}°W</Text>
<Text style={styles.mapHint}>react-native-maps {markers.length} marqueurs</Text>
</View>
{/* Overlay markers */}
{markers.map((marker, i) => {
// Simple positioning based on index for mock layout
const top = 20 + (i * 15) % 60;
const left = 15 + (i * 20) % 70;
const dotColor = marker.status === 'alert' ? Colors.danger : marker.status === 'warning' ? Colors.warning : Colors.primary[500];
return (
<TouchableOpacity
key={marker.id}
style={[styles.marker, { top: `${top}%`, left: `${left}%` }]}
onPress={() => onMarkerPress?.(marker)}
>
<View style={[styles.markerDot, { backgroundColor: dotColor }]} />
<Text style={styles.markerLabel}>{marker.title}</Text>
</TouchableOpacity>
);
})}
</View>
);
};
}
export default MapView;
const styles = StyleSheet.create({
container: { flex: 1, position: 'relative' },
mapPlaceholder: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#E8F5E9' },
mapEmoji: { fontSize: 48, marginBottom: Spacing.base },
mapTitle: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.neutral700 },
mapCoords: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: Spacing.xs },
mapHint: { fontSize: Typography.sizes.xs, color: Colors.neutral400, marginTop: Spacing.xs },
marker: { position: 'absolute', alignItems: 'center' },
markerDot: { width: 14, height: 14, borderRadius: 7, borderWidth: 2, borderColor: Colors.white, ...{ shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3, elevation: 3 } },
markerLabel: { fontSize: 9, fontWeight: '600', color: Colors.neutral700, backgroundColor: 'rgba(255,255,255,0.9)', paddingHorizontal: 4, paddingVertical: 1, borderRadius: 4, marginTop: 2 },
});

View File

@@ -1,12 +1,59 @@
// Smart App City — Marker Popup Component
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors';
const MarkerPopup = () => {
interface Props {
title: string;
subtitle?: string;
value?: string;
status?: 'ok' | 'warning' | 'alert';
onClose?: () => void;
onDetail?: () => void;
}
const STATUS_DOT = { ok: Colors.success, warning: Colors.warning, alert: Colors.danger };
export default function MarkerPopup({ title, subtitle, value, status, onClose, onDetail }: Props) {
return (
<View>
<Text>MarkerPopup</Text>
<View style={styles.popup}>
<View style={styles.header}>
<Text style={styles.title} numberOfLines={1}>{title}</Text>
{onClose && (
<TouchableOpacity onPress={onClose} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Text style={styles.close}></Text>
</TouchableOpacity>
)}
</View>
{subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
{value && (
<View style={styles.valueRow}>
<Text style={styles.value}>{value}</Text>
{status && <View style={[styles.statusDot, { backgroundColor: STATUS_DOT[status] }]} />}
</View>
)}
{onDetail && (
<TouchableOpacity style={styles.detailBtn} onPress={onDetail}>
<Text style={styles.detailText}>Voir détail </Text>
</TouchableOpacity>
)}
</View>
);
};
}
export default MarkerPopup;
const styles = StyleSheet.create({
popup: {
position: 'absolute', bottom: 100, left: 16, right: 16,
backgroundColor: Colors.white, borderRadius: BorderRadius.lg,
padding: Spacing.base, ...Shadows.lg,
},
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: Spacing.xs },
title: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.bold, color: Colors.neutral900, flex: 1 },
close: { fontSize: 16, color: Colors.neutral400, paddingLeft: Spacing.base },
subtitle: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginBottom: Spacing.xs },
valueRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.sm },
value: { fontSize: Typography.sizes.xl, fontWeight: Typography.weights.bold, color: Colors.primary[500] },
statusDot: { width: 10, height: 10, borderRadius: 5 },
detailBtn: { alignSelf: 'flex-end', paddingVertical: Spacing.xs, paddingHorizontal: Spacing.base, backgroundColor: Colors.primary[50], borderRadius: BorderRadius.md },
detailText: { fontSize: Typography.sizes.sm, color: Colors.primary[500], fontWeight: '600' },
});

View File

@@ -0,0 +1,33 @@
// Smart App City — Internationalization (i18n)
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
import { translations, Language } from '../stores/uiStore';
// Build i18next resources from our translation map
const resources: Record<string, { translation: Record<string, string> }> = {};
for (const [lang, trans] of Object.entries(translations)) {
resources[lang] = { translation: trans };
}
i18n
.use(initReactI18next)
.init({
resources,
lng: Localization.locale?.split('-')[0] ?? 'fr',
fallbackLng: 'fr',
interpolation: { escapeValue: false },
});
export default i18n;
// Helper: change language at runtime
export const changeLanguage = (lang: Language) => {
return i18n.changeLanguage(lang);
};
// Helper: get current language
export const getCurrentLanguage = (): Language => {
return (i18n.language?.split('-')[0] as Language) ?? 'fr';
};

View File

@@ -1,12 +1,73 @@
// Smart App City — Dashboard Screen (detailed view with charts)
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, ScrollView, StyleSheet } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius } from '../../../../src/theme/colors';
import { useIoTStore } from '../../../stores/iotStore';
import LineChart from '../../charts/LineChart';
import BarChart from '../../charts/BarChart';
import GaugeChart from '../../charts/GaugeChart';
import SectionHeader from '../../common/Header';
type Props = { navigation: NativeStackNavigationProp<any> };
const TEMP_DATA = [
{ label: '6h', value: 22 }, { label: '8h', value: 23 }, { label: '10h', value: 25 },
{ label: '12h', value: 27 }, { label: '14h', value: 29 }, { label: '16h', value: 28 },
{ label: '18h', value: 26 }, { label: '20h', value: 24 },
];
export default function DashboardScreen({ navigation }: Props) {
const sensors = useIoTStore((s) => s.sensors);
const DashboardScreen = () => {
return (
<View>
<Text>DashboardScreen</Text>
</View>
);
};
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.header}>
<Text style={styles.title}>Dashboard Analytics</Text>
</View>
export default DashboardScreen;
{/* Gauges */}
<View style={styles.section}>
<SectionHeader title="Vue d'ensemble" />
<View style={styles.gaugesRow}>
<GaugeChart value={28.3} max={50} label="Température" unit="°C" />
<GaugeChart value={72} max={100} label="Humidité" unit="%" />
<GaugeChart value={85} max={200} label="Qualité Air" unit="AQI" color={Colors.warning} />
</View>
</View>
{/* Line chart */}
<View style={styles.section}>
<SectionHeader title="Température — 24h" />
<LineChart data={TEMP_DATA} color={Colors.ocean[500]} height={160} />
</View>
{/* Bar chart */}
<View style={styles.section}>
<SectionHeader title="Énergie par zone (kWh)" />
<BarChart
data={[
{ label: 'Centre', value: 2340, color: Colors.primary[500] },
{ label: 'Schoelcher', value: 1890, color: Colors.ocean[500] },
{ label: 'Nord', value: 3120, color: Colors.indigo[500] },
]}
height={140}
/>
</View>
<View style={{ height: 80 }} />
</ScrollView>
);
}
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 },
section: { padding: Spacing.base },
gaugesRow: { flexDirection: 'row', justifyContent: 'space-around', marginTop: Spacing.base },
});

View File

@@ -1,12 +1,32 @@
// Smart App City — Layer Detail Screen
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, StyleSheet } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing } from '../../../../src/theme/colors';
const LayerDetailScreen = () => {
type Props = { navigation: NativeStackNavigationProp<any> };
export default function LayerDetailScreen({ navigation }: Props) {
return (
<View>
<Text>LayerDetailScreen</Text>
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Détail de la couche</Text>
</View>
<View style={styles.content}>
<Text style={styles.hint}>Sélectionnez une couche sur la carte pour voir ses détails.</Text>
</View>
</View>
);
};
}
export default LayerDetailScreen;
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 },
content: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: Spacing.xl },
hint: { fontSize: Typography.sizes.base, color: Colors.neutral500, textAlign: 'center' },
});

View File

@@ -1,12 +1,88 @@
// Smart App City — Sensor Detail Screen
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, ScrollView, 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';
import LineChart from '../../charts/LineChart';
import GaugeChart from '../../charts/GaugeChart';
type Props = { navigation: NativeStackNavigationProp<any> };
export default function SensorDetailScreen({ navigation }: Props) {
const sensors = useIoTStore((s) => s.sensors);
const sensor = sensors[0]; // In real app, get by route param
if (!sensor) {
return (
<View style={styles.container}>
<Text style={styles.empty}>Capteur introuvable</Text>
</View>
);
}
const SensorDetailScreen = () => {
return (
<View>
<Text>SensorDetailScreen</Text>
</View>
);
};
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.header}>
<Text style={styles.title}>{sensor.name}</Text>
<Text style={styles.subtitle}>{sensor.location}</Text>
<View style={styles.valueRow}>
<Text style={styles.value}>{sensor.value} {sensor.unit}</Text>
<View style={[styles.statusBadge, { backgroundColor: sensor.status === 'ok' ? Colors.success : sensor.status === 'warning' ? Colors.warning : Colors.danger }]}>
<Text style={styles.statusText}>{sensor.status.toUpperCase()}</Text>
</View>
</View>
</View>
export default SensorDetailScreen;
<View style={styles.section}>
<GaugeChart value={sensor.value} max={100} label={sensor.type} unit={sensor.unit} size={140} />
</View>
<View style={styles.section}>
<LineChart
data={[
{ label: '6h', value: sensor.value - 3 }, { label: '8h', value: sensor.value - 2 },
{ label: '10h', value: sensor.value - 1 }, { label: '12h', value: sensor.value },
{ label: '14h', value: sensor.value + 1 }, { label: '16h', value: sensor.value },
]}
title="Historique 24h"
height={160}
/>
</View>
<View style={styles.section}>
<View style={styles.infoCard}>
<Text style={styles.infoLabel}>Type</Text>
<Text style={styles.infoValue}>{sensor.type}</Text>
<Text style={styles.infoLabel}>Dernière mise à jour</Text>
<Text style={styles.infoValue}>{new Date(sensor.lastUpdate).toLocaleString('fr-FR')}</Text>
<Text style={styles.infoLabel}>Coordonnées</Text>
<Text style={styles.infoValue}>{sensor.latitude.toFixed(4)}, {sensor.longitude.toFixed(4)}</Text>
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.neutral50 },
header: {
backgroundColor: Colors.primary[500],
paddingTop: 50, paddingBottom: Spacing.xl,
paddingHorizontal: Spacing.base, alignItems: 'center',
},
title: { fontSize: Typography.sizes.lg, fontWeight: Typography.weights.bold, color: Colors.white, textAlign: 'center' },
subtitle: { fontSize: Typography.sizes.sm, color: 'rgba(255,255,255,0.8)', marginTop: Spacing.xs },
valueRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.base, marginTop: Spacing.base },
value: { fontSize: Typography.sizes.xxxl, fontWeight: Typography.weights.bold, color: Colors.white },
statusBadge: { borderRadius: BorderRadius.full, paddingHorizontal: Spacing.base, paddingVertical: 2 },
statusText: { color: Colors.white, fontSize: Typography.sizes.xs, fontWeight: '700' },
section: { padding: Spacing.base, alignItems: 'center' },
empty: { fontSize: Typography.sizes.lg, color: Colors.neutral500, textAlign: 'center', marginTop: 100 },
infoCard: {
backgroundColor: Colors.white, borderRadius: BorderRadius.lg,
padding: Spacing.base, width: '100%', ...Shadows.sm,
},
infoLabel: { fontSize: Typography.sizes.xs, color: Colors.neutral400, marginTop: Spacing.sm },
infoValue: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.semibold, color: Colors.neutral900 },
});

View File

@@ -1,12 +1,65 @@
// Smart App City — Notification Preferences Screen
import React from 'react';
import { View, Text } from 'react-native';
import { View, Text, ScrollView, Switch, StyleSheet } from 'react-native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../../../src/theme/colors';
import { useNotificationStore } from '../../../stores/notificationStore';
type Props = { navigation: NativeStackNavigationProp<any> };
export default function NotificationPrefsScreen({ navigation }: Props) {
const store = useNotificationStore();
const rows = [
{ key: 'pushEnabled' as const, label: 'Notifications push', desc: 'Recevoir des notifications push', icon: '🔔' },
{ key: 'alertNotifications' as const, label: 'Alertes IoT', desc: 'Alertes capteurs et seuils', icon: '🚨' },
{ key: 'eventNotifications' as const, label: 'Événements', desc: 'Événements de la ville', icon: '🎉' },
{ key: 'systemNotifications' as const, label: 'Système', desc: 'Mises à jour et maintenance', icon: '⚙️' },
];
const NotificationPrefsScreen = () => {
return (
<View>
<Text>NotificationPrefsScreen</Text>
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Préférences de notification</Text>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{rows.map((row) => (
<View key={row.key} style={styles.row}>
<Text style={styles.icon}>{row.icon}</Text>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{row.label}</Text>
<Text style={styles.rowDesc}>{row.desc}</Text>
</View>
<Switch
value={store[row.key]}
onValueChange={() => store[`set${row.key.charAt(0).toUpperCase() + row.key.slice(1)}`](!store[row.key])}
trackColor={{ false: Colors.neutral200, true: Colors.primary[300] }}
thumbColor={store[row.key] ? Colors.primary[500] : Colors.neutral400}
/>
</View>
))}
</ScrollView>
</View>
);
};
}
export default NotificationPrefsScreen;
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 },
row: {
flexDirection: 'row', alignItems: 'center',
backgroundColor: Colors.white,
paddingHorizontal: Spacing.base, paddingVertical: Spacing.base,
borderBottomWidth: 1, borderBottomColor: Colors.neutral100,
gap: Spacing.base,
},
icon: { fontSize: 20 },
rowContent: { flex: 1 },
rowLabel: { fontSize: Typography.sizes.base, fontWeight: Typography.weights.semibold, color: Colors.neutral900 },
rowDesc: { fontSize: Typography.sizes.sm, color: Colors.neutral500, marginTop: 2 },
});

View File

@@ -1 +1,48 @@
// TODO: Implement
// Smart App City — GIS Service (maps, geocoding, routing)
import { get, post } from './api';
export interface GeoPoint {
latitude: number;
longitude: number;
}
export interface GeoFeature {
id: string;
type: 'sensor' | 'zone' | 'poi' | 'event';
name: string;
description?: string;
location: GeoPoint;
properties?: Record<string, any>;
}
export interface MapLayer {
id: string;
name: string;
visible: boolean;
opacity: number;
type: 'markers' | 'heatmap' | 'polygon' | 'heatmap';
}
export const gisService = {
async geocode(address: string): Promise<GeoPoint[]> {
return get<GeoPoint[]>('/gis/geocode', { address });
},
async reverseGeocode(lat: number, lng: number): Promise<string> {
return get<string>(`/gis/reverse/${lat}/${lng}`);
},
async getFeatures(bounds: {
north: number; south: number; east: number; west: number;
}): Promise<GeoFeature[]> {
return get<GeoFeature[]>('/gis/features', bounds);
},
async searchPOI(query: string, location: GeoPoint, radius: number = 5000): Promise<GeoFeature[]> {
return get<GeoFeature[]>('/gis/poi', { query, ...location, radius });
},
async getRoute(from: GeoPoint, to: GeoPoint): Promise<{ distance: number; duration: number; points: GeoPoint[] }> {
return post('/gis/route', { from, to });
},
};

View File

@@ -0,0 +1,5 @@
// Smart App City — Store barrel export
export { useAuthStore } from './authStore';
export { useIoTStore } from './iotStore';
export { useNotificationStore } from './notificationStore';
export { useUIStore } from './uiStore';

View File

@@ -1 +1,53 @@
// TODO: Implement
// Smart App City — App Constants
export const APP_NAME = 'Smart App City';
export const APP_VERSION = '0.1.0';
// API
export const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'https://api-smartapp.digitribe.fr/api/v1';
export const API_TIMEOUT = 15000;
// Map
export const DEFAULT_MAP_CENTER = { latitude: 14.6161, longitude: -61.0588 }; // Fort-de-France
export const DEFAULT_MAP_ZOOM = 12;
export const MARTINIQUE_BOUNDS = {
north: 14.88,
south: 14.45,
east: -60.82,
west: -61.23,
};
// Sensor types
export const SENSOR_TYPES = [
{ key: 'temperature', label: 'Température', unit: '°C', icon: '🌡️', color: '#00ACC1' },
{ key: 'humidity', label: 'Humidité', unit: '%', icon: '💧', color: '#1565C0' },
{ key: 'air_quality', label: 'Qualité Air', unit: 'AQI', icon: '🌬️', color: '#2E7D32' },
{ key: 'noise', label: 'Bruit', unit: 'dB', icon: '🔊', color: '#3949AB' },
{ key: 'traffic', label: 'Trafic', unit: 'veh/h', icon: '🚗', color: '#F57C00' },
{ key: 'energy', label: 'Énergie', unit: 'kWh', icon: '⚡', color: '#D32F2F' },
];
// Alert severity
export const ALERT_SEVERITY = {
critical: { label: 'Critique', color: '#D32F2F', icon: '🔴' },
high: { label: 'Haute', color: '#F57C00', icon: '🟠' },
medium: { label: 'Moyenne', color: '#FF9800', icon: '🟡' },
low: { label: 'Basse', color: '#0288D1', icon: '🔵' },
};
// Storage keys
export const STORAGE_KEYS = {
ACCESS_TOKEN: 'access_token',
REFRESH_TOKEN: 'refresh_token',
AUTH_STORAGE: 'auth-storage',
THEME: 'theme',
LANGUAGE: 'language',
};
// Refresh intervals (ms)
export const REFRESH_INTERVALS = {
SENSORS: 30000, // 30s
ALERTS: 60000, // 1min
WEATHER: 300000, // 5min
NOTIFICATIONS: 30000, // 30s
};