From 5377e6ae4e01c45576e9b9c9dc62e33ffb0ec4d9 Mon Sep 17 00:00:00 2001 From: Aman Gupta <–aman07112006@gmail.com> Date: Sun, 16 Feb 2025 17:58:58 +0530 Subject: [PATCH 1/6] AboutPageFinal --- lib/config/routes/routes.dart | 17 ++ lib/config/routes/routes_consts.dart | 1 + .../home/presentation/pages/about.dart | 262 ++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 lib/features/home/presentation/pages/about.dart diff --git a/lib/config/routes/routes.dart b/lib/config/routes/routes.dart index 364c0e9..d2e9aa6 100644 --- a/lib/config/routes/routes.dart +++ b/lib/config/routes/routes.dart @@ -168,7 +168,24 @@ class UhlLinkRouter { return MaterialPage( key: state.pageKey, child: const SettingsPage()); }), +<<<<<<< HEAD +======= + GoRoute( + name: UhlLinkRoutesNames.settingsPage, + path: '/settings', + pageBuilder: (context, state) { + return MaterialPage( + key: state.pageKey, child: const SettingsPage()); + }), + GoRoute( + name: UhlLinkRoutesNames.aboutPage, + path: '/about', + pageBuilder: (context, state) { + return MaterialPage( + key: state.pageKey, child: AboutPage()); + }), +>>>>>>> 4ffb060 (AboutPageUpdated) GoRoute( name: UhlLinkRoutesNames.jobDetailsPage, path: '/job_details/:job', diff --git a/lib/config/routes/routes_consts.dart b/lib/config/routes/routes_consts.dart index b3a7350..ae8d666 100644 --- a/lib/config/routes/routes_consts.dart +++ b/lib/config/routes/routes_consts.dart @@ -25,6 +25,7 @@ class UhlLinkRoutesNames { static const String jobPortalPage = 'job_portal_page'; static const String achievementsPage = 'achievements_page'; + // Profile static const String porsPage = 'pors_page'; static const String settingsPage = 'settings_page'; diff --git a/lib/features/home/presentation/pages/about.dart b/lib/features/home/presentation/pages/about.dart new file mode 100644 index 0000000..edd1d19 --- /dev/null +++ b/lib/features/home/presentation/pages/about.dart @@ -0,0 +1,262 @@ +import 'package:flutter/material.dart'; + + +class AboutPage extends StatelessWidget { + AboutPage({super.key}); + final leads = [ + { + "Abhijeet Jha": + "https://github.com/Aman071106/IITApp/raw/main2/ContributorImages/AbhijeetJha.jpg" + }, + { + "Gaurav Kushawaha": + "https://github.com/Gaurav-Kushwaha-1225/Gaurav-Kushwaha-1225/raw/main/IMG20231002114533.jpg" + }, + { + "Shubh Sahu": + "https://raw.githubusercontent.com/shubhxtech/addplayer/refs/heads/main/Screenshot%202025-02-13%20200538.png" + } + ]; + final designTeam = [ + { + "Shubh Sahu": + "https://raw.githubusercontent.com/shubhxtech/addplayer/refs/heads/main/Screenshot%202025-02-13%20200538.png" + }, + { + "Gaurav Kushawaha": + "https://github.com/Gaurav-Kushwaha-1225/Gaurav-Kushwaha-1225/raw/main/IMG20231002114533.jpg" + }, + { + "Shubham": + "https://uxwing.com/wp-content/themes/uxwing/download/peoples-avatars/man-user-circle-icon.png" + } + ]; + final developers = [ + { + "Shubh Sahu": + "https://raw.githubusercontent.com/shubhxtech/addplayer/refs/heads/main/Screenshot%202025-02-13%20200538.png" + }, + { + "Gaurav Kushawaha": + "https://github.com/Gaurav-Kushwaha-1225/Gaurav-Kushwaha-1225/raw/main/IMG20231002114533.jpg" + }, + { + "Aman Gupta": + "https://github.com/Aman071106/IITApp/raw/main2/ContributorImages/profilePicAman.jpg" + }, + { + "Harsh Yadav": + "https://github.com/Aman071106/IITApp/raw/main2/ContributorImages/profilePicHarsh.jpg" + }, + { + "Shivam Soni": + "https://github.com/RandomYapper/Assests/raw/main/self_photo.jpg" + }, + { + "Utkarsh Sahu": "https://github.com/Utkarsh-1-Sahu/Image/raw/main/Utk.jpg" + }, + { + "Ayush Raj": + "https://github.com/ayush18-pixel/cafeforvertex/raw/main/images/IMG_1173.JPG" + }, + { + "Anshika Goel": + "https://github.com/anshika476/Assests/raw/main/anshika.png" + }, + { + "Naman Bhatia": + "https://github.com/naman-bhatia-2006/project-1/raw/main/gitphoto.jpg" + }, + {"Kripa Kanodia": "https://github.com/cooper235/image/raw/main/image.jpg"}, + { + "Nishant": + "https://github.com/Nishant-coder-cpu/image/raw/main/IMG_my.JPG" + }, + {"Dhanad": "https://github.com/Blackcoat123/My-Photo/raw/main/dhanad.jpg"}, + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).cardColor, + title: Text("About", style: Theme.of(context).textTheme.bodyMedium), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 10, + ), + Text( + "This app is your one-stop solution for everything related to IIT Mandi. Designed for students, faculty, and visitors, it offers features to simplify campus life, including:", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: Theme.of(context).colorScheme.onError), + ), + const SizedBox( + height: 10, + ), + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: Theme.of(context).colorScheme.onError), + // Base style + children: const [ + TextSpan( + text: "- ", + ), + TextSpan( + text: "Event Updates", + style: TextStyle( + decoration: TextDecoration.underline, + ), + ), + TextSpan( + text: + ": Stay informed about upcoming academic and cultural events.", + ), + ], + ), + ), + const SizedBox( + height: 10, + ), + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: Theme.of(context).colorScheme.onError), + // Base style + children: const [ + TextSpan( + text: "- ", + ), + TextSpan( + text: "Navigation", + style: TextStyle( + decoration: TextDecoration.underline, + ), + ), + TextSpan( + text: + ": Explore the campus with interactive maps and directions.", + ), + ], + ), + ), + const SizedBox( + height: 10, + ), + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + color: Theme.of(context).colorScheme.onError), + // Base style + children: const [ + TextSpan( + text: "- ", + ), + TextSpan( + text: "Others", + style: TextStyle( + decoration: TextDecoration.underline, + ), + ), + TextSpan( + text: + ": Quick access to features like lost/found, buy/sell and other achievements updates."), + ], + ), + ), + ], + ), + const SizedBox( + height: 30, + ), + Text( + "Project Leads:", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 18, color: Theme.of(context).colorScheme.onError), + ), + const SizedBox( + height: 15, + ), + Contributors(leads), + Text( + "Developers: ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 18, color: Theme.of(context).colorScheme.onError), + ), + const SizedBox( + height: 15, + ), + Contributors(developers), + const SizedBox( + height: 15, + ), + Text( + "Design: ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 18, color: Theme.of(context).colorScheme.onError), + ), + const SizedBox( + height: 15, + ), + Contributors(designTeam), + ], + ), + ), + ), + ); + } + + Container Contributors(List> contributors) { + double h = contributors.length / 2 * 150; + if (contributors.length % 2 != 0) h += 110; + return Container( + // color: Colors.amber, + height: h, + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + itemCount: contributors.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2), + itemBuilder: (context, index) { + String name = contributors[index].keys.first; + return Column( + children: [ + CircleAvatar( + radius: 50, + child: ClipOval( + child: AspectRatio( + aspectRatio: 1, + child: Image.network( + fit: BoxFit.cover, contributors[index][name]!), + ), + ), + ), + Text( + name, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 15, + color: Theme.of(context).colorScheme.onError), + ) + ], + ); + }), + ); + } +} From 827140f791ef106c8cc1ff9c335645cb4a07874d Mon Sep 17 00:00:00 2001 From: Aman Gupta <–aman07112006@gmail.com> Date: Sun, 16 Feb 2025 18:26:04 +0530 Subject: [PATCH 2/6] AboutFinal --- lib/config/routes/routes.dart | 15 ++------------- lib/config/routes/routes_consts.dart | 1 + 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/config/routes/routes.dart b/lib/config/routes/routes.dart index d2e9aa6..6503c19 100644 --- a/lib/config/routes/routes.dart +++ b/lib/config/routes/routes.dart @@ -11,6 +11,7 @@ import 'package:uhl_link/features/authentication/presentation/pages/login.dart'; import 'package:uhl_link/features/authentication/presentation/pages/otp_verification_page.dart'; import 'package:uhl_link/features/authentication/presentation/pages/sign_up_page.dart'; import 'package:uhl_link/features/authentication/presentation/pages/update_password.dart'; +import 'package:uhl_link/features/home/presentation/pages/about.dart'; import 'package:uhl_link/features/home/presentation/pages/job_portal.dart'; import 'package:uhl_link/features/home/presentation/pages/home.dart'; import 'package:uhl_link/features/home/presentation/widgets/PORs_page.dart'; @@ -168,24 +169,12 @@ class UhlLinkRouter { return MaterialPage( key: state.pageKey, child: const SettingsPage()); }), -<<<<<<< HEAD - -======= - GoRoute( - name: UhlLinkRoutesNames.settingsPage, - path: '/settings', - pageBuilder: (context, state) { - return MaterialPage( - key: state.pageKey, child: const SettingsPage()); - }), GoRoute( name: UhlLinkRoutesNames.aboutPage, path: '/about', pageBuilder: (context, state) { - return MaterialPage( - key: state.pageKey, child: AboutPage()); + return MaterialPage(key: state.pageKey, child: AboutPage()); }), ->>>>>>> 4ffb060 (AboutPageUpdated) GoRoute( name: UhlLinkRoutesNames.jobDetailsPage, path: '/job_details/:job', diff --git a/lib/config/routes/routes_consts.dart b/lib/config/routes/routes_consts.dart index ae8d666..0040879 100644 --- a/lib/config/routes/routes_consts.dart +++ b/lib/config/routes/routes_consts.dart @@ -29,6 +29,7 @@ class UhlLinkRoutesNames { // Profile static const String porsPage = 'pors_page'; static const String settingsPage = 'settings_page'; + static const String aboutPage = 'about_page'; // Job portal static const String jobDetailsPage = 'job_details_page'; From 7c8731da519a30de8ab34b078d61bc8999bf4c95 Mon Sep 17 00:00:00 2001 From: Aman Gupta <–aman07112006@gmail.com> Date: Wed, 19 Feb 2025 01:25:21 +0530 Subject: [PATCH 3/6] FeedDetail page done --- .../home/presentation/pages/FeedDetail.dart | 230 ++++++++++++++++++ pubspec.yaml | 4 +- 2 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 lib/features/home/presentation/pages/FeedDetail.dart diff --git a/lib/features/home/presentation/pages/FeedDetail.dart b/lib/features/home/presentation/pages/FeedDetail.dart new file mode 100644 index 0000000..9d2bebe --- /dev/null +++ b/lib/features/home/presentation/pages/FeedDetail.dart @@ -0,0 +1,230 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:readmore/readmore.dart'; +import 'package:url_launcher/url_launcher.dart'; +//dependecies +//-readmore +//url launcher +//-cached_network_images + +class EventDetailPage extends StatefulWidget { + final List images; + final String hostname; + final String description; + final String registerLink; + + EventDetailPage( + {required this.images, + required this.hostname, + required this.description, + required this.registerLink}); + + @override + _EventDetailPageState createState() => _EventDetailPageState(); +} + +class _EventDetailPageState extends State { + int _currentCarouselIndex = 0; + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final textScale = MediaQuery.of(context).textScaleFactor; + + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).cardColor, + title: Text("Event Details", + style: Theme.of(context).textTheme.bodyMedium), + centerTitle: true, + ), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(screenWidth * 0.04), // 4% of screen width + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Carousel Section + Column( + children: [ + SizedBox( + height: screenHeight * 0.3, // 30% of screen height + child: CarouselSlider( + items: widget.images.map((img) { + return CachedNetworkImage( + imageUrl: img.replaceFirst('600x300', '800x400'), + fit: BoxFit.contain, + width: double.infinity, + progressIndicatorBuilder: (context, url, progress) => + Container( + height: screenHeight * 0.3, + child: Center( + child: CircularProgressIndicator( + value: progress.progress, + ), + ), + ), + errorWidget: (context, url, error) => Container( + color: Colors.grey.shade200, + height: screenHeight * 0.3, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.broken_image, + size: screenWidth * 0.1, + color: Colors.grey, + ), + Text('Failed to load image', + style: TextStyle( + fontSize: 12 * + textScale * + (screenWidth / 360), + color: Colors.grey)), + ], + ), + ), + ); + }).toList(), + options: CarouselOptions( + aspectRatio: 16 / 9, + autoPlay: true, + enlargeCenterPage: true, + viewportFraction: 1.0, + height: screenHeight / 2, + onPageChanged: (index, reason) { + setState(() { + _currentCarouselIndex = index; + }); + }, + ), + ), + ), + SizedBox(height: screenHeight * 0.02), // 2% of screen height + // Dots Indicator + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: widget.images.asMap().entries.map((entry) { + return Container( + width: screenWidth * 0.02, // 2% of screen width + height: screenWidth * 0.02, + margin: EdgeInsets.symmetric( + horizontal: screenWidth * 0.01), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity( + _currentCarouselIndex == entry.key ? 0.9 : 0.4, + ), + ), + ); + }).toList(), + ), + ], + ), + + SizedBox(height: screenHeight * 0.03), // 3% of screen height + + // Event Details + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Hosted by', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 14 * textScale * (screenWidth / 360), + ), + ), + SizedBox(height: screenHeight * 0.01), + Text( + widget.hostname, + style: TextStyle( + fontSize: 28 * textScale * (screenWidth / 360), + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + SizedBox(height: screenHeight * 0.03), + Card( + color: Theme.of(context).cardColor, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(screenWidth * 0.04), + ), + child: Container( + width: double.infinity, + padding: EdgeInsets.all(screenWidth * 0.04), + child: ReadMoreText( + widget.description, + trimLines: 3, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16 * textScale * (screenWidth / 360), + height: 1.5, + ), + moreStyle: TextStyle( + fontSize: 16 * textScale * (screenWidth / 360), + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + SizedBox(height: screenHeight * 0.04), + + // Register Button + Center( + child: SizedBox( + width: screenWidth * 0.9, // 90% of screen width + height: screenHeight * 0.06, // 6% of screen height + child: ElevatedButton( + onPressed: () async { + Uri uri = Uri.parse(widget.registerLink); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, + mode: LaunchMode.externalApplication); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: + Theme.of(context).dialogBackgroundColor, + behavior: SnackBarBehavior.floating, + content: Text( + "Could not launch", + style: (TextStyle( + color: Theme.of(context) + .scaffoldBackgroundColor)), + ), + duration: const Duration( + seconds: 2), // Duration before auto-dismiss + ), + ); + throw 'Could not launch'; + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(screenWidth * 0.04), + ), + ), + child: Text( + 'Register Now', + style: TextStyle( + color: Theme.of(context).primaryColor, + fontSize: 18 * textScale * (screenWidth / 360), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index b54994a..f98d0cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: mailer: ^6.4.1 pin_code_fields: ^8.0.1 cached_network_image: - + readmore: dev_dependencies: flutter_test: @@ -90,7 +90,7 @@ flutter: assets: - assets/images/ - assets/pdf/ - - institute.env + - assets/institute.env - assets/cafeteria.json # - images/a_dot_ham.jpeg From 90086eedbebda73ed903d52e353c2fdb1282003e Mon Sep 17 00:00:00 2001 From: Aman Gupta <–aman07112006@gmail.com> Date: Wed, 19 Feb 2025 01:25:21 +0530 Subject: [PATCH 4/6] DetailedFeed --- lib/features/home/presentation/pages/FeedDetail.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/features/home/presentation/pages/FeedDetail.dart b/lib/features/home/presentation/pages/FeedDetail.dart index 9d2bebe..2b76efe 100644 --- a/lib/features/home/presentation/pages/FeedDetail.dart +++ b/lib/features/home/presentation/pages/FeedDetail.dart @@ -228,3 +228,5 @@ class _EventDetailPageState extends State { ); } } + +//7c8731da519a30de8ab34b078d61bc8999bf4c95 \ No newline at end of file From 2578d70f1a289a0a8b74b1603517847475a83e89 Mon Sep 17 00:00:00 2001 From: Aman Gupta <–aman07112006@gmail.com> Date: Thu, 20 Feb 2025 22:27:08 +0530 Subject: [PATCH 5/6] Routes --- lib/config/routes/routes.dart | 15 +++++++++++++++ lib/config/routes/routes_consts.dart | 2 ++ 2 files changed, 17 insertions(+) diff --git a/lib/config/routes/routes.dart b/lib/config/routes/routes.dart index 6503c19..c195235 100644 --- a/lib/config/routes/routes.dart +++ b/lib/config/routes/routes.dart @@ -18,6 +18,7 @@ import 'package:uhl_link/features/home/presentation/widgets/PORs_page.dart'; import 'package:uhl_link/features/home/presentation/widgets/academic_calender_page.dart'; import 'package:uhl_link/features/home/presentation/widgets/achievements_page.dart'; import 'package:uhl_link/features/home/presentation/widgets/campus_map_page.dart'; +import 'package:uhl_link/features/home/presentation/widgets/feed_add_item_page.dart'; import 'package:uhl_link/features/home/presentation/widgets/job_details_page.dart'; import 'package:uhl_link/features/home/presentation/widgets/lost_found_add_item_page.dart'; import 'package:uhl_link/features/home/presentation/widgets/lost_found_page.dart'; @@ -122,6 +123,20 @@ class UhlLinkRouter { user: jsonDecode(state.pathParameters['user']!), )); }), + + // Feed + GoRoute( + name: UhlLinkRoutesNames.feedAddItemPage, + path: '/feed_add_item/:user', + pageBuilder: (context, state) { + return MaterialPage( + key: state.pageKey, + child: FeedAddItemPage( + user: jsonDecode(state.pathParameters['user']!), + )); + }), + + // Academics GoRoute( diff --git a/lib/config/routes/routes_consts.dart b/lib/config/routes/routes_consts.dart index 0040879..056ea06 100644 --- a/lib/config/routes/routes_consts.dart +++ b/lib/config/routes/routes_consts.dart @@ -19,6 +19,8 @@ class UhlLinkRoutesNames { static const String quickLinksPage = 'quick_links_page'; static const String lostFoundPage = 'lost_found_Page'; static const String lostFoundAddItemPage = 'lost_found_add_item_page'; + static const String feedAddItemPage = 'feed_add_item_page'; + static const String feedDetailPage = 'feeddetail_page'; // Academics static const String academicCalenderPage = 'academic_calender_page'; From 021b807b96c58a656dc6ab68c925a7559069c751 Mon Sep 17 00:00:00 2001 From: Aman Gupta <–aman07112006@gmail.com> Date: Thu, 20 Feb 2025 22:42:36 +0530 Subject: [PATCH 6/6] Feed Page Backend and Frontend --- .../feed_portal_data_sources.dart | 63 ++++ lib/features/home/data/models/feed_model.dart | 35 +++ .../feed_repository_impl.dart | 44 +++ .../home/domain/entities/feed_entity.dart | 34 +++ .../domain/repositories/feed_repository.dart | 7 + .../home/domain/usecases/add_feed_item.dart | 17 ++ .../home/domain/usecases/get_feed_item.dart | 12 + .../bloc/feed_page_bloc/feed_bloc.dart | 52 ++++ .../bloc/feed_page_bloc/feed_event.dart | 26 ++ .../bloc/feed_page_bloc/feed_state.dart | 38 +++ .../home/presentation/pages/explore.dart | 250 ++++++++++++---- .../home/presentation/pages/home.dart | 4 +- .../widgets/feed_add_item_page.dart | 270 ++++++++++++++++++ .../presentation/widgets/feeddetail_page.dart | 214 ++++++++++++++ lib/main.dart | 21 +- lib/widgets/form_field_widget.dart | 226 ++++++++++----- 16 files changed, 1177 insertions(+), 136 deletions(-) create mode 100644 lib/features/home/data/data_sources/feed_portal_data_sources.dart create mode 100644 lib/features/home/data/models/feed_model.dart create mode 100644 lib/features/home/data/repository_implementations/feed_repository_impl.dart create mode 100644 lib/features/home/domain/entities/feed_entity.dart create mode 100644 lib/features/home/domain/repositories/feed_repository.dart create mode 100644 lib/features/home/domain/usecases/add_feed_item.dart create mode 100644 lib/features/home/domain/usecases/get_feed_item.dart create mode 100644 lib/features/home/presentation/bloc/feed_page_bloc/feed_bloc.dart create mode 100644 lib/features/home/presentation/bloc/feed_page_bloc/feed_event.dart create mode 100644 lib/features/home/presentation/bloc/feed_page_bloc/feed_state.dart create mode 100644 lib/features/home/presentation/widgets/feed_add_item_page.dart create mode 100644 lib/features/home/presentation/widgets/feeddetail_page.dart diff --git a/lib/features/home/data/data_sources/feed_portal_data_sources.dart b/lib/features/home/data/data_sources/feed_portal_data_sources.dart new file mode 100644 index 0000000..e7734f7 --- /dev/null +++ b/lib/features/home/data/data_sources/feed_portal_data_sources.dart @@ -0,0 +1,63 @@ +import 'dart:developer'; + +import 'package:mongo_dart/mongo_dart.dart'; +import 'package:uhl_link/features/home/data/models/feed_model.dart'; + +class FeedDB { + static Db? db; + static DbCollection? collection; + + FeedDB(); + + static Future connect(String connectionURL) async { + db = await Db.create(connectionURL); + await db?.open(); + inspect(db); + collection = db?.collection('Feed'); + } + + // Get All Data Method + Future> getFeedItems() async { + try { + final items = await collection?.find().toList(); + if (items != null) { + log("inside portal-${items.toString()}"); + return items.map((item) => Feeditem.fromJson(item)).toList(); + } else { + return []; + } + } catch (e) { + log(e.toString()); + return []; + } + } + + // Add Lost Found Item + Future addFeeditem( + String host, String description, List images, String link) async { + final itemValues = { + '_id': ObjectId(), + 'host': host, + 'description': description, + 'images': images, + 'link': link + }; + try { + final id = await collection?.insertOne(itemValues); + if (id != null && id.document != null) { + return Feeditem.fromJson(id.document!); + } else { + return null; + } + } catch (e) { + log(e.toString()); + return null; + } + } + + // Close Connection + Future close() async { + await db?.close(); + print('Connection to MongoDB closed'); + } +} diff --git a/lib/features/home/data/models/feed_model.dart b/lib/features/home/data/models/feed_model.dart new file mode 100644 index 0000000..650e645 --- /dev/null +++ b/lib/features/home/data/models/feed_model.dart @@ -0,0 +1,35 @@ +import 'package:mongo_dart/mongo_dart.dart'; + +class Feeditem { + final String id; + final String host; + final String description; + final List images; + final String link; + + Feeditem( + {required this.id, + required this.host, + required this.description, + required this.images, + required this.link}); + + factory Feeditem.fromJson(Map json) { + return Feeditem( + id: (json['_id'] as ObjectId).oid, + host: json['host'], + description: json['description'], + images: List.from(json['images']), + link: json['link']); + } + + Map toMap() { + return { + 'id': id, + 'host': host, + 'description': description, + 'images': images, + 'link': link + }; + } +} diff --git a/lib/features/home/data/repository_implementations/feed_repository_impl.dart b/lib/features/home/data/repository_implementations/feed_repository_impl.dart new file mode 100644 index 0000000..b473f91 --- /dev/null +++ b/lib/features/home/data/repository_implementations/feed_repository_impl.dart @@ -0,0 +1,44 @@ +import 'package:uhl_link/features/home/data/data_sources/feed_portal_data_sources.dart'; +import 'package:uhl_link/features/home/domain/entities/feed_entity.dart'; +import '../../domain/repositories/feed_repository.dart'; + +class FeedRepositoryImpl implements FeedRepository { + final FeedDB feedDatabase; + FeedRepositoryImpl(this.feedDatabase); + + @override + Future> getFeedItems() async { + List allItems = []; + final items = await feedDatabase.getFeedItems(); + if (items.isNotEmpty) { + for (int i = 0; i < items.length; i++) { + allItems.add(FeedItemEntity( + id: items[i].id, + host: items[i].host, + description: items[i].description, + images: items[i].images, + link: items[i].link)); + } + return allItems; + } else { + return allItems; + } + } + + @override + Future addFeedItem( + String host, String description, List images, String link) async { + final item = + await feedDatabase.addFeeditem(host, description, images, link); + if (item != null) { + return FeedItemEntity( + id: item.id, + host: item.host, + description: item.description, + images: item.images, + link: link); + } else { + return null; + } + } +} diff --git a/lib/features/home/domain/entities/feed_entity.dart b/lib/features/home/domain/entities/feed_entity.dart new file mode 100644 index 0000000..a0991eb --- /dev/null +++ b/lib/features/home/domain/entities/feed_entity.dart @@ -0,0 +1,34 @@ +class FeedItemEntity { + final String id; + final String host; + final String description; + final List images; + final String link; + + FeedItemEntity( + {required this.id, + required this.host, + required this.description, + required this.images, + required this.link}); + + factory FeedItemEntity.fromJson(Map json) { + return FeedItemEntity( + id: json['id'], + host: json['host'], + description: json['description'], + images: List.from(json['images']), + link: json['link'], + ); + } + + Map toMap() { + return { + 'id': id, + 'host': host, + 'description': description, + 'images': images, + 'link': link + }; + } +} diff --git a/lib/features/home/domain/repositories/feed_repository.dart b/lib/features/home/domain/repositories/feed_repository.dart new file mode 100644 index 0000000..a0effdf --- /dev/null +++ b/lib/features/home/domain/repositories/feed_repository.dart @@ -0,0 +1,7 @@ +import 'package:uhl_link/features/home/domain/entities/feed_entity.dart'; + +abstract class FeedRepository { + Future> getFeedItems(); + Future addFeedItem( + String host, String description, List images, String link); +} diff --git a/lib/features/home/domain/usecases/add_feed_item.dart b/lib/features/home/domain/usecases/add_feed_item.dart new file mode 100644 index 0000000..444f295 --- /dev/null +++ b/lib/features/home/domain/usecases/add_feed_item.dart @@ -0,0 +1,17 @@ +import '../entities/feed_entity.dart'; +import '../repositories/feed_repository.dart'; + +class AddFeedItem { + final FeedRepository repository; + + AddFeedItem(this.repository); + + Future execute( + String host, + String description, + List images, + String link) { + return repository.addFeedItem( + host, description, images,link); + } +} diff --git a/lib/features/home/domain/usecases/get_feed_item.dart b/lib/features/home/domain/usecases/get_feed_item.dart new file mode 100644 index 0000000..fb82b28 --- /dev/null +++ b/lib/features/home/domain/usecases/get_feed_item.dart @@ -0,0 +1,12 @@ +import '../entities/feed_entity.dart'; +import '../repositories/feed_repository.dart'; + +class GetFeedItem { + final FeedRepository repository; + + GetFeedItem(this.repository); + + Future> execute() { + return repository.getFeedItems(); + } +} diff --git a/lib/features/home/presentation/bloc/feed_page_bloc/feed_bloc.dart b/lib/features/home/presentation/bloc/feed_page_bloc/feed_bloc.dart new file mode 100644 index 0000000..a0363fc --- /dev/null +++ b/lib/features/home/presentation/bloc/feed_page_bloc/feed_bloc.dart @@ -0,0 +1,52 @@ +import 'dart:developer'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:uhl_link/features/home/domain/entities/feed_entity.dart'; + +import '../../../domain/usecases/add_feed_item.dart'; +import '../../../domain/usecases/get_feed_item.dart'; + +part 'feed_event.dart'; +part 'feed_state.dart'; + +class FeedBloc extends Bloc { + final GetFeedItem getFeedItems; + final AddFeedItem addFeedItem; + + FeedBloc({required this.getFeedItems, required this.addFeedItem}) + : super(FeedInitial()) { + on(onGetFeedItemsEvent); + on(onAddFeedItemEvent); + } + + void onGetFeedItemsEvent( + GetFeedItemsEvent event, Emitter emit) async { + emit(FeedItemsLoading()); + try { + final items = await getFeedItems.execute(); + emit(FeedItemsLoaded(items: items)); + } catch (e) { + emit(FeedItemsLoadingError(message: "Error during fetching items : $e")); + } + } + + void onAddFeedItemEvent( + AddFeedItemEvent event, Emitter emit) async { + emit(FeedAddingItem()); + try { + final item = await addFeedItem.execute( + event.host, + event.description, + event.images, + event.link); + if (item != null) { + emit(FeedItemAdded(item: item)); + } else { + emit(const FeedItemsAddingError(message: "Error during adding item.")); + } + } catch (e) { + emit(FeedItemsAddingError(message: "Error during fetching items : $e")); + } + } +} diff --git a/lib/features/home/presentation/bloc/feed_page_bloc/feed_event.dart b/lib/features/home/presentation/bloc/feed_page_bloc/feed_event.dart new file mode 100644 index 0000000..a4ee1a0 --- /dev/null +++ b/lib/features/home/presentation/bloc/feed_page_bloc/feed_event.dart @@ -0,0 +1,26 @@ +part of 'feed_bloc.dart'; + +abstract class FeedEvent extends Equatable { + const FeedEvent(); + + @override + List get props => []; +} + +//GetFeedItemsEvent +class GetFeedItemsEvent extends FeedEvent { + const GetFeedItemsEvent(); +} + +class AddFeedItemEvent extends FeedEvent { + final String host; + final String description; + final List images; + final String link; + + const AddFeedItemEvent( + {required this.host, + required this.description, + required this.images, + required this.link}); +} diff --git a/lib/features/home/presentation/bloc/feed_page_bloc/feed_state.dart b/lib/features/home/presentation/bloc/feed_page_bloc/feed_state.dart new file mode 100644 index 0000000..066b8e7 --- /dev/null +++ b/lib/features/home/presentation/bloc/feed_page_bloc/feed_state.dart @@ -0,0 +1,38 @@ +part of 'feed_bloc.dart'; + +abstract class FeedState extends Equatable { + const FeedState(); + + @override + List get props => []; +} + +class FeedInitial extends FeedState {} + +class FeedItemsLoading extends FeedState {} + +class FeedItemsLoaded extends FeedState { + final List items; + + const FeedItemsLoaded({required this.items}); +} + +class FeedItemsLoadingError extends FeedState { + final String message; + + const FeedItemsLoadingError({required this.message}); +} + +class FeedAddingItem extends FeedState {} + +class FeedItemAdded extends FeedState { + final FeedItemEntity item; + + const FeedItemAdded({required this.item}); +} + +class FeedItemsAddingError extends FeedState { + final String message; + + const FeedItemsAddingError({required this.message}); +} diff --git a/lib/features/home/presentation/pages/explore.dart b/lib/features/home/presentation/pages/explore.dart index 073e230..de8f5be 100644 --- a/lib/features/home/presentation/pages/explore.dart +++ b/lib/features/home/presentation/pages/explore.dart @@ -1,77 +1,209 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:uhl_link/config/routes/routes_consts.dart'; -import 'package:uhl_link/features/home/presentation/widgets/card.dart'; +import 'package:uhl_link/features/home/domain/entities/feed_entity.dart'; +import 'package:uhl_link/features/home/presentation/bloc/feed_page_bloc/feed_bloc.dart'; -class Explore extends StatefulWidget { +//for detail +import 'package:uhl_link/features/home/presentation/widgets/feeddetail_page.dart'; + +class FeedPage extends StatefulWidget { final bool isGuest; final Map? user; - - const Explore({super.key, required this.isGuest, required this.user}); + const FeedPage({super.key, required this.isGuest, required this.user}); @override - State createState() => _ExploreState(); + State createState() => _FeedPageState(); } -class _ExploreState extends State { +class _FeedPageState extends State { + @override + void initState() { + super.initState(); + Future.delayed(const Duration(seconds: 3)); + BlocProvider.of(context).add(const GetFeedItemsEvent()); + } + + List feedItems = []; + @override Widget build(BuildContext context) { - List> items = [ - { - "text": "Mess Menu", - "icon": Icons.restaurant_menu_rounded, - "route": UhlLinkRoutesNames.messMenuPage, - 'pathParameters': {} - }, - { - "text": "Campus Map", - "icon": Icons.map_outlined, - "route": UhlLinkRoutesNames.campusMapPage, - 'pathParameters': {} - }, - { - "text": "Quick Links", - "icon": Icons.link_rounded, - "route": UhlLinkRoutesNames.quickLinksPage, - 'pathParameters': {} - }, - { - "text": "Lost/Found", - "icon": Icons.luggage_rounded, - "route": UhlLinkRoutesNames.lostFoundPage, - 'pathParameters': { - "isGuest": jsonEncode(widget.isGuest), - "user": jsonEncode(widget.user) - } - }, - ]; - return SingleChildScrollView( - scrollDirection: Axis.vertical, - physics: const ClampingScrollPhysics(), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - for (int i = 0; i < items.length; i++) - CardWidget( - text: items[i]['text'], - icon: items[i]['icon'], - onTap: () { - if (items[i]['pathParameters'] != null && - items[i]['pathParameters'].isNotEmpty) { - GoRouter.of(context).pushNamed( - items[i]['route'], - pathParameters: items[i]['pathParameters'], - ); - } else { - GoRouter.of(context).pushNamed(items[i]['route']); - } - }, + final screenSize = MediaQuery.of(context).size; + + return Stack( + children: [ + SizedBox( + width: screenSize.width, + height: screenSize.height, + + // resizeToAvoidBottomInset: false, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 5, vertical: 10), + child: BlocBuilder( + builder: (context, state) { + if (state is FeedItemsLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is FeedItemsLoaded) { + feedItems = state.items; + return ListView.separated( + physics: const ClampingScrollPhysics(), + primary: false, + //no of items + itemCount: feedItems.length, + itemBuilder: (BuildContext context, int index) { + //----------------------feedItemCard--------------------------------- + return feedItemCard(context, index, screenSize); + }, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox( + height: 20, + ); + }, + ); + } else if (state is FeedItemsLoadingError) { + return const Center( + child: Text('Failed to load feed items', + style: TextStyle(color: Colors.red))); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ))), + widget.isGuest + ? Container() + : Positioned( + right: 0, + bottom: 0, + child: FloatingActionButton.extended( + onPressed: () { + GoRouter.of(context).pushNamed( + UhlLinkRoutesNames.feedAddItemPage, + pathParameters: {"user": jsonEncode(widget.user)}); + }, + label: Text('Add Events', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 14)), + icon: Icon(Icons.add_box_rounded, + size: 20, color: Theme.of(context).colorScheme.onPrimary), + ), + ), + ], + ); + } + + Card feedItemCard(BuildContext context, int index, Size screenSize) { + return Card( + color: Theme.of(context).cardColor, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: MediaQuery.of(context).size.height * 0.02, + horizontal: MediaQuery.of(context).size.height * 0.02, + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, // Aligns content to the start + children: [ + CarouselSlider( + items: feedItems[index] + .images + .map((image) => ClipRRect( + borderRadius: BorderRadius.circular(15), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Theme.of(context) + .cardColor + .withOpacity(0.2), + width: 1.5, + )), + child: Image.file(File(image), + height: + MediaQuery.of(context).size.height * 0.25, + errorBuilder: (context, object, stacktrace) { + return Icon(Icons.error_outline_rounded, + size: 40, + color: Theme.of(context).primaryColor); + }, fit: BoxFit.cover), + ), + )) + .toList(), + options: CarouselOptions( + height: screenSize.height * 0.3, + autoPlay: true, + aspectRatio: 16 / 9, + viewportFraction: 1, + autoPlayInterval: const Duration(seconds: 5), + enlargeCenterPage: true), + ), + SizedBox(height: MediaQuery.of(context).size.height * 0.01), + Column( + crossAxisAlignment: CrossAxisAlignment + .start, // Ensures text aligns to the left + children: [ + const Text("Hosted By", + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.bold)), + SizedBox( + height: MediaQuery.of(context).size.height * 0.008), + Text(feedItems[index].host, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).primaryColor)), + SizedBox( + height: MediaQuery.of(context).size.height * 0.008), + Text( + feedItems[index].description.length > 60 + ? '${feedItems[index].description.substring(0, 60)}...' + : feedItems[index].description, + textAlign: TextAlign.start, + softWrap: true, + overflow: TextOverflow.visible, + style: Theme.of(context).textTheme.labelSmall, + ), + + SizedBox( + height: MediaQuery.of(context).size.height * + 0.01), // Space between text and View More + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EventDetailPage( + images: feedItems[index].images, + hostname: feedItems[index].host, + description: feedItems[index].description, + registerLink: feedItems[index].link))); + }, + child: Text( + "View more...", + textAlign: TextAlign.start, + softWrap: true, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.blue.shade800, + ), + ), + ), + ], + ), + ], ), - ], + ], + ), ), ); } diff --git a/lib/features/home/presentation/pages/home.dart b/lib/features/home/presentation/pages/home.dart index 2a59ba6..6d8b7c1 100644 --- a/lib/features/home/presentation/pages/home.dart +++ b/lib/features/home/presentation/pages/home.dart @@ -34,7 +34,7 @@ class _HomePageState extends State { Widget build(BuildContext context) { List homePages = [ Dashboard(isGuest: widget.isGuest, user: widget.user), - Explore(isGuest: widget.isGuest, user: widget.user), + FeedPage(isGuest: widget.isGuest, user: widget.user), Academics(isGuest: widget.isGuest, user: widget.user), const JobPortalPage(), Profile(isGuest: widget.isGuest, user: widget.user), @@ -84,7 +84,7 @@ class _HomePageState extends State { BottomNavigationBarItem( icon: Icon(Icons.dashboard_rounded), label: "Dashboard"), BottomNavigationBarItem( - icon: Icon(Icons.travel_explore_rounded), label: "Explore"), + icon: Icon(Icons.feed), label: "Feed"), BottomNavigationBarItem( icon: Icon(Icons.school_rounded), label: "Academics"), BottomNavigationBarItem( diff --git a/lib/features/home/presentation/widgets/feed_add_item_page.dart b/lib/features/home/presentation/widgets/feed_add_item_page.dart new file mode 100644 index 0000000..417b981 --- /dev/null +++ b/lib/features/home/presentation/widgets/feed_add_item_page.dart @@ -0,0 +1,270 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:uhl_link/features/authentication/domain/entities/user_entity.dart'; + +import '../../../../widgets/form_field_widget.dart'; +import '../../../../widgets/screen_width_button.dart'; +// +import '../bloc/feed_page_bloc/feed_bloc.dart'; + +class FeedAddItemPage extends StatefulWidget { + final Map user; + const FeedAddItemPage({super.key, required this.user}); + + @override + State createState() => _FeedAddItemPageState(); +} + +class _FeedAddItemPageState extends State { + bool imageSelected = false; + + //-name + final TextEditingController hostController = TextEditingController(); + final FocusNode hostFocusNode = FocusNode(); + String? errorHostValue; + final GlobalKey hostKey = GlobalKey(); + + //-description + final TextEditingController descriptionController = TextEditingController(); + final FocusNode descriptionFocusNode = FocusNode(); + String? errorDescriptionValue; + final GlobalKey descriptionKey = GlobalKey(); + + //-link + final TextEditingController linkController = TextEditingController(); + final FocusNode linkFocusNode = FocusNode(); + String? errorLinkValue; + final GlobalKey linkKey = GlobalKey(); + + // + List images = []; + final ImagePicker picker = ImagePicker(); + + Future pickImage() async { + try { + final List pickedImages = await picker.pickMultiImage(limit: 3); + + setState(() { + for (XFile image in pickedImages) { + images.add(image.path); + } + }); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Error uploading image."))); + } + } + + String? itemStatus; + + @override + Widget build(BuildContext context) { + final height = MediaQuery.of(context).size.height; + final width = MediaQuery.of(context).size.width; + final aspectRatio = MediaQuery.of(context).size.aspectRatio; + UserEntity user = UserEntity.fromJson(widget.user); + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).cardColor, + title: + Text("Add Events", style: Theme.of(context).textTheme.bodyMedium), + centerTitle: true, + ), + resizeToAvoidBottomInset: true, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: Stack( + children: [ + SingleChildScrollView( + reverse: true, + // physics: const ClampingScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () async { + await pickImage(); + }, + child: Container( + width: width - 40, + height: height * 0.3, + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: + const BorderRadius.all(Radius.circular(12)), + border: Border.all( + color: Theme.of(context).colorScheme.scrim, + width: 1.5, + ), + ), + child: Center( + child: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(12)), + child: images.isNotEmpty + ? SizedBox( + width: width - 40, + height: height * 0.3, + child: GridView.builder( + itemCount: images.length, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemBuilder: + (BuildContext context, int index) { + return Image.file( + File(images[index]), + fit: BoxFit.cover, + alignment: Alignment.center, + ); + }, + ), + ) + : Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.image_rounded, + color: Theme.of(context) + .colorScheme + .scrim, + size: aspectRatio * 150, + ), + const SizedBox(height: 10), + Text( + "Upload your image here", + style: TextStyle( + color: Theme.of(context) + .colorScheme + .scrim, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ), + SizedBox(height: height * 0.02), + //hostname-string + FormFieldWidget( + focusNode: hostFocusNode, + fieldKey: hostKey, + controller: hostController, + obscureText: false, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Name is required.'; + } + return null; + }, + keyboardType: TextInputType.emailAddress, + errorText: errorHostValue, + prefixIcon: Icons.person, + showSuffixIcon: false, + hintText: "Organisation Name", + textInputAction: TextInputAction.next, + ), + SizedBox(height: height * 0.02), + //description-string + FormFieldWidget( + isLargeField: true, + focusNode: descriptionFocusNode, + fieldKey: descriptionKey, + controller: descriptionController, + obscureText: false, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Description is required.'; + } + return null; + }, + keyboardType: TextInputType.text, + errorText: errorDescriptionValue, + prefixIcon: Icons.image_aspect_ratio, + showSuffixIcon: false, + hintText: "Event Description", + textInputAction: TextInputAction.done, + ), + SizedBox(height: height * 0.02), + //registrationlink-string + FormFieldWidget( + focusNode: linkFocusNode, + fieldKey: linkKey, + controller: linkController, + obscureText: false, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Name is required.'; + } + return null; + }, + keyboardType: TextInputType.emailAddress, + errorText: errorLinkValue, + prefixIcon: Icons.inbox, + showSuffixIcon: false, + hintText: "Registration Link", + textInputAction: TextInputAction.next, + ), + ], + ), + ), + Positioned( + bottom: 1, + child: ScreenWidthButton( + text: "Add Event", + buttonFunc: () { + final bool isHostValid = + hostKey.currentState!.validate(); + final bool isDescriptionValid = + descriptionKey.currentState!.validate(); + final bool isLinkValid = + linkKey.currentState!.validate(); + + if (images.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Please Upload Images", + style: + Theme.of(context).textTheme.labelSmall), + backgroundColor: Theme.of(context).cardColor)); + } + + if (isHostValid && + isLinkValid && + isDescriptionValid && + itemStatus != null) { + BlocProvider.of(context).add( + AddFeedItemEvent( + host: hostController.text, + description: descriptionController.text, + images: images, + link: linkController.text)); + + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text("Please Upload Images", + style: + Theme.of(context).textTheme.labelSmall), + backgroundColor: Theme.of(context).cardColor)); + GoRouter.of(context).pop(); + } + }, + // isLoading: userLoading, + )) + ], + ), + )), + ); + } +} diff --git a/lib/features/home/presentation/widgets/feeddetail_page.dart b/lib/features/home/presentation/widgets/feeddetail_page.dart new file mode 100644 index 0000000..8ffcb90 --- /dev/null +++ b/lib/features/home/presentation/widgets/feeddetail_page.dart @@ -0,0 +1,214 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:readmore/readmore.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class EventDetailPage extends StatefulWidget { + final List images; + final String hostname; + final String description; + final String registerLink; + + EventDetailPage({ + required this.images, + required this.hostname, + required this.description, + required this.registerLink, + }); + + @override + _EventDetailPageState createState() => _EventDetailPageState(); +} + +class _EventDetailPageState extends State { + int _currentCarouselIndex = 0; + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + final textScale = MediaQuery.of(context).textScaleFactor; + + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).cardColor, + title: Text("Event Details", + style: Theme.of(context).textTheme.bodyMedium), + centerTitle: true, + ), + body: Padding( + padding: EdgeInsets.all(screenWidth * 0.04), // 4% of screen width + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Carousel Section + Column( + children: [ + SizedBox( + height: screenHeight * 0.3, // 30% of screen height + child: CarouselSlider( + items: widget.images + .map((image) => ClipRRect( + borderRadius: BorderRadius.circular(15), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Theme.of(context) + .cardColor + .withOpacity(0.2), + width: 1.5, + )), + child: Image.file(File(image), + height: MediaQuery.of(context).size.height * + 0.25, + errorBuilder: + (context, object, stacktrace) { + return Icon(Icons.error_outline_rounded, + size: 40, + color: Theme.of(context).primaryColor); + }, fit: BoxFit.cover), + ), + )) + .toList(), + options: CarouselOptions( + height: screenHeight * 0.3, + autoPlay: true, + aspectRatio: 16 / 9, + viewportFraction: 1, + autoPlayInterval: const Duration(seconds: 5), + enlargeCenterPage: true), + ), + ), + SizedBox(height: screenHeight * 0.02), + // Dots Indicator + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: widget.images.asMap().entries.map((entry) { + return Container( + width: screenWidth * 0.02, + height: screenWidth * 0.02, + margin: + EdgeInsets.symmetric(horizontal: screenWidth * 0.01), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity( + _currentCarouselIndex == entry.key ? 0.9 : 0.4), + ), + ); + }).toList(), + ), + ], + ), + + SizedBox(height: screenHeight * 0.03), + + // Event Details + Text( + 'Hosted by', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 15 * textScale * (screenWidth / 360), + ), + ), + SizedBox(height: screenHeight * 0.01), + Text( + widget.hostname, + style: TextStyle( + fontSize: 24 * textScale * (screenWidth / 360), + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + SizedBox(height: screenHeight * 0.03), + + // Description Card + Card( + color: Theme.of(context).cardColor, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(screenWidth * 0.04), + ), + child: Container( + width: double.infinity, + padding: EdgeInsets.all(screenWidth * 0.04), + child: ReadMoreText( + widget.description, + trimLines: 3, + style: TextStyle( + fontSize: 15, + color: Theme.of(context) + .scaffoldBackgroundColor + .computeLuminance() > + 0.5 + ? Colors.black + : Colors.white), + moreStyle: TextStyle( + color: Colors.blue, + fontSize: 16 * textScale * (screenWidth / 360), + fontWeight: FontWeight.bold, + ), + lessStyle: TextStyle( + color: Colors.redAccent, + fontSize: 16 * textScale * (screenWidth / 360), + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + + // Move button to the bottom + bottomNavigationBar: Padding( + padding: EdgeInsets.all(screenWidth * 0.04), + child: SizedBox( + width: screenWidth * 0.9, + height: screenHeight * 0.06, + child: ElevatedButton( + onPressed: () async { + Uri uri = Uri.parse(widget.registerLink); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Theme.of(context) + .scaffoldBackgroundColor + .computeLuminance() > + 0.5 + ? Colors.black + : Colors.white, + behavior: SnackBarBehavior.floating, + content: Text( + "Could not launch", + style: TextStyle( + fontWeight: FontWeight.w100, + color: Theme.of(context).scaffoldBackgroundColor), + ), + duration: const Duration(seconds: 2), + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(screenWidth * 0.04), + ), + ), + child: Text( + 'Register Now', + style: TextStyle( + color: Colors.white, + fontSize: 18 * textScale * (screenWidth / 360), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index be8ce3a..49f639f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,8 +9,10 @@ import 'package:uhl_link/features/authentication/data/data_sources/user_data_sou import 'package:uhl_link/features/authentication/domain/usecases/get_user_by_email.dart'; import 'package:uhl_link/features/authentication/domain/usecases/signup_user.dart'; import 'package:uhl_link/features/authentication/domain/usecases/update_password.dart'; +import 'package:uhl_link/features/home/data/data_sources/feed_portal_data_sources.dart'; import 'package:uhl_link/features/home/domain/usecases/add_lost_found_item.dart'; import 'package:uhl_link/features/home/domain/usecases/get_lost_found_items.dart'; +import 'package:uhl_link/features/home/presentation/bloc/feed_page_bloc/feed_bloc.dart'; import 'package:uhl_link/utils/theme.dart'; import 'features/authentication/data/repository_implementations/user_repository_impl.dart'; @@ -19,18 +21,22 @@ import 'features/authentication/domain/usecases/signin_user.dart'; import 'features/authentication/presentation/bloc/user_bloc.dart'; import 'features/home/data/data_sources/job_portal_data_sources.dart'; import 'features/home/data/data_sources/lost_found_data_sources.dart'; +import 'features/home/data/repository_implementations/feed_repository_impl.dart'; import 'features/home/data/repository_implementations/job_portal_repository_impl.dart'; import 'features/home/data/repository_implementations/lost_found_repository_impl.dart'; +import 'features/home/domain/usecases/add_feed_item.dart'; +import 'features/home/domain/usecases/get_feed_item.dart'; import 'features/home/domain/usecases/get_jobs.dart'; import 'features/home/presentation/bloc/job_portal_bloc/job_bloc.dart'; import 'features/home/presentation/bloc/lost_found_bloc/lnf_bloc.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await dotenv.load(fileName: "institute.env"); + await dotenv.load(fileName: "assets/institute.env"); await UhlUsersDB.connect(dotenv.env['DB_CONNECTION_URL']!); await JobPortalDB.connect(dotenv.env['DB_CONNECTION_URL']!); await LostFoundDB.connect(dotenv.env['DB_CONNECTION_URL']!); + await FeedDB.connect(dotenv.env['DB_CONNECTION_URL']!); const storage = FlutterSecureStorage(); final GoRouter router = UhlLinkRouter().router; runApp(BlocProvider( @@ -67,9 +73,16 @@ class UhlLink extends StatelessWidget { RepositoryProvider( create: (_) => AddLostFoundItem(LostFoundRepositoryImpl(LostFoundDB()))), + RepositoryProvider( + create: (_) => + GetFeedItem(FeedRepositoryImpl(FeedDB()))), + RepositoryProvider( + create: (_) => + AddFeedItem(FeedRepositoryImpl(FeedDB()))), ], child: MultiBlocProvider( providers: [ + BlocProvider( create: (context) => AuthenticationBloc( loginUser: SignInUser(UserRepositoryImpl(UhlUsersDB())), @@ -89,6 +102,12 @@ class UhlLink extends StatelessWidget { GetLostFoundItems(LostFoundRepositoryImpl(LostFoundDB())), addLostFoundItem: AddLostFoundItem( LostFoundRepositoryImpl(LostFoundDB())))), + BlocProvider( + create: (context) => FeedBloc( + getFeedItems: + GetFeedItem(FeedRepositoryImpl(FeedDB())), + addFeedItem: AddFeedItem( + FeedRepositoryImpl(FeedDB())))), ], child: BlocBuilder( builder: (context, state) { diff --git a/lib/widgets/form_field_widget.dart b/lib/widgets/form_field_widget.dart index 7a3fc05..2cd110b 100644 --- a/lib/widgets/form_field_widget.dart +++ b/lib/widgets/form_field_widget.dart @@ -13,6 +13,7 @@ class FormFieldWidget extends StatefulWidget { final bool showSuffixIcon; final String hintText; final TextInputAction textInputAction; + final bool isLargeField; final void Function()? onTap; bool? readOnly; @@ -32,6 +33,7 @@ class FormFieldWidget extends StatefulWidget { this.readOnly, required this.focusNode, required this.textInputAction, + this.isLargeField = false, // Default to false }); @override @@ -55,79 +57,155 @@ class _FormFieldWidgetState extends State { @override Widget build(BuildContext context) { - return Form( - key: widget.fieldKey, - child: TextFormField( - // autofocus: false, - focusNode: widget.focusNode, - onTapOutside: (event) { - widget.focusNode.unfocus(); - }, - readOnly: widget.readOnly ?? false, - onTap: widget.onTap, - controller: widget.controller, - obscureText: widget.obscureText, - cursorColor: Theme.of(context).primaryColor, - validator: widget.validator, - onChanged: widget.onChanged, - keyboardType: widget.keyboardType, - style: Theme.of(context).textTheme.labelSmall, - textInputAction: widget.textInputAction, - decoration: InputDecoration( - errorText: widget.errorText, - errorStyle: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Theme.of(context).colorScheme.onError, fontSize: 12), - contentPadding: const EdgeInsets.symmetric(vertical: 15), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.scrim, width: 1), - borderRadius: const BorderRadius.all(Radius.circular(12)), - gapPadding: 24), - prefixIcon: Icon( - widget.prefixIcon, - color: widget.focusNode.hasFocus - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.scrim, - size: 18, + return Container( + child: widget.isLargeField ? Container( + width: 400, // Set constant width + height: 200, // Set constant height + child: Form( + key: widget.fieldKey, + child: Stack( + children: [ + TextFormField( + focusNode: widget.focusNode, + onTapOutside: (event) { + widget.focusNode.unfocus(); + }, + readOnly: widget.readOnly ?? false, + onTap: widget.onTap, + controller: widget.controller, + obscureText: widget.obscureText, + cursorColor: Theme.of(context).primaryColor, + validator: widget.validator, + onChanged: widget.onChanged, + keyboardType: widget.keyboardType, + style: Theme.of(context).textTheme.labelSmall, + textInputAction: widget.textInputAction, + maxLines: null, // Allows unlimited lines but within fixed height + expands: true, // Makes text expand inside fixed container + textAlignVertical: TextAlignVertical.top, // Aligns input text to the top + decoration: InputDecoration( + errorText: widget.errorText, + errorStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.onError, fontSize: 12), + contentPadding: const EdgeInsets.only( + top: 10, left: 40, right: 10, bottom: 10), // Space for the icon + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.scrim, width: 1), + borderRadius: const BorderRadius.all(Radius.circular(12)), + gapPadding: 24), + hintText: widget.hintText, + hintStyle: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(color: Theme.of(context).colorScheme.scrim), + fillColor: Theme.of(context).cardColor, + filled: true, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).primaryColor, width: 2), + borderRadius: const BorderRadius.all(Radius.circular(12)), + gapPadding: 24), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.onError, width: 1), + borderRadius: const BorderRadius.all(Radius.circular(12)), + gapPadding: 24), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.onError, width: 1), + borderRadius: const BorderRadius.all(Radius.circular(12)), + gapPadding: 24), + ), ), - suffixIcon: widget.showSuffixIcon - ? GestureDetector( - onTap: () { - _togglevisibility(); - }, - child: Icon( - widget.obscureText - ? Icons.visibility_rounded - : Icons.visibility_off_rounded, - color: widget.focusNode.hasFocus - ? Theme.of(context).primaryColor - : Theme.of(context).colorScheme.scrim, - size: 18, - ), - ) - : const Icon(null), - hintText: widget.hintText, - hintStyle: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith(color: Theme.of(context).colorScheme.scrim), - fillColor: Theme.of(context).cardColor, - filled: true, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).primaryColor, width: 2), - borderRadius: const BorderRadius.all(Radius.circular(12)), - gapPadding: 24), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.onError, width: 1), - borderRadius: const BorderRadius.all(Radius.circular(12)), - gapPadding: 24), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.onError, width: 1), - borderRadius: const BorderRadius.all(Radius.circular(12)), - gapPadding: 24)), - )); + Positioned( + left: 10, + top: 13, + child: Icon( + widget.prefixIcon, + color: widget.focusNode.hasFocus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.scrim, + size: 18, + ), + ), + ], + ), + ), + ) : Form( + key: widget.fieldKey, + child: TextFormField( + // autofocus: false, + focusNode: widget.focusNode, + onTapOutside: (event) { + widget.focusNode.unfocus(); + }, + readOnly: widget.readOnly ?? false, + onTap: widget.onTap, + controller: widget.controller, + obscureText: widget.obscureText, + cursorColor: Theme.of(context).primaryColor, + validator: widget.validator, + onChanged: widget.onChanged, + keyboardType: widget.keyboardType, + style: Theme.of(context).textTheme.labelSmall, + textInputAction: widget.textInputAction, + decoration: InputDecoration( + errorText: widget.errorText, + errorStyle: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.onError, fontSize: 12), + contentPadding: const EdgeInsets.symmetric(vertical: 15), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.scrim, width: 1), + borderRadius: const BorderRadius.all(Radius.circular(12)), + gapPadding: 24), + prefixIcon: Icon( + widget.prefixIcon, + color: widget.focusNode.hasFocus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.scrim, + size: 18, + ), + suffixIcon: widget.showSuffixIcon + ? GestureDetector( + onTap: () { + _togglevisibility(); + }, + child: Icon( + widget.obscureText + ? Icons.visibility_rounded + : Icons.visibility_off_rounded, + color: widget.focusNode.hasFocus + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.scrim, + size: 18, + ), + ) + : const Icon(null), + hintText: widget.hintText, + hintStyle: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(color: Theme.of(context).colorScheme.scrim), + fillColor: Theme.of(context).cardColor, + filled: true, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).primaryColor, width: 2), + borderRadius: const BorderRadius.all(Radius.circular(12)), + gapPadding: 24), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.onError, width: 1), + borderRadius: const BorderRadius.all(Radius.circular(12)), + gapPadding: 24), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.onError, width: 1), + borderRadius: const BorderRadius.all(Radius.circular(12)), + gapPadding: 24)), + )) // Conditional rendering + ); } -} +} \ No newline at end of file