feat: application n8n Pilot complète (Expo managed workflow)
- Stack : Expo Router, Axios, Zustand, React Native Paper (thème sombre), date-fns - Sécurité : secrets dans Android Keystore via expo-secure-store, TLS obligatoire, headers X-N8N-API-KEY + X-App-Token injectés par intercepteur Axios - API : client.ts centralisé + workflows.ts + executions.ts (TypeScript strict) - Store : Zustand appStore avec chargement depuis secure store au démarrage - Hooks : usePolling (générique), useWorkflows, useExecutions - Composants : StatusBadge, WorkflowCard, ExecutionCard, SkeletonLoader - Screens : Dashboard, Workflows, Executions, Logs (détail exécution), Settings - Navigation Expo Router : 4 tabs + stack Logs + écran Setup initial - Docs : INSTALL.md, UPDATE.md, BACKUP.md, HAPROXY.md, SECURITY.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { View, Animated, StyleSheet, ViewStyle } from 'react-native';
|
||||
|
||||
interface SkeletonItemProps {
|
||||
width?: number | `${number}%`;
|
||||
height?: number;
|
||||
borderRadius?: number;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bloc de squelette animé avec effet de pulsation (shimmer).
|
||||
* Composant interne utilisé par SkeletonLoader pour construire les cartes placeholder.
|
||||
*/
|
||||
const SkeletonItem: React.FC<SkeletonItemProps> = ({
|
||||
width = '100%',
|
||||
height = 16,
|
||||
borderRadius = 4,
|
||||
style,
|
||||
}) => {
|
||||
const opacity = useRef(new Animated.Value(0.3)).current;
|
||||
|
||||
useEffect(() => {
|
||||
/** Boucle d'animation : alterne entre 30% et 70% d'opacité */
|
||||
const animation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(opacity, {
|
||||
toValue: 0.7,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(opacity, {
|
||||
toValue: 0.3,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
);
|
||||
animation.start();
|
||||
return () => animation.stop();
|
||||
}, [opacity]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.skeleton,
|
||||
{ width: width as number, height, borderRadius, opacity },
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface SkeletonLoaderProps {
|
||||
/** Nombre de cartes placeholder à afficher pendant le chargement initial */
|
||||
count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche des cartes placeholder animées pendant le chargement des données.
|
||||
* Simule la structure d'une WorkflowCard/ExecutionCard pour éviter le layout shift
|
||||
* et indiquer à l'utilisateur que du contenu arrive.
|
||||
*/
|
||||
const SkeletonLoader: React.FC<SkeletonLoaderProps> = ({ count = 5 }) => {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<View key={index} style={styles.card}>
|
||||
<SkeletonItem width="60%" height={18} style={{ marginBottom: 8 }} />
|
||||
<SkeletonItem width="85%" height={12} style={{ marginBottom: 6 }} />
|
||||
<SkeletonItem width="40%" height={12} />
|
||||
</View>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: '#1E1E1E',
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 6,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
},
|
||||
skeleton: {
|
||||
backgroundColor: '#2C2C2C',
|
||||
},
|
||||
});
|
||||
|
||||
export default SkeletonLoader;
|
||||
Reference in New Issue
Block a user