import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:shimmer/shimmer.dart'; import '../app_design.dart'; import '../app_state.dart'; import '../models/api_result_model.dart'; import '../models/mobile_child_model.dart'; import '../models/news_article_model.dart'; import '../models/riwayat_response_model.dart'; import '../services/api_service.dart'; import '../utils/nutrition_display_utils.dart'; import 'article_detail_screen.dart'; import 'articles_screen.dart'; import 'recommendation_screen.dart'; import 'riwayat_screen.dart'; class NutritionScreen extends StatefulWidget { const NutritionScreen({super.key}); @override State createState() => _NutritionScreenState(); } class _NutritionScreenState extends State { final ApiService _apiService = ApiService(); final SgiziAppState _appState = SgiziAppState.instance; late Future<_NutritionViewData> _future; String _activeCategory = 'Semua'; int _activeAgeIndex = 0; int _activeFoodIndex = 0; @override void initState() { super.initState(); _appState.addListener(_handleStateChanged); _future = _load(); } @override void dispose() { _appState.removeListener(_handleStateChanged); super.dispose(); } void _handleStateChanged() { setState(() => _future = _load()); } Future<_NutritionViewData> _load() async { final child = _appState.activeChild; if (child == null) { return const _NutritionViewData.noActiveChild(); } final history = await _apiService.getRiwayat(childId: child.id); final articles = await _apiService.getArticlesDb(); final sorted = [...history.riwayat]..sort((a, b) { final bDate = DateTime.tryParse(b.tanggalUkur); final aDate = DateTime.tryParse(a.tanggalUkur); if (aDate == null && bDate == null) return 0; if (aDate == null) return 1; if (bDate == null) return -1; return bDate.compareTo(aDate); }); final latest = sorted.isNotEmpty ? sorted.first : null; if (latest == null) { return _NutritionViewData( child: child, latestMeasurement: null, recommendations: const [], articles: articles, ); } final recommendationResponse = await _apiService.getRecommendations( status: '', childId: child.id, riwayatId: latest.id, ); return _NutritionViewData( child: child, latestMeasurement: latest, // Preview rekomendasi berdasarkan riwayat TERAKHIR anak aktif. recommendations: recommendationResponse.items, articles: articles, ); } void _retry() { setState(() => _future = _load()); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _NutriColors.background, body: SafeArea( child: FutureBuilder<_NutritionViewData>( future: _future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const _NutritionSkeleton(); } if (snapshot.hasError) { return ErrorState( message: 'Halaman nutrisi belum bisa dimuat. Periksa koneksi lalu coba lagi.', onRetry: _retry, ); } final data = snapshot.data!; if (data.child == null) { return EmptyState( title: 'Belum Ada Anak Aktif', message: 'Pilih anak aktif terlebih dahulu untuk melihat insight nutrisi terbaru.', actionLabel: 'Muat Ulang', onAction: _retry, ); } return SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const _NutritionHeader(), Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _AgeGuideSection( activeIndex: _activeAgeIndex, onPageChanged: (index) => setState(() => _activeAgeIndex = index), ), const SizedBox(height: 24), _CategoryChipsRow( activeCategory: _activeCategory, onCategorySelected: (label) => setState(() => _activeCategory = label), ), const SizedBox(height: 16), _ArticleSection( data: data, activeCategory: _activeCategory, onRetry: _retry, ), const SizedBox(height: 24), _LatestStatusSection(data: data), const SizedBox(height: 24), _FoodRecommendationSection( data: data, activeIndex: _activeFoodIndex, onPageChanged: (index) => setState(() => _activeFoodIndex = index), ), ], ), ), ], ), ); }, ), ), ); } } class _NutritionViewData { const _NutritionViewData({ required this.child, required this.latestMeasurement, required this.recommendations, required this.articles, }); const _NutritionViewData.noActiveChild() : child = null, latestMeasurement = null, recommendations = const [], articles = const []; final MobileChildModel? child; final RiwayatItemModel? latestMeasurement; final List recommendations; final List articles; } class _NutritionHeader extends StatelessWidget { const _NutritionHeader(); @override Widget build(BuildContext context) { return Container( width: double.infinity, decoration: const BoxDecoration( gradient: LinearGradient( colors: [_NutriColors.primaryDark, _NutriColors.primary], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.vertical( bottom: Radius.circular(32), ), ), child: Stack( children: [ Positioned( right: -40, top: -20, child: _FloatingCircle( size: 160, color: Colors.white.withValues(alpha: 0.08), ), ), Positioned( left: -24, bottom: -40, child: _FloatingCircle( size: 140, color: Colors.white.withValues(alpha: 0.06), ), ), Positioned( right: 24, top: 26, child: Icon( LucideIcons.activity, color: Colors.white.withValues(alpha: 0.5), size: 40, ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 18, 20, 24), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), Text( 'Nutrisi', style: AppTypography.h1.copyWith( color: Colors.white, fontSize: 26, ), ), const SizedBox(height: 6), Text( 'Panduan nutrisi dan edukasi tumbuh kembang si kecil secara optimal.', style: AppTypography.body.copyWith( color: Colors.white.withValues(alpha: 0.9), ), ), ], ), ), ], ), ), ], ), ) .animate() .fadeIn(duration: 500.ms, curve: Curves.easeOut) .slideY(begin: -0.1, end: 0, duration: 500.ms, curve: Curves.easeOut); } } class _FloatingCircle extends StatelessWidget { const _FloatingCircle({required this.size, required this.color}); final double size; final Color color; @override Widget build(BuildContext context) { return Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, color: color, ), ); } } class _AgeGuideSection extends StatelessWidget { const _AgeGuideSection({ required this.activeIndex, required this.onPageChanged, }); final int activeIndex; final ValueChanged onPageChanged; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Panduan Usia', style: AppTypography.h2.copyWith( color: SgColors.textPrimary, ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: _NutriColors.secondary.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: const [ Icon( PhosphorIconsLight.baby, size: 14, color: _NutriColors.primary, ), SizedBox(width: 6), Text( '0–5 Tahun', style: AppTypography.caption, ), ], ), ), ], ), const SizedBox(height: 12), CarouselSlider.builder( itemCount: _ageGuides.length, options: CarouselOptions( height: 292, viewportFraction: 0.9, enlargeCenterPage: true, enlargeFactor: 0.16, autoPlay: true, autoPlayInterval: const Duration(seconds: 6), enableInfiniteScroll: true, onPageChanged: (index, _) => onPageChanged(index), ), itemBuilder: (context, index, realIndex) { final guide = _ageGuides[index]; final isActive = index == activeIndex; return _AgeGuideCard( guide: guide, isActive: isActive, ); }, ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(_ageGuides.length, (index) { final isActive = index == activeIndex; return AnimatedContainer( duration: const Duration(milliseconds: 220), margin: const EdgeInsets.symmetric(horizontal: 3), height: 7, width: isActive ? 18 : 7, decoration: BoxDecoration( color: isActive ? _NutriColors.primary : _NutriColors.primary.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(99), ), ); }), ), ], ) .animate(delay: 80.ms) .fadeIn(duration: 420.ms, curve: Curves.easeOut) .slideY(begin: 0.03, end: 0, duration: 420.ms, curve: Curves.easeOut); } } class _CategoryChipsRow extends StatelessWidget { const _CategoryChipsRow({ required this.activeCategory, required this.onCategorySelected, }); final String activeCategory; final ValueChanged onCategorySelected; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Kategori Edukasi', style: _sectionLabelStyle()), const SizedBox(height: 10), SizedBox( height: 40, child: ListView.separated( scrollDirection: Axis.horizontal, itemBuilder: (_, index) { final label = _nutritionCategories[index]; final isActive = label == activeCategory; return _CategoryChip( label: label, isActive: isActive, onTap: () => onCategorySelected(label), ); }, separatorBuilder: (_, __) => const SizedBox(width: 8), itemCount: _nutritionCategories.length, ), ), ], ) .animate(delay: 140.ms) .fadeIn(duration: 380.ms, curve: Curves.easeOut) .slideY(begin: 0.03, end: 0, duration: 380.ms, curve: Curves.easeOut); } } class _ArticleSection extends StatelessWidget { const _ArticleSection({ required this.data, required this.activeCategory, required this.onRetry, }); final _NutritionViewData data; final String activeCategory; final VoidCallback onRetry; @override Widget build(BuildContext context) { final articles = data.articles .where( (a) => activeCategory == 'Semua' || a.category.toLowerCase().contains(activeCategory.toLowerCase()), ) .toList(); final isAll = activeCategory == 'Semua'; final visible = isAll ? articles.take(3).toList() : articles; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text('Artikel Edukasi', style: _sectionLabelStyle()), const SizedBox(width: 8), Icon( LucideIcons.bookOpenCheck, size: 16, color: _NutriColors.primary, ), const Spacer(), if (isAll && articles.length > 3) TextButton( onPressed: () { Navigator.of(context).push( fadeRoute( ArticlesScreen( title: 'Artikel Edukasi', initialCategory: activeCategory, articles: data.articles, ), ), ); }, child: const Text('Lihat Semua'), ), ], ), const SizedBox(height: 10), if (data.articles.isEmpty) _SectionEmptyInfo( message: 'Artikel edukasi belum tersedia. Sentuh untuk memuat ulang dari server.', actionLabel: 'Muat Ulang', onAction: onRetry, ) else if (articles.isEmpty) const _SectionEmptyInfo( message: 'Belum ada artikel untuk kategori ini. Coba pilih kategori lain.', ) else Column( children: visible .map( (article) => _ArticleCard( article: article, allArticles: data.articles, ), ) .toList(), ), ], ); } } class _LatestStatusSection extends StatelessWidget { const _LatestStatusSection({required this.data}); final _NutritionViewData data; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Status Gizi Anak', style: _sectionLabelStyle()), const SizedBox(height: 12), if (data.latestMeasurement == null) _SectionEmptyInfo( message: 'Belum ada riwayat pengukuran untuk anak aktif. Tambahkan pengukuran untuk menampilkan status terbaru.', actionLabel: 'Buka Riwayat', onAction: () { Navigator.of(context).push( fadeRoute(RiwayatScreen(childId: data.child!.id)), ); }, ) else _LatestNutritionStatusCard( child: data.child!, latest: data.latestMeasurement!, onOpenHistory: () => Navigator.of(context).push( fadeRoute(RiwayatScreen(childId: data.child!.id)), ), ), ], ); } } class _FoodRecommendationSection extends StatelessWidget { const _FoodRecommendationSection({ required this.data, required this.activeIndex, required this.onPageChanged, }); final _NutritionViewData data; final int activeIndex; final ValueChanged onPageChanged; @override Widget build(BuildContext context) { // Preview max 3, berasal dari rekomendasi untuk riwayat TERAKHIR anak aktif. final items = data.recommendations.take(3).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( 'Rekomendasi Untuk Anak', style: AppTypography.h2.copyWith( color: SgColors.textPrimary, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), TextButton( onPressed: () { Navigator.of(context).push( fadeRoute( RecommendationScreen( childId: data.child!.id, riwayatId: data.latestMeasurement?.id, childName: data.child!.nama, status: data.latestMeasurement?.statusGabungan, measuredAt: data.latestMeasurement?.tanggalUkur, ), ), ); }, child: const Text('Lihat Semua'), ), ], ), const SizedBox(height: 4), Text( 'Menu sesuai status gizi terbaru anak.', style: AppTypography.body, ), const SizedBox(height: 12), if (items.isEmpty) const _SectionEmptyInfo( message: 'Preview rekomendasi belum tersedia. Silakan lengkapi pengukuran terbaru.', ) else Column( children: [ CarouselSlider.builder( itemCount: items.length, options: CarouselOptions( height: 364, viewportFraction: 0.9, enlargeCenterPage: true, enlargeFactor: 0.18, autoPlay: true, autoPlayInterval: const Duration(seconds: 7), enableInfiniteScroll: items.length > 1, onPageChanged: (index, _) => onPageChanged(index), ), itemBuilder: (context, index, realIndex) { final item = items[index]; return _FoodPreviewCard( item: item, assetImage: _foodAssetByIndex(index), ); }, ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(items.length, (index) { final isActive = index == activeIndex; return AnimatedContainer( duration: const Duration(milliseconds: 220), margin: const EdgeInsets.symmetric(horizontal: 3), height: 7, width: isActive ? 18 : 7, decoration: BoxDecoration( color: isActive ? _NutriColors.primary : _NutriColors.primary.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(99), ), ); }), ), ], ), ], ) .animate(delay: 220.ms) .fadeIn(duration: 420.ms, curve: Curves.easeOut) .slideY(begin: 0.03, end: 0, duration: 420.ms, curve: Curves.easeOut); } } class _CategoryChip extends StatelessWidget { const _CategoryChip({ required this.label, required this.isActive, required this.onTap, }); final String label; final bool isActive; final VoidCallback onTap; @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(999), onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: isActive ? _NutriColors.primary : Colors.white, borderRadius: BorderRadius.circular(999), border: Border.all( color: isActive ? _NutriColors.primary : SgColors.border, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Center( child: Text( label, style: AppTypography.caption.copyWith( color: isActive ? Colors.white : SgColors.textPrimary, fontWeight: FontWeight.w700, ), ), ), ), ); } } class _EducationCard extends StatelessWidget { const _EducationCard({required this.item}); final _EducationItem item; @override Widget build(BuildContext context) { return HealthCard( margin: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( radius: 24, backgroundColor: const Color(0xFFE9F6F2), child: Icon(item.icon, color: SgColors.primary), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.title, style: AppTypography.h3, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 6), Text( item.description, style: AppTypography.body, maxLines: 3, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } } class _AgeGuideCard extends StatelessWidget { const _AgeGuideCard({required this.guide, required this.isActive}); final _AgeGuide guide; final bool isActive; @override Widget build(BuildContext context) { final card = AnimatedContainer( duration: const Duration(milliseconds: 240), curve: Curves.easeOut, margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2), padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: SgColors.border), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: isActive ? 0.10 : 0.05), blurRadius: isActive ? 30 : 18, offset: const Offset(0, 12), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: _NutriColors.secondary.withValues(alpha: 0.25), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( guide.icon, color: _NutriColors.primary, size: 18, ), const SizedBox(width: 6), Text( guide.title, style: AppTypography.h3.copyWith( color: SgColors.textPrimary, ), ), ], ), ), ], ), const SizedBox(height: 8), _InfoRow(label: 'Fokus', value: guide.focus), _InfoRow(label: 'Makanan dianjurkan', value: guide.foodSuggestion), _InfoRow(label: 'Tips singkat', value: guide.tip), ], ), ); return card .animate(target: isActive ? 1 : 0) .scale( begin: const Offset(0.96, 0.96), end: const Offset(1, 1), duration: 260.ms, curve: Curves.easeOut, ); } } const List _nutritionCategories = [ 'Semua', 'Stunting', 'MPASI', 'Protein', 'Vitamin', 'Gizi Seimbang', ]; class _LatestNutritionStatusCard extends StatelessWidget { const _LatestNutritionStatusCard({ required this.child, required this.latest, required this.onOpenHistory, }); final MobileChildModel child; final RiwayatItemModel latest; final VoidCallback onOpenHistory; @override Widget build(BuildContext context) { final visual = nutritionStatusVisual(latest.statusGabungan); return HealthCard( color: const Color(0xFFEAF7F7), borderColor: const Color(0xFFCBEAEA), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( child.nama, style: AppTypography.h2, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( latest.statusGabungan, style: AppTypography.h3, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), Flexible( child: StatusBadge( text: visual.badgeLabel, color: visual.color, ), ), ], ), const SizedBox(height: 10), Text( 'Pengukuran: ${formatMeasurementDate(latest.tanggalUkur)}', style: AppTypography.caption, ), const SizedBox(height: 12), Text( recommendationStatusExplanation(latest.statusGabungan), style: AppTypography.body.copyWith(color: SgColors.textPrimary), ), const SizedBox(height: 12), Text( 'Saran: ${_suggestionForStatus(latest.statusGabungan)}', style: AppTypography.body, ), const SizedBox(height: 16), PrimaryButton( label: 'Lihat Riwayat Lengkap', icon: Icons.timeline_rounded, onPressed: onOpenHistory, ), ], ), ) .animate() .fadeIn(duration: 420.ms, curve: Curves.easeOut) .slideY(begin: 0.02, end: 0, duration: 420.ms, curve: Curves.easeOut); } } class _FoodPreviewCard extends StatelessWidget { const _FoodPreviewCard({required this.item, required this.assetImage}); final RekomendasiModel item; final String assetImage; @override Widget build(BuildContext context) { return HealthCard( margin: EdgeInsets.zero, padding: EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), child: AspectRatio( aspectRatio: 16 / 9, child: Stack( fit: StackFit.expand, children: [ Image.asset( assetImage, fit: BoxFit.cover, ), Positioned( left: 12, top: 12, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6, ), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.42), borderRadius: BorderRadius.circular(999), ), child: Text( '${item.kalori} kkal', style: AppTypography.caption.copyWith( color: Colors.white, fontWeight: FontWeight.w700, ), ), ), ), ], ), ), ), Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.menu, style: AppTypography.h3, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 6), Text('${item.protein}g protein', style: AppTypography.caption), const SizedBox(height: 10), Text( 'Mengapa cocok? ${item.alasan}', style: AppTypography.body, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } } class _SectionEmptyInfo extends StatelessWidget { const _SectionEmptyInfo({required this.message, this.actionLabel, this.onAction}); final String message; final String? actionLabel; final VoidCallback? onAction; @override Widget build(BuildContext context) { return HealthCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(message, style: AppTypography.body), if (actionLabel != null && onAction != null) ...[ const SizedBox(height: 14), PrimaryButton(label: actionLabel!, onPressed: onAction, isOutlined: true), ], ], ), ); } } class _InfoRow extends StatelessWidget { const _InfoRow({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 132, child: Text(label, style: AppTypography.caption), ), const SizedBox(width: 8), Expanded( child: Text( value, style: AppTypography.body.copyWith(color: SgColors.textPrimary), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ); } } class _NutritionSkeleton extends StatelessWidget { const _NutritionSkeleton(); @override Widget build(BuildContext context) { return Shimmer.fromColors( baseColor: const Color(0xFFE3EBE8), highlightColor: const Color(0xFFF3F7F5), child: ListView( padding: const EdgeInsets.fromLTRB(20, 16, 20, 28), children: [ Container( width: double.infinity, height: 140, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), ), ), const SizedBox(height: 20), Container(width: 130, height: 18, color: Colors.white), const SizedBox(height: 12), SizedBox( height: 150, child: ListView.separated( scrollDirection: Axis.horizontal, itemBuilder: (_, __) => Container( width: 260, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(22), ), ), separatorBuilder: (_, __) => const SizedBox(width: 12), itemCount: 3, ), ), const SizedBox(height: 24), for (var i = 0; i < 3; i++) ...[ HealthCard( child: Container( height: 86, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), ), ), ), const SizedBox(height: 12), ], ], ), ); } } class _ArticleCard extends StatelessWidget { const _ArticleCard({ required this.article, required this.allArticles, }); final NewsArticleModel article; final List allArticles; @override Widget build(BuildContext context) { return HealthCard( margin: const EdgeInsets.only(bottom: 12), padding: EdgeInsets.zero, onTap: () { Navigator.of(context).push( fadeRoute( ArticleDetailScreen( article: article, related: allArticles, ), ), ); }, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Hero( tag: 'article-${article.id}', child: ClipRRect( borderRadius: const BorderRadius.horizontal(left: Radius.circular(20)), child: SizedBox( width: 104, height: 96, child: Image.asset( _articleAssetByIndex(article.id), fit: BoxFit.cover, ), ), ), ), const SizedBox(width: 12), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: _NutriColors.secondary.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(999), ), child: Text( article.category, style: AppTypography.caption.copyWith( color: _NutriColors.primaryDark, fontWeight: FontWeight.w700, ), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), Text( article.title, style: AppTypography.h3.copyWith( color: SgColors.textPrimary, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( article.description, style: AppTypography.body, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Text( 'Baca Selengkapnya', style: AppTypography.caption.copyWith( color: _NutriColors.primary, fontWeight: FontWeight.w700, ), ), ], ), ), ), ], ), ); } } class _EducationItem { const _EducationItem({ required this.title, required this.description, required this.icon, required this.categories, }); final String title; final String description; final IconData icon; final List categories; } class _AgeGuide { const _AgeGuide({ required this.title, required this.focus, required this.foodSuggestion, required this.tip, required this.icon, }); final String title; final String focus; final String foodSuggestion; final String tip; final IconData icon; } const List<_EducationItem> _educationItems = [ _EducationItem( title: 'Cegah Stunting Sejak Dini', description: 'Pantau tinggi dan berat badan rutin serta penuhi protein hewani harian agar tumbuh kembang optimal.', icon: Icons.height_rounded, categories: ['Stunting', 'Protein', 'Gizi Seimbang'], ), _EducationItem( title: 'MPASI Bertahap dan Tepat Tekstur', description: 'Mulai MPASI usia 6 bulan, tingkatkan tekstur bertahap, dan jaga frekuensi makan sesuai usia.', icon: Icons.ramen_dining_rounded, categories: ['MPASI', 'Gizi Seimbang'], ), _EducationItem( title: 'Protein untuk Pertumbuhan Tinggi Badan', description: 'Protein hewani seperti telur, ikan, ayam, dan daging membantu pembentukan jaringan tubuh anak.', icon: Icons.egg_alt_rounded, categories: ['Protein', 'Stunting'], ), _EducationItem( title: 'Vitamin dan Mineral Esensial', description: 'Lengkapi menu dengan sayur dan buah berwarna agar kebutuhan vitamin A, C, zat besi, dan zinc terpenuhi.', icon: Icons.eco_rounded, categories: ['Vitamin', 'Gizi Seimbang'], ), ]; const List<_AgeGuide> _ageGuides = [ _AgeGuide( title: 'Usia 0-6 Bulan', focus: 'ASI eksklusif sesuai kebutuhan bayi.', foodSuggestion: 'ASI sebagai sumber nutrisi utama.', tip: 'Susui lebih sering saat bayi menunjukkan tanda lapar.', icon: Icons.child_friendly_rounded, ), _AgeGuide( title: 'Usia 6-12 Bulan', focus: 'MPASI bertahap dengan tekstur meningkat.', foodSuggestion: 'Bubur tim, pure sayur, telur, ikan lembut.', tip: 'Kenalkan tekstur dari halus ke kasar secara bertahap.', icon: Icons.soup_kitchen_rounded, ), _AgeGuide( title: 'Usia 1-3 Tahun', focus: 'Variasi makanan dengan protein cukup.', foodSuggestion: 'Nasi, lauk hewani, sayur, buah, susu.', tip: 'Biasakan jam makan teratur dan camilan sehat.', icon: Icons.set_meal_rounded, ), _AgeGuide( title: 'Usia 3-5 Tahun', focus: 'Dukung pertumbuhan tinggi badan dan daya tahan tubuh.', foodSuggestion: 'Ikan, ayam, telur, tempe, sayur hijau, buah.', tip: 'Tambahkan protein hewani dan ajak anak aktif bergerak.', icon: Icons.directions_run_rounded, ), ]; String _suggestionForStatus(String status) { final value = status.toLowerCase(); if (value.contains('stunting')) { return 'Tambahkan protein hewani dan pantau tinggi badan rutin.'; } if (value.contains('kurang') || value.contains('underweight')) { return 'Tingkatkan porsi energi-protein, termasuk camilan bergizi.'; } if (value.contains('obesitas') || value.contains('lebih')) { return 'Atur porsi seimbang, kurangi gula tambahan, dan tingkatkan aktivitas fisik.'; } return 'Pertahankan pola makan gizi seimbang dan pemantauan berkala.'; } String _imageForIndex(int index) { const images = [ 'https://images.unsplash.com/photo-1512058564366-18510be2db19?auto=format&fit=crop&w=900&q=80', 'https://images.unsplash.com/photo-1490645935967-10de6ba17061?auto=format&fit=crop&w=900&q=80', 'https://images.unsplash.com/photo-1547592166-23ac45744acd?auto=format&fit=crop&w=900&q=80', ]; return images[index % images.length]; } TextStyle _sectionLabelStyle() { return AppTypography.caption.copyWith( letterSpacing: 1, fontWeight: FontWeight.w800, color: _NutriColors.primaryDark, ); } class _NutriColors { const _NutriColors._(); static const primary = Color(0xFF0B7A86); static const primaryDark = Color(0xFF085B63); static const secondary = Color(0xFF7FD6C2); static const background = Color(0xFFF5F7F6); } String _foodAssetByIndex(int index) { const assets = [ 'assets/image/onboarding_food.png', 'assets/image/onboarding_monitoring.png', 'assets/image/onboarding_consultation.png', ]; return assets[index % assets.length]; } String _articleAssetByIndex(int index) { const assets = [ 'assets/image/onboarding_food.png', 'assets/image/onboarding_monitoring.png', 'assets/image/onboarding_consultation.png', ]; return assets[index % assets.length]; }