440 lines
14 KiB
Dart
440 lines
14 KiB
Dart
import 'dart:async';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:proxibuy/core/gen/assets.gen.dart';
|
|
import 'package:qr_flutter/qr_flutter.dart';
|
|
import 'package:proxibuy/core/config/app_colors.dart';
|
|
import 'package:proxibuy/data/models/offer_model.dart';
|
|
import 'package:slide_countdown/slide_countdown.dart';
|
|
|
|
class ReservedListItemCard extends StatefulWidget {
|
|
final OfferModel offer;
|
|
|
|
const ReservedListItemCard({super.key, required this.offer});
|
|
|
|
@override
|
|
State<ReservedListItemCard> createState() => _ReservedListItemCardState();
|
|
}
|
|
|
|
class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|
bool _isExpanded = false;
|
|
Timer? _timer;
|
|
Duration _remaining = Duration.zero;
|
|
Future<String>? _qrTokenFuture;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_calculateRemainingTime();
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
_calculateRemainingTime();
|
|
});
|
|
}
|
|
|
|
Future<String> _generateQrToken() async {
|
|
const storage = FlutterSecureStorage();
|
|
final userID = await storage.read(key: 'userID');
|
|
|
|
if (userID == null) {
|
|
throw Exception("User ID not found");
|
|
}
|
|
|
|
final payload = {
|
|
'userID': userID,
|
|
'discountID': widget.offer.id,
|
|
'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
};
|
|
|
|
const secretKey = 'your_super_secret_key_for_qr';
|
|
final jwt = JWT(payload);
|
|
final token = jwt.sign(SecretKey(secretKey));
|
|
|
|
return token;
|
|
}
|
|
|
|
void _toggleExpansion() {
|
|
setState(() {
|
|
_isExpanded = !_isExpanded;
|
|
if (_isExpanded && _qrTokenFuture == null) {
|
|
_qrTokenFuture = _generateQrToken();
|
|
}
|
|
});
|
|
}
|
|
|
|
void _calculateRemainingTime() {
|
|
final now = DateTime.now();
|
|
if (widget.offer.expiryTime.isAfter(now)) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_remaining = widget.offer.expiryTime.difference(now);
|
|
});
|
|
}
|
|
} else {
|
|
if (mounted) {
|
|
setState(() => _remaining = Duration.zero);
|
|
}
|
|
_timer?.cancel();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isExpired = _remaining <= Duration.zero;
|
|
|
|
final cardContent = Column(
|
|
children: [
|
|
Card(
|
|
color: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
side: BorderSide(color: Colors.grey.shade300, width: 1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
elevation: 0,
|
|
clipBehavior: Clip.antiAlias,
|
|
margin: EdgeInsets.zero,
|
|
child: _buildOfferPrimaryDetails(),
|
|
),
|
|
_buildActionsRow(),
|
|
_buildExpansionPanel(),
|
|
],
|
|
);
|
|
|
|
if (isExpired) {
|
|
return ColorFiltered(
|
|
colorFilter: const ColorFilter.matrix(<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,
|
|
]),
|
|
child: cardContent,
|
|
);
|
|
}
|
|
|
|
return cardContent;
|
|
}
|
|
|
|
Widget _buildOfferPrimaryDetails() {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(15, 25, 15, 25),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: CachedNetworkImage(
|
|
imageUrl: widget.offer.coverImageUrl,
|
|
width: 90,
|
|
height: 90,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
SvgPicture.asset(Assets.icons.shop.path),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
widget.offer.storeName,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.normal,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
SvgPicture.asset(Assets.icons.shoppingCart.path),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
widget.offer.title,
|
|
style: TextStyle(color: AppColors.hint, fontSize: 14),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
SvgPicture.asset(Assets.icons.location.path),
|
|
SizedBox(width: 8),
|
|
Flexible(
|
|
child: Text(
|
|
"${widget.offer.address} (${widget.offer.distanceInMeters} متر تا تخفیف)",
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.normal,
|
|
color: AppColors.hint,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionsRow() {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
if (_remaining > Duration.zero)
|
|
Column(
|
|
children: [
|
|
Localizations.override(
|
|
context: context,
|
|
locale: const Locale('en'),
|
|
child: SlideCountdown(
|
|
duration: _remaining,
|
|
slideDirection: SlideDirection.up,
|
|
separator: ':',
|
|
style: const TextStyle(
|
|
fontSize: 25,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.countdown,
|
|
),
|
|
separatorStyle: const TextStyle(
|
|
fontSize: 20,
|
|
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(_remaining),
|
|
],
|
|
)
|
|
else
|
|
const Text(
|
|
'منقضی شده',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
SizedBox(width: 10),
|
|
TextButton(
|
|
onPressed: _toggleExpansion,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
_isExpanded ? 'بستن' : 'اطلاعات بیشتر',
|
|
style: TextStyle(color: AppColors.active),
|
|
),
|
|
const SizedBox(width: 12),
|
|
AnimatedRotation(
|
|
turns: _isExpanded ? 0.5 : 0,
|
|
duration: const Duration(milliseconds: 300),
|
|
child: SvgPicture.asset(
|
|
Assets.icons.arrowDown.path,
|
|
height: 20,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTimerLabels(Duration duration) {
|
|
const double columnWidth = 40;
|
|
const labelStyle = TextStyle(fontSize: 10, 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 _buildExpansionPanel() {
|
|
final formatCurrency = NumberFormat.decimalPattern('fa_IR');
|
|
|
|
return AnimatedCrossFade(
|
|
firstChild: Container(),
|
|
secondChild: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
|
width: double.infinity,
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 15,
|
|
vertical: 12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.singleOfferType,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
"تخفیف ${widget.offer.discountType}",
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.normal,
|
|
fontSize: 13,
|
|
overflow: TextOverflow.ellipsis
|
|
),
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: AppColors.singleOfferType,
|
|
fontWeight: FontWeight.normal,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
formatCurrency.format(widget.offer.originalPrice),
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.grey.shade600,
|
|
decoration: TextDecoration.lineThrough,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 1),
|
|
Text(
|
|
'${formatCurrency.format(widget.offer.finalPrice)} تومان',
|
|
style: const TextStyle(
|
|
color: AppColors.singleOfferType,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Color.fromARGB(255, 246, 246, 246),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: FutureBuilder<String>(
|
|
future: _qrTokenFuture,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const SizedBox(
|
|
height: 280.0,
|
|
child: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
if (snapshot.hasError) {
|
|
return const SizedBox(
|
|
height: 280.0,
|
|
child: Center(child: Text("خطا در ساخت کد QR")),
|
|
);
|
|
}
|
|
if (snapshot.hasData) {
|
|
return QrImageView(
|
|
data: snapshot.data!,
|
|
version: QrVersions.auto,
|
|
size: 280.0,
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
crossFadeState:
|
|
_isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
|
duration: const Duration(milliseconds: 300),
|
|
);
|
|
}
|
|
} |