EnakShop: katalog toko online
- Beranda toko: pencarian, banner promo, grid kategori, produk promo, dan rekomendasi - Halaman kategori dengan urutan terlaris/terbaru/termurah/termahal - Detail produk: harga diskon, varian, info, deskripsi, produk serupa - Data contoh 14 produk dan 3 banner; keranjang menyusul - DetailSection dan NumberedStep jadi komponen bersama - Tinggi banner EnakPromo dan EnakShop ikut skala font sistem Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5ba60747a8
commit
e273785268
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../sample/shop_sample_data.dart';
|
||||
|
||||
/// Grid kategori toko, 4 kolom, diakhiri tile "Semua" (`null`).
|
||||
class ShopCategoryGrid extends StatelessWidget {
|
||||
const ShopCategoryGrid({super.key, required this.onTap});
|
||||
|
||||
final ValueChanged<ShopCategory?> onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisExtent: MediaQuery.textScalerOf(context).scale(92),
|
||||
),
|
||||
itemCount: ShopCategory.values.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
final category = index < ShopCategory.values.length
|
||||
? ShopCategory.values[index]
|
||||
: null;
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(AppValue.borderRadius),
|
||||
onTap: () => onTap(category),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColor.borderLight),
|
||||
),
|
||||
child: Text(
|
||||
category?.emoji ?? '🏪',
|
||||
style: const TextStyle(fontSize: 26),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
category?.label ?? 'Semua',
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../sample/shop_sample_data.dart';
|
||||
import '../shop_ui.dart';
|
||||
|
||||
/// Kartu produk untuk grid dan daftar geser. Tinggi bagian teks mengikuti
|
||||
/// [shopCardInfoHeight].
|
||||
class ShopProductCard extends StatelessWidget {
|
||||
const ShopProductCard({
|
||||
super.key,
|
||||
required this.product,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final ShopProduct product;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ShopProductImage(product: product),
|
||||
if (product.tag != null)
|
||||
Positioned(
|
||||
top: 10,
|
||||
left: 10,
|
||||
child: ShopTagBadge(tag: product.tag!),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: shopCardInfoHeight(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ShopPrice(product: product),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
formatSoldCount(product.soldCount),
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../common/theme/theme.dart';
|
||||
import '../../../../sample/shop_sample_data.dart';
|
||||
|
||||
/// Banner promo toko; mengarah ke kategori promo.
|
||||
class ShopPromoBanner extends StatelessWidget {
|
||||
const ShopPromoBanner({super.key, required this.promo, required this.onTap});
|
||||
|
||||
final ShopPromo promo;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = promo.gradient.map(Color.new).toList();
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
gradient: LinearGradient(
|
||||
colors: colors,
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: onTap,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -20,
|
||||
bottom: -24,
|
||||
child: Container(
|
||||
width: 130,
|
||||
height: 130,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white.withOpacity(0.12),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 18,
|
||||
bottom: 14,
|
||||
child: Text(
|
||||
promo.category.emoji,
|
||||
style: const TextStyle(fontSize: 64),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 16, 110, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
promo.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.xl.copyWith(
|
||||
color: AppColor.textWhite,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
promo.subtitle,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textWhite.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'Lihat produk',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: colors.last,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user