deatail screen
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="4" height="19" viewBox="0 0 4 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 17.5L2 1.5" stroke="#2196F3" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 184 B |
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.23 19.7688C8.05 18.8888 9.3 18.9588 10.02 19.9188L11.03 21.2688C11.84 22.3388 13.15 22.3388 13.96 21.2688L14.97 19.9188C15.69 18.9588 16.94 18.8888 17.76 19.7688C19.54 21.6688 20.99 21.0388 20.99 18.3788V7.10885C21 3.07885 20.06 2.06885 16.28 2.06885H8.72C4.94 2.06885 4 3.07885 4 7.10885V18.3688C4 21.0388 5.46 21.6588 7.23 19.7688Z" stroke="#121110" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9.5 13.0688L15.5 7.06885" stroke="#121110" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M15.4945 13.0688H15.5035" stroke="#121110" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9.49451 7.56885H9.50349" stroke="#121110" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 896 B |
|
|
@ -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="M10.2976 2.83255L11.6176 5.47255C11.7976 5.84005 12.2776 6.19255 12.6826 6.26005L15.0751 6.65755C16.6051 6.91255 16.9651 8.02255 15.8626 9.11755L14.0026 10.9776C13.6876 11.2926 13.5151 11.9001 13.6126 12.3351L14.1451 14.6376C14.5651 16.4601 13.5976 17.1651 11.9851 16.2126L9.74255 14.8851C9.33755 14.6451 8.67005 14.6451 8.25755 14.8851L6.01505 16.2126C4.41005 17.1651 3.43505 16.4526 3.85505 14.6376L4.38755 12.3351C4.48505 11.9001 4.31255 11.2926 3.99755 10.9776L2.13755 9.11755C1.04255 8.02255 1.39505 6.91255 2.92505 6.65755L5.31755 6.26005C5.71505 6.19255 6.19505 5.84005 6.37505 5.47255L7.69505 2.83255C8.41505 1.40005 9.58505 1.40005 10.2976 2.83255Z" fill="#70BF73" stroke="#70BF73" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 852 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.2976 2.63248L11.6176 5.27248C11.7976 5.63998 12.2776 5.99248 12.6826 6.05998L15.0751 6.45748C16.6051 6.71248 16.9651 7.82248 15.8626 8.91748L14.0026 10.7775C13.6876 11.0925 13.5151 11.7 13.6126 12.135L14.1451 14.4375C14.5651 16.26 13.5976 16.965 11.9851 16.0125L9.74255 14.685C9.33755 14.445 8.67005 14.445 8.25755 14.685L6.01505 16.0125C4.41005 16.965 3.43505 16.2525 3.85505 14.4375L4.38755 12.135C4.48505 11.7 4.31255 11.0925 3.99755 10.7775L2.13755 8.91748C1.04255 7.82248 1.39505 6.71248 2.92505 6.45748L5.31755 6.05998C5.71505 5.99248 6.19505 5.63998 6.37505 5.27248L7.69505 2.63248C8.41505 1.19998 9.58505 1.19998 10.2976 2.63248Z" stroke="#70BF73" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.025 11.245C13.755 11.515 13.605 12.0325 13.695 12.4L14.2125 14.6575C14.43 15.595 14.295 16.3 13.83 16.6375C13.6425 16.7725 13.4175 16.84 13.155 16.84C12.7725 16.84 12.3225 16.6975 11.8275 16.405L9.62998 15.1C9.28498 14.8975 8.71498 14.8975 8.36998 15.1L6.17248 16.405C5.33998 16.8925 4.62748 16.975 4.16998 16.6375C3.99748 16.51 3.86998 16.3375 3.78748 16.1125L12.9075 6.99251C13.2525 6.64751 13.74 6.49001 14.2125 6.57251L14.97 6.70001C15.765 6.83501 16.2975 7.19501 16.47 7.72001C16.635 8.24501 16.41 8.85251 15.84 9.42251L14.025 11.245Z" fill="#70BF73"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -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="M10.2976 3.46585L11.6176 6.10585C11.7976 6.47335 12.2776 6.82585 12.6826 6.89335L15.0751 7.29085C16.6051 7.54585 16.9651 8.65585 15.8626 9.75085L14.0026 11.6109C13.6876 11.9259 13.5151 12.5334 13.6126 12.9684L14.1451 15.2709C14.5651 17.0934 13.5976 17.7984 11.9851 16.8459L9.74255 15.5184C9.33755 15.2784 8.67005 15.2784 8.25755 15.5184L6.01505 16.8459C4.41005 17.7984 3.43505 17.0859 3.85505 15.2709L4.38755 12.9684C4.48505 12.5334 4.31255 11.9259 3.99755 11.6109L2.13755 9.75085C1.04255 8.65585 1.39505 7.54585 2.92505 7.29085L5.31755 6.89335C5.71505 6.82585 6.19505 6.47335 6.37505 6.10585L7.69505 3.46585C8.41505 2.03335 9.58505 2.03335 10.2976 3.46585Z" stroke="#70BF73" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 837 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.08325 17.2356H11.5833C15.3333 17.2356 16.8333 15.7356 16.8333 11.9856V7.4856C16.8333 3.7356 15.3333 2.2356 11.5833 2.2356H7.08325C3.33325 2.2356 1.83325 3.7356 1.83325 7.4856V11.9856C1.83325 15.7356 3.33325 17.2356 7.08325 17.2356Z" stroke="#5F5F5F" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6.14575 9.73554L8.26825 11.858L12.5208 7.61304" stroke="#5F5F5F" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 538 B |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 10.3813C8.6925 10.3813 8.4375 10.1263 8.4375 9.81885V5.88135C8.4375 5.57385 8.6925 5.31885 9 5.31885C9.3075 5.31885 9.5625 5.57385 9.5625 5.88135V9.81885C9.5625 10.1263 9.3075 10.3813 9 10.3813Z" fill="#2196F3"/>
|
||||||
|
<path d="M9 13.0063C8.7975 13.0063 8.60998 12.9313 8.46748 12.7888C8.39998 12.7138 8.34751 12.6313 8.30251 12.5413C8.26501 12.4513 8.25 12.3538 8.25 12.2563C8.25 12.0613 8.33248 11.8663 8.46748 11.7238C8.74498 11.4462 9.25502 11.4462 9.53252 11.7238C9.66752 11.8663 9.75 12.0613 9.75 12.2563C9.75 12.3538 9.72749 12.4513 9.68999 12.5413C9.65249 12.6313 9.60002 12.7138 9.53252 12.7888C9.39002 12.9313 9.2025 13.0063 9 13.0063Z" fill="#2196F3"/>
|
||||||
|
<path d="M9.00014 17.1314C8.49764 17.1314 7.98763 17.0039 7.53763 16.7414L3.08263 14.1689C2.18263 13.6439 1.62012 12.6764 1.62012 11.6339V6.50393C1.62012 5.46143 2.18263 4.49393 3.08263 3.96893L7.53763 1.39644C8.43763 0.871436 9.55515 0.871436 10.4626 1.39644L14.9176 3.96893C15.8176 4.49393 16.3802 5.46143 16.3802 6.50393V11.6339C16.3802 12.6764 15.8176 13.6439 14.9176 14.1689L10.4626 16.7414C10.0126 17.0039 9.50264 17.1314 9.00014 17.1314ZM9.00014 2.13142C8.69264 2.13142 8.37763 2.21393 8.10013 2.37143L3.64513 4.94392C3.09013 5.26642 2.74512 5.85893 2.74512 6.50393V11.6339C2.74512 12.2714 3.09013 12.8714 3.64513 13.1939L8.10013 15.7664C8.65513 16.0889 9.34514 16.0889 9.89264 15.7664L14.3476 13.1939C14.9026 12.8714 15.2477 12.2789 15.2477 11.6339V6.50393C15.2477 5.86643 14.9026 5.26642 14.3476 4.94392L9.89264 2.37143C9.62264 2.21393 9.30764 2.13142 9.00014 2.13142Z" fill="#2196F3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -37,6 +37,9 @@ class $AssetsIconsGen {
|
||||||
/// File path: assets/icons/Google svg.svg
|
/// File path: assets/icons/Google svg.svg
|
||||||
SvgGenImage get googleSvg => const SvgGenImage('assets/icons/Google svg.svg');
|
SvgGenImage get googleSvg => const SvgGenImage('assets/icons/Google svg.svg');
|
||||||
|
|
||||||
|
/// File path: assets/icons/Line 2.svg
|
||||||
|
SvgGenImage get line2 => const SvgGenImage('assets/icons/Line 2.svg');
|
||||||
|
|
||||||
/// File path: assets/icons/LogoWithName.svg
|
/// File path: assets/icons/LogoWithName.svg
|
||||||
SvgGenImage get logoWithName =>
|
SvgGenImage get logoWithName =>
|
||||||
const SvgGenImage('assets/icons/LogoWithName.svg');
|
const SvgGenImage('assets/icons/LogoWithName.svg');
|
||||||
|
|
@ -127,6 +130,10 @@ class $AssetsIconsGen {
|
||||||
/// File path: assets/icons/pooshak.svg
|
/// File path: assets/icons/pooshak.svg
|
||||||
SvgGenImage get pooshak => const SvgGenImage('assets/icons/pooshak.svg');
|
SvgGenImage get pooshak => const SvgGenImage('assets/icons/pooshak.svg');
|
||||||
|
|
||||||
|
/// File path: assets/icons/receipt-disscount.svg
|
||||||
|
SvgGenImage get receiptDisscount =>
|
||||||
|
const SvgGenImage('assets/icons/receipt-disscount.svg');
|
||||||
|
|
||||||
/// File path: assets/icons/resturan.svg
|
/// File path: assets/icons/resturan.svg
|
||||||
SvgGenImage get resturan => const SvgGenImage('assets/icons/resturan.svg');
|
SvgGenImage get resturan => const SvgGenImage('assets/icons/resturan.svg');
|
||||||
|
|
||||||
|
|
@ -144,9 +151,18 @@ class $AssetsIconsGen {
|
||||||
SvgGenImage get shoppingCart =>
|
SvgGenImage get shoppingCart =>
|
||||||
const SvgGenImage('assets/icons/shopping-cart.svg');
|
const SvgGenImage('assets/icons/shopping-cart.svg');
|
||||||
|
|
||||||
|
/// File path: assets/icons/star fill.svg
|
||||||
|
SvgGenImage get starFill => const SvgGenImage('assets/icons/star fill.svg');
|
||||||
|
|
||||||
|
/// File path: assets/icons/star half.svg
|
||||||
|
SvgGenImage get starHalf => const SvgGenImage('assets/icons/star half.svg');
|
||||||
|
|
||||||
/// File path: assets/icons/star.svg
|
/// File path: assets/icons/star.svg
|
||||||
SvgGenImage get star => const SvgGenImage('assets/icons/star.svg');
|
SvgGenImage get star => const SvgGenImage('assets/icons/star.svg');
|
||||||
|
|
||||||
|
/// File path: assets/icons/star2.svg
|
||||||
|
SvgGenImage get star2 => const SvgGenImage('assets/icons/star2.svg');
|
||||||
|
|
||||||
/// File path: assets/icons/tala.svg
|
/// File path: assets/icons/tala.svg
|
||||||
SvgGenImage get tala => const SvgGenImage('assets/icons/tala.svg');
|
SvgGenImage get tala => const SvgGenImage('assets/icons/tala.svg');
|
||||||
|
|
||||||
|
|
@ -157,6 +173,10 @@ class $AssetsIconsGen {
|
||||||
SvgGenImage get tickCircle =>
|
SvgGenImage get tickCircle =>
|
||||||
const SvgGenImage('assets/icons/tick-circle.svg');
|
const SvgGenImage('assets/icons/tick-circle.svg');
|
||||||
|
|
||||||
|
/// File path: assets/icons/tick-square.svg
|
||||||
|
SvgGenImage get tickSquare =>
|
||||||
|
const SvgGenImage('assets/icons/tick-square.svg');
|
||||||
|
|
||||||
/// File path: assets/icons/tickPb.svg
|
/// File path: assets/icons/tickPb.svg
|
||||||
SvgGenImage get tickPb => const SvgGenImage('assets/icons/tickPb.svg');
|
SvgGenImage get tickPb => const SvgGenImage('assets/icons/tickPb.svg');
|
||||||
|
|
||||||
|
|
@ -168,6 +188,9 @@ class $AssetsIconsGen {
|
||||||
SvgGenImage get volumeHigh =>
|
SvgGenImage get volumeHigh =>
|
||||||
const SvgGenImage('assets/icons/volume-high.svg');
|
const SvgGenImage('assets/icons/volume-high.svg');
|
||||||
|
|
||||||
|
/// File path: assets/icons/warning-2.svg
|
||||||
|
SvgGenImage get warning2 => const SvgGenImage('assets/icons/warning-2.svg');
|
||||||
|
|
||||||
/// List of all assets
|
/// List of all assets
|
||||||
List<SvgGenImage> get values => [
|
List<SvgGenImage> get values => [
|
||||||
arrowRight2,
|
arrowRight2,
|
||||||
|
|
@ -177,6 +200,7 @@ class $AssetsIconsGen {
|
||||||
ellipse1,
|
ellipse1,
|
||||||
gold,
|
gold,
|
||||||
googleSvg,
|
googleSvg,
|
||||||
|
line2,
|
||||||
logoWithName,
|
logoWithName,
|
||||||
makeup,
|
makeup,
|
||||||
mobile,
|
mobile,
|
||||||
|
|
@ -205,18 +229,24 @@ class $AssetsIconsGen {
|
||||||
map,
|
map,
|
||||||
notification,
|
notification,
|
||||||
pooshak,
|
pooshak,
|
||||||
|
receiptDisscount,
|
||||||
resturan,
|
resturan,
|
||||||
routing,
|
routing,
|
||||||
scanBarcode,
|
scanBarcode,
|
||||||
shop,
|
shop,
|
||||||
shoppingCart,
|
shoppingCart,
|
||||||
|
starFill,
|
||||||
|
starHalf,
|
||||||
star,
|
star,
|
||||||
|
star2,
|
||||||
tala,
|
tala,
|
||||||
teria,
|
teria,
|
||||||
tickCircle,
|
tickCircle,
|
||||||
|
tickSquare,
|
||||||
tickPb,
|
tickPb,
|
||||||
timerPause,
|
timerPause,
|
||||||
volumeHigh,
|
volumeHigh,
|
||||||
|
warning2,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class CommentModel extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String userName;
|
||||||
|
final double rating;
|
||||||
|
final String comment;
|
||||||
|
final DateTime publishedAt;
|
||||||
|
final List<String> uploadedImageUrls;
|
||||||
|
|
||||||
|
const CommentModel({
|
||||||
|
required this.id,
|
||||||
|
required this.userName,
|
||||||
|
required this.rating,
|
||||||
|
required this.comment,
|
||||||
|
required this.publishedAt,
|
||||||
|
this.uploadedImageUrls = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls];
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:proxibuy/data/models/comment_model.dart';
|
||||||
|
import 'package:proxibuy/data/models/discount_info_model.dart';
|
||||||
import 'package:proxibuy/data/models/offer_model.dart';
|
import 'package:proxibuy/data/models/offer_model.dart';
|
||||||
import 'package:proxibuy/data/models/working_hours.dart';
|
import 'package:proxibuy/data/models/working_hours.dart';
|
||||||
|
|
||||||
|
|
@ -60,6 +62,36 @@ class MockOfferDataSource implements OfferDataSource {
|
||||||
longitude: 51.670,
|
longitude: 51.670,
|
||||||
originalPrice: 150000,
|
originalPrice: 150000,
|
||||||
finalPrice: 120000,
|
finalPrice: 120000,
|
||||||
|
features: [
|
||||||
|
"تهیه شده از بهترین و تازهترین مواد اولیه",
|
||||||
|
"محیطی دنج و مناسب برای قرارهای دوستانه",
|
||||||
|
"دارای منوی متنوع برای تمام سلیقهها",
|
||||||
|
],
|
||||||
|
discountInfo: const DiscountInfoModel(
|
||||||
|
name: "رفیقبازی",
|
||||||
|
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیقتو بیار و تخفیفتو ببر. دوستای بیشتر، تخفیف بیشتر.",
|
||||||
|
),
|
||||||
|
comments: [ // <-- بخش نظرات اضافه شد
|
||||||
|
CommentModel(
|
||||||
|
id: 'c1',
|
||||||
|
userName: 'سارا رضایی',
|
||||||
|
rating: 4.5,
|
||||||
|
comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر میزنم!',
|
||||||
|
publishedAt: DateTime.now().subtract(const Duration(days: 2)),
|
||||||
|
uploadedImageUrls: [
|
||||||
|
'https://picsum.photos/seed/user_img1/200/200',
|
||||||
|
'https://picsum.photos/seed/user_img2/200/200',
|
||||||
|
]
|
||||||
|
),
|
||||||
|
CommentModel(
|
||||||
|
id: 'c2',
|
||||||
|
userName: 'pbuser_157',
|
||||||
|
rating: 4,
|
||||||
|
comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.',
|
||||||
|
publishedAt: DateTime.now().subtract(const Duration(days: 5)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
),
|
),
|
||||||
OfferModel(
|
OfferModel(
|
||||||
id: '2',
|
id: '2',
|
||||||
|
|
@ -103,7 +135,7 @@ class MockOfferDataSource implements OfferDataSource {
|
||||||
Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'),
|
Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
WorkingHours(day: 'جمعه', shifts: []), // تعطیل
|
WorkingHours(day: 'جمعه', shifts: []),
|
||||||
],
|
],
|
||||||
discountType: 'رفیقبازی',
|
discountType: 'رفیقبازی',
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
|
@ -113,6 +145,36 @@ class MockOfferDataSource implements OfferDataSource {
|
||||||
longitude: 51.670,
|
longitude: 51.670,
|
||||||
originalPrice: 150000,
|
originalPrice: 150000,
|
||||||
finalPrice: 120000,
|
finalPrice: 120000,
|
||||||
|
features: [
|
||||||
|
"تهیه شده از بهترین و تازهترین مواد اولیه",
|
||||||
|
"محیطی دنج و مناسب برای قرارهای دوستانه",
|
||||||
|
"دارای منوی متنوع برای تمام سلیقهها",
|
||||||
|
],
|
||||||
|
discountInfo: const DiscountInfoModel(
|
||||||
|
name: "رفیقبازی",
|
||||||
|
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیقتو بیار و تخفیفتو ببر. دوستای بیشتر، تخفیف بیشتر.",
|
||||||
|
),
|
||||||
|
comments: [ // <-- بخش نظرات اضافه شد
|
||||||
|
CommentModel(
|
||||||
|
id: 'c1',
|
||||||
|
userName: 'سارا رضایی',
|
||||||
|
rating: 4.5,
|
||||||
|
comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر میزنم!',
|
||||||
|
publishedAt: DateTime.now().subtract(const Duration(days: 2)),
|
||||||
|
uploadedImageUrls: [
|
||||||
|
'https://picsum.photos/seed/user_img1/200/200',
|
||||||
|
'https://picsum.photos/seed/user_img2/200/200',
|
||||||
|
]
|
||||||
|
),
|
||||||
|
CommentModel(
|
||||||
|
id: 'c2',
|
||||||
|
userName: 'علی اکبری',
|
||||||
|
rating: 4,
|
||||||
|
comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.',
|
||||||
|
publishedAt: DateTime.now().subtract(const Duration(days: 5)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
),
|
),
|
||||||
OfferModel(
|
OfferModel(
|
||||||
id: '3',
|
id: '3',
|
||||||
|
|
@ -120,7 +182,7 @@ class MockOfferDataSource implements OfferDataSource {
|
||||||
title: 'چیزبرگر',
|
title: 'چیزبرگر',
|
||||||
discount: '۲۰٪',
|
discount: '۲۰٪',
|
||||||
imageUrls: [
|
imageUrls: [
|
||||||
'https://picsum.photos/seed/food/400/200',
|
'https://picsum.photos/seed/food/ 400/200',
|
||||||
'https://picsum.photos/seed/burger1/400/400',
|
'https://picsum.photos/seed/burger1/400/400',
|
||||||
'https://picsum.photos/seed/burger2/400/400',
|
'https://picsum.photos/seed/burger2/400/400',
|
||||||
],
|
],
|
||||||
|
|
@ -166,6 +228,35 @@ class MockOfferDataSource implements OfferDataSource {
|
||||||
longitude: 51.670,
|
longitude: 51.670,
|
||||||
originalPrice: 150000,
|
originalPrice: 150000,
|
||||||
finalPrice: 120000,
|
finalPrice: 120000,
|
||||||
|
features: [
|
||||||
|
"تهیه شده از بهترین و تازهترین مواد اولیه",
|
||||||
|
"محیطی دنج و مناسب برای قرارهای دوستانه",
|
||||||
|
"دارای منوی متنوع برای تمام سلیقهها",
|
||||||
|
],
|
||||||
|
discountInfo: const DiscountInfoModel(
|
||||||
|
name: "رفیقبازی",
|
||||||
|
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیقتو بیار و تخفیفتو ببر. دوستای بیشتر، تخفیف بیشتر.",
|
||||||
|
),
|
||||||
|
comments: [ // <-- بخش نظرات اضافه شد
|
||||||
|
CommentModel(
|
||||||
|
id: 'c1',
|
||||||
|
userName: 'سارا رضایی',
|
||||||
|
rating: 4.5,
|
||||||
|
comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر میزنم!',
|
||||||
|
publishedAt: DateTime.now().subtract(const Duration(days: 2)),
|
||||||
|
uploadedImageUrls: [
|
||||||
|
'https://picsum.photos/seed/user_img1/200/200',
|
||||||
|
'https://picsum.photos/seed/user_img2/200/200',
|
||||||
|
]
|
||||||
|
),
|
||||||
|
CommentModel(
|
||||||
|
id: 'c2',
|
||||||
|
userName: 'علی اکبری',
|
||||||
|
rating: 4,
|
||||||
|
comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.',
|
||||||
|
publishedAt: DateTime.now().subtract(const Duration(days: 5)),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
OfferModel(
|
OfferModel(
|
||||||
id: '4',
|
id: '4',
|
||||||
|
|
@ -219,6 +310,36 @@ class MockOfferDataSource implements OfferDataSource {
|
||||||
longitude: 51.670,
|
longitude: 51.670,
|
||||||
originalPrice: 150000,
|
originalPrice: 150000,
|
||||||
finalPrice: 120000,
|
finalPrice: 120000,
|
||||||
|
features: [
|
||||||
|
"تهیه شده از بهترین و تازهترین مواد اولیه",
|
||||||
|
"محیطی دنج و مناسب برای قرارهای دوستانه",
|
||||||
|
"دارای منوی متنوع برای تمام سلیقهها",
|
||||||
|
],
|
||||||
|
discountInfo: const DiscountInfoModel(
|
||||||
|
name: "رفیقبازی",
|
||||||
|
description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیقتو بیار و تخفیفتو ببر. دوستای بیشتر، تخفیف بیشتر.",
|
||||||
|
),
|
||||||
|
comments: [ // <-- بخش نظرات اضافه شد
|
||||||
|
CommentModel(
|
||||||
|
id: 'c1',
|
||||||
|
userName: 'سارا رضایی',
|
||||||
|
rating: 4.5,
|
||||||
|
comment: 'کیفیت برگر عالی بود و محیط خیلی خوبی داشت. حتما دوباره سر میزنم!',
|
||||||
|
publishedAt: DateTime.now().subtract(const Duration(days: 2)),
|
||||||
|
uploadedImageUrls: [
|
||||||
|
'https://picsum.photos/seed/user_img1/200/200',
|
||||||
|
'https://picsum.photos/seed/user_img2/200/200',
|
||||||
|
]
|
||||||
|
),
|
||||||
|
CommentModel(
|
||||||
|
id: 'c2',
|
||||||
|
userName: 'علی اکبری',
|
||||||
|
rating: 4,
|
||||||
|
comment: 'جای خوب و دنجی بود، فقط یکم شلوغ بود که طبیعیه. در کل راضی بودم.',
|
||||||
|
publishedAt: DateTime.now().subtract(const Duration(days: 5)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class DiscountInfoModel extends Equatable {
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
const DiscountInfoModel({
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [name, description];
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:proxibuy/data/models/comment_model.dart'; // <-- این خط اضافه شد
|
||||||
|
import 'package:proxibuy/data/models/discount_info_model.dart';
|
||||||
import 'package:proxibuy/data/models/working_hours.dart';
|
import 'package:proxibuy/data/models/working_hours.dart';
|
||||||
|
|
||||||
class OfferModel extends Equatable {
|
class OfferModel extends Equatable {
|
||||||
|
|
@ -20,6 +22,9 @@ class OfferModel extends Equatable {
|
||||||
final double longitude;
|
final double longitude;
|
||||||
final double originalPrice;
|
final double originalPrice;
|
||||||
final double finalPrice;
|
final double finalPrice;
|
||||||
|
final List<String> features;
|
||||||
|
final DiscountInfoModel? discountInfo;
|
||||||
|
final List<CommentModel> comments; // <-- این خط اضافه شد
|
||||||
|
|
||||||
const OfferModel({
|
const OfferModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -40,6 +45,9 @@ class OfferModel extends Equatable {
|
||||||
required this.longitude,
|
required this.longitude,
|
||||||
required this.originalPrice,
|
required this.originalPrice,
|
||||||
required this.finalPrice,
|
required this.finalPrice,
|
||||||
|
this.features = const [],
|
||||||
|
this.discountInfo,
|
||||||
|
this.comments = const [], // <-- این خط اضافه شد
|
||||||
});
|
});
|
||||||
|
|
||||||
String get coverImageUrl =>
|
String get coverImageUrl =>
|
||||||
|
|
@ -54,6 +62,9 @@ class OfferModel extends Equatable {
|
||||||
ratingCount,
|
ratingCount,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
|
features,
|
||||||
|
discountInfo,
|
||||||
|
comments, // <-- این خط اضافه شد
|
||||||
];
|
];
|
||||||
|
|
||||||
String get distanceAsString {
|
String get distanceAsString {
|
||||||
|
|
|
||||||
|
|
@ -95,10 +95,9 @@ class _OffersPageState extends State<OffersPage> {
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// چون asset مربوط به ویرایش وجود نداشت، از آیکون فلاتر استفاده شد
|
SvgPicture.asset(Assets.icons.edit.path),
|
||||||
const Icon(Icons.edit, size: 18, color: AppColors.primary),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Text('ویرایش'),
|
const Text('ویرایش',style: TextStyle(color: AppColors.active),),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:maps_launcher/maps_launcher.dart';
|
import 'package:maps_launcher/maps_launcher.dart';
|
||||||
|
|
@ -9,6 +10,7 @@ import 'package:proxibuy/data/repositories/offer_repository.dart';
|
||||||
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_bloc.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_event.dart';
|
||||||
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
|
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
|
||||||
|
import 'package:proxibuy/presentation/widgets/comments_section.dart';
|
||||||
import 'package:slide_countdown/slide_countdown.dart';
|
import 'package:slide_countdown/slide_countdown.dart';
|
||||||
|
|
||||||
class ProductDetailPage extends StatelessWidget {
|
class ProductDetailPage extends StatelessWidget {
|
||||||
|
|
@ -19,23 +21,76 @@ class ProductDetailPage extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => ProductDetailBloc(
|
create:
|
||||||
offerRepository: context.read<OfferRepository>(),
|
(context) => ProductDetailBloc(
|
||||||
)..add(ProductDetailFetchRequested(offerId: offerId)),
|
offerRepository: context.read<OfferRepository>(),
|
||||||
|
)..add(ProductDetailFetchRequested(offerId: offerId)),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: BlocBuilder<ProductDetailBloc, ProductDetailState>(
|
body: Stack(
|
||||||
builder: (context, state) {
|
children: [
|
||||||
if (state is ProductDetailLoadInProgress || state is ProductDetailInitial) {
|
BlocBuilder<ProductDetailBloc, ProductDetailState>(
|
||||||
return const Center(child: CircularProgressIndicator());
|
builder: (context, state) {
|
||||||
}
|
if (state is ProductDetailLoadInProgress ||
|
||||||
if (state is ProductDetailLoadFailure) {
|
state is ProductDetailInitial) {
|
||||||
return Center(child: Text('خطا: ${state.error}'));
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (state is ProductDetailLoadSuccess) {
|
if (state is ProductDetailLoadFailure) {
|
||||||
return ProductDetailView(offer: state.offer);
|
return Center(child: Text('خطا: ${state.error}'));
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
if (state is ProductDetailLoadSuccess) {
|
||||||
},
|
return ProductDetailView(offer: state.offer)
|
||||||
|
.animate()
|
||||||
|
.fadeIn(duration: 400.ms, curve: Curves.easeOut)
|
||||||
|
.slideY(
|
||||||
|
begin: 0.2,
|
||||||
|
duration: 400.ms,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.animate()
|
||||||
|
.fadeIn(
|
||||||
|
delay: 200.ms,
|
||||||
|
duration: 400.ms,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
)
|
||||||
|
.slideY(begin: 2, duration: 500.ms, curve: Curves.easeOut),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -73,6 +128,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
textDirection: TextDirection.rtl,
|
textDirection: TextDirection.rtl,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -80,6 +136,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
_buildMainImage(context),
|
_buildMainImage(context),
|
||||||
const SizedBox(height: 52.5),
|
const SizedBox(height: 52.5),
|
||||||
_buildProductInfo(),
|
_buildProductInfo(),
|
||||||
|
const SizedBox(height: 100),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|
@ -122,12 +179,13 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 40,
|
top: 40,
|
||||||
left: 16,
|
left: 16,
|
||||||
child: GestureDetector(
|
child:
|
||||||
child: SvgPicture.asset(Assets.icons.back.path),
|
GestureDetector(
|
||||||
onTap: () {
|
child: SvgPicture.asset(Assets.icons.back.path),
|
||||||
Navigator.pop(context);
|
onTap: () {
|
||||||
},
|
Navigator.pop(context);
|
||||||
),
|
},
|
||||||
|
).animate().fade(delay: 100.ms).scale(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -147,22 +205,40 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final img = imageList[index];
|
final img = imageList[index];
|
||||||
if (img == _uploadKey) {
|
if (img == _uploadKey) {
|
||||||
return _buildUploadButton();
|
return _buildUploadButton().animate().fade().scale(
|
||||||
|
delay: (index * 50).ms,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final isSelected = selectedImage == img;
|
final isSelected = selectedImage == img;
|
||||||
return _buildThumbnail(img, isSelected);
|
return _buildThumbnail(img, isSelected, index);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThumbnail(String img, bool isSelected) {
|
Widget _buildThumbnail(String img, bool isSelected, int index) {
|
||||||
const grayscaleMatrix = <double>[
|
const grayscaleMatrix = <double>[
|
||||||
0.2126, 0.7152, 0.0722, 0, 0,
|
0.2126,
|
||||||
0.2126, 0.7152, 0.0722, 0, 0,
|
0.7152,
|
||||||
0.2126, 0.7152, 0.0722, 0, 0,
|
0.0722,
|
||||||
0, 0, 0, 1, 0,
|
0,
|
||||||
|
0,
|
||||||
|
0.2126,
|
||||||
|
0.7152,
|
||||||
|
0.0722,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.2126,
|
||||||
|
0.7152,
|
||||||
|
0.0722,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
];
|
];
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
|
|
@ -186,17 +262,37 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: ColorFiltered(
|
child: ColorFiltered(
|
||||||
colorFilter: ColorFilter.matrix(isSelected ? <double>[
|
colorFilter: ColorFilter.matrix(
|
||||||
1, 0, 0, 0, 0,
|
isSelected
|
||||||
0, 1, 0, 0, 0,
|
? <double>[
|
||||||
0, 0, 1, 0, 0,
|
1,
|
||||||
0, 0, 0, 1, 0,
|
0,
|
||||||
] : grayscaleMatrix),
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
: grayscaleMatrix,
|
||||||
|
),
|
||||||
child: Image.network(img, width: 90, height: 90, fit: BoxFit.cover),
|
child: Image.network(img, width: 90, height: 90, fit: BoxFit.cover),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
).animate().fade().scale(delay: (index * 50).ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUploadButton() {
|
Widget _buildUploadButton() {
|
||||||
|
|
@ -221,197 +317,490 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProductInfo() {
|
Widget _buildProductInfo() {
|
||||||
final remainingDuration = widget.offer.expiryTime.isAfter(DateTime.now())
|
final remainingDuration =
|
||||||
? widget.offer.expiryTime.difference(DateTime.now())
|
widget.offer.expiryTime.isAfter(DateTime.now())
|
||||||
: Duration.zero;
|
? widget.offer.expiryTime.difference(DateTime.now())
|
||||||
|
: Duration.zero;
|
||||||
|
|
||||||
return Padding(
|
final animationList = <Widget>[
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0).copyWith(bottom: 24.0, top: 24.0),
|
Row(
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
SvgPicture.asset(Assets.icons.shop.path, height: 30),
|
||||||
children: [
|
const SizedBox(width: 6),
|
||||||
SvgPicture.asset(Assets.icons.shop.path, height: 30),
|
Expanded(
|
||||||
const SizedBox(width: 6),
|
child: Text(
|
||||||
Text(widget.offer.storeName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
widget.offer.storeName,
|
||||||
],
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildInfoRow(icon: Assets.icons.location, text: widget.offer.address),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
ExpandableInfoRow(
|
|
||||||
icon: Assets.icons.clock,
|
|
||||||
titleWidget: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.offer.isOpen ? "باز است" : "بسته است",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: widget.offer.isOpen ? Colors.green.shade700 : Colors.red.shade700,
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
children: widget.offer.workingHours.map((wh) => Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 10.0, bottom: 8.0, top: 4.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(wh.day, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
|
|
||||||
wh.isOpen
|
|
||||||
? Text(wh.shifts.map((s) => '${s.openAt} - ${s.closeAt}').join(' | '), style: const TextStyle(fontSize: 15, color: AppColors.hint))
|
|
||||||
: const Text('تعطیل', style: TextStyle(fontSize: 15, color: Colors.red)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)).toList(),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
InkWell(
|
|
||||||
onTap: () => _launchMaps(widget.offer.latitude, widget.offer.longitude, widget.offer.storeName),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
Assets.icons.map.path,
|
|
||||||
width: 25,
|
|
||||||
height: 25,
|
|
||||||
colorFilter: const ColorFilter.mode(AppColors.button, BlendMode.srcIn),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text('مسیر فروشگاه روی نقشه', style: TextStyle(fontSize: 17, color: AppColors.button)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 60,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: AppColors.selectedImg,
|
|
||||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(widget.offer.rating.toString(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 11)),
|
|
||||||
const SizedBox(width: 2),
|
|
||||||
SvgPicture.asset(Assets.icons.star.path, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 30,
|
|
||||||
width: 60,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[200],
|
|
||||||
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8)),
|
|
||||||
),
|
|
||||||
child: Center(child: Text('${widget.offer.ratingCount} نفر', style: const TextStyle(color: Colors.black, fontSize: 11, fontWeight: FontWeight.bold))),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 30),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.singleOfferType,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Text("تخفیف ${widget.offer.discountType}", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text('(${widget.offer.discount})', style: const TextStyle(fontSize: 16, color: AppColors.singleOfferType, fontWeight: FontWeight.normal)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
widget.offer.originalPrice.toStringAsFixed(0),
|
|
||||||
style: TextStyle(fontSize: 16, color: Colors.grey.shade600, decoration: TextDecoration.lineThrough),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 1),
|
|
||||||
Text('${widget.offer.finalPrice.toStringAsFixed(0)} تومان', style: const TextStyle(color: AppColors.singleOfferType, fontSize: 22, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildInfoRow(icon: Assets.icons.timerPause, text: "مهلت استفاده از تخفیف"),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
if (remainingDuration > Duration.zero)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Localizations.override(
|
|
||||||
context: context,
|
|
||||||
locale: const Locale('en'),
|
|
||||||
child: SlideCountdown(
|
|
||||||
duration: remainingDuration,
|
|
||||||
slideDirection: SlideDirection.up,
|
|
||||||
separator: ':',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 85,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.countdown,
|
|
||||||
),
|
|
||||||
separatorStyle: const TextStyle(
|
|
||||||
fontSize: 40,
|
|
||||||
color: AppColors.countdown,
|
|
||||||
),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
shouldShowDays: (duration) => duration.inDays > 0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 60.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text("ثانیه", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)),
|
|
||||||
Text("دقیقه", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)),
|
|
||||||
Text("ساعت", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildInfoRow(icon: Assets.icons.location, text: widget.offer.address),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ExpandableInfoRow(
|
||||||
|
icon: Assets.icons.clock,
|
||||||
|
titleWidget: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.offer.isOpen ? "باز است" : "بسته است",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color:
|
||||||
|
widget.offer.isOpen
|
||||||
|
? Colors.green.shade700
|
||||||
|
: Colors.red.shade700,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
children:
|
||||||
|
widget.offer.workingHours
|
||||||
|
.map(
|
||||||
|
(wh) => Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 10.0,
|
||||||
|
bottom: 8.0,
|
||||||
|
top: 4.0,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
wh.day,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
wh.isOpen
|
||||||
|
? Text(
|
||||||
|
wh.shifts
|
||||||
|
.map((s) => '${s.openAt} - ${s.closeAt}')
|
||||||
|
.join(' | '),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: AppColors.hint,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'تعطیل',
|
||||||
|
style: TextStyle(fontSize: 15, color: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap:
|
||||||
|
() => _launchMaps(
|
||||||
|
widget.offer.latitude,
|
||||||
|
widget.offer.longitude,
|
||||||
|
widget.offer.storeName,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.icons.map.path,
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
colorFilter: const ColorFilter.mode(
|
||||||
|
AppColors.button,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
'مسیر فروشگاه روی نقشه',
|
||||||
|
style: TextStyle(fontSize: 17, color: AppColors.button),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 60,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.selectedImg,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(8),
|
||||||
|
topRight: Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.offer.rating.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.icons.star.path,
|
||||||
|
colorFilter: const ColorFilter.mode(
|
||||||
|
Colors.white,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 30,
|
||||||
|
width: 60,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(8),
|
||||||
|
bottomRight: Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'${widget.offer.ratingCount} نفر',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.singleOfferType,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"تخفیف ${widget.offer.discountType}",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 17,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'(${widget.offer.discount})',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: AppColors.singleOfferType,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
widget.offer.originalPrice.toStringAsFixed(0),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 1),
|
||||||
|
Text(
|
||||||
|
'${widget.offer.finalPrice.toStringAsFixed(0)} تومان',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.singleOfferType,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildInfoRow(
|
||||||
|
icon: Assets.icons.timerPause,
|
||||||
|
text: "مهلت استفاده از تخفیف",
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
if (remainingDuration > Duration.zero)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Localizations.override(
|
||||||
|
context: context,
|
||||||
|
locale: const Locale('en'),
|
||||||
|
child: SlideCountdown(
|
||||||
|
duration: remainingDuration,
|
||||||
|
slideDirection: SlideDirection.up,
|
||||||
|
separator: ':',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 70,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.countdown,
|
||||||
|
),
|
||||||
|
separatorStyle: const TextStyle(
|
||||||
|
fontSize: 40,
|
||||||
|
color: AppColors.countdown,
|
||||||
|
),
|
||||||
|
decoration: const BoxDecoration(color: Colors.white),
|
||||||
|
shouldShowDays: (d) => d.inDays > 0,
|
||||||
|
shouldShowHours: (d) => d.inHours > 0,
|
||||||
|
shouldShowMinutes:
|
||||||
|
(d) =>
|
||||||
|
d.inSeconds >
|
||||||
|
0, // دقیقه تا آخرین ثانیه نمایش داده میشود
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_buildTimerLabels(remainingDuration),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildFeaturesSection(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildDiscountTypeSection(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
CommentsSection(comments: widget.offer.comments),
|
||||||
|
].animate(interval: 80.ms).fade(duration: 300.ms).slideX(begin: -0.1);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24.0,
|
||||||
|
).copyWith(bottom: 24.0, top: 24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: animationList
|
||||||
|
.animate(interval: 80.ms)
|
||||||
|
.fade(duration: 300.ms)
|
||||||
|
.slideX(begin: -0.1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimerLabels(Duration duration) {
|
||||||
|
const double columnWidth = 80;
|
||||||
|
const labelStyle = TextStyle(fontSize: 14, color: AppColors.selectedImg);
|
||||||
|
|
||||||
|
List<Widget> labels = [];
|
||||||
|
|
||||||
|
if (duration.inDays > 0) {
|
||||||
|
labels = [
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("ثانیه", style: labelStyle)),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("دقیقه", style: labelStyle)),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("ساعت", style: labelStyle)),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("روز", style: labelStyle)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else if (duration.inHours > 0) {
|
||||||
|
labels = [
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("ثانیه", style: labelStyle)),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("دقیقه", style: labelStyle)),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("ساعت", style: labelStyle)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else if (duration.inSeconds > 0) {
|
||||||
|
labels = [
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("ثانیه", style: labelStyle)),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: columnWidth,
|
||||||
|
child: Center(child: Text("دقیقه", style: labelStyle)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(mainAxisAlignment: MainAxisAlignment.center, children: labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFeaturesSection() {
|
||||||
|
if (widget.offer.features.isEmpty) return const SizedBox.shrink();
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'ویژگیها',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: Divider(color: Colors.grey[400], thickness: 1)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
...widget.offer.features.map(
|
||||||
|
(feature) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.icons.tickSquare.path,
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
colorFilter: const ColorFilter.mode(
|
||||||
|
AppColors.confirm,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
feature,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.4,
|
||||||
|
color: AppColors.hint,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDiscountTypeSection() {
|
||||||
|
final info = widget.offer.discountInfo;
|
||||||
|
if (info == null) return const SizedBox.shrink();
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'نوع تخفیف',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: Divider(color: Colors.grey[400], thickness: 1)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
Assets.icons.tickSquare.path,
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
colorFilter: const ColorFilter.mode(
|
||||||
|
AppColors.confirm,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"${widget.offer.discount} تخفیف ${info.name}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.4,
|
||||||
|
color: AppColors.confirm,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 7),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 0.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: SvgPicture.asset(Assets.icons.warning2.path, height: 24),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
info.description,
|
||||||
|
style: const TextStyle(fontSize: 14, color: AppColors.hint),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInfoRow({required SvgGenImage icon, required String text}) {
|
Widget _buildInfoRow({required SvgGenImage icon, required String text}) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(icon.path, width: 22, height: 22, colorFilter: ColorFilter.mode(Colors.grey.shade600, BlendMode.srcIn)),
|
SvgPicture.asset(
|
||||||
|
icon.path,
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
colorFilter: ColorFilter.mode(Colors.grey.shade600, BlendMode.srcIn),
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 16, color: AppColors.hint))),
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(fontSize: 16, color: AppColors.hint),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -452,14 +841,25 @@ class _ExpandableInfoRowState extends State<ExpandableInfoRow> {
|
||||||
onTap: _toggleExpand,
|
onTap: _toggleExpand,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(widget.icon.path, width: 22, height: 22, colorFilter: ColorFilter.mode(Colors.grey.shade600, BlendMode.srcIn)),
|
SvgPicture.asset(
|
||||||
|
widget.icon.path,
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Colors.grey.shade600,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(child: widget.titleWidget),
|
Expanded(child: widget.titleWidget),
|
||||||
if (widget.children.isNotEmpty)
|
if (widget.children.isNotEmpty)
|
||||||
AnimatedRotation(
|
AnimatedRotation(
|
||||||
turns: _isExpanded ? 0.5 : 0,
|
turns: _isExpanded ? 0.5 : 0,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: const Icon(Icons.keyboard_arrow_down, color: Colors.black),
|
child: const Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -467,10 +867,13 @@ class _ExpandableInfoRowState extends State<ExpandableInfoRow> {
|
||||||
AnimatedCrossFade(
|
AnimatedCrossFade(
|
||||||
firstChild: Container(),
|
firstChild: Container(),
|
||||||
secondChild: Column(children: widget.children),
|
secondChild: Column(children: widget.children),
|
||||||
crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
crossFadeState:
|
||||||
|
_isExpanded
|
||||||
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:proxibuy/data/models/offer_model.dart';
|
|
||||||
import 'package:proxibuy/data/repositories/offer_repository.dart';
|
import 'package:proxibuy/data/repositories/offer_repository.dart';
|
||||||
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart';
|
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart';
|
||||||
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
|
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
|
||||||
|
|
@ -21,7 +19,6 @@ class ProductDetailBloc extends Bloc<ProductDetailEvent, ProductDetailState> {
|
||||||
) async {
|
) async {
|
||||||
emit(ProductDetailLoadInProgress());
|
emit(ProductDetailLoadInProgress());
|
||||||
try {
|
try {
|
||||||
// از ریپازیتوری میخواهیم که محصول با این ID را به ما بدهد
|
|
||||||
final offer = await _offerRepository.fetchOfferById(event.offerId);
|
final offer = await _offerRepository.fetchOfferById(event.offerId);
|
||||||
if (offer != null) {
|
if (offer != null) {
|
||||||
emit(ProductDetailLoadSuccess(offer));
|
emit(ProductDetailLoadSuccess(offer));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||||
|
import 'package:proxibuy/data/models/comment_model.dart';
|
||||||
|
import 'package:proxibuy/presentation/widgets/user_comment_card.dart';
|
||||||
|
|
||||||
|
class CommentsSection extends StatelessWidget {
|
||||||
|
final List<CommentModel> comments;
|
||||||
|
|
||||||
|
const CommentsSection({super.key, required this.comments});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (comments.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 10,),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(Assets.icons.line2.path),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text('نظرات کاربران', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: comments.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return UserCommentCard(comment: comments[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'package:proxibuy/core/config/app_colors.dart';
|
||||||
|
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||||
|
import 'package:proxibuy/data/models/comment_model.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
|
class CustomStarRating extends StatelessWidget {
|
||||||
|
final double rating;
|
||||||
|
final int starCount;
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
const CustomStarRating({
|
||||||
|
super.key,
|
||||||
|
required this.rating,
|
||||||
|
this.starCount = 5,
|
||||||
|
this.size = 10.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<Widget> stars = [];
|
||||||
|
double remaining = rating;
|
||||||
|
|
||||||
|
for (int i = 0; i < starCount; i++) {
|
||||||
|
if (remaining >= 1) {
|
||||||
|
stars.add(_buildStar(Assets.icons.starFill.path));
|
||||||
|
remaining -= 1;
|
||||||
|
} else if (remaining >= 0.5) {
|
||||||
|
stars.add(_buildStar(Assets.icons.star2.path));
|
||||||
|
remaining = 0;
|
||||||
|
} else {
|
||||||
|
stars.add(_buildStar(Assets.icons.starHalf.path,));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Row(mainAxisSize: MainAxisSize.min, children: stars);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStar(String asset, {Color? color}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 1),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
asset,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
colorFilter: color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserCommentCard extends StatelessWidget {
|
||||||
|
final CommentModel comment;
|
||||||
|
|
||||||
|
const UserCommentCard({super.key, required this.comment});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final String formattedDate = intl.DateFormat('yyyy/MM/dd', 'fa').format(comment.publishedAt);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
// color: Colors.white,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
comment.userName,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
formattedDate,
|
||||||
|
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
CustomStarRating(rating: comment.rating, size: 18),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'"${comment.comment}"',
|
||||||
|
style: const TextStyle(fontSize: 14, height: 1.6, color: Colors.black87),
|
||||||
|
),
|
||||||
|
if (comment.uploadedImageUrls.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'عکسهای آپلود شده توسط کاربر',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
color: AppColors.active,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
height: 80,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: comment.uploadedImageUrls.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final imageUrl = comment.uploadedImageUrls[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
child: Image.network(
|
||||||
|
imageUrl,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||