add promotion/detail
This commit is contained in:
@@ -6,8 +6,10 @@ library;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:worker/features/home/presentation/pages/home_page.dart';
|
import 'package:worker/features/home/domain/entities/promotion.dart';
|
||||||
|
import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
|
||||||
import 'package:worker/features/products/presentation/pages/products_page.dart';
|
import 'package:worker/features/products/presentation/pages/products_page.dart';
|
||||||
|
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
|
||||||
|
|
||||||
/// App Router
|
/// App Router
|
||||||
///
|
///
|
||||||
@@ -25,17 +27,17 @@ class AppRouter {
|
|||||||
|
|
||||||
// Route definitions
|
// Route definitions
|
||||||
routes: [
|
routes: [
|
||||||
// Home Route
|
// Main Route (with bottom navigation)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.home,
|
path: RouteNames.home,
|
||||||
name: RouteNames.home,
|
name: RouteNames.home,
|
||||||
pageBuilder: (context, state) => MaterialPage(
|
pageBuilder: (context, state) => MaterialPage(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
child: const HomePage(),
|
child: const MainScaffold(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Products Route
|
// Products Route (full screen, no bottom nav)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.products,
|
path: RouteNames.products,
|
||||||
name: RouteNames.products,
|
name: RouteNames.products,
|
||||||
@@ -45,6 +47,19 @@ class AppRouter {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Promotion Detail Route
|
||||||
|
GoRoute(
|
||||||
|
path: RouteNames.promotionDetail,
|
||||||
|
name: RouteNames.promotionDetail,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final promotionId = state.pathParameters['id'];
|
||||||
|
return MaterialPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
child: PromotionDetailPage(promotionId: promotionId),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// TODO: Add more routes as features are implemented
|
// TODO: Add more routes as features are implemented
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -105,14 +105,8 @@ class HomePage extends ConsumerWidget {
|
|||||||
? PromotionSlider(
|
? PromotionSlider(
|
||||||
promotions: promotions,
|
promotions: promotions,
|
||||||
onPromotionTap: (promotion) {
|
onPromotionTap: (promotion) {
|
||||||
// TODO: Navigate to promotion details
|
// Navigate to promotion details
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
context.push('/promotions/${promotion.id}');
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'${l10n.viewDetails}: ${promotion.title}',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
@@ -225,7 +219,7 @@ class HomePage extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Bottom Padding (for FAB and bottom nav clearance)
|
// Bottom Padding (for bottom nav clearance)
|
||||||
const SizedBox(height: 100),
|
const SizedBox(height: 100),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -233,112 +227,6 @@ class HomePage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Floating Action Button (Chat) - positioned like HTML: bottom: 90px
|
|
||||||
floatingActionButton: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 20), // 90px - 70px (bottom nav)
|
|
||||||
child: FloatingActionButton(
|
|
||||||
onPressed: () => _showComingSoon(context, l10n.chat, l10n),
|
|
||||||
backgroundColor: AppColors.accentCyan,
|
|
||||||
elevation: 8,
|
|
||||||
child: const Icon(Icons.chat_bubble, size: 24, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Bottom Navigation Bar
|
|
||||||
bottomNavigationBar: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, -2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
|
||||||
child: SizedBox(
|
|
||||||
height: 70,
|
|
||||||
child: BottomNavigationBar(
|
|
||||||
type: BottomNavigationBarType.fixed,
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
selectedItemColor: AppColors.primaryBlue,
|
|
||||||
unselectedItemColor: const Color(0xFF666666),
|
|
||||||
selectedFontSize: 11,
|
|
||||||
unselectedFontSize: 11,
|
|
||||||
iconSize: 24,
|
|
||||||
currentIndex: 0,
|
|
||||||
elevation: 0,
|
|
||||||
items: [
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.home),
|
|
||||||
label: 'Trang chủ',
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.loyalty),
|
|
||||||
label: 'Hội viên',
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.local_offer),
|
|
||||||
label: 'Khuyến mãi',
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.notifications),
|
|
||||||
Positioned(
|
|
||||||
top: -4,
|
|
||||||
right: -4,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.danger,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
constraints: const BoxConstraints(
|
|
||||||
minWidth: 20,
|
|
||||||
minHeight: 20,
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'5',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
label: 'Thông báo',
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
icon: const Icon(Icons.account_circle),
|
|
||||||
label: 'Cài đặt',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onTap: (index) {
|
|
||||||
// TODO: Implement navigation
|
|
||||||
final labels = [
|
|
||||||
'Trang chủ',
|
|
||||||
'Hội viên',
|
|
||||||
'Khuyến mãi',
|
|
||||||
'Thông báo',
|
|
||||||
'Cài đặt',
|
|
||||||
];
|
|
||||||
_showComingSoon(context, labels[index], l10n);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ library;
|
|||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
import 'package:worker/features/home/domain/entities/promotion.dart';
|
import 'package:worker/features/home/domain/entities/promotion.dart';
|
||||||
|
|
||||||
@@ -58,9 +60,17 @@ class PromotionSlider extends StatelessWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return _PromotionCard(
|
return _PromotionCard(
|
||||||
promotion: promotions[index],
|
promotion: promotions[index],
|
||||||
onTap: onPromotionTap != null
|
onTap: () {
|
||||||
? () => onPromotionTap!(promotions[index])
|
if (onPromotionTap != null) {
|
||||||
: null,
|
onPromotionTap!(promotions[index]);
|
||||||
|
} else {
|
||||||
|
// Navigate to promotion detail page
|
||||||
|
context.pushNamed(
|
||||||
|
RouteNames.promotionDetail,
|
||||||
|
extra: promotions[index],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
216
lib/features/main/presentation/pages/main_scaffold.dart
Normal file
216
lib/features/main/presentation/pages/main_scaffold.dart
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/// Main Scaffold with Bottom Navigation
|
||||||
|
///
|
||||||
|
/// Root navigation wrapper that manages the bottom navigation bar
|
||||||
|
/// and displays different pages based on the selected tab.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/features/home/presentation/pages/home_page.dart';
|
||||||
|
import 'package:worker/features/main/presentation/providers/current_page_provider.dart';
|
||||||
|
import 'package:worker/features/promotions/presentation/pages/promotions_page.dart';
|
||||||
|
|
||||||
|
/// Main Scaffold Page
|
||||||
|
///
|
||||||
|
/// Manages bottom navigation and page switching for:
|
||||||
|
/// - Home (index 0)
|
||||||
|
/// - Loyalty (index 1) - Coming soon
|
||||||
|
/// - Promotions (index 2)
|
||||||
|
/// - Notifications (index 3) - Coming soon
|
||||||
|
/// - Account (index 4) - Coming soon
|
||||||
|
class MainScaffold extends ConsumerWidget {
|
||||||
|
const MainScaffold({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final currentIndex = ref.watch(currentPageIndexProvider);
|
||||||
|
|
||||||
|
// Define pages
|
||||||
|
final pages = [
|
||||||
|
const HomePage(),
|
||||||
|
_buildComingSoonPage('Hội viên'), // Loyalty
|
||||||
|
const PromotionsPage(),
|
||||||
|
_buildComingSoonPage('Thông báo'), // Notifications
|
||||||
|
_buildComingSoonPage('Cài đặt'), // Account
|
||||||
|
];
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: IndexedStack(
|
||||||
|
index: currentIndex,
|
||||||
|
children: pages,
|
||||||
|
),
|
||||||
|
floatingActionButton: currentIndex == 0
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Chat - Đang phát triển'),
|
||||||
|
duration: Duration(seconds: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
backgroundColor: AppColors.accentCyan,
|
||||||
|
elevation: 8,
|
||||||
|
child: const Icon(Icons.chat_bubble, size: 24, color: Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
bottomNavigationBar: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 70,
|
||||||
|
child: BottomNavigationBar(
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
selectedItemColor: AppColors.primaryBlue,
|
||||||
|
unselectedItemColor: const Color(0xFF666666),
|
||||||
|
selectedFontSize: 11,
|
||||||
|
unselectedFontSize: 11,
|
||||||
|
iconSize: 24,
|
||||||
|
currentIndex: currentIndex,
|
||||||
|
elevation: 0,
|
||||||
|
items: [
|
||||||
|
const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.home),
|
||||||
|
label: 'Trang chủ',
|
||||||
|
),
|
||||||
|
const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.loyalty),
|
||||||
|
label: 'Hội viên',
|
||||||
|
),
|
||||||
|
const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.local_offer),
|
||||||
|
label: 'Khuyến mãi',
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
icon: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.notifications),
|
||||||
|
Positioned(
|
||||||
|
top: -4,
|
||||||
|
right: -4,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.danger,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 20,
|
||||||
|
minHeight: 20,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'5',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
label: 'Thông báo',
|
||||||
|
),
|
||||||
|
const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.account_circle),
|
||||||
|
label: 'Cài đặt',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onTap: (index) {
|
||||||
|
ref.read(currentPageIndexProvider.notifier).setIndex(index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build coming soon placeholder page
|
||||||
|
Widget _buildComingSoonPage(String title) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF4F6F8),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF212121),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Coming soon content
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.construction,
|
||||||
|
size: 80,
|
||||||
|
color: AppColors.grey500.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Đang phát triển',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Tính năng này sẽ sớm ra mắt',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/// Provider: Current Page Index Provider
|
||||||
|
///
|
||||||
|
/// Manages the state of the current bottom navigation page index.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'current_page_provider.g.dart';
|
||||||
|
|
||||||
|
/// Current Page Index Notifier
|
||||||
|
///
|
||||||
|
/// Manages which page is currently displayed in the bottom navigation.
|
||||||
|
/// Pages:
|
||||||
|
/// - 0: Home
|
||||||
|
/// - 1: Loyalty
|
||||||
|
/// - 2: Promotions
|
||||||
|
/// - 3: Notifications
|
||||||
|
/// - 4: Account
|
||||||
|
@riverpod
|
||||||
|
class CurrentPageIndex extends _$CurrentPageIndex {
|
||||||
|
@override
|
||||||
|
int build() => 0;
|
||||||
|
|
||||||
|
/// Set the current page index
|
||||||
|
void setIndex(int index) {
|
||||||
|
if (index >= 0 && index <= 4) {
|
||||||
|
state = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'current_page_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
/// Current Page Index Notifier
|
||||||
|
///
|
||||||
|
/// Manages which page is currently displayed in the bottom navigation.
|
||||||
|
/// Pages:
|
||||||
|
/// - 0: Home
|
||||||
|
/// - 1: Loyalty
|
||||||
|
/// - 2: Promotions
|
||||||
|
/// - 3: Notifications
|
||||||
|
/// - 4: Account
|
||||||
|
|
||||||
|
@ProviderFor(CurrentPageIndex)
|
||||||
|
const currentPageIndexProvider = CurrentPageIndexProvider._();
|
||||||
|
|
||||||
|
/// Current Page Index Notifier
|
||||||
|
///
|
||||||
|
/// Manages which page is currently displayed in the bottom navigation.
|
||||||
|
/// Pages:
|
||||||
|
/// - 0: Home
|
||||||
|
/// - 1: Loyalty
|
||||||
|
/// - 2: Promotions
|
||||||
|
/// - 3: Notifications
|
||||||
|
/// - 4: Account
|
||||||
|
final class CurrentPageIndexProvider
|
||||||
|
extends $NotifierProvider<CurrentPageIndex, int> {
|
||||||
|
/// Current Page Index Notifier
|
||||||
|
///
|
||||||
|
/// Manages which page is currently displayed in the bottom navigation.
|
||||||
|
/// Pages:
|
||||||
|
/// - 0: Home
|
||||||
|
/// - 1: Loyalty
|
||||||
|
/// - 2: Promotions
|
||||||
|
/// - 3: Notifications
|
||||||
|
/// - 4: Account
|
||||||
|
const CurrentPageIndexProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'currentPageIndexProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$currentPageIndexHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
CurrentPageIndex create() => CurrentPageIndex();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(int value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<int>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$currentPageIndexHash() => r'677ac5cabc001e152a7a79cc7fb7d3789ad49545';
|
||||||
|
|
||||||
|
/// Current Page Index Notifier
|
||||||
|
///
|
||||||
|
/// Manages which page is currently displayed in the bottom navigation.
|
||||||
|
/// Pages:
|
||||||
|
/// - 0: Home
|
||||||
|
/// - 1: Loyalty
|
||||||
|
/// - 2: Promotions
|
||||||
|
/// - 3: Notifications
|
||||||
|
/// - 4: Account
|
||||||
|
|
||||||
|
abstract class _$CurrentPageIndex extends $Notifier<int> {
|
||||||
|
int build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<int, int>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<int, int>,
|
||||||
|
int,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
/// Promotion Detail Page
|
||||||
|
///
|
||||||
|
/// Displays full details of a selected promotion including:
|
||||||
|
/// - Banner image
|
||||||
|
/// - Title and date range
|
||||||
|
/// - Program content
|
||||||
|
/// - Terms and conditions
|
||||||
|
/// - Contact information
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:worker/core/router/app_router.dart';
|
||||||
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/features/home/domain/entities/promotion.dart';
|
||||||
|
import 'package:worker/features/home/presentation/providers/promotions_provider.dart';
|
||||||
|
import 'package:worker/features/promotions/presentation/widgets/highlight_box.dart';
|
||||||
|
import 'package:worker/features/promotions/presentation/widgets/promotion_section.dart';
|
||||||
|
|
||||||
|
/// Promotion Detail Page
|
||||||
|
///
|
||||||
|
/// Full-screen detail view of a promotion with scrollable content
|
||||||
|
/// and fixed bottom action bar.
|
||||||
|
class PromotionDetailPage extends ConsumerStatefulWidget {
|
||||||
|
const PromotionDetailPage({
|
||||||
|
this.promotionId,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Promotion ID
|
||||||
|
final String? promotionId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PromotionDetailPage> createState() =>
|
||||||
|
_PromotionDetailPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PromotionDetailPageState extends ConsumerState<PromotionDetailPage> {
|
||||||
|
bool _isBookmarked = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Watch promotions provider
|
||||||
|
final promotionsAsync = ref.watch(promotionsProvider);
|
||||||
|
|
||||||
|
return promotionsAsync.when(
|
||||||
|
data: (promotions) {
|
||||||
|
// Find promotion by ID
|
||||||
|
final promotion = promotions.firstWhere(
|
||||||
|
(p) => p.id == widget.promotionId,
|
||||||
|
orElse: () => promotions.first,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _buildDetailContent(promotion);
|
||||||
|
},
|
||||||
|
loading: () => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Chi tiết khuyến mãi'),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: AppColors.primaryBlue,
|
||||||
|
elevation: 1,
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, stack) => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Chi tiết khuyến mãi'),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: AppColors.primaryBlue,
|
||||||
|
elevation: 1,
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 64,
|
||||||
|
color: AppColors.danger,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Không thể tải thông tin khuyến mãi',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
error.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailContent(Promotion promotion) {
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// Scrollable Content
|
||||||
|
CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
// App Bar
|
||||||
|
SliverAppBar(
|
||||||
|
pinned: true,
|
||||||
|
// backgroundColor: Colors.white,
|
||||||
|
foregroundColor: AppColors.primaryBlue,
|
||||||
|
elevation: 1,
|
||||||
|
shadowColor: Colors.black.withValues(alpha: 0.1),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'Chi tiết khuyến mãi',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
// Share Button
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
color: const Color(0xFF64748B),
|
||||||
|
onPressed: _handleShare,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bookmark Button
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isBookmarked ? Icons.bookmark : Icons.bookmark_border,
|
||||||
|
),
|
||||||
|
color: const Color(0xFF64748B),
|
||||||
|
onPressed: _handleBookmark,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Banner Image
|
||||||
|
_buildBannerImage(promotion),
|
||||||
|
|
||||||
|
// Promotion Header
|
||||||
|
_buildPromotionHeader(promotion),
|
||||||
|
|
||||||
|
// Program Content Section
|
||||||
|
_buildProgramContentSection(),
|
||||||
|
|
||||||
|
// Terms & Conditions Section
|
||||||
|
_buildTermsSection(),
|
||||||
|
|
||||||
|
// Contact Info Section
|
||||||
|
_buildContactSection(),
|
||||||
|
|
||||||
|
// Bottom padding for action bar
|
||||||
|
const SizedBox(height: 100),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Fixed Bottom Action Bar
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: _buildActionBar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build banner image section
|
||||||
|
Widget _buildBannerImage(Promotion promotion) {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: promotion.imageUrl,
|
||||||
|
width: double.infinity,
|
||||||
|
height: 200,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Container(
|
||||||
|
height: 200,
|
||||||
|
color: AppColors.grey100,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Container(
|
||||||
|
height: 200,
|
||||||
|
color: AppColors.grey100,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.image_not_supported,
|
||||||
|
size: 64,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build promotion header with title and date
|
||||||
|
Widget _buildPromotionHeader(Promotion promotion) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Color(0xFFE2E8F0),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
promotion.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF1E293B),
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Date Range and Status
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Clock icon and date
|
||||||
|
const Icon(
|
||||||
|
Icons.access_time,
|
||||||
|
size: 18,
|
||||||
|
color: Color(0xFFF59E0B), // warning color
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_formatDateRange(promotion),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Color(0xFFF59E0B),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Status Badge
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF10B981), // success color
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.local_fire_department,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Đang diễn ra',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build program content section
|
||||||
|
Widget _buildProgramContentSection() {
|
||||||
|
return const PromotionSection(
|
||||||
|
title: 'Nội dung chương trình',
|
||||||
|
icon: Icons.card_giftcard,
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
PromotionContentText(
|
||||||
|
'Chương trình khuyến mãi đặc biệt dành cho các công trình xây dựng với mức giảm giá hấp dẫn nhất trong năm.',
|
||||||
|
),
|
||||||
|
|
||||||
|
// Highlight Box
|
||||||
|
HighlightBox(
|
||||||
|
emoji: '🎉',
|
||||||
|
text: 'Giảm giá lên đến 30% cho tất cả sản phẩm gạch men cao cấp',
|
||||||
|
),
|
||||||
|
|
||||||
|
// Discount Details
|
||||||
|
PromotionContentText(
|
||||||
|
'Ưu đãi chi tiết:',
|
||||||
|
isBold: true,
|
||||||
|
),
|
||||||
|
PromotionBulletList(
|
||||||
|
items: [
|
||||||
|
'Gạch men 60x60cm: Giảm 25% - 30%',
|
||||||
|
'Gạch men 80x80cm: Giảm 20% - 25%',
|
||||||
|
'Gạch men 120x60cm: Giảm 15% - 20%',
|
||||||
|
'Gạch granite 60x60cm: Giảm 20% - 25%',
|
||||||
|
'Gạch ốp tường: Giảm 15% - 20%',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Additional Benefits
|
||||||
|
PromotionContentText(
|
||||||
|
'Ưu đãi bổ sung:',
|
||||||
|
isBold: true,
|
||||||
|
),
|
||||||
|
PromotionBulletList(
|
||||||
|
items: [
|
||||||
|
'Miễn phí vận chuyển cho đơn hàng từ 500m²',
|
||||||
|
'Tặng keo dán gạch cho đơn hàng từ 200m²',
|
||||||
|
'Hỗ trợ thiết kế 3D miễn phí',
|
||||||
|
'Bảo hành sản phẩm lên đến 15 năm',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build terms and conditions section
|
||||||
|
Widget _buildTermsSection() {
|
||||||
|
return const PromotionSection(
|
||||||
|
title: 'Điều kiện áp dụng',
|
||||||
|
icon: Icons.description,
|
||||||
|
content: PromotionBulletList(
|
||||||
|
items: [
|
||||||
|
'Áp dụng cho tất cả khách hàng là thợ xây dựng đã đăng ký tài khoản',
|
||||||
|
'Đơn hàng tối thiểu: 50m² sản phẩm gạch men',
|
||||||
|
'Thanh toán tối thiểu 50% giá trị đơn hàng khi đặt',
|
||||||
|
'Không áp dụng đồng thời với các chương trình khuyến mãi khác',
|
||||||
|
'Giá đã bao gồm VAT, chưa bao gồm phí vận chuyển',
|
||||||
|
'Sản phẩm không áp dụng đổi trả sau khi đã cắt, gia công',
|
||||||
|
'Thời gian giao hàng: 3-7 ngày làm việc tùy theo khu vực',
|
||||||
|
'Khuyến mãi có thể kết thúc sớm nếu hết hàng',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build contact information section
|
||||||
|
Widget _buildContactSection() {
|
||||||
|
return const PromotionSection(
|
||||||
|
title: 'Thông tin liên hệ',
|
||||||
|
icon: Icons.phone,
|
||||||
|
isLast: true,
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ContactInfo(
|
||||||
|
label: 'Hotline',
|
||||||
|
value: '1900-xxxx (8:00 - 18:00 hàng ngày)',
|
||||||
|
),
|
||||||
|
ContactInfo(
|
||||||
|
label: 'Email',
|
||||||
|
value: 'promotion@company.com',
|
||||||
|
),
|
||||||
|
ContactInfo(
|
||||||
|
label: 'Zalo',
|
||||||
|
value: '0123.456.789',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build fixed bottom action bar
|
||||||
|
Widget _buildActionBar() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
border: const Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: Color(0xFFE2E8F0),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _handleViewProducts,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primaryBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.visibility, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Xem sản phẩm áp dụng',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format date range for display
|
||||||
|
String _formatDateRange(Promotion promotion) {
|
||||||
|
final startDay = promotion.startDate.day.toString().padLeft(2, '0');
|
||||||
|
final startMonth = promotion.startDate.month.toString().padLeft(2, '0');
|
||||||
|
final startYear = promotion.startDate.year;
|
||||||
|
final endDay = promotion.endDate.day.toString().padLeft(2, '0');
|
||||||
|
final endMonth = promotion.endDate.month.toString().padLeft(2, '0');
|
||||||
|
final endYear = promotion.endDate.year;
|
||||||
|
|
||||||
|
return '$startDay/$startMonth/$startYear - $endDay/$endMonth/$endYear';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle share button press
|
||||||
|
void _handleShare() {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Tính năng chia sẻ đang phát triển'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle bookmark button press
|
||||||
|
void _handleBookmark() {
|
||||||
|
setState(() {
|
||||||
|
_isBookmarked = !_isBookmarked;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
_isBookmarked ? 'Đã lưu khuyến mãi' : 'Đã bỏ lưu khuyến mãi',
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle view products button press
|
||||||
|
void _handleViewProducts() {
|
||||||
|
// Navigate to products page
|
||||||
|
context.push(RouteNames.products);
|
||||||
|
}
|
||||||
|
}
|
||||||
195
lib/features/promotions/presentation/pages/promotions_page.dart
Normal file
195
lib/features/promotions/presentation/pages/promotions_page.dart
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/// Promotions Page
|
||||||
|
///
|
||||||
|
/// Displays all available promotions with a featured promotion card
|
||||||
|
/// at the top and a scrollable list of promotion cards below.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/features/home/domain/entities/promotion.dart';
|
||||||
|
import 'package:worker/features/home/presentation/providers/promotions_provider.dart';
|
||||||
|
import 'package:worker/features/promotions/presentation/widgets/featured_promotion_card.dart';
|
||||||
|
import 'package:worker/features/promotions/presentation/widgets/promotion_card.dart';
|
||||||
|
|
||||||
|
/// Promotions Page
|
||||||
|
///
|
||||||
|
/// Shows:
|
||||||
|
/// - Header bar with title
|
||||||
|
/// - Featured promotion card (gradient background)
|
||||||
|
/// - List of promotion cards
|
||||||
|
///
|
||||||
|
/// Layout designed for 375px width mobile screens.
|
||||||
|
class PromotionsPage extends ConsumerWidget {
|
||||||
|
const PromotionsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Watch promotions provider (same as home page)
|
||||||
|
final promotionsAsync = ref.watch(promotionsProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF4F6F8),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
_buildHeader(),
|
||||||
|
|
||||||
|
// Scrollable content
|
||||||
|
Expanded(
|
||||||
|
child: promotionsAsync.when(
|
||||||
|
data: (promotions) => _buildPromotionsContent(
|
||||||
|
context,
|
||||||
|
promotions,
|
||||||
|
),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (error, stack) => Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: AppColors.danger,
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Không thể tải khuyến mãi',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.grey900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
error.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build header bar
|
||||||
|
Widget _buildHeader() {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Khuyến mãi',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF212121),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build promotions content with featured card and list
|
||||||
|
Widget _buildPromotionsContent(
|
||||||
|
BuildContext context,
|
||||||
|
List<Promotion> promotions,
|
||||||
|
) {
|
||||||
|
if (promotions.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.local_offer_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Chưa có khuyến mãi',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.grey900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Hãy quay lại sau để xem các ưu đãi mới',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
// Featured Promotion Card (first promotion)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16, bottom: 24),
|
||||||
|
child: FeaturedPromotionCard(
|
||||||
|
title: promotions.first.title,
|
||||||
|
subtitle: promotions.first.description,
|
||||||
|
timerText: 'Còn 2 ngày 15:30:45',
|
||||||
|
onTap: () {
|
||||||
|
context.push('/promotions/${promotions.first.id}');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Promotion List (all promotions)
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
sliver: SliverList(
|
||||||
|
delegate: SliverChildListDelegate(
|
||||||
|
promotions
|
||||||
|
.map(
|
||||||
|
(promotion) => PromotionCard(
|
||||||
|
promotion: promotion,
|
||||||
|
onTap: () {
|
||||||
|
context.push('/promotions/${promotion.id}');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom padding for navigation clearance
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/// Featured Promotion Card Widget
|
||||||
|
///
|
||||||
|
/// Displays a prominent featured promotion with gradient background,
|
||||||
|
/// typically shown at the top of the promotions page.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
|
||||||
|
/// Featured Promotion Card
|
||||||
|
///
|
||||||
|
/// Shows a special promotion with:
|
||||||
|
/// - Gradient background (primary blue to light blue)
|
||||||
|
/// - Large title and subtitle
|
||||||
|
/// - Countdown timer (optional)
|
||||||
|
/// - Percentage icon
|
||||||
|
class FeaturedPromotionCard extends StatelessWidget {
|
||||||
|
/// Card title
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Card subtitle/description
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
/// Optional timer text (e.g., "Còn 2 ngày 15:30:45")
|
||||||
|
final String? timerText;
|
||||||
|
|
||||||
|
/// Optional tap callback
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const FeaturedPromotionCard({
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
this.timerText,
|
||||||
|
this.onTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [AppColors.primaryBlue, AppColors.lightBlue],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColors.primaryBlue.withValues(alpha: 0.3),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Left side - Text content
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Timer (if provided)
|
||||||
|
if (timerText != null) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.access_time,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
timerText!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.white.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Right side - Icon
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Icon(
|
||||||
|
Icons.percent,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.white.withValues(alpha: 0.9),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/// Highlight Box Widget
|
||||||
|
///
|
||||||
|
/// A highlighted box with gradient background used to emphasize
|
||||||
|
/// important information in promotion details.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Highlight Box Widget
|
||||||
|
///
|
||||||
|
/// Displays important promotion information with:
|
||||||
|
/// - Yellow/orange gradient background
|
||||||
|
/// - Border styling
|
||||||
|
/// - Centered text
|
||||||
|
/// - Optional emoji/icon
|
||||||
|
class HighlightBox extends StatelessWidget {
|
||||||
|
const HighlightBox({
|
||||||
|
required this.text,
|
||||||
|
this.emoji,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Text to display in the highlight box
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
/// Optional emoji or icon to display before text
|
||||||
|
final String? emoji;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFEF3C7), // #fef3c7
|
||||||
|
Color(0xFFFED7AA), // #fed7aa
|
||||||
|
],
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFFFBBF24), // #fbbf24
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
emoji != null ? '$emoji $text' : text,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF92400E), // #92400e - brown color
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
197
lib/features/promotions/presentation/widgets/promotion_card.dart
Normal file
197
lib/features/promotions/presentation/widgets/promotion_card.dart
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
/// Promotion Card Widget
|
||||||
|
///
|
||||||
|
/// Displays an individual promotion with image, title, description,
|
||||||
|
/// date range, and action button.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:worker/core/router/app_router.dart';
|
||||||
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/features/home/domain/entities/promotion.dart';
|
||||||
|
|
||||||
|
/// Promotion Card
|
||||||
|
///
|
||||||
|
/// Shows:
|
||||||
|
/// - Promotion image (150px height)
|
||||||
|
/// - Title and description
|
||||||
|
/// - Date range with calendar icon
|
||||||
|
/// - "Chi tiết" button
|
||||||
|
class PromotionCard extends StatelessWidget {
|
||||||
|
/// Promotion data
|
||||||
|
final Promotion promotion;
|
||||||
|
|
||||||
|
/// Callback when card or detail button is tapped
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const PromotionCard({
|
||||||
|
required this.promotion,
|
||||||
|
this.onTap,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Image
|
||||||
|
CachedNetworkImage(
|
||||||
|
imageUrl: promotion.imageUrl,
|
||||||
|
height: 150,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (context, url) => Container(
|
||||||
|
height: 150,
|
||||||
|
color: AppColors.grey100,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorWidget: (context, url, error) => Container(
|
||||||
|
height: 150,
|
||||||
|
color: AppColors.grey100,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.image_not_supported,
|
||||||
|
size: 48,
|
||||||
|
color: AppColors.grey500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Title
|
||||||
|
Text(
|
||||||
|
promotion.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF212121),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Description
|
||||||
|
Text(
|
||||||
|
promotion.description,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF666666),
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Bottom row: Date and button
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Date range
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.calendar_today,
|
||||||
|
size: 12,
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_formatDateRange(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
|
// Detail button
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (onTap != null) {
|
||||||
|
onTap!();
|
||||||
|
} else {
|
||||||
|
// Navigate to promotion detail page
|
||||||
|
context.pushNamed(
|
||||||
|
RouteNames.promotionDetail,
|
||||||
|
extra: promotion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primaryBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Chi tiết',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format date range for display
|
||||||
|
String _formatDateRange() {
|
||||||
|
final startDay = promotion.startDate.day.toString().padLeft(2, '0');
|
||||||
|
final startMonth = promotion.startDate.month.toString().padLeft(2, '0');
|
||||||
|
final endDay = promotion.endDate.day.toString().padLeft(2, '0');
|
||||||
|
final endMonth = promotion.endDate.month.toString().padLeft(2, '0');
|
||||||
|
final year = promotion.endDate.year;
|
||||||
|
|
||||||
|
return '$startDay/$startMonth - $endDay/$endMonth/$year';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/// Promotion Section Widget
|
||||||
|
///
|
||||||
|
/// A reusable section widget for organizing content in promotion details.
|
||||||
|
/// Each section has a title with icon and customizable content.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
|
||||||
|
/// Promotion Section Widget
|
||||||
|
///
|
||||||
|
/// Displays a content section with:
|
||||||
|
/// - Icon and title
|
||||||
|
/// - Custom content widget
|
||||||
|
/// - Bottom border separator
|
||||||
|
class PromotionSection extends StatelessWidget {
|
||||||
|
const PromotionSection({
|
||||||
|
required this.title,
|
||||||
|
required this.icon,
|
||||||
|
required this.content,
|
||||||
|
this.isLast = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Section title
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Icon to display next to title
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
/// Content widget to display in the section
|
||||||
|
final Widget content;
|
||||||
|
|
||||||
|
/// Whether this is the last section (no bottom border)
|
||||||
|
final bool isLast;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: isLast
|
||||||
|
? null
|
||||||
|
: const Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Color(0xFFE2E8F0), // --border-color
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Section Title with Icon
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: 20,
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF1E293B), // --text-primary
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Section Content
|
||||||
|
content,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Promotion Content Text Widget
|
||||||
|
///
|
||||||
|
/// Standard text styling for section content with proper line height.
|
||||||
|
class PromotionContentText extends StatelessWidget {
|
||||||
|
const PromotionContentText(
|
||||||
|
this.text, {
|
||||||
|
this.isBold = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final bool isBold;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: const Color(0xFF64748B), // --text-secondary
|
||||||
|
height: 1.7,
|
||||||
|
fontWeight: isBold ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Promotion Bullet List Widget
|
||||||
|
///
|
||||||
|
/// Displays a list with custom bullet points.
|
||||||
|
class PromotionBulletList extends StatelessWidget {
|
||||||
|
const PromotionBulletList({
|
||||||
|
required this.items,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<String> items;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: items.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final item = entry.value;
|
||||||
|
final isLast = index == items.length - 1;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: isLast
|
||||||
|
? null
|
||||||
|
: const Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Color(0xFFF1F5F9), // Light border
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Custom bullet point
|
||||||
|
const Text(
|
||||||
|
'•',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.primaryBlue,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Item text
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
item,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: Color(0xFF64748B), // --text-secondary
|
||||||
|
height: 1.7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contact Info Widget
|
||||||
|
///
|
||||||
|
/// Displays contact information with labels and values.
|
||||||
|
class ContactInfo extends StatelessWidget {
|
||||||
|
const ContactInfo({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '$label: ',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
height: 1.7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
color: Color(0xFF64748B),
|
||||||
|
height: 1.7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user