feat(login): � Refote visuelle du login - Fond paper2 et image river_logo_desktop positionnée à 1/4 de la largeur restante - ✨ Séparation desktop/mobile
This commit is contained in:
parent
9519fafe3a
commit
f4c211e0dd
@ -1,155 +1,139 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../services/auth_service.dart';
|
||||
import '../../theme/theme_provider.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
String _getThemeName(ThemeType type) {
|
||||
switch (type) {
|
||||
case ThemeType.defaultTheme:
|
||||
return "P'titsPas";
|
||||
case ThemeType.pastelTheme:
|
||||
return "Pastel";
|
||||
case ThemeType.darkTheme:
|
||||
return "Sombre";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await AuthService.login(
|
||||
_emailController.text,
|
||||
_passwordController.text,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
context.go('/home');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur lors de la connexion: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class LoginPage extends StatelessWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connexion'),
|
||||
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);
|
||||
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) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
final imageDimensions = snapshot.data!;
|
||||
final imageHeight = h;
|
||||
final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height);
|
||||
final remainingWidth = w - imageWidth;
|
||||
final leftMargin = remainingWidth / 4;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
// Fond en papier
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer votre email';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Veuillez entrer un email valide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
border: OutlineInputBorder(),
|
||||
// Image principale
|
||||
Positioned(
|
||||
left: leftMargin,
|
||||
top: 0,
|
||||
height: imageHeight,
|
||||
width: imageWidth,
|
||||
child: Image.asset(
|
||||
'assets/images/river_logo_desktop.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer votre mot de passe';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _login,
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const Text('Se connecter'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => context.go('/parent-register'),
|
||||
child: const Text('Créer un compte parent'),
|
||||
// Contenu
|
||||
const Center(
|
||||
child: Text('Formulaire de connexion à implémenter'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Version mobile (à implémenter)
|
||||
return const Center(
|
||||
child: Text('Version mobile à implémenter'),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ImageDimensions> _getImageDimensions() async {
|
||||
final image = Image.asset('assets/images/river_logo_desktop.png');
|
||||
final completer = Completer<ImageDimensions>();
|
||||
image.image.resolve(const ImageConfiguration()).addListener(
|
||||
ImageStreamListener((info, _) {
|
||||
completer.complete(ImageDimensions(
|
||||
width: info.image.width.toDouble(),
|
||||
height: info.image.height.toDouble(),
|
||||
));
|
||||
}),
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
class ImageDimensions {
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
ImageDimensions({required this.width, required this.height});
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
// Champ texte avec fond image
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
class _ImageTextField extends StatelessWidget {
|
||||
final String bg;
|
||||
final double width;
|
||||
final double height;
|
||||
final String hint;
|
||||
final bool obscure;
|
||||
const _ImageTextField({
|
||||
required this.bg,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.hint,
|
||||
this.obscure = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: Image.asset(bg, fit: BoxFit.fill)),
|
||||
TextField(
|
||||
obscureText: obscure,
|
||||
style: GoogleFonts.merienda(fontSize: width * 0.045),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: hint,
|
||||
hintStyle: GoogleFonts.merienda(
|
||||
fontSize: width * 0.045,
|
||||
color: Colors.black54,
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.07, // 7 % latéral
|
||||
vertical: height * 0.22, // 22 % vertical
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,43 +1,36 @@
|
||||
name: petitspas
|
||||
name: p_tits_pas
|
||||
description: Application de gestion de la garde d'enfants pour les collectivités locales.
|
||||
publish_to: 'none'
|
||||
version: 1.0.0+1
|
||||
version: 0.1.0
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
sdk: '>=3.2.6 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.2
|
||||
# Gestion d'état
|
||||
provider: ^6.1.4
|
||||
# Navigation
|
||||
go_router: ^13.0.0
|
||||
# API
|
||||
dio: ^5.0.0
|
||||
# Local storage
|
||||
shared_preferences: ^2.2.0
|
||||
# UI
|
||||
flutter_svg: ^2.0.0
|
||||
google_fonts: ^6.2.1
|
||||
# Formulaires
|
||||
form_validator: ^2.1.1
|
||||
# Dates
|
||||
intl: ^0.18.0
|
||||
# Images
|
||||
image_picker: ^1.0.0
|
||||
# PDF
|
||||
pdf: ^3.10.0
|
||||
printing: ^5.11.0
|
||||
provider: ^6.1.1
|
||||
go_router: ^13.2.5
|
||||
google_fonts: ^6.1.0
|
||||
shared_preferences: ^2.2.2
|
||||
image_picker: ^1.0.7
|
||||
js: ^0.6.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
build_runner: ^2.4.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
assets:
|
||||
- assets/images/
|
||||
- assets/images/logo.png
|
||||
- assets/images/river_logo_desktop.png
|
||||
- assets/images/paper2.png
|
||||
|
||||
fonts:
|
||||
- family: Merienda
|
||||
fonts:
|
||||
- asset: assets/fonts/Merienda-VariableFont_wght.ttf
|
||||
style: normal
|
||||
Loading…
x
Reference in New Issue
Block a user