1650 lines
54 KiB
Dart
1650 lines
54 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:auto_size_text/auto_size_text.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/screens/calendar/calendar_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/calendar/schedule_list_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/enhanced_community_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/panen/analisis_input_screen.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:tugas_akhir_supabase/screens/image_processing/plant_scanner_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/calendar/schedule_detail_screen.dart';
|
|
import 'package:tugas_akhir_supabase/utils/app_events.dart';
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
class HomeContent extends StatefulWidget {
|
|
final String userId;
|
|
|
|
const HomeContent({super.key, required this.userId});
|
|
|
|
@override
|
|
State<HomeContent> createState() => _HomeContentState();
|
|
}
|
|
|
|
class _HomeContentState extends State<HomeContent> {
|
|
// Data for dynamic content
|
|
List<Map<String, dynamic>> _scheduleData = [];
|
|
List<Map<String, dynamic>> _analysisData = [];
|
|
bool _isLoadingSchedules = true;
|
|
bool _isLoadingAnalysis = true;
|
|
|
|
// Stream subscription untuk AppEventBus
|
|
StreamSubscription? _scheduleUpdatedSubscription;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchRecentAnalysis();
|
|
_fetchSchedules();
|
|
|
|
// Dengarkan event jadwal diperbarui
|
|
_scheduleUpdatedSubscription = AppEventBus().onScheduleUpdated.listen((
|
|
event,
|
|
) {
|
|
debugPrint('INFO: HomeContent menerima event jadwal diperbarui');
|
|
// Refresh data jadwal dan analisis
|
|
_fetchSchedules();
|
|
_fetchRecentAnalysis();
|
|
|
|
// Tampilkan snackbar jika tidak sedang dalam proses refresh
|
|
if (!_isLoadingSchedules && !_isLoadingAnalysis && mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Data jadwal berhasil diperbarui'),
|
|
duration: Duration(seconds: 2),
|
|
backgroundColor: Color(0xFF056839),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Batalkan subscription saat widget dihapus
|
|
_scheduleUpdatedSubscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _fetchRecentAnalysis() async {
|
|
setState(() => _isLoadingAnalysis = true);
|
|
|
|
try {
|
|
final user = Supabase.instance.client.auth.currentUser;
|
|
if (user == null) {
|
|
setState(() => _isLoadingAnalysis = false);
|
|
return;
|
|
}
|
|
|
|
// Tambahkan timeout untuk mencegah permintaan menggantung
|
|
final completer = Completer<List<dynamic>>();
|
|
|
|
// Set timeout untuk mencegah app hanging
|
|
Future.delayed(const Duration(seconds: 10), () {
|
|
if (!completer.isCompleted) {
|
|
completer.completeError(
|
|
TimeoutException('Koneksi timeout saat memuat aktivitas.'),
|
|
);
|
|
}
|
|
});
|
|
|
|
// Get the latest daily logs
|
|
Supabase.instance.client
|
|
.from('daily_logs')
|
|
.select('*, crop_schedules!inner(id, crop_name, field_id, user_id)')
|
|
.eq('crop_schedules.user_id', user.id)
|
|
.order('date', ascending: false)
|
|
.limit(5)
|
|
.then((value) {
|
|
if (!completer.isCompleted) completer.complete(value);
|
|
})
|
|
.catchError((error) {
|
|
if (!completer.isCompleted) completer.completeError(error);
|
|
});
|
|
|
|
final response = await completer.future;
|
|
|
|
debugPrint('Daily logs response: $response');
|
|
|
|
if (response.isNotEmpty) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_analysisData =
|
|
response.map((item) {
|
|
final cropName =
|
|
item['crop_schedules']['crop_name'] ?? 'Tanaman';
|
|
final fieldId = item['crop_schedules']['field_id'] ?? 'Lahan';
|
|
final cost = item['cost'] ?? 0;
|
|
final note = item['note'] ?? 'Aktivitas pertanian';
|
|
final date = item['date']; // Store the date for navigation
|
|
|
|
// Clean up the location display - remove UUIDs
|
|
String location = cropName;
|
|
if (fieldId != null && !fieldId.contains('-')) {
|
|
location = '$cropName - $fieldId';
|
|
}
|
|
|
|
// Determine icon based on note content
|
|
IconData icon = Icons.calendar_today;
|
|
Color iconColor = const Color(0xFF00897B);
|
|
Color iconBgColor = const Color(0xFFE0F2F1);
|
|
Color tagColor = const Color(0xFFE0F2F1);
|
|
Color tagTextColor = const Color(0xFF00897B);
|
|
|
|
if (note.toLowerCase().contains('panen')) {
|
|
icon = Icons.agriculture;
|
|
iconColor = Colors.orange[700]!;
|
|
iconBgColor = const Color(0xFFFFF3E0);
|
|
tagColor = Colors.orange[100]!;
|
|
tagTextColor = Colors.orange[800]!;
|
|
} else if (note.toLowerCase().contains('hama') ||
|
|
note.toLowerCase().contains('penyakit')) {
|
|
icon = Icons.bug_report;
|
|
iconColor = Colors.red[700]!;
|
|
iconBgColor = Colors.red[50]!;
|
|
tagColor = Colors.red[100]!;
|
|
tagTextColor = Colors.red[700]!;
|
|
} else if (note.toLowerCase().contains('pupuk')) {
|
|
icon = Icons.eco;
|
|
iconColor = Colors.green[700]!;
|
|
iconBgColor = Colors.green[50]!;
|
|
tagColor = Colors.green[100]!;
|
|
tagTextColor = Colors.green[700]!;
|
|
} else if (note.toLowerCase().contains('air') ||
|
|
note.toLowerCase().contains('irigasi')) {
|
|
icon = Icons.water_drop;
|
|
iconColor = Colors.blue[700]!;
|
|
iconBgColor = Colors.blue[50]!;
|
|
tagColor = Colors.blue[100]!;
|
|
tagTextColor = Colors.blue[700]!;
|
|
}
|
|
|
|
return {
|
|
'title': note,
|
|
'location': location,
|
|
'cost':
|
|
'Biaya: Rp ${NumberFormat('#,###', 'id_ID').format(cost)}',
|
|
'tag': cropName,
|
|
'tagColor': tagColor,
|
|
'tagTextColor': tagTextColor,
|
|
'icon': icon,
|
|
'iconBgColor': iconBgColor,
|
|
'iconColor': iconColor,
|
|
'crop_schedules':
|
|
item['crop_schedules'], // Store the entire crop_schedules object
|
|
'date': date, // Store the date for navigation
|
|
};
|
|
}).toList();
|
|
_isLoadingAnalysis = false;
|
|
});
|
|
}
|
|
} else {
|
|
if (mounted) {
|
|
setState(() => _isLoadingAnalysis = false);
|
|
}
|
|
}
|
|
} on TimeoutException catch (e) {
|
|
debugPrint('Timeout fetching analysis data: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoadingAnalysis = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Gagal memuat aktivitas: Koneksi timeout'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error fetching analysis data: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoadingAnalysis = false);
|
|
// Tampilkan pesan error yang lebih informatif
|
|
String errorMessage = 'Terjadi kesalahan';
|
|
if (e.toString().contains('not found') ||
|
|
e.toString().contains('not exist')) {
|
|
errorMessage = 'Data tidak ditemukan';
|
|
} else if (e.toString().contains('permission') ||
|
|
e.toString().contains('access')) {
|
|
errorMessage = 'Tidak memiliki akses';
|
|
} else if (e.toString().contains('network') ||
|
|
e.toString().contains('connection')) {
|
|
errorMessage = 'Masalah koneksi jaringan';
|
|
}
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Gagal memuat aktivitas: $errorMessage'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _fetchSchedules() async {
|
|
setState(() => _isLoadingSchedules = true);
|
|
|
|
try {
|
|
final user = Supabase.instance.client.auth.currentUser;
|
|
if (user == null) {
|
|
setState(() => _isLoadingSchedules = false);
|
|
return;
|
|
}
|
|
|
|
// Tambahkan timeout untuk mencegah permintaan menggantung
|
|
final completer = Completer<List<dynamic>>();
|
|
|
|
// Set timeout untuk mencegah app hanging
|
|
Future.delayed(const Duration(seconds: 10), () {
|
|
if (!completer.isCompleted) {
|
|
completer.completeError(
|
|
TimeoutException('Koneksi timeout saat memuat jadwal.'),
|
|
);
|
|
}
|
|
});
|
|
|
|
// Get active schedules
|
|
final now = DateTime.now();
|
|
Supabase.instance.client
|
|
.from('crop_schedules')
|
|
.select('id, crop_name, field_id, start_date, end_date')
|
|
.eq('user_id', user.id)
|
|
.or('end_date.gte.${now.toIso8601String()}')
|
|
.order('start_date', ascending: true)
|
|
.limit(3)
|
|
.then((value) {
|
|
if (!completer.isCompleted) completer.complete(value);
|
|
})
|
|
.catchError((error) {
|
|
if (!completer.isCompleted) completer.completeError(error);
|
|
});
|
|
|
|
final response = await completer.future;
|
|
|
|
debugPrint('Schedules response: $response');
|
|
|
|
if (response.isNotEmpty) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_scheduleData =
|
|
response.map((item) {
|
|
final cropName = item['crop_name'] ?? 'Tanaman';
|
|
final fieldId = item['field_id'] ?? 'Lahan';
|
|
final startDate =
|
|
DateTime.tryParse(item['start_date']) ?? DateTime.now();
|
|
final endDate =
|
|
DateTime.tryParse(item['end_date']) ??
|
|
DateTime.now().add(const Duration(days: 90));
|
|
|
|
// Format dates
|
|
final startFormatted = DateFormat(
|
|
'dd/MM',
|
|
'id_ID',
|
|
).format(startDate);
|
|
final endFormatted = DateFormat(
|
|
'dd/MM',
|
|
'id_ID',
|
|
).format(endDate);
|
|
|
|
// Determine status and colors
|
|
String status = 'Belum mulai';
|
|
Color statusColor = Colors.orange[100]!;
|
|
Color statusTextColor = Colors.orange[700]!;
|
|
IconData icon = Icons.eco;
|
|
Color iconColor = Colors.green[700]!;
|
|
|
|
if (now.isAfter(startDate)) {
|
|
if (now.isBefore(endDate)) {
|
|
status = 'Berlangsung';
|
|
statusColor = Colors.green[100]!;
|
|
statusTextColor = Colors.green[700]!;
|
|
} else {
|
|
status = 'Selesai';
|
|
statusColor = Colors.grey[300]!;
|
|
statusTextColor = Colors.grey[700]!;
|
|
}
|
|
}
|
|
|
|
// Set icon based on crop name
|
|
if (cropName.toLowerCase().contains('cabai') ||
|
|
cropName.toLowerCase().contains('cabe')) {
|
|
icon = Icons.local_fire_department;
|
|
iconColor = Colors.red[700]!;
|
|
} else if (cropName.toLowerCase().contains('padi')) {
|
|
icon = Icons.grass;
|
|
iconColor = Colors.green[700]!;
|
|
} else if (cropName.toLowerCase().contains('jagung')) {
|
|
icon = Icons.grass;
|
|
iconColor = Colors.amber[700]!;
|
|
}
|
|
|
|
return {
|
|
'id': item['id'],
|
|
'cropName': cropName,
|
|
'location': fieldId,
|
|
'period': '$startFormatted - $endFormatted',
|
|
'status': status,
|
|
'statusColor': statusColor,
|
|
'statusTextColor': statusTextColor,
|
|
'icon': icon,
|
|
'iconColor': iconColor,
|
|
'start_date': item['start_date'],
|
|
'end_date': item['end_date'],
|
|
};
|
|
}).toList();
|
|
_isLoadingSchedules = false;
|
|
});
|
|
}
|
|
} else {
|
|
if (mounted) {
|
|
setState(() => _isLoadingSchedules = false);
|
|
}
|
|
}
|
|
} on TimeoutException catch (e) {
|
|
debugPrint('Timeout fetching schedules: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoadingSchedules = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Gagal memuat jadwal: Koneksi timeout'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error fetching schedules: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoadingSchedules = false);
|
|
// Tampilkan pesan error yang lebih informatif
|
|
String errorMessage = 'Terjadi kesalahan';
|
|
if (e.toString().contains('not found') ||
|
|
e.toString().contains('not exist')) {
|
|
errorMessage = 'Data tidak ditemukan';
|
|
} else if (e.toString().contains('permission') ||
|
|
e.toString().contains('access')) {
|
|
errorMessage = 'Tidak memiliki akses';
|
|
} else if (e.toString().contains('network') ||
|
|
e.toString().contains('connection')) {
|
|
errorMessage = 'Masalah koneksi jaringan';
|
|
}
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Gagal memuat jadwal: $errorMessage'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return RefreshIndicator(
|
|
onRefresh: () async {
|
|
await _fetchSchedules();
|
|
await _fetchRecentAnalysis();
|
|
},
|
|
color: const Color(0xFF056839),
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: SafeArea(
|
|
bottom:
|
|
true, // Pastikan konten tidak tertutup oleh notch atau navigation bar
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildWelcomeCard(),
|
|
const SizedBox(height: 16),
|
|
_buildQuickActions(),
|
|
const SizedBox(height: 20),
|
|
_buildTipsSection(),
|
|
const SizedBox(height: 20),
|
|
_buildMainServicesSection(),
|
|
const SizedBox(height: 20),
|
|
_buildAnalysisSection(),
|
|
const SizedBox(height: 20),
|
|
_buildScheduleSection(),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWelcomeCard() {
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final isSmallScreen = screenWidth < 360;
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
|
decoration: const BoxDecoration(color: Color(0xFF056839)),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
'TaniSMART',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Tingkatkan Produktivitas',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: isSmallScreen ? 18 : 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
'Pertanian Anda',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: isSmallScreen ? 18 : 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'Solusi Pertanian Cerdas!',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: isSmallScreen ? 12 : 14,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 38,
|
|
height: 38,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFFFB74D),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.wb_sunny,
|
|
color: Colors.white,
|
|
size: 20,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickActions() {
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final isSmallScreen = screenWidth < 360;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
_buildActionItem(
|
|
Icons.eco,
|
|
'Tanaman',
|
|
const Color(0xFF4CAF50),
|
|
isSmallScreen,
|
|
),
|
|
_buildActionItem(
|
|
Icons.water_drop,
|
|
'Irigasi',
|
|
const Color(0xFF2196F3),
|
|
isSmallScreen,
|
|
),
|
|
_buildActionItem(
|
|
Icons.bug_report,
|
|
'Hama',
|
|
const Color(0xFFFF5252),
|
|
isSmallScreen,
|
|
),
|
|
_buildActionItem(
|
|
Icons.eco,
|
|
'Pupuk',
|
|
const Color(0xFF4CAF50),
|
|
isSmallScreen,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionItem(
|
|
IconData icon,
|
|
String label,
|
|
Color color,
|
|
bool isSmallScreen,
|
|
) {
|
|
final iconSize = isSmallScreen ? 48.0 : 60.0;
|
|
final fontSize = isSmallScreen ? 11.0 : 13.0;
|
|
|
|
return Expanded(
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
height: iconSize,
|
|
width: iconSize,
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: color, size: iconSize * 0.45),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
label,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: fontSize,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTipsSection() {
|
|
// Menggunakan MediaQuery untuk mendapatkan ukuran layar
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final isSmallScreen = screenWidth < 360; // Deteksi layar kecil
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Text(
|
|
'Tips & Trik Bertani',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
// Tinggi yang adaptif berdasarkan ukuran layar
|
|
height: isSmallScreen ? 170 : 155,
|
|
child: ListView(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
children: [
|
|
_buildTipCard(
|
|
'Waktu Tanam Optimal',
|
|
'Menanam padi di awal musim hujan meningkatkan hasil panen hingga 30%',
|
|
const Color(0xFFF9A825),
|
|
Icons.wb_sunny,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_buildTipCard(
|
|
'Rotasi Tanaman',
|
|
'Bergantian menanam padi dan kedelai menjaga kesehatan tanah & nutrisi',
|
|
const Color(0xFF4CAF50),
|
|
Icons.sync,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_buildTipCard(
|
|
'Penggunaan Pupuk',
|
|
'Gunakan pupuk organik untuk menjaga kualitas tanah jangka panjang',
|
|
const Color(0xFF7CB342),
|
|
Icons.eco,
|
|
),
|
|
const SizedBox(width: 12),
|
|
_buildTipCard(
|
|
'Pengendalian Hama',
|
|
'Tanam tanaman pendamping seperti kemangi untuk mengusir hama alami',
|
|
const Color.fromARGB(255, 193, 87, 0),
|
|
Icons.bug_report,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTipCard(
|
|
String title,
|
|
String description,
|
|
Color color,
|
|
IconData icon,
|
|
) {
|
|
return AnimatedTipCard(
|
|
title: title,
|
|
description: description,
|
|
color: color,
|
|
icon: icon,
|
|
);
|
|
}
|
|
|
|
Widget _buildMainServicesSection() {
|
|
// Deteksi ukuran layar untuk responsivitas
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final isSmallScreen = screenWidth < 360;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Text(
|
|
'Layanan Utama',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: GridView.count(
|
|
crossAxisCount: 2,
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
mainAxisSpacing: isSmallScreen ? 12 : 16,
|
|
crossAxisSpacing: isSmallScreen ? 12 : 16,
|
|
childAspectRatio:
|
|
isSmallScreen
|
|
? 0.95
|
|
: 1.0, // Sedikit lebih tinggi pada layar kecil
|
|
children: [
|
|
_buildServiceCardCompact(
|
|
'Scan Penyakit',
|
|
'assets/icons/scanner_icon.png',
|
|
const Color(0xFFFFF2F2),
|
|
const Color(0xFFFF9494),
|
|
Icons.document_scanner_rounded,
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const PlantScannerScreen(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
_buildServiceCardCompact(
|
|
'Analisis Panen',
|
|
'assets/icons/analysis_icon.png',
|
|
const Color(0xFFEBFFFD),
|
|
const Color(0xFF71DECE),
|
|
Icons.insights_rounded,
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder:
|
|
(_) => AnalisisInputScreen(
|
|
userId: widget.userId,
|
|
scheduleData: null,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
_buildServiceCardCompact(
|
|
'Kalender Tanam',
|
|
'assets/icons/calendar_icon.png',
|
|
const Color(0xFFF2F8FF),
|
|
const Color(0xFF5CA0FF),
|
|
Icons.calendar_today_rounded,
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const KalenderTanamScreen(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
_buildServiceCardCompact(
|
|
'Komunitas',
|
|
'assets/icons/community_icon.png',
|
|
const Color(0xFFFFF8E8),
|
|
const Color(0xFFFFBD59),
|
|
Icons.forum_rounded,
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const EnhancedCommunityScreen(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildServiceCardCompact(
|
|
String title,
|
|
String iconAssetPath,
|
|
Color bgColor,
|
|
Color iconColor,
|
|
IconData fallbackIcon,
|
|
VoidCallback onTap,
|
|
) {
|
|
return AnimatedServiceCard(
|
|
bgColor: bgColor,
|
|
iconColor: iconColor,
|
|
fallbackIcon: fallbackIcon,
|
|
title: title,
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
|
|
Widget _buildAnalysisSection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Analisis Terbaru',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_isLoadingAnalysis
|
|
? const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20.0),
|
|
child: CircularProgressIndicator(color: Color(0xFF056839)),
|
|
),
|
|
)
|
|
: _analysisData.isEmpty
|
|
? _buildEmptyState(
|
|
'Belum ada aktivitas',
|
|
'Catat aktivitas pertanian Anda untuk melihatnya di sini',
|
|
Icons.calendar_today,
|
|
)
|
|
: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Column(children: _buildAnalysisItems()),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildAnalysisItems() {
|
|
try {
|
|
// Hanya tampilkan 3 item teratas
|
|
final itemsToShow = _analysisData.take(3).toList();
|
|
|
|
return itemsToShow.map((item) {
|
|
// Pastikan data valid dengan nilai default
|
|
final title = item['title'] ?? 'Aktivitas';
|
|
final location = item['location'] ?? 'Lokasi tidak tersedia';
|
|
final cost = item['cost'] ?? 'Biaya: Rp 0';
|
|
final tag = item['tag'] ?? 'Tag';
|
|
|
|
// Periksa apakah crop_schedules ada dan valid
|
|
final hasValidSchedule =
|
|
item['crop_schedules'] != null &&
|
|
item['crop_schedules'] is Map &&
|
|
item['crop_schedules']['id'] != null;
|
|
|
|
// Buat handler untuk navigasi yang aman
|
|
VoidCallback? onTapHandler;
|
|
if (hasValidSchedule) {
|
|
final scheduleId = item['crop_schedules']['id'];
|
|
onTapHandler = () {
|
|
try {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => ScheduleDetailScreen(scheduleId: scheduleId),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
debugPrint('Error navigating to schedule detail: $e');
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Gagal membuka detail jadwal'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
return _buildCompactAnalysisItem(
|
|
title,
|
|
location,
|
|
cost,
|
|
tag,
|
|
onTapHandler,
|
|
);
|
|
}).toList();
|
|
} catch (e) {
|
|
debugPrint('Error building analysis items: $e');
|
|
// Tampilkan item dummy jika terjadi error
|
|
return [
|
|
_buildCompactAnalysisItem(
|
|
'panen',
|
|
'Cabai - lahan cabaii',
|
|
'Biaya: Rp 100.000',
|
|
'Cabai',
|
|
null,
|
|
),
|
|
];
|
|
}
|
|
}
|
|
|
|
Widget _buildCompactAnalysisItem(
|
|
String title,
|
|
String location,
|
|
String cost,
|
|
String tag,
|
|
VoidCallback? onTap,
|
|
) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 70,
|
|
height: 70,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE0F2F1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Center(
|
|
child: Icon(
|
|
Icons.calendar_today,
|
|
color: Color(0xFF009688),
|
|
size: 18,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
Text(
|
|
location,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
cost,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black87,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFFF3E0),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
tag,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFFF57C00),
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildScheduleSection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Jadwal Anda',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
TextButton.icon(
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const ScheduleListScreen(),
|
|
),
|
|
);
|
|
},
|
|
icon: Text(
|
|
'Lihat Semua',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF056839),
|
|
),
|
|
),
|
|
label: const Icon(
|
|
Icons.arrow_forward,
|
|
color: Color(0xFF056839),
|
|
size: 14,
|
|
),
|
|
style: TextButton.styleFrom(
|
|
padding: EdgeInsets.zero,
|
|
minimumSize: Size.zero,
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_isLoadingSchedules
|
|
? const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20.0),
|
|
child: CircularProgressIndicator(color: Color(0xFF056839)),
|
|
),
|
|
)
|
|
: _scheduleData.isEmpty
|
|
? _buildEmptyState(
|
|
'Belum ada jadwal tanam',
|
|
'Tambahkan jadwal tanam untuk melihatnya di sini',
|
|
Icons.calendar_today,
|
|
)
|
|
: _buildScheduleHorizontalList(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildScheduleHorizontalList() {
|
|
return SizedBox(
|
|
height: 142,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
itemCount: _scheduleData.isEmpty ? 3 : _scheduleData.length,
|
|
itemBuilder: (context, index) {
|
|
// Jika tidak ada data, gunakan dummy data
|
|
if (_scheduleData.isEmpty) {
|
|
return _buildCompactScheduleCard(index);
|
|
}
|
|
|
|
// Jika ada data, gunakan data asli
|
|
final schedule = _scheduleData[index];
|
|
final scheduleId = schedule['id'];
|
|
|
|
return _buildCompactScheduleCard(
|
|
index,
|
|
scheduleId: scheduleId,
|
|
onTap: () {
|
|
try {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder:
|
|
(_) => ScheduleDetailScreen(scheduleId: scheduleId),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
debugPrint('Error navigating to schedule detail: $e');
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Gagal membuka detail jadwal'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCompactScheduleCard(
|
|
int index, {
|
|
String? scheduleId,
|
|
VoidCallback? onTap,
|
|
}) {
|
|
// Data dummy hanya digunakan jika tidak ada data asli
|
|
final dummyItems = [
|
|
{
|
|
'crop': 'Jagung',
|
|
'period': '03/05 - 08/09',
|
|
'status': '0%',
|
|
'statusColor': Colors.amber[100]!,
|
|
'statusTextColor': Colors.amber[800]!,
|
|
},
|
|
{
|
|
'crop': 'Cabai',
|
|
'period': '10/06 - 08/09',
|
|
'status': 'Berlangsung',
|
|
'statusColor': Colors.green[100]!,
|
|
'statusTextColor': Colors.green[800]!,
|
|
},
|
|
{
|
|
'crop': 'Cabai',
|
|
'period': '19/06 - 17/09',
|
|
'status': '0%',
|
|
'statusColor': Colors.grey[200]!,
|
|
'statusTextColor': Colors.grey[700]!,
|
|
},
|
|
];
|
|
|
|
// Variabel untuk menyimpan data yang akan ditampilkan
|
|
String cropName = 'Tanaman';
|
|
String period = '';
|
|
String status = 'Belum mulai';
|
|
Color statusColor = Colors.grey[200]!;
|
|
Color statusTextColor = Colors.grey[700]!;
|
|
|
|
// Gunakan data asli jika tersedia
|
|
if (_scheduleData.isNotEmpty && index < _scheduleData.length) {
|
|
final schedule = _scheduleData[index];
|
|
|
|
// Ambil nama tanaman
|
|
cropName = schedule['crop_name'] ?? schedule['cropName'] ?? 'Tanaman';
|
|
|
|
// Format tanggal dari data asli
|
|
final startDate =
|
|
DateTime.tryParse(schedule['start_date']) ?? DateTime.now();
|
|
final endDate =
|
|
DateTime.tryParse(schedule['end_date']) ??
|
|
DateTime.now().add(const Duration(days: 90));
|
|
final startFormatted = DateFormat('dd/MM', 'id_ID').format(startDate);
|
|
final endFormatted = DateFormat('dd/MM', 'id_ID').format(endDate);
|
|
period = '$startFormatted - $endFormatted';
|
|
|
|
// Tentukan status berdasarkan tanggal
|
|
final now = DateTime.now();
|
|
if (now.isAfter(startDate)) {
|
|
if (now.isBefore(endDate)) {
|
|
status = 'Berlangsung';
|
|
statusColor = Colors.green[100]!;
|
|
statusTextColor = Colors.green[700]!;
|
|
} else {
|
|
status = 'Selesai';
|
|
statusColor = Colors.grey[300]!;
|
|
statusTextColor = Colors.grey[700]!;
|
|
}
|
|
} else {
|
|
status = 'Belum mulai';
|
|
statusColor = Colors.orange[100]!;
|
|
statusTextColor = Colors.orange[700]!;
|
|
}
|
|
} else {
|
|
// Gunakan data dummy jika tidak ada data asli
|
|
final dummyItem = dummyItems[index % dummyItems.length];
|
|
cropName = dummyItem['crop'] as String;
|
|
period = dummyItem['period'] as String;
|
|
status = dummyItem['status'] as String;
|
|
statusColor = dummyItem['statusColor'] as Color;
|
|
statusTextColor = dummyItem['statusTextColor'] as Color;
|
|
}
|
|
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 170,
|
|
margin: const EdgeInsets.only(right: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 30,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color:
|
|
cropName.toLowerCase().contains('cabai')
|
|
? Colors.red.shade50
|
|
: Colors.amber.shade50,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Center(
|
|
child: Icon(
|
|
cropName.toLowerCase().contains('cabai')
|
|
? Icons.local_fire_department
|
|
: Icons.grass,
|
|
color:
|
|
cropName.toLowerCase().contains('cabai')
|
|
? Colors.red
|
|
: Colors.amber,
|
|
size: 10,
|
|
),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: statusColor,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
status,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w500,
|
|
color: statusTextColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
cropName,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
const Divider(thickness: 1, height: 6),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.calendar_today,
|
|
size: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
period,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black87,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState(String title, String subtitle, IconData icon) {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[50],
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey[200]!),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 40, color: Colors.grey[400]),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
title,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[700],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
subtitle,
|
|
style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey[600]),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AnimatedServiceCard extends StatefulWidget {
|
|
final Color bgColor;
|
|
final Color iconColor;
|
|
final IconData fallbackIcon;
|
|
final String title;
|
|
final VoidCallback onTap;
|
|
|
|
const AnimatedServiceCard({
|
|
super.key,
|
|
required this.bgColor,
|
|
required this.iconColor,
|
|
required this.fallbackIcon,
|
|
required this.title,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
State<AnimatedServiceCard> createState() => _AnimatedServiceCardState();
|
|
}
|
|
|
|
class _AnimatedServiceCardState extends State<AnimatedServiceCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
bool _isPressed = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(seconds: 3),
|
|
vsync: this,
|
|
)..repeat(reverse: true);
|
|
|
|
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Deteksi ukuran layar untuk responsivitas
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final isSmallScreen = screenWidth < 360;
|
|
|
|
return InkWell(
|
|
onTap: widget.onTap,
|
|
onHighlightChanged: (pressed) {
|
|
setState(() => _isPressed = pressed);
|
|
},
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: widget.iconColor.withOpacity(_isPressed ? 0.3 : 0.2),
|
|
blurRadius: _isPressed ? 15 : 10,
|
|
spreadRadius:
|
|
_isPressed
|
|
? 2 + _animation.value * 3
|
|
: 1 + _animation.value * 2,
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
flex: 7,
|
|
child: Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: widget.bgColor,
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(16),
|
|
topRight: Radius.circular(16),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: 1.0 + (_animation.value * 0.1),
|
|
child: Icon(
|
|
widget.fallbackIcon,
|
|
color: widget.iconColor,
|
|
size: isSmallScreen ? 32 : 36,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isSmallScreen ? 8 : 10,
|
|
vertical: isSmallScreen ? 8 : 10,
|
|
),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(16),
|
|
bottomRight: Radius.circular(16),
|
|
),
|
|
),
|
|
child: Text(
|
|
widget.title,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: isSmallScreen ? 12 : 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AnimatedTipCard extends StatefulWidget {
|
|
final String title;
|
|
final String description;
|
|
final Color color;
|
|
final IconData icon;
|
|
|
|
const AnimatedTipCard({
|
|
super.key,
|
|
required this.title,
|
|
required this.description,
|
|
required this.color,
|
|
required this.icon,
|
|
});
|
|
|
|
@override
|
|
State<AnimatedTipCard> createState() => _AnimatedTipCardState();
|
|
}
|
|
|
|
class _AnimatedTipCardState extends State<AnimatedTipCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
bool _isActive = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(seconds: 4),
|
|
vsync: this,
|
|
)..repeat(reverse: false);
|
|
|
|
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Deteksi ukuran layar untuk responsivitas
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final isSmallScreen = screenWidth < 360;
|
|
final cardWidth = screenWidth * (isSmallScreen ? 0.7 : 0.75);
|
|
|
|
return GestureDetector(
|
|
onTapDown: (_) => setState(() => _isActive = true),
|
|
onTapUp: (_) => setState(() => _isActive = false),
|
|
onTapCancel: () => setState(() => _isActive = false),
|
|
child: AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
width: cardWidth,
|
|
padding: EdgeInsets.all(isSmallScreen ? 12 : 14),
|
|
decoration: BoxDecoration(
|
|
color: widget.color,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: widget.color.withOpacity(_isActive ? 0.4 : 0.2),
|
|
blurRadius: _isActive ? 10 : 3,
|
|
spreadRadius: _isActive ? 2 : 0,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
widget.color,
|
|
Color.lerp(widget.color, Colors.white, 0.2)!,
|
|
],
|
|
stops: const [0.6, 1.0],
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
// Shimmer effect
|
|
Positioned.fill(
|
|
child: IgnorePointer(
|
|
ignoring: true,
|
|
child: ShaderMask(
|
|
blendMode: BlendMode.srcATop,
|
|
shaderCallback: (bounds) {
|
|
return LinearGradient(
|
|
begin: Alignment(-1.0 + 2 * _animation.value, -0.5),
|
|
end: Alignment(0.0 + 2 * _animation.value, 0.5),
|
|
colors: const [
|
|
Colors.transparent,
|
|
Colors.white,
|
|
Colors.transparent,
|
|
],
|
|
stops: const [0.0, 0.5, 1.0],
|
|
).createShader(bounds);
|
|
},
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
color: Colors.white.withOpacity(0.1),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Content
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
padding: EdgeInsets.all(isSmallScreen ? 5 : 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(
|
|
_isActive ? 0.4 : 0.3,
|
|
),
|
|
shape: BoxShape.circle,
|
|
boxShadow:
|
|
_isActive
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.white.withOpacity(0.3),
|
|
blurRadius: 8,
|
|
spreadRadius: 2,
|
|
),
|
|
]
|
|
: [],
|
|
),
|
|
child: AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return Transform.rotate(
|
|
angle: _isActive ? _animation.value * 0.1 : 0,
|
|
child: Icon(
|
|
widget.icon,
|
|
color: Colors.white,
|
|
size:
|
|
_isActive
|
|
? (isSmallScreen ? 16 : 18)
|
|
: (isSmallScreen ? 14 : 16),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
widget.title,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: isSmallScreen ? 13 : 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child: Text(
|
|
widget.description,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: isSmallScreen ? 11 : 12,
|
|
color: Colors.white,
|
|
height: 1.4,
|
|
),
|
|
maxLines: 3,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|