import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'custom_info_card.dart'; import 'favorites_page.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); static const Color primaryColor = Color.fromARGB(255, 254, 100, 91); static const Color secondaryColor = Colors.orange; @override Widget build(BuildContext context) => MaterialApp( title: 'UPR App', theme: ThemeData( primaryColor: primaryColor, appBarTheme: const AppBarTheme( backgroundColor: primaryColor, elevation: 0, ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( selectedItemColor: Colors.white, unselectedItemColor: Colors.white70, backgroundColor: primaryColor, type: BottomNavigationBarType.fixed, showSelectedLabels: true, showUnselectedLabels: true, ), ), home: const HomePage(), debugShowCheckedModeBanner: false, ); } class HomePage extends StatefulWidget { const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State { List> allCards = []; List> filteredCards = []; List> favoriteCards = []; int _currentIndex = 0; bool _isLoading = false; static const String _sheetUrl = 'https://script.google.com/macros/s/AKfycbw1htmk7rlwLMOkOrpZ9ED-7ErWgMlYNtLKxoQ9QO-FopqTAhJuQkR7Gs1LxTJakbMT/exec'; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { final prefs = await SharedPreferences.getInstance(); setState(() { allCards = prefs.getString('infoCardsData') != null ? List>.from(json.decode(prefs.getString('infoCardsData')!)) : [_defaultCard()]; filteredCards = allCards; }); await _fetchData(); } Future _fetchData() async { try { setState(() => _isLoading = true); final response = await http.get(Uri.parse(_sheetUrl)).timeout(const Duration(seconds: 10)); if (response.statusCode == 200) { final newData = List>.from(json.decode(response.body)); if (json.encode(newData) != json.encode(allCards)) { setState(() { allCards = newData; filteredCards = _filterCardsByCategory(_currentIndex); }); (await SharedPreferences.getInstance()).setString('infoCardsData', response.body); } } } catch (e) { if (kDebugMode) print('Fetch error: $e'); } finally { if (mounted) setState(() => _isLoading = false); } } Future _loadFavorites() async { final prefs = await SharedPreferences.getInstance(); final favKeys = prefs.getKeys().where((k) => k.startsWith('fav_') && (prefs.getBool(k) ?? false)); setState(() => favoriteCards = allCards.where((c) => favKeys.contains('fav_${c['title']}')).toList()); } List> _filterCardsByCategory(int index) { switch (index) { case 0: return allCards; case 1: return allCards.where((card) => card['categoria'] == 'Actividades').toList(); case 2: return allCards.where((card) => card['categoria'] == 'Noticias UPR').toList(); case 3: return allCards.where((card) => card['categoria'] == 'Fechas Importantes').toList(); case 4: return favoriteCards; default: return allCards; } } Map _defaultCard() => { 'image': 'assets/icons/icono1.jpg', 'title': 'Tutorias de programación', 'subtitle': '8:00am a 11:30am En biblioteca Lázaro', 'texto': 'Detalles completos sobre tutorías...', 'categoria': 'Actividades', }; @override Widget build(BuildContext context) => Scaffold( appBar: _currentIndex != 4 ? _buildAppBar(context) : null, body: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [MyApp.primaryColor, MyApp.secondaryColor], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), child: _currentIndex == 4 ? FavoritesPage(favoriteCards: favoriteCards, onRefresh: _loadFavorites) : _buildHome(), ), bottomNavigationBar: _buildBottomNavBar(), ); AppBar _buildAppBar(BuildContext context) => AppBar( flexibleSpace: Padding( padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20), child: Image.asset('assets/header_image.png', fit: BoxFit.cover), ), toolbarHeight: MediaQuery.of(context).size.height * 0.19, ); Widget _buildBottomNavBar() => BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { setState(() { _currentIndex = index; filteredCards = _filterCardsByCategory(index); if (index == 4) _loadFavorites(); }); }, items: const [ BottomNavigationBarItem( icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Inicio', ), BottomNavigationBarItem( icon: Icon(Icons.event_outlined), activeIcon: Icon(Icons.event), label: 'Actividades', ), BottomNavigationBarItem( icon: Icon(Icons.article_outlined), activeIcon: Icon(Icons.article), label: 'Noticias', ), BottomNavigationBarItem( icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Fechas', ), BottomNavigationBarItem( icon: Icon(Icons.favorite_outline), activeIcon: Icon(Icons.favorite), label: 'Favoritos', ), ], ); Widget _buildHome() => SafeArea( child: Column(children: [ Expanded(child: _isLoading ? _buildLoader() : RefreshIndicator( onRefresh: _fetchData, child: filteredCards.isEmpty ? _buildEmptyState() : ListView.builder( itemCount: filteredCards.length, itemBuilder: (ctx, i) => CustomInfoCard( imagePath: "assets/icons/${filteredCards[i]['image']}", title: filteredCards[i]['title'], subtitle: filteredCards[i]['subtitle'], texto: filteredCards[i]['texto'], categoria: filteredCards[i]['categoria'], ), ), )), ]), ); Widget _buildLoader() => const Center(child: CircularProgressIndicator(color: Colors.white)); Widget _buildEmptyState() => Center(child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.info_outline, size: 50, color: Colors.white), const SizedBox(height: 16), Text( 'No hay ${_getCategoryName(_currentIndex)} disponibles', style: const TextStyle(color: Colors.white, fontSize: 18), ), ], )); String _getCategoryName(int index) { switch (index) { case 0: return 'contenidos'; case 1: return 'actividades'; case 2: return 'noticias'; case 3: return 'fechas importantes'; default: return 'elementos'; } } }