Skip to content

Frontend - React Native

React Native Expo TypeScript

L’application mobile Mindlet est dĂ©veloppĂ©e avec React Native et Expo, permettant un dĂ©ploiement cross-platform sur iOS et Android Ă  partir d’une base de code unique.

ComposantTechnologieRĂŽle
FrameworkReact NativeBase de l’application
ToolchainExpoBuild, déploiement, OTA updates
LangageTypeScriptTypage statique
État globalZustandGestion d’état lĂ©gĂšre
NavigationReact NavigationNavigation entre écrans
StylingNativeWindTailwind CSS pour RN
HTTP ClientAxiosRequĂȘtes API
FormsReact Hook FormGestion des formulaires
ValidationZodValidation des schémas
src/
├── app/ # Écrans (file-based routing)
│ ├── (auth)/ # Écrans authentification
│ ├── (tabs)/ # Navigation principale
│ └── _layout.tsx # Layout racine
├── components/ # Composants rĂ©utilisables
│ ├── ui/ # Composants UI de base
│ ├── cards/ # Composants liĂ©s aux cartes
│ └── collections/ # Composants collections
├── hooks/ # Custom hooks
├── services/ # Services API
├── stores/ # Stores Zustand
├── types/ # Types TypeScript
├── utils/ # Utilitaires
└── constants/ # Constantes

Accueil

Écran d'accueil

Collections

Collections

Révision

Révision

Profil

Profil

interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
refreshToken: () => Promise<void>;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (email, password) => {
const response = await authService.login(email, password);
set({
user: response.user,
token: response.token,
isAuthenticated: true,
});
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false });
},
}),
{ name: 'auth-storage' }
)
);
import axios from 'axios';
import { useAuthStore } from '@/stores/authStore';
const api = axios.create({
baseURL: process.env.EXPO_PUBLIC_API_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Intercepteur pour ajouter le token
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Intercepteur pour gérer les erreurs
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
export default api;
class CardsService {
async getAll(collectionId: number): Promise<Card[]> {
const response = await api.get(`/cards`, {
params: { collection_id: collectionId },
});
return response.data.data;
}
async create(data: CreateCardDTO): Promise<Card> {
const response = await api.post('/cards', data);
return response.data.data;
}
async generateFromDocument(documentId: number): Promise<Card[]> {
const response = await api.post('/cards/generate', {
document_id: documentId,
});
return response.data.data;
}
}
export const cardsService = new CardsService();

Nous utilisons NativeWind (Tailwind CSS pour React Native) pour un styling cohérent :

import { View, Text, TouchableOpacity } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline';
disabled?: boolean;
}
export function Button({
title,
onPress,
variant = 'primary',
disabled = false
}: ButtonProps) {
const variants = {
primary: 'bg-indigo-600 text-white',
secondary: 'bg-gray-200 text-gray-800',
outline: 'border-2 border-indigo-600 text-indigo-600',
};
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled}
className={`
px-6 py-3 rounded-xl items-center justify-center
${variants[variant]}
${disabled ? 'opacity-50' : ''}
`}
>
<Text className="font-semibold text-base">{title}</Text>
</TouchableOpacity>
);
}
import { useState } from 'react';
import { View, Text, Pressable } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring
} from 'react-native-reanimated';
interface FlashcardProps {
question: string;
answer: string;
}
export function Flashcard({ question, answer }: FlashcardProps) {
const [isFlipped, setIsFlipped] = useState(false);
const rotation = useSharedValue(0);
const flipCard = () => {
rotation.value = withSpring(isFlipped ? 0 : 180);
setIsFlipped(!isFlipped);
};
const frontStyle = useAnimatedStyle(() => ({
transform: [{ rotateY: `${rotation.value}deg` }],
backfaceVisibility: 'hidden',
}));
const backStyle = useAnimatedStyle(() => ({
transform: [{ rotateY: `${rotation.value + 180}deg` }],
backfaceVisibility: 'hidden',
}));
return (
<Pressable onPress={flipCard} className="w-full aspect-[3/4]">
<Animated.View
style={frontStyle}
className="absolute inset-0 bg-white rounded-2xl p-6 shadow-lg"
>
<Text className="text-xl font-semibold text-center">
{question}
</Text>
</Animated.View>
<Animated.View
style={backStyle}
className="absolute inset-0 bg-indigo-600 rounded-2xl p-6 shadow-lg"
>
<Text className="text-xl font-semibold text-center text-white">
{answer}
</Text>
</Animated.View>
</Pressable>
);
}
import { Stack, Tabs } from 'expo-router';
// Layout principal avec tabs
export default function TabsLayout() {
return (
<Tabs screenOptions={{ headerShown: false }}>
<Tabs.Screen
name="home"
options={{
title: 'Accueil',
tabBarIcon: ({ color }) => <HomeIcon color={color} />,
}}
/>
<Tabs.Screen
name="collections"
options={{
title: 'Collections',
tabBarIcon: ({ color }) => <FolderIcon color={color} />,
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explorer',
tabBarIcon: ({ color }) => <SearchIcon color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profil',
tabBarIcon: ({ color }) => <UserIcon color={color} />,
}}
/>
</Tabs>
);
}
FonctionnalitéImplémentation
VoiceOver/TalkBackLabels accessibles sur tous les éléments interactifs
Police adaptéeOption de police OpenDyslexic pour la dyslexie
Contraste élevéMode contraste pour malvoyants
Taille de texteRespect des préférences systÚme
Animations réduitesRespect de prefers-reduced-motion
<TouchableOpacity
accessible={true}
accessibilityLabel="Retourner la carte pour voir la réponse"
accessibilityRole="button"
accessibilityHint="Double-tap pour retourner"
onPress={flipCard}
>
{/* Contenu */}
</TouchableOpacity>
  • Lazy loading : Chargement diffĂ©rĂ© des Ă©crans
  • Memoization : useMemo et useCallback pour Ă©viter les re-renders
  • FlatList virtualization : Affichage optimisĂ© des longues listes
  • Image caching : Cache des images avec expo-image
  • Bundle splitting : SĂ©paration du code par routes

Application mobile pensĂ©e pour l’expĂ©rience utilisateur et l’accessibilitĂ©.