diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg
new file mode 100644
index 0000000..4c58540
--- /dev/null
+++ b/assets/icons/arrow-left.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/card-pos.svg b/assets/icons/card-pos.svg
new file mode 100644
index 0000000..8abb308
--- /dev/null
+++ b/assets/icons/card-pos.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/ticket-discount.svg b/assets/icons/ticket-discount.svg
new file mode 100644
index 0000000..e9535d4
--- /dev/null
+++ b/assets/icons/ticket-discount.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/core/config/app_colors.dart b/lib/core/config/app_colors.dart
index 60660ff..6caa5b8 100644
--- a/lib/core/config/app_colors.dart
+++ b/lib/core/config/app_colors.dart
@@ -14,4 +14,6 @@ class AppColors {
static const Color selectedImg = Color.fromARGB(255, 76, 175, 80);
static const Color singleOfferType = Color.fromARGB(255, 244, 67, 54);
static const Color countdown = Color.fromARGB(255, 54, 124, 57);
+ static const Color countdownBorderRserve = Color.fromARGB(255, 186, 222, 251);
+ static const Color expiryReserve = Color.fromARGB(255, 183, 28, 28);
}
\ No newline at end of file
diff --git a/lib/core/gen/assets.gen.dart b/lib/core/gen/assets.gen.dart
index 20174c1..af834ff 100644
--- a/lib/core/gen/assets.gen.dart
+++ b/lib/core/gen/assets.gen.dart
@@ -75,12 +75,18 @@ class $AssetsIconsGen {
/// File path: assets/icons/arayesh.svg
SvgGenImage get arayesh => const SvgGenImage('assets/icons/arayesh.svg');
+ /// File path: assets/icons/arrow-left.svg
+ SvgGenImage get arrowLeft => const SvgGenImage('assets/icons/arrow-left.svg');
+
/// File path: assets/icons/back.svg
SvgGenImage get back => const SvgGenImage('assets/icons/back.svg');
/// File path: assets/icons/backArrow.svg
SvgGenImage get backArrow => const SvgGenImage('assets/icons/backArrow.svg');
+ /// File path: assets/icons/card-pos.svg
+ SvgGenImage get cardPos => const SvgGenImage('assets/icons/card-pos.svg');
+
/// File path: assets/icons/cinama.svg
SvgGenImage get cinama => const SvgGenImage('assets/icons/cinama.svg');
@@ -180,6 +186,10 @@ class $AssetsIconsGen {
/// File path: assets/icons/tickPb.svg
SvgGenImage get tickPb => const SvgGenImage('assets/icons/tickPb.svg');
+ /// File path: assets/icons/ticket-discount.svg
+ SvgGenImage get ticketDiscount =>
+ const SvgGenImage('assets/icons/ticket-discount.svg');
+
/// File path: assets/icons/timer-pause.svg
SvgGenImage get timerPause =>
const SvgGenImage('assets/icons/timer-pause.svg');
@@ -212,8 +222,10 @@ class $AssetsIconsGen {
vector,
addImg,
arayesh,
+ arrowLeft,
back,
backArrow,
+ cardPos,
cinama,
clock,
clockProduct,
@@ -244,6 +256,7 @@ class $AssetsIconsGen {
tickCircle,
tickSquare,
tickPb,
+ ticketDiscount,
timerPause,
volumeHigh,
warning2,
diff --git a/lib/data/models/datasources/offer_data_source.dart b/lib/data/models/datasources/offer_data_source.dart
index 2d5eb31..6d0db42 100644
--- a/lib/data/models/datasources/offer_data_source.dart
+++ b/lib/data/models/datasources/offer_data_source.dart
@@ -91,7 +91,7 @@ class MockOfferDataSource implements OfferDataSource {
publishedAt: DateTime.now().subtract(const Duration(days: 5)),
),
],
-
+ qrCodeData: 'PROXIBUY-OFFER-ID-1',
),
OfferModel(
id: '2',
@@ -174,7 +174,7 @@ class MockOfferDataSource implements OfferDataSource {
publishedAt: DateTime.now().subtract(const Duration(days: 5)),
),
],
-
+ qrCodeData: 'PROXIBUY-OFFER-ID-1',
),
OfferModel(
id: '3',
@@ -257,6 +257,7 @@ class MockOfferDataSource implements OfferDataSource {
publishedAt: DateTime.now().subtract(const Duration(days: 5)),
),
],
+ qrCodeData: 'PROXIBUY-OFFER-ID-1',
),
OfferModel(
id: '4',
@@ -339,7 +340,7 @@ class MockOfferDataSource implements OfferDataSource {
publishedAt: DateTime.now().subtract(const Duration(days: 5)),
),
],
-
+ qrCodeData: 'PROXIBUY-OFFER-ID-1',
),
];
diff --git a/lib/data/models/offer_model.dart b/lib/data/models/offer_model.dart
index 703f5e8..45aa9ad 100644
--- a/lib/data/models/offer_model.dart
+++ b/lib/data/models/offer_model.dart
@@ -24,7 +24,8 @@ class OfferModel extends Equatable {
final double finalPrice;
final List features;
final DiscountInfoModel? discountInfo;
- final List comments; // <-- این خط اضافه شد
+ final List comments;
+ final String qrCodeData;
const OfferModel({
required this.id,
@@ -47,7 +48,8 @@ class OfferModel extends Equatable {
required this.finalPrice,
this.features = const [],
this.discountInfo,
- this.comments = const [], // <-- این خط اضافه شد
+ this.comments = const [],
+ required this.qrCodeData,
});
String get coverImageUrl =>
@@ -64,7 +66,8 @@ class OfferModel extends Equatable {
longitude,
features,
discountInfo,
- comments, // <-- این خط اضافه شد
+ comments,
+ qrCodeData
];
String get distanceAsString {
diff --git a/lib/data/models/product_model.dart b/lib/data/models/product_model.dart
new file mode 100644
index 0000000..c76e519
--- /dev/null
+++ b/lib/data/models/product_model.dart
@@ -0,0 +1,17 @@
+class Product {
+ final String category;
+ final String name;
+ final String imageUrl;
+ final double originalPrice;
+ final double discountPercentage;
+
+ Product({
+ required this.category,
+ required this.name,
+ required this.imageUrl,
+ required this.originalPrice,
+ required this.discountPercentage,
+ });
+
+ double get finalPrice => originalPrice * (1 - discountPercentage / 100);
+}
\ No newline at end of file
diff --git a/lib/presentation/pages/product_detail_page.dart b/lib/presentation/pages/product_detail_page.dart
index 1bd1331..79ae772 100644
--- a/lib/presentation/pages/product_detail_page.dart
+++ b/lib/presentation/pages/product_detail_page.dart
@@ -7,6 +7,7 @@ import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/data/models/offer_model.dart';
import 'package:proxibuy/data/repositories/offer_repository.dart';
+import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_bloc.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
@@ -54,41 +55,52 @@ class ProductDetailPage extends StatelessWidget {
bottom: 30,
left: 24,
right: 24,
- child: ElevatedButton(
- onPressed: () {
- print("دکمه فشرده شد!");
- },
- style: ElevatedButton.styleFrom(
- backgroundColor: Colors.green,
- elevation: 5,
- padding: const EdgeInsets.symmetric(vertical: 16),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(15),
- ),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SvgPicture.asset(Assets.icons.receiptDisscount.path),
- const SizedBox(width: 8),
- const Text(
- 'رزرو تخفیف',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.normal,
- color: Colors.black,
+ // دکمه را در یک BlocBuilder قرار میدهیم تا به state دسترسی داشته باشد
+ child: BlocBuilder(
+ builder: (context, state) {
+ // دکمه فقط زمانی نمایش داده میشود که اطلاعات محصول با موفقیت لود شده باشد
+ if (state is ProductDetailLoadSuccess) {
+ return ElevatedButton(
+ onPressed: () {
+ // با کلیک روی دکمه، به صفحه تایید رزرو منتقل میشویم
+ // و اطلاعات محصول را به آن پاس میدهیم
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => ReservationConfirmationPage(offer: state.offer),
),
+ );
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.confirm, // رنگ دکمه را به سبز تغییر دادم تا با مفهوم رزرو همخوانی داشته باشد
+ elevation: 5,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(50), // گردی بیشتر برای زیبایی
),
- ],
- ),
- )
- .animate()
- .fadeIn(
- delay: 200.ms,
- duration: 400.ms,
- curve: Curves.easeOut,
- )
- .slideY(begin: 2, duration: 500.ms, curve: Curves.easeOut),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(Assets.icons.receiptDisscount.path,),
+ const SizedBox(width: 12),
+ const Text(
+ 'رزرو تخفیف',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.normal,
+ color: Colors.black,
+ ),
+ ),
+ ],
+ ),
+ ).animate()
+ .fadeIn(delay: 200.ms, duration: 400.ms, curve: Curves.easeOut)
+ .slideY(begin: 2, duration: 500.ms, curve: Curves.easeOut);
+ }
+ // اگر اطلاعات در حال لود شدن باشد، چیزی نمایش داده نمیشود
+ return const SizedBox.shrink();
+ },
+ ),
),
],
),
diff --git a/lib/presentation/pages/reservation_details_screen.dart b/lib/presentation/pages/reservation_details_screen.dart
new file mode 100644
index 0000000..66ca24a
--- /dev/null
+++ b/lib/presentation/pages/reservation_details_screen.dart
@@ -0,0 +1,358 @@
+import 'dart:async';
+
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:proxibuy/core/config/app_colors.dart';
+import 'package:proxibuy/core/gen/assets.gen.dart';
+import 'package:proxibuy/data/models/offer_model.dart';
+import 'package:qr_flutter/qr_flutter.dart';
+import 'package:flutter_animate/flutter_animate.dart';
+
+class ReservationConfirmationPage extends StatefulWidget {
+ final OfferModel offer;
+
+ const ReservationConfirmationPage({super.key, required this.offer});
+
+ @override
+ State createState() =>
+ _ReservationConfirmationPageState();
+}
+
+class _ReservationConfirmationPageState extends State {
+ Timer? _timer;
+ Duration _remaining = Duration.zero;
+
+ @override
+ void initState() {
+ super.initState();
+ _calculateRemainingTime();
+ _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
+ _calculateRemainingTime();
+ });
+ }
+
+ void _calculateRemainingTime() {
+ final now = DateTime.now();
+ if (widget.offer.expiryTime.isAfter(now)) {
+ if (mounted) {
+ setState(() {
+ _remaining = widget.offer.expiryTime.difference(now);
+ });
+ }
+ } else {
+ if (mounted) {
+ setState(() {
+ _remaining = Duration.zero;
+ });
+ }
+ _timer?.cancel();
+ }
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Directionality(
+ textDirection: TextDirection.rtl,
+ child: Scaffold(
+ backgroundColor: Colors.grey[50],
+ appBar: _buildCustomAppBar(context),
+ body: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // ✅ انیمیشن برای عنوان اصلی
+ Text(
+ 'تخفیف ${widget.offer.discountType} رزرو شد!',
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.bold,
+ color: Colors.black87,
+ ),
+ ).animate().fadeIn(delay: 300.ms, duration: 500.ms).slideY(begin: -0.2, end: 0),
+
+ const SizedBox(height: 8),
+ // ✅ انیمیشن برای خط جداکننده
+ const Divider(thickness: 1.5)
+ .animate()
+ .fadeIn(delay: 400.ms)
+ .scaleX(begin: 0, duration: 600.ms, curve: Curves.easeInOut),
+
+ const SizedBox(height: 18),
+ _buildOfferDetailsCard()
+ .animate()
+ .fadeIn(delay: 600.ms, duration: 500.ms)
+ .slideX(begin: 0.5, end: 0, curve: Curves.easeOutCubic),
+
+ const SizedBox(height: 18),
+ _buildTimerCard()
+ .animate()
+ .fadeIn(delay: 800.ms, duration: 500.ms)
+ .scale(begin: const Offset(0.8, 0.8), curve: Curves.easeOutBack),
+
+ const SizedBox(height: 18),
+ _buildQrCodeCard()
+ .animate()
+ .fadeIn(delay: 1000.ms, duration: 500.ms)
+ .flipV(begin: -0.5, end: 0, curve: Curves.easeOut),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ PreferredSizeWidget _buildCustomAppBar(BuildContext context) {
+ return PreferredSize(
+ preferredSize: const Size.fromHeight(70.0),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: const BorderRadius.vertical(
+ bottom: Radius.circular(15),
+ ),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.08),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Column(
+ children: [
+ SizedBox(height: 15),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Row(
+ children: [
+ const Text(
+ 'رزرو شده',
+ style: TextStyle(
+ color: Colors.black,
+ fontWeight: FontWeight.normal,
+ fontSize: 16,
+ ),
+ ),
+ IconButton(
+ icon: SvgPicture.asset(Assets.icons.arrowLeft.path),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildOfferDetailsCard() {
+ return Container(
+ padding: const EdgeInsets.all(4),
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: CachedNetworkImage(
+ imageUrl: widget.offer.coverImageUrl,
+ width: 110,
+ height: 110,
+ fit: BoxFit.cover,
+ ),
+ ),
+ const SizedBox(width: 16),
+ // جزئیات متنی
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Text(
+ widget.offer.title,
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.normal,
+ color: AppColors.hint,
+ ),
+ ),
+ const SizedBox(height: 10),
+ Row(
+ children: [
+ SvgPicture.asset(Assets.icons.ticketDiscount.path),
+ const SizedBox(width: 6),
+ Text(
+ '(${widget.offer.discount})',
+ style: const TextStyle(
+ fontSize: 16,
+ color: AppColors.singleOfferType,
+ fontWeight: FontWeight.normal,
+ ),
+ ),
+ const SizedBox(width: 6),
+ Text(
+ widget.offer.originalPrice.toStringAsFixed(0),
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.grey.shade600,
+ decoration: TextDecoration.lineThrough,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ Row(
+ children: [
+ SvgPicture.asset(Assets.icons.cardPos.path,height: 22,color: Color.fromARGB(255, 157, 157, 155),),
+ SizedBox(width: 6,),
+ Text(
+ '${widget.offer.finalPrice.toStringAsFixed(0)} تومان',
+ style: const TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.normal,
+ color: AppColors.singleOfferType,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTimerCard() {
+ if (_remaining.inSeconds <= 0) {
+ return const Center(
+ child: Text(
+ 'مهلت این تخفیف به پایان رسیده است',
+ style: TextStyle(color: AppColors.singleOfferType, fontSize: 16),
+ ),
+ );
+ }
+
+ String days = _remaining.inDays.toString();
+ String hours = (_remaining.inHours % 24).toString();
+ String minutes = (_remaining.inMinutes % 60).toString();
+ String seconds = (_remaining.inSeconds % 60).toString();
+
+ return Container(
+ padding: const EdgeInsets.symmetric(vertical: 20),
+ decoration: BoxDecoration(
+ color: Color.fromARGB(255, 246, 246, 246),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Column(
+ children: [
+ Text("مدت",style: TextStyle(color: AppColors.expiryReserve,fontSize: 15),),
+ SizedBox(height: 7,),
+ Text("اعتبار",style: TextStyle(color: AppColors.expiryReserve,fontSize: 15),),
+ ],
+ ),
+ SizedBox(width: 25,),
+ _buildTimeBlock(seconds, 'ثانیه'),
+ SizedBox(width: 20,),
+ _buildTimeBlock(minutes, 'دقیقه'),
+ SizedBox(width: 20,),
+ _buildTimeBlock(hours, 'ساعت'),
+ SizedBox(width: 20,),
+ if (_remaining.inDays > 0) _buildTimeBlock(days, 'روز'),
+ ],
+ ),
+ SizedBox(height: 20 ,),
+ Text("لطفا QR Code زیر رو به فروشنده نشون بده.",style: TextStyle(fontSize: 15),)
+ ],
+ ),
+ );
+ }
+
+ Widget _buildTimeBlock(String value, String label) {
+ return Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 3),
+ decoration: BoxDecoration(
+ border: Border.all(color: AppColors.countdownBorderRserve, width: 1.5),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ value,
+ style: const TextStyle(
+ fontSize: 20,
+ fontFamily: 'Dana',
+ fontWeight: FontWeight.bold,
+ color: Colors.black,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ label,
+ style: const TextStyle(
+ fontSize: 12,
+ fontFamily: 'Dana',
+ color: Colors.black,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+ Widget _buildQrCodeCard() {
+ return Center(
+ child: Container(
+ width: 500,
+ padding: const EdgeInsets.all(20),
+ decoration: BoxDecoration(
+ color: Color.fromARGB(255, 246, 246, 246),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: QrImageView(
+ data: widget.offer.qrCodeData,
+ version: QrVersions.auto,
+ size: 280.0,
+ ),
+ ),
+ SizedBox(height: 10,),
+ Text(widget.offer.qrCodeData,style: TextStyle(fontWeight: FontWeight.bold,fontSize: 20),)
+ ],
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/pubspec.lock b/pubspec.lock
index 406ebb3..3eb44e7 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -829,6 +829,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.0"
+ qr:
+ dependency: transitive
+ description:
+ name: qr
+ sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.2"
+ qr_flutter:
+ dependency: "direct main"
+ description:
+ name: qr_flutter
+ sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.0"
rxdart:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 03e2c72..28f555c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -49,6 +49,7 @@ dependencies:
flutter_animate: ^4.5.2
maps_launcher: ^3.0.0+1
slide_countdown: ^2.0.2
+ qr_flutter: ^4.1.0
dev_dependencies:
flutter_test: