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
+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C17.5 2 22 6.5 22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2Z" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.17188 9.16813L14.8319 14.8281" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.8319 9.16813L9.17188 14.8281" stroke="#9E9E9E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.48047 5.64954L10.5805 3.24954C10.9805 2.84954 11.8805 2.64954 12.4805 2.64954H16.2805C17.4805 2.64954 18.7805 3.54954 19.0805 4.74954L21.4805 12.0495C21.9805 13.4495 21.0805 14.6495 19.5805 14.6495H15.5805C14.9805 14.6495 14.4805 15.1495 14.5805 15.8495L15.0805 19.0495C15.2805 19.9495 14.6805 20.9495 13.7805 21.2495C12.9805 21.5495 11.9805 21.1495 11.5805 20.5495L7.48047 14.4495" stroke="#9E9E9E" stroke-width="2" stroke-miterlimit="10"/>
<path d="M2.37891 5.65156V15.4516C2.37891 16.8516 2.97891 17.3516 4.37891 17.3516H5.37891C6.77891 17.3516 7.37891 16.8516 7.37891 15.4516V5.65156C7.37891 4.25156 6.77891 3.75156 5.37891 3.75156H4.37891C2.97891 3.75156 2.37891 4.25156 2.37891 5.65156Z" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 892 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.48047 18.3505L10.5805 20.7505C10.9805 21.1505 11.8805 21.3505 12.4805 21.3505H16.2805C17.4805 21.3505 18.7805 20.4505 19.0805 19.2505L21.4805 11.9505C21.9805 10.5505 21.0805 9.35046 19.5805 9.35046H15.5805C14.9805 9.35046 14.4805 8.85046 14.5805 8.15046L15.0805 4.95046C15.2805 4.05046 14.6805 3.05046 13.7805 2.75046C12.9805 2.45046 11.9805 2.85046 11.5805 3.45046L7.48047 9.55046" stroke="#34C759" stroke-width="2" stroke-miterlimit="10"/>
<path d="M2.37891 18.3484V8.54844C2.37891 7.14844 2.97891 6.64844 4.37891 6.64844H5.37891C6.77891 6.64844 7.37891 7.14844 7.37891 8.54844V18.3484C7.37891 19.7484 6.77891 20.2484 5.37891 20.2484H4.37891C2.97891 20.2484 2.37891 19.7484 2.37891 18.3484Z" stroke="#34C759" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 892 B

+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,
),
];
+63 -21
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()),
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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
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';