diff --git a/lib/controllers/blog_controller.dart b/lib/controllers/blog_controller.dart index 554ec2e..e292152 100644 --- a/lib/controllers/blog_controller.dart +++ b/lib/controllers/blog_controller.dart @@ -1,75 +1,88 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:cloudinary_api/uploader/cloudinary_uploader.dart'; -import 'package:cloudinary_url_gen/cloudinary.dart'; +import 'package:cloudinary_public/cloudinary_public.dart'; +import 'package:communityapp/models/blog_model.dart'; import 'package:communityapp/views/learning/blogs/blogpage.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:logger/logger.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:get/get.dart'; class BlogController extends GetxController { final Logger log = Logger(); // Logger instance - RxBool _isLoading = false.obs; - bool get isLoading => _isLoading.value; - - RxBool _isUploadingImage = false.obs; - bool get isUploadingImage => _isUploadingImage.value; + RxBool isLoading = false.obs; final titleController = TextEditingController().obs; final contentController = TextEditingController().obs; - Rx localImage = Rx(null); - RxString imageUrl = ''.obs; - void changeLoad() { - _isLoading.value = !_isLoading.value; - } + var localImage = Rx(null); // For mobile + var webImageBytes = Rx(null); // For web + var imageUrl = RxString(""); // Stores uploaded image URL + var isUploadingImage = false.obs; // Uploading status - // PICK IMAGE AND UPLOAD TO CLOUDINARY - Future getImage() async { - log.i("🔑 Loaded Cloudinary API Key: ${dotenv.env['CloudinaryApi']}"); + final CloudinaryPublic cloudinary = + CloudinaryPublic('daj7vxuyb', 'nehi1ybz', cache: false); + Future getImage() async { final ImagePicker picker = ImagePicker(); - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, imageQuality: 60, maxWidth: 150, maxHeight: 600); + XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image != null) { - final imageFile = File(image.path); - localImage.value = imageFile; - _isUploadingImage.value = true; // Mark image as uploading - - var cloudinary = Cloudinary.fromStringUrl( - 'cloudinary://239118281366527:${dotenv.env['CloudinaryApi']}@daj7vxuyb'); - - try { - log.i("📤 Uploading image..."); - final response = await cloudinary.uploader().upload(imageFile); - - if (response != null && response.data != null) { - if (response.data!.secureUrl != null) { - imageUrl.value = response.data!.secureUrl!; - log.i("✅ Image Uploaded Successfully: ${imageUrl.value}"); - } else { - log.e("❌ Upload failed, secureUrl is null"); - Get.snackbar("Upload Error", "Failed to get image URL from Cloudinary."); - } - } else { - log.e("❌ Upload failed, response data is null"); - Get.snackbar("Upload Error", "Cloudinary response is empty."); - } - } catch (e) { - log.e("🚨 Cloudinary Upload Error: $e"); - Get.snackbar("Upload Error", "Something went wrong while uploading."); - } finally { - _isUploadingImage.value = false; // Upload complete + if (kIsWeb) { + webImageBytes.value = await image.readAsBytes(); + } else { + localImage.value = File(image.path); } + update(); // Notify UI + await uploadImage(); } else { log.w("❌ No image selected"); } } + Future uploadImage() async { + isUploadingImage.value = true; + update(); + + try { + log.i("📤 Uploading image..."); + + CloudinaryResponse response; + + if (kIsWeb && webImageBytes.value != null) { + response = await cloudinary.uploadFile( + CloudinaryFile.fromByteData( + ByteData.view(webImageBytes.value!.buffer), + resourceType: CloudinaryResourceType.Image, + identifier: + 'web_upload_${DateTime.now().millisecondsSinceEpoch}'), + ); + } else if (!kIsWeb && localImage.value != null) { + response = await cloudinary.uploadFile( + CloudinaryFile.fromFile(localImage.value!.path, + resourceType: CloudinaryResourceType.Image), + ); + } else { + log.e("❌ No image found for upload."); + Get.snackbar("Upload Error", "No image selected."); + return; + } + + imageUrl.value = response.secureUrl; + log.i("✅ Image Uploaded Successfully: ${imageUrl.value}"); + } catch (e) { + log.e("🚨 Cloudinary Upload Error: $e"); + Get.snackbar("Upload Error", "Something went wrong while uploading."); + } finally { + isUploadingImage.value = false; + update(); + } + } + // SUBMIT POST TO FIREBASE Future submitPost() async { log.i("🚀 Submitting Post..."); @@ -84,7 +97,7 @@ class BlogController extends GetxController { log.w("❌ Submission Failed: Some fields are empty."); return; } - if ( content.isEmpty ) { + if (content.isEmpty) { Get.snackbar("Error", " Content is required."); log.w("❌ Submission Failed: Some fields are empty."); return; @@ -95,25 +108,33 @@ class BlogController extends GetxController { return; } - - // Check if image is still uploading - if (_isUploadingImage.value) { + if (isUploadingImage.value) { Get.snackbar("Uploading", "Please wait for image to finish uploading."); log.w("⏳ Waiting for image upload to complete..."); return; } - changeLoad(); + isUploadingImage.value = !isUploadingImage.value; try { log.i("📡 Uploading Data to Firebase..."); - await FirebaseFirestore.instance.collection('blogPosts').add({ - 'title': title, - 'content': content, - 'image': uploadedImageUrl, - 'timestamp': FieldValue.serverTimestamp(), - }); + + BlogModel blogModel = BlogModel( + title: title, + content: content, + author: FirebaseAuth.instance.currentUser?.displayName ?? "Anonymous", + imageUrl: uploadedImageUrl, + createdAt: DateTime.now().toIso8601String()); + + final blogRef = FirebaseFirestore.instance + .collection('blogPosts') + .withConverter( + fromFirestore: (snapshot, _) => + BlogModel.fromJson(snapshot.data() as Map), + toFirestore: (blog, _) => blog.toJson(), + ); + await blogRef.doc().set(blogModel); log.i("✅ Post Uploaded Successfully!"); @@ -122,6 +143,7 @@ class BlogController extends GetxController { contentController.value.clear(); imageUrl.value = ''; localImage.value = null; + webImageBytes.value = null; // 🔄 Navigate back after submission Get.snackbar("Success", "Post uploaded successfully!"); @@ -131,6 +153,20 @@ class BlogController extends GetxController { Get.snackbar("Error", "Failed to upload post."); } - changeLoad(); + isUploadingImage.value = !isUploadingImage.value; + } + + Stream> getAllBlogPostsStream() { + final blogRef = FirebaseFirestore.instance + .collection('blogPosts') + .orderBy('createdAt', descending: true) + .withConverter( + fromFirestore: (snapshot, _) => + BlogModel.fromJson(snapshot.data() as Map), + toFirestore: (blog, _) => blog.toJson(), + ); + + return blogRef.snapshots().map((querySnapshot) => + querySnapshot.docs.map((doc) => doc.data()).toList()); } } diff --git a/lib/models/blog_model.dart b/lib/models/blog_model.dart new file mode 100644 index 0000000..8b47417 --- /dev/null +++ b/lib/models/blog_model.dart @@ -0,0 +1,54 @@ +class BlogModel { + BlogModel({ + required this.title, + required this.content, + required this.author, + required this.imageUrl, + required this.createdAt, + }); + + final String? title; + final String? content; + final String? author; + final String? imageUrl; + final String? createdAt; + + BlogModel copyWith({ + String? title, + String? content, + String? author, + String? imageUrl, + String? createdAt, + }) { + return BlogModel( + title: title ?? this.title, + content: content ?? this.content, + author: author ?? this.author, + imageUrl: imageUrl ?? this.imageUrl, + createdAt: createdAt ?? this.createdAt, + ); + } + + factory BlogModel.fromJson(Map json) { + return BlogModel( + title: json["title"], + content: json["content"], + author: json["author"], + imageUrl: json["image"], + createdAt: json["createdAt"], + ); + } + + Map toJson() => { + "title": title, + "content": content, + "author": author, + "image": imageUrl, + "createdAt": createdAt, + }; + + @override + String toString() { + return "$title, $content, $author, $imageUrl, $createdAt, "; + } +} diff --git a/lib/views/learning/blogs/blogpage.dart b/lib/views/learning/blogs/blogpage.dart index f9760e9..cf1062f 100644 --- a/lib/views/learning/blogs/blogpage.dart +++ b/lib/views/learning/blogs/blogpage.dart @@ -1,14 +1,18 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:communityapp/controllers/blog_controller.dart'; +import 'package:communityapp/models/blog_model.dart'; import 'package:communityapp/views/learning/blogs/infoPage.dart'; -import 'package:communityapp/views/learning/blogs/postPage.dart'; +import 'package:communityapp/views/learning/blogs/post_page.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; +import 'package:http/http.dart'; import 'package:intl/intl.dart'; +import 'detailed_post_view.dart'; + class Blog_Page extends StatefulWidget { const Blog_Page({super.key}); @@ -21,49 +25,41 @@ class _Blog_PageState extends State { double height = Get.height; String truncateWithEllipsis(String text, int cutoff) { - return (text.length > cutoff) ? '${text.substring(0, cutoff)}...' : text; -} + return (text.length > cutoff) ? '${text.substring(0, cutoff)}...' : text; + } + // initalising controller BlogController blogController = Get.put(BlogController()); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Color(0xFF110E2B), - appBar: appBar(), - body: Stack( - children: [ - Positioned( - right: 0, - top: 0, - child: SvgPicture.asset('assets/svgs/box.svg'), - ), - Positioned( - right: 0, - top: height * 0.13, - child: SvgPicture.asset('assets/svgs/box.svg'), - ), - Positioned( - top: height * 0.4, - left: 0, - child: SvgPicture.asset('assets/svgs/rectangle.svg'), - ), - - - - - - Column( - + body: Stack(children: [ + Positioned( + right: 0, + top: 0, + child: SvgPicture.asset('assets/svgs/box.svg'), + ), + Positioned( + right: 0, + top: height * 0.13, + child: SvgPicture.asset('assets/svgs/box.svg'), + ), + Positioned( + top: height * 0.4, + left: 0, + child: SvgPicture.asset('assets/svgs/rectangle.svg'), + ), + Column( children: [ Expanded(child: buildBlock()), ], ), - ] - ), + ]), floatingActionButton: FloatingActionButton( onPressed: () { - Get.to(post_Page()); + Get.to(PostPage()); }, tooltip: 'Add Blog', splashColor: Colors.blue, @@ -80,16 +76,13 @@ class _Blog_PageState extends State { PreferredSizeWidget appBar() { return PreferredSize( - preferredSize: Size.fromHeight(height * 0.2), child: Container( decoration: BoxDecoration( - color: Color(0xFF110E2B), + color: Color(0xFF110E2B), ), - padding: EdgeInsets.only( top: height * 0.05, left: width * 0.03, right: width * 0.02), - width: width * 0.9, height: height * 0.125, child: Row( @@ -128,95 +121,106 @@ class _Blog_PageState extends State { } Widget buildBlock() { - return - StreamBuilder( - stream: FirebaseFirestore.instance - .collection('blogPosts') - .orderBy('timestamp', descending: true) - .snapshots(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center( - child: CircularProgressIndicator(), - ); - } - if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { - return const Center(child: Text("No posts yet!")); - } - return ListView.builder( - padding: EdgeInsets.all(height * 0.01), - itemCount: snapshot.data!.docs.length, - itemBuilder: (context, index) { - final doc = snapshot.data!.docs[index]; - // String content = doc['content'] ?? ''; - String title=doc['title'] ?? ''; - + return StreamBuilder>( + stream: blogController.getAllBlogPostsStream(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } - var timestamp = doc['timestamp']; - String formattedTimestamp = ''; - - if (timestamp != null) { - // Format the date and time up to seconds - DateFormat dateFormat = DateFormat('dd/MM/yyyy'); - formattedTimestamp = dateFormat.format(timestamp.toDate()); - formattedTimestamp='Posted: '+formattedTimestamp; - } - - return Card( - margin: EdgeInsets.symmetric(vertical: height*0.024,horizontal: width*0.02), - child: Padding( - padding: const EdgeInsets.all(14), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text("No posts yet!")); + } - // Image - - Expanded( - child: SizedBox( - - width: width*0.3, - height: height*0.2, - // used for providing shape to images - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(15)), - child: Image.network(doc['image'] ?? FlutterLogo( - size: height*0.2, - ), + final blogPosts = snapshot.data!; + + return ListView.builder( + padding: const EdgeInsets.all(10), + itemCount: blogPosts.length, + itemBuilder: (context, index) { + final blog = blogPosts[index]; + + // Ensure non-null values + final String title = blog.title ?? 'Untitled'; + final String author = blog.author ?? 'Anonymous'; + final String imageUrl = blog.imageUrl ?? ''; + final String content = blog.content ?? ''; + final String createdAt = blog.createdAt ?? ''; + + // Format timestamp + String formattedTimestamp = 'Unknown Date'; + if (createdAt.isNotEmpty) { + try { + DateTime dateTime = DateTime.parse(createdAt); + formattedTimestamp = + 'Posted: ${DateFormat('dd/MM/yyyy').format(dateTime)}'; + } catch (e) { + formattedTimestamp = 'Invalid Date'; + } + } + + return GestureDetector( + onTap: () { + Get.to(() => DetailedPostView(blogModel: blog)); + }, + child: Card( + margin: EdgeInsets.symmetric( + vertical: height * 0.024, horizontal: width * 0.02), + child: Padding( + padding: const EdgeInsets.all(14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: imageUrl.isNotEmpty + ? Image.network( + imageUrl, + width: width * 0.3, + height: height * 0.2, fit: BoxFit.cover, - ))), - ), - SizedBox(width: width*0.04,), - // Markdown-formatted Content - Expanded( - child: Container( - height: height*0.2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - RichText( - maxLines: 4, // Prevents overflow + errorBuilder: (context, error, stackTrace) => + FlutterLogo( + size: height * 0.2), // Error fallback + ) + : FlutterLogo( + size: height * 0.2), // Default placeholder + ), + SizedBox(width: width * 0.04), + + // Content + Expanded( + child: SizedBox( + height: height * 0.2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Title (Markdown formatted) + RichText( + maxLines: 4, overflow: TextOverflow.ellipsis, text: TextSpan( - children: parseMarkdownText(title), - style: TextStyle( + children: parseMarkdownText(title), + style: const TextStyle( fontSize: 22, color: Colors.black, fontWeight: FontWeight.w800, - ), - ), + ), + ), ), - - Text(FirebaseAuth.instance.currentUser!.displayName ?? 'Anonymous', + + // Author Name + Text( + author, style: const TextStyle( - fontSize: 15, - color: Colors.black, - fontWeight: FontWeight.w500), + fontSize: 15, + color: Colors.black, + fontWeight: FontWeight.w500), ), - - - // timestamp view + + // Timestamp Align( alignment: Alignment.bottomRight, child: Text( @@ -227,40 +231,42 @@ class _Blog_PageState extends State { fontWeight: FontWeight.w500), ), ), - ], - ), - ), - ) - ], - ), - )); - }, + ], + ), + ), + ) + ], + ), + ), + ), ); - }); - + }, + ); + }, + ); } // code for converting markdown to text List parseMarkdownText(String text) { - List spans = []; - RegExp exp = RegExp(r"\*\*(.*?)\*\*"); // Match bold (**text**) - int lastIndex = 0; + List spans = []; + RegExp exp = RegExp(r"\*\*(.*?)\*\*"); // Match bold (**text**) + int lastIndex = 0; - for (Match match in exp.allMatches(text)) { - if (match.start > lastIndex) { - spans.add(TextSpan(text: text.substring(lastIndex, match.start))); + for (Match match in exp.allMatches(text)) { + if (match.start > lastIndex) { + spans.add(TextSpan(text: text.substring(lastIndex, match.start))); + } + spans.add(TextSpan( + text: match.group(1), + style: TextStyle(fontWeight: FontWeight.bold), // Make bold + )); + lastIndex = match.end; + } + if (lastIndex < text.length) { + spans.add(TextSpan(text: text.substring(lastIndex))); } - spans.add(TextSpan( - text: match.group(1), - style: TextStyle(fontWeight: FontWeight.bold), // Make bold - )); - lastIndex = match.end; - } - if (lastIndex < text.length) { - spans.add(TextSpan(text: text.substring(lastIndex))); - } - return spans; -} + return spans; + } } diff --git a/lib/views/learning/blogs/detailed_post_view.dart b/lib/views/learning/blogs/detailed_post_view.dart new file mode 100644 index 0000000..51f64ce --- /dev/null +++ b/lib/views/learning/blogs/detailed_post_view.dart @@ -0,0 +1,191 @@ +import 'package:communityapp/models/blog_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; + +class DetailedPostView extends StatelessWidget { + DetailedPostView({super.key, required this.blogModel}); + + final BlogModel blogModel; + @override + Widget build(BuildContext context) { + double width = Get.width; + double height = Get.height; + + return Scaffold( + backgroundColor: Color(0xFF110E2B), + appBar: appBar(), + body: Stack( + children: [ + Positioned( + right: 0, + top: 0, + child: SvgPicture.asset('assets/svgs/box.svg'), + ), + Positioned( + right: 0, + top: height * 0.13, + child: SvgPicture.asset('assets/svgs/box.svg'), + ), + Positioned( + top: height * 0.4, + left: 0, + child: SvgPicture.asset('assets/svgs/rectangle.svg'), + ), + SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title + Text( + blogModel.title ?? "Title", + style: TextStyle( + fontSize: 24, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 16), + + // Image (Handles null imageUrl) + if (blogModel.imageUrl != null && + blogModel.imageUrl!.isNotEmpty) + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + blogModel.imageUrl!, + width: double.infinity, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) => Icon( + Icons.broken_image, + size: 80, + color: Colors.grey), + ), + ), + SizedBox(height: 16), + + // Description (Expands dynamically) + _descriptionUI() + ], + ), + ), + ], + ), + ); + } + + PreferredSizeWidget appBar() { + double width = Get.width; + double height = Get.height; + return PreferredSize( + preferredSize: Size.fromHeight(height * 0.2), + child: Container( + decoration: BoxDecoration( + color: Color(0xFF110E2B), + ), + padding: EdgeInsets.only( + top: height * 0.05, left: width * 0.03, right: width * 0.02), + width: width * 0.9, + height: height * 0.125, + child: Row( + children: [ + // menu option + SizedBox(width: width * 0.27), + Text( + 'HackSlash', + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + color: Colors.white), + ), + SizedBox(width: width * 0.18), + ], + ), + )); + } + + Widget _descriptionUI() { + return MarkdownBody( + data: blogModel.content ?? "No description available.", + styleSheet: MarkdownStyleSheet( + // 🌟 General Paragraph Text + p: const TextStyle(fontSize: 18, color: Colors.white70), + + // 🏆 Headers - Adjusted for Dark Blue Background + h1: const TextStyle( + fontSize: 26, fontWeight: FontWeight.bold, color: Colors.white), + h2: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFFB0E0E6)), // Light Blue + h3: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Color(0xFFFFD700)), // Gold/Yellow + h4: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF87CEFA)), // Sky Blue + h5: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF98FB98)), // Light Green + h6: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFFFFA07A)), // Light Salmon + + // ✅ Bold and Italic Styling + strong: + const TextStyle(fontWeight: FontWeight.bold, color: Colors.white), + em: const TextStyle(fontStyle: FontStyle.italic, color: Colors.white70), + + // 🔗 Hyperlink Styling + a: const TextStyle( + color: Color(0xFFFFA500), + decoration: TextDecoration.underline), // Orange Links + + // 📌 Lists + listBullet: const TextStyle( + fontSize: 18, color: Color(0xFF87CEFA)), // Light Sky Blue + + // 🗒️ Blockquote Styling + blockquote: TextStyle( + fontSize: 18, + fontStyle: FontStyle.italic, + color: Colors.grey[300], + decoration: TextDecoration.underline, + ), + + // 🖋️ Code Block Styling + code: const TextStyle( + fontSize: 16, + fontFamily: 'monospace', + color: Color(0xFFFFA07A), // Light Salmon + backgroundColor: Color(0xFF2E2E2E), // Dark Gray Background + ), + + // 📤 Horizontal Line (Divider) + horizontalRuleDecoration: BoxDecoration( + border: Border( + top: BorderSide(width: 1.5, color: Colors.white70), + ), + ), + + // 📎 Table Styling + tableHead: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + color: Colors.white, + ), + tableBody: const TextStyle(fontSize: 16, color: Colors.white70), + ), + ); + } +} diff --git a/lib/views/learning/blogs/postPage.dart b/lib/views/learning/blogs/post_page.dart similarity index 61% rename from lib/views/learning/blogs/postPage.dart rename to lib/views/learning/blogs/post_page.dart index 798e40b..be6bffc 100644 --- a/lib/views/learning/blogs/postPage.dart +++ b/lib/views/learning/blogs/post_page.dart @@ -1,15 +1,16 @@ import 'package:communityapp/controllers/blog_controller.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -class post_Page extends StatefulWidget { - const post_Page({super.key}); +class PostPage extends StatefulWidget { + const PostPage({super.key}); @override - State createState() => _PostPageState(); + State createState() => _PostPageState(); } -class _PostPageState extends State { +class _PostPageState extends State { BlogController blogController = Get.put(BlogController()); double height = Get.height; @@ -22,7 +23,7 @@ class _PostPageState extends State { body: Container( decoration: BoxDecoration( gradient: LinearGradient( - colors: [Color(0xFF2E3A59),Color(0xFF110E2B)], + colors: [Color(0xFF2E3A59), Color(0xFF110E2B)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), @@ -46,9 +47,9 @@ class _PostPageState extends State { return AppBar( centerTitle: true, backgroundColor: Color.fromARGB(255, 56, 50, 112), - title: Text("New Post", style: TextStyle(color: Colors.grey, fontSize: 22, fontWeight: FontWeight.w600)), - - + title: Text("New Post", + style: TextStyle( + color: Colors.grey, fontSize: 22, fontWeight: FontWeight.w600)), ); } @@ -57,9 +58,12 @@ class _PostPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _inputField("Title", "Enter Title...", blogController.titleController.value), + _inputField( + "Title", "Enter Title...", blogController.titleController.value), SizedBox(height: height * 0.02), - _inputField("Content", "Write content in Markdown...", blogController.contentController.value, maxLines: 5), + _inputField("Content", "Write content in Markdown...", + blogController.contentController.value, + maxLines: 5), SizedBox(height: height * 0.03), _imagePreview(), ], @@ -67,11 +71,17 @@ class _PostPageState extends State { ); } - Widget _inputField(String label, String hint, TextEditingController controller, {int maxLines = 1}) { + Widget _inputField( + String label, String hint, TextEditingController controller, + {int maxLines = 1}) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)), + Text(label, + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), SizedBox(height: 6), TextField( controller: controller, @@ -79,7 +89,6 @@ class _PostPageState extends State { cursorColor: Colors.white, style: TextStyle(color: Colors.white), decoration: InputDecoration( - hintText: hint, hintStyle: TextStyle(color: Colors.white54), filled: true, @@ -96,7 +105,17 @@ class _PostPageState extends State { Widget _imagePreview() { return Obx(() { - if (blogController.localImage.value != null) { + if (kIsWeb && blogController.webImageBytes.value != null) { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.memory( + blogController.webImageBytes.value!, + height: height * 0.25, + width: double.infinity, + fit: BoxFit.cover, + ), + ); + } else if (!kIsWeb && blogController.localImage.value != null) { return ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( @@ -107,7 +126,7 @@ class _PostPageState extends State { ), ); } else { - return SizedBox(); + return const SizedBox(); } }); } @@ -117,25 +136,34 @@ class _PostPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ FloatingActionButton( - onPressed: blogController.getImage, + onPressed: () async { + await blogController.getImage(); + }, backgroundColor: Colors.blueAccent, child: Icon(Icons.image, color: Colors.white), ), SizedBox( width: width * 0.7, child: Obx(() => ElevatedButton( - onPressed: blogController.isLoading ? null : blogController.submitPost, + onPressed: blogController.isLoading.value + ? null + : blogController.submitPost, style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, padding: EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), ), - child: blogController.isLoading + child: blogController.isLoading.value ? CircularProgressIndicator(color: Colors.white) - : Text("Submit", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold,color: Colors.white)), + : Text("Submit", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white)), )), ), ], ); } -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 5b4c150..34baf9b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,18 +50,18 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: @@ -138,10 +138,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -154,10 +154,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" cloud_firestore: dependency: "direct main" description: @@ -190,6 +190,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + cloudinary_public: + dependency: "direct main" + description: + name: cloudinary_public + sha256: "30c2aac5a31b468e5e955d2bbbac3eebcd1c66c9c7c2ffd75828606503bdda68" + url: "https://pub.dev" + source: hosted + version: "0.23.1" cloudinary_url_gen: dependency: "direct main" description: @@ -210,10 +218,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -270,6 +278,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.1" + dio: + dependency: transitive + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" dots_indicator: dependency: "direct main" description: @@ -290,10 +314,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: @@ -773,6 +797,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.1" + image_picker_web: + dependency: "direct main" + description: + name: image_picker_web + sha256: b5cf4faf66714f17b3e86b37a39d19743603163a08b968b28cadfc5df1dc2b75 + url: "https://pub.dev" + source: hosted + version: "4.0.0" image_picker_windows: dependency: transitive description: @@ -825,18 +857,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -897,10 +929,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -913,10 +945,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: "direct main" description: @@ -937,10 +969,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -1150,10 +1182,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -1166,18 +1198,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -1190,26 +1222,26 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" timeline_tile: dependency: "direct main" description: @@ -1358,10 +1390,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" watcher: dependency: transitive description: @@ -1435,5 +1467,5 @@ packages: source: hosted version: "9.1.1" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index c44a94a..e876c83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,8 @@ dependencies: flutter_markdown: ^0.7.6 youtube_player_flutter: ^9.1.1 flutter_svg: ^2.0.17 + image_picker_web: ^4.0.0 + cloudinary_public: ^0.23.1 dev_dependencies: