Accueil

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.
| Composant | Technologie | RĂŽle |
|---|---|---|
| Framework | React Native | Base de lâapplication |
| Toolchain | Expo | Build, déploiement, OTA updates |
| Langage | TypeScript | Typage statique |
| Ătat global | Zustand | Gestion dâĂ©tat lĂ©gĂšre |
| Navigation | React Navigation | Navigation entre écrans |
| Styling | NativeWind | Tailwind CSS pour RN |
| HTTP Client | Axios | RequĂȘtes API |
| Forms | React Hook Form | Gestion des formulaires |
| Validation | Zod | Validation 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/ # ConstantesAccueil

Collections

Révision

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' } ));interface CollectionsState { collections: Collection[]; isLoading: boolean; fetchCollections: () => Promise<void>; createCollection: (data: CreateCollectionDTO) => Promise<Collection>; deleteCollection: (id: number) => Promise<void>;}
export const useCollectionsStore = create<CollectionsState>((set, get) => ({ collections: [], isLoading: false,
fetchCollections: async () => { set({ isLoading: true }); try { const collections = await collectionsService.getAll(); set({ collections, isLoading: false }); } catch (error) { set({ isLoading: false }); throw error; } },
createCollection: async (data) => { const collection = await collectionsService.create(data); set((state) => ({ collections: [...state.collections, collection], })); return collection; },}));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 tokenapi.interceptors.request.use((config) => { const token = useAuthStore.getState().token; if (token) { config.headers.Authorization = `Bearer ${token}`; } return config;});
// Intercepteur pour gérer les erreursapi.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 tabsexport 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/TalkBack | Labels accessibles sur tous les éléments interactifs |
| Police adaptée | Option de police OpenDyslexic pour la dyslexie |
| Contraste élevé | Mode contraste pour malvoyants |
| Taille de texte | Respect des préférences systÚme |
| Animations réduites | Respect 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>useMemo et useCallback pour Ă©viter les re-rendersApplication mobile pensĂ©e pour lâexpĂ©rience utilisateur et lâaccessibilitĂ©.