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.
This commit is contained in:
amirrezaghabeli
2025-08-03 19:11:51 +03:30
parent a2f123ce9e
commit df02cff668
15 changed files with 648 additions and 27 deletions
+4 -4
View File
@@ -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<BottomNavigation> createState() => _BottomNavigationState();
State<TenderBottomNavigation> createState() => _TenderBottomNavigationState();
}
class _BottomNavigationState extends State<BottomNavigation> {
class _TenderBottomNavigationState extends State<TenderBottomNavigation> {
int activeTab = 0;
@override
+5
View File
@@ -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';
}
+8
View File
@@ -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);
}
+1 -1
View File
@@ -13,7 +13,7 @@ class Homescreen extends StatelessWidget {
return Scaffold(
backgroundColor: Colors.white,
body: _body(),
bottomNavigationBar: BottomNavigation(),
bottomNavigationBar: TenderBottomNavigation(),
);
}
@@ -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<String, dynamic> 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<String, dynamic> toJson() {
return {
'date': date,
'deadline': deadline,
'title': title,
'description': description,
'tenderId': tenderId,
'location': location,
'country': country,
'matchPercentage': matchPercentage,
};
}
}
final List<Tender> 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,
),
];
+64 -22
View File
@@ -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<TendersScreen> createState() => _TendersScreenState();
}
class _TendersScreenState extends State<TendersScreen>
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,
),
],
),
],
),
),
),
);
@@ -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);
}
}
@@ -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<MainTabBar> createState() => _MainTabBarState();
}
class _MainTabBarState extends State<MainTabBar> {
@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);
},
),
],
),
);
}
}
+39
View File
@@ -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,
),
),
),
),
),
);
}
}
@@ -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,
),
],
);
}
}
+311
View File
@@ -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),
),
],
);
}
}
+4
View File
@@ -0,0 +1,4 @@
export 'last_tenders_tab.dart';
export 'main_tab_bar.dart';
export 'tender_app_bar.dart';
export 'tender_card.dart';