Luis Aparicio пре 2 недеља
родитељ
комит
09d8f77ef5
4 измењених фајлова са 288 додато и 206 уклоњено
  1. 99
    52
      lib/custom_info_card.dart
  2. 21
    17
      lib/detail_page.dart
  3. 55
    0
      lib/favorites_page.dart
  4. 113
    137
      lib/main.dart

+ 99
- 52
lib/custom_info_card.dart Прегледај датотеку

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

+ 21
- 17
lib/detail_page.dart Прегледај датотеку

@@ -15,7 +15,7 @@ class DetailPage extends StatelessWidget {
15 15
     return Scaffold(
16 16
       appBar: AppBar(
17 17
         flexibleSpace: Padding(
18
-          padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
18
+          padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top + -20),
19 19
           child: Image.asset(
20 20
             'assets/header_image.png',
21 21
             fit: BoxFit.cover,
@@ -23,24 +23,28 @@ class DetailPage extends StatelessWidget {
23 23
         ),
24 24
         toolbarHeight: MediaQuery.of(context).size.height * 0.19,
25 25
       ),
26
-      body: SingleChildScrollView( // Permite el desplazamiento
26
+      body: Container(
27
+        decoration: const BoxDecoration(
28
+          color: Color.fromARGB(255, 254, 242, 221),
29
+        ),
27 30
         child: Padding(
28 31
           padding: const EdgeInsets.all(16.0),
29
-          child: Column(
30
-            crossAxisAlignment: CrossAxisAlignment.center,
31
-            children: [
32
-              Text(
33
-                title,
34
-                style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
35
-                textAlign: TextAlign.center,
36
-              ),
37
-              const SizedBox(height: 8),
38
-              Text(
39
-                texto,
40
-                style: const TextStyle(fontSize: 18, color: Colors.black),
41
-                textAlign: TextAlign.justify,
42
-              ),
43
-            ],
32
+          child: SingleChildScrollView( // Agregado para evitar overflow
33
+            child: Column(
34
+              crossAxisAlignment: CrossAxisAlignment.center,
35
+              children: [
36
+                Text(
37
+                  title,
38
+                  style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
39
+                ),
40
+                const SizedBox(height: 8),
41
+                Text(
42
+                  texto,
43
+                  style: const TextStyle(fontSize: 18, color: Colors.black),
44
+                  textAlign: TextAlign.center,
45
+                ),
46
+              ],
47
+            ),
44 48
           ),
45 49
         ),
46 50
       ),

+ 55
- 0
lib/favorites_page.dart Прегледај датотеку

@@ -0,0 +1,55 @@
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
+
7
+  const FavoritesPage({super.key, required this.favoriteCards});
8
+
9
+  @override
10
+  Widget build(BuildContext context) {
11
+    return Scaffold(
12
+      appBar: AppBar(
13
+        flexibleSpace: Padding(
14
+          padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),
15
+          child: Image.asset(
16
+            'assets/header_image.png',
17
+            fit: BoxFit.cover,
18
+          ),
19
+        ),
20
+        toolbarHeight: MediaQuery.of(context).size.height * 0.19,
21
+        title: const Text('Mis Favoritos', style: TextStyle(color: Colors.white)),
22
+      ),
23
+      body: Container(
24
+        decoration: const BoxDecoration(
25
+          gradient: LinearGradient(
26
+            colors: [Color.fromARGB(255, 254, 100, 91), Colors.orange],
27
+            begin: Alignment.topCenter,
28
+            end: Alignment.bottomCenter,
29
+          ),
30
+        ),
31
+        child: favoriteCards.isEmpty
32
+            ? const Center(
33
+                child: Text(
34
+                  'No tienes favoritos aún',
35
+                  style: TextStyle(color: Colors.white, fontSize: 18),
36
+                ),
37
+              )
38
+            : ListView.builder(
39
+                padding: const EdgeInsets.only(top: 16),
40
+                itemCount: favoriteCards.length,
41
+                itemBuilder: (context, index) {
42
+                  final card = favoriteCards[index];
43
+                  return CustomInfoCard(
44
+                    imagePath: "assets/icons/${card['image']}",
45
+                    title: card['title'],
46
+                    subtitle: card['subtitle'],
47
+                    texto: card['texto'],
48
+                    isFavorite: true,
49
+                  );
50
+                },
51
+              ),
52
+      ),
53
+    );
54
+  }
55
+}

+ 113
- 137
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'; // Nuevo import
7 8
 
8 9
 void main() => runApp(const MyApp());
9 10
 
@@ -39,6 +40,8 @@ class HomePage extends StatefulWidget {
39 40
 
40 41
 class _HomePageState extends State<HomePage> {
41 42
   List<Map<String, dynamic>> infoCards = [];
43
+  List<Map<String, dynamic>> favoriteCards = [];
44
+  int _currentIndex = 0;
42 45
   static const String googleSheetUrl = 'https://script.google.com/macros/s/AKfycbw1htmk7rlwLMOkOrpZ9ED-7ErWgMlYNtLKxoQ9QO-FopqTAhJuQkR7Gs1LxTJakbMT/exec';
43 46
 
44 47
   @override
@@ -49,106 +52,72 @@ class _HomePageState extends State<HomePage> {
49 52
 
50 53
   Future<void> _loadInfoCards() async {
51 54
     final prefs = await SharedPreferences.getInstance();
52
-
53
-    // Load default data if no saved data
54 55
     String? savedData = prefs.getString('infoCardsData');
56
+    
55 57
     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
-      }
58
+      infoCards = savedData != null 
59
+          ? List<Map<String, dynamic>>.from(json.decode(savedData))
60
+          : _defaultInfoCards();
69 61
     });
70
-  
62
+    
71 63
     await _fetchUpdatedData();
72 64
   }
73 65
 
66
+  Future<void> _loadFavorites() async {
67
+    final prefs = await SharedPreferences.getInstance();
68
+    final allKeys = prefs.getKeys().where((key) => key.startsWith('fav_')).toList();
69
+    
70
+    setState(() {
71
+      favoriteCards = infoCards.where((card) {
72
+        return allKeys.contains('fav_${card['title']}') && 
73
+               (prefs.getBool('fav_${card['title']}') ?? false);
74
+      }).toList();
75
+    });
76
+  }
77
+
74 78
   List<Map<String, dynamic>> _defaultInfoCards() {
75 79
     return [
76 80
       {
77 81
         'image': 'assets/icons/icono1.jpg',
78 82
         'title': 'Tutorias de programación',
79 83
         'subtitle': '8:00am a 11:30am En biblioteca Lázaro',
84
+        'texto': 'Detalles completos sobre tutorías...',
80 85
       },
81 86
     ];
82 87
   }
83 88
 
84
-  Future<void> _fetchUpdatedData() async {    
89
+  Future<void> _fetchUpdatedData() async {
85 90
     try {
86 91
       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}');
92
+      if (response.statusCode == 200) {
93
+        final List<Map<String, dynamic>> newData = List<Map<String, dynamic>>.from(json.decode(response.body));
94
+        
95
+        if (json.encode(newData) != json.encode(infoCards)) {
96
+          setState(() => infoCards = newData);
97
+          final prefs = await SharedPreferences.getInstance();
98
+          await prefs.setString('infoCardsData', json.encode(newData));
130 99
         }
131 100
       }
132 101
     } catch (e) {
133
-      if (kDebugMode) {
134
-        print('Error fetching data: $e');
135
-      }
102
+      if (kDebugMode) print('Error fetching data: $e');
136 103
     }
137 104
   }
138 105
 
139 106
   @override
140 107
   Widget build(BuildContext context) {
141 108
     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
-          ),
149
-        ),
150
-        toolbarHeight: MediaQuery.of(context).size.height * 0.19,
151
-      ),
109
+      appBar: _currentIndex == 0 
110
+          ? AppBar(
111
+              flexibleSpace: Padding(
112
+                padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top - 20),
113
+                child: Image.asset(
114
+                  'assets/header_image.png',
115
+                  fit: BoxFit.cover,
116
+                ),
117
+              ),
118
+              toolbarHeight: MediaQuery.of(context).size.height * 0.19,
119
+            )
120
+          : null,
152 121
       body: Container(
153 122
         decoration: const BoxDecoration(
154 123
           gradient: LinearGradient(
@@ -157,71 +126,78 @@ class _HomePageState extends State<HomePage> {
157 126
             end: Alignment.bottomCenter,
158 127
           ),
159 128
         ),
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 == 0
130
+            ? RefreshIndicator(
131
+                onRefresh: _fetchUpdatedData,
132
+                child: _buildMainContent(),
133
+              )
134
+            : FavoritesPage(favoriteCards: favoriteCards),
135
+      ),
136
+      bottomNavigationBar: BottomNavigationBar(
137
+        currentIndex: _currentIndex,
138
+        selectedItemColor: Colors.white,
139
+        unselectedItemColor: Colors.white70,
140
+        backgroundColor: MyApp.primaryColor,
141
+        items: const [
142
+          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Inicio'),
143
+          BottomNavigationBarItem(icon: Icon(Icons.favorite), label: 'Favoritos'),
144
+        ],
145
+        onTap: (index) {
146
+          if (index == 1) _loadFavorites();
147
+          setState(() => _currentIndex = index);
148
+        },
149
+      ),
150
+    );
151
+  }
152
+
153
+  Widget _buildMainContent() {
154
+    return SafeArea(
155
+      child: Column(
156
+        children: [
157
+          Expanded(
158
+            child: ListView.builder(
159
+              itemCount: infoCards.isEmpty ? 1 : infoCards.length,
160
+              itemBuilder: (context, index) {
161
+                if (infoCards.isEmpty) {
162
+                  return _buildErrorWidget();
163
+                }
164
+                final card = infoCards[index];
165
+                return CustomInfoCard(
166
+                  imagePath: "assets/icons/${card['image']}",
167
+                  title: card['title'],
168
+                  subtitle: card['subtitle'],
169
+                  texto: card['texto'],
170
+                );
171
+              },
172
+            ),
222 173
           ),
174
+        ],
175
+      ),
176
+    );
177
+  }
178
+
179
+  Widget _buildErrorWidget() {
180
+    return Card(
181
+      margin: const EdgeInsets.all(16),
182
+      color: Colors.white,
183
+      child: Padding(
184
+        padding: const EdgeInsets.all(16),
185
+        child: Column(
186
+          children: [
187
+            const Icon(Icons.warning_amber_rounded, size: 48, color: Colors.orange),
188
+            const SizedBox(height: 16),
189
+            const Text('No se pudo cargar la información', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
190
+            const SizedBox(height: 8),
191
+            const Text('Desliza hacia abajo para intentar de nuevo', textAlign: TextAlign.center),
192
+            const SizedBox(height: 16),
193
+            ElevatedButton(
194
+              onPressed: _fetchUpdatedData,
195
+              style: ElevatedButton.styleFrom(backgroundColor: MyApp.primaryColor),
196
+              child: const Text('Reintentar'),
197
+            ),
198
+          ],
223 199
         ),
224 200
       ),
225 201
     );
226 202
   }
227
-}
203
+}