From 676124567fd812e35b2773e47d997dce89bed0c9 Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Tue, 5 Aug 2025 08:23:05 +0330 Subject: [PATCH] go router added --- lib/core/constants/colors.dart | 1 + lib/core/routes/app_routes.dart | 136 ++++++++++++ lib/core/routes/app_routes.g.dart | 136 ++++++++++++ lib/core/routes/app_shell_screen.dart | 101 +++++++++ lib/core/utils/either.dart | 70 ++++++ lib/core/utils/logger.dart | 205 ++++++++++++++++++ lib/main.dart | 14 +- lib/views/home/home_screen.dart | 115 +++++----- lib/views/home/progress_bar_column.dart | 2 +- lib/views/profile/profile_screen.dart | 25 +++ lib/views/tenders/tenders_screen.dart | 53 +---- .../tenders/widgets/main_tenders_slider.dart | 116 ++++++++++ .../widgets/tender_action_buttons.dart | 17 +- pubspec.yaml | 4 + 14 files changed, 887 insertions(+), 108 deletions(-) create mode 100644 lib/core/routes/app_routes.g.dart create mode 100644 lib/core/routes/app_shell_screen.dart create mode 100644 lib/core/utils/either.dart create mode 100644 lib/core/utils/logger.dart create mode 100644 lib/views/profile/profile_screen.dart create mode 100644 lib/views/tenders/widgets/main_tenders_slider.dart diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart index 068dc61..78d7a52 100644 --- a/lib/core/constants/colors.dart +++ b/lib/core/constants/colors.dart @@ -22,6 +22,7 @@ class AppColors { static const Color warningColor = Color(0xFFFFC107); static const Color grey = Color(0xFF555555); + static const Color grey50 = Color(0xFF9E9E9E); static const Color grey60 = Color(0xFF777777); static const Color grey70 = Color(0xFF444444); static const Color grey80 = Color(0xFF222222); diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index e69de29..0c82310 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../views/home/home_screen.dart'; +import '../../views/login/login_screen.dart'; +import '../../views/profile/profile_screen.dart'; +import '../../views/tenders/tenders_screen.dart'; +import 'app_shell_screen.dart'; + +part 'app_routes.g.dart'; + +// Global navigator keys +final GlobalKey rootNavigatorKey = GlobalKey(); +final GlobalKey homeNavigatorKey = GlobalKey(); +final GlobalKey tendersNavigatorKey = + GlobalKey(); +final GlobalKey profileNavigatorKey = + GlobalKey(); + +// Main app router +final GoRouter appRouter = GoRouter( + routes: $appRoutes, + initialLocation: '/login', + navigatorKey: rootNavigatorKey, +); + +// Login route - outside the shell +@TypedGoRoute(path: '/login') +class LoginScreenRoute extends GoRouteData with _$LoginScreenRoute { + const LoginScreenRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const LoginScreen(); + } +} + +// Shell route with bottom navigation using StatefulShellRoute.indexedStack +@TypedStatefulShellRoute( + branches: >[ + TypedStatefulShellBranch( + routes: >[ + TypedGoRoute(path: '/home'), + ], + ), + TypedStatefulShellBranch( + routes: >[ + TypedGoRoute(path: '/tenders'), + ], + ), + TypedStatefulShellBranch( + routes: >[ + TypedGoRoute(path: '/profile'), + ], + ), + ], +) +class AppShellRouteData extends StatefulShellRouteData { + const AppShellRouteData(); + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Widget builder( + BuildContext context, + GoRouterState state, + StatefulNavigationShell navigationShell, + ) { + return AppShellScreen(navigationShell: navigationShell); + } +} + +// Branch data classes +class HomeBranch extends StatefulShellBranchData { + const HomeBranch(); + static final GlobalKey $navigatorKey = homeNavigatorKey; +} + +class TendersBranch extends StatefulShellBranchData { + const TendersBranch(); + static final GlobalKey $navigatorKey = tendersNavigatorKey; +} + +class ProfileBranch extends StatefulShellBranchData { + const ProfileBranch(); + static final GlobalKey $navigatorKey = profileNavigatorKey; +} + +// Route data classes +class HomeRouteData extends GoRouteData with _$HomeRouteData { + const HomeRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const HomeScreen(); + } + + // @override + // Page buildPage(BuildContext context, GoRouterState state) { + // return CustomTransitionPage( + // key: state.pageKey, + // child: const HomeScreen(), + // transitionDuration: const Duration(milliseconds: 500), + // transitionsBuilder: (context, animation, secondaryAnimation, child) { + // const begin = Offset(1.0, 0.0); // From right to left + // const end = Offset.zero; + // const curve = Curves.linearToEaseOut; + + // var tween = Tween( + // begin: begin, + // end: end, + // ).chain(CurveTween(curve: curve)); + // var offsetAnimation = animation.drive(tween); + // return SlideTransition(position: offsetAnimation, child: child); + // }, + // ); + // } +} + +class TendersRouteData extends GoRouteData with _$TendersRouteData { + const TendersRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const TendersScreen(); + } +} + +class ProfileRouteData extends GoRouteData with _$ProfileRouteData { + const ProfileRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const ProfileScreen(); + } +} diff --git a/lib/core/routes/app_routes.g.dart b/lib/core/routes/app_routes.g.dart new file mode 100644 index 0000000..c89110a --- /dev/null +++ b/lib/core/routes/app_routes.g.dart @@ -0,0 +1,136 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_routes.dart'; + +// ************************************************************************** +// GoRouterGenerator +// ************************************************************************** + +List get $appRoutes => [$loginScreenRoute, $appShellRouteData]; + +RouteBase get $loginScreenRoute => + GoRouteData.$route(path: '/login', factory: _$LoginScreenRoute._fromState); + +mixin _$LoginScreenRoute on GoRouteData { + static LoginScreenRoute _fromState(GoRouterState state) => + const LoginScreenRoute(); + + @override + String get location => GoRouteData.$location('/login'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +RouteBase get $appShellRouteData => StatefulShellRouteData.$route( + parentNavigatorKey: AppShellRouteData.$parentNavigatorKey, + factory: $AppShellRouteDataExtension._fromState, + branches: [ + StatefulShellBranchData.$branch( + navigatorKey: HomeBranch.$navigatorKey, + + routes: [ + GoRouteData.$route(path: '/home', factory: _$HomeRouteData._fromState), + ], + ), + StatefulShellBranchData.$branch( + navigatorKey: TendersBranch.$navigatorKey, + + routes: [ + GoRouteData.$route( + path: '/tenders', + + factory: _$TendersRouteData._fromState, + ), + ], + ), + StatefulShellBranchData.$branch( + navigatorKey: ProfileBranch.$navigatorKey, + + routes: [ + GoRouteData.$route( + path: '/profile', + + factory: _$ProfileRouteData._fromState, + ), + ], + ), + ], +); + +extension $AppShellRouteDataExtension on AppShellRouteData { + static AppShellRouteData _fromState(GoRouterState state) => + const AppShellRouteData(); +} + +mixin _$HomeRouteData on GoRouteData { + static HomeRouteData _fromState(GoRouterState state) => const HomeRouteData(); + + @override + String get location => GoRouteData.$location('/home'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin _$TendersRouteData on GoRouteData { + static TendersRouteData _fromState(GoRouterState state) => + const TendersRouteData(); + + @override + String get location => GoRouteData.$location('/tenders'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin _$ProfileRouteData on GoRouteData { + static ProfileRouteData _fromState(GoRouterState state) => + const ProfileRouteData(); + + @override + String get location => GoRouteData.$location('/profile'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} diff --git a/lib/core/routes/app_shell_screen.dart b/lib/core/routes/app_shell_screen.dart new file mode 100644 index 0000000..1072daa --- /dev/null +++ b/lib/core/routes/app_shell_screen.dart @@ -0,0 +1,101 @@ +// Shell screen with bottom navigation +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; + +import '../utils/size_config.dart'; + +class AppShellScreen extends StatelessWidget { + const AppShellScreen({required this.navigationShell, super.key}); + + final StatefulNavigationShell navigationShell; + + void _gotoBranch(int index) { + navigationShell.goBranch( + index, + initialLocation: index == navigationShell.currentIndex, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: Container( + height: 72.0.h(), + decoration: BoxDecoration( + color: const Color(0xFFE3E3E3), + border: Border.fromBorderSide( + BorderSide(color: const Color(0xFFE3E3E3)), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _bottomNavigationItem( + context: context, + text: 'Home', + isActive: navigationShell.currentIndex == 0, + onTap: () => _gotoBranch(0), + iconPath: 'assets/icons/home.svg', + activeIconPath: 'assets/icons/homeActive.svg', + ), + _bottomNavigationItem( + context: context, + text: 'Tenders', + isActive: navigationShell.currentIndex == 1, + onTap: () => _gotoBranch(1), + iconPath: 'assets/icons/task_square.svg', + activeIconPath: 'assets/icons/task-square_active.svg', + ), + _bottomNavigationItem( + context: context, + text: 'Profile', + isActive: navigationShell.currentIndex == 2, + onTap: () => _gotoBranch(2), + iconPath: 'assets/icons/profile-circle.svg', + activeIconPath: 'assets/icons/profile-circle_active.svg', + ), + ], + ), + ), + ); + } + + Widget _bottomNavigationItem({ + required BuildContext context, + required String text, + required bool isActive, + required VoidCallback onTap, + required String iconPath, + required String activeIconPath, + }) { + return InkWell( + onTap: onTap, + child: SizedBox( + width: 64.0.w(), + height: 72.0.h(), + child: Padding( + padding: EdgeInsets.only(top: 12.0.h(), bottom: 14.0.h()), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset(isActive ? activeIconPath : iconPath), + Text( + text, + style: TextStyle( + fontSize: 12.0.sp(), + fontWeight: FontWeight.w400, + color: + isActive + ? const Color(0xFF0164FF) + : const Color(0xFF777777), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/core/utils/either.dart b/lib/core/utils/either.dart new file mode 100644 index 0000000..88828b8 --- /dev/null +++ b/lib/core/utils/either.dart @@ -0,0 +1,70 @@ +typedef OnError = void Function(E error); + +abstract class Either { + const Either(); + + E? getError(); + + S? getSuccess(); + + bool isError(); + + bool isSuccess(); + + void when({ + required OnError error, + required void Function(S success) success, + }); +} + +class Success extends Either { + final S _success; + + const Success(this._success); + + @override + E? getError() => null; + + @override + S? getSuccess() => _success; + + @override + bool isError() => false; + + @override + bool isSuccess() => true; + + @override + void when({ + required OnError error, + required void Function(S success) success, + }) { + success(_success); + } +} + +class Error extends Either { + final E _error; + + const Error(this._error); + + @override + E? getError() => _error; + + @override + S? getSuccess() => null; + + @override + bool isError() => true; + + @override + bool isSuccess() => false; + + @override + void when({ + required OnError error, + required void Function(S success) success, + }) { + error(_error); + } +} diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart new file mode 100644 index 0000000..fbb4b79 --- /dev/null +++ b/lib/core/utils/logger.dart @@ -0,0 +1,205 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +/// AppLogger is a singleton class that provides centralized logging functionality +/// throughout the application using the logger package. +class AppLogger { + // Private constructor + AppLogger._internal(); + + // Singleton instance + static final AppLogger _instance = AppLogger._internal(); + + // Factory constructor to return the singleton instance + factory AppLogger() => _instance; + + // Logger instance + late final Logger _logger; + + // Initialize the logger with custom configuration + void init({Level? logLevel, bool? stackTraceEnabled}) { + _logger = Logger( + level: logLevel ?? (kDebugMode ? Level.debug : Level.info), + printer: PrettyPrinter( + methodCount: stackTraceEnabled ?? false ? 2 : 0, + errorMethodCount: 8, + lineLength: 120, + colors: true, + printEmojis: true, + dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, + ), + output: _CustomConsoleOutput(), + filter: _CustomLogFilter(), + ); + } + + // Get the logger instance (for advanced usage) + Logger get logger => _logger; + + // Convenient logging methods + void debug(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.d(message, error: error, stackTrace: stackTrace); + } + + void info(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.i(message, error: error, stackTrace: stackTrace); + } + + void warning(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.w(message, error: error, stackTrace: stackTrace); + } + + void error(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.e(message, error: error, stackTrace: stackTrace); + } + + void fatal(dynamic message, {dynamic error, StackTrace? stackTrace}) { + _logger.f(message, error: error, stackTrace: stackTrace); + } + + // Network logging + void logNetwork({ + required String method, + required String url, + int? statusCode, + dynamic requestBody, + dynamic responseBody, + dynamic error, + }) { + final buffer = StringBuffer(); + buffer.writeln('🌐 Network Request:'); + buffer.writeln(' Method: $method'); + buffer.writeln(' URL: $url'); + + if (statusCode != null) { + buffer.writeln(' Status: $statusCode'); + } + + if (requestBody != null) { + buffer.writeln(' Request Body: $requestBody'); + } + + if (responseBody != null) { + buffer.writeln(' Response: $responseBody'); + } + + if (error != null) { + buffer.writeln(' Error: $error'); + } + + if (error != null) { + _logger.e(buffer.toString()); + } else if (statusCode != null && statusCode >= 400) { + _logger.w(buffer.toString()); + } else { + _logger.i(buffer.toString()); + } + } + + // Performance logging + void logPerformance(String operation, Duration duration) { + final milliseconds = duration.inMilliseconds; + final message = '⏱️ Performance: $operation took ${milliseconds}ms'; + + if (milliseconds > 1000) { + _logger.w(message); + } else { + _logger.d(message); + } + } + + // Navigation logging + void logNavigation(String from, String to) { + _logger.i('🧭 Navigation: $from → $to'); + } + + // Custom log with tag + void logWithTag(String tag, dynamic message, {Level? level}) { + final taggedMessage = '[$tag] $message'; + + switch (level ?? Level.info) { + case Level.debug: + debug(taggedMessage); + break; + case Level.info: + info(taggedMessage); + break; + case Level.warning: + warning(taggedMessage); + break; + case Level.error: + error(taggedMessage); + break; + case Level.fatal: + fatal(taggedMessage); + break; + default: + info(taggedMessage); + } + } + + // Close the logger (call this in app disposal if needed) + void close() { + _logger.close(); + } +} + +/// Custom log filter to control which logs are shown +class _CustomLogFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) { + // In debug mode, show all logs + if (kDebugMode) { + return true; + } + + // In release mode, only show warnings and above + return event.level.value >= Level.warning.value; + } +} + +/// Custom console output for better control over log output +class _CustomConsoleOutput extends ConsoleOutput { + @override + void output(OutputEvent event) { + // You can customize how logs are output here + // For example, you could write to a file in addition to console + super.output(event); + + // Example: Write errors to a file (uncomment if needed) + // if (event.level == Level.error || event.level == Level.fatal) { + // _writeToFile(event.lines.join('\n')); + // } + } + + // Example method for writing to file (implement if needed) + // Future _writeToFile(String log) async { + // // Implementation for file writing + // } +} + +// Global logger instance for easy access +final appLogger = AppLogger(); + +// Extension for convenient logging on any object +extension LoggerExtension on Object { + void logDebug([String? message]) { + appLogger.debug(message ?? toString()); + } + + void logInfo([String? message]) { + appLogger.info(message ?? toString()); + } + + void logWarning([String? message]) { + appLogger.warning(message ?? toString()); + } + + void logError([String? message, dynamic error, StackTrace? stackTrace]) { + appLogger.error( + message ?? toString(), + error: error, + stackTrace: stackTrace, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index b4402f8..203717e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,13 +2,21 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'core/routes/app_routes.dart'; +import 'core/utils/logger.dart'; import 'core/utils/size_config.dart'; import 'data/repositories/auth_repository.dart'; import 'data/services/auth_service.dart'; import 'view_models/auth_view_model.dart'; -import 'views/login/login_screen.dart'; void main() { + // Initialize the logger + appLogger.init(); + + // Log app start + appLogger.info('🚀 Starting TM App...'); + // appLogger.logNavigation('Home', 'Login'); + runApp( MultiProvider( providers: [ @@ -34,12 +42,12 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { SizeConfig.init(context); - return MaterialApp( + return MaterialApp.router( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), ), - home: LoginScreen(), + routerConfig: appRouter, ); } } diff --git a/lib/views/home/home_screen.dart b/lib/views/home/home_screen.dart index 3a743c9..80238b1 100644 --- a/lib/views/home/home_screen.dart +++ b/lib/views/home/home_screen.dart @@ -1,77 +1,86 @@ import 'package:flutter/material.dart'; import '../../../core/constants/strings.dart'; -import '../../common/widgets/bottom_navigation.dart'; +import '../../core/constants/colors.dart'; import '../../core/utils/size_config.dart'; import 'widgets.dart'; -class Homescreen extends StatelessWidget { - const Homescreen({super.key}); +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: _body(), - bottomNavigationBar: TenderBottomNavigation(), - ); - } - - SafeArea _body() { return SafeArea( - child: ListView( - children: [ - Padding( - padding: EdgeInsetsDirectional.fromSTEB( - 24.0.w(), - 26.0.h(), - 24.0.w(), - 24.0.h(), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _appbar(), - SizedBox(height: 32.0.h()), - _progressBarsRow(), - SizedBox(height: 32.0.h()), - _firstStatisticsRow(), - SizedBox(height: 8.0.h()), - _secondStatisticsRow(), - SizedBox(height: 32.0.h()), - _yourTenderText(), - SizedBox(height: 28.0.h()), - _bottomListView(), - ], - ), - ), - ], + child: Scaffold( + backgroundColor: Colors.white, + body: _body(context), + appBar: _appbar(), ), ); } - Widget _appbar() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Widget _body(BuildContext context) { + return ListView( children: [ - Text( - AppStrings.home, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xFF222222), + Padding( + padding: EdgeInsetsDirectional.fromSTEB( + 24.0.w(), + 26.0.h(), + 24.0.w(), + 24.0.h(), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // _appbar(), + // SizedBox(height: 32.0.h()), + _progressBarsRow(), + SizedBox(height: 32.0.h()), + _firstStatisticsRow(), + SizedBox(height: 8.0.h()), + _secondStatisticsRow(), + SizedBox(height: 32.0.h()), + _yourTenderText(), + SizedBox(height: 28.0.h()), + _bottomListView(), + ], ), ), - Image.asset( - 'assets/icons/tenderLogo.png', - width: 59.0.w(), - height: 28.0.h(), - fit: BoxFit.cover, - ), ], ); } + PreferredSize _appbar() { + return PreferredSize( + preferredSize: Size.fromHeight(56.0.h()), + child: Container( + color: AppColors.backgroundColor, + padding: EdgeInsets.symmetric(horizontal: 24.0.w()), + height: 56.0.h(), + width: double.infinity, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppStrings.home, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF222222), + ), + ), + Image.asset( + 'assets/icons/tenderLogo.png', + width: 59.0.w(), + height: 28.0.h(), + fit: BoxFit.cover, + ), + ], + ), + ), + ); + } + Widget _progressBarsRow() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/views/home/progress_bar_column.dart b/lib/views/home/progress_bar_column.dart index c1554c3..46265e3 100644 --- a/lib/views/home/progress_bar_column.dart +++ b/lib/views/home/progress_bar_column.dart @@ -30,7 +30,7 @@ class _ProgressBarColumnState extends State // Initialize the animation controller _controller = AnimationController( vsync: this, - duration: Duration(milliseconds: 1500), // duration of the animation + duration: Duration(milliseconds: 2000), // duration of the animation ); // Create a Tween animation from 0.0 to 0.75 diff --git a/lib/views/profile/profile_screen.dart b/lib/views/profile/profile_screen.dart new file mode 100644 index 0000000..dddeccc --- /dev/null +++ b/lib/views/profile/profile_screen.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import '../../core/constants/colors.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.backgroundColor, + appBar: AppBar( + title: const Text('Profile'), + backgroundColor: AppColors.grey10, + elevation: 0, + ), + body: const Center( + child: Text( + 'Profile Screen', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + ), + ); + } +} diff --git a/lib/views/tenders/tenders_screen.dart b/lib/views/tenders/tenders_screen.dart index 0ed81b3..8e4c58e 100644 --- a/lib/views/tenders/tenders_screen.dart +++ b/lib/views/tenders/tenders_screen.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; -import '../../common/widgets/bottom_navigation.dart'; -import '../../core/constants/assets.dart'; import '../../core/constants/colors.dart'; import '../../core/utils/size_config.dart'; -import 'models/tender_model.dart'; +import 'widgets/main_tenders_slider.dart'; import 'widgets/widgets.dart'; class TendersScreen extends StatefulWidget { @@ -39,59 +36,17 @@ class _TendersScreenState extends State Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.backgroundColor, - bottomNavigationBar: TenderBottomNavigation(), body: SafeArea( child: SingleChildScrollView( child: Padding( - padding: EdgeInsets.symmetric( - // horizontal: 24.0.w(), - vertical: 24.0.h(), - ), + padding: EdgeInsets.symmetric(vertical: 24.0.h()), child: Column( children: [ TenderAppBar(), SizedBox(height: 24.0.h()), - - MainTabBar(controller: controller), + // MainTabBar(controller: controller), SizedBox(height: 24.0.h()), - Stack( - children: [ - SizedBox( - width: double.infinity, - height: 762.0.h(), - child: PageView.builder( - scrollDirection: Axis.horizontal, - itemCount: sampleTenders.length, - itemBuilder: (context, index) { - final tender = sampleTenders[index]; - return TenderCard( - date: tender.date, - deadline: tender.deadline, - title: tender.title, - description: tender.description, - tenderId: tender.tenderId, - location: tender.location, - country: tender.country, - matchPercentage: tender.matchPercentage, - ); - }, - ), - ), - Positioned( - top: 237.0.h(), - bottom: 366.0.h(), - left: 9.0.w(), - right: 9.0.w(), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SvgPicture.asset(AssetsManager.arrowCircleLeft), - SvgPicture.asset(AssetsManager.arrowCircleRight), - ], - ), - ), - ], - ), + MainTendersSlider(), ], ), ), diff --git a/lib/views/tenders/widgets/main_tenders_slider.dart b/lib/views/tenders/widgets/main_tenders_slider.dart new file mode 100644 index 0000000..816207d --- /dev/null +++ b/lib/views/tenders/widgets/main_tenders_slider.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../core/constants/assets.dart'; +import '../../../core/utils/size_config.dart'; +import '../models/tender_model.dart'; +import 'tender_card.dart'; + +class MainTendersSlider extends StatefulWidget { + const MainTendersSlider({super.key}); + + @override + State createState() => _MainTendersSliderState(); +} + +class _MainTendersSliderState extends State { + final PageController pageController = PageController(); + + List likedTenders = []; + List dislikedTenders = []; + + void _onDislike(Tender tender) { + if (likedTenders.contains(tender)) { + likedTenders.remove(tender); + } + + if (!dislikedTenders.contains(tender)) { + dislikedTenders.add(tender); + } else { + dislikedTenders.remove(tender); + } + setState(() {}); + } + + void _onLike(Tender tender) { + if (dislikedTenders.contains(tender)) { + dislikedTenders.remove(tender); + } + + if (!likedTenders.contains(tender)) { + likedTenders.add(tender); + } else { + likedTenders.remove(tender); + } + setState(() {}); + } + + void _goToPreviousPage() { + pageController.animateToPage( + pageController.page!.toInt() - 1, + duration: Duration(milliseconds: 300), + curve: Curves.easeInOutCubic, + ); + } + + void _goToNextPage() { + pageController.animateToPage( + pageController.page!.toInt() + 1, + duration: Duration(milliseconds: 300), + curve: Curves.easeInOutCubic, + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + SizedBox( + width: double.infinity, + height: 762.0.h(), + child: PageView.builder( + controller: pageController, + scrollDirection: Axis.horizontal, + itemCount: sampleTenders.length, + itemBuilder: (context, index) { + final tender = sampleTenders[index]; + return TenderCard( + date: tender.date, + deadline: tender.deadline, + title: tender.title, + description: tender.description, + tenderId: tender.tenderId, + location: tender.location, + country: tender.country, + matchPercentage: tender.matchPercentage, + isLiked: likedTenders.contains(tender), + isDisliked: dislikedTenders.contains(tender), + onDislike: () => _onDislike(tender), + onLike: () => _onLike(tender), + ); + }, + ), + ), + Positioned( + top: 237.0.h(), + bottom: 366.0.h(), + left: 9.0.w(), + right: 9.0.w(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: _goToPreviousPage, + child: SvgPicture.asset(AssetsManager.arrowCircleLeft), + ), + GestureDetector( + onTap: _goToNextPage, + child: SvgPicture.asset(AssetsManager.arrowCircleRight), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/views/tenders/widgets/tender_action_buttons.dart b/lib/views/tenders/widgets/tender_action_buttons.dart index 34c2d31..f739dba 100644 --- a/lib/views/tenders/widgets/tender_action_buttons.dart +++ b/lib/views/tenders/widgets/tender_action_buttons.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import '../../../core/constants/assets.dart'; +import '../../../core/constants/colors.dart'; import '../../../core/utils/size_config.dart'; class TenderActionButtons extends StatelessWidget { @@ -30,7 +31,13 @@ class TenderActionButtons extends StatelessWidget { // Dislike button GestureDetector( onTap: onDislike, - child: SvgPicture.asset(AssetsManager.dislike), + child: SvgPicture.asset( + AssetsManager.dislike, + colorFilter: ColorFilter.mode( + isDisliked ? AppColors.errorColor : AppColors.grey50, + BlendMode.srcATop, + ), + ), ), SizedBox(width: 16.0.w()), @@ -44,7 +51,13 @@ class TenderActionButtons extends StatelessWidget { // Like button GestureDetector( onTap: onLike, - child: SvgPicture.asset(AssetsManager.like), + child: SvgPicture.asset( + AssetsManager.like, + colorFilter: ColorFilter.mode( + isLiked ? AppColors.successColor : AppColors.grey50, + BlendMode.srcATop, + ), + ), ), ], ); diff --git a/pubspec.yaml b/pubspec.yaml index 775a829..1936050 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,12 +16,16 @@ dependencies: freezed: ^3.1.0 freezed_annotation: ^3.1.0 flutter_svg: ^2.2.0 + go_router: ^16.0.0 + logger: ^2.6.0 + dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 build_runner: ^2.5.4 + go_router_builder: ^3.0.0 flutter: