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.
This commit is contained in:
Julien Martin 2025-05-06 23:44:10 +02:00
parent bbdacd68aa
commit df56ba11df
28 changed files with 1597 additions and 87 deletions

View File

@ -236,4 +236,22 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante :
- Messages d'erreur clairs en cas de : - Messages d'erreur clairs en cas de :
- Email non trouvé - Email non trouvé
- Lien expiré - Lien expiré
- Mot de passe non conforme - 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.

View 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

View File

@ -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);
}
}
}

View File

@ -0,0 +1 @@
flutter.sdk=C:\\Users\\marti\\dev\\flutter

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

View File

@ -1,44 +1,43 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart';
import 'screens/auth/login_screen.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
import 'screens/legal/legal_page.dart'; // import 'package:provider/provider.dart'; // Supprimer Provider
import 'screens/legal/privacy_page.dart'; 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( class MyApp extends StatelessWidget {
routes: [ const MyApp({super.key});
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});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp.router( // Pas besoin de Provider.of ici
return MaterialApp(
title: 'P\'titsPas', title: 'P\'titsPas',
routerConfig: _router, theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
debugShowCheckedModeBanner: false, textTheme: GoogleFonts.meriendaTextTheme(
theme: ThemeData( ThemeData.light().textTheme,
fontFamily: 'Merienda',
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF8AD0C8),
brightness: Brightness.light,
), ),
// 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,
); );
} }
} }

View File

@ -1,26 +1,76 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../screens/auth/login_screen.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'; import '../screens/home/home_screen.dart';
class AppRouter { class AppRouter {
static const String login = '/login'; 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 const String home = '/home';
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen;
bool slideTransition = false;
switch (settings.name) { switch (settings.name) {
case login: 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: case home:
return MaterialPageRoute(builder: (_) => const HomeScreen()); screen = const HomeScreen();
break;
default: default:
return MaterialPageRoute( screen = Scaffold(
builder: (_) => Scaffold( body: Center(
body: Center( child: Text('Route non définie : ${settings.name}'),
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);
}
} }
} }

View File

@ -197,7 +197,19 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 10), const SizedBox(height: 10),
// Lien de création de compte // Lien de création de compte
Center( 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 const SizedBox(height: 20), // Réduit l'espacement en bas
], ],

View 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;
*/
},
),
),
],
);
}
}

View 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;
*/
},
),
),
],
);
}
}

View 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 é 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 é 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;
},
),
),
],
);
}
}

View 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> { ... }

View File

@ -1,50 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../theme/theme_provider.dart';
import '../../theme/app_theme.dart';
class HomeScreen extends StatelessWidget { class HomeScreen extends StatelessWidget {
const HomeScreen({super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Accueil'), 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( body: const Center(
child: Text('Bienvenue sur P\'titsPas !'), child: Text('Bienvenue sur P\'titsPas !'),

View 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,
),
);
}
}

View File

@ -126,6 +126,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.3" version: "2.0.3"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -240,6 +245,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1+1" version: "0.2.1+1"
intl:
dependency: transitive
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
js: js:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -9,6 +9,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
provider: ^6.1.1 provider: ^6.1.1
go_router: ^13.2.5 go_router: ^13.2.5
google_fonts: ^6.1.0 google_fonts: ^6.1.0
@ -27,13 +29,7 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/images/logo.png - assets/images/ # Déclarer le dossier entier
- 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
fonts: fonts:
- family: Merienda - family: Merienda

View File

@ -18,7 +18,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <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 --> <!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
@ -27,11 +27,17 @@
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon --> <!-- 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> <title>P'titsPas</title>
<link rel="manifest" href="manifest.json"> <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> <script>
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null; const serviceWorkerVersion = null;

55
lib/main.dart Normal file
View 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,
),
),
),
);
},
);
}
}