add checkout page

This commit is contained in:
mohamadmahdi jebeli 2025-08-17 13:33:49 +03:30
parent af14d2d7fc
commit e1c0f90818
16 changed files with 1364 additions and 51 deletions

View File

@ -0,0 +1,3 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.6825 15.0292L11.5725 10.1392C12.15 9.56167 12.15 8.61667 11.5725 8.03917L6.6825 3.14917" stroke="#2196F3" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@ -0,0 +1,8 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 6.375H10.125" stroke="#2A2926" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.5 12.375H6" stroke="#2A2926" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.875 12.375H10.875" stroke="#2A2926" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 9.0225V12.0825C16.5 14.715 15.8325 15.375 13.17 15.375H4.83C2.1675 15.375 1.5 14.715 1.5 12.0825V5.9175C1.5 3.285 2.1675 2.625 4.83 2.625H10.125" stroke="#2A2926" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.375 4.6875H16.5" stroke="#2A2926" stroke-linecap="round"/>
<path d="M14.4375 6.75V2.625" stroke="#2A2926" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 824 B

View File

@ -0,0 +1,3 @@
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 3.47059L4.83333 7L10.5 1" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -0,0 +1,8 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.1" x="0.295898" width="28" height="28" rx="4" fill="#9D9D9B"/>
<path d="M20.9375 21.5H7.0625C6.755 21.5 6.5 21.245 6.5 20.9375C6.5 20.63 6.755 20.375 7.0625 20.375H20.9375C21.245 20.375 21.5 20.63 21.5 20.9375C21.5 21.245 21.245 21.5 20.9375 21.5Z" fill="#9D9D9B"/>
<path opacity="0.4" d="M20.4427 15.275L15.0202 20.6975C13.9552 21.7625 12.2377 21.7625 11.1802 20.705L7.72266 17.2475L16.9927 7.97754L20.4502 11.435C21.5077 12.4925 21.5077 14.21 20.4427 15.275Z" fill="#9D9D9B"/>
<path d="M16.9924 7.97762L7.71492 17.2476L7.03242 16.5651C5.97492 15.5076 5.97492 13.7901 7.03992 12.7251L12.4624 7.30262C13.5274 6.23762 15.2449 6.23762 16.3024 7.29512L16.9924 7.97762Z" fill="#9D9D9B"/>
<path d="M14.6677 18.1999L13.6552 19.2124C13.4452 19.4224 13.1077 19.4224 12.8977 19.2124C12.6877 19.0024 12.6877 18.6649 12.8977 18.4549L13.9102 17.4424C14.1202 17.2324 14.4577 17.2324 14.6677 17.4424C14.8777 17.6524 14.8777 17.9899 14.6677 18.1999Z" fill="#9D9D9B"/>
<path d="M17.9525 14.915L15.935 16.9325C15.725 17.1425 15.3875 17.1425 15.1775 16.9325C14.9675 16.7225 14.9675 16.385 15.1775 16.175L17.195 14.1575C17.405 13.9475 17.7425 13.9475 17.9525 14.1575C18.155 14.3675 18.155 14.705 17.9525 14.915Z" fill="#9D9D9B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,8 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.1" x="0.295898" width="28" height="28" rx="4" fill="#2196F3"/>
<path d="M20.9375 21.5H7.0625C6.755 21.5 6.5 21.245 6.5 20.9375C6.5 20.63 6.755 20.375 7.0625 20.375H20.9375C21.245 20.375 21.5 20.63 21.5 20.9375C21.5 21.245 21.245 21.5 20.9375 21.5Z" fill="#2196F3"/>
<path opacity="0.4" d="M20.4424 15.275L15.0199 20.6975C13.9549 21.7625 12.2374 21.7625 11.1799 20.705L7.72241 17.2475L16.9924 7.97754L20.4499 11.435C21.5074 12.4925 21.5074 14.21 20.4424 15.275Z" fill="#2196F3"/>
<path d="M16.9924 7.97762L7.71492 17.2476L7.03242 16.5651C5.97492 15.5076 5.97492 13.7901 7.03992 12.7251L12.4624 7.30262C13.5274 6.23762 15.2449 6.23762 16.3024 7.29512L16.9924 7.97762Z" fill="#2196F3"/>
<path d="M14.6675 18.1999L13.655 19.2124C13.445 19.4224 13.1075 19.4224 12.8975 19.2124C12.6875 19.0024 12.6875 18.6649 12.8975 18.4549L13.91 17.4424C14.12 17.2324 14.4575 17.2324 14.6675 17.4424C14.8775 17.6524 14.8775 17.9899 14.6675 18.1999Z" fill="#2196F3"/>
<path d="M17.9525 14.915L15.935 16.9325C15.725 17.1425 15.3875 17.1425 15.1775 16.9325C14.9675 16.7225 14.9675 16.385 15.1775 16.175L17.195 14.1575C17.405 13.9475 17.7425 13.9475 17.9525 14.1575C18.155 14.3675 18.155 14.705 17.9525 14.915Z" fill="#2196F3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,7 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.1" x="0.884766" width="28" height="28" rx="4" fill="#9D9D9B"/>
<path d="M18.1875 21.875C18.9124 21.875 19.5 21.2874 19.5 20.5625C19.5 19.8376 18.9124 19.25 18.1875 19.25C17.4626 19.25 16.875 19.8376 16.875 20.5625C16.875 21.2874 17.4626 21.875 18.1875 21.875Z" fill="#9D9D9B"/>
<path d="M12.1875 21.875C12.9124 21.875 13.5 21.2874 13.5 20.5625C13.5 19.8376 12.9124 19.25 12.1875 19.25C11.4626 19.25 10.875 19.8376 10.875 20.5625C10.875 21.2874 11.4626 21.875 12.1875 21.875Z" fill="#9D9D9B"/>
<path opacity="0.4" d="M9.63 7.955L9.48 9.7925C9.45 10.145 9.7275 10.4375 10.08 10.4375H21.5625C21.8775 10.4375 22.14 10.1975 22.1625 9.8825C22.26 8.555 21.2475 7.475 19.92 7.475H10.7175C10.6425 7.145 10.4925 6.83 10.26 6.5675C9.89249 6.17 9.3675 5.9375 8.8275 5.9375H7.5C7.1925 5.9375 6.9375 6.1925 6.9375 6.5C6.9375 6.8075 7.1925 7.0625 7.5 7.0625H8.805C9.0375 7.0625 9.255 7.16 9.4125 7.325C9.57 7.4975 9.645 7.7225 9.63 7.955Z" fill="#9D9D9B"/>
<path d="M21.3825 11.5625H9.87754C9.56254 11.5625 9.30754 11.8025 9.27754 12.11L9.00754 15.3725C8.90254 16.6475 9.90754 17.75 11.19 17.75H19.53C20.655 17.75 21.645 16.8275 21.7275 15.7025L21.975 12.2C22.005 11.855 21.735 11.5625 21.3825 11.5625Z" fill="#9D9D9B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,7 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.1" x="0.884766" width="28" height="28" rx="4" fill="#2196F3"/>
<path d="M18.1875 21.875C18.9124 21.875 19.5 21.2874 19.5 20.5625C19.5 19.8376 18.9124 19.25 18.1875 19.25C17.4626 19.25 16.875 19.8376 16.875 20.5625C16.875 21.2874 17.4626 21.875 18.1875 21.875Z" fill="#2196F3"/>
<path d="M12.1875 21.875C12.9124 21.875 13.5 21.2874 13.5 20.5625C13.5 19.8376 12.9124 19.25 12.1875 19.25C11.4626 19.25 10.875 19.8376 10.875 20.5625C10.875 21.2874 11.4626 21.875 12.1875 21.875Z" fill="#2196F3"/>
<path opacity="0.4" d="M9.63 7.955L9.48 9.7925C9.45 10.145 9.7275 10.4375 10.08 10.4375H21.5625C21.8775 10.4375 22.14 10.1975 22.1625 9.8825C22.26 8.555 21.2475 7.475 19.92 7.475H10.7175C10.6425 7.145 10.4925 6.83 10.26 6.5675C9.89249 6.17 9.3675 5.9375 8.8275 5.9375H7.5C7.1925 5.9375 6.9375 6.1925 6.9375 6.5C6.9375 6.8075 7.1925 7.0625 7.5 7.0625H8.805C9.0375 7.0625 9.255 7.16 9.4125 7.325C9.57 7.4975 9.645 7.7225 9.63 7.955Z" fill="#2196F3"/>
<path d="M21.3825 11.5625H9.87754C9.56254 11.5625 9.30754 11.8025 9.27754 12.11L9.00754 15.3725C8.90254 16.6475 9.90754 17.75 11.19 17.75H19.53C20.655 17.75 21.645 16.8275 21.7275 15.7025L21.975 12.2C22.005 11.855 21.735 11.5625 21.3825 11.5625Z" fill="#2196F3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 12.5C19.5 11.12 20.62 10 22 10V9C22 5 21 4 17 4H7C3 4 2 5 2 9V9.5C3.38 9.5 4.5 10.62 4.5 12C4.5 13.38 3.38 14.5 2 14.5V15C2 19 3 20 7 20H17C21 20 22 19 22 15C20.62 15 19.5 13.88 19.5 12.5Z" stroke="#9D9D9B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 14.75L15 8.75" stroke="#9D9D9B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.9945 14.75H15.0035" stroke="#9D9D9B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.99451 9.25H9.00349" stroke="#9D9D9B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 736 B

View File

@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.625 9.375C14.625 8.34 15.465 7.5 16.5 7.5V6.75C16.5 3.75 15.75 3 12.75 3H5.25C2.25 3 1.5 3.75 1.5 6.75V7.125C2.535 7.125 3.375 7.965 3.375 9C3.375 10.035 2.535 10.875 1.5 10.875V11.25C1.5 14.25 2.25 15 5.25 15H12.75C15.75 15 16.5 14.25 16.5 11.25C15.465 11.25 14.625 10.41 14.625 9.375Z" stroke="#9D9D9B" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.75 11.0625L11.25 6.5625" stroke="#9D9D9B" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.2459 11.0625H11.2526" stroke="#9D9D9B" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.74588 6.9375H6.75262" stroke="#9D9D9B" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 774 B

View File

@ -62,6 +62,10 @@ class $AssetsIconsGen {
/// File path: assets/icons/arrow-left.svg /// File path: assets/icons/arrow-left.svg
SvgGenImage get arrowLeft => const SvgGenImage('assets/icons/arrow-left.svg'); SvgGenImage get arrowLeft => const SvgGenImage('assets/icons/arrow-left.svg');
/// File path: assets/icons/arrow-right.svg
SvgGenImage get arrowRight =>
const SvgGenImage('assets/icons/arrow-right.svg');
/// File path: assets/icons/back.svg /// File path: assets/icons/back.svg
SvgGenImage get back => const SvgGenImage('assets/icons/back.svg'); SvgGenImage get back => const SvgGenImage('assets/icons/back.svg');
@ -73,12 +77,19 @@ class $AssetsIconsGen {
SvgGenImage get calendarTick => SvgGenImage get calendarTick =>
const SvgGenImage('assets/icons/calendar-tick.svg'); const SvgGenImage('assets/icons/calendar-tick.svg');
/// File path: assets/icons/card-add.svg
SvgGenImage get cardAdd => const SvgGenImage('assets/icons/card-add.svg');
/// File path: assets/icons/card-pos.svg /// File path: assets/icons/card-pos.svg
SvgGenImage get cardPos => const SvgGenImage('assets/icons/card-pos.svg'); SvgGenImage get cardPos => const SvgGenImage('assets/icons/card-pos.svg');
/// File path: assets/icons/category-2.svg /// File path: assets/icons/category-2.svg
SvgGenImage get category2 => const SvgGenImage('assets/icons/category-2.svg'); SvgGenImage get category2 => const SvgGenImage('assets/icons/category-2.svg');
/// File path: assets/icons/check-alternative.svg
SvgGenImage get checkAlternative =>
const SvgGenImage('assets/icons/check-alternative.svg');
/// File path: assets/icons/clander.svg /// File path: assets/icons/clander.svg
SvgGenImage get clander => const SvgGenImage('assets/icons/clander.svg'); SvgGenImage get clander => const SvgGenImage('assets/icons/clander.svg');
@ -88,6 +99,14 @@ class $AssetsIconsGen {
/// File path: assets/icons/coin.svg /// File path: assets/icons/coin.svg
SvgGenImage get coin => const SvgGenImage('assets/icons/coin.svg'); SvgGenImage get coin => const SvgGenImage('assets/icons/coin.svg');
/// File path: assets/icons/delivery off.svg
SvgGenImage get deliveryOff =>
const SvgGenImage('assets/icons/delivery off.svg');
/// File path: assets/icons/delivery on.svg
SvgGenImage get deliveryOn =>
const SvgGenImage('assets/icons/delivery on.svg');
/// File path: assets/icons/dislike.svg /// File path: assets/icons/dislike.svg
SvgGenImage get dislike => const SvgGenImage('assets/icons/dislike.svg'); SvgGenImage get dislike => const SvgGenImage('assets/icons/dislike.svg');
@ -178,6 +197,13 @@ class $AssetsIconsGen {
/// File path: assets/icons/ph_cheese.svg /// File path: assets/icons/ph_cheese.svg
SvgGenImage get phCheese => const SvgGenImage('assets/icons/ph_cheese.svg'); SvgGenImage get phCheese => const SvgGenImage('assets/icons/ph_cheese.svg');
/// File path: assets/icons/pick up off.svg
SvgGenImage get pickUpOff =>
const SvgGenImage('assets/icons/pick up off.svg');
/// File path: assets/icons/pickup on.svg
SvgGenImage get pickupOn => const SvgGenImage('assets/icons/pickup on.svg');
/// File path: assets/icons/profile 2.svg /// File path: assets/icons/profile 2.svg
SvgGenImage get profile2 => const SvgGenImage('assets/icons/profile 2.svg'); SvgGenImage get profile2 => const SvgGenImage('assets/icons/profile 2.svg');
@ -241,6 +267,14 @@ class $AssetsIconsGen {
/// File path: assets/icons/tick.svg /// File path: assets/icons/tick.svg
SvgGenImage get tick => const SvgGenImage('assets/icons/tick.svg'); SvgGenImage get tick => const SvgGenImage('assets/icons/tick.svg');
/// File path: assets/icons/ticket-discount.svg
SvgGenImage get ticketDiscount =>
const SvgGenImage('assets/icons/ticket-discount.svg');
/// File path: assets/icons/ticket-discount2.svg
SvgGenImage get ticketDiscount2 =>
const SvgGenImage('assets/icons/ticket-discount2.svg');
/// File path: assets/icons/timer-pause.svg /// File path: assets/icons/timer-pause.svg
SvgGenImage get timerPause => SvgGenImage get timerPause =>
const SvgGenImage('assets/icons/timer-pause.svg'); const SvgGenImage('assets/icons/timer-pause.svg');
@ -268,14 +302,19 @@ class $AssetsIconsGen {
arrowDownBlack, arrowDownBlack,
arrowDown, arrowDown,
arrowLeft, arrowLeft,
arrowRight,
back, back,
calendarTick2, calendarTick2,
calendarTick, calendarTick,
cardAdd,
cardPos, cardPos,
category2, category2,
checkAlternative,
clander, clander,
clock, clock,
coin, coin,
deliveryOff,
deliveryOn,
dislike, dislike,
down, down,
elementEqual, elementEqual,
@ -302,6 +341,8 @@ class $AssetsIconsGen {
next, next,
notificationBing, notificationBing,
phCheese, phCheese,
pickUpOff,
pickupOn,
profile2, profile2,
profile, profile,
receiptDiscount2, receiptDiscount2,
@ -321,6 +362,8 @@ class $AssetsIconsGen {
star, star,
stashStarsLight, stashStarsLight,
tick, tick,
ticketDiscount,
ticketDiscount2,
timerPause, timerPause,
timerStart, timerStart,
timer, timer,

View File

@ -21,4 +21,6 @@ class LightAppColors{
static const cardBackground = Color.fromARGB(255, 242, 242, 241); static const cardBackground = Color.fromARGB(255, 242, 242, 241);
static const offerTimer = Color.fromARGB(255, 244, 67, 54); static const offerTimer = Color.fromARGB(255, 244, 67, 54);
static const offerCardDetail = Color.fromARGB(255, 73, 69, 79); static const offerCardDetail = Color.fromARGB(255, 73, 69, 79);
static const divider = Color.fromARGB(255, 189, 189, 188);
static const textPrice = Color.fromARGB(255, 85, 84, 81);
} }

View File

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:dots_indicator/dots_indicator.dart'; import 'package:dots_indicator/dots_indicator.dart';
@ -21,6 +23,14 @@ class _DiscoverState extends State<Discover> with TickerProviderStateMixin {
late AnimationController _staggeredController; late AnimationController _staggeredController;
late List<Animation<double>> _staggeredAnimations; late List<Animation<double>> _staggeredAnimations;
final Map<String, bool> _filters = {
'Top 10 Offers': true,
'Flash Sale': true,
'special discount': false,
'Occasion Specials': true,
'First Purchase': false,
};
final List<String> categoryIcons = [ final List<String> categoryIcons = [
Assets.icons.stashStarsLight.path, Assets.icons.stashStarsLight.path,
Assets.icons.shoppingCart.path, Assets.icons.shoppingCart.path,
@ -83,8 +93,100 @@ class _DiscoverState extends State<Discover> with TickerProviderStateMixin {
); );
} }
void _showFilterMenu() {
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black.withOpacity(0.1),
transitionDuration: const Duration(milliseconds: 400),
pageBuilder: (context, animation1, animation2) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 250,
margin: const EdgeInsets.only(top: kToolbarHeight + 65, right: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
spreadRadius: 5,
)
],
),
child: StatefulBuilder(
builder: (context, setDialogState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: _filters.keys.map((String filterName) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
setDialogState(() {
setState(() {
_filters[filterName] = !_filters[filterName]!;
});
});
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 18.0, vertical: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
filterName,
style: TextStyle(fontSize: 16),
),
_buildCustomCheckbox(_filters[filterName]!),
],
),
),
),
);
}).toList(),
);
},
),
),
),
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
final curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
return ScaleTransition(
scale: Tween<double>(begin: 0.8, end: 1.0).animate(curvedAnimation),
alignment: Alignment.topRight,
child: FadeTransition(
opacity: curvedAnimation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.2, -0.2),
end: Offset.zero,
).animate(curvedAnimation),
child: child,
),
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// You can use the _filters map to conditionally show/hide sections
// Example: if (_filters['Flash Sale']!) ... [ ... Flash Sale Section ... ]
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: _buildAppBar(), appBar: _buildAppBar(),
@ -106,15 +208,20 @@ class _DiscoverState extends State<Discover> with TickerProviderStateMixin {
0, 0,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildAnimatedSection(_buildSectionTitle("what's on your mind?"), 1), _buildAnimatedSection(
_buildSectionTitle("what's on your mind?"), 1),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAnimatedSection(_buildCategoryIcons(), 2), _buildAnimatedSection(_buildCategoryIcons(), 2),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildAnimatedSection(_buildSectionTitle("Top 10 Discount & Offers"), 3), _buildAnimatedSection(
_buildSectionTitle("Top 10 Discount & Offers"), 3),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAnimatedSection(_buildTopOffersSection(), 4), _buildAnimatedSection(_buildTopOffersSection(), 4),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildAnimatedSection(_buildSectionTitle("Flash Sale"), 5), _buildAnimatedSection(
_buildSectionTitle("Flash Sale",
showSeeAll: true, onSeeAllTap: () {}),
5),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAnimatedSection(_buildFlashSaleSection(), 6), _buildAnimatedSection(_buildFlashSaleSection(), 6),
const SizedBox(height: 24), const SizedBox(height: 24),
@ -122,7 +229,10 @@ class _DiscoverState extends State<Discover> with TickerProviderStateMixin {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAnimatedSection(_buildSpecialDiscountSection(), 1), _buildAnimatedSection(_buildSpecialDiscountSection(), 1),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildAnimatedSection(_buildSectionTitle("Seasonal Discount"), 2), _buildAnimatedSection(
_buildSectionTitle("Seasonal Discount",
showSeeAll: true, onSeeAllTap: () {}),
2),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAnimatedSection(_buildSeasonalDiscountSection(), 3), _buildAnimatedSection(_buildSeasonalDiscountSection(), 3),
const SizedBox(height: 24), const SizedBox(height: 24),
@ -130,7 +240,10 @@ class _DiscoverState extends State<Discover> with TickerProviderStateMixin {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAnimatedSection(_buildCraftingSomethingSection(), 5), _buildAnimatedSection(_buildCraftingSomethingSection(), 5),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildAnimatedSection(_buildSectionTitle("First Purchase Discount"), 6), _buildAnimatedSection(
_buildSectionTitle("First Purchase Discount",
showSeeAll: true, onSeeAllTap: () {}),
6),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAnimatedSection(_buildFirstPurchaseSection(), 7), _buildAnimatedSection(_buildFirstPurchaseSection(), 7),
const SizedBox(height: 100), const SizedBox(height: 100),
@ -178,15 +291,45 @@ class _DiscoverState extends State<Discover> with TickerProviderStateMixin {
), ),
child: IconButton( child: IconButton(
icon: SvgPicture.asset(Assets.icons.sort.path, color: Colors.white), icon: SvgPicture.asset(Assets.icons.sort.path, color: Colors.white),
onPressed: () { onPressed: _showFilterMenu,
CustomBottomSheet.show(context, [ ),
"Food & Dining", );
"Entertainment & Leisure", }
"Health & Fitness",
"Travel & Transportation", Widget _buildCustomCheckbox(bool isChecked) {
]); return AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: 24,
height: 24,
decoration: BoxDecoration(
color: isChecked ? LightAppColors.primary : Colors.transparent,
border: isChecked
? null
: Border.all(color: Color.fromARGB(255, 89, 93, 98), width: 2),
borderRadius: BorderRadius.circular(6),
),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: FadeTransition(
opacity: animation,
child: child,
),
);
}, },
padding: const EdgeInsets.all(8), child: isChecked
? const Icon(
Icons.check,
color: Colors.white,
size: 18,
key: ValueKey('checked'),
)
: const SizedBox(
key: ValueKey('unchecked'),
),
), ),
); );
} }
@ -363,17 +506,43 @@ class _DiscoverState extends State<Discover> with TickerProviderStateMixin {
); );
} }
Widget _buildSectionTitle(String title) { Widget _buildSectionTitle(String title,
{bool showSeeAll = false, VoidCallback? onSeeAllTap}) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
title, title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.normal), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.normal),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
const Expanded(child: Divider(color: Colors.grey, thickness: 1)), const Expanded(
child: Divider(color: Colors.grey, thickness: 1),
),
if (showSeeAll) ...[
const SizedBox(width: 8),
InkWell(
onTap: onSeeAllTap,
child: Row(
children: [
Text(
'See all',
style: TextStyle(
color: LightAppColors.primary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 7),
SvgPicture.asset(
Assets.icons.arrowRight.path,
),
],
),
),
],
], ],
), ),
); );
@ -626,8 +795,7 @@ class FlashSaleCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final timer = final timer = RemainingTime()
RemainingTime()
..initializeFromExpiry(expiryTimeString: expiryTimeString); ..initializeFromExpiry(expiryTimeString: expiryTimeString);
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -674,8 +842,7 @@ class FlashSaleCard extends StatelessWidget {
), ),
ValueListenableBuilder<int>( ValueListenableBuilder<int>(
valueListenable: timer.remainingSeconds, valueListenable: timer.remainingSeconds,
builder: builder: (context, _, __) => Text(
(context, _, __) => Text(
timer.formatTime(), timer.formatTime(),
style: const TextStyle( style: const TextStyle(
color: LightAppColors.offerTimer, color: LightAppColors.offerTimer,
@ -684,7 +851,9 @@ class FlashSaleCard extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(height: 5,) const SizedBox(
height: 5,
)
], ],
), ),
], ],
@ -731,7 +900,8 @@ class FlashSaleCard extends StatelessWidget {
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
location, location,
style: const TextStyle(color: LightAppColors.offerCardDetail, fontSize: 12), style: const TextStyle(
color: LightAppColors.offerCardDetail, fontSize: 12),
), ),
], ],
), ),
@ -769,7 +939,9 @@ class FlashSaleCard extends StatelessWidget {
], ],
), ),
), ),
const SizedBox(width: 10,), const SizedBox(
width: 10,
),
Text( Text(
'($discountPercent% off)', '($discountPercent% off)',
style: const TextStyle( style: const TextStyle(
@ -800,11 +972,18 @@ class FlashSaleCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SvgPicture.asset(Assets.icons.shoppingCart.path,color: Colors.white,width: 20,), SvgPicture.asset(
const SizedBox(width: 5,), Assets.icons.shoppingCart.path,
color: Colors.white,
width: 20,
),
const SizedBox(
width: 5,
),
const Text( const Text(
"Reservation", "Reservation",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15), style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 15),
), ),
], ],
), ),
@ -987,15 +1166,15 @@ class FirstPurchaseCard extends StatelessWidget {
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
category, category,
style: const TextStyle(color: LightAppColors.nearbyPopuphint, fontSize: 14), style: const TextStyle(
color: LightAppColors.nearbyPopuphint, fontSize: 14),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
discount, discount,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: LightAppColors.nearbyPopuphint color: LightAppColors.nearbyPopuphint),
),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Row( Row(

View File

@ -0,0 +1,709 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
class CheckoutPage extends StatefulWidget {
const CheckoutPage({super.key});
@override
State<CheckoutPage> createState() => _CheckoutPageState();
}
class _CheckoutPageState extends State<CheckoutPage>
with TickerProviderStateMixin {
int _selectedTabIndex = 0;
late AnimationController _deliveryController;
late AnimationController _pickupController;
late Animation<Color?> _deliveryColorAnimation;
late Animation<Color?> _pickupColorAnimation;
late Animation<double> _deliveryScaleAnimation;
late Animation<double> _pickupScaleAnimation;
late Animation<Offset> _deliverySlideAnimation;
late Animation<Offset> _pickupSlideAnimation;
static const Color greyTextMid = Color(0xFF616161);
static const Color greyTextLight = Color(0xFF9E9E9E);
static const Color activeColor = Color(0xFF189CFF);
@override
void initState() {
super.initState();
_deliveryController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
_pickupController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
_deliveryColorAnimation =
ColorTween(begin: greyTextLight, end: activeColor)
.animate(_deliveryController);
_pickupColorAnimation = ColorTween(begin: greyTextLight, end: activeColor)
.animate(_pickupController);
_deliveryScaleAnimation =
Tween<double>(begin: 1.0, end: 1.1).animate(_deliveryController);
_pickupScaleAnimation =
Tween<double>(begin: 1.0, end: 1.1).animate(_pickupController);
_deliverySlideAnimation =
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(0, -0.1))
.animate(_deliveryController);
_pickupSlideAnimation =
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(0, -0.1))
.animate(_pickupController);
if (_selectedTabIndex == 0) {
_deliveryController.forward();
} else {
_pickupController.forward();
}
}
@override
void dispose() {
_deliveryController.dispose();
_pickupController.dispose();
super.dispose();
}
void _onTabSelected(int index) {
setState(() {
_selectedTabIndex = index;
});
if (index == 0) {
_deliveryController.forward();
_pickupController.reverse();
} else {
_pickupController.forward();
_deliveryController.reverse();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: Column(
children: [
_buildTabs(),
Container(height: 1, color: LightAppColors.divider, width: 370),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin:
Offset(_selectedTabIndex == 0 ? 0.5 : -0.5, 0),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: _selectedTabIndex == 0
? _buildDeliveryContent()
: _buildPickupContent(),
),
),
],
),
bottomNavigationBar: _payBar(),
);
}
Widget _buildDeliveryContent() {
return SingleChildScrollView(
key: const ValueKey('delivery'),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
_sectionHeader("Deliver to:",
trailingAction: "Select address", onAction: () {}),
const SizedBox(height: 10),
_selectAddressRow(),
const SizedBox(height: 20),
const DottedDivider(),
const SizedBox(height: 20),
_sectionHeader("Delivery time:"),
const SizedBox(height: 12),
_deliveryTimeCards(),
const SizedBox(height: 20),
const DottedDivider(),
const SizedBox(height: 15),
_sectionHeader("Payment Method:"),
const SizedBox(height: 12),
_clickableCard(
leading: Assets.icons.cardAdd.path,
title: "Add a credit card",
onTap: () {},
),
const SizedBox(height: 24),
const DottedDivider(),
const SizedBox(height: 24),
_sectionHeader("promo code:"),
const SizedBox(height: 12),
_promoCodeField(),
const SizedBox(height: 28),
_priceDetails(),
const SizedBox(height: 40),
],
),
);
}
Widget _buildPickupContent() {
return SingleChildScrollView(
key: const ValueKey('pickup'),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
_sectionHeader("Pickup from:",
trailingAction: "Select address", onAction: () {}),
const SizedBox(height: 10),
_pickupAddressRow("Mall of the Emirates"),
const SizedBox(height: 20),
const DottedDivider(),
const SizedBox(height: 20),
_sectionHeader("Pickup time:",
trailingAction: "Select time", onAction: () {}),
const SizedBox(height: 12),
_pickupTimeRow("Set a time for your Pickup"),
const SizedBox(height: 20),
const DottedDivider(),
const SizedBox(height: 15),
_sectionHeader("Payment Method:"),
const SizedBox(height: 12),
_clickableCard(
leading: Assets.icons.cardAdd.path,
title: "Add a credit card",
onTap: () {},
),
const SizedBox(height: 24),
const DottedDivider(),
const SizedBox(height: 24),
_sectionHeader("promo code:"),
const SizedBox(height: 12),
_promoCodeField(),
const SizedBox(height: 28),
_priceDetails(),
const SizedBox(height: 40),
],
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
elevation: 0,
backgroundColor: Colors.white,
centerTitle: false,
leading: IconButton(
icon: SvgPicture.asset(
Assets.icons.arrowLeft.path,
height: 20,
),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text(
'Checkout',
style: TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.normal,
),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(
color: LightAppColors.divider,
height: 1.0,
),
),
);
}
Widget _buildTabs() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
children: [
_buildAnimatedTabItem(
index: 0,
selectedIcon: Assets.icons.deliveryOn.path,
unselectedIcon: Assets.icons.deliveryOff.path,
label: 'Delivery',
controller: _deliveryController,
colorAnimation: _deliveryColorAnimation,
scaleAnimation: _deliveryScaleAnimation,
slideAnimation: _deliverySlideAnimation,
),
const SizedBox(width: 16),
_buildAnimatedTabItem(
index: 1,
selectedIcon: Assets.icons.pickupOn.path,
unselectedIcon: Assets.icons.pickUpOff.path,
label: 'Pickup',
controller: _pickupController,
colorAnimation: _pickupColorAnimation,
scaleAnimation: _pickupScaleAnimation,
slideAnimation: _pickupSlideAnimation,
),
],
),
);
}
Widget _buildAnimatedTabItem({
required int index,
required String selectedIcon,
required String unselectedIcon,
required String label,
required AnimationController controller,
required Animation<Color?> colorAnimation,
required Animation<double> scaleAnimation,
required Animation<Offset> slideAnimation,
}) {
final bool selected = _selectedTabIndex == index;
return Expanded(
child: InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () => _onTabSelected(index),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.fromLTRB(6, 8, 6, 0),
child: Column(
children: [
AnimatedBuilder(
animation: controller,
builder: (context, child) {
return SlideTransition(
position: slideAnimation,
child: ScaleTransition(
scale: scaleAnimation,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
selected ? selectedIcon : unselectedIcon,
color: colorAnimation.value,
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorAnimation.value,
),
),
],
),
),
);
},
),
const SizedBox(height: 6),
AnimatedContainer(
duration: const Duration(milliseconds: 400),
height: 5,
width: selected ? 100 : 0,
child: selected
? SvgPicture.asset(
Assets.icons.shape.path,
color: LightAppColors.primary,
)
: const SizedBox(),
),
],
),
),
),
);
}
Widget _sectionHeader(String title,
{String? trailingAction, VoidCallback? onAction}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
if (trailingAction != null)
GestureDetector(
onTap: onAction,
child: Text(
trailingAction,
style: const TextStyle(
color: LightAppColors.offerTimer,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
Widget _selectAddressRow() {
return Row(
children: [
SvgPicture.asset(
Assets.icons.location.path,
color: const Color.fromARGB(255, 85, 84, 81),
),
const SizedBox(width: 10),
const Text(
"Select address",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: greyTextMid,
),
),
],
);
}
Widget _pickupAddressRow(String address) {
return Row(
children: [
SvgPicture.asset(
Assets.icons.location.path,
color: const Color.fromARGB(255, 85, 84, 81),
),
const SizedBox(width: 10),
Text(
address,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: greyTextMid,
),
),
],
);
}
Widget _pickupTimeRow(String time) {
return Text(
time,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: greyTextMid,
),
);
}
Widget _deliveryTimeCards() {
return Column(
children: [
_clickableCard(
customChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"ASAP",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.black),
),
SizedBox(height: 4),
Text(
"Delivered directly to you",
style: TextStyle(
fontSize: 13, color: greyTextLight, height: 1.1),
),
],
),
onTap: () {},
),
const SizedBox(height: 12),
_clickableCard(
customChild: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text("Schedule",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.black)),
SizedBox(height: 4),
Text("Select a time",
style: TextStyle(
fontSize: 13,
color: greyTextLight,
height: 1.1)),
],
),
),
SvgPicture.asset(
Assets.icons.arrowRight.path,
height: 20,
color: Colors.black,
),
],
),
onTap: () {},
),
],
);
}
Widget _clickableCard({
String? leading,
String? title,
Widget? customChild,
VoidCallback? onTap,
}) {
return InkWell(
borderRadius: BorderRadius.circular(6),
onTap: onTap,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: LightAppColors.divider),
),
child: customChild ??
Row(
children: [
if (leading != null) SvgPicture.asset(leading),
if (leading != null) const SizedBox(width: 10),
if (title != null)
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: greyTextMid,
),
),
),
const Icon(Icons.arrow_forward_ios,
size: 16, color: greyTextLight),
],
),
),
);
}
Widget _promoCodeField() {
return TextField(
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
prefixIcon: Padding(
padding: const EdgeInsets.all(15.0),
child: SvgPicture.asset(
Assets.icons.ticketDiscount2.path,
),
),
hintText: 'Enter your promo code here',
hintStyle: const TextStyle(color: greyTextLight, fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: LightAppColors.divider),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: LightAppColors.divider),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: LightAppColors.divider),
),
fillColor: Colors.white,
filled: true,
isDense: true,
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
),
);
}
Widget _priceDetails() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const DottedDivider(),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
"Price Details",
style: TextStyle(
color: LightAppColors.textPrice,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
"1 Item",
style: TextStyle(
color: LightAppColors.textPrice,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 12),
const DottedDivider(),
const SizedBox(height: 14),
_priceRow("Subtotal", "27.900"),
const SizedBox(height: 12),
_priceRow("Sales tax", "0.000"),
const SizedBox(height: 14),
const DottedDivider(),
const SizedBox(height: 14),
_priceRow("Total", "27.900", isTotal: true),
],
);
}
Widget _priceRow(String label, String amount, {bool isTotal = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 15,
color:
isTotal ? LightAppColors.textPrice : LightAppColors.popupText,
fontWeight: isTotal ? FontWeight.w600 : FontWeight.normal,
),
),
Text(
"AED $amount",
style: TextStyle(
fontSize: 15,
color:
isTotal ? LightAppColors.textPrice : LightAppColors.popupText,
fontWeight: isTotal ? FontWeight.w700 : FontWeight.w600,
),
),
],
);
}
Widget _payBar() {
return SafeArea(
top: false,
child: Container(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
decoration: const BoxDecoration(color: Colors.white),
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: LightAppColors.offerTimer,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
onPressed: () {},
child: const Text(
"Pay AED 27.900",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.2,
),
),
),
),
),
);
}
}
class DottedDivider extends StatelessWidget {
final double height;
final Color color;
final double dashWidth;
final double dashSpace;
const DottedDivider({
super.key,
this.height = 1,
this.color = const Color(0xFFE0E0E0),
this.dashWidth = 4,
this.dashSpace = 3,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
width: double.infinity,
child: CustomPaint(
painter: _DottedLinePainter(
color: color,
dashWidth: dashWidth,
dashSpace: dashSpace,
),
),
);
}
}
class _DottedLinePainter extends CustomPainter {
final Color color;
final double dashWidth;
final double dashSpace;
_DottedLinePainter({
required this.color,
required this.dashWidth,
required this.dashSpace,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = size.height
..style = PaintingStyle.stroke;
double x = 0;
while (x < size.width) {
canvas.drawLine(Offset(x, 0), Offset(x + dashWidth, 0), paint);
x += dashWidth + dashSpace;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -55,8 +55,6 @@ class _ProductdetailState extends State<Productdetail>
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lba/res/colors.dart'; import 'package:lba/res/colors.dart';
import 'package:lba/widgets/reserve_bottom_sheet.dart';
class PriceReserveWidget extends StatelessWidget { class PriceReserveWidget extends StatelessWidget {
const PriceReserveWidget({super.key}); const PriceReserveWidget({super.key});
@ -9,8 +10,7 @@ class PriceReserveWidget extends StatelessWidget {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 12),
decoration: BoxDecoration( decoration: const BoxDecoration(),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -24,8 +24,7 @@ class PriceReserveWidget extends StatelessWidget {
Row( Row(
children: [ children: [
Container( Container(
padding: padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: LightAppColors.offerTimer, color: LightAppColors.offerTimer,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@ -41,8 +40,8 @@ class PriceReserveWidget extends StatelessWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Row( Row(
children: [ children: const [
const Text( Text(
'2126.88', '2126.88',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
@ -50,8 +49,8 @@ class PriceReserveWidget extends StatelessWidget {
decoration: TextDecoration.lineThrough, decoration: TextDecoration.lineThrough,
), ),
), ),
SizedBox(width: 5,), SizedBox(width: 5),
const Text( Text(
'(12%)', '(12%)',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
@ -86,7 +85,12 @@ class PriceReserveWidget extends StatelessWidget {
height: 50, height: 50,
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
// TODO: Add reservation logic here showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const ReserveBottomSheet(),
);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: LightAppColors.offerTimer, backgroundColor: LightAppColors.offerTimer,

View File

@ -0,0 +1,322 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
import 'package:lba/screens/product/checkout.dart';
class ReserveBottomSheet extends StatefulWidget {
const ReserveBottomSheet({super.key});
@override
State<ReserveBottomSheet> createState() => _ReserveBottomSheetState();
}
class _ReserveBottomSheetState extends State<ReserveBottomSheet> {
int _quantity = 1;
final double _pricePerItem = 27.900;
final bool _isHotDeal = false;
void _incrementQuantity() {
if (_isHotDeal) return;
setState(() {
_quantity++;
});
}
void _decrementQuantity() {
if (_quantity <= 1) return;
setState(() {
_quantity--;
});
}
@override
Widget build(BuildContext context) {
final subtotal = _pricePerItem * _quantity;
const salesTax = 0.000;
final total = subtotal + salesTax;
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: const Color.fromARGB(255, 234, 247, 238),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 16),
Divider(color: Colors.grey.shade300, thickness: 1),
const SizedBox(height: 16),
_buildPriceDetails(subtotal, salesTax, total),
const SizedBox(height: 24),
_buildFooter(),
],
),
),
),
],
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SvgPicture.asset(Assets.icons.shop.path,
height: 22, color: Colors.grey.shade700),
const SizedBox(width: 10),
const Text(
"Al Rawabi Dairy Company L.L.C",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.only(left: 4.0),
child: Text(
"Philadelphia Honey Pecan Cream Cheese Spread,\n7.5 oz Tub",
style:
TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
),
),
const SizedBox(height: 6),
const Padding(
padding: EdgeInsets.only(left: 4.0),
child: Text(
"Pickup Now - 10:00 PM Today",
style: TextStyle(fontSize: 13, color: Colors.grey),
),
),
],
);
}
Widget _buildPriceDetails(double subtotal, double salesTax, double total) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Price Details",
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
),
Text(
"$_quantity Item",
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
const SizedBox(height: 12),
const DashedLine(),
const SizedBox(height: 12),
_priceRow("Subtotal", subtotal.toStringAsFixed(3)),
const SizedBox(height: 10),
_priceRow("Sales tax", salesTax.toStringAsFixed(3)),
const SizedBox(height: 12),
const DashedLine(),
const SizedBox(height: 12),
_priceRow("Total", total.toStringAsFixed(3), isTotal: true),
],
);
}
Widget _buildFooter() {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildQuantitySelector(),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const CheckoutPage(),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
final tween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: LightAppColors.offerTimer,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding:
const EdgeInsets.symmetric(horizontal: 65, vertical: 10),
),
child: const Text(
'Reserve',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
],
),
if (_isHotDeal) ...[
const SizedBox(height: 16),
_buildWarningMessage(),
]
],
);
}
Widget _priceRow(String label, String amount, {bool isTotal = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
color: Colors.grey.shade700,
),
),
Text(
"AED $amount",
style: TextStyle(
fontSize: 15,
fontWeight: isTotal ? FontWeight.bold : FontWeight.w500,
color: Colors.black,
),
),
],
);
}
Widget _buildQuantitySelector() {
final bool canDecrement = _quantity > 1;
final bool canIncrement = !_isHotDeal;
return Container(
height: 45,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
IconButton(
icon: Icon(Icons.remove,
color: canDecrement ? Colors.red : Colors.grey),
onPressed: _decrementQuantity,
splashRadius: 20,
),
Text(
_quantity.toString(),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
IconButton(
icon: Icon(Icons.add,
color: canIncrement ? Colors.red : Colors.grey),
onPressed: _incrementQuantity,
splashRadius: 20,
),
],
),
);
}
Widget _buildWarningMessage() {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.orangeAccent, size: 20),
const SizedBox(width: 10),
Expanded(
child: RichText(
text: TextSpan(
style: TextStyle(
fontSize: 12.5,
color: Colors.grey.shade800,
fontFamily: 'Roboto'),
children: const [
TextSpan(text: "Due to the "),
TextSpan(
text: "Hot",
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text:
" label discount, you can only purchase or reserve one item."),
],
),
),
),
],
);
}
}
class DashedLine extends StatelessWidget {
const DashedLine({super.key, this.height = 1, this.color = Colors.grey});
final double height;
final Color color;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final boxWidth = constraints.constrainWidth();
const dashWidth = 5.0;
final dashHeight = height;
final dashCount = (boxWidth / (2 * dashWidth)).floor();
return Flex(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
direction: Axis.horizontal,
children: List.generate(dashCount, (_) {
return SizedBox(
width: dashWidth,
height: dashHeight,
child: DecoratedBox(
decoration: BoxDecoration(color: color),
),
);
}),
);
},
);
}
}