Nav apraksta

main.dart 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import 'dart:convert';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:http/http.dart' as http;
  5. import 'package:shared_preferences/shared_preferences.dart';
  6. import 'custom_info_card.dart';
  7. import 'favorites_page.dart';
  8. void main() => runApp(const MyApp());
  9. class MyApp extends StatelessWidget {
  10. const MyApp({super.key});
  11. static const Color primaryColor = Color.fromARGB(255, 254, 100, 91);
  12. static const Color secondaryColor = Colors.orange;
  13. @override
  14. Widget build(BuildContext context) => MaterialApp(
  15. title: 'UPR App',
  16. theme: ThemeData(
  17. primaryColor: primaryColor,
  18. appBarTheme: const AppBarTheme(
  19. backgroundColor: primaryColor,
  20. elevation: 0,
  21. ),
  22. bottomNavigationBarTheme: const BottomNavigationBarThemeData(
  23. selectedItemColor: Colors.white,
  24. unselectedItemColor: Colors.white70,
  25. backgroundColor: primaryColor,
  26. type: BottomNavigationBarType.fixed,
  27. showSelectedLabels: true,
  28. showUnselectedLabels: true,
  29. ),
  30. ),
  31. home: const HomePage(),
  32. debugShowCheckedModeBanner: false,
  33. );
  34. }
  35. class HomePage extends StatefulWidget {
  36. const HomePage({super.key});
  37. @override
  38. State<HomePage> createState() => _HomePageState();
  39. }
  40. class _HomePageState extends State<HomePage> {
  41. List<Map<String, dynamic>> allCards = [];
  42. List<Map<String, dynamic>> filteredCards = [];
  43. List<Map<String, dynamic>> favoriteCards = [];
  44. int _currentIndex = 0;
  45. bool _isLoading = false;
  46. static const String _sheetUrl = 'https://script.google.com/macros/s/AKfycbw1htmk7rlwLMOkOrpZ9ED-7ErWgMlYNtLKxoQ9QO-FopqTAhJuQkR7Gs1LxTJakbMT/exec';
  47. @override
  48. void initState() {
  49. super.initState();
  50. _loadData();
  51. }
  52. Future<void> _loadData() async {
  53. final prefs = await SharedPreferences.getInstance();
  54. setState(() {
  55. allCards = prefs.getString('infoCardsData') != null
  56. ? List<Map<String, dynamic>>.from(json.decode(prefs.getString('infoCardsData')!))
  57. : [_defaultCard()];
  58. filteredCards = allCards;
  59. });
  60. await _fetchData();
  61. }
  62. Future<void> _fetchData() async {
  63. try {
  64. setState(() => _isLoading = true);
  65. final response = await http.get(Uri.parse(_sheetUrl)).timeout(const Duration(seconds: 10));
  66. if (response.statusCode == 200) {
  67. final newData = List<Map<String, dynamic>>.from(json.decode(response.body));
  68. if (json.encode(newData) != json.encode(allCards)) {
  69. setState(() {
  70. allCards = newData;
  71. filteredCards = _filterCardsByCategory(_currentIndex);
  72. });
  73. (await SharedPreferences.getInstance()).setString('infoCardsData', response.body);
  74. }
  75. }
  76. } catch (e) {
  77. if (kDebugMode) print('Fetch error: $e');
  78. } finally {
  79. if (mounted) setState(() => _isLoading = false);
  80. }
  81. }
  82. Future<void> _loadFavorites() async {
  83. final prefs = await SharedPreferences.getInstance();
  84. final favKeys = prefs.getKeys().where((k) => k.startsWith('fav_') && (prefs.getBool(k) ?? false));
  85. setState(() => favoriteCards = allCards.where((c) => favKeys.contains('fav_${c['title']}')).toList());
  86. }
  87. List<Map<String, dynamic>> _filterCardsByCategory(int index) {
  88. switch (index) {
  89. case 0: return allCards;
  90. case 1: return allCards.where((card) => card['categoria'] == 'Actividades').toList();
  91. case 2: return allCards.where((card) => card['categoria'] == 'Noticias UPR').toList();
  92. case 3: return allCards.where((card) => card['categoria'] == 'Fechas Importantes').toList();
  93. case 4: return favoriteCards;
  94. default: return allCards;
  95. }
  96. }
  97. Map<String, dynamic> _defaultCard() => {
  98. 'image': 'assets/icons/icono1.jpg',
  99. 'title': 'Tutorias de programación',
  100. 'subtitle': '8:00am a 11:30am En biblioteca Lázaro',
  101. 'texto': 'Detalles completos sobre tutorías...',
  102. 'categoria': 'Actividades',
  103. };
  104. @override
  105. Widget build(BuildContext context) => Scaffold(
  106. appBar: _currentIndex != 4 ? _buildAppBar(context) : null,
  107. body: Container(
  108. decoration: const BoxDecoration(
  109. gradient: LinearGradient(
  110. colors: [MyApp.primaryColor, MyApp.secondaryColor],
  111. begin: Alignment.topCenter,
  112. end: Alignment.bottomCenter,
  113. ),
  114. ),
  115. child: _currentIndex == 4
  116. ? FavoritesPage(favoriteCards: favoriteCards, onRefresh: _loadFavorites)
  117. : _buildHome(),
  118. ),
  119. bottomNavigationBar: _buildBottomNavBar(),
  120. );
  121. AppBar _buildAppBar(BuildContext context) => AppBar(
  122. flexibleSpace: Padding(
  123. padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),
  124. child: Image.asset('assets/header_image.png', fit: BoxFit.cover),
  125. ),
  126. toolbarHeight: MediaQuery.of(context).size.height * 0.19,
  127. );
  128. Widget _buildBottomNavBar() => BottomNavigationBar(
  129. currentIndex: _currentIndex,
  130. onTap: (index) {
  131. setState(() {
  132. _currentIndex = index;
  133. filteredCards = _filterCardsByCategory(index);
  134. if (index == 4) _loadFavorites();
  135. });
  136. },
  137. items: const [
  138. BottomNavigationBarItem(
  139. icon: Icon(Icons.home_outlined),
  140. activeIcon: Icon(Icons.home),
  141. label: 'Inicio',
  142. ),
  143. BottomNavigationBarItem(
  144. icon: Icon(Icons.event_outlined),
  145. activeIcon: Icon(Icons.event),
  146. label: 'Actividades',
  147. ),
  148. BottomNavigationBarItem(
  149. icon: Icon(Icons.article_outlined),
  150. activeIcon: Icon(Icons.article),
  151. label: 'Noticias',
  152. ),
  153. BottomNavigationBarItem(
  154. icon: Icon(Icons.calendar_today_outlined),
  155. activeIcon: Icon(Icons.calendar_today),
  156. label: 'Fechas',
  157. ),
  158. BottomNavigationBarItem(
  159. icon: Icon(Icons.favorite_outline),
  160. activeIcon: Icon(Icons.favorite),
  161. label: 'Favoritos',
  162. ),
  163. ],
  164. );
  165. Widget _buildHome() => SafeArea(
  166. child: Column(children: [
  167. Expanded(child: _isLoading ? _buildLoader() : RefreshIndicator(
  168. onRefresh: _fetchData,
  169. child: filteredCards.isEmpty ? _buildEmptyState() : ListView.builder(
  170. itemCount: filteredCards.length,
  171. itemBuilder: (ctx, i) => CustomInfoCard(
  172. imagePath: "assets/icons/${filteredCards[i]['image']}",
  173. title: filteredCards[i]['title'],
  174. subtitle: filteredCards[i]['subtitle'],
  175. texto: filteredCards[i]['texto'],
  176. categoria: filteredCards[i]['categoria'],
  177. ),
  178. ),
  179. )),
  180. ]),
  181. );
  182. Widget _buildLoader() => const Center(child: CircularProgressIndicator(color: Colors.white));
  183. Widget _buildEmptyState() => Center(child: Column(
  184. mainAxisSize: MainAxisSize.min,
  185. children: [
  186. const Icon(Icons.info_outline, size: 50, color: Colors.white),
  187. const SizedBox(height: 16),
  188. Text(
  189. 'No hay ${_getCategoryName(_currentIndex)} disponibles',
  190. style: const TextStyle(color: Colors.white, fontSize: 18),
  191. ),
  192. ],
  193. ));
  194. String _getCategoryName(int index) {
  195. switch (index) {
  196. case 0: return 'contenidos';
  197. case 1: return 'actividades';
  198. case 2: return 'noticias';
  199. case 3: return 'fechas importantes';
  200. default: return 'elementos';
  201. }
  202. }
  203. }