2 次代码提交

作者 SHA1 备注 提交日期
  Luis Aparicio 11f1907ce7 add categories 3 周前
  Luis Aparicio 09d8f77ef5 add favorites function 1 个月前
共有 4 个文件被更改,包括 395 次插入268 次删除
  1. 120
    69
      lib/custom_info_card.dart
  2. 37
    23
      lib/detail_page.dart
  3. 66
    0
      lib/favorites_page.dart
  4. 172
    176
      lib/main.dart

+ 120
- 69
lib/custom_info_card.dart 查看文件

@@ -1,11 +1,10 @@
1 1
 import 'package:flutter/material.dart';
2
-import 'detail_page.dart'; // Importa la nueva pantalla
2
+import 'package:shared_preferences/shared_preferences.dart';
3
+import 'detail_page.dart';
3 4
 
4
-class CustomInfoCard extends StatelessWidget {
5
-  final String imagePath;
6
-  final String title;
7
-  final String subtitle;
8
-  final String texto;
5
+class CustomInfoCard extends StatefulWidget {
6
+  final String imagePath, title, subtitle, texto, categoria;
7
+  final bool isFavorite;
9 8
 
10 9
   const CustomInfoCard({
11 10
     super.key,
@@ -13,81 +12,133 @@ class CustomInfoCard extends StatelessWidget {
13 12
     required this.title,
14 13
     required this.subtitle,
15 14
     required this.texto,
15
+    required this.categoria,
16
+    this.isFavorite = false,
16 17
   });
17 18
 
18 19
   @override
19
-  Widget build(BuildContext context) {
20
-    return GestureDetector(
21
-      onTap: () {
22
-        Navigator.push(
23
-          context,
24
-          MaterialPageRoute(
25
-            builder: (context) => DetailPage(
26
-              title: title,
27
-              texto: texto,
28
-            ),
29
-          ),
30
-        );
31
-      },
32
-      child: SizedBox(
33
-        width: double.infinity,
34
-        height: 130,
35
-        child: Card(
20
+  State<CustomInfoCard> createState() => _CustomInfoCardState();
21
+}
22
+
23
+class _CustomInfoCardState extends State<CustomInfoCard> {
24
+  late bool _isFavorite;
25
+
26
+  @override
27
+  void initState() {
28
+    super.initState();
29
+    _isFavorite = widget.isFavorite;
30
+    if (!widget.isFavorite) _loadFavorite();
31
+  }
32
+
33
+  Future<void> _loadFavorite() async {
34
+    final prefs = await SharedPreferences.getInstance();
35
+    if (mounted) setState(() => _isFavorite = prefs.getBool('fav_${widget.title}') ?? false);
36
+  }
37
+
38
+  Future<void> _toggleFavorite() async {
39
+    setState(() => _isFavorite = !_isFavorite);
40
+    (await SharedPreferences.getInstance()).setBool('fav_${widget.title}', _isFavorite);
41
+  }
42
+
43
+  @override
44
+  Widget build(BuildContext context) => GestureDetector(
45
+    onTap: () => Navigator.push(
46
+      context,
47
+      MaterialPageRoute(
48
+        builder: (_) => DetailPage(
49
+          title: widget.title,
50
+          texto: widget.texto,
51
+          categoria: widget.categoria,
52
+        ),
53
+      ),
54
+    ),
55
+    child: SizedBox(
56
+      width: double.infinity,
57
+      height: 130,
58
+      child: Stack(children: [
59
+        Card(
36 60
           margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
37
-          shape: RoundedRectangleBorder(
38
-            borderRadius: BorderRadius.circular(30),
39
-          ),
61
+          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
40 62
           color: const Color.fromARGB(255, 254, 242, 221),
41 63
           child: Padding(
42 64
             padding: const EdgeInsets.only(right: 8),
43
-            child: Row(
44
-              children: [
45
-                Container(
46
-                  width: 90,
47
-                  height: 90,
48
-                  decoration: BoxDecoration(
49
-                    color: Colors.white,
50
-                    borderRadius: BorderRadius.circular(36),
51
-                  ),
52
-                  child: ClipRRect(
53
-                    borderRadius: BorderRadius.circular(36),
54
-                    child: Image.asset(
55
-                      imagePath,
56
-                      fit: BoxFit.cover,
57
-                      width: 90,
58
-                      height: 90,
59
-                    ),
65
+            child: Row(children: [
66
+              Container(
67
+                width: 90,
68
+                height: 90,
69
+                decoration: BoxDecoration(
70
+                  color: Colors.white,
71
+                  borderRadius: BorderRadius.circular(36),
72
+                ),
73
+                child: ClipRRect(
74
+                  borderRadius: BorderRadius.circular(36),
75
+                  child: Image.asset(
76
+                    widget.imagePath,
77
+                    fit: BoxFit.cover,
78
+                    width: 90,
79
+                    height: 90,
60 80
                   ),
61 81
                 ),
62
-                const SizedBox(width: 16),
63
-                Expanded(
64
-                  child: Column(
65
-                    mainAxisAlignment: MainAxisAlignment.center,
66
-                    crossAxisAlignment: CrossAxisAlignment.start,
67
-                    children: [
68
-                      Text(
69
-                        title,
70
-                        style: const TextStyle(
71
-                          fontSize: 16,
72
-                          fontWeight: FontWeight.bold,
73
-                        ),
74
-                      ),
75
-                      const SizedBox(height: 4),
76
-                      Text(
77
-                        subtitle,
78
-                        style: TextStyle(
79
-                          fontSize: 14,
80
-                          color: Colors.grey[600],
81
-                        ),
82
+              ),
83
+              const SizedBox(width: 16),
84
+              Expanded(
85
+                child: Column(
86
+                  mainAxisAlignment: MainAxisAlignment.center,
87
+                  crossAxisAlignment: CrossAxisAlignment.start,
88
+                  children: [
89
+                    Text(
90
+                      widget.title,
91
+                      style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
92
+                      maxLines: 1,
93
+                      overflow: TextOverflow.ellipsis,
94
+                    ),
95
+                    const SizedBox(height: 4),
96
+                    Text(
97
+                      widget.subtitle,
98
+                      style: TextStyle(fontSize: 14, color: Colors.grey[600]),
99
+                      maxLines: 2,
100
+                      overflow: TextOverflow.ellipsis,
101
+                    ),
102
+                    const SizedBox(height: 4),
103
+                    Text(
104
+                      widget.categoria,
105
+                      style: TextStyle(
106
+                        fontSize: 12,
107
+                        color: _getCategoryColor(widget.categoria),
108
+                        fontWeight: FontWeight.bold,
82 109
                       ),
83
-                    ],
84
-                  ),
110
+                    ),
111
+                  ],
85 112
                 ),
86
-              ],
113
+              ),
114
+            ]),
115
+          ),
116
+        ),
117
+        Positioned(
118
+          top: 12,
119
+          right: 28,
120
+          child: IconButton(
121
+            icon: Icon(
122
+              _isFavorite ? Icons.favorite : Icons.favorite_border,
123
+              color: _isFavorite ? Colors.red : Colors.grey[400],
87 124
             ),
125
+            onPressed: _toggleFavorite,
88 126
           ),
89 127
         ),
90
-      ),
91
-    );
128
+      ]),
129
+    ),
130
+  );
131
+
132
+  Color _getCategoryColor(String categoria) {
133
+    switch (categoria) {
134
+      case 'Actividades':
135
+        return Colors.blue;
136
+      case 'Noticias UPR':
137
+        return Colors.green;
138
+      case 'Fechas Importantes':
139
+        return Colors.purple;
140
+      default:
141
+        return Colors.grey;
142
+    }
92 143
   }
93
-}
144
+}

+ 37
- 23
lib/detail_page.dart 查看文件

@@ -3,47 +3,61 @@ import 'package:flutter/material.dart';
3 3
 class DetailPage extends StatelessWidget {
4 4
   final String title;
5 5
   final String texto;
6
+  final String categoria;
6 7
 
7 8
   const DetailPage({
8 9
     super.key,
9 10
     required this.title,
10 11
     required this.texto,
12
+    required this.categoria,
11 13
   });
12 14
 
13 15
   @override
14
-  Widget build(BuildContext context) {
15
-    return Scaffold(
16
-      appBar: AppBar(
17
-        flexibleSpace: Padding(
18
-          padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
19
-          child: Image.asset(
20
-            'assets/header_image.png',
21
-            fit: BoxFit.cover,
22
-          ),
23
-        ),
24
-        toolbarHeight: MediaQuery.of(context).size.height * 0.19,
16
+  Widget build(BuildContext context) => Scaffold(
17
+    appBar: AppBar(
18
+      flexibleSpace: Padding(
19
+        padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),
20
+        child: Image.asset('assets/header_image.png', fit: BoxFit.cover),
25 21
       ),
26
-      body: SingleChildScrollView( // Permite el desplazamiento
27
-        child: Padding(
28
-          padding: const EdgeInsets.all(16.0),
22
+      toolbarHeight: MediaQuery.of(context).size.height * 0.19,
23
+      title: Text(
24
+        categoria,
25
+        style: const TextStyle(color: Colors.white, fontSize: 16),
26
+      ),
27
+    ),
28
+    body: Container(
29
+      decoration: const BoxDecoration(color: Color.fromARGB(255, 254, 242, 221)),
30
+      child: Padding(
31
+        padding: const EdgeInsets.all(16.0),
32
+        child: SingleChildScrollView(
29 33
           child: Column(
30
-            crossAxisAlignment: CrossAxisAlignment.center,
34
+            crossAxisAlignment: CrossAxisAlignment.start,
31 35
             children: [
32 36
               Text(
33 37
                 title,
34
-                style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
35
-                textAlign: TextAlign.center,
38
+                style: const TextStyle(
39
+                  fontSize: 24,
40
+                  fontWeight: FontWeight.bold,
41
+                  color: Colors.black87,
42
+                ),
36 43
               ),
37 44
               const SizedBox(height: 8),
38
-              Text(
39
-                texto,
40
-                style: const TextStyle(fontSize: 18, color: Colors.black),
41
-                textAlign: TextAlign.justify,
45
+              Container(
46
+                padding: const EdgeInsets.all(12),
47
+                decoration: BoxDecoration(
48
+                  color: Colors.white,
49
+                  borderRadius: BorderRadius.circular(8),
50
+                ),
51
+                child: Text(
52
+                  texto,
53
+                  style: const TextStyle(fontSize: 16, height: 1.5),
54
+                  textAlign: TextAlign.justify,
55
+                ),
42 56
               ),
43 57
             ],
44 58
           ),
45 59
         ),
46 60
       ),
47
-    );
48
-  }
61
+    ),
62
+  );
49 63
 }

+ 66
- 0
lib/favorites_page.dart 查看文件

@@ -0,0 +1,66 @@
1
+import 'package:flutter/material.dart';
2
+import 'custom_info_card.dart';
3
+
4
+class FavoritesPage extends StatelessWidget {
5
+  final List<Map<String, dynamic>> favoriteCards;
6
+  final VoidCallback? onRefresh;
7
+
8
+  const FavoritesPage({super.key, required this.favoriteCards, this.onRefresh});
9
+
10
+  @override
11
+  Widget build(BuildContext context) => Scaffold(
12
+    appBar: AppBar(
13
+      flexibleSpace: Padding(
14
+        padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),
15
+        child: Image.asset('assets/header_image.png', fit: BoxFit.cover),
16
+      ),
17
+      toolbarHeight: MediaQuery.of(context).size.height * 0.19,
18
+      title: const Text('Mis Favoritos', style: TextStyle(color: Colors.white)),
19
+    ),
20
+    body: Container(
21
+      decoration: const BoxDecoration(
22
+        gradient: LinearGradient(
23
+          colors: [Color.fromARGB(255, 254, 100, 91), Colors.orange],
24
+          begin: Alignment.topCenter,
25
+          end: Alignment.bottomCenter,
26
+        ),
27
+      ),
28
+      child: favoriteCards.isEmpty
29
+          ? _buildEmptyState()
30
+          : RefreshIndicator(
31
+              onRefresh: () async => onRefresh?.call(),
32
+              child: ListView.builder(
33
+                padding: const EdgeInsets.only(top: 16),
34
+                itemCount: favoriteCards.length,
35
+                itemBuilder: (_, i) => CustomInfoCard(
36
+                  imagePath: "assets/icons/${favoriteCards[i]['image']}",
37
+                  title: favoriteCards[i]['title'],
38
+                  subtitle: favoriteCards[i]['subtitle'],
39
+                  texto: favoriteCards[i]['texto'],
40
+                  categoria: favoriteCards[i]['categoria'],
41
+                  isFavorite: true,
42
+                ),
43
+              ),
44
+            ),
45
+    ),
46
+  );
47
+
48
+  Widget _buildEmptyState() => Center(
49
+    child: Column(
50
+      mainAxisAlignment: MainAxisAlignment.center,
51
+      children: [
52
+        const Icon(Icons.favorite_border, size: 64, color: Colors.white54),
53
+        const SizedBox(height: 16),
54
+        const Text(
55
+          'No tienes favoritos aún',
56
+          style: TextStyle(color: Colors.white, fontSize: 18),
57
+        ),
58
+        const SizedBox(height: 8),
59
+        Text(
60
+          'Agrega favoritos desde las otras secciones',
61
+          style: TextStyle(color: Colors.white.withOpacity(0.8)),
62
+        ),
63
+      ],
64
+    ),
65
+  );
66
+}

+ 172
- 176
lib/main.dart 查看文件

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