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:
@@ -1,12 +1,57 @@
|
|||||||
|
// Smart App City — Alert Card Component
|
||||||
import React from 'react';
|
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 = () => {
|
interface Props {
|
||||||
return (
|
id: string;
|
||||||
<View>
|
title: string;
|
||||||
<Text>AlertCard</Text>
|
message: string;
|
||||||
</View>
|
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 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,74 @@
|
|||||||
|
// Smart App City — Sensor Card Component
|
||||||
import React from 'react';
|
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 = () => {
|
export interface SensorCardData {
|
||||||
return (
|
id: string;
|
||||||
<View>
|
name: string;
|
||||||
<Text>SensorCard</Text>
|
type: string;
|
||||||
</View>
|
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 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,38 @@
|
|||||||
|
// Smart App City — Stats Card Component
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<View style={[styles.card, style]}>
|
||||||
<Text>StatsCard</Text>
|
{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>
|
</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 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,52 @@
|
|||||||
|
// Smart App City — Zone Card Component
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<TouchableOpacity style={styles.card} onPress={onPress}>
|
||||||
<Text>ZoneCard</Text>
|
<View style={[styles.colorBar, { backgroundColor: zone.color }]} />
|
||||||
</View>
|
<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 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,61 @@
|
|||||||
|
// Smart App City — Bar Chart Component
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<View style={[styles.container, style]}>
|
||||||
<Text>BarChart</Text>
|
{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>
|
</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' },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,47 @@
|
|||||||
|
// Smart App City — Gauge Chart Component
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<View style={[styles.container, style]}>
|
||||||
<Text>GaugeChart</Text>
|
<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>
|
</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' },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,64 @@
|
|||||||
|
// Smart App City — Line Chart Component (SVG-based)
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<View style={[styles.container, style]}>
|
||||||
<Text>LineChart</Text>
|
{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>
|
</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' },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,73 @@
|
|||||||
|
// Smart App City — Map View Component (react-native-maps wrapper)
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<View style={[styles.container, style]}>
|
||||||
<Text>MapView</Text>
|
{/* 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>
|
</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 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,59 @@
|
|||||||
|
// Smart App City — Marker Popup Component
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<View style={styles.popup}>
|
||||||
<Text>MarkerPopup</Text>
|
<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>
|
</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' },
|
||||||
|
});
|
||||||
|
|||||||
@@ -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';
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,73 @@
|
|||||||
|
// Smart App City — Dashboard Screen (detailed view with charts)
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||||
<Text>DashboardScreen</Text>
|
<View style={styles.header}>
|
||||||
</View>
|
<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 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
|
// Smart App City — Layer Detail Screen
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<View style={styles.container}>
|
||||||
<Text>LayerDetailScreen</Text>
|
<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>
|
</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' },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,88 @@
|
|||||||
|
// Smart App City — Sensor Detail Screen
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||||
<Text>SensorDetailScreen</Text>
|
<View style={styles.header}>
|
||||||
</View>
|
<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 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,12 +1,65 @@
|
|||||||
|
// Smart App City — Notification Preferences Screen
|
||||||
import React from 'react';
|
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 (
|
return (
|
||||||
<View>
|
<View style={styles.container}>
|
||||||
<Text>NotificationPrefsScreen</Text>
|
<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>
|
</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 },
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user