feat(auth): Amélioration UI et logique inscription parent étape 3
- Ajout du switch "Enfant à naître" et ajustement du champ prénom. - Amélioration de la gestion de l'affichage des photos (placeholder, kIsWeb). - Refactorisation des boutons avec HoverReliefWidget. - Localisation du DatePicker en français. - Nettoyage de l'intégration (annulée) de image_cropper. - Mise à jour de EVOLUTIONS_CDC.md.
@ -237,3 +237,21 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante :
|
||||
- Email non trouvé
|
||||
- Lien expiré
|
||||
- Mot de passe non conforme
|
||||
|
||||
## X. Amélioration de la Gestion des Photos Utilisateurs (Proposition)
|
||||
|
||||
### X.1 Recadrage et Redimensionnement des Photos
|
||||
|
||||
#### X.1.1 Fonctionnalités
|
||||
- **Contexte :** Lors du téléchargement de photos par les utilisateurs (photos de profil, photos d'enfants).
|
||||
- **Besoin :** Permettre à l'utilisateur de recadrer l'image (notamment en format carré pour les avatars) et potentiellement de la faire pivoter ou de zoomer avant son enregistrement final.
|
||||
- **Objectif :** Améliorer l'expérience utilisateur, assurer une meilleure qualité et cohérence visuelle des images stockées et affichées dans l'application.
|
||||
|
||||
#### X.1.2 Solution Technique Envisagée (pour discussion)
|
||||
- L'intégration d'une librairie Flutter tierce dédiée au recadrage d'image (par exemple, `image_cropper` ou `crop_image`) sera nécessaire après la sélection initiale de l'image via `image_picker`.
|
||||
- La tentative initiale avec `image_cropper` (version 5.0.1) a rencontré des difficultés techniques d'intégration (erreur "Too many positional arguments" persistante avec `AndroidUiSettings`) et a été mise en attente. Une investigation plus approfondie ou l'évaluation d'alternatives sera requise.
|
||||
|
||||
#### X.1.3 Impact sur l'application
|
||||
- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`).
|
||||
- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes.
|
||||
- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée.
|
||||
32
frontend/android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Suppression de l'activité pour image_cropper -->
|
||||
<!--
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||
-->
|
||||
|
||||
<!-- Don't delete the meta-data below. -->
|
||||
<meta-data
|
||||
@ -0,0 +1,44 @@
|
||||
package io.flutter.plugins;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.flutter.Log;
|
||||
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
|
||||
/**
|
||||
* Generated file. Do not edit.
|
||||
* This file is generated by the Flutter tool based on the
|
||||
* plugins that support the Android platform.
|
||||
*/
|
||||
@Keep
|
||||
public final class GeneratedPluginRegistrant {
|
||||
private static final String TAG = "GeneratedPluginRegistrant";
|
||||
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
||||
}
|
||||
try {
|
||||
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/android/local.properties
Normal file
@ -0,0 +1 @@
|
||||
flutter.sdk=C:\\Users\\marti\\dev\\flutter
|
||||
BIN
frontend/assets/images/card_blue.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
frontend/assets/images/card_blue_h.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
frontend/assets/images/card_lavander.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/card_lavander_h.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/card_yellow_h.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/coche.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
frontend/assets/images/photo.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
frontend/assets/images/plus.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
frontend/assets/images/square.png
Normal file
|
After Width: | Height: | Size: 890 KiB |
BIN
frontend/assets/images/switch_off.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
frontend/assets/images/switch_on.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
@ -1,44 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'screens/auth/login_screen.dart';
|
||||
import 'screens/legal/legal_page.dart';
|
||||
import 'screens/legal/privacy_page.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
|
||||
// import 'package:provider/provider.dart'; // Supprimer Provider
|
||||
import 'navigation/app_router.dart';
|
||||
// import 'theme/app_theme.dart'; // Supprimer AppTheme
|
||||
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
|
||||
|
||||
void main() => runApp(const PtiPasApp());
|
||||
void main() {
|
||||
runApp(const MyApp()); // Exécution simple
|
||||
}
|
||||
|
||||
final _router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (_, __) => const LoginPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/legal',
|
||||
builder: (_, __) => const LegalPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/privacy',
|
||||
builder: (_, __) => const PrivacyPage(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
class PtiPasApp extends StatelessWidget {
|
||||
const PtiPasApp({super.key});
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
// Pas besoin de Provider.of ici
|
||||
|
||||
return MaterialApp(
|
||||
title: 'P\'titsPas',
|
||||
routerConfig: _router,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
fontFamily: 'Merienda',
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: const Color(0xFF8AD0C8),
|
||||
brightness: Brightness.light,
|
||||
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
|
||||
textTheme: GoogleFonts.meriendaTextTheme(
|
||||
ThemeData.light().textTheme,
|
||||
),
|
||||
// TODO: Définir les couleurs principales si besoin
|
||||
),
|
||||
localizationsDelegates: const [ // Configuration pour la localisation
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [ // Langues supportées
|
||||
Locale('fr', 'FR'), // Français
|
||||
// Locale('en', 'US'), // Anglais, si besoin
|
||||
],
|
||||
locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut
|
||||
initialRoute: AppRouter.login,
|
||||
onGenerateRoute: AppRouter.generateRoute,
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,26 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../screens/auth/login_screen.dart';
|
||||
import '../screens/auth/register_choice_screen.dart';
|
||||
import '../screens/auth/parent_register_step1_screen.dart';
|
||||
import '../screens/auth/parent_register_step2_screen.dart';
|
||||
import '../screens/auth/parent_register_step3_screen.dart';
|
||||
import '../screens/home/home_screen.dart';
|
||||
|
||||
class AppRouter {
|
||||
static const String login = '/login';
|
||||
static const String registerChoice = '/register-choice';
|
||||
static const String parentRegisterStep1 = '/parent-register/step1';
|
||||
static const String parentRegisterStep2 = '/parent-register/step2';
|
||||
static const String parentRegisterStep3 = '/parent-register/step3';
|
||||
static const String home = '/home';
|
||||
|
||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||
Widget screen;
|
||||
bool slideTransition = false;
|
||||
|
||||
switch (settings.name) {
|
||||
case login:
|
||||
return MaterialPageRoute(builder: (_) => const LoginScreen());
|
||||
screen = const LoginPage();
|
||||
break;
|
||||
case registerChoice:
|
||||
screen = const RegisterChoiceScreen();
|
||||
slideTransition = true; // Activer la transition pour cet écran
|
||||
break;
|
||||
case parentRegisterStep1:
|
||||
screen = const ParentRegisterStep1Screen();
|
||||
slideTransition = true; // Activer la transition pour cet écran
|
||||
break;
|
||||
case parentRegisterStep2:
|
||||
screen = const ParentRegisterStep2Screen();
|
||||
slideTransition = true;
|
||||
break;
|
||||
case parentRegisterStep3:
|
||||
screen = const ParentRegisterStep3Screen();
|
||||
slideTransition = true;
|
||||
break;
|
||||
case home:
|
||||
return MaterialPageRoute(builder: (_) => const HomeScreen());
|
||||
screen = const HomeScreen();
|
||||
break;
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: Center(
|
||||
child: Text('Route non définie: [36m${settings.name}[0m'),
|
||||
),
|
||||
screen = Scaffold(
|
||||
body: Center(
|
||||
child: Text('Route non définie : ${settings.name}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (slideTransition) {
|
||||
return PageRouteBuilder(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => screen,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
const begin = Offset(1.0, 0.0); // Glisse depuis la droite
|
||||
const end = Offset.zero;
|
||||
const curve = Curves.easeInOut; // Animation douce
|
||||
|
||||
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
|
||||
var offsetAnimation = animation.drive(tween);
|
||||
|
||||
return SlideTransition(
|
||||
position: offsetAnimation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
transitionDuration: const Duration(milliseconds: 400), // Durée de la transition
|
||||
);
|
||||
} else {
|
||||
// Transition par défaut pour les autres écrans
|
||||
return MaterialPageRoute(builder: (_) => screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,7 +197,19 @@ class _LoginPageState extends State<LoginPage> {
|
||||
const SizedBox(height: 10),
|
||||
// Lien de création de compte
|
||||
Center(
|
||||
child: Container(), // Suppression du bouton 'Créer un compte'
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/register-choice');
|
||||
},
|
||||
child: Text(
|
||||
'Créer un compte',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 16,
|
||||
color: const Color(0xFF2D6A4F),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20), // Réduit l'espacement en bas
|
||||
],
|
||||
|
||||
226
frontend/lib/screens/auth/parent_register_step1_screen.dart
Normal file
@ -0,0 +1,226 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
|
||||
class ParentRegisterStep1Screen extends StatefulWidget {
|
||||
const ParentRegisterStep1Screen({super.key});
|
||||
|
||||
@override
|
||||
State<ParentRegisterStep1Screen> createState() => _ParentRegisterStep1ScreenState();
|
||||
}
|
||||
|
||||
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Contrôleurs pour les champs
|
||||
final _lastNameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_lastNameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
_addressController.dispose();
|
||||
_postalCodeController.dispose();
|
||||
_cityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Fond papier
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu centré
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Indicateur d'étape (à rendre dynamique)
|
||||
Text(
|
||||
'Étape 1/X',
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Texte d'instruction
|
||||
Text(
|
||||
'Merci de renseigner les informations du premier parent :',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Carte jaune contenant le formulaire
|
||||
Container(
|
||||
width: screenSize.width * 0.6,
|
||||
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/card_yellow_h.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Votre nom de famille')),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Votre prénom')),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone')),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail')),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe')),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe')),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Numéro et nom de votre rue'),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 2, child: _buildTextField(_postalCodeController, 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal')),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(flex: 3, child: _buildTextField(_cityController, 'Ville', hintText: 'Votre ville')),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron de navigation gauche (Retour)
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20, // Centré verticalement
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(math.pi), // Inverse horizontalement
|
||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context), // Retour à l'écran de choix
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron de navigation droit (Suivant)
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20, // Centré verticalement
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
// TODO: Sauvegarder les données du parent 1
|
||||
Navigator.pushNamed(context, '/parent-register/step2'); // Naviguer vers l'étape 2
|
||||
}
|
||||
},
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget pour construire les champs de texte avec le fond personnalisé
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String label, {
|
||||
TextInputType? keyboardType,
|
||||
bool obscureText = false,
|
||||
String? hintText,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$label :',
|
||||
style: GoogleFonts.merienda(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Container(
|
||||
height: 50,
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/input_field_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black87),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
|
||||
hintText: hintText ?? label,
|
||||
hintStyle: GoogleFonts.merienda(fontSize: 16, color: Colors.black38),
|
||||
),
|
||||
validator: (value) {
|
||||
// Validation désactivée
|
||||
return null;
|
||||
/*
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Ce champ est obligatoire';
|
||||
}
|
||||
// TODO: Ajouter des validations spécifiques (email, téléphone, mot de passe)
|
||||
if (label == 'Confirmation' && value != _passwordController.text) {
|
||||
return 'Les mots de passe ne correspondent pas';
|
||||
}
|
||||
return null;
|
||||
*/
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
374
frontend/lib/screens/auth/parent_register_step2_screen.dart
Normal file
@ -0,0 +1,374 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
|
||||
class ParentRegisterStep2Screen extends StatefulWidget {
|
||||
const ParentRegisterStep2Screen({super.key});
|
||||
|
||||
@override
|
||||
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
|
||||
}
|
||||
|
||||
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// TODO: Recevoir les infos du parent 1 pour pré-remplir l'adresse
|
||||
// String? _parent1Address;
|
||||
// String? _parent1PostalCode;
|
||||
// String? _parent1City;
|
||||
|
||||
bool _addParent2 = false; // Par défaut, on n'ajoute pas le parent 2
|
||||
bool _sameAddressAsParent1 = false;
|
||||
|
||||
// Contrôleurs pour les champs du parent 2
|
||||
final _lastNameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_lastNameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
_addressController.dispose();
|
||||
_postalCodeController.dispose();
|
||||
_cityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Helper pour activer/désactiver tous les champs sauf l'adresse
|
||||
bool get _parent2FieldsEnabled => _addParent2;
|
||||
// Helper pour activer/désactiver les champs d'adresse
|
||||
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Fond papier
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu centré
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Indicateur d'étape
|
||||
Text(
|
||||
'Étape 2/X', // Mettre à jour le numéro d'étape total
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Texte d'instruction
|
||||
Text(
|
||||
'Renseignez les informations du deuxième parent (optionnel) :',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Carte bleue contenant le formulaire
|
||||
Container(
|
||||
width: screenSize.width * 0.6,
|
||||
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
|
||||
decoration: const BoxDecoration( // Retour à la décoration
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/card_blue_h.png'), // Utilisation de l'image horizontale
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
// Suppression du Stack et Transform.rotate
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView( // Le SingleChildScrollView redevient l'enfant direct
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// --- Interrupteurs sur une ligne ---
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Option 1: Ajouter Parent 2
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.person_add_alt_1, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Ajouter Parent 2 ?',
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
_buildCustomSwitch(
|
||||
value: _addParent2,
|
||||
onChanged: (bool? newValue) {
|
||||
final bool actualValue = newValue ?? false;
|
||||
setState(() {
|
||||
_addParent2 = actualValue;
|
||||
if (!_addParent2) {
|
||||
_formKey.currentState?.reset();
|
||||
_lastNameController.clear();
|
||||
_firstNameController.clear();
|
||||
_phoneController.clear();
|
||||
_emailController.clear();
|
||||
_passwordController.clear();
|
||||
_confirmPasswordController.clear();
|
||||
_addressController.clear();
|
||||
_postalCodeController.clear();
|
||||
_cityController.clear();
|
||||
_sameAddressAsParent1 = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// Option 2: Même Adresse
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey), // Griser l'icône si désactivé
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Même Adresse ?',
|
||||
style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), // Griser le texte si désactivé
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
_buildCustomSwitch(
|
||||
value: _sameAddressAsParent1,
|
||||
onChanged: _addParent2 ? (bool? newValue) {
|
||||
final bool actualValue = newValue ?? false;
|
||||
setState(() {
|
||||
_sameAddressAsParent1 = actualValue;
|
||||
if (_sameAddressAsParent1) {
|
||||
_addressController.clear();
|
||||
_postalCodeController.clear();
|
||||
_cityController.clear();
|
||||
// TODO: Pré-remplir
|
||||
}
|
||||
});
|
||||
} : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 25), // Espacement ajusté après les switchs
|
||||
|
||||
// --- Champs du Parent 2 (conditionnels) ---
|
||||
// Nom & Prénom
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField(_lastNameController, 'Nom', hintText: 'Nom du deuxième parent', enabled: _parent2FieldsEnabled)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildTextField(_firstNameController, 'Prénom', hintText: 'Prénom du deuxième parent', enabled: _parent2FieldsEnabled)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Téléphone & Email
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField(_phoneController, 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son numéro de téléphone', enabled: _parent2FieldsEnabled)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildTextField(_emailController, 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son adresse e-mail', enabled: _parent2FieldsEnabled)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Mot de passe
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField(_passwordController, 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildTextField(_confirmPasswordController, 'Confirmation', obscureText: true, hintText: 'Confirmer son mot de passe', enabled: _parent2FieldsEnabled)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// --- Champs Adresse (conditionnels) ---
|
||||
_buildTextField(_addressController, 'Adresse (Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 2, child: _buildTextField(_postalCodeController, 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(flex: 3, child: _buildTextField(_cityController, 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron de navigation gauche (Retour)
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(math.pi),
|
||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context), // Retour à l'étape 1
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron de navigation droit (Suivant)
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: () {
|
||||
// Si on n'ajoute pas de parent 2, on passe directement
|
||||
if (!_addParent2) {
|
||||
// Naviguer vers l'étape 3 (enfants)
|
||||
print('Passer à l\'étape 3 (enfants) - Sans Parent 2');
|
||||
Navigator.pushNamed(context, '/parent-register/step3');
|
||||
return;
|
||||
}
|
||||
// Si on ajoute un parent 2
|
||||
// Valider seulement si on n'utilise PAS la même adresse
|
||||
bool isFormValid = true;
|
||||
// TODO: Remettre la validation quand elle sera prête
|
||||
/*
|
||||
if (!_sameAddressAsParent1) {
|
||||
isFormValid = _formKey.currentState?.validate() ?? false;
|
||||
}
|
||||
*/
|
||||
|
||||
if (isFormValid) {
|
||||
// TODO: Sauvegarder les données du parent 2
|
||||
print('Passer à l\'étape 3 (enfants) - Avec Parent 2');
|
||||
Navigator.pushNamed(context, '/parent-register/step3');
|
||||
}
|
||||
},
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- NOUVEAU WIDGET ---
|
||||
// Widget pour construire un switch personnalisé avec images
|
||||
Widget _buildCustomSwitch({required bool value, required ValueChanged<bool?>? onChanged}) {
|
||||
// --- DEBUG ---
|
||||
print("Building Custom Switch with value: $value");
|
||||
// -------------
|
||||
const double switchHeight = 25.0;
|
||||
const double switchWidth = 40.0;
|
||||
|
||||
return InkWell(
|
||||
onTap: onChanged != null ? () => onChanged(!value) : null,
|
||||
child: Opacity(
|
||||
// Griser le switch si désactivé
|
||||
opacity: onChanged != null ? 1.0 : 0.5,
|
||||
child: Image.asset(
|
||||
value ? 'assets/images/switch_on.png' : 'assets/images/switch_off.png',
|
||||
height: switchHeight,
|
||||
width: switchWidth,
|
||||
fit: BoxFit.contain, // Ou BoxFit.fill selon le rendu souhaité
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget pour construire les champs de texte (identique à l'étape 1)
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String label, {
|
||||
TextInputType? keyboardType,
|
||||
bool obscureText = false,
|
||||
String? hintText,
|
||||
bool enabled = true,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$label :',
|
||||
style: GoogleFonts.merienda(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Container(
|
||||
height: 50,
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/input_field_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
enabled: enabled,
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: enabled ? Colors.black87 : Colors.grey),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 14),
|
||||
hintText: hintText ?? label,
|
||||
hintStyle: GoogleFonts.merienda(fontSize: 16, color: Colors.black38),
|
||||
),
|
||||
validator: (value) {
|
||||
if (!enabled) return null; // Ne pas valider si désactivé
|
||||
// Le reste de la validation (commentée précédemment)
|
||||
return null;
|
||||
/*
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Ce champ est obligatoire';
|
||||
}
|
||||
// TODO: Validations spécifiques
|
||||
if (label == 'Confirmation' && value != _passwordController.text) {
|
||||
return 'Les mots de passe ne correspondent pas';
|
||||
}
|
||||
return null;
|
||||
*/
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
472
frontend/lib/screens/auth/parent_register_step3_screen.dart
Normal file
@ -0,0 +1,472 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
import '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
|
||||
import 'dart:io' show File, Platform; // Ajout de Platform
|
||||
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
|
||||
|
||||
// TODO: Créer un modèle de données pour l'enfant
|
||||
// class ChildData { ... }
|
||||
|
||||
class ParentRegisterStep3Screen extends StatefulWidget {
|
||||
const ParentRegisterStep3Screen({super.key});
|
||||
|
||||
@override
|
||||
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
|
||||
}
|
||||
|
||||
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
// TODO: Gérer une liste d'enfants et leurs contrôleurs respectifs
|
||||
// List<ChildData> _children = [ChildData()]; // Commencer avec un enfant
|
||||
final _formKey = GlobalKey<FormState>(); // Une clé par enfant sera nécessaire si validation complexe
|
||||
|
||||
// Contrôleurs pour le premier enfant (pour l'instant)
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _dobController = TextEditingController();
|
||||
bool _photoConsent = false;
|
||||
bool _multipleBirth = false;
|
||||
bool _isUnbornChild = false; // Nouvelle variable d'état
|
||||
// TODO: Ajouter variable pour stocker l'image sélectionnée (par enfant)
|
||||
// File? _childImage;
|
||||
|
||||
// File? _childImage; // Déjà présent et commenté
|
||||
// Liste pour stocker les images des enfants (si gestion multi-enfants)
|
||||
List<File?> _childImages = [null]; // Initialiser avec null pour le premier enfant
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_dobController.dispose();
|
||||
// TODO: Disposer les contrôleurs de tous les enfants
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// TODO: Pré-remplir le nom de famille avec celui du parent 1
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime now = DateTime.now();
|
||||
DateTime initialDatePickerDate = now;
|
||||
DateTime firstDatePickerDate = DateTime(1980);
|
||||
DateTime lastDatePickerDate = now;
|
||||
|
||||
if (_isUnbornChild) {
|
||||
firstDatePickerDate = now; // Ne peut pas être avant aujourd'hui si à naître
|
||||
lastDatePickerDate = now.add(const Duration(days: 300)); // Environ 10 mois dans le futur
|
||||
// Si une date de naissance avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage
|
||||
if (_dobController.text.isNotEmpty) {
|
||||
try {
|
||||
// Tenter de parser la date existante
|
||||
List<String> parts = _dobController.text.split('/');
|
||||
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
||||
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
||||
initialDatePickerDate = parsedDate;
|
||||
}
|
||||
} catch (e) { /* Ignorer si le format est incorrect */ }
|
||||
}
|
||||
} else {
|
||||
// Si une date prévisionnelle avait été entrée, on la garde pour initialDate si elle est dans la nouvelle plage
|
||||
if (_dobController.text.isNotEmpty) {
|
||||
try {
|
||||
List<String> parts = _dobController.text.split('/');
|
||||
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
||||
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
||||
initialDatePickerDate = parsedDate;
|
||||
}
|
||||
} catch (e) { /* Ignorer */ }
|
||||
}
|
||||
}
|
||||
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: initialDatePickerDate,
|
||||
firstDate: firstDatePickerDate,
|
||||
lastDate: lastDatePickerDate,
|
||||
locale: const Locale('fr', 'FR'),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_dobController.text = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode pour sélectionner une image
|
||||
Future<void> _pickImage(int childIndex) async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
try {
|
||||
final XFile? pickedFile = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
imageQuality: 70,
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
);
|
||||
|
||||
if (pickedFile != null) {
|
||||
// On utilise directement le fichier sélectionné, sans recadrage
|
||||
setState(() {
|
||||
if (childIndex < _childImages.length) {
|
||||
_childImages[childIndex] = File(pickedFile.path);
|
||||
} else {
|
||||
print("Erreur: Index d'enfant hors limites pour l'image.");
|
||||
}
|
||||
});
|
||||
} // Fin de if (pickedFile != null)
|
||||
|
||||
} catch (e) {
|
||||
print("Erreur lors de la sélection de l'image: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Fond papier
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu centré et scrollable
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40.0), // Ajout de padding vertical
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Indicateur d'étape
|
||||
Text(
|
||||
'Étape 3/X', // Mettre à jour le numéro d'étape total
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Texte d'instruction
|
||||
Text(
|
||||
'Merci de renseigner les informations de/vos enfant(s) :',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Zone principale : Cartes enfants + Bouton Ajouter
|
||||
// Utilisation d'une Row pour placer côte à côte comme sur la maquette
|
||||
// Il faudra peut-être ajuster pour les petits écrans (Wrap?)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center, // CHANGED: pour centrer verticalement
|
||||
children: [
|
||||
// TODO: Remplacer par une ListView ou Column dynamique basée sur _children
|
||||
// Pour l'instant, une seule carte
|
||||
_buildChildCard(context, 0), // Index 0 pour le premier enfant
|
||||
|
||||
const SizedBox(width: 30),
|
||||
|
||||
HoverReliefWidget(
|
||||
onPressed: () {
|
||||
print("Ajouter un enfant via HoverReliefWidget");
|
||||
// setState(() { _children.add(ChildData()); });
|
||||
},
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: Image.asset(
|
||||
'assets/images/plus.png',
|
||||
height: 80,
|
||||
width: 80,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Chevrons de navigation (identiques aux étapes précédentes)
|
||||
// Chevron Gauche (Retour Step 2)
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(math.pi),
|
||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context), // Retour étape 2
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron Droit (Suivant Step 4)
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: () {
|
||||
// TODO: Valider les infos enfants et Naviguer vers l'étape 4
|
||||
print('Passer à l\'étape 4 (Situation familiale)');
|
||||
// Navigator.pushNamed(context, '/parent-register/step4');
|
||||
},
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget pour construire UNE carte enfant
|
||||
// L'index permettra de lier aux bons contrôleurs et données
|
||||
Widget _buildChildCard(BuildContext context, int index) {
|
||||
final File? currentChildImage = (index < _childImages.length) ? _childImages[index] : null;
|
||||
|
||||
// TODO: Déterminer la couleur de base de card_lavander.png et ajuster ces couleurs d'ombre
|
||||
final Color baseLavandeColor = Colors.purple.shade200; // Placeholder pour la couleur de la carte lavande
|
||||
final Color initialPhotoShadow = baseLavandeColor.withAlpha(90);
|
||||
final Color hoverPhotoShadow = baseLavandeColor.withAlpha(130);
|
||||
|
||||
return Container(
|
||||
width: 300,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('assets/images/card_lavander.png'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
HoverReliefWidget(
|
||||
onPressed: () {
|
||||
_pickImage(index);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
initialShadowColor: initialPhotoShadow, // Ombre lavande
|
||||
hoverShadowColor: hoverPhotoShadow, // Ombre lavande au survol
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: currentChildImage != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: kIsWeb // Condition pour le Web
|
||||
? Image.network( // Utiliser Image.network pour le Web
|
||||
currentChildImage.path,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
// Optionnel: Afficher un placeholder ou un message en cas d'erreur de chargement
|
||||
print("Erreur de chargement de l'image réseau: $error");
|
||||
return const Icon(Icons.broken_image, size: 40);
|
||||
},
|
||||
)
|
||||
: Image.file( // Utiliser Image.file pour les autres plateformes
|
||||
currentChildImage,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: Image.asset(
|
||||
'assets/images/photo.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10), // Espace après la photo
|
||||
|
||||
// Nouveau Switch pour "Enfant à naître ?"
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Aligner le label à gauche, switch à droite
|
||||
children: [
|
||||
Text(
|
||||
'Enfant à naître ?',
|
||||
style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Switch(
|
||||
value: _isUnbornChild,
|
||||
onChanged: (bool newValue) {
|
||||
setState(() {
|
||||
_isUnbornChild = newValue;
|
||||
// Optionnel: Réinitialiser la date si le type change
|
||||
// _dobController.clear();
|
||||
});
|
||||
},
|
||||
activeColor: Theme.of(context).primaryColor, // Utiliser une couleur de thème
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15), // Espace après le switch
|
||||
|
||||
_buildTextField(
|
||||
_firstNameController,
|
||||
'Prénom',
|
||||
hintText: _isUnbornChild ? 'Prénom (optionnel)' : 'Prénom de l\'enfant', // HintText ajusté
|
||||
isRequired: !_isUnbornChild,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_buildTextField(_lastNameController, 'Nom', hintText: 'Nom de l\'enfant', enabled: true),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_buildTextField(
|
||||
_dobController,
|
||||
_isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
||||
hintText: 'JJ/MM/AAAA',
|
||||
readOnly: true,
|
||||
onTap: () => _selectDate(context),
|
||||
suffixIcon: Icons.calendar_today,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildCustomCheckbox(
|
||||
label: 'Consentement photo',
|
||||
value: _photoConsent,
|
||||
onChanged: (newValue) {
|
||||
setState(() => _photoConsent = newValue);
|
||||
}
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildCustomCheckbox(
|
||||
label: 'Naissance multiple',
|
||||
value: _multipleBirth,
|
||||
onChanged: (newValue) {
|
||||
setState(() => _multipleBirth = newValue);
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget pour construire une checkbox personnalisée
|
||||
Widget _buildCustomCheckbox({required String label, required bool value, required ValueChanged<bool> onChanged}) {
|
||||
const double checkboxSize = 20.0;
|
||||
const double checkmarkSizeFactor = 1.4; // Augmenté pour une coche plus grande
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onChanged(!value),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox( // Envelopper le Stack dans un SizedBox pour fixer sa taille
|
||||
width: checkboxSize,
|
||||
height: checkboxSize,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/square.png',
|
||||
height: checkboxSize, // Taille fixe
|
||||
width: checkboxSize, // Taille fixe
|
||||
),
|
||||
if (value)
|
||||
Image.asset(
|
||||
'assets/images/coche.png',
|
||||
height: checkboxSize * checkmarkSizeFactor,
|
||||
width: checkboxSize * checkmarkSizeFactor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(label, style: GoogleFonts.merienda(fontSize: 14)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget pour construire les champs de texte (peut être externalisé)
|
||||
// Ajout de onTap et suffixIcon pour le DatePicker
|
||||
Widget _buildTextField(
|
||||
TextEditingController controller,
|
||||
String label, {
|
||||
TextInputType? keyboardType,
|
||||
bool obscureText = false,
|
||||
String? hintText,
|
||||
bool enabled = true,
|
||||
bool readOnly = false,
|
||||
VoidCallback? onTap,
|
||||
IconData? suffixIcon,
|
||||
bool isRequired = true, // Nouveau paramètre, par défaut à true
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.merienda(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
height: 45, // Hauteur fixe pour correspondre à l'image de fond
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage( // Rétablir input_field_bg.png
|
||||
image: AssetImage('assets/images/input_field_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
// Pas de borderRadius ici si l'image de fond les a déjà
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
enabled: enabled,
|
||||
readOnly: readOnly,
|
||||
onTap: onTap,
|
||||
style: GoogleFonts.merienda(fontSize: 15, color: enabled ? Colors.black87 : Colors.grey),
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12), // Augmentation du padding vertical
|
||||
hintText: hintText,
|
||||
hintStyle: GoogleFonts.merienda(fontSize: 15, color: Colors.black38),
|
||||
suffixIcon: suffixIcon != null ? Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0), // Espace pour l'icône
|
||||
child: Icon(suffixIcon, color: Colors.black54, size: 20),
|
||||
) : null,
|
||||
isDense: true, // Aide à réduire la hauteur par défaut
|
||||
),
|
||||
validator: (value) {
|
||||
if (!enabled) return null;
|
||||
if (readOnly) return null;
|
||||
if (isRequired && (value == null || value.isEmpty)) { // Validation conditionnée par isRequired
|
||||
return 'Ce champ est obligatoire';
|
||||
}
|
||||
// TODO: Validations spécifiques (à garder si pertinent pour d'autres champs)
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
161
frontend/lib/screens/auth/register_choice_screen.dart
Normal file
@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
import '../../widgets/hover_relief_widget.dart'; // Import du widget générique
|
||||
|
||||
class RegisterChoiceScreen extends StatelessWidget {
|
||||
const RegisterChoiceScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Fond papier
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
),
|
||||
|
||||
// Bouton Retour (chevron gauche)
|
||||
Positioned(
|
||||
top: 40,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(math.pi),
|
||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu principal en Row (Gauche / Droite)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||
child: Row(
|
||||
children: [
|
||||
// Partie Gauche: Texte d'instruction centré
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Veuillez choisir votre\ntype de compte :',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Espace entre les deux parties
|
||||
SizedBox(width: screenSize.width * 0.05),
|
||||
|
||||
// Partie Droite: Carte rose avec les boutons
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: screenSize.height * 0.78, // Augmenté pour éviter l'overflow
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 2 / 3,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/card_rose.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Bouton "Parents" avec HoverReliefWidget appliqué uniquement à l'image
|
||||
_buildChoiceButton(
|
||||
context: context,
|
||||
iconPath: 'assets/images/icon_parents.png',
|
||||
label: 'Parents',
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/parent-register/step1');
|
||||
},
|
||||
),
|
||||
// Bouton "Assistante Maternelle" avec HoverReliefWidget appliqué uniquement à l'image
|
||||
_buildChoiceButton(
|
||||
context: context,
|
||||
iconPath: 'assets/images/icon_assmat.png',
|
||||
label: 'Assistante Maternelle',
|
||||
onPressed: () {
|
||||
// TODO: Naviguer vers l'écran d'inscription assmat
|
||||
print('Choix: Assistante Maternelle');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Nouvelle méthode helper pour construire les boutons de choix
|
||||
Widget _buildChoiceButton({
|
||||
required BuildContext context,
|
||||
required String iconPath,
|
||||
required String label,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
// TODO: Déterminer la couleur de base de card_rose.png et ajuster ces couleurs d'ombre
|
||||
final Color baseRoseColor = Colors.pink.shade300; // Placeholder
|
||||
final Color initialShadow = baseRoseColor.withAlpha(90); // Rose plus foncé et transparent pour l'ombre initiale
|
||||
final Color hoverShadow = baseRoseColor.withAlpha(130); // Rose encore plus foncé pour l'ombre au survol
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
HoverReliefWidget(
|
||||
onPressed: onPressed,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
initialShadowColor: initialShadow, // Ombre rose initiale
|
||||
hoverShadowColor: hoverShadow, // Ombre rose au survol
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Image.asset(iconPath, height: 140),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
label,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black.withOpacity(0.85),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- La classe HoverChoiceButton peut maintenant être supprimée si elle n'est plus utilisée ailleurs ---
|
||||
// class HoverChoiceButton extends StatefulWidget { ... }
|
||||
// class _HoverChoiceButtonState extends State<HoverChoiceButton> { ... }
|
||||
@ -1,50 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../theme/theme_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
String _getThemeName(ThemeType type) {
|
||||
switch (type) {
|
||||
case ThemeType.defaultTheme:
|
||||
return "P'titsPas";
|
||||
case ThemeType.pastelTheme:
|
||||
return "Pastel";
|
||||
case ThemeType.darkTheme:
|
||||
return "Sombre";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Accueil'),
|
||||
actions: [
|
||||
Consumer<ThemeProvider>(
|
||||
builder: (context, themeProvider, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: DropdownButton<ThemeType>(
|
||||
value: themeProvider.currentTheme,
|
||||
items: ThemeType.values.map((ThemeType type) {
|
||||
return DropdownMenuItem<ThemeType>(
|
||||
value: type,
|
||||
child: Text(_getThemeName(type)),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (ThemeType? newValue) {
|
||||
if (newValue != null) {
|
||||
themeProvider.setTheme(newValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Bienvenue sur P\'titsPas !'),
|
||||
|
||||
88
frontend/lib/widgets/hover_relief_widget.dart
Normal file
@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HoverReliefWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final BorderRadius borderRadius;
|
||||
final double initialElevation;
|
||||
final double hoverElevation;
|
||||
final double scaleFactor;
|
||||
final bool enableHoverEffect; // Pour activer/désactiver l'effet de survol
|
||||
final Color initialShadowColor; // Nouveau paramètre
|
||||
final Color hoverShadowColor; // Nouveau paramètre
|
||||
|
||||
const HoverReliefWidget({
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(15.0)),
|
||||
this.initialElevation = 4.0,
|
||||
this.hoverElevation = 8.0,
|
||||
this.scaleFactor = 1.03, // Légèrement réduit par rapport à l'exemple précédent
|
||||
this.enableHoverEffect = true, // Par défaut, l'effet est activé
|
||||
this.initialShadowColor = const Color(0x26000000), // Default: Colors.black.withOpacity(0.15)
|
||||
this.hoverShadowColor = const Color(0x4D000000), // Default: Colors.black.withOpacity(0.3)
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HoverReliefWidget> createState() => _HoverReliefWidgetState();
|
||||
}
|
||||
|
||||
class _HoverReliefWidgetState extends State<HoverReliefWidget> {
|
||||
bool _isHovering = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool canHover = widget.enableHoverEffect && widget.onPressed != null;
|
||||
|
||||
final hoverTransform = Matrix4.identity()..scale(widget.scaleFactor);
|
||||
final transform = _isHovering && canHover ? hoverTransform : Matrix4.identity();
|
||||
final shadowColor = _isHovering && canHover ? widget.hoverShadowColor : widget.initialShadowColor;
|
||||
final elevation = _isHovering && canHover ? widget.hoverElevation : widget.initialElevation;
|
||||
|
||||
Widget content = AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
transform: transform,
|
||||
transformAlignment: Alignment.center,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
elevation: elevation,
|
||||
shadowColor: shadowColor,
|
||||
borderRadius: widget.borderRadius,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.onPressed == null) {
|
||||
// Si non cliquable, on retourne juste le contenu avec l'élévation initiale (pas de survol)
|
||||
// Ajustement: pour toujours avoir un Material de base même si non cliquable et sans hover.
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
elevation: widget.initialElevation, // Utilise l'élévation initiale
|
||||
shadowColor: widget.initialShadowColor, // Appliqué ici pour l'état non cliquable
|
||||
borderRadius: widget.borderRadius,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) {
|
||||
if (widget.enableHoverEffect) setState(() => _isHovering = true);
|
||||
},
|
||||
onExit: (_) {
|
||||
if (widget.enableHoverEffect) setState(() => _isHovering = false);
|
||||
},
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: InkWell(
|
||||
onTap: widget.onPressed,
|
||||
borderRadius: widget.borderRadius,
|
||||
hoverColor: Colors.transparent,
|
||||
splashColor: Colors.grey.withOpacity(0.2),
|
||||
highlightColor: Colors.grey.withOpacity(0.1),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -126,6 +126,11 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -240,6 +245,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
js:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@ -9,6 +9,8 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
provider: ^6.1.1
|
||||
go_router: ^13.2.5
|
||||
google_fonts: ^6.1.0
|
||||
@ -27,13 +29,7 @@ flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/images/logo.png
|
||||
- assets/images/river_logo_desktop.png
|
||||
- assets/images/paper2.png
|
||||
- assets/images/field_email.png
|
||||
- assets/images/field_password.png
|
||||
- assets/images/btn_green.png
|
||||
- assets/images/icon.png
|
||||
- assets/images/ # Déclarer le dossier entier
|
||||
|
||||
fonts:
|
||||
- family: Merienda
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="P'titsPas - Grandir pas à pas, sereinement">
|
||||
<meta name="description" content="Application de gestion de la garde d'enfants pour les collectivités locales.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
@ -27,11 +27,17 @@
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="assets/images/icon.png"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>P'titsPas</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<!-- Suppression des dépendances image_cropper web -->
|
||||
<!--
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css">
|
||||
-->
|
||||
|
||||
<script>
|
||||
// The value below is injected by flutter build, do not touch.
|
||||
const serviceWorkerVersion = null;
|
||||
|
||||
55
lib/main.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/screens/auth/login_screen.dart';
|
||||
import 'package:p_tits_pas/screens/auth/register_choice_screen.dart';
|
||||
import 'package:p_tits_pas/screens/auth/parent_register_step1_screen.dart';
|
||||
import 'package:p_tits_pas/screens/auth/parent_register_step2_screen.dart';
|
||||
import 'package:p_tits_pas/screens/auth/parent_register_step3_screen.dart';
|
||||
|
||||
void main() {
|
||||
// TODO: Initialiser SharedPreferences, Provider, etc.
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'P\'titsPas',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue, // TODO: Utiliser la palette de la charte graphique
|
||||
textTheme: GoogleFonts.merriweatherTextTheme(
|
||||
Theme.of(context).textTheme,
|
||||
),
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
// Gestionnaire de routes initial (simple pour l'instant)
|
||||
initialRoute: '/', // Ou '/login' selon le point d'entrée désiré
|
||||
routes: {
|
||||
'/': (context) => const LoginScreen(), // Exemple, pourrait être RegisterChoiceScreen aussi
|
||||
'/login': (context) => const LoginScreen(),
|
||||
'/register-choice': (context) => const RegisterChoiceScreen(),
|
||||
'/parent-register/step1': (context) => const ParentRegisterStep1Screen(),
|
||||
'/parent-register/step2': (context) => const ParentRegisterStep2Screen(),
|
||||
'/parent-register/step3': (context) => const ParentRegisterStep3Screen(),
|
||||
// TODO: Ajouter les autres routes (step 4, etc., dashboard...)
|
||||
},
|
||||
// Gestion des routes inconnues
|
||||
onUnknownRoute: (settings) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
body: Center(
|
||||
child: Text(
|
||||
'Route inconnue :\n${settings.name}',
|
||||
style: GoogleFonts.merriweather(fontSize: 20, color: Colors.red),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||