From e4af5f10d7d6b8b897c434b7489e98ea5cb68568 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 5 Sep 2025 02:53:03 +0700 Subject: [PATCH] feat: draw page --- lib/presentation/components/image/image.dart | 3 + .../components/image/network_image.dart | 61 + lib/presentation/pages/draw/draw_page.dart | 626 +++++++-- .../pages/draw_detail/draw_detail_page.dart | 1161 ++++++++++++++++- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 120 ++ pubspec.yaml | 2 + 7 files changed, 1856 insertions(+), 119 deletions(-) create mode 100644 lib/presentation/components/image/network_image.dart diff --git a/lib/presentation/components/image/image.dart b/lib/presentation/components/image/image.dart index e4f8ffa..c61ef77 100644 --- a/lib/presentation/components/image/image.dart +++ b/lib/presentation/components/image/image.dart @@ -1,7 +1,10 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; import 'dart:math' as math; import '../../../common/theme/theme.dart'; import '../assets/assets.gen.dart'; part 'image_placeholder.dart'; +part 'network_image.dart'; diff --git a/lib/presentation/components/image/network_image.dart b/lib/presentation/components/image/network_image.dart new file mode 100644 index 0000000..344ddde --- /dev/null +++ b/lib/presentation/components/image/network_image.dart @@ -0,0 +1,61 @@ +part of 'image.dart'; + +class AppNetworkImage extends StatelessWidget { + final String? url; + final double? height; + final double? width; + final double? borderRadius; + final BoxFit? fit; + final bool? isCanZoom; + final VoidCallback? onTap; + + const AppNetworkImage({ + super.key, + this.url, + this.height, + this.width, + this.borderRadius = 0, + this.fit = BoxFit.cover, + this.isCanZoom = false, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + Widget customPhoto( + double? heightx, + double? widthx, + BoxFit? fitx, + double? radius, + ) { + return CachedNetworkImage( + imageUrl: url.toString(), + placeholder: (context, url) => Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: height, + width: width, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(radius ?? 0), + ), + ), + ), + errorWidget: (context, url, error) => + ImagePlaceholder(height: height, width: width), + height: heightx, + width: widthx, + fit: fitx, + ); + } + + return GestureDetector( + onTap: onTap, + child: ClipRRect( + borderRadius: BorderRadius.circular(borderRadius!), + child: customPhoto(height, width, BoxFit.fill, borderRadius), + ), + ); + } +} diff --git a/lib/presentation/pages/draw/draw_page.dart b/lib/presentation/pages/draw/draw_page.dart index 9bdd31b..28e1316 100644 --- a/lib/presentation/pages/draw/draw_page.dart +++ b/lib/presentation/pages/draw/draw_page.dart @@ -1,7 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:carousel_slider/carousel_slider.dart'; import '../../../common/theme/theme.dart'; +import '../../components/image/image.dart'; import '../../router/app_router.gr.dart'; // Models (simplified) @@ -11,6 +13,7 @@ class DrawEvent { final String description; final int entryPoints; final String icon; + final String imageUrl; final Color primaryColor; final String prize; final String prizeValue; @@ -26,6 +29,7 @@ class DrawEvent { required this.description, required this.entryPoints, required this.icon, + required this.imageUrl, required this.primaryColor, required this.prize, required this.prizeValue, @@ -46,6 +50,22 @@ class UserEntry { UserEntry({required this.drawId, required this.entryDate}); } +class CarouselBanner { + final String id; + final String imageUrl; + final String title; + final String subtitle; + final Color backgroundColor; + + CarouselBanner({ + required this.id, + required this.imageUrl, + required this.title, + required this.subtitle, + required this.backgroundColor, + }); +} + @RoutePage() class DrawPage extends StatefulWidget { const DrawPage({super.key}); @@ -55,7 +75,14 @@ class DrawPage extends StatefulWidget { } class _DrawPageState extends State { - String selectedTab = 'active'; // 'active' or 'finished' + final TextEditingController _dateFilterController = TextEditingController(); + final CarouselSliderController _carouselController = + CarouselSliderController(); + final ScrollController _scrollController = ScrollController(); + + int _currentBannerIndex = 0; + DateTime? _selectedFilterDate; + bool _isCollapsed = false; final List userEntries = [ UserEntry( @@ -64,18 +91,103 @@ class _DrawPageState extends State { ), ]; + final List banners = [ + CarouselBanner( + id: "1", + imageUrl: + "https://images.unsplash.com/photo-1610375461246-83df859d849d?w=800&h=400&fit=crop&crop=center", + title: "Gebyar Undian Emas", + subtitle: "Menangkan hadiah emas 3 gram!", + backgroundColor: Color(0xFFFF6B6B), + ), + CarouselBanner( + id: "2", + imageUrl: + "https://images.unsplash.com/photo-1592750475338-74b7b21085ab?w=800&h=400&fit=crop&crop=center", + title: "Undian iPhone 15 Pro", + subtitle: "Smartphone premium menanti Anda!", + backgroundColor: Color(0xFF4ECDC4), + ), + CarouselBanner( + id: "3", + imageUrl: + "https://images.unsplash.com/photo-1593640408182-31c70c8268f5?w=800&h=400&fit=crop&crop=center", + title: "Undian Laptop Gaming", + subtitle: "Laptop gaming terbaru untuk gamers!", + backgroundColor: Color(0xFF45B7D1), + ), + CarouselBanner( + id: "4", + imageUrl: + "https://images.unsplash.com/photo-1549298916-b41d501d3772?w=800&h=400&fit=crop&crop=center", + title: "Undian Sneakers Limited", + subtitle: "Sepatu branded edition terbatas!", + backgroundColor: Color(0xFF96CEB4), + ), + CarouselBanner( + id: "5", + imageUrl: + "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=800&h=400&fit=crop&crop=center", + title: "Undian Camera DSLR", + subtitle: "Kamera profesional untuk fotografer!", + backgroundColor: Color(0xFFFECEA8), + ), + CarouselBanner( + id: "6", + imageUrl: + "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=800&h=400&fit=crop&crop=center", + title: "Undian Gaming Console", + subtitle: "PlayStation 5 siap dimainkan!", + backgroundColor: Color(0xFFFF9AA2), + ), + CarouselBanner( + id: "7", + imageUrl: + "https://images.unsplash.com/photo-1512499617640-c74ae3a79d37?w=800&h=400&fit=crop&crop=center", + title: "Undian Motor Sport", + subtitle: "Motor sport impian Anda!", + backgroundColor: Color(0xFFB5EAD7), + ), + CarouselBanner( + id: "8", + imageUrl: + "https://images.unsplash.com/photo-1484704849700-f032a568e944?w=800&h=400&fit=crop&crop=center", + title: "Undian Home Theater", + subtitle: "Sistem audio premium untuk rumah!", + backgroundColor: Color(0xFFC7CEDB), + ), + CarouselBanner( + id: "9", + imageUrl: + "https://images.unsplash.com/photo-1434493789847-2f02dc6ca35d?w=800&h=400&fit=crop&crop=center", + title: "Undian Luxury Watch", + subtitle: "Jam tangan mewah Swiss Made!", + backgroundColor: Color(0xFFFFB7B2), + ), + CarouselBanner( + id: "10", + imageUrl: + "https://images.unsplash.com/photo-1558618666-fbd1c326d4a4?w=800&h=400&fit=crop&crop=center", + title: "Undian Travel Voucher", + subtitle: "Liburan gratis ke destinasi impian!", + backgroundColor: Color(0xFFE2F0CB), + ), + ]; + final List drawEvents = [ DrawEvent( id: "1", name: "Emas 3 Gram", - description: "Gebyar Undian Enaklo\nMenangkan hadiah menarik", + description: "Gebyar Undian Enaklo\nMenangkan hadiah emas batangan", entryPoints: 0, icon: "👑", + imageUrl: + "https://images.unsplash.com/photo-1610375461246-83df859d849d?w=400&h=200&fit=crop&crop=center", primaryColor: AppColor.primary, prize: "Emas 3 Gram", prizeValue: "Rp 2.500.000", drawDate: DateTime.now().add(Duration(hours: 1, minutes: 20)), - totalParticipants: 0, + totalParticipants: 245, hadiah: 2, status: 'active', minSpending: 50000, @@ -86,25 +198,193 @@ class _DrawPageState extends State { description: "Undian Smartphone Premium\nDapatkan iPhone terbaru", entryPoints: 0, icon: "📱", - primaryColor: AppColor.info, + imageUrl: + "https://images.unsplash.com/photo-1592750475338-74b7b21085ab?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFF007AFF), prize: "iPhone 15 Pro", prizeValue: "Rp 18.000.000", - drawDate: DateTime.now().subtract(Duration(days: 1)), - totalParticipants: 156, + drawDate: DateTime.now().add(Duration(days: 2)), + totalParticipants: 1456, hadiah: 1, - status: 'ended', + status: 'active', minSpending: 100000, ), + DrawEvent( + id: "3", + name: "Laptop Gaming", + description: "Undian Laptop Gaming ROG\nPerforma tinggi untuk gaming", + entryPoints: 0, + icon: "💻", + imageUrl: + "https://images.unsplash.com/photo-1593640408182-31c70c8268f5?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFFFF4444), + prize: "ROG Strix G15", + prizeValue: "Rp 15.000.000", + drawDate: DateTime.now().add(Duration(days: 5)), + totalParticipants: 892, + hadiah: 1, + status: 'active', + minSpending: 75000, + ), + DrawEvent( + id: "4", + name: "Sneakers Limited", + description: "Undian Sepatu Nike Air Jordan\nEdition terbatas collectors", + entryPoints: 0, + icon: "👟", + imageUrl: + "https://images.unsplash.com/photo-1549298916-b41d501d3772?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFF32CD32), + prize: "Nike Air Jordan", + prizeValue: "Rp 3.500.000", + drawDate: DateTime.now().add(Duration(days: 3)), + totalParticipants: 567, + hadiah: 3, + status: 'active', + minSpending: 30000, + ), + DrawEvent( + id: "5", + name: "Camera DSLR", + description: + "Undian Kamera Canon EOS\nKamera profesional untuk fotografer", + entryPoints: 0, + icon: "📷", + imageUrl: + "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFF8B4513), + prize: "Canon EOS R5", + prizeValue: "Rp 25.000.000", + drawDate: DateTime.now().add(Duration(days: 7)), + totalParticipants: 334, + hadiah: 1, + status: 'active', + minSpending: 150000, + ), + DrawEvent( + id: "6", + name: "PlayStation 5", + description: "Undian Gaming Console\nPS5 bundle dengan 3 games", + entryPoints: 0, + icon: "🎮", + imageUrl: + "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFF0070D1), + prize: "PlayStation 5", + prizeValue: "Rp 8.500.000", + drawDate: DateTime.now().add(Duration(days: 4)), + totalParticipants: 1789, + hadiah: 2, + status: 'active', + minSpending: 60000, + ), + DrawEvent( + id: "7", + name: "Motor Yamaha R15", + description: "Undian Motor Sport\nYamaha R15 V4 warna special edition", + entryPoints: 0, + icon: "🏍️", + imageUrl: + "https://images.unsplash.com/photo-1512499617640-c74ae3a79d37?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFFDC143C), + prize: "Yamaha R15 V4", + prizeValue: "Rp 35.000.000", + drawDate: DateTime.now().add(Duration(days: 10)), + totalParticipants: 456, + hadiah: 1, + status: 'active', + minSpending: 200000, + ), + DrawEvent( + id: "8", + name: "Home Theater", + description: "Undian Sound System\nSony Home Theater 7.1 surround", + entryPoints: 0, + icon: "🔊", + imageUrl: + "https://images.unsplash.com/photo-1484704849700-f032a568e944?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFF4B0082), + prize: "Sony HT-A7000", + prizeValue: "Rp 12.000.000", + drawDate: DateTime.now().add(Duration(days: 6)), + totalParticipants: 298, + hadiah: 1, + status: 'active', + minSpending: 80000, + ), + DrawEvent( + id: "9", + name: "Luxury Watch", + description: "Undian Jam Tangan Mewah\nRolex Submariner Swiss Made", + entryPoints: 0, + icon: "⌚", + imageUrl: + "https://images.unsplash.com/photo-1434493789847-2f02dc6ca35d?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFFB8860B), + prize: "Rolex Submariner", + prizeValue: "Rp 150.000.000", + drawDate: DateTime.now().add(Duration(days: 14)), + totalParticipants: 123, + hadiah: 1, + status: 'active', + minSpending: 500000, + ), + DrawEvent( + id: "10", + name: "Travel Voucher Bali", + description: "Undian Liburan Gratis\nPaket tour Bali 4D3N all inclusive", + entryPoints: 0, + icon: "✈️", + imageUrl: + "https://images.unsplash.com/photo-1558618666-fbd1c326d4a4?w=400&h=200&fit=crop&crop=center", + primaryColor: Color(0xFF20B2AA), + prize: "Bali Tour Package", + prizeValue: "Rp 10.000.000", + drawDate: DateTime.now().subtract(Duration(days: 2)), + totalParticipants: 2156, + hadiah: 5, + status: 'ended', + minSpending: 40000, + ), ]; + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _dateFilterController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + final bool isCollapsed = + _scrollController.hasClients && + _scrollController.offset > (280 - kToolbarHeight); + + if (_isCollapsed != isCollapsed) { + setState(() { + _isCollapsed = isCollapsed; + }); + } + } + List get filteredDraws { - return drawEvents.where((draw) { - if (selectedTab == 'active') { - return draw.isActive; - } else { - return !draw.isActive; - } - }).toList(); + List filtered = drawEvents; + + if (_selectedFilterDate != null) { + filtered = filtered.where((draw) { + return draw.drawDate.year == _selectedFilterDate!.year && + draw.drawDate.month == _selectedFilterDate!.month && + draw.drawDate.day == _selectedFilterDate!.day; + }).toList(); + } + + return filtered; } String _getTimeRemaining(DateTime targetDate) { @@ -122,92 +402,205 @@ class _DrawPageState extends State { } } + Future _selectDate() async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedFilterDate ?? DateTime.now(), + firstDate: DateTime.now().subtract(Duration(days: 365)), + lastDate: DateTime.now().add(Duration(days: 365)), + ); + + if (picked != null) { + setState(() { + _selectedFilterDate = picked; + _dateFilterController.text = + "${picked.day}/${picked.month}/${picked.year}"; + }); + } + } + + void _clearDateFilter() { + setState(() { + _selectedFilterDate = null; + _dateFilterController.clear(); + }); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, - appBar: AppBar(title: Text("Undian")), - body: Column( - children: [ - // Tab selector - Container( - margin: EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColor.surface, - borderRadius: BorderRadius.circular(25), - boxShadow: [ - BoxShadow( - color: AppColor.black.withOpacity(0.05), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], + body: CustomScrollView( + controller: _scrollController, + slivers: [ + // SliverAppBar with Carousel Background + SliverAppBar( + expandedHeight: 280, + floating: false, + pinned: true, + backgroundColor: AppColor.surface, + leading: Container( + margin: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + borderRadius: BorderRadius.circular(20), + ), + child: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), ), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => setState(() => selectedTab = 'active'), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: selectedTab == 'active' - ? AppColor.primary - : Colors.transparent, - borderRadius: BorderRadius.circular(25), - ), - child: Text( - "Aktif (${drawEvents.where((d) => d.isActive).length})", - textAlign: TextAlign.center, - style: AppStyle.md.copyWith( - color: selectedTab == 'active' - ? AppColor.textWhite - : AppColor.textSecondary, - fontWeight: FontWeight.w600, - ), - ), - ), + flexibleSpace: FlexibleSpaceBar( + title: AnimatedOpacity( + opacity: _isCollapsed ? 1.0 : 0.0, + duration: Duration(milliseconds: 200), + child: Text( + "Undian", + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, ), ), - Expanded( - child: GestureDetector( - onTap: () => setState(() => selectedTab = 'finished'), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: selectedTab == 'finished' - ? AppColor.primary - : Colors.transparent, - borderRadius: BorderRadius.circular(25), - ), - child: Text( - "Selesai (${drawEvents.where((d) => !d.isActive).length})", - textAlign: TextAlign.center, - style: AppStyle.md.copyWith( - color: selectedTab == 'finished' - ? AppColor.textWhite - : AppColor.textSecondary, - fontWeight: FontWeight.w600, - ), + ), + titlePadding: EdgeInsets.only(left: 72, bottom: 16), + collapseMode: CollapseMode.parallax, + background: Stack( + children: [ + // Carousel Slider + Positioned.fill( + child: CarouselSlider.builder( + carouselController: _carouselController, + itemCount: banners.length, + options: CarouselOptions( + height: double.infinity, + viewportFraction: 1.0, + autoPlay: true, + autoPlayInterval: Duration(seconds: 4), + autoPlayAnimationDuration: Duration(milliseconds: 800), + onPageChanged: (index, reason) { + setState(() => _currentBannerIndex = index); + }, ), + itemBuilder: (context, index, realIndex) { + final banner = banners[index]; + return GestureDetector( + onTap: () { + context.router.push(DrawDetailRoute()); + }, + child: AppNetworkImage(url: banner.imageUrl), + ); + }, ), ), - ), - ], + + // Page indicators + Positioned( + bottom: 20, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: banners.asMap().entries.map((entry) { + return GestureDetector( + onTap: () => + _carouselController.animateToPage(entry.key), + child: Container( + width: _currentBannerIndex == entry.key ? 24 : 8, + height: 8, + margin: EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: _currentBannerIndex == entry.key + ? Colors.white + : Colors.white.withOpacity(0.4), + ), + ), + ); + }).toList(), + ), + ), + ], + ), ), ), - // Draw list - Expanded( - child: ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 16), - itemCount: filteredDraws.length, - itemBuilder: (context, index) { - final draw = filteredDraws[index]; - return _buildSimpleDrawCard(draw); - }, + // Date Filter + SliverToBoxAdapter( + child: Container( + margin: EdgeInsets.all(16), + child: TextFormField( + controller: _dateFilterController, + readOnly: true, + onTap: _selectDate, + decoration: InputDecoration( + hintText: "Filter berdasarkan tanggal", + hintStyle: AppStyle.md.copyWith( + color: AppColor.textSecondary, + ), + prefixIcon: Icon( + Icons.calendar_today, + color: AppColor.textSecondary, + ), + suffixIcon: _selectedFilterDate != null + ? IconButton( + icon: Icon( + Icons.clear, + color: AppColor.textSecondary, + ), + onPressed: _clearDateFilter, + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: AppColor.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: AppColor.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: AppColor.primary, width: 2), + ), + filled: true, + fillColor: AppColor.surface, + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + style: AppStyle.md.copyWith(color: AppColor.textPrimary), + ), ), ), + + // Draw List Header + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + "Daftar Undian (${filteredDraws.length})", + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + ), + ), + + // Draw List + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final draw = filteredDraws[index]; + return Container( + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: _buildSimpleDrawCard(draw), + ); + }, childCount: filteredDraws.length), + ), + + // Bottom padding + SliverToBoxAdapter(child: SizedBox(height: 100)), ], ), ); @@ -220,14 +613,14 @@ class _DrawPageState extends State { onTap: () => context.router.push(DrawDetailRoute()), child: Container( margin: EdgeInsets.only(bottom: 8), - padding: EdgeInsets.all(12), + padding: EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [draw.primaryColor, draw.primaryColor.withOpacity(0.8)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: AppColor.black.withOpacity(0.08), @@ -245,39 +638,63 @@ class _DrawPageState extends State { // Name Text( draw.name, - style: AppStyle.md.copyWith( + style: AppStyle.lg.copyWith( fontWeight: FontWeight.w600, color: AppColor.textWhite, ), ), - SizedBox(height: 4), + SizedBox(height: 6), // Description Text( draw.description, style: AppStyle.sm.copyWith( color: AppColor.textWhite.withOpacity(0.9), ), - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis, ), - SizedBox(height: 6), - // Date - Text( - draw.isActive ? "Berakhir: $timeRemaining" : "Selesai", - style: AppStyle.xs.copyWith( - color: AppColor.textWhite.withOpacity(0.8), - fontWeight: FontWeight.w500, - ), + SizedBox(height: 8), + // Date and participants + Row( + children: [ + Icon( + Icons.access_time, + size: 16, + color: AppColor.textWhite.withOpacity(0.8), + ), + SizedBox(width: 4), + Text( + draw.isActive ? "Berakhir: $timeRemaining" : "Selesai", + style: AppStyle.xs.copyWith( + color: AppColor.textWhite.withOpacity(0.8), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(width: 16), + Icon( + Icons.people, + size: 16, + color: AppColor.textWhite.withOpacity(0.8), + ), + SizedBox(width: 4), + Text( + "${draw.totalParticipants}", + style: AppStyle.xs.copyWith( + color: AppColor.textWhite.withOpacity(0.8), + fontWeight: FontWeight.w500, + ), + ), + ], ), ], ), ), - // Price + // Prize info Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text(draw.icon, style: AppStyle.lg), - SizedBox(height: 2), + Text(draw.icon, style: TextStyle(fontSize: 28)), + SizedBox(height: 4), Text( draw.prize, style: AppStyle.sm.copyWith( @@ -285,6 +702,13 @@ class _DrawPageState extends State { color: AppColor.textWhite, ), ), + Text( + draw.prizeValue, + style: AppStyle.xs.copyWith( + color: AppColor.textWhite.withOpacity(0.8), + fontWeight: FontWeight.w500, + ), + ), ], ), ], diff --git a/lib/presentation/pages/draw/pages/draw_detail/draw_detail_page.dart b/lib/presentation/pages/draw/pages/draw_detail/draw_detail_page.dart index addb295..55e742b 100644 --- a/lib/presentation/pages/draw/pages/draw_detail/draw_detail_page.dart +++ b/lib/presentation/pages/draw/pages/draw_detail/draw_detail_page.dart @@ -1,31 +1,1156 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:carousel_slider/carousel_slider.dart'; -import '../../../../../common/theme/theme.dart'; -import '../../../../router/app_router.gr.dart'; -import 'widgets/bottom_navbar.dart'; +import '../../../../../../common/theme/theme.dart'; +import '../../../../components/image/image.dart'; @RoutePage() -class DrawDetailPage extends StatelessWidget { +class DrawDetailPage extends StatefulWidget { const DrawDetailPage({super.key}); @override - Widget build(BuildContext context) { - return AutoTabsRouter.pageView( - routes: [ - DrawTodayRoute(), - DrawMyNumberRoute(), - DrawWinnerRoute(), - DrawInfoRoute(), - ], - physics: const NeverScrollableScrollPhysics(), - builder: (context, child, pageController) => Scaffold( - body: child, - backgroundColor: AppColor.primary, - bottomNavigationBar: DrawDetailBottomNavbar( - tabsRouter: AutoTabsRouter.of(context), + State createState() => _DrawDetailPageState(); +} + +class _DrawDetailPageState extends State + with TickerProviderStateMixin { + late AnimationController _fadeController; + late AnimationController _slideController; + late AnimationController _scaleController; + + late Animation _fadeAnimation; + late Animation _scaleAnimation; + + final CarouselSliderController _carouselController = + CarouselSliderController(); + final CarouselSliderController _numberCarouselController = + CarouselSliderController(); + final ScrollController _scrollController = ScrollController(); + + int _currentBannerIndex = 0; + int _currentNumberIndex = 0; + bool _isCollapsed = false; + + List lotteryNumbers = ['849302', '156789', '973421', '642853']; + bool isOngoing = true; + DateTime endTime = DateTime.now().add(Duration(hours: 2, minutes: 30)); + + final List prizes = [ + PrizeModel( + title: 'HADIAH UTAMA', + description: '3KG Emas Batangan', + icon: Icons.stars_rounded, + color: AppColor.warning, + isMain: true, + ), + PrizeModel( + title: 'HADIAH KE-2', + description: '1KG Emas Batangan', + icon: Icons.diamond_outlined, + color: AppColor.primary, + isMain: false, + ), + PrizeModel( + title: 'HADIAH KE-3', + description: 'Voucher Belanja 10 Juta', + icon: Icons.card_giftcard_outlined, + color: AppColor.success, + isMain: false, + ), + PrizeModel( + title: 'HADIAH HIBURAN', + description: 'Voucher Belanja 1 Juta', + icon: Icons.local_activity_outlined, + color: AppColor.info, + isMain: false, + ), + ]; + + final List banners = [ + BannerModel( + imageUrl: 'https://picsum.photos/800/400?random=1', + title: 'Undian Berhadiah Emas', + ), + BannerModel( + imageUrl: 'https://picsum.photos/800/400?random=2', + title: 'Kesempatan Menang 3KG Emas', + ), + BannerModel( + imageUrl: 'https://picsum.photos/800/400?random=3', + title: 'Undian Harian Terbesar', + ), + ]; + + @override + void initState() { + super.initState(); + + _scrollController.addListener(() { + bool isCollapsed = + _scrollController.hasClients && + _scrollController.offset > (280 - kToolbarHeight); + if (this._isCollapsed != isCollapsed) { + setState(() { + this._isCollapsed = isCollapsed; + }); + } + }); + + _fadeController = AnimationController( + duration: Duration(milliseconds: 1200), + vsync: this, + ); + _slideController = AnimationController( + duration: Duration(milliseconds: 800), + vsync: this, + ); + _scaleController = AnimationController( + duration: Duration(milliseconds: 1000), + vsync: this, + ); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut), + ); + _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut), + ); + + _startAnimations(); + } + + void _startAnimations() async { + await Future.delayed(Duration(milliseconds: 100)); + _fadeController.forward(); + await Future.delayed(Duration(milliseconds: 200)); + _slideController.forward(); + await Future.delayed(Duration(milliseconds: 300)); + _scaleController.forward(); + } + + @override + void dispose() { + _fadeController.dispose(); + _slideController.dispose(); + _scaleController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + Widget _buildCard({required Widget child}) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [AppColor.primary, AppColor.primary.withOpacity(0.8)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppColor.black.withOpacity(0.08), + blurRadius: 30, + spreadRadius: 0, + offset: Offset(0, 15), + ), + ], + ), + child: child, + ); + } + + void _showFullImage(String imageUrl, String title) { + int dialogCurrentIndex = banners.indexWhere( + (banner) => banner.imageUrl == imageUrl, + ); + if (dialogCurrentIndex == -1) dialogCurrentIndex = 0; + + final CarouselSliderController dialogCarouselController = + CarouselSliderController(); + + showDialog( + context: context, + useSafeArea: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (context, setDialogState) { + return Material( + type: MaterialType.transparency, + child: Container( + width: double.infinity, + height: MediaQuery.of(context).size.height, + color: Colors.black.withOpacity(0.9), + child: SafeArea( + child: Column( + children: [ + // Header dengan close button + Container( + padding: EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Gallery', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + Icons.close, + color: Colors.white, + size: 24, + ), + ), + ), + ], + ), + ), + + // Main carousel + Expanded( + child: CarouselSlider.builder( + itemCount: banners.length, + carouselController: dialogCarouselController, + options: CarouselOptions( + height: double.infinity, + viewportFraction: 1.0, + enableInfiniteScroll: false, + initialPage: dialogCurrentIndex, + onPageChanged: (index, reason) { + setDialogState(() { + dialogCurrentIndex = index; + }); + }, + ), + itemBuilder: (context, index, realIndex) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16), + child: InteractiveViewer( + minScale: 0.8, + maxScale: 3.0, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + banners[index].imageUrl, + fit: BoxFit.contain, + width: double.infinity, + loadingBuilder: (context, child, progress) { + if (progress == null) return child; + return Container( + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular( + 12, + ), + ), + child: Center( + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation( + Colors.white, + ), + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular( + 12, + ), + ), + child: Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + color: Colors.white, + size: 48, + ), + SizedBox(height: 8), + Text( + 'Error loading image', + style: TextStyle( + color: Colors.white, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + }, + ), + ), + + // Bottom area + Container( + padding: EdgeInsets.all(16), + child: Column( + children: [ + // Title + Text( + banners[dialogCurrentIndex].title, + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + + SizedBox(height: 16), + + // Thumbnails + SizedBox( + height: 60, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: banners.length, + itemBuilder: (context, index) { + bool isActive = dialogCurrentIndex == index; + return GestureDetector( + onTap: () { + setDialogState(() { + dialogCurrentIndex = index; + }); + dialogCarouselController.animateToPage( + index, + ); + }, + child: Container( + width: 80, + height: 60, + margin: EdgeInsets.symmetric( + horizontal: 4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isActive + ? Colors.white + : Colors.white.withOpacity(0.3), + width: isActive ? 3 : 1, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Stack( + children: [ + Image.network( + banners[index].imageUrl, + width: 80, + height: 60, + fit: BoxFit.cover, + ), + if (!isActive) + Container( + color: Colors.black.withOpacity( + 0.5, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + } + + Widget _buildLotteryCard() { + return ScaleTransition( + scale: _scaleAnimation, + child: _buildCard( + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isOngoing + ? AppColor.warning.withOpacity(0.2) + : AppColor.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + isOngoing + ? '🕐 UNDIAN SEDANG BERLANGSUNG' + : 'NOMOR UNDIAN GLOBAL', + style: TextStyle( + color: isOngoing ? AppColor.warning : AppColor.white, + fontWeight: FontWeight.w700, + fontSize: 11, + letterSpacing: 1, + ), + ), + ), + SizedBox(height: 20), + if (isOngoing) _buildCountdown() else _buildNumberCarousel(), + ], ), ), ); } + + Widget _buildCountdown() { + return StreamBuilder( + stream: Stream.periodic(Duration(seconds: 1), (_) => DateTime.now()), + builder: (context, snapshot) { + if (!snapshot.hasData) return Container(); + + final now = snapshot.data!; + final difference = endTime.difference(now); + + if (difference.isNegative) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + isOngoing = false; + }); + }); + return Container(); + } + + final hours = difference.inHours; + final minutes = difference.inMinutes % 60; + final seconds = difference.inSeconds % 60; + + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 24), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.warning.withOpacity(0.2), + AppColor.warning.withOpacity(0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColor.warning.withOpacity(0.3), + width: 1, + ), + ), + child: Column( + children: [ + Text( + 'Pengundian dimulai dalam:', + style: TextStyle( + color: AppColor.warning, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildTimeUnit(hours.toString().padLeft(2, '0'), 'JAM'), + Text( + ':', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColor.warning, + ), + ), + _buildTimeUnit(minutes.toString().padLeft(2, '0'), 'MENIT'), + Text( + ':', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColor.warning, + ), + ), + _buildTimeUnit(seconds.toString().padLeft(2, '0'), 'DETIK'), + ], + ), + ], + ), + ); + }, + ); + } + + Widget _buildTimeUnit(String time, String label) { + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppColor.warning, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: AppColor.warning.withOpacity(0.3), + blurRadius: 8, + offset: Offset(0, 4), + ), + ], + ), + child: Text( + time, + style: TextStyle( + color: AppColor.white, + fontSize: 24, + fontWeight: FontWeight.w900, + ), + ), + ), + SizedBox(height: 4), + Text( + label, + style: TextStyle( + color: AppColor.warning, + fontWeight: FontWeight.w600, + fontSize: 10, + ), + ), + ], + ); + } + + Widget _buildNumberCarousel() { + return Column( + children: [ + Container( + height: 80, + child: CarouselSlider.builder( + carouselController: _numberCarouselController, + itemCount: lotteryNumbers.length, + options: CarouselOptions( + height: 80, + viewportFraction: 1.0, + enableInfiniteScroll: true, + autoPlay: false, + onPageChanged: (index, reason) { + setState(() => _currentNumberIndex = index); + }, + ), + itemBuilder: (context, index, realIndex) { + return Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.white.withOpacity(0.2), + AppColor.white.withOpacity(0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColor.white.withOpacity(0.3), + width: 1, + ), + ), + child: Center( + child: Text( + lotteryNumbers[index], + style: TextStyle( + color: AppColor.white, + fontSize: 42, + fontWeight: FontWeight.w900, + letterSpacing: 12, + height: 1.0, + ), + ), + ), + ); + }, + ), + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: lotteryNumbers.asMap().entries.map((entry) { + return GestureDetector( + onTap: () => _numberCarouselController.animateToPage(entry.key), + child: Container( + width: _currentNumberIndex == entry.key ? 24 : 8, + height: 8, + margin: EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: _currentNumberIndex == entry.key + ? AppColor.white + : AppColor.white.withOpacity(0.4), + ), + ), + ); + }).toList(), + ), + SizedBox(height: 12), + Text( + '${_currentNumberIndex + 1} dari ${lotteryNumbers.length} nomor', + style: TextStyle( + color: AppColor.white.withOpacity(0.8), + fontWeight: FontWeight.w500, + fontSize: 11, + ), + ), + ], + ); + } + + Widget _buildPrizeCard() { + return SlideTransition( + position: Tween(begin: Offset(0, 0.5), end: Offset.zero).animate( + CurvedAnimation( + parent: _slideController, + curve: Interval(0.3, 1.0, curve: Curves.easeOutCubic), + ), + ), + child: _buildCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'DAFTAR HADIAH', + style: TextStyle( + color: AppColor.white.withOpacity(0.9), + fontWeight: FontWeight.w700, + fontSize: 14, + letterSpacing: 1.5, + ), + ), + SizedBox(height: 20), + ...prizes.map((prize) => _buildPrizeItem(prize)).toList(), + ], + ), + ), + ); + } + + Widget _buildPrizeItem(PrizeModel prize) { + return Container( + margin: EdgeInsets.only(bottom: 16), + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + prize.color.withOpacity(prize.isMain ? 0.3 : 0.2), + prize.color.withOpacity(prize.isMain ? 0.2 : 0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: prize.color.withOpacity(prize.isMain ? 0.5 : 0.3), + width: prize.isMain ? 2 : 1, + ), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(prize.isMain ? 12 : 10), + decoration: BoxDecoration( + color: prize.color, + borderRadius: BorderRadius.circular(12), + boxShadow: prize.isMain + ? [ + BoxShadow( + color: prize.color.withOpacity(0.3), + blurRadius: 10, + offset: Offset(0, 4), + ), + ] + : null, + ), + child: Icon( + prize.icon, + color: AppColor.white, + size: prize.isMain ? 24 : 20, + ), + ), + SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + prize.title, + style: TextStyle( + color: prize.color, + fontWeight: FontWeight.w700, + fontSize: 11, + letterSpacing: 0.5, + ), + ), + SizedBox(height: 4), + Text( + prize.description, + style: TextStyle( + color: AppColor.white, + fontWeight: prize.isMain + ? FontWeight.w700 + : FontWeight.w600, + fontSize: 14, + ), + ), + ], + ), + ), + if (prize.isMain) + Container( + padding: EdgeInsets.all(6), + decoration: BoxDecoration( + color: prize.color.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(Icons.star, color: prize.color, size: 16), + ), + ], + ), + ); + } + + Widget _buildShoppingCard() { + return SlideTransition( + position: Tween(begin: Offset(0, 0.8), end: Offset.zero).animate( + CurvedAnimation( + parent: _slideController, + curve: Interval(0.5, 1.0, curve: Curves.easeOutCubic), + ), + ), + child: _buildCard( + child: Column( + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(16), + ), + child: Icon( + Icons.shopping_bag_outlined, + color: AppColor.white, + size: 24, + ), + ), + SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Perbesar Peluang Menang!', + style: TextStyle( + color: AppColor.white, + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + SizedBox(height: 4), + Text( + 'Belanja sekarang untuk ticket tambahan', + style: TextStyle( + color: AppColor.white.withOpacity(0.8), + fontWeight: FontWeight.w500, + fontSize: 11, + ), + ), + ], + ), + ), + ], + ), + SizedBox(height: 20), + Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColor.white.withOpacity(0.2), + width: 1, + ), + ), + child: Column( + children: [ + _buildBenefit('🎫', 'Setiap Rp 100K = 1 ticket undian'), + SizedBox(height: 12), + _buildBenefit( + '⚡', + 'Semakin banyak belanja, semakin berpeluang', + ), + SizedBox(height: 12), + _buildBenefit('🏆', 'Kesempatan menang hingga 3KG emas!'), + ], + ), + ), + SizedBox(height: 20), + Container( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Mengarahkan ke halaman belanja...'), + backgroundColor: AppColor.success, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColor.white, + foregroundColor: AppColor.primary, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.shopping_cart_outlined, size: 20), + SizedBox(width: 12), + Text( + 'Mulai Belanja Sekarang', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildBenefit(String icon, String text) { + return Row( + children: [ + Text(icon, style: TextStyle(fontSize: 16)), + SizedBox(width: 12), + Expanded( + child: Text( + text, + style: TextStyle( + color: AppColor.white.withOpacity(0.9), + fontWeight: FontWeight.w500, + fontSize: 11, + ), + ), + ), + ], + ); + } + + Widget _buildTermsAndConditions() { + return SlideTransition( + position: Tween(begin: Offset(0, 1.0), end: Offset.zero).animate( + CurvedAnimation( + parent: _slideController, + curve: Interval(0.7, 1.0, curve: Curves.easeOutCubic), + ), + ), + child: _buildCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.description_outlined, + color: AppColor.white.withOpacity(0.9), + size: 20, + ), + SizedBox(width: 12), + Text( + 'Syarat & Ketentuan', + style: TextStyle( + color: AppColor.white, + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + ], + ), + SizedBox(height: 20), + _buildTermItem( + '1.', + 'Undian berlaku untuk seluruh pelanggan yang berbelanja minimal Rp 100.000', + ), + _buildTermItem( + '2.', + 'Setiap pembelian Rp 100.000 mendapat 1 nomor undian otomatis', + ), + _buildTermItem( + '3.', + 'Pengundian dilakukan secara live streaming setiap hari pukul 20:00 WIB', + ), + _buildTermItem( + '4.', + 'Pemenang akan dihubungi maksimal 24 jam setelah pengundian', + ), + _buildTermItem( + '5.', + 'Hadiah tidak dapat diuangkan atau dipindahtangankan', + ), + _buildTermItem( + '6.', + 'Keputusan panitia adalah final dan tidak dapat diganggu gugat', + ), + SizedBox(height: 20), + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: AppColor.white, size: 16), + SizedBox(width: 12), + Expanded( + child: Text( + 'Untuk informasi lebih lanjut, hubungi customer service kami', + style: TextStyle( + color: AppColor.white.withOpacity(0.9), + fontWeight: FontWeight.w500, + fontSize: 11, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildTermItem(String number, String text) { + return Padding( + padding: EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 24, + child: Text( + number, + style: TextStyle( + color: AppColor.white, + fontWeight: FontWeight.w600, + fontSize: 11, + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: Text( + text, + style: TextStyle( + color: AppColor.white.withOpacity(0.9), + fontWeight: FontWeight.w500, + fontSize: 11, + height: 1.4, + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + controller: _scrollController, + physics: BouncingScrollPhysics(), + slivers: [ + SliverAppBar( + expandedHeight: 280, + floating: false, + pinned: true, + backgroundColor: AppColor.surface, + leading: Container( + margin: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + borderRadius: BorderRadius.circular(20), + ), + child: IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ), + flexibleSpace: FlexibleSpaceBar( + title: AnimatedOpacity( + opacity: _isCollapsed ? 1.0 : 0.0, + duration: Duration(milliseconds: 200), + child: Text( + "Undian", + style: TextStyle( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + fontSize: 18, + ), + ), + ), + titlePadding: EdgeInsets.only(left: 72, bottom: 16), + collapseMode: CollapseMode.parallax, + background: Stack( + children: [ + Positioned.fill( + child: CarouselSlider.builder( + carouselController: _carouselController, + itemCount: banners.length, + options: CarouselOptions( + height: double.infinity, + viewportFraction: 1.0, + autoPlay: true, + autoPlayInterval: Duration(seconds: 4), + autoPlayAnimationDuration: Duration(milliseconds: 800), + onPageChanged: (index, reason) { + setState(() => _currentBannerIndex = index); + }, + ), + itemBuilder: (context, index, realIndex) { + final banner = banners[index]; + return GestureDetector( + onTap: () => + _showFullImage(banner.imageUrl, banner.title), + child: AppNetworkImage(url: banner.imageUrl), + ); + }, + ), + ), + + Positioned( + bottom: 20, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: banners.asMap().entries.map((entry) { + return GestureDetector( + onTap: () => + _carouselController.animateToPage(entry.key), + child: Container( + width: _currentBannerIndex == entry.key ? 24 : 8, + height: 8, + margin: EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: _currentBannerIndex == entry.key + ? Colors.white + : Colors.white.withOpacity(0.4), + ), + ), + ); + }).toList(), + ), + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: AppColor.primaryGradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: FadeTransition( + opacity: _fadeAnimation, + child: Column( + children: [ + SizedBox(height: 20), + _buildLotteryCard(), + SizedBox(height: 20), + _buildPrizeCard(), + SizedBox(height: 20), + if (isOngoing) _buildShoppingCard(), + if (isOngoing) SizedBox(height: 20), + _buildTermsAndConditions(), + SizedBox(height: 40), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class PrizeModel { + final String title; + final String description; + final IconData icon; + final Color color; + final bool isMain; + + PrizeModel({ + required this.title, + required this.description, + required this.icon, + required this.color, + this.isMain = false, + }); +} + +class BannerModel { + final String imageUrl; + final String title; + + BannerModel({required this.imageUrl, required this.title}); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a79a42f..311f992 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,11 +8,13 @@ import Foundation import connectivity_plus import path_provider_foundation import shared_preferences_foundation +import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 62adc26..c6d44f7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.11.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" carousel_slider: dependency: "direct main" description: @@ -342,6 +366,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_gen_core: dependency: transitive description: @@ -624,6 +656,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -760,6 +800,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shared_preferences: dependency: "direct main" description: @@ -832,6 +880,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -861,6 +917,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -893,6 +997,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -997,6 +1109,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 15e27c8..80453d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,8 @@ dependencies: shared_preferences: ^2.5.3 carousel_slider: ^5.1.1 url_launcher: ^6.3.2 + cached_network_image: ^3.4.1 + shimmer: ^3.0.0 dev_dependencies: flutter_test: