import DashboardScreen from "./src/screens/DashboardScreen";
import SettingsScreen from "./src/screens/SettingsScreen";
+import HistoryScreen from "./src/screens/HistoryScreen";
// Types des routes (pour éviter les erreurs de navigation)
export type RootStackParamList = {
Dashboard: undefined;
Settings: undefined;
+ History: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
component={DashboardScreen}
options={{ title: "Dashboard" }}
/>
+
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{ title: "Paramètres" }}
/>
+
+ <Stack.Screen
+ name="History"
+ component={HistoryScreen}
+ options={{ title: "Historique" }}
+ />
+
</Stack.Navigator>
</NavigationContainer>
);
type Props = {
onGoSettings: () => void;
+ onGoHistory: () => void;
};
/**
* - Historique (placeholder)
* - Paramètres (navigation réelle via callback)
*/
-export default function ActionsCard({ onGoSettings }: Props) {
+export default function ActionsCard({ onGoSettings, onGoHistory }: Props) {
return (
<View style={ui.card}>
<Text style={ui.title}>Actions</Text>
<Text style={ui.buttonText}>Voir stratégie</Text>
</TouchableOpacity>
- <TouchableOpacity style={ui.button} onPress={() => {}}>
+ <TouchableOpacity style={ui.button} onPress={onGoHistory}>
<Text style={ui.buttonText}>Historique</Text>
</TouchableOpacity>
import { StyleSheet } from "react-native";
+import { theme } from "../../theme/theme";
export const ui = StyleSheet.create({
+ // Écran
screen: {
- backgroundColor: "#f6f7fb",
+ backgroundColor: theme.colors.bg,
+ },
+ container: {
+ padding: theme.spacing.l,
+ },
+ centered: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ backgroundColor: theme.colors.bg,
},
- card: {
- backgroundColor: "#fff",
- borderRadius: 14,
- padding: 14,
- marginBottom: 12,
+ // Card
+card: {
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.radius.card,
+ padding: 14,
+ marginBottom: theme.spacing.m,
- // ombre iOS
- shadowColor: "#000",
- shadowOpacity: 0.08,
- shadowRadius: 10,
- shadowOffset: { width: 0, height: 4 },
+ borderWidth: 1,
+ borderColor: theme.colors.border,
- // ombre Android
- elevation: 3,
- },
+ shadowColor: "#000",
+ shadowOpacity: 0.08,
+ shadowRadius: 10,
+ shadowOffset: { width: 0, height: 4 },
+
+ elevation: 3,
+},
title: {
fontSize: 16,
- fontWeight: "700",
+ fontWeight: "800",
+ color: theme.colors.text,
marginBottom: 10,
},
- muted: {
- opacity: 0.65,
- fontSize: 12,
- },
-
- rowBetween: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- },
-
value: {
fontSize: 16,
+ color: theme.colors.text,
},
valueBold: {
fontSize: 16,
- fontWeight: "700",
+ fontWeight: "800",
+ color: theme.colors.text,
},
bigCenter: {
fontSize: 28,
- fontWeight: "800",
+ fontWeight: "900",
textAlign: "center",
+ color: theme.colors.text,
marginVertical: 6,
},
+ muted: {
+ fontSize: 12,
+ color: theme.colors.muted,
+ },
+
+ rowBetween: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+
+ // Badge (pill)
badge: {
alignSelf: "center",
paddingHorizontal: 12,
paddingVertical: 6,
- borderRadius: 999,
+ borderRadius: theme.radius.pill,
marginTop: 6,
},
-
badgeText: {
- fontWeight: "800",
+ fontWeight: "900",
fontSize: 12,
letterSpacing: 0.3,
},
+ // Boutons
button: {
- backgroundColor: "#111827",
+ backgroundColor: theme.colors.primary,
paddingVertical: 12,
- borderRadius: 12,
+ borderRadius: theme.radius.button,
alignItems: "center",
flexGrow: 1,
flexBasis: "48%",
},
-
buttonText: {
color: "#fff",
+ fontWeight: "900",
+ },
+
+ // Messages (warning/error/info)
+ banner: {
+ ...StyleSheet.flatten({
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.radius.card,
+ padding: 12,
+ marginBottom: theme.spacing.m,
+ }),
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+
+ bannerText: {
fontWeight: "800",
+ color: theme.colors.text,
},
});
\ No newline at end of file
--- /dev/null
+import type { Signal } from "../types/Signal";
+
+export const SIGNALS_MOCK: Signal[] = [
+ {
+ signalId: "sig-001",
+ pair: "BTC/EUR",
+ timestamp: Date.now() - 1000 * 60 * 8,
+ action: "BUY",
+ criticality: "INFO",
+ status: "ACTIVE",
+ confidence: 0.82,
+ reason: "RSI < 30, retournement haussier détecté",
+ priceAtSignal: 42150.23,
+ },
+ {
+ signalId: "sig-002",
+ pair: "BTC/EUR",
+ timestamp: Date.now() - 1000 * 60 * 35,
+ action: "HOLD",
+ criticality: "WARNING",
+ status: "ACTIVE",
+ confidence: 0.61,
+ reason: "Marché incertain, volatilité élevée",
+ priceAtSignal: 41980.12,
+ },
+ {
+ signalId: "sig-003",
+ pair: "BTC/EUR",
+ timestamp: Date.now() - 1000 * 60 * 90,
+ action: "SELL",
+ criticality: "CRITICAL",
+ status: "SUPERSEDED",
+ confidence: 0.74,
+ reason: "Cassure support + volume vendeur",
+ priceAtSignal: 42540.55,
+ },
+];
\ No newline at end of file
import StrategyCard from "../components/StrategyCard";
import WalletCard from "../components/WalletCard";
import ActionsCard from "../components/ActionsCard";
+
import { ui } from "../components/ui/uiStyles";
// ✅ Socket.IO
* DashboardScreen
* ----------------
* Écran principal mobile.
- *
- * Responsabilités :
- * - Charger les données (mock/API REST)
- * - Charger les paramètres utilisateur
- * - Gérer le refresh automatique (settings.refreshMode)
- * - Gérer la navigation
- * - Recevoir les alertes temps réel via Socket.IO (non bloquant)
- *
- * L'affichage "business" est délégué aux composants (Cards).
+ * - Charge Dashboard + Settings
+ * - Refresh auto si activé
+ * - Socket.IO optionnel (non bloquant) pour alertes live
+ * - UI cohérente via uiStyles/theme
*/
export default function DashboardScreen() {
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
-
const [loading, setLoading] = useState<boolean>(true);
// erreurs REST/refresh
const [error, setError] = useState<string | null>(null);
- // état Socket.IO (non bloquant)
+ // Socket state (non bloquant)
const [socketConnected, setSocketConnected] = useState<boolean>(false);
const [socketError, setSocketError] = useState<string | null>(null);
-
- // alertes reçues (optionnel mais utile)
const [liveAlerts, setLiveAlerts] = useState<Alert[]>([]);
const navigation = useNavigation();
/**
- * Chargement initial + rechargement à chaque focus
- * (utile quand on revient des Paramètres)
+ * Chargement initial + rechargement quand on revient sur l'écran
*/
useFocusEffect(
useCallback(() => {
setSettings(userSettings);
}
} catch {
- if (isActive) {
- setError("Impossible de charger le dashboard.");
- }
+ if (isActive) setError("Impossible de charger le dashboard.");
} finally {
- if (isActive) {
- setLoading(false);
- }
+ if (isActive) setLoading(false);
}
}
);
/**
- * Refresh automatique si activé
+ * Refresh auto (sans clignotement global)
*/
useEffect(() => {
if (!settings) return;
}, [settings]);
/**
- * Socket.IO : connexion / écoute des alertes
- * - Non bloquant : si socket KO, l'app continue via REST
- * - Se déclenche quand on a settings (souvent nécessaire pour userId)
- *
- * IMPORTANT :
- * 👉 Version propre : on n'utilise PAS @ts-expect-error.
- * 👉 On lit un userId optionnel (si présent), sinon on désactive le socket
- * et on affiche un message clair.
+ * Socket.IO (non bloquant)
+ * - Si userId absent => on désactive socket proprement.
+ * - Si serveur pas prêt => connect_error timeout => on affiche un message, sans crasher.
*/
useEffect(() => {
if (!settings) return;
setSocketError(null);
- // ✅ Proposition propre : un champ optionnel dans UserSettings : userId?: string
- // Si pas dispo => on ne connecte pas (pas de fake "user-123" en prod)
+ // ✅ userId optionnel pour Step 1/2/3 (pas d'auth)
const userId = settings.userId;
if (!userId) {
setSocketConnected(false);
- setSocketError("Socket désactivé : userId absent (pas connecté).");
+ setSocketError("Socket désactivé : userId absent.");
return;
}
setSocketConnected(true);
} catch {
setSocketConnected(false);
- setSocketError("Socket: impossible de se connecter (URL/userId).");
+ setSocketError("Socket : paramètres invalides (URL ou userId).");
return;
}
setLiveAlerts((prev) => [alert, ...prev].slice(0, 50));
});
- // ping optionnel pour debug
socketService.ping();
return () => {
// Chargement
if (loading) {
return (
- <View style={styles.centered}>
+ <View style={ui.centered}>
<Text>Chargement du dashboard…</Text>
</View>
);
}
- // Erreur bloquante
+ // Erreur bloquante (si rien à afficher)
if (error && (!summary || !settings)) {
return (
- <View style={styles.centered}>
+ <View style={ui.centered}>
<Text style={styles.errorText}>{error}</Text>
</View>
);
// Sécurité
if (!summary || !settings) {
return (
- <View style={styles.centered}>
+ <View style={ui.centered}>
<Text>Initialisation…</Text>
</View>
);
return (
<SafeAreaView style={styles.safeArea}>
- <ScrollView contentContainerStyle={styles.container}>
- {/* Warning REST/refresh (non bloquant) */}
+ <ScrollView contentContainerStyle={ui.container}>
+ {/* Bannière warning REST/refresh (non bloquant) */}
{error && (
- <View style={styles.warningCard}>
- <Text style={styles.warningText}>{error}</Text>
+ <View style={[ui.banner, styles.bannerWarning]}>
+ <Text style={[ui.bannerText, styles.bannerWarningText]}>{error}</Text>
</View>
)}
- {/* Socket status (non bloquant) */}
- <View style={styles.socketCard}>
- <Text style={styles.socketTitle}>
+ {/* Bannière Socket (non bloquant) */}
+ <View style={[ui.banner, styles.bannerSocket]}>
+ <Text style={[ui.bannerText, styles.socketTitle]}>
Socket.IO : {socketConnected ? "connecté ✅" : "déconnecté ⚠️"}
</Text>
- {!!socketError && <Text style={styles.socketMuted}>{socketError}</Text>}
-
- <Text style={styles.socketMuted}>Alertes reçues : {liveAlerts.length}</Text>
+ {!!socketError && <Text style={ui.muted}>{socketError}</Text>}
+ <Text style={[ui.muted, { marginTop: 4 }]}>
+ Alertes reçues : {liveAlerts.length}
+ </Text>
</View>
<MarketCard summary={summary} settings={settings} />
<StrategyCard summary={summary} />
<WalletCard settings={settings} />
- <ActionsCard onGoSettings={() => navigation.navigate("Settings" as never)} />
+ <ActionsCard
+ onGoSettings={() => navigation.navigate("Settings" as never)}
+ onGoHistory={() => navigation.navigate("History" as never)}
+ />
</ScrollView>
</SafeAreaView>
);
}
-/* ===================== STYLES ===================== */
-
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: ui.screen.backgroundColor,
},
- container: {
- padding: 16,
- },
-
- centered: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- backgroundColor: ui.screen.backgroundColor,
- },
-
errorText: {
color: "#dc2626",
- fontWeight: "800",
+ fontWeight: "900",
},
- // Bannières en style "card" comme les mockups
- warningCard: {
- ...ui.card,
- borderWidth: 1,
+ bannerWarning: {
borderColor: "#ca8a04",
},
-
- warningText: {
+ bannerWarningText: {
color: "#ca8a04",
- fontWeight: "800",
},
- socketCard: {
- ...ui.card,
- borderWidth: 1,
+ bannerSocket: {
borderColor: "#cbd5e1",
},
-
socketTitle: {
- fontWeight: "800",
color: "#0f172a",
- marginBottom: 6,
- },
-
- socketMuted: {
- ...ui.muted,
- marginTop: 2,
},
});
\ No newline at end of file
--- /dev/null
+import { View, Text, StyleSheet, FlatList } from "react-native";
+import { useEffect, useState } from "react";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+import type { Signal, SignalAction, SignalCriticality } from "../types/Signal";
+import { fetchRecentSignals } from "../services/signalService";
+import { ui } from "../components/ui/uiStyles";
+
+function getActionColor(action: SignalAction): string {
+ switch (action) {
+ case "BUY":
+ return "#16a34a";
+ case "SELL":
+ return "#dc2626";
+ case "STOP_LOSS":
+ return "#991b1b";
+ case "HOLD":
+ default:
+ return "#ca8a04";
+ }
+}
+
+function getCriticalityColor(level: SignalCriticality): string {
+ switch (level) {
+ case "CRITICAL":
+ return "#b91c1c";
+ case "WARNING":
+ return "#ca8a04";
+ case "INFO":
+ default:
+ return "#2563eb";
+ }
+}
+
+function formatDate(ms: number): string {
+ return new Date(ms).toLocaleString();
+}
+
+export default function HistoryScreen() {
+ const [items, setItems] = useState<Signal[]>([]);
+ const [loading, setLoading] = useState<boolean>(true);
+ const [error, setError] = useState<string | null>(null);
+
+ useEffect(() => {
+ let active = true;
+
+ async function load() {
+ try {
+ setError(null);
+ setLoading(true);
+ const data = await fetchRecentSignals(20);
+ if (active) setItems(data);
+ } catch {
+ if (active) setError("Impossible de charger l'historique.");
+ } finally {
+ if (active) setLoading(false);
+ }
+ }
+
+ load();
+
+ return () => {
+ active = false;
+ };
+ }, []);
+
+ if (loading) {
+ return (
+ <View style={styles.centered}>
+ <Text>Chargement de l'historique…</Text>
+ </View>
+ );
+ }
+
+ if (error) {
+ return (
+ <View style={styles.centered}>
+ <Text style={styles.errorText}>{error}</Text>
+ </View>
+ );
+ }
+
+ return (
+ <SafeAreaView style={styles.safeArea}>
+ <FlatList
+ contentContainerStyle={styles.container}
+ data={items}
+ keyExtractor={(it) => it.signalId}
+ ListEmptyComponent={
+ <View style={ui.card}>
+ <Text style={ui.title}>Aucun signal</Text>
+ <Text style={ui.muted}>Aucun historique disponible pour l’instant.</Text>
+ </View>
+ }
+ renderItem={({ item }) => {
+ const actionColor = getActionColor(item.action);
+ const critColor = getCriticalityColor(item.criticality);
+
+ return (
+ <View style={ui.card}>
+ <View style={ui.rowBetween}>
+ <Text style={ui.valueBold}>{item.pair}</Text>
+ <Text style={ui.muted}>{formatDate(item.timestamp)}</Text>
+ </View>
+
+ {/* Badges */}
+ <View style={{ flexDirection: "row", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
+ <View style={[ui.badge, { backgroundColor: `${actionColor}22`, marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: actionColor }]}>{item.action}</Text>
+ </View>
+
+ <View style={[ui.badge, { backgroundColor: `${critColor}22`, marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: critColor }]}>{item.criticality}</Text>
+ </View>
+
+ <View style={[ui.badge, { backgroundColor: "#11182722", marginTop: 0 }]}>
+ <Text style={[ui.badgeText, { color: "#111827" }]}>{item.status}</Text>
+ </View>
+ </View>
+
+ <View style={[ui.rowBetween, { marginTop: 10 }]}>
+ <Text style={ui.value}>Confiance</Text>
+ <Text style={ui.valueBold}>{(item.confidence * 100).toFixed(1)}%</Text>
+ </View>
+
+ <View style={[ui.rowBetween, { marginTop: 6 }]}>
+ <Text style={ui.value}>Prix au signal</Text>
+ <Text style={ui.valueBold}>{item.priceAtSignal.toFixed(2)}</Text>
+ </View>
+
+ <Text style={[ui.muted, { marginTop: 10 }]}>{item.reason}</Text>
+ </View>
+ );
+ }}
+ />
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: ui.screen.backgroundColor,
+ },
+ container: {
+ padding: 16,
+ },
+ centered: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ backgroundColor: ui.screen.backgroundColor,
+ },
+ errorText: {
+ color: "#dc2626",
+ fontWeight: "800",
+ },
+});
\ No newline at end of file
--- /dev/null
+import type { Signal } from "../types/Signal";
+import { SIGNALS_MOCK } from "../mocks/signals.mock";
+
+/**
+ * Plus tard (API REST):
+ * GET /api/alerts/events?userId=...&limit=10
+ * ou GET /api/signals?userId=...&pair=BTC/EUR&limit=...
+ *
+ * Pour l’instant : mock.
+ */
+export async function fetchRecentSignals(limit: number = 20): Promise<Signal[]> {
+ // simulation d'appel réseau
+ return new Promise((resolve) => {
+ setTimeout(() => resolve(SIGNALS_MOCK.slice(0, limit)), 250);
+ });
+}
\ No newline at end of file
--- /dev/null
+export const theme = {
+ colors: {
+ // Fond menthe très léger (crypto vibe)
+ bg: "#F2FBF6",
+
+ // Cartes
+ card: "#FFFFFF",
+
+ // Textes
+ text: "#0F172A",
+ muted: "#64748B",
+
+ // Accent principal (boutons, éléments actifs)
+ primary: "#14532D", // vert profond
+
+ // Bordures douces
+ border: "#DCEFE6",
+
+ // États
+ success: "#16A34A",
+ warning: "#CA8A04",
+ danger: "#DC2626",
+ info: "#0EA5E9", // bleu clair (pas foncé)
+ },
+
+ radius: {
+ card: 14,
+ button: 12,
+ pill: 999,
+ },
+
+ spacing: {
+ s: 8,
+ m: 12,
+ l: 16,
+ },
+} as const;
\ No newline at end of file
--- /dev/null
+export type SignalAction = "BUY" | "SELL" | "HOLD" | "STOP_LOSS";
+export type SignalCriticality = "CRITICAL" | "WARNING" | "INFO";
+export type SignalStatus = "ACTIVE" | "SUPERSEDED" | "EXECUTED" | "INVALID";
+
+export interface Signal {
+ signalId: string;
+ pair: string; // ex "BTC/EUR"
+ timestamp: number; // ms
+ action: SignalAction;
+ criticality: SignalCriticality;
+ status: SignalStatus;
+ confidence: number; // 0..1
+ reason: string;
+ priceAtSignal: number;
+}
\ No newline at end of file