diff --git a/assets/icons/send.svg b/assets/icons/send.svg
new file mode 100644
index 0000000..6c076f1
--- /dev/null
+++ b/assets/icons/send.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart
index 4b1d5bf..c1f7bfb 100644
--- a/lib/gen/assets.gen.dart
+++ b/lib/gen/assets.gen.dart
@@ -420,6 +420,9 @@ class $AssetsIconsGen {
SvgGenImage get selectedList =>
const SvgGenImage('assets/icons/selected list.svg');
+ /// File path: assets/icons/send.svg
+ SvgGenImage get send => const SvgGenImage('assets/icons/send.svg');
+
/// File path: assets/icons/shield.svg
SvgGenImage get shield => const SvgGenImage('assets/icons/shield.svg');
@@ -601,6 +604,7 @@ class $AssetsIconsGen {
routing,
searchNormal,
selectedList,
+ send,
shield,
shoppingCart,
slide2,
diff --git a/lib/screens/product/item.dart b/lib/screens/product/item.dart
index 9c58757..21c5d44 100644
--- a/lib/screens/product/item.dart
+++ b/lib/screens/product/item.dart
@@ -5,6 +5,8 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
import 'package:lba/widgets/buildWarpedInfo.dart';
+import 'package:lba/widgets/compact_review_input_widget.dart';
+import 'package:lba/widgets/leave_review_widget.dart';
import 'package:lba/widgets/orderType.dart';
import 'package:lba/widgets/price_reserve_widget.dart';
import 'package:lba/widgets/rate.dart';
@@ -156,8 +158,10 @@ class _ItemState extends State- with TickerProviderStateMixin {
alignment: Alignment.centerLeft,
child: Text(
widget.title,
- style:
- const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ ),
),
),
0,
@@ -213,13 +217,21 @@ class _ItemState extends State
- with TickerProviderStateMixin {
),
const SizedBox(height: 15),
_buildAnimatedWidget(
- buildWrappedInfo("Dimensions:", widget.dimensions), 4),
+ buildWrappedInfo("Dimensions:", widget.dimensions),
+ 4,
+ ),
_buildAnimatedWidget(buildWrappedInfo("Colour:", widget.colour), 5),
_buildAnimatedWidget(
- buildWrappedInfo("Material:", widget.material), 6),
+ buildWrappedInfo("Material:", widget.material),
+ 6,
+ ),
_buildAnimatedWidget(
- buildWrappedInfo("Description:", widget.description), 7),
- const SizedBox(height: 30),
+ buildWrappedInfo("Description:", widget.description),
+ 7,
+ ),
+ const SizedBox(height: 12),
+ const ReviewComposerWidget(),
+ const SizedBox(height: 15),
const Center(
child: Text(
"Top reviews from the United Arab Emirates",
@@ -238,8 +250,9 @@ class _ItemState extends State
- with TickerProviderStateMixin {
position: Tween(
begin: const Offset(-1, 0),
end: Offset.zero,
- ).animate(CurvedAnimation(
- parent: animation, curve: Curves.easeInOut)),
+ ).animate(
+ CurvedAnimation(parent: animation, curve: Curves.easeInOut),
+ ),
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
@@ -258,17 +271,20 @@ class _ItemState extends State
- with TickerProviderStateMixin {
review['userLiked'] = null;
} else {
if (review['userLiked'] == false) {
- review['noCount'] = (review['noCount'] as int) - 1;
+ review['noCount'] =
+ (review['noCount'] as int) - 1;
}
review['userLiked'] = true;
- review['yesCount'] = (review['yesCount'] as int) + 1;
+ review['yesCount'] =
+ (review['yesCount'] as int) + 1;
}
} else {
if (review['userLiked'] == false) {
review['userLiked'] = null;
} else {
if (review['userLiked'] == true) {
- review['yesCount'] = (review['yesCount'] as int) - 1;
+ review['yesCount'] =
+ (review['yesCount'] as int) - 1;
}
review['userLiked'] = false;
review['noCount'] = (review['noCount'] as int) + 1;
@@ -309,10 +325,10 @@ class _ItemState extends State
- with TickerProviderStateMixin {
),
],
),
- const SizedBox(height: 7),
- const PriceReserveWidget(),
+ const SizedBox(height: 7),
+ const PriceReserveWidget(),
],
),
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/widgets/compact_review_input_widget.dart b/lib/widgets/compact_review_input_widget.dart
new file mode 100644
index 0000000..e29f911
--- /dev/null
+++ b/lib/widgets/compact_review_input_widget.dart
@@ -0,0 +1,604 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:lba/gen/assets.gen.dart';
+import 'package:lba/res/colors.dart';
+
+class ReviewComposerWidget extends StatefulWidget {
+ const ReviewComposerWidget({super.key});
+
+ @override
+ State createState() => _ReviewComposerWidgetState();
+}
+
+class _ReviewComposerWidgetState extends State
+ with TickerProviderStateMixin {
+ double _rating = 0;
+ final TextEditingController _reviewController = TextEditingController();
+ XFile? _image;
+ final FocusNode _focusNode = FocusNode();
+ bool _isFocused = false;
+
+ late AnimationController _focusController;
+ late AnimationController _buttonController;
+ late AnimationController _starController;
+ late AnimationController _gradientController;
+ late Animation _gradientAnimation;
+
+ late List _starControllers;
+ late List> _starAnimations;
+
+ @override
+ void initState() {
+ super.initState();
+ _focusController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 300),
+ );
+ _buttonController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 400),
+ );
+ _starController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 600),
+ );
+ _gradientController = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 800),
+ );
+
+ _gradientAnimation = Tween(begin: 0, end: 1).animate(
+ CurvedAnimation(parent: _gradientController, curve: Curves.easeInOut),
+ );
+
+ _starControllers = List.generate(
+ 5,
+ (index) => AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 300),
+ ),
+ );
+
+ _starAnimations =
+ _starControllers
+ .map(
+ (controller) => Tween(begin: 0.0, end: 1.0).animate(
+ CurvedAnimation(parent: controller, curve: Curves.elasticOut),
+ ),
+ )
+ .toList();
+
+ _focusNode.addListener(() {
+ setState(() {
+ _isFocused = _focusNode.hasFocus;
+ if (_isFocused) {
+ _focusController.forward();
+ } else {
+ _focusController.reverse();
+ }
+ });
+ });
+
+ _reviewController.addListener(() {
+ if (_reviewController.text.isNotEmpty) {
+ _triggerGradientAnimation();
+ }
+
+ if (_reviewController.text.isNotEmpty || _image != null) {
+ _buttonController.forward();
+ } else {
+ _buttonController.reverse();
+ }
+ });
+ }
+
+ void _triggerGradientAnimation() {
+ if (!_gradientController.isAnimating) {
+ _gradientController.forward().then((_) {
+ _gradientController.reverse();
+ });
+ }
+ }
+
+ Future _animateStarsSequentially(int targetRating) async {
+ for (var controller in _starControllers) {
+ controller.reset();
+ }
+
+ for (int i = 0; i < targetRating; i++) {
+ if (i > 0) {
+ await Future.delayed(Duration(milliseconds: 120));
+ }
+
+ _starControllers[i].forward().then((_) {
+ Future.delayed(Duration(milliseconds: 100), () {
+ if (mounted) {
+ _starControllers[i].reverse().then((_) {
+ _starControllers[i].forward();
+ });
+ }
+ });
+ });
+ }
+ }
+
+ @override
+ void dispose() {
+ _reviewController.dispose();
+ _focusNode.dispose();
+ _focusController.dispose();
+ _buttonController.dispose();
+ _starController.dispose();
+ _gradientController.dispose();
+
+ for (var controller in _starControllers) {
+ controller.dispose();
+ }
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedBuilder(
+ animation: _gradientAnimation,
+ builder: (context, child) {
+ return Transform.scale(
+ scale: 1.0 + (_gradientAnimation.value * 0.005),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ margin: const EdgeInsets.symmetric(vertical: 12.0),
+ padding: const EdgeInsets.fromLTRB(16, 12, 16, 10),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ AppColors.primary.withOpacity(
+ 0.05 + (_gradientAnimation.value * 0.03),
+ ),
+ AppColors.borderPrimary.withOpacity(
+ 0.03 + (_gradientAnimation.value * 0.02),
+ ),
+ AppColors.buttonPrimary.withOpacity(
+ 0.04 + (_gradientAnimation.value * 0.02),
+ ),
+ AppColors.surface,
+ ],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ stops: [
+ 0.0,
+ 0.3 + (_gradientAnimation.value * 0.2),
+ 0.7 + (_gradientAnimation.value * 0.1),
+ 1.0,
+ ],
+ ),
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(
+ color:
+ _isFocused
+ ? AppColors.primary.withOpacity(
+ 0.6 + (_gradientAnimation.value * 0.2),
+ )
+ : AppColors.divider.withOpacity(
+ 0.3 + (_gradientAnimation.value * 0.1),
+ ),
+ width: _isFocused ? 1.5 : 1,
+ ),
+ boxShadow: [
+ if (_isFocused)
+ BoxShadow(
+ color: AppColors.primary.withOpacity(
+ 0.15 + (_gradientAnimation.value * 0.05),
+ ),
+ blurRadius: 12 + (_gradientAnimation.value * 4),
+ spreadRadius: 1 + (_gradientAnimation.value * 0.5),
+ offset: const Offset(0, 4),
+ )
+ else
+ BoxShadow(
+ color: Colors.black.withOpacity(
+ 0.03 + (_gradientAnimation.value * 0.02),
+ ),
+ blurRadius: 4 + (_gradientAnimation.value * 2),
+ spreadRadius: 0,
+ offset: const Offset(0, 1),
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildHeader(),
+ const SizedBox(height: 10),
+ if (_image != null) _buildImagePreview(),
+ _buildInputSection(),
+ const SizedBox(height: 8),
+ _buildActionBar(),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildHeader() {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ flex: 3,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ _rating > 0 ? 'Thanks for rating!' : 'Rate Your Experience',
+ style: TextStyle(
+ fontWeight: FontWeight.w600,
+ fontSize: 15,
+ color:
+ _rating > 0 ? AppColors.primary : AppColors.textPrimary,
+ ),
+ ),
+ if (_rating > 0)
+ Text(
+ _getRatingText(_rating),
+ style: TextStyle(
+ fontSize: 12,
+ color: AppColors.textSecondary,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(flex: 2, child: _buildAnimatedStarRating()),
+ ],
+ );
+ }
+
+ Widget _buildInputSection() {
+ return Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color:
+ _isFocused
+ ? AppColors.surface.withOpacity(0.7)
+ : AppColors.surface.withOpacity(0.4),
+ borderRadius: BorderRadius.circular(12),
+ border:
+ _isFocused
+ ? Border.all(color: AppColors.primary.withOpacity(0.25))
+ : null,
+ ),
+ child: TextField(
+ controller: _reviewController,
+ focusNode: _focusNode,
+ maxLines: null,
+ minLines: 2,
+ keyboardType: TextInputType.multiline,
+ style: TextStyle(
+ fontSize: 14,
+ height: 1.4,
+ color: AppColors.textPrimary,
+ ),
+ decoration: InputDecoration(
+ hintText: _getHintText(),
+ hintStyle: TextStyle(
+ color: AppColors.textSecondary.withOpacity(0.6),
+ fontSize: 14,
+ height: 1.4,
+ ),
+ border: InputBorder.none,
+ focusedBorder: InputBorder.none,
+ enabledBorder: InputBorder.none,
+ isDense: true,
+ contentPadding: EdgeInsets.zero,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildActionBar() {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [_buildImageButton(), _buildSubmitButton()],
+ );
+ }
+
+ Widget _buildImageButton() {
+ return AnimatedContainer(
+ duration: const Duration(milliseconds: 200),
+ child: InkWell(
+ onTap: _pickImage,
+ borderRadius: BorderRadius.circular(10),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
+ decoration: BoxDecoration(
+ color: AppColors.primary.withOpacity(0.08),
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(color: AppColors.primary.withOpacity(0.15)),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SvgPicture.asset(
+ Assets.icons.galleryAdd.path,
+ width: 16,
+ height: 16,
+ colorFilter: ColorFilter.mode(
+ AppColors.primary,
+ BlendMode.srcIn,
+ ),
+ ),
+ const SizedBox(width: 6),
+ Text(
+ 'Photo',
+ style: TextStyle(
+ color: AppColors.primary,
+ fontWeight: FontWeight.w500,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Future _pickImage() async {
+ final ImagePicker picker = ImagePicker();
+ final XFile? image = await picker.pickImage(source: ImageSource.gallery);
+ if (image != null) {
+ setState(() => _image = image);
+ _buttonController.forward();
+ }
+ }
+
+ void _submit() {
+ if (_reviewController.text.isEmpty && _image == null) return;
+ print(
+ 'Rating: $_rating, Review: ${_reviewController.text}, Image: ${_image?.path}',
+ );
+ setState(() {
+ _rating = 0;
+ _reviewController.clear();
+ _image = null;
+ });
+ FocusScope.of(context).unfocus();
+ }
+
+ Widget _buildSubmitButton() {
+ return ScaleTransition(
+ scale: CurvedAnimation(
+ parent: _buttonController,
+ curve: Curves.elasticOut,
+ ),
+ child: FadeTransition(
+ opacity: _buttonController,
+ child: Container(
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [AppColors.primary, AppColors.primary.withOpacity(0.8)],
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ ),
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: AppColors.primary.withOpacity(0.25),
+ blurRadius: 6,
+ offset: const Offset(0, 3),
+ ),
+ ],
+ ),
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: _submit,
+ borderRadius: BorderRadius.circular(12),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SvgPicture.asset(
+ Assets.icons.send.path,
+ width: 16,
+ height: 16,
+ colorFilter: ColorFilter.mode(
+ AppColors.surface,
+ BlendMode.srcIn,
+ ),
+ ),
+ const SizedBox(width: 6),
+ Text(
+ 'Post',
+ style: TextStyle(
+ color: AppColors.surface,
+ fontWeight: FontWeight.w600,
+ fontSize: 13,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ String _getRatingText(double rating) {
+ if (rating >= 4.5) return 'Excellent! 🎉';
+ if (rating >= 3.5) return 'Great! 👍';
+ if (rating >= 2.5) return 'Good 👌';
+ if (rating >= 1.5) return 'Fair 😐';
+ return 'Could be better 😔';
+ }
+
+ String _getHintText() {
+ if (_rating >= 4.5) {
+ return 'Tell others what made this experience amazing...';
+ } else if (_rating >= 3.5) {
+ return 'Share what you liked about this experience...';
+ } else if (_rating >= 2.5) {
+ return 'Share your thoughts about this experience...';
+ } else if (_rating >= 1.5) {
+ return 'Help us understand what could be improved...';
+ } else {
+ return 'Share your honest feedback to help others...';
+ }
+ }
+
+ Widget _buildAnimatedStarRating() {
+ return SizedBox(
+ width: 120,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: List.generate(5, (index) {
+ return AnimatedBuilder(
+ animation: _starAnimations[index],
+ builder: (context, child) {
+ final isActive = index < _rating;
+ final animationValue = _starAnimations[index].value;
+ final scaleValue =
+ isActive ? (1.0 + (animationValue * 0.1)) : 1.0;
+
+ return Transform.scale(
+ scale: scaleValue,
+ child: InkWell(
+ onTap: () {
+ setState(() => _rating = index + 1.0);
+ _animateStarsSequentially(index + 1);
+ _starController.forward().then(
+ (_) => _starController.reverse(),
+ );
+ },
+ borderRadius: BorderRadius.circular(10),
+ child: Container(
+ width: 20,
+ height: 20,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ if (isActive && animationValue > 0.5)
+ Container(
+ width: 14,
+ height: 14,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.green.withOpacity(0.3),
+ blurRadius: 3,
+ spreadRadius: 1,
+ ),
+ ],
+ ),
+ ),
+
+ SvgPicture.asset(
+ isActive
+ ? Assets.icons.starFill.path
+ : Assets.icons.star.path,
+ width: 13,
+ height: 13,
+ // colorFilter: ColorFilter.mode(
+ // isActive
+ // ? AppColors.primary
+ // : AppColors.textSecondary.withOpacity(0.4),
+ // BlendMode.srcIn,
+ // ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }),
+ ),
+ );
+ }
+
+ Widget _buildImagePreview() {
+ return TweenAnimationBuilder(
+ duration: const Duration(milliseconds: 400),
+ tween: Tween(begin: 0.0, end: 1.0),
+ curve: Curves.elasticOut,
+ builder: (context, value, child) {
+ return Transform.scale(
+ scale: value,
+ child: Container(
+ margin: const EdgeInsets.only(bottom: 10.0),
+ padding: const EdgeInsets.all(3),
+ decoration: BoxDecoration(
+ color: AppColors.surface,
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(
+ color: AppColors.primary.withOpacity(0.15),
+ width: 1,
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: AppColors.primary.withOpacity(0.08),
+ blurRadius: 4,
+ offset: const Offset(0, 1),
+ ),
+ ],
+ ),
+ child: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image.file(
+ File(_image!.path),
+ width: 60,
+ height: 60,
+ fit: BoxFit.cover,
+ ),
+ ),
+ Positioned(
+ top: -1,
+ right: -1,
+ child: GestureDetector(
+ onTap: () => setState(() => _image = null),
+ child: Container(
+ padding: const EdgeInsets.all(3),
+ decoration: BoxDecoration(
+ color: Colors.red.withOpacity(0.85),
+ shape: BoxShape.circle,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.2),
+ blurRadius: 2,
+ offset: const Offset(0, 1),
+ ),
+ ],
+ ),
+ child: const Icon(
+ Icons.close,
+ color: Colors.white,
+ size: 12,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/widgets/leave_review_widget.dart b/lib/widgets/leave_review_widget.dart
new file mode 100644
index 0000000..e69de29
diff --git a/lib/widgets/price_reserve_widget.dart b/lib/widgets/price_reserve_widget.dart
index 0887f92..0ec2c63 100644
--- a/lib/widgets/price_reserve_widget.dart
+++ b/lib/widgets/price_reserve_widget.dart
@@ -23,7 +23,10 @@ class PriceReserveWidget extends StatelessWidget {
Row(
children: [
Container(
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 3,
+ ),
decoration: BoxDecoration(
color: AppColors.offerTimer,
borderRadius: BorderRadius.circular(4),
@@ -80,7 +83,7 @@ class PriceReserveWidget extends StatelessWidget {
const SizedBox(width: 16),
SizedBox(
- width: 200,
+ width: 150,
height: 50,
child: ElevatedButton(
onPressed: () {
@@ -96,7 +99,10 @@ class PriceReserveWidget extends StatelessWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 12,
+ ),
),
child: Text(
'Reserve',
@@ -112,4 +118,4 @@ class PriceReserveWidget extends StatelessWidget {
),
);
}
-}
\ No newline at end of file
+}