menu page
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../common/data/service_data.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../sample/product_sample_data.dart';
|
||||
import '../../components/delegate/category_delegate.dart';
|
||||
import 'widgets/header.dart';
|
||||
import 'widgets/product_section.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MenuPage extends StatefulWidget {
|
||||
final Service service;
|
||||
const MenuPage({super.key, required this.service});
|
||||
|
||||
@override
|
||||
State<MenuPage> createState() => _MenuPageState();
|
||||
}
|
||||
|
||||
class _MenuPageState extends State<MenuPage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final List<GlobalKey> _productSectionKeys = [];
|
||||
|
||||
String _selectedCategoryId = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedCategoryId = categories.isNotEmpty ? categories.first.id : "";
|
||||
|
||||
// Initialize keys for each category
|
||||
for (int i = 0; i < categories.length; i++) {
|
||||
_productSectionKeys.add(GlobalKey());
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToCategory(String categoryId) {
|
||||
setState(() {
|
||||
_selectedCategoryId = categoryId;
|
||||
});
|
||||
|
||||
final categoryIndex = categories.indexWhere((cat) => cat.id == categoryId);
|
||||
if (categoryIndex >= 0 && categoryIndex < _productSectionKeys.length) {
|
||||
final key = _productSectionKeys[categoryIndex];
|
||||
final context = key.currentContext;
|
||||
|
||||
if (context != null) {
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: 0.1,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Product> _getProductsByCategory(String categoryId) {
|
||||
return products
|
||||
.where((product) => product.categoryId == categoryId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
body: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
// App Bar with merchant info
|
||||
SliverAppBar(
|
||||
expandedHeight: 200,
|
||||
pinned: true,
|
||||
backgroundColor: AppColor.primary,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: AppColor.textWhite),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.share, color: AppColor.textWhite),
|
||||
onPressed: () {},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.favorite_border, color: AppColor.textWhite),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: MenuHeader(service: widget.service),
|
||||
),
|
||||
),
|
||||
|
||||
// Categories (will be pinned)
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: SliverCategoryDelegate(
|
||||
categories: categories,
|
||||
selectedCategoryId: _selectedCategoryId,
|
||||
onCategoryTap: _scrollToCategory,
|
||||
),
|
||||
),
|
||||
|
||||
// Product sections by category
|
||||
...categories.map((category) {
|
||||
final categoryProducts = _getProductsByCategory(category.id);
|
||||
final categoryIndex = categories.indexOf(category);
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
key: _productSectionKeys[categoryIndex],
|
||||
child: MenuProductSection(
|
||||
category: category,
|
||||
categoryProducts: categoryProducts,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/constant/app_constant.dart';
|
||||
import '../../../../common/data/service_data.dart';
|
||||
import '../../../../common/theme/theme.dart';
|
||||
|
||||
class MenuHeader extends StatelessWidget {
|
||||
final Service service;
|
||||
const MenuHeader({super.key, required this.service});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.primary, AppColor.primary.withOpacity(0.8)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background decoration
|
||||
Positioned(
|
||||
right: 20,
|
||||
top: 60,
|
||||
child: Opacity(
|
||||
opacity: 0.1,
|
||||
child: Icon(Icons.store, size: 100, color: AppColor.textWhite),
|
||||
),
|
||||
),
|
||||
|
||||
// Content
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Service
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
service.name,
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
service.description,
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
// Merchant
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.black.withOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.asset(merchant.imageUrl),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
merchant.name,
|
||||
style: AppStyle.h6.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
merchant.category,
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.textWhite.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
Row(children: [SizedBox(width: 12)]),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: merchant.isOpen
|
||||
? AppColor.success
|
||||
: AppColor.error,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
merchant.isOpen ? "BUKA" : "TUTUP",
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../sample/product_sample_data.dart';
|
||||
import '../../../components/card/product_card.dart';
|
||||
import '../../../components/card/product_empty_card.dart';
|
||||
|
||||
class MenuProductSection extends StatelessWidget {
|
||||
final ProductCategory category;
|
||||
final List<Product> categoryProducts;
|
||||
const MenuProductSection({
|
||||
super.key,
|
||||
required this.category,
|
||||
required this.categoryProducts,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section header
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(category.icon, style: AppStyle.h5),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
category.name,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
"(${categoryProducts.length})",
|
||||
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Products grid
|
||||
if (categoryProducts.isEmpty)
|
||||
ProductEmptyCard()
|
||||
else
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 0.75,
|
||||
),
|
||||
itemCount: categoryProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ProductCard(product: categoryProducts[index]);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user