fix(login): position formulaire sous slogan par ratio image (river_logo_mobile)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MARTIN Julien 2026-02-08 18:53:37 +01:00
parent 813fdb8449
commit 480f4a9396
4 changed files with 273 additions and 33 deletions

View File

@ -1,12 +1,37 @@
# 🎫 Liste Complète des Tickets - Projet P'titsPas
**Version** : 1.0
**Date** : 25 Novembre 2025
**Version** : 1.2
**Date** : 27 Janvier 2026
**Auteur** : Équipe PtitsPas
**Estimation totale** : ~173h
---
## 🔗 Liste des tickets Gitea
Correspondance entre les numéros dissues Gitea et les tickets de ce document.
| Gitea # | Titre court | Priorité | Statut | Section doc |
|--------|--------------|----------|--------|-------------|
| 1 | BDD - Champs manquants CDC | P0 | Ouvert | § Ticket #1 |
| 2 | BDD - Table présentation dossier parent | P0 | Ouvert | § Ticket #2 |
| 3 | BDD - Tokens création MDP | P0 | ✅ Fermé | § Ticket #3 |
| 4 | BDD - Champ genre enfants | P0 | ✅ Fermé | § Ticket #4 |
| 5 | BDD - Supprimer champs obsolètes | P0 | Ouvert | § Ticket #5 |
| 6 | BDD - Table configuration système | P0 | Ouvert | § Ticket #6 |
| 68 | BDD - Documents légaux & acceptations | P0 | ✅ Fermé | § Ticket #7 |
| 73 | Frontend - Inscription Parent Étape 1 | P3 | ✅ Fermé (PR) | § Ticket #36 |
| 78 | Frontend - Infrastructure formulaires multi-modes | P3 | ✅ Fermé | § Ticket #78 |
| 79 | Frontend - Renommer Nanny en AM | P3 | ✅ Fermé | § Ticket #79 |
| 81 | Frontend - Corrections refactoring widgets | P3 | ✅ Fermé | § Ticket #81 |
| 83 | Frontend - RegisterChoiceScreen mobile | P3 | ✅ Fermé | § Ticket #83 |
*Les autres tickets (sans numéro Gitea dans ce tableau) sont décrits dans les sections par priorité cidessous ; les numéros de section (#1 à #83) sont les références internes du document.*
**Point API (tickets frontend)** 27/01/2026 : 20 issues avec le label `frontend` dans Gitea (12 ouvertes, 8 fermées). Numéros concernés : 3542, 4351, 54, 82, 83. Les #73, #78, #79, #81 sont fermés mais sans label dans lAPI. Détail : `docs/POINT_TICKETS_FRONT_API.txt`.
---
## 📊 Vue d'ensemble
### Répartition par priorité
@ -1159,7 +1184,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit
---
**Dernière mise à jour** : 25 Novembre 2025
**Version** : 1.0
**Statut** : ✅ Prêt pour création dans Gitea
**Dernière mise à jour** : 27 Janvier 2026
**Version** : 1.2
**Statut** : ✅ À jour

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

View File

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:url_launcher/url_launcher.dart';
import 'package:go_router/go_router.dart';
import 'package:p_tits_pas/services/bug_report_service.dart';
@ -17,7 +16,7 @@ class LoginScreen extends StatefulWidget {
State<LoginScreen> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginScreen> {
class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@ -25,13 +24,28 @@ class _LoginPageState extends State<LoginScreen> {
bool _isLoading = false;
String? _errorMessage;
static const double _mobileBreakpoint = 900.0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
if (mounted) setState(() {});
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
@ -122,15 +136,19 @@ class _LoginPageState extends State<LoginScreen> {
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < _mobileBreakpoint;
if (isMobile) {
return Scaffold(
backgroundColor: Colors.transparent,
body: _buildMobileLayout(context),
);
}
return Scaffold(
backgroundColor: Colors.transparent,
body: LayoutBuilder(
builder: (context, constraints) {
// Version desktop (web)
if (kIsWeb) {
final w = constraints.maxWidth;
final h = constraints.maxHeight;
return FutureBuilder(
future: _getImageDimensions(),
builder: (context, snapshot) {
@ -289,18 +307,19 @@ class _LoginPageState extends State<LoginScreen> {
),
),
),
// Pied de page
// Pied de page (Wrap pour éviter overflow sur petite largeur)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: Colors.transparent,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
child: Wrap(
alignment: WrapAlignment.center,
runSpacing: 8,
children: [
_FooterLink(
text: 'Contact support',
@ -349,17 +368,207 @@ class _LoginPageState extends State<LoginScreen> {
);
},
);
}
// Version mobile (à implémenter)
return const Center(
child: Text('Version mobile à implémenter'),
);
},
),
);
}
/// Dimensions de river_logo_mobile.png (à mettre à jour si l'asset change).
static const int _riverLogoMobileWidth = 600;
static const int _riverLogoMobileHeight = 1080;
/// Fraction de la hauteur de l'image où se termine visuellement le slogan (0 = haut, 1 = bas).
static const double _sloganEndFraction = 0.42;
static const double _gapBelowSlogan = 12.0;
Widget _buildMobileLayout(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final h = constraints.maxHeight;
final w = constraints.maxWidth;
final imageAspectRatio = _riverLogoMobileHeight / _riverLogoMobileWidth;
final formTop = w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan;
return Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: Image.asset(
'assets/images/paper2.png',
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
Positioned(
top: 0,
left: 0,
right: 0,
height: h * 1.2,
child: OverflowBox(
alignment: Alignment.topCenter,
minWidth: w,
maxWidth: w,
minHeight: 0,
maxHeight: h * 2.5,
child: Image.asset(
'assets/images/river_logo_mobile.png',
width: w,
fit: BoxFit.fitWidth,
),
),
),
Positioned(
top: formTop,
left: 0,
right: 0,
bottom: 0,
child: SafeArea(
top: false,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 16),
CustomAppTextField(
controller: _emailController,
labelText: 'Email',
showLabel: false,
hintText: 'Votre adresse email',
validator: _validateEmail,
style: CustomAppTextFieldStyle.lavande,
fieldHeight: 48,
fieldWidth: double.infinity,
),
const SizedBox(height: 12),
CustomAppTextField(
controller: _passwordController,
labelText: 'Mot de passe',
showLabel: false,
hintText: 'Votre mot de passe',
obscureText: true,
validator: _validatePassword,
style: CustomAppTextFieldStyle.jaune,
fieldHeight: 48,
fieldWidth: double.infinity,
),
if (_errorMessage != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.red.shade300),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red.shade700, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
_errorMessage!,
style: GoogleFonts.merienda(fontSize: 12, color: Colors.red.shade700),
),
),
],
),
),
],
const SizedBox(height: 12),
_isLoading
? const CircularProgressIndicator()
: ImageButton(
bg: 'assets/images/bg_green.png',
width: double.infinity,
height: 44,
text: 'Se connecter',
textColor: const Color(0xFF2D6A4F),
onPressed: _handleLogin,
),
const SizedBox(height: 12),
TextButton(
onPressed: () { /* TODO */ },
child: Text(
'Mot de passe oublié ?',
style: GoogleFonts.merienda(
fontSize: 14,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
TextButton(
onPressed: () => context.go('/register-choice'),
child: Text(
'Créer un compte',
style: GoogleFonts.merienda(
fontSize: 16,
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 12, top: 8),
child: Wrap(
alignment: WrapAlignment.center,
runSpacing: 6,
spacing: 4,
children: [
_FooterLink(
text: 'Contact support',
fontSize: 11,
onTap: () async {
final uri = Uri(scheme: 'mailto', path: 'support@supernounou.local');
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Impossible d\'ouvrir le client mail', style: GoogleFonts.merienda())),
);
}
},
),
_FooterLink(
text: 'Signaler un bug',
fontSize: 11,
onTap: () => _showBugReportDialog(context),
),
_FooterLink(
text: 'Mentions légales',
fontSize: 11,
onTap: () => context.go('/legal'),
),
_FooterLink(
text: 'Politique de confidentialité',
fontSize: 11,
onTap: () => context.go('/privacy'),
),
],
),
),
],
),
),
),
],
);
},
);
}
void _showBugReportDialog(BuildContext context) {
final TextEditingController controller = TextEditingController();
@ -471,10 +680,12 @@ class ImageDimensions {
class _FooterLink extends StatelessWidget {
final String text;
final VoidCallback onTap;
final double fontSize;
const _FooterLink({
required this.text,
required this.onTap,
this.fontSize = 14.0,
});
@override
@ -482,11 +693,11 @@ class _FooterLink extends StatelessWidget {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: EdgeInsets.symmetric(horizontal: fontSize > 12 ? 8.0 : 4.0),
child: Text(
text,
style: GoogleFonts.merienda(
fontSize: 14,
fontSize: fontSize,
color: Colors.black87,
decoration: TextDecoration.underline,
),

View File

@ -25,11 +25,13 @@ class CustomAppTextField extends StatefulWidget {
final IconData? suffixIcon;
final double labelFontSize;
final double inputFontSize;
final bool showLabel;
const CustomAppTextField({
super.key,
required this.controller,
required this.labelText,
this.showLabel = true,
this.hintText = '',
this.fieldWidth = 300.0,
this.fieldHeight = 53.0,
@ -73,6 +75,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showLabel) ...[
Text(
widget.labelText,
style: GoogleFonts.merienda(
@ -82,6 +85,7 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
),
),
const SizedBox(height: 6),
],
SizedBox(
width: widget.fieldWidth,
height: dynamicFieldHeight,