diff --git a/README.md b/README.md index bb9eabf..5f97a74 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,4 @@ -# afterwork +# AfterWork Project -A new Flutter project. -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +This project is structured according to best practices in Flutter development. diff --git a/config/router.dart b/config/router.dart new file mode 100644 index 0000000..e69de29 diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/assets/images/background.webp b/lib/assets/images/background.webp new file mode 100644 index 0000000..7bd2a94 Binary files /dev/null and b/lib/assets/images/background.webp differ diff --git a/lib/assets/images/logo.png b/lib/assets/images/logo.png new file mode 100644 index 0000000..847469e Binary files /dev/null and b/lib/assets/images/logo.png differ diff --git a/lib/config/injection/injection.dart b/lib/config/injection/injection.dart new file mode 100644 index 0000000..5197f95 --- /dev/null +++ b/lib/config/injection/injection.dart @@ -0,0 +1,22 @@ +import 'package:get_it/get_it.dart'; +import 'package:http/http.dart' as http; + +import '../../data/datasources/user_remote_data_source.dart'; +import '../../data/repositories/user_repository_impl.dart'; +import '../../domain/usecases/get_user.dart'; + +final sl = GetIt.instance; + +void init() { + // Register Http Client + sl.registerLazySingleton(() => http.Client()); + + // Register Data Sources + sl.registerLazySingleton(() => UserRemoteDataSource(sl())); + + // Register Repositories + sl.registerLazySingleton(() => UserRepositoryImpl(remoteDataSource: sl())); + + // Register Use Cases + sl.registerLazySingleton(() => GetUser(sl())); +} diff --git a/lib/config/router.dart b/lib/config/router.dart new file mode 100644 index 0000000..f9ed4b7 --- /dev/null +++ b/lib/config/router.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:afterwork/presentation/screens/login/login_screen.dart'; +import 'package:afterwork/presentation/screens/home/home_screen.dart'; +import 'package:afterwork/presentation/screens/event/event_screen.dart'; +import 'package:afterwork/presentation/screens/story/story_screen.dart'; + +class AppRouter { + static Route generateRoute(RouteSettings settings) { + switch (settings.name) { + case '/': + return MaterialPageRoute(builder: (_) => const LoginScreen()); + case '/home': + return MaterialPageRoute(builder: (_) => const HomeScreen()); + case '/event': + return MaterialPageRoute(builder: (_) => const EventScreen()); + case '/story': + return MaterialPageRoute(builder: (_) => const StoryScreen()); + default: + return MaterialPageRoute( + builder: (_) => const Scaffold( + body: Center(child: Text('Page non trouvée')), + ), + ); + } + } +} diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/constants/urls.dart b/lib/core/constants/urls.dart new file mode 100644 index 0000000..1f807fd --- /dev/null +++ b/lib/core/constants/urls.dart @@ -0,0 +1,6 @@ +class Urls { + static const String baseUrl = 'http://192.168.1.145:8085'; + // static const String login = baseUrl + 'auth/login'; + // static const String events = baseUrl + 'events'; +// Ajoute d'autres URLs ici +} diff --git a/lib/core/errors/exceptions.dart b/lib/core/errors/exceptions.dart new file mode 100644 index 0000000..3d1ab7a --- /dev/null +++ b/lib/core/errors/exceptions.dart @@ -0,0 +1,12 @@ +class ServerException implements Exception {} + +class CacheException implements Exception {} + +class AuthenticationException implements Exception { + final String message; + + AuthenticationException(this.message); + + @override + String toString() => 'AuthenticationException: $message'; +} diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart new file mode 100644 index 0000000..80edf12 --- /dev/null +++ b/lib/core/errors/failures.dart @@ -0,0 +1,9 @@ +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + @override + List get props => []; +} + +class ServerFailure extends Failure {} +class CacheFailure extends Failure {} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..6a9666f --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static final ThemeData lightTheme = ThemeData( + primaryColor: Colors.blue, + colorScheme: const ColorScheme.light( + secondary: Colors.orange, + ), + brightness: Brightness.light, + buttonTheme: const ButtonThemeData(buttonColor: Colors.blue), + ); + + static final ThemeData darkTheme = ThemeData( + primaryColor: Colors.black, + colorScheme: const ColorScheme.dark( + secondary: Colors.red, + ), + brightness: Brightness.dark, + ); +} diff --git a/lib/core/utils/input_converter.dart b/lib/core/utils/input_converter.dart new file mode 100644 index 0000000..1a4a72a --- /dev/null +++ b/lib/core/utils/input_converter.dart @@ -0,0 +1,16 @@ +import 'package:dartz/dartz.dart'; +import 'package:afterwork/core/errors/failures.dart'; + +class InputConverter { + Either stringToUnsignedInteger(String str) { + try { + final integer = int.parse(str); + if (integer < 0) throw const FormatException(); + return Right(integer); + } catch (e) { + return Left(InvalidInputFailure()); + } + } +} + +class InvalidInputFailure extends Failure {} diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart new file mode 100644 index 0000000..a229fbf --- /dev/null +++ b/lib/core/utils/validators.dart @@ -0,0 +1,21 @@ +class Validators { + static String? validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre email'; + } + if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) { + return 'Veuillez entrer un email valide'; + } + return null; + } + + static String? validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre mot de passe'; + } + if (value.length < 6) { + return 'Le mot de passe doit comporter au moins 6 caractères'; + } + return null; + } +} diff --git a/lib/data/datasources/user_remote_data_source.dart b/lib/data/datasources/user_remote_data_source.dart new file mode 100644 index 0000000..447b5df --- /dev/null +++ b/lib/data/datasources/user_remote_data_source.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; +import 'package:afterwork/data/models/user_model.dart'; +import 'package:afterwork/core/constants/urls.dart'; +import 'package:http/http.dart' as http; + +import '../../core/errors/exceptions.dart'; + +class UserRemoteDataSource { + final http.Client client; + + UserRemoteDataSource(this.client); + + Future authenticateUser(String email, String password) async { + if (email.isEmpty || password.isEmpty) { + throw Exception('Email ou mot de passe vide'); + } + + final response = await client.post( + Uri.parse('${Urls.baseUrl}/users/authenticate'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'email': email, + 'motDePasse': password, + }), + ); + + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body); + return UserModel.fromJson(jsonResponse); + } else if (response.statusCode == 401) { + throw AuthenticationException('Email ou mot de passe incorrect'); + } else { + throw ServerException(); + } + } + + Future getUser(String id) async { + final response = await client.get(Uri.parse('${Urls.baseUrl}/user/$id')); + + if (response.statusCode == 200) { + return UserModel.fromJson(json.decode(response.body)); + } else { + throw ServerException(); + } + } +} diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart new file mode 100644 index 0000000..a987f32 --- /dev/null +++ b/lib/data/models/user_model.dart @@ -0,0 +1,37 @@ +import 'package:afterwork/domain/entities/user.dart'; + +class UserModel extends User { + const UserModel({ + required String id, + required String nom, + required String prenoms, + required String email, + required String motDePasse, + }) : super( + id: id, + nom: nom, + prenoms: prenoms, + email: email, + motDePasse: motDePasse, + ); + + factory UserModel.fromJson(Map json) { + return UserModel( + id: json['id'] ?? '', + nom: json['nom'] ?? '', + prenoms: json['prenoms'] ?? '', + email: json['email'] ?? '', + motDePasse: json['motDePasse'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'nom': nom, + 'prenoms': prenoms, + 'email': email, + 'motDePasse': motDePasse, + }; + } +} diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart new file mode 100644 index 0000000..020e840 --- /dev/null +++ b/lib/data/repositories/user_repository_impl.dart @@ -0,0 +1,21 @@ +import 'package:afterwork/domain/entities/user.dart'; +import 'package:afterwork/domain/repositories/user_repository.dart'; +import 'package:afterwork/data/datasources/user_remote_data_source.dart'; +import 'package:afterwork/data/models/user_model.dart'; + +class UserRepositoryImpl implements UserRepository { + final UserRemoteDataSource remoteDataSource; + + UserRepositoryImpl({required this.remoteDataSource}); + + @override + Future getUser(String id) async { + UserModel userModel = await remoteDataSource.getUser(id); + return userModel; // Retourne un UserModel qui est un sous-type de User + } + + Future authenticateUser(String email, String password) async { + UserModel userModel = await remoteDataSource.authenticateUser(email, password); + return userModel; // Retourne un UserModel qui est un sous-type de User + } +} diff --git a/lib/domain/entities/user.dart b/lib/domain/entities/user.dart new file mode 100644 index 0000000..5a25f95 --- /dev/null +++ b/lib/domain/entities/user.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; + +class User extends Equatable { + final String id; + final String nom; + final String prenoms; + final String email; + final String motDePasse; + + const User({ + required this.id, + required this.nom, + required this.prenoms, + required this.email, + required this.motDePasse, + }); + + @override + List get props => [id, nom, prenoms, email, motDePasse]; +} diff --git a/lib/domain/repositories/user_repository.dart b/lib/domain/repositories/user_repository.dart new file mode 100644 index 0000000..4491667 --- /dev/null +++ b/lib/domain/repositories/user_repository.dart @@ -0,0 +1,5 @@ +import 'package:afterwork/domain/entities/user.dart'; + +abstract class UserRepository { + Future getUser(String id); +} diff --git a/lib/domain/usecases/get_user.dart b/lib/domain/usecases/get_user.dart new file mode 100644 index 0000000..c9f8077 --- /dev/null +++ b/lib/domain/usecases/get_user.dart @@ -0,0 +1,19 @@ +import 'package:dartz/dartz.dart'; +import 'package:afterwork/domain/entities/user.dart'; +import 'package:afterwork/domain/repositories/user_repository.dart'; +import 'package:afterwork/core/errors/failures.dart'; + +class GetUser { + final UserRepository repository; + + GetUser(this.repository); + + Future> call(String id) async { + try { + final user = await repository.getUser(id); + return Right(user); + } catch (e) { + return Left(ServerFailure()); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 8e94089..b0b7c1b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,125 +1,23 @@ import 'package:flutter/material.dart'; +import 'config/router.dart'; +import 'core/theme/app_theme.dart'; void main() { - runApp(const MyApp()); + runApp(const AfterWorkApp()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class AfterWorkApp extends StatelessWidget { + const AfterWorkApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + title: 'AfterWork', + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, // Ajout du thème sombre + themeMode: ThemeMode.system, // Choix automatique du thème en fonction du système + onGenerateRoute: AppRouter.generateRoute, + initialRoute: '/', ); } } diff --git a/lib/presentation/screens/event/event_screen.dart b/lib/presentation/screens/event/event_screen.dart new file mode 100644 index 0000000..f2f695c --- /dev/null +++ b/lib/presentation/screens/event/event_screen.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +class EventScreen extends StatelessWidget { + const EventScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Créer un Événement'), + backgroundColor: Colors.blueAccent, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Titre de l\'événement', + style: TextStyle(fontSize: 18, color: Colors.white), + ), + const SizedBox(height: 8), + TextField( + decoration: InputDecoration( + hintText: 'Entrez le titre de l\'événement', + filled: true, + fillColor: Colors.white.withOpacity(0.1), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide.none, + ), + hintStyle: const TextStyle(color: Colors.white70), + ), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + // Logique pour créer l'événement + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + padding: const EdgeInsets.all(16.0), + ), + child: const Text('Créer l\'événement'), + ), + ], + ), + ), + backgroundColor: Colors.black, + ); + } +} diff --git a/lib/presentation/screens/home/home_screen.dart b/lib/presentation/screens/home/home_screen.dart new file mode 100644 index 0000000..2762241 --- /dev/null +++ b/lib/presentation/screens/home/home_screen.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + _HomeScreenState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 5, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + void _onMenuSelected(BuildContext context, String option) { + // Implémente la logique pour chaque option ici + switch (option) { + case 'Publier': + // Redirige vers la page de publication + break; + case 'Story': + // Redirige vers la page de création de Story + break; + default: + break; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, // Fond noir pour l'AppBar + elevation: 0, // Enlève l'ombre sous l'AppBar + leading: Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset( + 'lib/assets/images/logo.png', // Chemin correct de ton logo + height: 40, + ), + ), + actions: [ + // Bouton + + CircleAvatar( + backgroundColor: Colors.white, // Cercle blanc + radius: 18, // Réduit la taille du bouton + child: PopupMenuButton( + onSelected: (value) => _onMenuSelected(context, value), + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'Publier', + child: Text('Publier'), + ), + const PopupMenuItem( + value: 'Story', + child: Text('Story'), + ), + ], + icon: const Icon(Icons.add, color: Colors.blueAccent, size: 20), // Icône bleue légèrement plus petite + color: Colors.white, // Menu contextuel en blanc + ), + ), + const SizedBox(width: 8), // Réduit l'espacement entre les boutons + // Bouton Recherche + CircleAvatar( + backgroundColor: Colors.white, + radius: 18, // Réduit la taille du bouton + child: IconButton( + icon: const Icon(Icons.search, color: Colors.blueAccent, size: 20), // Icône bleue légèrement plus petite + onPressed: () { + // Implémente la logique de recherche ici + }, + ), + ), + const SizedBox(width: 8), // Réduit l'espacement entre les boutons + // Bouton Messagerie + CircleAvatar( + backgroundColor: Colors.white, + radius: 18, // Réduit la taille du bouton + child: IconButton( + icon: const Icon(Icons.message, color: Colors.blueAccent, size: 20), // Icône bleue légèrement plus petite + onPressed: () { + // Implémente la logique de messagerie ici + }, + ), + ), + const SizedBox(width: 8), // Réduit l'espacement entre les boutons + ], + bottom: TabBar( + controller: _tabController, + indicatorColor: Colors.blueAccent, + tabs: const [ + Tab(icon: Icon(Icons.home), text: 'Accueil'), + Tab(icon: Icon(Icons.event), text: 'Événements'), + Tab(icon: Icon(Icons.location_city), text: 'Établissements'), + Tab(icon: Icon(Icons.people), text: 'Social'), + Tab(icon: Icon(Icons.person), text: 'Profil'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _getSelectedScreen(0), + _getSelectedScreen(1), + _getSelectedScreen(2), + _getSelectedScreen(3), + _getSelectedScreen(4), + ], + ), + backgroundColor: Colors.black, // Arrière-plan de l'écran en noir + ); + } + + // Cette méthode retourne le widget correspondant à l'index sélectionné + Widget _getSelectedScreen(int index) { + switch (index) { + case 0: + return const Center( + child: Text( + 'Accueil', + style: TextStyle(color: Colors.white, fontSize: 24), + ), + ); + case 1: + return const Center( + child: Text( + 'Événements', + style: TextStyle(color: Colors.white, fontSize: 24), + ), + ); + case 2: + return const Center( + child: Text( + 'Établissements', + style: TextStyle(color: Colors.white, fontSize: 24), + ), + ); + case 3: + return const Center( + child: Text( + 'Social', + style: TextStyle(color: Colors.white, fontSize: 24), + ), + ); + case 4: + return const Center( + child: Text( + 'Profil', + style: TextStyle(color: Colors.white, fontSize: 24), + ), + ); + default: + return const Center( + child: Text( + 'Accueil', + style: TextStyle(color: Colors.white, fontSize: 24), + ), + ); + } + } +} diff --git a/lib/presentation/screens/login/login_screen.dart b/lib/presentation/screens/login/login_screen.dart new file mode 100644 index 0000000..984daa8 --- /dev/null +++ b/lib/presentation/screens/login/login_screen.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; +import 'package:afterwork/data/datasources/user_remote_data_source.dart'; +import 'package:afterwork/data/models/user_model.dart'; +import 'package:afterwork/presentation/screens/home/home_screen.dart'; +import 'package:http/http.dart' as http; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + _LoginScreenState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State with SingleTickerProviderStateMixin { + final _formKey = GlobalKey(); + String _email = ''; + String _password = ''; + bool _isPasswordVisible = false; + bool _isSubmitting = false; + + final UserRemoteDataSource _userRemoteDataSource = UserRemoteDataSource(http.Client()); + + late AnimationController _controller; + late Animation _buttonScaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _buttonScaleAnimation = Tween(begin: 1.0, end: 1.1).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _togglePasswordVisibility() { + setState(() { + _isPasswordVisible = !_isPasswordVisible; + }); + } + + void _submit() async { + if (_formKey.currentState!.validate()) { + setState(() { + _isSubmitting = true; + }); + _formKey.currentState!.save(); + + try { + UserModel user = await _userRemoteDataSource.authenticateUser(_email, _password); + print('Connexion réussie : ${user.email}'); + + // Navigation vers la page d'accueil + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const HomeScreen()), + ); + } catch (e) { + print('Erreur lors de la connexion: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } finally { + setState(() { + _isSubmitting = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + // Arrière-plan avec dégradé + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF4A90E2), Color(0xFF9013FE)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + // Contenu de la page + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Logo avec légère animation + AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: _buttonScaleAnimation.value, + child: child, + ); + }, + child: GestureDetector( + onTapDown: (_) => _controller.forward(), + onTapUp: (_) => _controller.reverse(), + child: Image.asset( + 'lib/assets/images/logo.png', + height: size.height * 0.2, + ), + ), + ), + const SizedBox(height: 20), + const Text( + 'Bienvenue sur AfterWork', + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: Colors.white, + shadows: [ + Shadow( + offset: Offset(0, 2), + blurRadius: 6, + color: Colors.black26, + ), + ], + ), + ), + const SizedBox(height: 40), + // Champ Email + TextFormField( + decoration: InputDecoration( + labelText: 'Email', + filled: true, + fillColor: Colors.white.withOpacity(0.1), + labelStyle: const TextStyle(color: Colors.white), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide.none, + ), + prefixIcon: const Icon(Icons.email, color: Colors.white), + ), + keyboardType: TextInputType.emailAddress, + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre email'; + } + if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) { + return 'Veuillez entrer un email valide'; + } + return null; + }, + onSaved: (value) { + _email = value ?? ''; // Utiliser une chaîne vide si value est null + }, + ), + const SizedBox(height: 20), + // Champ Mot de passe + TextFormField( + decoration: InputDecoration( + labelText: 'Mot de passe', + filled: true, + fillColor: Colors.white.withOpacity(0.1), + labelStyle: const TextStyle(color: Colors.white), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide.none, + ), + prefixIcon: const Icon(Icons.lock, color: Colors.white), + suffixIcon: IconButton( + icon: Icon( + _isPasswordVisible ? Icons.visibility : Icons.visibility_off, + color: Colors.white, + ), + onPressed: _togglePasswordVisibility, + ), + ), + obscureText: !_isPasswordVisible, + style: const TextStyle(color: Colors.white), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre mot de passe'; + } + if (value.length < 6) { + return 'Le mot de passe doit comporter au moins 6 caractères'; + } + return null; + }, + onSaved: (value) { + _password = value ?? ''; // Utiliser une chaîne vide si value est null + }, + ), + const SizedBox(height: 20), + // Bouton de connexion avec animation de soumission + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16.0), + textStyle: const TextStyle(fontSize: 18), + backgroundColor: _isSubmitting ? Colors.grey : Colors.blueAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + onPressed: _isSubmitting ? null : _submit, + child: _isSubmitting + ? const CircularProgressIndicator(color: Colors.white) + : const Text('Connexion'), + ), + ), + const SizedBox(height: 20), + // Lien pour s'inscrire + TextButton( + onPressed: () { + // Naviguer vers la page d'inscription + }, + child: const Text( + 'Pas encore de compte ? Inscrivez-vous', + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/screens/story/story_screen.dart b/lib/presentation/screens/story/story_screen.dart new file mode 100644 index 0000000..a8509de --- /dev/null +++ b/lib/presentation/screens/story/story_screen.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class StoryScreen extends StatelessWidget { + const StoryScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Créer une Story'), + backgroundColor: Colors.blueAccent, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Texte de la Story', + style: TextStyle(fontSize: 18, color: Colors.white), + ), + const SizedBox(height: 8), + TextField( + maxLines: 5, + decoration: InputDecoration( + hintText: 'Rédigez votre Story', + filled: true, + fillColor: Colors.white.withOpacity(0.1), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide.none, + ), + hintStyle: const TextStyle(color: Colors.white70), + ), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + // Logique pour créer la story + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + padding: const EdgeInsets.all(16.0), + ), + child: const Text('Publier la Story'), + ), + ], + ), + ), + backgroundColor: Colors.black, + ); + } +} diff --git a/lib/presentation/state_management/event_bloc.dart b/lib/presentation/state_management/event_bloc.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/presentation/state_management/payment_bloc.dart b/lib/presentation/state_management/payment_bloc.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/presentation/state_management/reservation_bloc.dart b/lib/presentation/state_management/reservation_bloc.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/presentation/state_management/user_bloc.dart b/lib/presentation/state_management/user_bloc.dart new file mode 100644 index 0000000..b1cc3ab --- /dev/null +++ b/lib/presentation/state_management/user_bloc.dart @@ -0,0 +1,43 @@ +import 'package:afterwork/domain/entities/user.dart'; +import 'package:afterwork/domain/usecases/get_user.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class UserBloc extends Bloc { + final GetUser getUser; + + UserBloc({required this.getUser}) : super(UserInitial()); + + Stream mapEventToState(UserEvent event) async* { + if (event is GetUserById) { + yield UserLoading(); + final either = await getUser(event.id); + + yield either.fold( + (failure) => UserError(), + (user) => UserLoaded(user: user), + ); + } + } +} + +abstract class UserEvent {} + +class GetUserById extends UserEvent { + final String id; + + GetUserById(this.id); +} + +abstract class UserState {} + +class UserInitial extends UserState {} + +class UserLoading extends UserState {} + +class UserLoaded extends UserState { + final User user; + + UserLoaded({required this.user}); +} + +class UserError extends UserState {} diff --git a/lib/presentation/widgets/custom_appbar.dart b/lib/presentation/widgets/custom_appbar.dart new file mode 100644 index 0000000..635b287 --- /dev/null +++ b/lib/presentation/widgets/custom_appbar.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final List actions; + + const CustomAppBar({ + Key? key, + required this.title, + this.actions = const [], + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.black, + title: Text( + title, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + actions: actions, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(56.0); +} diff --git a/lib/presentation/widgets/custom_button.dart b/lib/presentation/widgets/custom_button.dart new file mode 100644 index 0000000..08d499f --- /dev/null +++ b/lib/presentation/widgets/custom_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class CustomButton extends StatelessWidget { + final String text; + final VoidCallback onPressed; + + const CustomButton({ + Key? key, + required this.text, + required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16.0), + textStyle: const TextStyle(fontSize: 18), + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: Text(text), + ); + } +} diff --git a/lib/presentation/widgets/custom_drawer.dart b/lib/presentation/widgets/custom_drawer.dart new file mode 100644 index 0000000..774bb63 --- /dev/null +++ b/lib/presentation/widgets/custom_drawer.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class CustomDrawer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: Colors.blueAccent, + ), + child: Text( + 'AfterWork', + style: TextStyle( + color: Colors.white, + fontSize: 24, + ), + ), + ), + ListTile( + leading: Icon(Icons.home), + title: Text('Accueil'), + onTap: () { + Navigator.pushNamed(context, '/home'); + }, + ), + ListTile( + leading: Icon(Icons.event), + title: Text('Événements'), + onTap: () { + Navigator.pushNamed(context, '/event'); + }, + ), + ListTile( + leading: Icon(Icons.camera_alt), // Icône mise à jour pour la story + title: Text('Story'), + onTap: () { + Navigator.pushNamed(context, '/story'); + }, + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 9e51fc8..781db0c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -49,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -62,6 +86,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: @@ -75,6 +107,30 @@ packages: description: flutter source: sdk version: "0.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" + http: + dependency: "direct main" + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" leak_tracker: dependency: transitive description: @@ -131,6 +187,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -139,6 +203,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" sky_engine: dependency: transitive description: flutter @@ -192,6 +264,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 19c8718..8376ad8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,79 +1,55 @@ name: afterwork description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: ^3.5.1 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + # State management with flutter_bloc + flutter_bloc: ^8.0.1 + + # HTTP client for API requests + http: ^0.13.3 + + # Dependency injection with get_it + get_it: ^7.2.0 + + # Equatable for easier comparison of objects + equatable: ^2.0.3 + + # Functional programming utilities + dartz: ^0.10.1 + dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^4.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true + assets: + - lib/assets/images/logo.png + - lib/assets/images/background.webp + # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: + # list giving the asset and other descriptors for the font. For example: # fonts: # - family: Schyler # fonts: diff --git a/test/widget_test.dart b/test/widget_test.dart index 3745161..e69de29 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:afterwork/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}