NIM_E31222534/Androidnya/lib/screens/dashboard/artikel_screen.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',
},
];