937 lines
33 KiB
Dart
937 lines
33 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../services/artikel_service.dart';
|
|
import '../dashboard/artikel_detail_screen.dart';
|
|
import 'dart:async';
|
|
|
|
class ArtikelScreen extends StatefulWidget {
|
|
@override
|
|
_ArtikelScreenState createState() => _ArtikelScreenState();
|
|
}
|
|
|
|
class _ArtikelScreenState extends State<ArtikelScreen> with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late Animation<double> _fadeAnimation;
|
|
|
|
String _selectedCategory = 'Semua';
|
|
final List<String> _categories = ['Semua', 'Gizi', 'Imunisasi', 'Kesehatan', 'Tumbuh Kembang', 'Vitamin', 'Stunting'];
|
|
|
|
// Artikel service
|
|
final ArtikelService _artikelService = ArtikelService();
|
|
|
|
// Artikel data
|
|
List<ArtikelModel> _artikelList = [];
|
|
bool _isLoading = true;
|
|
String? _error;
|
|
int _currentPage = 1;
|
|
int _totalPages = 1;
|
|
bool _hasMore = false; // Set default ke false untuk mencegah infinite loading
|
|
|
|
// Flag untuk menentukan apakah menggunakan data dummy atau API
|
|
// Set ke false untuk mencoba menggunakan API dulu
|
|
final bool _useDummyDataOnly = false;
|
|
|
|
// Flag untuk mencegah multiple loading calls
|
|
bool _isLoadingMore = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(milliseconds: 1000),
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(
|
|
begin: 0.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: Interval(0.1, 1.0, curve: Curves.easeOut),
|
|
));
|
|
|
|
_animationController.forward();
|
|
|
|
// Inisialisasi data
|
|
_loadArtikels();
|
|
}
|
|
|
|
void _initializeData() {
|
|
if (_useDummyDataOnly) {
|
|
// Langsung gunakan data dummy jika flag aktif
|
|
setState(() {
|
|
_artikelList = _getDummyArtikels();
|
|
_isLoading = false;
|
|
_hasMore = false;
|
|
});
|
|
} else {
|
|
// Coba load dari API
|
|
_loadArtikels();
|
|
}
|
|
}
|
|
|
|
Future<void> _loadArtikels({bool refresh = false}) async {
|
|
if (_isLoadingMore) return;
|
|
|
|
if (refresh) {
|
|
setState(() {
|
|
_currentPage = 1;
|
|
_artikelList = [];
|
|
_isLoading = true;
|
|
});
|
|
}
|
|
|
|
if (_useDummyDataOnly) {
|
|
// Langsung gunakan data dummy jika flag aktif
|
|
setState(() {
|
|
_artikelList = _getDummyArtikels();
|
|
_isLoading = false;
|
|
_hasMore = false;
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isLoadingMore = true;
|
|
if (_artikelList.isEmpty) {
|
|
_isLoading = true;
|
|
}
|
|
});
|
|
|
|
try {
|
|
print('Memulai request artikel dari API untuk halaman $_currentPage');
|
|
final result = await _artikelService.getArtikels(
|
|
page: _currentPage,
|
|
perPage: 10,
|
|
).timeout(Duration(seconds: 5), onTimeout: () {
|
|
throw TimeoutException('Timeout saat mengambil data artikel');
|
|
});
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() {
|
|
if (refresh || _currentPage == 1) {
|
|
_artikelList = result.data;
|
|
} else {
|
|
_artikelList.addAll(result.data);
|
|
}
|
|
|
|
_currentPage = result.currentPage + 1;
|
|
_totalPages = result.lastPage;
|
|
_hasMore = result.currentPage < result.lastPage;
|
|
_isLoading = false;
|
|
_isLoadingMore = false;
|
|
_error = null;
|
|
});
|
|
|
|
print('Berhasil mendapatkan ${result.data.length} artikel. Halaman: ${result.currentPage}/$_totalPages');
|
|
} catch (e) {
|
|
print('Error saat memuat artikel: $e');
|
|
|
|
if (!mounted) return;
|
|
|
|
// Gunakan data dummy sebagai fallback jika API gagal
|
|
setState(() {
|
|
if (_artikelList.isEmpty) {
|
|
// Hanya gunakan dummy jika belum ada data sama sekali
|
|
_artikelList = _getDummyArtikels();
|
|
}
|
|
_isLoading = false;
|
|
_isLoadingMore = false;
|
|
_error = 'Gagal memuat data dari server. Menampilkan data lokal.';
|
|
});
|
|
}
|
|
}
|
|
|
|
List<ArtikelModel> _getDummyArtikels() {
|
|
// Konversi data dummy yang sudah ada ke ArtikelModel
|
|
print('Menggunakan data dummy pada ArtikelScreen');
|
|
return artikelList.map((artikel) => ArtikelModel(
|
|
id: int.parse(artikel['views'] ?? '1'),
|
|
judul: artikel['title'] ?? '',
|
|
isiArtikel: artikel['description'] ?? '',
|
|
tanggal: DateTime.now().subtract(Duration(days: int.parse(artikel['views'] ?? '1'))),
|
|
gambarUrl: artikel['imageUrl'],
|
|
)).toList();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
List<ArtikelModel> _filteredArticles() {
|
|
if (_selectedCategory == 'Semua') {
|
|
return _artikelList;
|
|
} else {
|
|
return _artikelList.where((artikel) {
|
|
// Tentukan kategori berdasarkan judul artikel
|
|
String kategori = _detectArticleCategory(artikel.judul);
|
|
return kategori == _selectedCategory;
|
|
}).toList();
|
|
}
|
|
}
|
|
|
|
// Fungsi untuk mendeteksi kategori berdasarkan judul artikel
|
|
String _detectArticleCategory(String judul) {
|
|
judul = judul.toLowerCase();
|
|
|
|
// Cek kata kunci imunisasi terlebih dahulu
|
|
if (judul.contains('imunisasi') || judul.contains('vaksin') ||
|
|
judul.contains('vaksinasi') || judul.contains('suntik') ||
|
|
judul.contains('kekebalan') || judul.contains('campak') ||
|
|
judul.contains('polio') || judul.contains('bcg') || judul.contains('dpt') ||
|
|
judul.contains('booster') || judul.contains('pengebalan') ||
|
|
judul.contains('mmr') || judul.contains('dpt-hb-hib') || judul.contains('hib')) {
|
|
return 'Imunisasi';
|
|
}
|
|
// Cek kata kunci stunting
|
|
else if (judul.contains('stunting') || judul.contains('pendek') ||
|
|
judul.contains('mencegah stunting') || judul.contains('perawakan pendek') ||
|
|
judul.contains('kerdil')) {
|
|
return 'Stunting';
|
|
}
|
|
// Cek kata kunci vitamin
|
|
else if (judul.contains('vitamin') || judul.contains('suplemen') ||
|
|
judul.contains('vit a') || judul.contains('vitamin a') ||
|
|
judul.contains('vitamin d') || judul.contains('multivitamin') ||
|
|
judul.contains('mineral')) {
|
|
return 'Vitamin';
|
|
}
|
|
// Cek kata kunci gizi
|
|
else if (judul.contains('gizi') || judul.contains('nutrisi') || judul.contains('makanan') ||
|
|
judul.contains('makan') || judul.contains('asi') || judul.contains('mpasi') ||
|
|
judul.contains('bergizi') || judul.contains('menu') || judul.contains('porsi')) {
|
|
return 'Gizi';
|
|
}
|
|
// Cek kata kunci tumbuh kembang
|
|
else if (judul.contains('tumbuh') || judul.contains('kembang') ||
|
|
judul.contains('perkembangan') || judul.contains('usia') ||
|
|
judul.contains('tahapan') || judul.contains('motorik') ||
|
|
judul.contains('balita') || judul.contains('bayi') ||
|
|
judul.contains('milestones') || judul.contains('kemampuan') ||
|
|
judul.contains('pertumbuhan')) {
|
|
return 'Tumbuh Kembang';
|
|
}
|
|
// Cek kata kunci kesehatan
|
|
else if (judul.contains('sehat') || judul.contains('penyakit') ||
|
|
judul.contains('obat') || judul.contains('virus') || judul.contains('bakteri') ||
|
|
judul.contains('demam') || judul.contains('flu') || judul.contains('batuk') ||
|
|
judul.contains('diare') || judul.contains('kesehatan') || judul.contains('infeksi') ||
|
|
judul.contains('kebersihan') || judul.contains('rumah sakit') || judul.contains('dokter') ||
|
|
judul.contains('perawatan') || judul.contains('puskesmas') || judul.contains('klinik')) {
|
|
return 'Kesehatan';
|
|
}
|
|
// Default kategori
|
|
else {
|
|
return 'Kesehatan';
|
|
}
|
|
}
|
|
|
|
// Fungsi untuk navigasi ke detail artikel
|
|
void _navigateToArticleDetail(ArtikelModel artikel) {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => ArtikelDetailScreen(
|
|
artikelId: artikel.id,
|
|
initialData: artikel, // Pass artikel sebagai initialData
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Fungsi untuk mendapatkan info kategori dari artikel
|
|
Map<String, dynamic> getArticleCategory(ArtikelModel article) {
|
|
// Deteksi kategori berdasarkan judul
|
|
final category = _detectArticleCategory(article.judul);
|
|
|
|
return {
|
|
'name': category,
|
|
'color': getCategoryColor(category),
|
|
'icon': getCategoryIcon(category),
|
|
};
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final screenSize = MediaQuery.of(context).size;
|
|
final filteredArticles = _filteredArticles();
|
|
|
|
return Scaffold(
|
|
body: RefreshIndicator(
|
|
onRefresh: () {
|
|
// Jika menggunakan dummy, cukup refresh state
|
|
if (_useDummyDataOnly) {
|
|
setState(() {
|
|
_artikelList = _getDummyArtikels();
|
|
});
|
|
return Future.value();
|
|
}
|
|
return _loadArtikels(refresh: true);
|
|
},
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
// Custom App Bar
|
|
SliverAppBar(
|
|
expandedHeight: screenSize.height * 0.25,
|
|
pinned: true,
|
|
backgroundColor: Colors.teal.shade700,
|
|
flexibleSpace: FlexibleSpaceBar(
|
|
title: Text(
|
|
'Artikel Kesehatan',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 5,
|
|
offset: Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
background: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
Image.network(
|
|
'https://images.unsplash.com/photo-1505751172876-fa1923c5c528?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1470&q=80',
|
|
fit: BoxFit.cover,
|
|
),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.black.withOpacity(0.7),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Categories
|
|
SliverToBoxAdapter(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(top: 16, bottom: 8),
|
|
child: SizedBox(
|
|
height: 40,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: _categories.length,
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
itemBuilder: (context, index) {
|
|
final category = _categories[index];
|
|
final isSelected = category == _selectedCategory;
|
|
return Padding(
|
|
padding: EdgeInsets.only(right: 8),
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
setState(() {
|
|
_selectedCategory = category;
|
|
});
|
|
},
|
|
child: AnimatedContainer(
|
|
duration: Duration(milliseconds: 300),
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? Colors.teal.shade700 : Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
category,
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : Colors.grey.shade800,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
if (_isLoading && _artikelList.isEmpty)
|
|
SliverFillRemaining(
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
color: Colors.teal.shade700,
|
|
),
|
|
),
|
|
)
|
|
else if (_error != null && _artikelList.isEmpty)
|
|
SliverFillRemaining(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.error_outline,
|
|
color: Colors.red,
|
|
size: 48,
|
|
),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Gagal memuat artikel',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(_error!),
|
|
SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: () => _loadArtikels(refresh: true),
|
|
child: Text('Coba Lagi'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.teal.shade700,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
else ...[
|
|
// Featured Article if available
|
|
if (filteredArticles.isNotEmpty) SliverToBoxAdapter(
|
|
child: FadeTransition(
|
|
opacity: _fadeAnimation,
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Artikel Unggulan',
|
|
style: TextStyle(
|
|
fontSize: screenSize.width * 0.05,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
SizedBox(height: 16),
|
|
GestureDetector(
|
|
onTap: () => _navigateToArticleDetail(filteredArticles.first),
|
|
child: _buildFeaturedArticleCard(filteredArticles.first),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Article List
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: EdgeInsets.fromLTRB(16, 8, 16, 0),
|
|
child: Text(
|
|
filteredArticles.length > 1 ? 'Artikel Lainnya' : 'Artikel',
|
|
style: TextStyle(
|
|
fontSize: screenSize.width * 0.05,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
if (filteredArticles.length <= 1)
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Text(
|
|
'Tidak ada artikel lainnya dalam kategori ini',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
else
|
|
SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) {
|
|
// Skip the first item since it's featured
|
|
if (index == 0) return SizedBox.shrink();
|
|
|
|
final articleIndex = index - 1;
|
|
if (articleIndex >= filteredArticles.length - 1) {
|
|
// Load more if we're at the end and there are more articles
|
|
if (_hasMore && !_isLoading) {
|
|
_loadArtikels();
|
|
}
|
|
|
|
if (articleIndex < filteredArticles.length) {
|
|
return Padding(
|
|
padding: EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
child: ArticleListItem(
|
|
article: filteredArticles[articleIndex],
|
|
index: articleIndex,
|
|
animation: _animationController,
|
|
onTap: () => _navigateToArticleDetail(filteredArticles[articleIndex]),
|
|
detectCategory: _detectArticleCategory,
|
|
),
|
|
);
|
|
} else {
|
|
// Show loading indicator at the end of the list
|
|
return Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Center(
|
|
child: _hasMore
|
|
? Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircularProgressIndicator(color: Colors.teal.shade700),
|
|
SizedBox(height: 8),
|
|
Text('Memuat artikel lainnya...'),
|
|
],
|
|
)
|
|
: Text(_error != null ? _error! : 'Tidak ada artikel lagi'),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
child: ArticleListItem(
|
|
article: filteredArticles[articleIndex],
|
|
index: articleIndex,
|
|
animation: _animationController,
|
|
onTap: () => _navigateToArticleDetail(filteredArticles[articleIndex]),
|
|
detectCategory: _detectArticleCategory,
|
|
),
|
|
);
|
|
},
|
|
childCount: filteredArticles.length > 1 ? filteredArticles.length + 1 : 1,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFeaturedArticleCard(ArtikelModel article) {
|
|
// Deteksi kategori dari judul artikel
|
|
final categoryInfo = getArticleCategory(article);
|
|
final category = categoryInfo['name'];
|
|
final categoryColor = categoryInfo['color'];
|
|
final categoryIcon = categoryInfo['icon'];
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Article image
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(16),
|
|
topRight: Radius.circular(16),
|
|
),
|
|
child: Container(
|
|
width: double.infinity,
|
|
height: 200,
|
|
color: categoryColor.withOpacity(0.2),
|
|
child: article.gambarUrl != null
|
|
? Image.network(
|
|
article.gambarUrl!,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Center(
|
|
child: Icon(
|
|
categoryIcon,
|
|
size: 64,
|
|
color: categoryColor,
|
|
),
|
|
);
|
|
},
|
|
)
|
|
: Center(
|
|
child: Icon(
|
|
categoryIcon,
|
|
size: 64,
|
|
color: categoryColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Article details
|
|
Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Category and date
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: categoryColor,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
category,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
Spacer(),
|
|
Text(
|
|
_formatDate(article.tanggal),
|
|
style: TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 12),
|
|
// Title
|
|
Text(
|
|
article.judul,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 8),
|
|
// Description
|
|
Text(
|
|
article.isiArtikel,
|
|
style: TextStyle(
|
|
color: Colors.grey.shade700,
|
|
fontSize: 14,
|
|
),
|
|
maxLines: 3,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 16),
|
|
// Read more button
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: () => _navigateToArticleDetail(article),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.teal.shade700,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text('Baca Selengkapnya'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatDate(DateTime date) {
|
|
final months = [
|
|
'', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
|
|
'Jul', 'Ags', 'Sep', 'Okt', 'Nov', 'Des'
|
|
];
|
|
return '${date.day} ${months[date.month]} ${date.year}';
|
|
}
|
|
}
|
|
|
|
class ArticleListItem extends StatelessWidget {
|
|
final ArtikelModel article;
|
|
final int index;
|
|
final Animation<double> animation;
|
|
final VoidCallback onTap;
|
|
final Function(String) detectCategory;
|
|
|
|
const ArticleListItem({
|
|
Key? key,
|
|
required this.article,
|
|
required this.index,
|
|
required this.animation,
|
|
required this.onTap,
|
|
required this.detectCategory,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final delay = 0.2 + (index * 0.1);
|
|
final slideAnimation = Tween<Offset>(
|
|
begin: Offset(0, 0.2),
|
|
end: Offset.zero,
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: animation,
|
|
curve: Interval(delay, delay + 0.2, curve: Curves.easeOut),
|
|
),
|
|
);
|
|
|
|
// Deteksi kategori artikel berdasarkan judul
|
|
final category = detectCategory(article.judul);
|
|
final categoryColor = getCategoryColor(category);
|
|
final categoryIcon = getCategoryIcon(category);
|
|
|
|
return AnimatedBuilder(
|
|
animation: animation,
|
|
builder: (context, child) {
|
|
return Transform.translate(
|
|
offset: slideAnimation.value,
|
|
child: Opacity(
|
|
opacity: animation.value,
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
child: GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 5,
|
|
offset: Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Article image
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(12),
|
|
bottomLeft: Radius.circular(12),
|
|
),
|
|
child: Container(
|
|
width: 100,
|
|
height: 100,
|
|
color: categoryColor.withOpacity(0.2),
|
|
child: article.gambarUrl != null
|
|
? Image.network(
|
|
article.gambarUrl!,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Center(
|
|
child: Icon(
|
|
categoryIcon,
|
|
size: 40,
|
|
color: categoryColor,
|
|
),
|
|
);
|
|
},
|
|
)
|
|
: Center(
|
|
child: Icon(
|
|
categoryIcon,
|
|
size: 40,
|
|
color: categoryColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Article details
|
|
Expanded(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Category and date
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: categoryColor,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
category,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
Spacer(),
|
|
Text(
|
|
_formatDate(article.tanggal),
|
|
style: TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 8),
|
|
// Title
|
|
Text(
|
|
article.judul,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 4),
|
|
// Description
|
|
Text(
|
|
article.isiArtikel,
|
|
style: TextStyle(
|
|
color: Colors.grey.shade600,
|
|
fontSize: 12,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatDate(DateTime date) {
|
|
final months = [
|
|
'', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
|
|
'Jul', 'Ags', 'Sep', 'Okt', 'Nov', 'Des'
|
|
];
|
|
return '${date.day} ${months[date.month]} ${date.year}';
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
IconData getCategoryIcon(String category) {
|
|
switch (category) {
|
|
case 'Gizi':
|
|
return Icons.restaurant;
|
|
case 'Imunisasi':
|
|
return Icons.vaccines;
|
|
case 'Tumbuh Kembang':
|
|
return Icons.child_care;
|
|
case 'Vitamin':
|
|
return Icons.medication;
|
|
case 'Stunting':
|
|
return Icons.height;
|
|
default:
|
|
return Icons.healing;
|
|
}
|
|
}
|
|
|
|
Color getCategoryColor(String category) {
|
|
switch (category) {
|
|
case 'Gizi':
|
|
return Colors.orange.shade700;
|
|
case 'Imunisasi':
|
|
return Colors.purple.shade700;
|
|
case 'Tumbuh Kembang':
|
|
return Colors.blue.shade700;
|
|
case 'Vitamin':
|
|
return Colors.amber.shade700;
|
|
case 'Stunting':
|
|
return Colors.red.shade700;
|
|
default:
|
|
return Colors.teal.shade700;
|
|
}
|
|
}
|
|
|
|
// Sample data untuk fallback dan kategori sementara
|
|
final List<Map<String, String>> artikelList = [
|
|
{
|
|
'title': 'Pentingnya ASI Eksklusif untuk Perkembangan Bayi',
|
|
'description': 'ASI eksklusif adalah pemberian ASI saja pada bayi sampai usia 6 bulan. Manfaatnya sangat banyak untuk tumbuh kembang dan kekebalan tubuh bayi.',
|
|
'imageUrl': 'https://img.freepik.com/free-photo/young-mother-showing-breastfeeding-her-baby_23-2149046913.jpg',
|
|
'date': '20 Feb 2025',
|
|
'views': '325',
|
|
},
|
|
{
|
|
'title': 'Mencegah Stunting Sejak Dini pada Anak',
|
|
'description': 'Stunting dapat dicegah dengan memperhatikan asupan gizi sejak masa kehamilan dan memberikan makanan bergizi seimbang pada anak.',
|
|
'imageUrl': 'https://img.freepik.com/free-photo/doctor-check-up-little-boy-office_23-2148982292.jpg',
|
|
'date': '18 Feb 2025',
|
|
'views': '198',
|
|
},
|
|
{
|
|
'title': 'Panduan Makanan Bergizi untuk Balita',
|
|
'description': 'Makanan bergizi seimbang sangat penting untuk pertumbuhan optimal balita. Pelajari menu-menu sehat yang bisa diberikan sesuai usia anak.',
|
|
'imageUrl': 'https://img.freepik.com/free-photo/close-up-young-attractive-smiling-mother-feeding-her-cute-baby-son-with-spoon-organic-healthy-baby-food-white-kitchen-with-big-window_8353-12056.jpg',
|
|
'date': '15 Feb 2025',
|
|
'views': '276',
|
|
},
|
|
{
|
|
'title': 'Jadwal Imunisasi Lengkap untuk Anak',
|
|
'description': 'Imunisasi adalah cara efektif untuk melindungi anak dari berbagai penyakit berbahaya. Ketahui jadwal imunisasi yang tepat untuk anak.',
|
|
'imageUrl': 'https://img.freepik.com/free-photo/doctor-vaccinating-little-girl_23-2148982283.jpg',
|
|
'date': '10 Feb 2025',
|
|
'views': '214',
|
|
},
|
|
{
|
|
'title': 'Tips Merawat Kesehatan Anak selama Musim Hujan',
|
|
'description': 'Musim hujan meningkatkan risiko beberapa penyakit pada anak. Ikuti tips ini untuk menjaga kesehatan anak tetap optimal.',
|
|
'imageUrl': 'https://img.freepik.com/free-photo/cute-child-with-umbrella_1149-537.jpg',
|
|
'date': '05 Feb 2025',
|
|
'views': '187',
|
|
},
|
|
{
|
|
'title': 'Manfaat Vitamin A untuk Penglihatan Anak',
|
|
'description': 'Vitamin A sangat penting untuk perkembangan penglihatan anak. Ketahui sumber-sumber makanan kaya vitamin A dan jadwal pemberian vitamin A di Posyandu.',
|
|
'imageUrl': 'https://img.freepik.com/free-photo/doctor-examining-little-boy_23-2148982280.jpg',
|
|
'date': '01 Feb 2025',
|
|
'views': '165',
|
|
},
|
|
{
|
|
'title': 'Mengenal Tahapan Perkembangan Anak di Usia 1-5 Tahun',
|
|
'description': 'Setiap anak memiliki tahapan perkembangan yang berbeda. Kenali perkembangan normal anak sesuai usianya untuk memastikan pertumbuhan yang optimal.',
|
|
'imageUrl': 'https://img.freepik.com/free-photo/mother-helping-her-daughter-draw_23-2148797344.jpg',
|
|
'date': '25 Jan 2025',
|
|
},
|
|
];
|
|
|