]> git.digitality.be Git - pdw25-26/commitdiff
Mobile : theme + HistoryScreen
authorThibaud Moustier <thibaudmoustier0@gmail.com>
Mon, 23 Feb 2026 17:58:28 +0000 (18:58 +0100)
committerThibaud Moustier <thibaudmoustier0@gmail.com>
Mon, 23 Feb 2026 17:58:28 +0000 (18:58 +0100)
mobile-ui/App.tsx
mobile-ui/src/components/ActionsCard.tsx
mobile-ui/src/components/ui/uiStyles.ts
mobile-ui/src/mocks/signals.mock.ts [new file with mode: 0644]
mobile-ui/src/screens/DashboardScreen.tsx
mobile-ui/src/screens/HistoryScreen.tsx [new file with mode: 0644]
mobile-ui/src/services/signalService.ts [new file with mode: 0644]
mobile-ui/src/theme/theme.ts [new file with mode: 0644]
mobile-ui/src/types/Signal.ts [new file with mode: 0644]

index c94646d7b9b25f0bca3621acfcad9e1d18e06650..e491b2b9915dfb303d3b0153f767ddc6300c5872 100644 (file)
@@ -3,11 +3,13 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
 
 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>();
@@ -21,11 +23,19 @@ export default function App() {
           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>
   );
index eadcfeec796f9e43f471596451cfdf4b1f04537c..dfc2a6c428fe5276ec3dff390476430743a8d084 100644 (file)
@@ -3,6 +3,7 @@ import { ui } from "./ui/uiStyles";
 
 type Props = {
   onGoSettings: () => void;
+  onGoHistory: () => void;
 };
 
 /**
@@ -14,7 +15,7 @@ type Props = {
  * - 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>
@@ -24,7 +25,7 @@ export default function ActionsCard({ onGoSettings }: Props) {
           <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>
 
index 3f2233f45979d0185332b516386feab9716cfad4..ec44b35dbdb5772f3ece0567b516a0dbc830950a 100644 (file)
 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
diff --git a/mobile-ui/src/mocks/signals.mock.ts b/mobile-ui/src/mocks/signals.mock.ts
new file mode 100644 (file)
index 0000000..0bad531
--- /dev/null
@@ -0,0 +1,37 @@
+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
index 168536f299170f1ce46575392f67b9f851a983c2..a8a34b03a023a47ec806fde1ab5c8dd5ff1b3ee7 100644 (file)
@@ -12,6 +12,7 @@ import MarketCard from "../components/MarketCard";
 import StrategyCard from "../components/StrategyCard";
 import WalletCard from "../components/WalletCard";
 import ActionsCard from "../components/ActionsCard";
+
 import { ui } from "../components/ui/uiStyles";
 
 // ✅ Socket.IO
@@ -23,37 +24,28 @@ import type { Alert } from "../types/Alert";
  * 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(() => {
@@ -74,13 +66,9 @@ export default function DashboardScreen() {
             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);
         }
       }
 
@@ -93,7 +81,7 @@ export default function DashboardScreen() {
   );
 
   /**
-   * Refresh automatique si activé
+   * Refresh auto (sans clignotement global)
    */
   useEffect(() => {
     if (!settings) return;
@@ -117,27 +105,21 @@ export default function DashboardScreen() {
   }, [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;
     }
 
@@ -146,7 +128,7 @@ export default function DashboardScreen() {
       setSocketConnected(true);
     } catch {
       setSocketConnected(false);
-      setSocketError("Socket: impossible de se connecter (URL/userId).");
+      setSocketError("Socket : paramètres invalides (URL ou userId).");
       return;
     }
 
@@ -154,7 +136,6 @@ export default function DashboardScreen() {
       setLiveAlerts((prev) => [alert, ...prev].slice(0, 50));
     });
 
-    // ping optionnel pour debug
     socketService.ping();
 
     return () => {
@@ -167,16 +148,16 @@ export default function DashboardScreen() {
   // 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>
     );
@@ -185,7 +166,7 @@ export default function DashboardScreen() {
   // Sécurité
   if (!summary || !settings) {
     return (
-      <View style={styles.centered}>
+      <View style={ui.centered}>
         <Text>Initialisation…</Text>
       </View>
     );
@@ -193,85 +174,61 @@ export default function DashboardScreen() {
 
   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
diff --git a/mobile-ui/src/screens/HistoryScreen.tsx b/mobile-ui/src/screens/HistoryScreen.tsx
new file mode 100644 (file)
index 0000000..d065eec
--- /dev/null
@@ -0,0 +1,158 @@
+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
diff --git a/mobile-ui/src/services/signalService.ts b/mobile-ui/src/services/signalService.ts
new file mode 100644 (file)
index 0000000..17c27bf
--- /dev/null
@@ -0,0 +1,16 @@
+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
diff --git a/mobile-ui/src/theme/theme.ts b/mobile-ui/src/theme/theme.ts
new file mode 100644 (file)
index 0000000..21a4b39
--- /dev/null
@@ -0,0 +1,37 @@
+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
diff --git a/mobile-ui/src/types/Signal.ts b/mobile-ui/src/types/Signal.ts
new file mode 100644 (file)
index 0000000..acc9937
--- /dev/null
@@ -0,0 +1,15 @@
+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