Merge pull request 'tablet_desktop_android_screen' (#19) from tablet_desktop_android_screen into main

Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/19
This commit is contained in:
a.ghabeli
2025-08-09 08:32:30 +03:30
22 changed files with 1240 additions and 558 deletions
+34
View File
@@ -41,4 +41,38 @@ class AppStrings {
//your tenders
static const String yourTendersTitle = 'Your tenders';
// Tender Detail Page
static const String tenderDetailTitle = 'Tender Detail';
static const String tenderStatusOpen = 'Open';
static const String tenderIdLabel = 'ID';
static const String tenderDeadlineLabel = 'Deadline';
static const String tenderApprovalText = 'For approval';
static const String tenderSubmissionText = 'For submission';
static const String tenderClientLabel = 'Client';
static const String tenderDeliveryLocationsLabel = 'Delivery Locations';
static const String tenderReferenceNumberLabel = 'Reference Number';
static const String tenderLocationLabel = 'Locations';
static const String tenderLocationCountry = 'UK';
static const String tenderPdfDocument = 'PDF , Document';
static const String tenderMatchProfile = 'Match with your profile';
static const String tenderIncompleteResume = 'Incomplete Resume Information';
static const String tenderNoExperience =
'No experience in e-platform development';
static const String tenderSubmitButton = 'Submit';
static const String tenderRejectButton = 'Reject';
static const String tenderDateExample = '2025-05-21';
static const String tenderTitleExample =
'Operation, support and further development of open e-platform Operation, support and further development of open e-platform';
static const String tenderIdExample = 'JNDFKMDV-100-JF';
static const String tenderApprovalDateExample = '2025-05-21';
static const String tenderSubmissionDateExample = '2025-05-21';
static const String tenderClientExample =
'Procurement Notice Procurement Notice';
static const String tenderDeliveryLocationExample = 'Norrbotten County';
static const String tenderReferenceNumberExample = 'KLF 2025/120';
static const String tenderDescriptionExample =
'Luleå Municipality is procuring on behalf of Norrbottens e-board (all municipalities in Norrbotten)...';
static const String tenderMatchPercentageExample = '75%';
static const String locationDescription = 'Luleå Municipality is procuring on behalf of Norrbottens e-board (all municipalities in Norrbotten).

The procurement includes operation, support, and further development of the existing open e-platform.

No additional information, amendments, or answers to questions will be provided after June 16.o additional information, amendments, or answers to questions will be provided after June 16.o additional information, amendments, or answers to questions will be provided after June 16.website: www.tenders.com Lorem ipsum is amet Luleå Municipality is procuring on behalf of Norrbottens e-board (all municipalities in Norrbotten).

The procurement includes operation, support, and further development of the existing open e-platform.

No additional information, amendments, or answers to questions will be provided after June 16. o additional information, amendments, or answers to questions will be provided after June 16. o additional information, amendments, or answers to questions will be provided after June 16.website: www.tenders.com';
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
enum DisplaySize {
extraSmall, //mobile
small, //tablet
medium, //laptop
large, //desktop
}
extension DisplaySizeDetector on Size {
DisplaySize get displaySize {
if (width < 600) {
return DisplaySize.extraSmall;
} else if (width < 1024) {
return DisplaySize.small;
} else if (width < 1440) {
return DisplaySize.medium;
} else {
return DisplaySize.large;
}
}
String get displaySizeName {
switch (displaySize) {
case DisplaySize.extraSmall:
return 'Mobile';
case DisplaySize.small:
return 'Tablet';
case DisplaySize.medium:
return 'Laptop';
case DisplaySize.large:
return 'Desktop';
}
}
}
+42
View File
@@ -0,0 +1,42 @@
// lib/view_models/login_view_model.dart
import 'package:flutter/material.dart';
class LoginViewModel with ChangeNotifier {
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final FocusNode usernameFocus = FocusNode();
final FocusNode passwordFocus = FocusNode();
bool _obscurePassword = true;
LoginViewModel() {
usernameController.addListener(_onTextChanged);
passwordController.addListener(_onTextChanged);
usernameFocus.addListener(_onTextChanged);
passwordFocus.addListener(_onTextChanged);
}
bool get obscurePassword => _obscurePassword;
bool get isLoginEnabled =>
usernameController.text.isNotEmpty &&
passwordController.text.isNotEmpty;
void togglePasswordVisibility() {
_obscurePassword = !_obscurePassword;
notifyListeners();
}
void _onTextChanged() {
notifyListeners();
}
@override
void dispose() {
usernameController.dispose();
passwordController.dispose();
usernameFocus.dispose();
passwordFocus.dispose();
super.dispose();
}
}
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailDesktopScreen extends StatelessWidget {
const TenderDetailDesktopScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(context),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 740,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isBig: true,),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(),
],
),
),
),
),
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: Colors.white,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailMobileScreen extends StatelessWidget {
const TenderDetailMobileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(context),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isBig: false),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(),
],
),
),
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: Colors.white,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
+14 -422
View File
@@ -1,432 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/login/widgets/login_button.dart';
import 'package:tm_app/core/utils/display_size_detector.dart';
import 'package:tm_app/views/detail/detail_desktop_screen.dart';
import 'package:tm_app/views/detail/detail_mobile_screen.dart';
import 'package:tm_app/views/detail/detail_tablet_screen.dart';
class TenderDetailScreen extends StatelessWidget {
const TenderDetailScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: Colors.white,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
final displaySize = MediaQuery.of(context).size.displaySize;
title: const Text(
'Tender Detail',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w600),
),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(width: 0.5, color: AppColors.grey50),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'2025-05-21',
style: TextStyle(
color: AppColors.grey60,
fontWeight: FontWeight.w400,
fontSize: 12.0.sp(),
),
),
StatusTag(
text: 'Open',
textColor: AppColors.cyanTeal,
backgroundColor: AppColors.cyanAqua,
),
],
),
SizedBox(height: 12.0.h()),
Text(
'Operation, support and further development of open e-platform Operation, support and further development of open e-platform',
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 18.0.sp(),
),
),
SizedBox(height: 10.0.h()),
Divider(),
SizedBox(height: 5.0.h()),
infoItem('ID', 'JNDFKMDV-100-JF'),
deadlineItem(
title: 'Deadline',
approvalText: 'For approval',
approvalDate: '2025-05-21',
submissionText: 'For submission',
submissionDate: '2025-05-21',
),
infoItem('Client', 'Procurement Notice Procurement Notice'),
infoItem('Delivery Locations', 'Norrbotten County'),
infoItem('Reference Number', 'KLF 2025/120'),
Padding(
padding: const EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
'Reference Number',
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
Row(
children: [
SvgPicture.asset(
AssetsManager.location,
height: 20.0.h(),
width: 20.0.w(),
),
SizedBox(width: 4.0.w()),
Text(
'UK',
style: TextStyle(
color: AppColors.grey70,
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
),
),
SizedBox(width: 4.0.w()),
Image.asset(
AssetsManager.seFlag,
height: 20.0.h(),
width: 20.0.w(),
),
],
),
SizedBox(height: 8.0.h()),
],
),
),
SizedBox(height: 16.0.h()),
Text(
'Locations',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0.sp(),
),
),
SizedBox(height: 8.0.h()),
const Text(
'Luleå Municipality is procuring on behalf of Norrbottens e-board (all municipalities in Norrbotten).\n\n'
'The procurement includes operation, support, and further development of the existing open e-platform.\n\n'
'No additional information, amendments, or answers to questions will be provided after June 16.\n\n'
'0 additional information, amendments, or answers to questions will be provided after June 16.\n\n'
'0 additional information, amendments, or answers to questions will be provided after June 16 website: www.tenders.com\n\n'
'Lorem ipsum is amet\n\n'
'Luleå Municipality is procuring on behalf of Norrbottens e-board (all municipalities in Norrbotten).\n\n'
'The procurement includes operation, support, and further development of the existing open e-platform.\n\n'
'No additional information, amendments, or answers to questions will be provided after June 16.\n\n'
'0 additional information, amendments, or answers to questions will be provided after June 16.\n\n'
'0 additional information, amendments, or answers to questions will be provided after June 16.',
style: TextStyle(height: 1.5),
),
SizedBox(height: 16.0.h()),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(AssetsManager.export),
SizedBox(height: 6.0.h()),
Text(
'PDF , Document',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
],
),
],
),
),
SizedBox(height: 24.0.h()),
ResumeMatchCard(),
SizedBox(height: 24.0.h()),
Column(
children: [
BaseButton(
isEnabled: true,
text: 'Submit',
backgroundColor: AppColors.lightBlue,
textColor: AppColors.mainBlue,
onPressed: () {},
),
SizedBox(height: 16.0.h()),
BaseButton(
isEnabled: true,
text: 'reject',
borderColor: AppColors.red10,
textColor: AppColors.red10,
backgroundColor: Colors.white,
borderWidth: 1,
onPressed: () {},
),
SizedBox(height: 16.0.h()),
],
),
],
),
),
),
);
}
Widget deadlineItem({
required String title,
required String approvalText,
required String approvalDate,
required String submissionText,
required String submissionDate,
}) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
approvalText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
Text(
approvalDate,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
SizedBox(height: 4.0.h()),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
submissionText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
Text(
submissionDate,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
SizedBox(height: 8.0.h()),
Divider(),
],
),
);
}
Widget infoItem(String title, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
Text(
value,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
SizedBox(height: 8.0.h()),
Divider(),
],
),
);
}
}
class ResumeMatchCard extends StatelessWidget {
const ResumeMatchCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.paleOrange,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Match with your profile',
style: TextStyle(
fontSize: 12.0.sp(),
color: Colors.black87,
fontWeight: FontWeight.w400,
),
),
Text(
'75%',
style: TextStyle(
fontSize: 12.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.jellyBean,
),
),
],
),
SizedBox(height: 6.0.h()),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: 0.75,
backgroundColor: AppColors.grey20,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.jellyBean),
minHeight: 6,
),
),
SizedBox(height: 16.0.h()),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.warning_rounded, color: Colors.red),
SizedBox(width: 8.0.w()),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Incomplete Resume Information',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
color: Colors.black87,
),
),
SizedBox(height: 4.0.h()),
Text(
'No experience in e-platform development',
style: TextStyle(
fontSize: 14.0.sp(),
color: Colors.black54,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
],
),
);
}
}
class StatusTag extends StatelessWidget {
final String text;
final Color textColor;
final Color backgroundColor;
const StatusTag({
required this.text,
required this.textColor,
required this.backgroundColor,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.0.w(), vertical: 6.0.h()),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(
text,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w500,
fontSize: 14.0.sp(),
),
),
);
switch (displaySize) {
case DisplaySize.large:
case DisplaySize.medium:
return const TenderDetailDesktopScreen();
case DisplaySize.small:
return const TenderDetailTabletScreen();
case DisplaySize.extraSmall:
return const TenderDetailMobileScreen();
}
}
}
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
class TenderDetailTabletScreen extends StatelessWidget {
const TenderDetailTabletScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(context),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
width: 768,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TenderDetailHeader(isBig: true,),
SizedBox(height: 24.0.h()),
const TenderDetailCard(),
SizedBox(height: 24.0.h()),
TenderDetailActions(),
],
),
),
),
),
),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: Colors.white,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
}
+163
View File
@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
class DeadlineItem extends StatelessWidget {
final String title;
final String approvalText;
final String approvalDate;
final String submissionText;
final String submissionDate;
final bool isBig;
const DeadlineItem({
required this.title, required this.approvalText, required this.approvalDate, required this.submissionText, required this.submissionDate, required this.isBig, super.key,
});
@override
Widget build(BuildContext context) {
return isBig ? Padding(
padding: EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
approvalText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
SizedBox(height: 4.0.h()),
Text(
approvalDate,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
),
VerticalDivider(
color: AppColors.grey30,
thickness: 1,
width: 24,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
submissionText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
SizedBox(height: 4.0.h()),
Text(
submissionDate,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
),
],
),
),
SizedBox(height: 8.0.h()),
const Divider(),
],
),
) : Padding(
padding: EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
approvalText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
Text(
approvalDate,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
SizedBox(height: 4.0.h()),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
submissionText,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
Text(
submissionDate,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
],
),
SizedBox(height: 8.0.h()),
const Divider(),
],
),
);
}
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
class InfoItem extends StatelessWidget {
const InfoItem({required this.title, required this.value, super.key});
final String title;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
title,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
Text(
value,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w400,
fontSize: 14.0.sp(),
),
),
SizedBox(height: 8.0.h()),
const Divider(),
],
),
);
}
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/size_config.dart';
class StatusTag extends StatelessWidget {
final String text;
final Color textColor;
final Color backgroundColor;
const StatusTag({
required this.text,
required this.textColor,
required this.backgroundColor,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.0.w(), vertical: 6.0.h()),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(
text,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w500,
fontSize: 14.0.sp(),
),
),
);
}
}
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/login/widgets/login_button.dart';
class TenderDetailActions extends StatelessWidget {
const TenderDetailActions({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
BaseButton(
isEnabled: true,
text: AppStrings.tenderSubmitButton,
backgroundColor: AppColors.lightBlue,
textColor: AppColors.mainBlue,
onPressed: () {},
),
SizedBox(height: 16.0.h()),
BaseButton(
isEnabled: true,
text: AppStrings.tenderRejectButton,
borderColor: AppColors.red10,
textColor: AppColors.red10,
backgroundColor: Colors.white,
borderWidth: 1,
onPressed: () {},
),
SizedBox(height: 16.0.h()),
],
);
}
}
@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
class TenderDetailCard extends StatelessWidget {
const TenderDetailCard({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.paleOrange,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
AppStrings.tenderMatchProfile,
style: TextStyle(
fontSize: 12.0,
color: Colors.black87,
fontWeight: FontWeight.w400,
),
),
Text(
AppStrings.tenderMatchPercentageExample,
style: TextStyle(
fontSize: 12.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.jellyBean,
),
),
],
),
SizedBox(height: 6.0.h()),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: 0.75,
backgroundColor: AppColors.grey20,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.jellyBean),
minHeight: 6,
),
),
SizedBox(height: 16.0.h()),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.warning_rounded, color: Colors.red),
SizedBox(width: 8.0.w()),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
AppStrings.tenderIncompleteResume,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0,
color: Colors.black87,
),
),
SizedBox(height: 4.0),
Text(
AppStrings.tenderNoExperience,
style: TextStyle(
fontSize: 14.0,
color: Colors.black54,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
],
),
);
}
}
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/views/detail/widgets/status_tag.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_info_section.dart';
import 'package:tm_app/views/detail/widgets/tender_document_section.dart';
import 'package:tm_app/views/detail/widgets/tender_location_section.dart';
class TenderDetailHeader extends StatelessWidget {
final bool isBig;
const TenderDetailHeader({required this.isBig, super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
border: Border.all(width: 0.5, color: AppColors.grey50),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDateAndStatus(),
SizedBox(height: 12.0.h()),
_buildTitle(),
SizedBox(height: 10.0.h()),
const Divider(),
SizedBox(height: 5.0.h()),
TenderDetailInfoSection(isBig: isBig,),
TenderLocationSection(),
SizedBox(height: 16.0.h()),
TenderDocumentSection(),
],
),
);
}
Widget _buildDateAndStatus() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppStrings.tenderDateExample,
style: TextStyle(
color: AppColors.grey60,
fontWeight: FontWeight.w400,
fontSize: 12.0.sp(),
),
),
const StatusTag(
text: AppStrings.tenderStatusOpen,
textColor: AppColors.cyanTeal,
backgroundColor: AppColors.cyanAqua,
),
],
);
Widget _buildTitle() => Text(
AppStrings.tenderTitleExample,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 18.0.sp(),
),
);
}
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/views/detail/widgets/deadline_item.dart';
import 'package:tm_app/views/detail/widgets/info_item.dart';
class TenderDetailInfoSection extends StatelessWidget {
final bool isBig;
const TenderDetailInfoSection({required this.isBig ,super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoItem(
title: AppStrings.tenderIdLabel,
value: AppStrings.tenderIdExample,
),
DeadlineItem(
title: AppStrings.tenderDeadlineLabel,
approvalText: AppStrings.tenderApprovalText,
approvalDate: AppStrings.tenderApprovalDateExample,
submissionText: AppStrings.tenderSubmissionText,
submissionDate: AppStrings.tenderSubmissionDateExample, isBig: isBig,
),
InfoItem(
title: AppStrings.tenderClientLabel,
value: AppStrings.tenderClientExample,
),
InfoItem(
title: AppStrings.tenderDeliveryLocationsLabel,
value: AppStrings.tenderDeliveryLocationExample,
),
InfoItem(
title: AppStrings.tenderReferenceNumberLabel,
value: AppStrings.tenderReferenceNumberExample,
),
],
);
}
}
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/utils/size_config.dart';
class TenderDocumentSection extends StatelessWidget {
const TenderDocumentSection({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SvgPicture.asset(AssetsManager.export),
SizedBox(height: 6.0.h()),
Text(
AppStrings.tenderPdfDocument,
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 16.0.sp(),
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
],
);
}
}
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
class TenderLocationSection extends StatelessWidget {
const TenderLocationSection({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 45.0.h(),
child: Text(
AppStrings.tenderReferenceNumberLabel,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
),
Row(
children: [
SvgPicture.asset(
AssetsManager.location,
height: 20.0.h(),
width: 20.0.w(),
),
SizedBox(width: 4.0.w()),
Text(
AppStrings.tenderLocationCountry,
style: TextStyle(
color: AppColors.grey70,
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
),
),
SizedBox(width: 4.0.w()),
Image.asset(
AssetsManager.seFlag,
height: 20.0.h(),
width: 20.0.w(),
),
],
),
SizedBox(height: 8.0.h()),
Text(
AppStrings.tenderLocationLabel,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0.sp()),
),
SizedBox(height: 8.0.h()),
Text(
AppStrings.locationDescription,
style: TextStyle(fontWeight: FontWeight.w400, fontSize: 14.0.sp()),
),
SizedBox(height: 16.0.h()),
],
),
);
}
}
+94
View File
@@ -0,0 +1,94 @@
// lib/views/login_desktop_screen.dart
import 'package:flutter/material.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import '../../core/constants/assets.dart';
import '../../core/constants/strings.dart';
import '../../core/utils/size_config.dart';
import 'widgets/widgets.dart';
import '../../view_models/login_view_model.dart';
class LoginDesktopScreen extends StatefulWidget {
const LoginDesktopScreen({super.key});
@override
State<LoginDesktopScreen> createState() => _LoginDesktopScreenState();
}
class _LoginDesktopScreenState extends State<LoginDesktopScreen> {
late final LoginViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = LoginViewModel();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: SizedBox(
width: 120.0.w(),
child: ListenableBuilder(
listenable: _viewModel,
builder: (context, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: _viewModel.usernameController,
focusNode: _viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
fieldHeight: 60.0.h(),
borderRedus: 4.0.w(),
prefixIconPadding: 4.0.w(),
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: _viewModel.passwordController,
focusNode: _viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
prefixIconPadding: 4.0.w(),
isPassword: true,
obscureText: _viewModel.obscurePassword,
fieldHeight: 60.0.h(),
onToggleVisibility: _viewModel.togglePasswordVisibility,
borderRedus: 4.0.w(),
),
SizedBox(height: 32.0.h()),
BaseButton(
isEnabled: _viewModel.isLoginEnabled,
onPressed: _viewModel.isLoginEnabled
? () => HomeRouteData().go(context)
: null,
),
],
);
},
),
),
),
),
);
}
}
+88
View File
@@ -0,0 +1,88 @@
// lib/views/login_screen.dart
import 'package:flutter/material.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import '../../core/constants/assets.dart';
import '../../core/constants/strings.dart';
import '../../core/utils/size_config.dart';
import 'widgets/widgets.dart';
import '../../view_models/login_view_model.dart';
class LoginMobileScreen extends StatefulWidget {
const LoginMobileScreen({super.key});
@override
State<LoginMobileScreen> createState() => _LoginMobileScreenState();
}
class _LoginMobileScreenState extends State<LoginMobileScreen> {
late final LoginViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = LoginViewModel();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: ListenableBuilder(
listenable: _viewModel,
builder: (context, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: _viewModel.usernameController,
focusNode: _viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: _viewModel.passwordController,
focusNode: _viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
isPassword: true,
obscureText: _viewModel.obscurePassword,
onToggleVisibility: _viewModel.togglePasswordVisibility,
),
SizedBox(height: 32.0.h()),
BaseButton(
isEnabled: _viewModel.isLoginEnabled,
onPressed: _viewModel.isLoginEnabled
? () {
HomeRouteData().go(context);
}
: null,
),
],
);
},
),
),
),
);
}
}
+15 -94
View File
@@ -1,103 +1,24 @@
// lib/views/login_screen.dart
import 'package:flutter/material.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/core/utils/display_size_detector.dart';
import 'package:tm_app/views/login/login_desktop_screen.dart';
import 'package:tm_app/views/login/login_mobile_screen.dart';
import 'package:tm_app/views/login/login_tablet_screen.dart';
import '../../core/constants/assets.dart';
import '../../core/constants/strings.dart';
import '../../core/utils/size_config.dart';
import 'widgets/widgets.dart';
class LoginScreen extends StatefulWidget {
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _usernameFocus = FocusNode();
final _passwordFocus = FocusNode();
bool _obscurePassword = true;
bool get _isLoginEnabled =>
_usernameController.text.isNotEmpty &&
_passwordController.text.isNotEmpty;
@override
void initState() {
super.initState();
_usernameController.addListener(_updateState);
_passwordController.addListener(_updateState);
_usernameFocus.addListener(_updateState);
_passwordFocus.addListener(_updateState);
}
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
_usernameFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
void _updateState() => setState(() {});
void _togglePasswordVisibility() =>
setState(() => _obscurePassword = !_obscurePassword);
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const LoginLogo(),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: _usernameController,
focusNode: _usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
),
final displaySize = MediaQuery.of(context).size.displaySize;
SizedBox(height: 16.0.h()),
LoginTextField(
controller: _passwordController,
focusNode: _passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
isPassword: true,
obscureText: _obscurePassword,
onToggleVisibility: _togglePasswordVisibility,
),
SizedBox(height: 32.0.h()),
BaseButton(
isEnabled: _isLoginEnabled,
onPressed:
_isLoginEnabled
? () {
HomeRouteData().go(context);
}
: null,
),
],
),
),
),
);
switch (displaySize) {
case DisplaySize.large:
case DisplaySize.medium:
return const LoginDesktopScreen();
case DisplaySize.small:
return const LoginTabletScreen();
case DisplaySize.extraSmall:
return const LoginMobileScreen();
}
}
}
+94
View File
@@ -0,0 +1,94 @@
// lib/views/login_tablet_screen.dart
import 'package:flutter/material.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import '../../core/constants/assets.dart';
import '../../core/constants/strings.dart';
import '../../core/utils/size_config.dart';
import 'widgets/widgets.dart';
import '../../view_models/login_view_model.dart';
class LoginTabletScreen extends StatefulWidget {
const LoginTabletScreen({super.key});
@override
State<LoginTabletScreen> createState() => _LoginTabletScreenState();
}
class _LoginTabletScreenState extends State<LoginTabletScreen> {
late final LoginViewModel _viewModel;
@override
void initState() {
super.initState();
_viewModel = LoginViewModel();
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: SizedBox(
width: 120.0.w(),
child: ListenableBuilder(
listenable: _viewModel,
builder: (context, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LoginLogo(width: 190.0.w(), height: 88.0.h()),
SizedBox(height: 32.0.h()),
const LoginTitle(),
SizedBox(height: 32.0.h()),
LoginTextField(
controller: _viewModel.usernameController,
focusNode: _viewModel.usernameFocus,
label: AppStrings.usernameLabel,
iconPath: AssetsManager.userIcon,
fieldHeight: 60.0.h(),
borderRedus: 4.0.w(),
prefixIconPadding: 4.0.w(),
),
SizedBox(height: 16.0.h()),
LoginTextField(
controller: _viewModel.passwordController,
focusNode: _viewModel.passwordFocus,
label: AppStrings.passwordLabel,
iconPath: AssetsManager.passwordIcon,
prefixIconPadding: 4.0.w(),
isPassword: true,
obscureText: _viewModel.obscurePassword,
fieldHeight: 60.0.h(),
onToggleVisibility: _viewModel.togglePasswordVisibility,
borderRedus: 4.0.w(),
),
SizedBox(height: 32.0.h()),
BaseButton(
isEnabled: _viewModel.isLoginEnabled,
onPressed: _viewModel.isLoginEnabled
? () => HomeRouteData().go(context)
: null,
),
],
);
},
),
),
),
),
);
}
}
+6 -4
View File
@@ -1,13 +1,15 @@
import 'package:flutter/material.dart';
import '../../../core/constants/assets.dart';
import '../../../core/utils/size_config.dart';
// ignore: must_be_immutable
class LoginLogo extends StatelessWidget {
const LoginLogo({super.key});
LoginLogo({required this.width,required this.height,super.key});
double width;
double height;
@override
Widget build(BuildContext context) {
return Image.asset(AssetsManager.logo, width: 190.0.w(), height: 88.0.h());
return Image.asset(AssetsManager.logo, );
}
}
+51 -38
View File
@@ -12,6 +12,9 @@ class LoginTextField extends StatelessWidget {
final String iconPath;
final bool isPassword;
final bool obscureText;
final double? borderRedus;
final double? fieldHeight;
final double? prefixIconPadding;
final VoidCallback? onToggleVisibility;
const LoginTextField({
@@ -22,52 +25,62 @@ class LoginTextField extends StatelessWidget {
super.key,
this.isPassword = false,
this.obscureText = false,
this.fieldHeight,
this.onToggleVisibility,
this.borderRedus,
this.prefixIconPadding,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
focusNode: focusNode,
obscureText: isPassword ? obscureText : false,
decoration: InputDecoration(
labelText: label,
prefixIcon: Padding(
padding: EdgeInsets.all(12.0.w()),
child: SvgPicture.asset(
iconPath,
width: 24.0.w(),
height: 24.0.h(),
colorFilter: ColorFilter.mode(
focusNode.hasFocus ? Colors.black87 : Colors.grey,
BlendMode.srcIn,
return SizedBox(
height: fieldHeight ?? 55.0.h(),
child: TextField(
controller: controller,
focusNode: focusNode,
obscureText: isPassword ? obscureText : false,
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(
vertical: (fieldHeight != null ? (fieldHeight! / 2.8) : 16.0).h(),
horizontal: 12.0.w(),
),
labelText: label,
prefixIcon: Padding(
padding: EdgeInsets.all(prefixIconPadding ?? 12.0.w()),
child: SvgPicture.asset(
iconPath,
width: 24.0.w(),
height: 24.0.h(),
colorFilter: ColorFilter.mode(
focusNode.hasFocus ? Colors.black87 : Colors.grey,
BlendMode.srcIn,
),
),
),
),
suffixIcon:
isPassword
? IconButton(
onPressed: onToggleVisibility,
icon: SvgPicture.asset(
obscureText
? AssetsManager.passwordVisibility
: AssetsManager.passwordVisibilityOff,
width: 24.0.w(),
height: 24.0.h(),
colorFilter: ColorFilter.mode(
focusNode.hasFocus ? Colors.black87 : Colors.grey,
BlendMode.srcIn,
suffixIcon:
isPassword
? IconButton(
onPressed: onToggleVisibility,
icon: SvgPicture.asset(
obscureText
? AssetsManager.passwordVisibility
: AssetsManager.passwordVisibilityOff,
width: 24.0.w(),
height: 24.0.h(),
colorFilter: ColorFilter.mode(
focusNode.hasFocus ? Colors.black87 : Colors.grey,
BlendMode.srcIn,
),
),
),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0.w()),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.borderBlue),
borderRadius: BorderRadius.circular(12.0.w()),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(borderRedus ?? 12.0.w()),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.borderBlue),
borderRadius: BorderRadius.circular(borderRedus ?? 12.0.w()),
),
),
),
);