diff --git a/app/lib/core/models/blog_model.dart b/app/lib/core/models/blog_model.dart index e69de29b..b97cb7d7 100644 --- a/app/lib/core/models/blog_model.dart +++ b/app/lib/core/models/blog_model.dart @@ -0,0 +1,49 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class BlogModel { + final String id; + final List tags; + final String title; + final String description; + final String memberID; + final Timestamp createdTime; + final Timestamp updatedTime; + final String image; + + BlogModel( + {required this.id, + required this.tags, + required this.title, + required this.description, + required this.memberID, + required this.createdTime, + required this.updatedTime, + required this.image}); + + // Converts a Firestore Document to a UserModel + factory BlogModel.fromDocument(Map doc, String id) { + return BlogModel( + id: id, + tags: doc['tags'], + title: doc['title'], + description: doc['description'], + memberID: doc['memberID'], + createdTime: doc['createdTime'], + updatedTime: doc['updatedTime'], + image: doc['image'], + ); + } + + // Converts a UserModel to a Firestore Document + Map toDocument() { + return {// The 'id' field is added here to prevent errors when creating a new post + 'tags': tags, + 'title': title, + 'description': description, + 'memberID': memberID, + 'createdTime': createdTime, + 'updatedTime': updatedTime, + 'image': image, + }; + } +} \ No newline at end of file diff --git a/app/lib/features/blog/blog_page_test.dart b/app/lib/features/blog/blog_page_test.dart new file mode 100644 index 00000000..d9712a59 --- /dev/null +++ b/app/lib/features/blog/blog_page_test.dart @@ -0,0 +1,120 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:app/core/models/blog_model.dart'; +import 'package:provider/provider.dart'; +import 'blog_viewmodel.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; + +class BlogPage extends StatefulWidget { + const BlogPage({Key? key}) : super(key: key); + + @override + BlogPageState createState() => BlogPageState(); +} + +class BlogPageState extends State { + HtmlEditorController controller = HtmlEditorController(); + final ScrollController _scrollController = ScrollController(); + String htmlContent = ''; + Future saveContent() async { + htmlContent = await controller.getText(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => BlogViewModel(), + child: Scaffold( + appBar: AppBar( + title: const Text('Profile'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Consumer( + builder: (context, blogViewModel, child) { + Provider.of(context, listen: false) + .fetchAll(); + final blogs = blogViewModel.blogPosts; + + return Column( + children: [ + HtmlEditor( + controller: controller, + htmlEditorOptions:const HtmlEditorOptions( + hint: "Insert the blog description here...", + //initalText: "text content initial, if any", + ), + otherOptions: const OtherOptions( + height: 200, + ), + ), + Expanded( + child: blogs.isEmpty + ? const Center( + child: Text( + 'No blog posts found. Click the button below to create one.'), + ) + : ListView.builder( + controller: _scrollController, + itemCount: blogs.length, + itemBuilder: (context, index) { + final blog = blogs[index]; + return ListTile( + title: Text(blog.title), + subtitle: Html(data: blog.description), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + blogViewModel.delete(blog.id); + }, + ), + ); + }, + ), + ), + ElevatedButton( + onPressed: () async { + await saveContent(); + BlogModel newBlog = BlogModel( + id: '1', // The 'id' should be generated by Firestore when creating a new post + tags: ['Flutter', 'Firebase'], + title: 'New Blog Post', + description: htmlContent, + memberID: 'member123', + createdTime: Timestamp.now(), + updatedTime: Timestamp.now(), + image: 'url_to_image', + ); + // Call createBlog method from BlogViewModel + Provider.of(context, listen: false) + .createBlog((blogs.length+1).toString(), newBlog); + }, + child: const Text('Create Blog'), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + await saveContent(); + // Call createBlog method from BlogViewModel + Provider.of(context, listen: false) + .updateBlog("1", htmlContent); + }, + child: const Text('Update Blog 1 Description'), + ), + const SizedBox(height: 20), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/features/blog/blog_viewmodel.dart b/app/lib/features/blog/blog_viewmodel.dart index e69de29b..b5bd1781 100644 --- a/app/lib/features/blog/blog_viewmodel.dart +++ b/app/lib/features/blog/blog_viewmodel.dart @@ -0,0 +1,75 @@ +import 'package:flutter/foundation.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:app/core/models/blog_model.dart'; + +class BlogViewModel with ChangeNotifier { + List _blogPosts = []; + List get blogPosts => _blogPosts; + + BlogModel? _blogPost; + BlogModel? get blogPost => _blogPost; + + Future fetchAll() async { + // Fetch all documents from Firestore + var querySnapshot = + await FirebaseFirestore.instance.collection('blogPosts').get(); + // Map the documents to BlogModel instances + _blogPosts = querySnapshot.docs + .map((doc) => BlogModel.fromDocument(doc.data(), doc.id)) + .toList(); + // Notify listeners that the blog posts have been updated + notifyListeners(); + } + + Future fetchById(String id) async { + var doc = + await FirebaseFirestore.instance.collection('blogPosts').doc(id).get(); + if (doc.exists) { + _blogPost = BlogModel.fromDocument(doc.data()!, doc.id); + } + notifyListeners(); + } + + Future delete(String id) async { + await FirebaseFirestore.instance.collection('blogPosts').doc(id).delete(); + // Remove the blog post from the list + _blogPosts.removeWhere((blog) => blog.id == id); + // Notify listeners that the blog posts have been updated + notifyListeners(); + } + + Future createBlog(String id, BlogModel blog) async { + await FirebaseFirestore.instance + .collection('blogPosts') + .doc(id) + .set(blog.toDocument()); + notifyListeners(); + } + + Future updateBlog(String id, String description) async { + var doc = + await FirebaseFirestore.instance.collection('blogPosts').doc(id).get(); + if (doc.exists) { + BlogModel currentBlog = BlogModel.fromDocument(doc.data()!, doc.id); + BlogModel updatedBlog = BlogModel( + id: currentBlog.id, + tags: currentBlog.tags, + title: currentBlog.title, + description: description, // update the description + memberID: currentBlog.memberID, + createdTime: currentBlog.createdTime, + updatedTime: Timestamp.now(), // update the updatedTime + image: currentBlog.image, + ); + await FirebaseFirestore.instance + .collection('blogPosts') + .doc(id) + .set(updatedBlog.toDocument()); + int indexToUpdate = _blogPosts.indexWhere((blog) => blog.id == id); + if (indexToUpdate != -1) { + _blogPosts[indexToUpdate] = updatedBlog; + } + } + notifyListeners(); + } +} \ No newline at end of file diff --git a/app/lib/main.dart b/app/lib/main.dart index 4ed8ea75..f765c0e4 100755 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,6 +1,7 @@ import 'package:app/app/app_routes.dart'; import 'package:app/features/home/home_page.dart'; import 'package:app/features/profile/profile_page.dart'; +import 'package:app/features/blog/blog_page_test.dart'; import 'package:app/features/team/team_page.dart'; import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; @@ -26,7 +27,7 @@ class MyApp extends StatelessWidget { ); } } -enum AppTab { Home, Notification, Team, Profile } +enum AppTab { Home, Blog, Team, Profile } class MainScaffold extends StatefulWidget { const MainScaffold({super.key}); @@ -53,10 +54,10 @@ class _MainScaffoldState extends State { icon: Icon(Icons.home), label: 'Home', ), - // BottomNavigationBarItem( - // icon: Icon(Icons.notifications), - // label: 'Notifications', - // ), + BottomNavigationBarItem( + icon: Icon(Icons.notifications), + label: 'Blog', + ), BottomNavigationBarItem( icon: Icon(Icons.group), label: 'Team', @@ -77,8 +78,8 @@ class _MainScaffoldState extends State { switch (_selectedTab) { case AppTab.Home: return const HomePage(); - // case AppTab.Notification: - // return const NotificationsPage(); + case AppTab.Blog: + return const BlogPage(); case AppTab.Team: return const TeamPage(); case AppTab.Profile: diff --git a/app/pubspec.lock b/app/pubspec.lock index 221279fa..cc251e04 100755 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -72,7 +72,15 @@ packages: sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" + csslib: + dependency: transitive + description: + name: csslib + sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" + url: "https://pub.dev" + source: hosted + version: "0.17.3" cupertino_icons: dependency: "direct main" description: @@ -89,6 +97,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file_picker: + dependency: transitive + description: + name: file_picker + sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + url: "https://pub.dev" + source: hosted + version: "5.5.0" firebase_auth: dependency: "direct main" description: @@ -137,11 +161,91 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + flex_color_picker: + dependency: transitive + description: + name: flex_color_picker + sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c + url: "https://pub.dev" + source: hosted + version: "3.3.0" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + url: "https://pub.dev" + source: hosted + version: "1.4.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" + url: "https://pub.dev" + source: hosted + version: "3.0.0-beta.2" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350 + url: "https://pub.dev" + source: hosted + version: "5.8.0" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + url: "https://pub.dev" + source: hosted + version: "5.4.1" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_lints: dependency: "direct dev" description: @@ -224,6 +328,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.12.0+2" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + html_editor_enhanced: + dependency: "direct main" + description: + name: html_editor_enhanced + sha256: c2a0d0a50970fe4aa565e79e57081c8a69c3b2e94162e8f1df3e1b750c7aeb5e + url: "https://pub.dev" + source: hosted + version: "2.5.1" http: dependency: transitive description: @@ -240,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + infinite_listview: + dependency: transitive + description: + name: infinite_listview + sha256: f6062c1720eb59be553dfa6b89813d3e8dd2f054538445aaa5edaddfa5195ce6 + url: "https://pub.dev" + source: hosted + version: "1.1.0" intl: dependency: transitive description: @@ -264,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" local_auth: dependency: "direct main" description: @@ -336,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + numberpicker: + dependency: transitive + description: + name: numberpicker + sha256: "4c129154944b0f6b133e693f8749c3f8bfb67c4d07ef9dcab48b595c22d1f156" + url: "https://pub.dev" + source: hosted + version: "2.1.2" path: dependency: transitive description: @@ -344,6 +488,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" plugin_platform_interface: dependency: transitive description: @@ -352,6 +504,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: adf7a637f97c077041d36801b43be08559fd4322d2127b3f20bb7be1b9eebc22 + url: "https://pub.dev" + source: hosted + version: "0.9.3+7" provider: dependency: "direct main" description: @@ -437,6 +597,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" sdks: - dart: ">=3.0.0-0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index ce4a6cd8..378c36fc 100755 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: local_auth: ^2.1.6 provider: ^6.0.5 flutter_profile_picture: ^2.0.0 + html_editor_enhanced: ^2.5.1 + flutter_html: ^3.0.0-beta.2 dev_dependencies: flutter_test: