From df02cff66866dfd45f441db35a41f3e70761f305 Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Sun, 3 Aug 2025 19:11:51 +0330 Subject: [PATCH] Refactor: rename BottomNavigation and update TendersScreen structure - Renamed BottomNavigation to TenderBottomNavigation for clarity. - Updated TendersScreen to use TenderBottomNavigation. - Converted TendersScreen from StatelessWidget to StatefulWidget to manage tab state. - Added TabController for handling tab changes and improved layout with SafeArea and PageView. --- assets/icons/close-circle.svg | 5 + assets/icons/dislike.svg | 4 + assets/icons/like.svg | 4 + lib/common/widgets/bottom_navigation.dart | 8 +- lib/core/constants/assets.dart | 5 + lib/core/constants/colors.dart | 8 + lib/views/home/home_screen.dart | 2 +- lib/views/tenders/models/tender_model.dart | 94 ++++++ lib/views/tenders/tenders_screen.dart | 86 +++-- .../tenders/widgets/last_tenders_tab.dart | 10 + lib/views/tenders/widgets/main_tab_bar.dart | 62 ++++ lib/views/tenders/widgets/tab_item.dart | 39 +++ lib/views/tenders/widgets/tender_app_bar.dart | 33 ++ lib/views/tenders/widgets/tender_card.dart | 311 ++++++++++++++++++ lib/views/tenders/widgets/widgets.dart | 4 + 15 files changed, 648 insertions(+), 27 deletions(-) create mode 100644 assets/icons/close-circle.svg create mode 100644 assets/icons/dislike.svg create mode 100644 assets/icons/like.svg create mode 100644 lib/views/tenders/models/tender_model.dart create mode 100644 lib/views/tenders/widgets/last_tenders_tab.dart create mode 100644 lib/views/tenders/widgets/main_tab_bar.dart create mode 100644 lib/views/tenders/widgets/tab_item.dart create mode 100644 lib/views/tenders/widgets/tender_app_bar.dart create mode 100644 lib/views/tenders/widgets/tender_card.dart create mode 100644 lib/views/tenders/widgets/widgets.dart diff --git a/assets/icons/close-circle.svg b/assets/icons/close-circle.svg new file mode 100644 index 0000000..06ef87a --- /dev/null +++ b/assets/icons/close-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/dislike.svg b/assets/icons/dislike.svg new file mode 100644 index 0000000..0f98436 --- /dev/null +++ b/assets/icons/dislike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/like.svg b/assets/icons/like.svg new file mode 100644 index 0000000..5b3fd65 --- /dev/null +++ b/assets/icons/like.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/common/widgets/bottom_navigation.dart b/lib/common/widgets/bottom_navigation.dart index 970137b..d87568b 100644 --- a/lib/common/widgets/bottom_navigation.dart +++ b/lib/common/widgets/bottom_navigation.dart @@ -3,14 +3,14 @@ import 'package:flutter_svg/svg.dart'; import '../../core/utils/size_config.dart'; -class BottomNavigation extends StatefulWidget { - const BottomNavigation({super.key}); +class TenderBottomNavigation extends StatefulWidget { + const TenderBottomNavigation({super.key}); @override - State createState() => _BottomNavigationState(); + State createState() => _TenderBottomNavigationState(); } -class _BottomNavigationState extends State { +class _TenderBottomNavigationState extends State { int activeTab = 0; @override diff --git a/lib/core/constants/assets.dart b/lib/core/constants/assets.dart index ef6a7c8..28debfe 100644 --- a/lib/core/constants/assets.dart +++ b/lib/core/constants/assets.dart @@ -10,4 +10,9 @@ class AssetsManager { // tenders page static const tenderLogo = 'assets/icons/tenderLogo.png'; + static const like = 'assets/icons/like.svg'; + static const dislike = 'assets/icons/dislike.svg'; + static const closeCircle = 'assets/icons/close-circle.svg'; + static const seFlag = 'assets/icons/SE.png'; + static const location = 'assets/icons/location.svg'; } diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart index aaf487f..068dc61 100644 --- a/lib/core/constants/colors.dart +++ b/lib/core/constants/colors.dart @@ -21,5 +21,13 @@ class AppColors { static const Color errorColor = Color(0xFFDC3545); static const Color warningColor = Color(0xFFFFC107); + static const Color grey = Color(0xFF555555); + static const Color grey60 = Color(0xFF777777); static const Color grey70 = Color(0xFF444444); + static const Color grey80 = Color(0xFF222222); + static const Color grey10 = Color(0xFFF4F4F4); + static const Color grey0 = Color(0xFFFCFCFC); + static const Color backgroundColor = Color(0xFFFFFFFF); + static const Color borderColor = Color(0xFFE1E1E1); + static const Color primary2 = Color(0xFFE5EFFF); } diff --git a/lib/views/home/home_screen.dart b/lib/views/home/home_screen.dart index 49a04d7..3a743c9 100644 --- a/lib/views/home/home_screen.dart +++ b/lib/views/home/home_screen.dart @@ -13,7 +13,7 @@ class Homescreen extends StatelessWidget { return Scaffold( backgroundColor: Colors.white, body: _body(), - bottomNavigationBar: BottomNavigation(), + bottomNavigationBar: TenderBottomNavigation(), ); } diff --git a/lib/views/tenders/models/tender_model.dart b/lib/views/tenders/models/tender_model.dart new file mode 100644 index 0000000..f270bd0 --- /dev/null +++ b/lib/views/tenders/models/tender_model.dart @@ -0,0 +1,94 @@ +class Tender { + final String date; + final String deadline; + final String title; + final String description; + final String tenderId; + final String location; + final String country; + final double matchPercentage; + + Tender({ + required this.date, + required this.deadline, + required this.title, + required this.description, + required this.tenderId, + required this.location, + required this.country, + required this.matchPercentage, + }); + + factory Tender.fromJson(Map json) { + return Tender( + date: json['date'] as String, + deadline: json['deadline'] as String, + title: json['title'] as String, + description: json['description'] as String, + tenderId: json['tenderId'] as String, + location: json['location'] as String, + country: json['country'] as String, + matchPercentage: (json['matchPercentage'] as num).toDouble(), + ); + } + + Map toJson() { + return { + 'date': date, + 'deadline': deadline, + 'title': title, + 'description': description, + 'tenderId': tenderId, + 'location': location, + 'country': country, + 'matchPercentage': matchPercentage, + }; + } +} + +final List sampleTenders = [ + Tender( + date: '2025-05-20', + deadline: '2025-06-10', + title: 'Digital Services Tender', + description: + 'A tender for digital services and IT infrastructure development. The project involves modernizing existing systems and implementing new digital solutions for improved efficiency.', + tenderId: 'KLF 2025/119', + location: 'Stockholm', + country: 'SE', + matchPercentage: 85.0, + ), + Tender( + date: '2025-04-15', + deadline: '2025-05-01', + title: 'Construction Project', + description: + 'Seeking contractors for a large-scale construction project in the city center. Includes both residential and commercial buildings.', + tenderId: 'CST 2025/042', + location: 'Gothenburg', + country: 'SE', + matchPercentage: 72.0, + ), + Tender( + date: '2025-03-10', + deadline: '2025-04-05', + title: 'Healthcare Equipment Supply', + description: + 'Supplying medical and healthcare equipment to regional hospitals. Includes installation and maintenance services.', + tenderId: 'HLT 2025/301', + location: 'Malmö', + country: 'SE', + matchPercentage: 90.0, + ), + Tender( + date: '2025-02-28', + deadline: '2025-03-20', + title: 'IT Consulting Services', + description: + 'Looking for experienced IT consultants to support digital transformation initiatives for public sector organizations.', + tenderId: 'ITC 2025/210', + location: 'Uppsala', + country: 'SE', + matchPercentage: 78.0, + ), +]; diff --git a/lib/views/tenders/tenders_screen.dart b/lib/views/tenders/tenders_screen.dart index 7d0175f..15bdd52 100644 --- a/lib/views/tenders/tenders_screen.dart +++ b/lib/views/tenders/tenders_screen.dart @@ -1,40 +1,82 @@ import 'package:flutter/material.dart'; -import '../../core/constants/assets.dart'; +import '../../common/widgets/bottom_navigation.dart'; import '../../core/constants/colors.dart'; -import '../../core/constants/strings.dart'; import '../../core/utils/size_config.dart'; +import 'models/tender_model.dart'; +import 'widgets/widgets.dart'; -class TendersScreen extends StatelessWidget { +class TendersScreen extends StatefulWidget { const TendersScreen({super.key}); + @override + State createState() => _TendersScreenState(); +} + +class _TendersScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController controller; + + @override + void initState() { + super.initState(); + controller = TabController(length: 2, vsync: this); + // Add listener to rebuild when tab changes + controller.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( - body: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.0.w(), vertical: 24.0.h()), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + backgroundColor: AppColors.backgroundColor, + bottomNavigationBar: TenderBottomNavigation(), + body: SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 24.0.w(), + vertical: 24.0.h(), + ), + child: Column( children: [ - Text( - AppStrings.tendersTitle, - style: TextStyle( - fontSize: 20.0.sp(), - fontWeight: FontWeight.w600, - color: AppColors.grey70, + TenderAppBar(), + SizedBox(height: 24.0.h()), + + MainTabBar(controller: controller), + SizedBox(height: 24.0.h()), + 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, + ); + }, ), ), - Image.asset( - AssetsManager.tenderLogo, - width: 59.0.w(), - height: 28.0.h(), - fit: BoxFit.cover, - ), ], ), - ], + ), ), ), ); diff --git a/lib/views/tenders/widgets/last_tenders_tab.dart b/lib/views/tenders/widgets/last_tenders_tab.dart new file mode 100644 index 0000000..983cb7d --- /dev/null +++ b/lib/views/tenders/widgets/last_tenders_tab.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class LastTendersTab extends StatelessWidget { + const LastTendersTab({super.key}); + + @override + Widget build(BuildContext context) { + return Container(color: Colors.blue); + } +} diff --git a/lib/views/tenders/widgets/main_tab_bar.dart b/lib/views/tenders/widgets/main_tab_bar.dart new file mode 100644 index 0000000..db954a2 --- /dev/null +++ b/lib/views/tenders/widgets/main_tab_bar.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import '../../../core/constants/colors.dart'; +import '../../../core/utils/size_config.dart'; +import 'tab_item.dart'; + +class MainTabBar extends StatefulWidget { + const MainTabBar({super.key, required this.controller}); + + final TabController controller; + + @override + State createState() => _MainTabBarState(); +} + +class _MainTabBarState extends State { + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: 51.0.h(), + padding: EdgeInsets.symmetric(horizontal: 8.0.w(), vertical: 8.0.h()), + decoration: BoxDecoration( + color: AppColors.grey10, + borderRadius: BorderRadius.circular(10), + ), + alignment: Alignment.center, + child: TabBar( + indicatorSize: TabBarIndicatorSize.tab, + dividerColor: Colors.transparent, + indicator: BoxDecoration( + color: AppColors.backgroundColor, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + controller: widget.controller, + tabs: [ + TabItem( + title: 'New', + isActive: widget.controller.index == 0, + onTap: () { + widget.controller.animateTo(0); + }, + ), + TabItem( + title: 'Last', + isActive: widget.controller.index == 1, + onTap: () { + widget.controller.animateTo(1); + }, + ), + ], + ), + ); + } +} diff --git a/lib/views/tenders/widgets/tab_item.dart b/lib/views/tenders/widgets/tab_item.dart new file mode 100644 index 0000000..0fe819c --- /dev/null +++ b/lib/views/tenders/widgets/tab_item.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import '../../../core/constants/colors.dart'; +import '../../../core/utils/size_config.dart'; + +class TabItem extends StatelessWidget { + const TabItem({ + super.key, + required this.title, + required this.isActive, + required this.onTap, + }); + + final String title; + final bool isActive; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Tab( + child: SizedBox( + width: 170.0.w(), + child: Center( + child: Text( + title, + style: TextStyle( + fontSize: 16.0.sp(), + fontWeight: FontWeight.w600, + color: isActive ? AppColors.mainBlue : AppColors.grey60, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/tenders/widgets/tender_app_bar.dart b/lib/views/tenders/widgets/tender_app_bar.dart new file mode 100644 index 0000000..2014d31 --- /dev/null +++ b/lib/views/tenders/widgets/tender_app_bar.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../../core/constants/assets.dart'; +import '../../../core/constants/colors.dart'; +import '../../../core/constants/strings.dart'; +import '../../../core/utils/size_config.dart'; + +class TenderAppBar extends StatelessWidget { + const TenderAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppStrings.tendersTitle, + style: TextStyle( + fontSize: 20.0.sp(), + fontWeight: FontWeight.w600, + color: AppColors.grey70, + ), + ), + Image.asset( + AssetsManager.tenderLogo, + width: 59.0.w(), + height: 28.0.h(), + fit: BoxFit.cover, + ), + ], + ); + } +} diff --git a/lib/views/tenders/widgets/tender_card.dart b/lib/views/tenders/widgets/tender_card.dart new file mode 100644 index 0000000..430e81c --- /dev/null +++ b/lib/views/tenders/widgets/tender_card.dart @@ -0,0 +1,311 @@ +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 TenderCard extends StatelessWidget { + final String date; + final String deadline; + final String title; + final String description; + final String tenderId; + final String location; + final String country; + final double matchPercentage; + final VoidCallback? onApply; + final VoidCallback? onLike; + final VoidCallback? onDislike; + final VoidCallback? onClose; + final bool isLiked; + final bool isDisliked; + + const TenderCard({ + super.key, + required this.date, + required this.deadline, + required this.title, + required this.description, + required this.tenderId, + required this.location, + required this.country, + required this.matchPercentage, + this.onApply, + this.onLike, + this.onDislike, + this.onClose, + this.isLiked = false, + this.isDisliked = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: 364.0.w(), + height: 587.0.h(), + decoration: BoxDecoration( + color: AppColors.grey0, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderColor), + ), + child: Column( + children: [ + // Main content + Padding( + padding: EdgeInsets.all(16.0.w()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Date + Text( + date, + style: TextStyle( + color: AppColors.grey60, + fontSize: 12.0.sp(), + fontWeight: FontWeight.w400, + ), + ), + SizedBox(height: 12.0.h()), + + // Deadline badge + Container( + width: double.infinity, + height: 24.0.h(), + padding: EdgeInsets.symmetric( + horizontal: 8.0.w(), + vertical: 4.0.h(), + ), + decoration: BoxDecoration( + color: AppColors.primary2, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Deadline:', + style: TextStyle( + color: AppColors.mainBlue, + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + ), + ), + Text( + deadline, + style: TextStyle( + color: AppColors.grey80, + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SizedBox(height: 12.0.h()), + + // Title + Text( + title, + style: TextStyle( + color: AppColors.grey80, + fontSize: 16.0.sp(), + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.0.h()), + + // Description + Text( + description, + style: TextStyle( + color: AppColors.grey70, + fontSize: 16.0.sp(), + fontWeight: FontWeight.w400, + height: 1.5, + ), + ), + SizedBox(height: 12.0.h()), + + // Tender ID + Text( + tenderId, + style: TextStyle( + color: AppColors.grey60, + fontSize: 14.0.sp(), + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + Spacer(), + // Bottom section with progress and actions + Padding( + padding: EdgeInsets.all(16.0.w()), + child: Column( + children: [ + // Match percentage + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Match with your profile', + style: TextStyle( + color: AppColors.primaryTextColor, + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + ), + ), + Text( + '${matchPercentage.toInt()}%', + style: TextStyle( + color: const Color(0xFF2484A8), + fontSize: 16.0.sp(), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 8.0.h()), + + // Progress bar + Container( + width: double.infinity, + height: 4.0.h(), + decoration: BoxDecoration( + color: AppColors.grey10, + borderRadius: BorderRadius.circular(2), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: matchPercentage / 100, + child: Container( + decoration: BoxDecoration( + color: const Color(0xFF34BC9B), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + SizedBox(height: 16.0.h()), + + // Location and apply button + Row( + children: [ + // Location info + Expanded( + child: Row( + children: [ + SvgPicture.asset(AssetsManager.location), + SizedBox(width: 4.0.w()), + Text( + location, + style: TextStyle( + color: AppColors.primaryTextColor, + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 8.0.w()), + // Country flag placeholder + Image.asset( + AssetsManager.seFlag, + width: 32.0.w(), + height: 21.0.h(), + ), + ], + ), + ), + + // Apply button + GestureDetector( + onTap: onApply, + child: Container( + width: 108.0.w(), + height: 40.0.h(), + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.primary2, + borderRadius: BorderRadius.circular(100), + ), + child: Text( + 'See More', + style: TextStyle( + color: AppColors.mainBlue, + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 24.0.h()), + TenderActionButtons( + isDisliked: isDisliked, + isLiked: isLiked, + onDislike: onDislike, + onLike: onLike, + onClose: onClose, + ), + ], + ); + } +} + +// Action buttons row widget +class TenderActionButtons extends StatelessWidget { + final VoidCallback? onLike; + final VoidCallback? onDislike; + final VoidCallback? onClose; + final bool isLiked; + final bool isDisliked; + + const TenderActionButtons({ + super.key, + this.onLike, + this.onDislike, + this.onClose, + this.isLiked = false, + this.isDisliked = false, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: 16.0.w()), + + // Dislike button + GestureDetector( + onTap: onDislike, + child: SvgPicture.asset(AssetsManager.dislike), + ), + + SizedBox(width: 16.0.w()), + + // Close button + GestureDetector( + onTap: onClose, + child: SvgPicture.asset(AssetsManager.closeCircle), + ), + SizedBox(width: 16.0.w()), + // Like button + GestureDetector( + onTap: onLike, + child: SvgPicture.asset(AssetsManager.like), + ), + ], + ); + } +} diff --git a/lib/views/tenders/widgets/widgets.dart b/lib/views/tenders/widgets/widgets.dart new file mode 100644 index 0000000..b0aa9ea --- /dev/null +++ b/lib/views/tenders/widgets/widgets.dart @@ -0,0 +1,4 @@ +export 'last_tenders_tab.dart'; +export 'main_tab_bar.dart'; +export 'tender_app_bar.dart'; +export 'tender_card.dart';