added new design for tenders desktop

This commit is contained in:
llsajjad
2025-09-20 23:09:00 +03:30
parent fae6509698
commit 8a289f2d05
9 changed files with 199 additions and 50 deletions
+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="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z" stroke="#888888" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 22L20 20" stroke="#888888" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 430 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.75 5.25H16.25" stroke="#0164FF" stroke-width="1.5" stroke-linecap="round"/>
<path d="M5 9H14" stroke="#0164FF" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 12.75H11" stroke="#0164FF" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 353 B

+2
View File
@@ -40,6 +40,8 @@ class AssetsManager {
static const arrowLeftSmall = 'assets/icons/arrow-left-small.svg'; static const arrowLeftSmall = 'assets/icons/arrow-left-small.svg';
static const arrowRightSmall = 'assets/icons/arrow-right-small.svg'; static const arrowRightSmall = 'assets/icons/arrow-right-small.svg';
static const tick = 'assets/icons/tick.svg'; static const tick = 'assets/icons/tick.svg';
static const normalSearch = 'assets/icons/search_normal.svg';
static const sort = 'assets/icons/sort.svg';
//home page //home page
static const arrows = 'assets/icons/arrows.svg'; static const arrows = 'assets/icons/arrows.svg';
+1 -1
View File
@@ -37,7 +37,7 @@ void main() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
List<SingleChildWidget> providers = [ List<SingleChildWidget> providers = [
Provider(create: (context) => prefs), Provider(create: (context) => prefs),
...providersRemote, ...providersRemote,
]; ];
// Configure web URL strategy if running on web // Configure web URL strategy if running on web
+37 -8
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../core/theme/colors.dart'; import '../../core/theme/colors.dart';
import '../../core/utils/size_config.dart'; import '../../core/utils/size_config.dart';
@@ -16,6 +17,8 @@ class BaseButton extends StatelessWidget {
final double? elevation; final double? elevation;
final double? width; final double? width;
final bool isLoading; final bool isLoading;
final String? icon;
final Color? iconColor;
const BaseButton({ const BaseButton({
required this.isEnabled, required this.isEnabled,
@@ -29,6 +32,8 @@ class BaseButton extends StatelessWidget {
this.elevation, this.elevation,
this.width, this.width,
this.isLoading = false, this.isLoading = false,
this.icon,
this.iconColor,
super.key, super.key,
}); });
@@ -72,14 +77,38 @@ class BaseButton extends StatelessWidget {
), ),
), ),
), ),
child: Text( child:
text ?? LoginStrings.loginButton, icon != null
style: TextStyle( ? Row(
fontSize: 16.0.sp(), crossAxisAlignment: CrossAxisAlignment.center,
fontWeight: FontWeight.bold, mainAxisAlignment: MainAxisAlignment.center,
color: textColor ?? AppColors.white, children: [
), SvgPicture.asset(
), icon!,
colorFilter: ColorFilter.mode(
iconColor ?? AppColors.white,
BlendMode.srcIn,
),
),
SizedBox(width: 7.0.w()),
Text(
text ?? LoginStrings.loginButton,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.bold,
color: textColor ?? AppColors.white,
),
),
],
)
: Text(
text ?? LoginStrings.loginButton,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.bold,
color: textColor ?? AppColors.white,
),
),
), ),
); );
} }
+84 -1
View File
@@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/view_models/tenders_view_model.dart';
import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart'; import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart';
@@ -49,6 +52,86 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
const DesktopNavigationWidget( const DesktopNavigationWidget(
currentIndex: 1, // Tenders index currentIndex: 1, // Tenders index
), ),
SizedBox(height: 48.0.h()),
//search box
SizedBox(
height: 48.0.h(),
width: 680,
child: TextField(
style: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey0),
decoration: InputDecoration(
hintText: 'Search',
hintStyle: TextStyle(
fontSize: 16.0.sp(),
color: AppColors.grey60,
),
prefixIcon: Padding(
padding: EdgeInsets.all(12.0.w()),
child: SvgPicture.asset(
AssetsManager.normalSearch,
width: 24.0.w(),
height: 24.0.h(),
),
),
filled: true,
fillColor: AppColors.grey0,
contentPadding: EdgeInsets.symmetric(
horizontal: 12.0.w(),
vertical: 12.0.h(),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0.w()),
borderSide: const BorderSide(
color: AppColors.mainBlue,
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0.w()),
borderSide: BorderSide(color: AppColors.grey40, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0.w()),
borderSide: const BorderSide(
color: AppColors.grey,
width: 1.5,
),
),
),
),
),
Container(
margin: EdgeInsets.only(top: 25.0.h()),
height: 40.0.h(),
width: 680,
child: Row(
children: [
BaseButton(
width: 178.0.w(),
isEnabled: true,
onPressed: () {},
text: 'Filter',
icon: AssetsManager.filter,
iconColor: AppColors.textBlue,
backgroundColor: AppColors.primary2,
textColor: AppColors.textBlue,
),
SizedBox(width: 10.0.w()),
BaseButton(
width: 178.0.w(),
isEnabled: true,
onPressed: () {},
text: 'Sort',
icon: AssetsManager.sort,
iconColor: AppColors.textBlue,
backgroundColor: AppColors.primary2,
textColor: AppColors.textBlue,
),
],
),
),
Expanded( Expanded(
child: Consumer<TendersViewModel>( child: Consumer<TendersViewModel>(
builder: (context, viewModel, child) { builder: (context, viewModel, child) {
@@ -74,7 +157,7 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
width: 740, width: 740,
height: 800, height: 800,
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(vertical: 24.0.h()), padding: EdgeInsets.symmetric(vertical: 4.0.h()),
child: MainTendersSlider( child: MainTendersSlider(
tenders: viewModel.tendersResponse?.data?.tenders ?? [], tenders: viewModel.tendersResponse?.data?.tenders ?? [],
isDesktop: true, isDesktop: true,
@@ -15,6 +15,7 @@ class MainTendersSlider extends StatefulWidget {
super.key, super.key,
this.isDesktop = false, this.isDesktop = false,
}); });
final bool isDesktop; final bool isDesktop;
final List<TenderData> tenders; final List<TenderData> tenders;
@@ -24,7 +25,6 @@ class MainTendersSlider extends StatefulWidget {
class _MainTendersSliderState extends State<MainTendersSlider> { class _MainTendersSliderState extends State<MainTendersSlider> {
final PageController pageController = PageController(initialPage: 0); final PageController pageController = PageController(initialPage: 0);
int currentPage = 1; int currentPage = 1;
void _goToPreviousPage() { void _goToPreviousPage() {
@@ -32,17 +32,22 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
return; return;
} }
final targetIndex = currentPage - 2;
if (targetIndex < 0) {
return;
}
pageController.previousPage( pageController.previousPage(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutCubic, curve: Curves.easeInOutCubic,
); );
setState(() { setState(() {
currentPage = currentPage - 1; currentPage = currentPage - 1;
}); });
// Fetch feedback and approvals for the previous tender if not already loaded
final viewModel = context.read<TendersViewModel>(); final viewModel = context.read<TendersViewModel>();
final previousTenderId = widget.tenders[currentPage - 2].id!; final previousTenderId = widget.tenders[targetIndex].id!;
if (!viewModel.hasFeedbackForTender(previousTenderId)) { if (!viewModel.hasFeedbackForTender(previousTenderId)) {
viewModel.getTenderFeedback(previousTenderId); viewModel.getTenderFeedback(previousTenderId);
} }
@@ -56,17 +61,22 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
return; return;
} }
final targetIndex = currentPage;
if (targetIndex >= widget.tenders.length) {
return;
}
pageController.nextPage( pageController.nextPage(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutCubic, curve: Curves.easeInOutCubic,
); );
setState(() { setState(() {
currentPage = currentPage + 1; currentPage = currentPage + 1;
}); });
// Fetch feedback and approvals for the next tender if not already loaded
final viewModel = context.read<TendersViewModel>(); final viewModel = context.read<TendersViewModel>();
final nextTenderId = widget.tenders[currentPage].id!; final nextTenderId = widget.tenders[targetIndex].id!;
if (!viewModel.hasFeedbackForTender(nextTenderId)) { if (!viewModel.hasFeedbackForTender(nextTenderId)) {
viewModel.getTenderFeedback(nextTenderId); viewModel.getTenderFeedback(nextTenderId);
} }
@@ -83,11 +93,27 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isLtr = Directionality.of(context) == TextDirection.ltr; final bool isLtr = Directionality.of(context) == TextDirection.ltr;
return SingleChildScrollView(
child: Stack( return widget.isDesktop ? _tendersListView() : _tendersSlider(isLtr);
children: [_tendersPageView(), _nextPreviousArrows(isLtr: isLtr)], }
),
Widget _tendersListView() {
return ListView.separated(
padding: EdgeInsets.all(16.0.h()),
itemCount: widget.tenders.length,
separatorBuilder:
(_, __) => SizedBox(height: widget.isDesktop ? 0 : 16.0.h()),
itemBuilder: (context, index) {
final tender = widget.tenders[index];
return TenderCard(tender: tender, isDesktop: widget.isDesktop);
},
);
}
Widget _tendersSlider(bool isLtr) {
return Stack(
children: [_tendersPageView(), _nextPreviousArrows(isLtr: isLtr)],
); );
} }
@@ -97,25 +123,21 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
height: 722.0.h(), height: 722.0.h(),
child: PageView.builder( child: PageView.builder(
controller: pageController, controller: pageController,
scrollDirection: Axis.horizontal,
itemCount: widget.tenders.length, itemCount: widget.tenders.length,
onPageChanged: (index) { onPageChanged: (index) {
setState(() { setState(() {
currentPage = index + 1; currentPage = index + 1;
}); });
// Fetch feedback and approvals for the new tender if not already loaded
final viewModel = context.read<TendersViewModel>(); final viewModel = context.read<TendersViewModel>();
if (!viewModel.hasFeedbackForTender(widget.tenders[index].id!)) { final id = widget.tenders[index].id!;
viewModel.getTenderFeedback(widget.tenders[index].id!); if (!viewModel.hasFeedbackForTender(id)) {
viewModel.getTenderFeedback(id);
} }
if (!viewModel.hasTenderApprovalForTender( if (!viewModel.hasTenderApprovalForTender(id)) {
widget.tenders[index].id!, viewModel.getTenderApprovals(id);
)) {
viewModel.getTenderApprovals(widget.tenders[index].id!);
} }
// Check if we need to load more tenders (when user reaches near the end)
if (index >= widget.tenders.length - 1 && if (index >= widget.tenders.length - 1 &&
viewModel.hasMoreData && viewModel.hasMoreData &&
!viewModel.isLoadingMore) { !viewModel.isLoadingMore) {
@@ -35,13 +35,13 @@ class TenderActionButtonsRow extends StatelessWidget {
// Dislike button // Dislike button
_dislikeButton(viewModel), _dislikeButton(viewModel),
SizedBox(width: 32.0.w()), isDesktop ? Container() : SizedBox(width: 32.0.w()),
//reject button //reject button
_rejectButton(viewModel), isDesktop ? Container() : _rejectButton(viewModel),
SizedBox(width: 32.0.w()), isDesktop ? Container() : SizedBox(width: 32.0.w()),
//submit button //submit button
_submitButton(viewModel, context), isDesktop ? Container() : _submitButton(viewModel, context),
SizedBox(width: 32.0.w()), SizedBox(width: isDesktop ? 16.0.h() : 32.0.w()),
// Like button // Like button
_likeButton(viewModel), _likeButton(viewModel),
], ],
+20 -16
View File
@@ -26,7 +26,7 @@ class TenderCard extends StatelessWidget {
Container( Container(
width: double.infinity, width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 16.0.w()), margin: EdgeInsets.symmetric(horizontal: 16.0.w()),
height: 587.0.h(), height: isDesktop ? 290.0.h() : 587.0.h(),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.grey0, color: AppColors.grey0,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -56,38 +56,41 @@ class TenderCard extends StatelessWidget {
// Deadline badge // Deadline badge
_deadlineBadge(), _deadlineBadge(),
SizedBox(height: 12.0.h()), isDesktop ? Container() : SizedBox(height: 12.0.h()),
// Approval status badge // Approval status badge
// _approvalStatusBadge(), // _approvalStatusBadge(),
SizedBox(height: 12.0.h()), SizedBox(height: isDesktop ? 6.0.h() : 12.0.h()),
// Title // Title
_tenderTitle(), _tenderTitle(),
SizedBox(height: 8.0.h()), SizedBox(height: isDesktop ? 4.0.h() : 8.0.h()),
// Description // Description
_tenderDescription(), _tenderDescription(),
SizedBox(height: 12.0.h()), isDesktop ? Container() : SizedBox(height: 12.0.h()),
// Tender ID // Tender ID
_idText(), isDesktop ? Container() : _idText(),
], ],
), ),
), ),
const Spacer(), const Spacer(),
// Bottom section with progress and actions // Bottom section with progress and actions
Padding( Padding(
padding: EdgeInsets.all(16.0.w()), padding: EdgeInsets.symmetric(
vertical: isDesktop ? 8.0.w() : 16.0.w(),
horizontal: 16.0.w(),
),
child: Column( child: Column(
children: [ children: [
// Match percentage // Match percentage
_matchPercentage(), _matchPercentage(),
SizedBox(height: 8.0.h()), SizedBox(height: isDesktop ? 4.0.h() : 8.0.h()),
// Progress bar // Progress bar
_progressBar(), _progressBar(),
SizedBox(height: 16.0.h()), SizedBox(height: isDesktop ? 8.0.h() : 16.0.h()),
// Location and apply button // Location and apply button
Row( Row(
@@ -105,7 +108,7 @@ class TenderCard extends StatelessWidget {
], ],
), ),
), ),
SizedBox(height: 72.0.h()), SizedBox(height: isDesktop ? 20.0.h() : 72.0.h()),
if (!isDesktop) TenderActionButtonsRow(tender: tender), if (!isDesktop) TenderActionButtonsRow(tender: tender),
], ],
); );
@@ -158,10 +161,11 @@ class TenderCard extends StatelessWidget {
Widget _tenderTitle() { Widget _tenderTitle() {
return Text( return Text(
tender.title ?? '', tender.title ?? '',
maxLines: isDesktop ? 2 : 4,
style: TextStyle( style: TextStyle(
color: AppColors.grey80, color: AppColors.grey80,
fontSize: 16.0.sp(), fontSize: 16.0.sp(),
fontWeight: FontWeight.w600, fontWeight: isDesktop ? FontWeight.w500 : FontWeight.w600,
), ),
); );
} }
@@ -169,11 +173,11 @@ class TenderCard extends StatelessWidget {
Widget _tenderDescription() { Widget _tenderDescription() {
return Text( return Text(
tender.description ?? '', tender.description ?? '',
maxLines: 4, maxLines: isDesktop ? 2 : 4,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: AppColors.grey70, color: AppColors.grey70,
fontSize: 16.0.sp(), fontSize: isDesktop ? 14.0.sp() : 16.0.sp(),
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
height: 1.5, height: 1.5,
), ),
@@ -199,15 +203,15 @@ class TenderCard extends StatelessWidget {
TenderDetailsStrings.tenderMatchProfile, TenderDetailsStrings.tenderMatchProfile,
style: TextStyle( style: TextStyle(
color: AppColors.grey80, color: AppColors.grey80,
fontSize: 14.0.sp(), fontSize: isDesktop ? 12.0.sp() : 14.0.sp(),
fontWeight: FontWeight.w500, fontWeight: isDesktop ? FontWeight.w400 : FontWeight.w500,
), ),
), ),
Text( Text(
'${45}%', '${45}%',
style: TextStyle( style: TextStyle(
color: AppColors.secondaryTextColor, color: AppColors.secondaryTextColor,
fontSize: 16.0.sp(), fontSize: isDesktop ? 12.0.sp() : 16.0.sp(),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),