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
|
||||
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
|
||||
SvgGenImage get logoWithName =>
|
||||
const SvgGenImage('assets/icons/LogoWithName.svg');
|
||||
|
|
@ -127,6 +130,10 @@ class $AssetsIconsGen {
|
|||
/// File path: 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
|
||||
SvgGenImage get resturan => const SvgGenImage('assets/icons/resturan.svg');
|
||||
|
||||
|
|
@ -144,9 +151,18 @@ class $AssetsIconsGen {
|
|||
SvgGenImage get shoppingCart =>
|
||||
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
|
||||
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
|
||||
SvgGenImage get tala => const SvgGenImage('assets/icons/tala.svg');
|
||||
|
||||
|
|
@ -157,6 +173,10 @@ class $AssetsIconsGen {
|
|||
SvgGenImage get tickCircle =>
|
||||
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
|
||||
SvgGenImage get tickPb => const SvgGenImage('assets/icons/tickPb.svg');
|
||||
|
||||
|
|
@ -168,6 +188,9 @@ class $AssetsIconsGen {
|
|||
SvgGenImage get volumeHigh =>
|
||||
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<SvgGenImage> get values => [
|
||||
arrowRight2,
|
||||
|
|
@ -177,6 +200,7 @@ class $AssetsIconsGen {
|
|||
ellipse1,
|
||||
gold,
|
||||
googleSvg,
|
||||
line2,
|
||||
logoWithName,
|
||||
makeup,
|
||||
mobile,
|
||||
|
|
@ -205,18 +229,24 @@ class $AssetsIconsGen {
|
|||
map,
|
||||
notification,
|
||||
pooshak,
|
||||
receiptDisscount,
|
||||
resturan,
|
||||
routing,
|
||||
scanBarcode,
|
||||
shop,
|
||||
shoppingCart,
|
||||
starFill,
|
||||
starHalf,
|
||||
star,
|
||||
star2,
|
||||
tala,
|
||||
teria,
|
||||
tickCircle,
|
||||
tickSquare,
|
||||
tickPb,
|
||||
timerPause,
|
||||
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/working_hours.dart';
|
||||
|
||||
|
|
@ -60,6 +62,36 @@ class MockOfferDataSource implements OfferDataSource {
|
|||
longitude: 51.670,
|
||||
originalPrice: 150000,
|
||||
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(
|
||||
id: '2',
|
||||
|
|
@ -103,7 +135,7 @@ class MockOfferDataSource implements OfferDataSource {
|
|||
Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'),
|
||||
],
|
||||
),
|
||||
WorkingHours(day: 'جمعه', shifts: []), // تعطیل
|
||||
WorkingHours(day: 'جمعه', shifts: []),
|
||||
],
|
||||
discountType: 'رفیقبازی',
|
||||
isOpen: true,
|
||||
|
|
@ -113,6 +145,36 @@ class MockOfferDataSource implements OfferDataSource {
|
|||
longitude: 51.670,
|
||||
originalPrice: 150000,
|
||||
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(
|
||||
id: '3',
|
||||
|
|
@ -120,7 +182,7 @@ class MockOfferDataSource implements OfferDataSource {
|
|||
title: 'چیزبرگر',
|
||||
discount: '۲۰٪',
|
||||
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/burger2/400/400',
|
||||
],
|
||||
|
|
@ -166,6 +228,35 @@ class MockOfferDataSource implements OfferDataSource {
|
|||
longitude: 51.670,
|
||||
originalPrice: 150000,
|
||||
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(
|
||||
id: '4',
|
||||
|
|
@ -219,6 +310,36 @@ class MockOfferDataSource implements OfferDataSource {
|
|||
longitude: 51.670,
|
||||
originalPrice: 150000,
|
||||
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:proxibuy/data/models/comment_model.dart'; // <-- این خط اضافه شد
|
||||
import 'package:proxibuy/data/models/discount_info_model.dart';
|
||||
import 'package:proxibuy/data/models/working_hours.dart';
|
||||
|
||||
class OfferModel extends Equatable {
|
||||
|
|
@ -20,6 +22,9 @@ class OfferModel extends Equatable {
|
|||
final double longitude;
|
||||
final double originalPrice;
|
||||
final double finalPrice;
|
||||
final List<String> features;
|
||||
final DiscountInfoModel? discountInfo;
|
||||
final List<CommentModel> comments; // <-- این خط اضافه شد
|
||||
|
||||
const OfferModel({
|
||||
required this.id,
|
||||
|
|
@ -40,6 +45,9 @@ class OfferModel extends Equatable {
|
|||
required this.longitude,
|
||||
required this.originalPrice,
|
||||
required this.finalPrice,
|
||||
this.features = const [],
|
||||
this.discountInfo,
|
||||
this.comments = const [], // <-- این خط اضافه شد
|
||||
});
|
||||
|
||||
String get coverImageUrl =>
|
||||
|
|
@ -54,6 +62,9 @@ class OfferModel extends Equatable {
|
|||
ratingCount,
|
||||
latitude,
|
||||
longitude,
|
||||
features,
|
||||
discountInfo,
|
||||
comments, // <-- این خط اضافه شد
|
||||
];
|
||||
|
||||
String get distanceAsString {
|
||||
|
|
|
|||
|
|
@ -95,10 +95,9 @@ class _OffersPageState extends State<OffersPage> {
|
|||
},
|
||||
child: Row(
|
||||
children: [
|
||||
// چون asset مربوط به ویرایش وجود نداشت، از آیکون فلاتر استفاده شد
|
||||
const Icon(Icons.edit, size: 18, color: AppColors.primary),
|
||||
SvgPicture.asset(Assets.icons.edit.path),
|
||||
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_animate/flutter_animate.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.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_event.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';
|
||||
|
||||
class ProductDetailPage extends StatelessWidget {
|
||||
|
|
@ -19,24 +21,77 @@ class ProductDetailPage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ProductDetailBloc(
|
||||
create:
|
||||
(context) => ProductDetailBloc(
|
||||
offerRepository: context.read<OfferRepository>(),
|
||||
)..add(ProductDetailFetchRequested(offerId: offerId)),
|
||||
child: Scaffold(
|
||||
body: BlocBuilder<ProductDetailBloc, ProductDetailState>(
|
||||
body: Stack(
|
||||
children: [
|
||||
BlocBuilder<ProductDetailBloc, ProductDetailState>(
|
||||
builder: (context, state) {
|
||||
if (state is ProductDetailLoadInProgress || state is ProductDetailInitial) {
|
||||
if (state is ProductDetailLoadInProgress ||
|
||||
state is ProductDetailInitial) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (state is ProductDetailLoadFailure) {
|
||||
return Center(child: Text('خطا: ${state.error}'));
|
||||
}
|
||||
if (state is ProductDetailLoadSuccess) {
|
||||
return ProductDetailView(offer: state.offer);
|
||||
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,
|
||||
child: SingleChildScrollView(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -80,6 +136,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
_buildMainImage(context),
|
||||
const SizedBox(height: 52.5),
|
||||
_buildProductInfo(),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
|
|
@ -122,12 +179,13 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
Positioned(
|
||||
top: 40,
|
||||
left: 16,
|
||||
child: GestureDetector(
|
||||
child:
|
||||
GestureDetector(
|
||||
child: SvgPicture.asset(Assets.icons.back.path),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
).animate().fade(delay: 100.ms).scale(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -147,22 +205,40 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
itemBuilder: (context, index) {
|
||||
final img = imageList[index];
|
||||
if (img == _uploadKey) {
|
||||
return _buildUploadButton();
|
||||
return _buildUploadButton().animate().fade().scale(
|
||||
delay: (index * 50).ms,
|
||||
);
|
||||
}
|
||||
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>[
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
0.2126,
|
||||
0.7152,
|
||||
0.0722,
|
||||
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(
|
||||
|
|
@ -186,17 +262,37 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.matrix(isSelected ? <double>[
|
||||
1, 0, 0, 0, 0,
|
||||
0, 1, 0, 0, 0,
|
||||
0, 0, 1, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
] : grayscaleMatrix),
|
||||
colorFilter: ColorFilter.matrix(
|
||||
isSelected
|
||||
? <double>[
|
||||
1,
|
||||
0,
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
).animate().fade().scale(delay: (index * 50).ms);
|
||||
}
|
||||
|
||||
Widget _buildUploadButton() {
|
||||
|
|
@ -221,20 +317,23 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
}
|
||||
|
||||
Widget _buildProductInfo() {
|
||||
final remainingDuration = widget.offer.expiryTime.isAfter(DateTime.now())
|
||||
final remainingDuration =
|
||||
widget.offer.expiryTime.isAfter(DateTime.now())
|
||||
? widget.offer.expiryTime.difference(DateTime.now())
|
||||
: Duration.zero;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0).copyWith(bottom: 24.0, top: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
final animationList = <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
SvgPicture.asset(Assets.icons.shop.path, height: 30),
|
||||
const SizedBox(width: 6),
|
||||
Text(widget.offer.storeName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.offer.storeName,
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -248,24 +347,53 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
widget.offer.isOpen ? "باز است" : "بسته است",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: widget.offer.isOpen ? Colors.green.shade700 : Colors.red.shade700,
|
||||
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),
|
||||
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)),
|
||||
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)),
|
||||
? 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(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
|
|
@ -273,7 +401,12 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => _launchMaps(widget.offer.latitude, widget.offer.longitude, widget.offer.storeName),
|
||||
onTap:
|
||||
() => _launchMaps(
|
||||
widget.offer.latitude,
|
||||
widget.offer.longitude,
|
||||
widget.offer.storeName,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
|
|
@ -283,10 +416,16 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
Assets.icons.map.path,
|
||||
width: 25,
|
||||
height: 25,
|
||||
colorFilter: const ColorFilter.mode(AppColors.button, BlendMode.srcIn),
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.button,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('مسیر فروشگاه روی نقشه', style: TextStyle(fontSize: 17, color: AppColors.button)),
|
||||
const Text(
|
||||
'مسیر فروشگاه روی نقشه',
|
||||
style: TextStyle(fontSize: 17, color: AppColors.button),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -299,18 +438,32 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
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)),
|
||||
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)),
|
||||
],
|
||||
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,
|
||||
|
|
@ -318,9 +471,21 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
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)),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(child: Text('${widget.offer.ratingCount} نفر', style: const TextStyle(color: Colors.black, fontSize: 11, fontWeight: FontWeight.bold))),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -337,7 +502,14 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
color: AppColors.singleOfferType,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text("تخفیف ${widget.offer.discountType}", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
child: Text(
|
||||
"تخفیف ${widget.offer.discountType}",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
|
@ -346,22 +518,43 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('(${widget.offer.discount})', style: const TextStyle(fontSize: 16, color: AppColors.singleOfferType, fontWeight: FontWeight.normal)),
|
||||
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),
|
||||
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)),
|
||||
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: "مهلت استفاده از تخفیف"),
|
||||
_buildInfoRow(
|
||||
icon: Assets.icons.timerPause,
|
||||
text: "مهلت استفاده از تخفیف",
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (remainingDuration > Duration.zero)
|
||||
Column(
|
||||
|
|
@ -374,7 +567,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
slideDirection: SlideDirection.up,
|
||||
separator: ':',
|
||||
style: const TextStyle(
|
||||
fontSize: 85,
|
||||
fontSize: 70,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.countdown,
|
||||
),
|
||||
|
|
@ -382,36 +575,232 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
fontSize: 40,
|
||||
color: AppColors.countdown,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
),
|
||||
shouldShowDays: (duration) => duration.inDays > 0,
|
||||
decoration: const BoxDecoration(color: Colors.white),
|
||||
shouldShowDays: (d) => d.inDays > 0,
|
||||
shouldShowHours: (d) => d.inHours > 0,
|
||||
shouldShowMinutes:
|
||||
(d) =>
|
||||
d.inSeconds >
|
||||
0, // دقیقه تا آخرین ثانیه نمایش داده میشود
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 60.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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: [
|
||||
Text("ثانیه", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)),
|
||||
Text("دقیقه", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)),
|
||||
Text("ساعت", style: TextStyle(fontSize: 14, color: AppColors.selectedImg)),
|
||||
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}) {
|
||||
return Row(
|
||||
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),
|
||||
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,
|
||||
child: Row(
|
||||
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),
|
||||
Expanded(child: widget.titleWidget),
|
||||
if (widget.children.isNotEmpty)
|
||||
AnimatedRotation(
|
||||
turns: _isExpanded ? 0.5 : 0,
|
||||
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,7 +867,10 @@ class _ExpandableInfoRowState extends State<ExpandableInfoRow> {
|
|||
AnimatedCrossFade(
|
||||
firstChild: Container(),
|
||||
secondChild: Column(children: widget.children),
|
||||
crossFadeState: _isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
crossFadeState:
|
||||
_isExpanded
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
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/presentation/product_detail/bloc/product_detail_event.dart';
|
||||
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
|
||||
|
|
@ -21,7 +19,6 @@ class ProductDetailBloc extends Bloc<ProductDetailEvent, ProductDetailState> {
|
|||
) async {
|
||||
emit(ProductDetailLoadInProgress());
|
||||
try {
|
||||
// از ریپازیتوری میخواهیم که محصول با این ID را به ما بدهد
|
||||
final offer = await _offerRepository.fetchOfferById(event.offerId);
|
||||
if (offer != null) {
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||