Compare commits

..

2 Commits

Author SHA1 Message Date
8ffe5d27ea feat: Récupération Step1 et Step2 depuis master
Ticket #38
2025-12-02 15:50:31 +01:00
9a6590c31b fix(frontend): Step3 - Amélioration genre + ajout fichiers Docker
- Suppression label 'Genre', boutons radio centrés (Fille/Garçon)
- Réduction espacements pour éviter dépassement
- Ajout docker-compose.yml et Dockerfiles depuis master

Ticket #38
2025-12-02 15:40:46 +01:00
8 changed files with 648 additions and 29 deletions

42
backend/Dockerfile Normal file
View File

@ -0,0 +1,42 @@
FROM node:22-alpine AS builder
WORKDIR /app
# Copier les fichiers de configuration
COPY package*.json ./
COPY tsconfig*.json ./
COPY nest-cli.json ./
# Installer TOUTES les dépendances (dev + prod pour le build)
RUN npm install && npm cache clean --force
# Copier le code source
COPY src ./src
# Builder l'application
RUN npm run build
# Stage production
FROM node:22-alpine AS production
WORKDIR /app
# Installer seulement les dépendances de production
COPY package*.json ./
RUN npm install --only=production && npm cache clean --force
# Copier le build depuis le stage builder
COPY --from=builder /app/dist ./dist
# Créer un utilisateur non-root
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001
# Créer le dossier uploads et donner les permissions
RUN mkdir -p /app/uploads/photos && chown -R nestjs:nodejs /app/uploads
USER nestjs
EXPOSE 3000
CMD ["node", "dist/main"]

100
docker-compose.yml Normal file
View File

@ -0,0 +1,100 @@
services:
# Base de données PostgreSQL
database:
image: postgres:17
container_name: ptitspas-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./database/BDD.sql:/docker-entrypoint-initdb.d/01_init.sql
- postgres_data:/var/lib/postgresql/data
networks:
- ptitspas_network
# Interface d'administration DB
pgadmin:
image: dpage/pgadmin4
container_name: ptitspas-pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
SCRIPT_NAME: /pgadmin
depends_on:
- database
labels:
- "traefik.enable=true"
- "traefik.http.routers.ptitspas-pgadmin.rule=Host(\"app.ptits-pas.fr\") && PathPrefix(\"/pgadmin\")"
- "traefik.http.routers.ptitspas-pgadmin.entrypoints=websecure"
- "traefik.http.routers.ptitspas-pgadmin.tls.certresolver=leresolver"
- "traefik.http.routers.ptitspas-pgadmin.priority=30"
- "traefik.http.services.ptitspas-pgadmin.loadbalancer.server.port=80"
networks:
- ptitspas_network
- proxy_network
# Backend NestJS
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: ptitspas-backend
restart: unless-stopped
environment:
POSTGRES_HOST: ${POSTGRES_HOST}
POSTGRES_PORT: ${POSTGRES_PORT}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
API_PORT: ${API_PORT}
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET}
JWT_ACCESS_EXPIRES: ${JWT_ACCESS_EXPIRES}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
JWT_REFRESH_EXPIRES: ${JWT_REFRESH_EXPIRES}
NODE_ENV: ${NODE_ENV}
depends_on:
- database
labels:
- "traefik.enable=true"
- "traefik.http.routers.ptitspas-api.rule=Host(\"app.ptits-pas.fr\") && PathPrefix(\"/api\")"
- "traefik.http.routers.ptitspas-api.entrypoints=websecure"
- "traefik.http.routers.ptitspas-api.tls.certresolver=leresolver"
- "traefik.http.routers.ptitspas-api.priority=20"
- "traefik.http.services.ptitspas-api.loadbalancer.server.port=3000"
networks:
- ptitspas_network
- proxy_network
# Frontend Flutter
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: ptitspas-frontend
restart: unless-stopped
environment:
API_URL: ${API_URL}
depends_on:
- backend
labels:
- "traefik.enable=true"
- "traefik.http.routers.ptitspas-front.rule=Host(\"app.ptits-pas.fr\")"
- "traefik.http.routers.ptitspas-front.entrypoints=websecure"
- "traefik.http.routers.ptitspas-front.tls.certresolver=leresolver"
- "traefik.http.routers.ptitspas-front.priority=10"
- "traefik.http.services.ptitspas-front.loadbalancer.server.port=80"
networks:
- ptitspas_network
- proxy_network
volumes:
postgres_data:
networks:
ptitspas_network:
driver: bridge
proxy_network:
external: true

16
frontend/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
# Stage builder
FROM ghcr.io/cirruslabs/flutter:3.19.0 AS builder
WORKDIR /app
COPY pubspec.* ./
RUN flutter pub get
COPY . .
RUN flutter build web --release
# Stage production
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/build/web /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1 +1 @@
flutter.sdk=C:\\Users\\marti\\dev\\flutter
flutter.sdk=/home/deploy/snap/flutter/common/flutter

View File

@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../../models/parent_user_registration_data.dart'; // Import du modèle de données
import '../../../utils/data_generator.dart'; // Import du générateur de données
import '../../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
import '../../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep1Screen extends StatefulWidget {
const ParentRegisterStep1Screen({super.key});
@override
State<ParentRegisterStep1Screen> createState() => _ParentRegisterStep1ScreenState();
}
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData;
// Contrôleurs pour les champs (restauration CP et Ville)
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _addressController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityController = TextEditingController();
@override
void initState() {
super.initState();
_registrationData = UserRegistrationData();
_generateAndFillData();
}
void _generateAndFillData() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
// Utilisation des méthodes publiques de DataGenerator
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
}
@override
void dispose() {
_lastNameController.dispose();
_firstNameController.dispose();
_phoneController.dispose();
_emailController.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
Text(
'Étape 1/6',
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 10),
// Texte d'instruction
Text(
'Informations du Parent Principal',
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: 50, horizontal: 50),
constraints: const BoxConstraints(minHeight: 570),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(CardColorHorizontal.peach.path),
fit: BoxFit.fill,
),
),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(height: 10),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
],
),
const SizedBox(height: 32),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
],
),
const SizedBox(height: 32),
CustomAppTextField(
controller: _addressController,
labelText: 'Adresse (N° et Rue)',
hintText: 'Numéro et nom de votre rue',
style: CustomAppTextFieldStyle.beige,
fieldWidth: double.infinity,
labelFontSize: 22.0,
inputFontSize: 20.0,
),
const SizedBox(height: 32),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
],
),
const SizedBox(height: 10),
],
),
),
),
],
),
),
),
// 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) {
_registrationData.updateParent1(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _addressController.text,
postalCode: _postalCodeController.text,
city: _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: '', // Pas de mot de passe à cette étape
)
);
Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData);
}
},
tooltip: 'Suivant',
),
),
],
),
);
}
}

View File

@ -0,0 +1,244 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:math' as math; // Pour la rotation du chevron
import '../../../models/parent_user_registration_data.dart'; // Import du modèle
import '../../../utils/data_generator.dart'; // Import du générateur
import '../../../widgets/custom_app_text_field.dart'; // Import du widget
import '../../../models/card_assets.dart'; // Import des enums de cartes
class ParentRegisterStep2Screen extends StatefulWidget {
final UserRegistrationData registrationData; // Accepte les données de l'étape 1
const ParentRegisterStep2Screen({super.key, required this.registrationData});
@override
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
}
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
final _formKey = GlobalKey<FormState>();
late UserRegistrationData _registrationData; // Copie locale pour modification
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
// Contrôleurs pour les champs du parent 2
final _lastNameController = TextEditingController();
final _firstNameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _addressController = TextEditingController();
final _postalCodeController = TextEditingController();
final _cityController = TextEditingController();
@override
void initState() {
super.initState();
_registrationData = widget.registrationData; // Récupère les données de l'étape 1
if (_addParent2) {
_generateAndFillParent2Data();
}
}
void _generateAndFillParent2Data() {
final String genFirstName = DataGenerator.firstName();
final String genLastName = DataGenerator.lastName();
_firstNameController.text = genFirstName;
_lastNameController.text = genLastName;
_phoneController.text = DataGenerator.phone();
_emailController.text = DataGenerator.email(genFirstName, genLastName);
_sameAddressAsParent1 = DataGenerator.boolean();
if (!_sameAddressAsParent1) {
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
} else {
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
}
}
@override
void dispose() {
_lastNameController.dispose();
_firstNameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_addressController.dispose();
_postalCodeController.dispose();
_cityController.dispose();
super.dispose();
}
bool get _parent2FieldsEnabled => _addParent2;
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
),
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Étape 2/6', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
const SizedBox(height: 10),
Text(
'Informations du Deuxième Parent (Optionnel)',
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Container(
width: screenSize.width * 0.6,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill),
),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(height: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 12,
child: Row(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)),
const Spacer(),
Switch(value: _addParent2, onChanged: (val) => setState(() {
_addParent2 = val ?? false;
if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields();
}), activeColor: Theme.of(context).primaryColor),
]),
),
Expanded(flex: 1, child: const SizedBox()),
Expanded(
flex: 12,
child: Row(children: [
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey),
const SizedBox(width: 8),
Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)),
const Spacer(),
Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() {
_sameAddressAsParent1 = val ?? false;
if (_sameAddressAsParent1) {
_addressController.text = _registrationData.parent1.address;
_postalCodeController.text = _registrationData.parent1.postalCode;
_cityController.text = _registrationData.parent1.city;
} else {
_addressController.text = DataGenerator.address();
_postalCodeController.text = DataGenerator.postalCode();
_cityController.text = DataGenerator.city();
}
}) : null, activeColor: Theme.of(context).primaryColor),
]),
),
]),
const SizedBox(height: 32),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
],
),
const SizedBox(height: 32),
Row(
children: [
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
Expanded(flex: 1, child: const SizedBox()),
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
],
),
const SizedBox(height: 32),
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0),
const SizedBox(height: 32),
Row(
children: [
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
const SizedBox(width: 20),
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
],
),
const SizedBox(height: 10),
],
),
),
),
),
],
),
),
),
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),
tooltip: 'Retour',
),
),
Positioned(
top: screenSize.height / 2 - 20,
right: 40,
child: IconButton(
icon: Image.asset('assets/images/chevron_right.png', height: 40),
onPressed: () {
if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) {
if (_addParent2) {
_registrationData.updateParent2(
ParentData(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
address: _sameAddressAsParent1 ? _registrationData.parent1.address : _addressController.text,
postalCode: _sameAddressAsParent1 ? _registrationData.parent1.postalCode : _postalCodeController.text,
city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text,
phone: _phoneController.text,
email: _emailController.text,
password: '', // Pas de mot de passe à cette étape
)
);
} else {
_registrationData.updateParent2(null);
}
Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData);
}
},
tooltip: 'Suivant',
),
),
],
),
);
}
void _clearParent2Fields() {
_formKey.currentState?.reset();
_lastNameController.clear();
_firstNameController.clear();
_phoneController.clear();
_emailController.clear();
_addressController.clear();
_postalCodeController.clear();
_cityController.clear();
_sameAddressAsParent1 = false;
setState(() {});
}
}

View File

@ -408,7 +408,7 @@ class _ChildCardWidgetState extends State<_ChildCardWidget> {
),
),
),
const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo
const SizedBox(height: 8.0 * 1.1),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -416,7 +416,7 @@ class _ChildCardWidgetState extends State<_ChildCardWidget> {
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
],
),
const SizedBox(height: 9.0 * 1.1), // 9.9
const SizedBox(height: 6.0 * 1.1)
CustomAppTextField(
controller: _firstNameController,
labelText: 'Prénom',
@ -432,33 +432,28 @@ class _ChildCardWidgetState extends State<_ChildCardWidget> {
enabled: true,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 9.0 * 1.1), // 9.9
const SizedBox(height: 6.0 * 1.1),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Genre', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
Row(
children: [
Radio<String>(
value: 'H',
groupValue: widget.childData.gender,
onChanged: (value) => widget.onGenderChanged(value!),
activeColor: Theme.of(context).primaryColor,
),
Text('H', style: GoogleFonts.merienda(fontSize: 16 * 1.1)),
const SizedBox(width: 20),
Radio<String>(
value: 'F',
groupValue: widget.childData.gender,
onChanged: (value) => widget.onGenderChanged(value!),
activeColor: Theme.of(context).primaryColor,
),
Text('F', style: GoogleFonts.merienda(fontSize: 16 * 1.1)),
Text('Fille', style: GoogleFonts.merienda(fontSize: 16 * 1.1)),
const SizedBox(width: 20),
Radio<String>(
value: 'H',
groupValue: widget.childData.gender,
onChanged: (value) => widget.onGenderChanged(value!),
activeColor: Theme.of(context).primaryColor,
),
Text('Garçon', style: GoogleFonts.merienda(fontSize: 16 * 1.1)),
],
),
],
),
const SizedBox(height: 9.0 * 1.1), // 9.9
const SizedBox(height: 6.0 * 1.1)
CustomAppTextField(
controller: _dobController,
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
@ -468,7 +463,7 @@ class _ChildCardWidgetState extends State<_ChildCardWidget> {
suffixIcon: Icons.calendar_today,
fieldHeight: 55.0 * 1.1, // 60.5
),
const SizedBox(height: 11.0 * 1.1), // 12.1
const SizedBox(height: 8.0 * 1.1),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -476,14 +471,14 @@ class _ChildCardWidgetState extends State<_ChildCardWidget> {
label: 'Consentement photo',
value: widget.childData.photoConsent,
onChanged: widget.onTogglePhotoConsent,
checkboxSize: 22.0 * 1.1, // 24.2
checkboxSize: 22.0 * 1.1,
),
const SizedBox(height: 6.0 * 1.1), // 6.6
const SizedBox(height: 4.0 * 1.1),
AppCustomCheckbox(
label: 'Naissance multiple',
value: widget.childData.multipleBirth,
onChanged: widget.onToggleMultipleBirth,
checkboxSize: 22.0 * 1.1, // 24.2
checkboxSize: 22.0 * 1.1,
),
],
),

13
frontend/nginx.conf Normal file
View File

@ -0,0 +1,13 @@
server {
listen 80;
server_name ynov.ptits-pas.fr;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Gestion des erreurs
error_page 404 /index.html;
}