menu page
This commit is contained in:
@@ -8,10 +8,10 @@ import '../../../../../application/customer/customer_point_loader/customer_point
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../components/image/image.dart';
|
||||
import '../../../../router/app_router.gr.dart';
|
||||
import 'widgets/feature_section.dart';
|
||||
import 'widgets/lottery_card.dart';
|
||||
import 'widgets/point_card.dart';
|
||||
import 'widgets/popular_merchant_section.dart';
|
||||
import 'widgets/service_section.dart';
|
||||
|
||||
@RoutePage()
|
||||
class HomePage extends StatefulWidget {
|
||||
@@ -52,7 +52,7 @@ class _HomePageState extends State<HomePage> {
|
||||
children: [
|
||||
_buildHeaderSection(),
|
||||
const SizedBox(height: 70),
|
||||
HomeFeatureSection(),
|
||||
HomeServiceSection(),
|
||||
HomeLotteryBanner(onTap: () => context.router.push(DrawRoute())),
|
||||
HomePopularMerchantSection(),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../common/data/service_data.dart';
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../components/card/gradient_card.dart';
|
||||
import '../../../../../router/app_router.gr.dart';
|
||||
|
||||
class HomeServiceSection extends StatelessWidget {
|
||||
const HomeServiceSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(services.length, (index) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: index == 0 ? 12 : 0),
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
context.router.push(MenuRoute(service: services[index])),
|
||||
child: GradientCard(
|
||||
child: _content(
|
||||
services[index].name,
|
||||
services[index].description,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Column _content(String title, String subtitle) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AppStyle.lg.copyWith(
|
||||
color: AppColor.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle,
|
||||
style: AppStyle.md.copyWith(
|
||||
color: AppColor.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,9 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../../common/theme/theme.dart';
|
||||
import '../../../../../sample/product_sample_data.dart';
|
||||
import '../../../../../sample/sample_data.dart';
|
||||
|
||||
// Models
|
||||
class ProductCategory {
|
||||
final String id;
|
||||
final String name;
|
||||
final String icon;
|
||||
final int productCount;
|
||||
|
||||
ProductCategory({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.productCount,
|
||||
});
|
||||
}
|
||||
|
||||
class Product {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final int price;
|
||||
final String categoryId;
|
||||
final String imageUrl;
|
||||
final bool isAvailable;
|
||||
final double rating;
|
||||
final int soldCount;
|
||||
|
||||
Product({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.price,
|
||||
required this.categoryId,
|
||||
required this.imageUrl,
|
||||
required this.isAvailable,
|
||||
required this.rating,
|
||||
required this.soldCount,
|
||||
});
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class MerchantDetailPage extends StatefulWidget {
|
||||
final MerchantModel merchant;
|
||||
@@ -58,89 +20,6 @@ class _MerchantDetailPageState extends State<MerchantDetailPage> {
|
||||
|
||||
String _selectedCategoryId = "";
|
||||
|
||||
// Sample data
|
||||
final List<ProductCategory> categories = [
|
||||
ProductCategory(id: "1", name: "Makanan", icon: "🍽️", productCount: 8),
|
||||
ProductCategory(id: "2", name: "Minuman", icon: "🥤", productCount: 6),
|
||||
ProductCategory(id: "3", name: "Snack", icon: "🍿", productCount: 5),
|
||||
ProductCategory(id: "4", name: "Es Krim", icon: "🍦", productCount: 4),
|
||||
ProductCategory(id: "5", name: "Paket", icon: "📦", productCount: 3),
|
||||
];
|
||||
|
||||
final List<Product> products = [
|
||||
// Makanan
|
||||
Product(
|
||||
id: "1",
|
||||
name: "Nasi Gudeg",
|
||||
description: "Gudeg khas Yogyakarta dengan ayam dan telur",
|
||||
price: 25000,
|
||||
categoryId: "1",
|
||||
imageUrl: "https://via.placeholder.com/150",
|
||||
isAvailable: true,
|
||||
rating: 4.5,
|
||||
soldCount: 50,
|
||||
),
|
||||
Product(
|
||||
id: "2",
|
||||
name: "Soto Ayam",
|
||||
description: "Soto ayam kuning dengan nasi dan kerupuk",
|
||||
price: 18000,
|
||||
categoryId: "1",
|
||||
imageUrl: "https://via.placeholder.com/150",
|
||||
isAvailable: true,
|
||||
rating: 4.3,
|
||||
soldCount: 75,
|
||||
),
|
||||
Product(
|
||||
id: "3",
|
||||
name: "Gado-gado",
|
||||
description: "Gado-gado segar dengan bumbu kacang",
|
||||
price: 15000,
|
||||
categoryId: "1",
|
||||
imageUrl: "https://via.placeholder.com/150",
|
||||
isAvailable: false,
|
||||
rating: 4.2,
|
||||
soldCount: 30,
|
||||
),
|
||||
|
||||
// Minuman
|
||||
Product(
|
||||
id: "4",
|
||||
name: "Es Teh Manis",
|
||||
description: "Es teh manis segar",
|
||||
price: 5000,
|
||||
categoryId: "2",
|
||||
imageUrl: "https://via.placeholder.com/150",
|
||||
isAvailable: true,
|
||||
rating: 4.0,
|
||||
soldCount: 120,
|
||||
),
|
||||
Product(
|
||||
id: "5",
|
||||
name: "Jus Jeruk",
|
||||
description: "Jus jeruk segar tanpa gula tambahan",
|
||||
price: 12000,
|
||||
categoryId: "2",
|
||||
imageUrl: "https://via.placeholder.com/150",
|
||||
isAvailable: true,
|
||||
rating: 4.4,
|
||||
soldCount: 45,
|
||||
),
|
||||
|
||||
// Snack
|
||||
Product(
|
||||
id: "6",
|
||||
name: "Keripik Pisang",
|
||||
description: "Keripik pisang renyah dan manis",
|
||||
price: 8000,
|
||||
categoryId: "3",
|
||||
imageUrl: "https://via.placeholder.com/150",
|
||||
isAvailable: true,
|
||||
rating: 4.1,
|
||||
soldCount: 25,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Reference in New Issue
Block a user