From 55b4609badbad2f1ae5eba02de7bc438f9e9eb27 Mon Sep 17 00:00:00 2001 From: Karagwa Date: Wed, 22 Jan 2025 16:24:36 +0300 Subject: [PATCH 01/57] Implemented the guest user experience and pages --- mobile-v3/assets/icons/chevron-right.svg | 3 + .../assets/images/shared/Frame 26085560.svg | 36 ++++ mobile-v3/lib/main.dart | 3 +- .../lib/src/app/auth/bloc/auth_bloc.dart | 2 + .../lib/src/app/auth/bloc/auth_event.dart | 4 + .../lib/src/app/auth/bloc/auth_state.dart | 3 + .../src/app/auth/pages/welcome_screen.dart | 204 ++++++++++-------- .../app/dashboard/pages/dashboard_page.dart | 142 ++++++++---- .../app/dashboard/widgets/analytics_card.dart | 11 +- .../app/profile/pages/guest_profile page.dart | 137 ++++++++++++ .../pages/widgets/guest_settings_widget.dart | 56 +++++ .../profile/pages/widgets/settings_tile.dart | 10 +- mobile-v3/lib/src/meta/utils/colors.dart | 4 + 13 files changed, 479 insertions(+), 136 deletions(-) create mode 100644 mobile-v3/assets/icons/chevron-right.svg create mode 100644 mobile-v3/assets/images/shared/Frame 26085560.svg create mode 100644 mobile-v3/lib/src/app/profile/pages/guest_profile page.dart create mode 100644 mobile-v3/lib/src/app/profile/pages/widgets/guest_settings_widget.dart diff --git a/mobile-v3/assets/icons/chevron-right.svg b/mobile-v3/assets/icons/chevron-right.svg new file mode 100644 index 0000000000..64a8a55b4b --- /dev/null +++ b/mobile-v3/assets/icons/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/mobile-v3/assets/images/shared/Frame 26085560.svg b/mobile-v3/assets/images/shared/Frame 26085560.svg new file mode 100644 index 0000000000..7f82ee3633 --- /dev/null +++ b/mobile-v3/assets/images/shared/Frame 26085560.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-v3/lib/main.dart b/mobile-v3/lib/main.dart index cba73c46c4..8c3aa917ed 100644 --- a/mobile-v3/lib/main.dart +++ b/mobile-v3/lib/main.dart @@ -16,6 +16,7 @@ import 'package:airqo/src/app/other/places/repository/google_places_repository.d import 'package:airqo/src/app/other/theme/bloc/theme_bloc.dart'; import 'package:airqo/src/app/other/theme/repository/theme_repository.dart'; import 'package:airqo/src/app/profile/bloc/user_bloc.dart'; +import 'package:airqo/src/app/profile/pages/guest_profile%20page.dart'; import 'package:airqo/src/app/profile/repository/user_repository.dart'; import 'package:airqo/src/app/shared/bloc/connectivity_bloc.dart'; import 'package:airqo/src/app/shared/pages/nav_page.dart'; @@ -123,7 +124,7 @@ class AirqoMobile extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, - theme: isLightTheme ? AppTheme.lightTheme : AppTheme.darkTheme, + theme: isLightTheme ? AppTheme.darkTheme : AppTheme.lightTheme, // theme: isLightTheme ? ThemeData( // splashColor: Colors.transparent, // highlightColor: Colors.transparent, diff --git a/mobile-v3/lib/src/app/auth/bloc/auth_bloc.dart b/mobile-v3/lib/src/app/auth/bloc/auth_bloc.dart index 82bcb66c7c..65bb2f3c6e 100644 --- a/mobile-v3/lib/src/app/auth/bloc/auth_bloc.dart +++ b/mobile-v3/lib/src/app/auth/bloc/auth_bloc.dart @@ -41,6 +41,8 @@ class AuthBloc extends Bloc { ), ); } + } else if (event is UseAsGuest) { + emit(GuestUser()); } }); } diff --git a/mobile-v3/lib/src/app/auth/bloc/auth_event.dart b/mobile-v3/lib/src/app/auth/bloc/auth_event.dart index da4d0641f3..64f93b4e9c 100644 --- a/mobile-v3/lib/src/app/auth/bloc/auth_event.dart +++ b/mobile-v3/lib/src/app/auth/bloc/auth_event.dart @@ -19,3 +19,7 @@ class RegisterUser extends AuthEvent { const RegisterUser(this.model); } + +class UseAsGuest extends AuthEvent { + const UseAsGuest(); +} diff --git a/mobile-v3/lib/src/app/auth/bloc/auth_state.dart b/mobile-v3/lib/src/app/auth/bloc/auth_state.dart index 60f94cc6c4..4acd39f900 100644 --- a/mobile-v3/lib/src/app/auth/bloc/auth_state.dart +++ b/mobile-v3/lib/src/app/auth/bloc/auth_state.dart @@ -24,3 +24,6 @@ class AuthLoadingError extends AuthState { } enum AuthPurpose { LOGIN, REGISTER } + +final class GuestUser extends AuthState {} + diff --git a/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart b/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart index 2e5febe4c1..b2256097e0 100644 --- a/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart +++ b/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart @@ -5,8 +5,13 @@ import 'package:airqo/src/app/auth/widgets/know_your_air.dart'; import 'package:airqo/src/app/auth/widgets/welcome_widget.dart'; import 'package:airqo/src/meta/utils/colors.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import '../../shared/pages/nav_page.dart'; +import '../bloc/auth_bloc.dart'; + class WelcomeScreen extends StatefulWidget { const WelcomeScreen({super.key}); @@ -38,101 +43,128 @@ class _WelcomeScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - Stack( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.6, - child: PageView( - controller: controller, - onPageChanged: changeIndex, - children: [ - WelcomeWidget(), - BreatheClean(), - KnowYourAir(), - ], + return BlocListener( + listener: (context, state) { + if (state is GuestUser) { + Future.microtask(() => Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => NavPage()), + )); + } + }, + child: Scaffold( + body: Column( + children: [ + Stack( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: PageView( + controller: controller, + onPageChanged: changeIndex, + children: [ + WelcomeWidget(), + BreatheClean(), + KnowYourAir(), + ], + ), ), - ), - Container( - width: double.infinity, - height: MediaQuery.of(context).size.height * 0.6, - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AnimatedSmoothIndicator( - activeIndex: currentIndex, - count: 3, - effect: ExpandingDotsEffect( - dotWidth: 7, - radius: 7, - dotColor: Color(0xff60646C), - // activeDotColor: Color(0xffF6F6F7), - activeDotColor: AppColors.primaryColor, - dotHeight: 7), - ), - SizedBox(height: 16) - ], + Container( + width: double.infinity, + height: MediaQuery.of(context).size.height * 0.6, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AnimatedSmoothIndicator( + activeIndex: currentIndex, + count: 3, + effect: ExpandingDotsEffect( + dotWidth: 7, + radius: 7, + dotColor: Color(0xff60646C), + // activeDotColor: Color(0xffF6F6F7), + activeDotColor: AppColors.primaryColor, + dotHeight: 7), + ), + SizedBox(height: 16) + ], + ), ), - ), - ], - ), - SizedBox( - height: MediaQuery.of(context).size.height * 0.4, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16 * 2), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - InkWell( - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (context) => CreateAccountScreen())), - child: Container( - height: 56, - decoration: BoxDecoration( - color: AppColors.primaryColor, - borderRadius: BorderRadius.circular(4)), - child: Center( - child: Text( - "Create Account", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Colors.white, + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16 * 2), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => CreateAccountScreen())), + child: Container( + height: 56, + decoration: BoxDecoration( + color: AppColors.primaryColor, + borderRadius: BorderRadius.circular(4)), + child: Center( + child: Text( + "Create Account", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), ), ), ), ), - ), - SizedBox(height: 16), - InkWell( - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (context) => LoginPage())), - child: Container( - height: 56, - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Theme.of(context).highlightColor, - borderRadius: BorderRadius.circular(4)), + SizedBox(height: 18), + InkWell( + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => LoginPage())), + child: Container( + height: 56, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Theme.of(context).highlightColor, + borderRadius: BorderRadius.circular(4)), + child: Center( + child: Text( + "Login Here", + style: TextStyle( + fontWeight: FontWeight.w500, color: Colors.black), + ), + ), + ), + ), + SizedBox(height: 18), + + InkWell( + onTap: () => Navigator.of(context).push(MaterialPageRoute( + + builder: (context) => NavPage())), child: Center( - child: Text( - "Login Here", - style: TextStyle( - fontWeight: FontWeight.w500, color: Colors.black), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Continue as guest ", + style: TextStyle( + color: AppColors.boldHeadlineColor2, + fontWeight: FontWeight.w500)), + SvgPicture.asset('assets/icons/chevron-right.svg', + height: 16.0, + width: 16.0, + color: AppColors.boldHeadlineColor2,) + ], ), ), ), - ), - SizedBox(height: 16), - Text( - "", - style: TextStyle(color: AppColors.boldHeadlineColor), - ) - ], - ), - )) - ], - )); + + ], + ), + )) + ], + )), + ); } } diff --git a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart index c9c4a77146..44c918e599 100644 --- a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart +++ b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart @@ -1,3 +1,4 @@ +import 'package:airqo/src/app/auth/bloc/auth_bloc.dart'; import 'package:airqo/src/app/dashboard/bloc/dashboard/dashboard_bloc.dart'; import 'package:airqo/src/app/dashboard/widgets/analytics_card.dart'; import 'package:airqo/src/app/dashboard/widgets/countries_chip.dart'; @@ -14,6 +15,8 @@ import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; +import '../../profile/pages/guest_profile page.dart'; +import '../../profile/pages/profile_page.dart'; import '../models/airquality_response.dart'; class DashboardPage extends StatefulWidget { @@ -27,6 +30,7 @@ class _DashboardPageState extends State { DashboardBloc? dashboardBloc; UserBloc? userBloc; ThemeBloc? themeBloc; + AuthBloc? authBloc; List filteredMeasurements = []; String currentFilter = "All"; @@ -38,6 +42,8 @@ class _DashboardPageState extends State { themeBloc = context.read(); userBloc = context.read()..add(LoadUser()); + + authBloc = context.read()..add(UseAsGuest()); super.initState(); } @@ -96,33 +102,69 @@ class _DashboardPageState extends State { ), SizedBox(width: 8), GestureDetector( - // onTap: () => Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) { - // return ProfilePage(); - // }, - // ), - // ), - child: BlocBuilder( - builder: (context, state) { - if (state is UserLoaded) { - String firstName = - state.model.users[0].firstName[0].toUpperCase(); - String lastName = - state.model.users[0].lastName[0].toUpperCase(); - return CircleAvatar( - child: Center(child: Text("${firstName}${lastName}")), - radius: 24, - backgroundColor: Theme.of(context).highlightColor, - ); - } else if (state is UserLoadingError) { - return Container(); - } else { - return ShimmerContainer( - height: 44, borderRadius: 1000, width: 44); - } - }, - ), + onTap: () { + final authState = context.read().state; + if (authState is GuestUser) { + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => GuestProfilePage(), + ), + ); + } else { + // Navigate to the regular profile page + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ProfilePage(), + ), + ); + } + }, + child: BlocBuilder( + builder: (context, authState) { + if (authState is GuestUser) { + // Display default avatar for guest users + return Container( + //margin: const EdgeInsets.symmetric(horizontal: 20), + child: CircleAvatar( + backgroundColor: Theme.of(context).highlightColor, + + child: Center( + child: SvgPicture.asset( + "assets/icons/user_icon.svg", + height:22, + width: 22, + ), + ), + radius: 24, + ), + ); + } else { + return BlocBuilder( + builder: (context, userState) { + if (userState is UserLoaded) { + String firstName = userState.model.users[0].firstName[0].toUpperCase(); + String lastName = userState.model.users[0].lastName[0].toUpperCase(); + return CircleAvatar( + child: Center(child: Text("${firstName}${lastName}")), + radius: 24, + backgroundColor: Theme.of(context).highlightColor, + ); + } else if (userState is UserLoadingError) { + return Container(); // Handle error state (optional) + } else { + return ShimmerContainer( + height: 44, + borderRadius: 1000, + width: 44, + ); + } + }, + ); + } + }, + ) + ), SizedBox(width: 8), ], @@ -138,26 +180,44 @@ class _DashboardPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 16), - BlocBuilder( - builder: (context, state) { - if (state is UserLoaded) { + BlocBuilder( + builder: (context, authState) { + if (authState is GuestUser) { return Text( - "Hello ${state.model.users[0].firstName} 👋🏼", + "Hi, Guest 👋🏼", style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w700, - color: AppColors.boldHeadlineColor), - ); - } - return Text( - "Hello 👋🏼", - style: TextStyle( fontSize: 28, fontWeight: FontWeight.w700, - color: AppColors.boldHeadlineColor), - ); + color: AppColors.boldHeadlineColor2, + ), + ); + } else { + return BlocBuilder( + builder: (context, userState) { + if (userState is UserLoaded) { + return Text( + "Hi ${userState.model.users[0].firstName} 👋🏼", + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + color: AppColors.boldHeadlineColor, + ), + ); + } + return Text( + "Hi,👋🏼", + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + color: AppColors.boldHeadlineColor, + ), + ); + }, + ); + } }, ), + Text( "Today’s Air Quality • ${DateFormat.MMMMd().format(DateTime.now())}", style: TextStyle( diff --git a/mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart b/mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart index 6613987f3a..6ad860b629 100644 --- a/mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart +++ b/mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart @@ -58,13 +58,16 @@ class AnalyticsCard extends StatelessWidget { ? measurement.pm25!.value! .toStringAsFixed(2) : "-", - style: Theme.of(context).textTheme.titleLarge + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 40, + color: AppColors.boldHeadlineColor2), ), Text(" μg/m3", style: TextStyle( fontWeight: FontWeight.w600, fontSize: 20, - color: AppColors.secondaryHeadlineColor)) + color: AppColors.boldHeadlineColor2)) ]), ]), SizedBox( @@ -96,12 +99,12 @@ class AnalyticsCard extends StatelessWidget { style: TextStyle( fontSize: 28, fontWeight: FontWeight.w700, - color: AppColors.boldHeadlineColor)), + color: AppColors.secondaryHeadlineColor3)), Text(measurement.healthTips![0].description!, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, - color: AppColors.secondaryHeadlineColor)) + color: AppColors.secondaryHeadlineColor2)) ], ), ), diff --git a/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart b/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart new file mode 100644 index 0000000000..792b45c7eb --- /dev/null +++ b/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart @@ -0,0 +1,137 @@ +import 'package:airqo/src/app/profile/pages/widgets/guest_settings_widget.dart'; +import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../meta/utils/colors.dart'; +import '../../auth/pages/register_page.dart'; +import '../../shared/widgets/spinner.dart'; + +class GuestProfilePage extends StatefulWidget { + const GuestProfilePage({super.key}); + + @override + State createState() => _GuestProfilePageState(); +} + +class _GuestProfilePageState extends State { + get loading => false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.close)), + SizedBox(width: 16) + ], + ), + body: SingleChildScrollView( + child: Container( + padding: const EdgeInsets.only(left: 16, right: 16, top: 8), + child: Column( + + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + + Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + child: CircleAvatar( + backgroundColor: Theme.of(context).highlightColor, + child: Center( + child: SvgPicture.asset( + "assets/icons/user_icon.svg"), + ), + radius: 50, + ), + ), + + + Text( + "Guest User", + style: TextStyle( + color: AppColors.boldHeadlineColor2, + fontSize: 24, + fontWeight: FontWeight.w700, + ), + ), + ],), + SizedBox(height: 32), + Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + 'Settings', + style:TextStyle( + color: AppColors.boldHeadlineColor2, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + + ), + ), + SizedBox( + height: 5, + ), + + Divider( + color:Theme.of(context).highlightColor, + indent: 20, + ), + + + + + GuestSettingsWidget(), + + + + SizedBox( + height: 18, + ), + InkWell( + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => CreateAccountScreen())), + child: Container( + height: 56, + decoration: BoxDecoration( + color: AppColors.primaryColor, + borderRadius: + BorderRadius.circular(4)), + child: Center( + child: loading + ? Spinner() + : Text( + "Create Account", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ), + ), + SizedBox( + height: 76, + ), + + Center( + child: SvgPicture.asset('assets/images/shared/Frame 26085560.svg') + ), + SizedBox( + height: 220, + ) + ], + ), + ), + ), + ); + } +} + diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/guest_settings_widget.dart b/mobile-v3/lib/src/app/profile/pages/widgets/guest_settings_widget.dart new file mode 100644 index 0000000000..4104643366 --- /dev/null +++ b/mobile-v3/lib/src/app/profile/pages/widgets/guest_settings_widget.dart @@ -0,0 +1,56 @@ +import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; +import 'package:flutter/material.dart'; + +class GuestSettingsWidget extends StatelessWidget { + const GuestSettingsWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox(height: 8), + SettingsTile( + switchValue: true, + iconPath: "assets/images/shared/location_icon.svg", + title: "Location", + onChanged: (value) { + print(value); + }, + description: + "AirQo to use your precise location to locate the Air Quality of your nearest location"), + SettingsTile( + + iconPath: "assets/icons/notification.svg", + title: "Notifications", + onChanged: (value) { + print(value); + }, + description: + "Create an account to get air quality alerts"), + SettingsTile( + iconPath: "assets/images/shared/feedback_icon.svg", + title: "Send Feedback", + onChanged: (value) { + print(value); + }, + ), + SettingsTile( + iconPath: "assets/images/shared/airqo_story_icon.svg", + title: "Our Story", + onChanged: (value) { + print(value); + }, + ), + + SettingsTile( + iconPath: "assets/images/shared/terms_and_privacy.svg", + title: "Terms and Privacy Policy", + onChanged: (value) { + print(value); + }, + ), + + ], + ); + } +} diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/settings_tile.dart b/mobile-v3/lib/src/app/profile/pages/widgets/settings_tile.dart index e1eae5ea06..4911dbec69 100644 --- a/mobile-v3/lib/src/app/profile/pages/widgets/settings_tile.dart +++ b/mobile-v3/lib/src/app/profile/pages/widgets/settings_tile.dart @@ -38,7 +38,9 @@ class SettingsTile extends StatelessWidget { subtitle: description != null ? Text(description!, style: TextStyle( - color: AppColors.secondaryHeadlineColor, fontSize: 13)) + color: AppColors.secondaryHeadlineColor2, + fontSize: 14, + fontWeight: FontWeight.w400)) : null, leading: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -55,11 +57,11 @@ class SettingsTile extends StatelessWidget { title: Text(title, style: TextStyle( fontSize: 18, - fontWeight: FontWeight.w700, - color: AppColors.boldHeadlineColor)), + fontWeight: FontWeight.w500, + color: AppColors.secondaryHeadlineColor3)), ), Divider( - color: Theme.of(context).highlightColor, + color:Theme.of(context).highlightColor, indent: 80, ) ], diff --git a/mobile-v3/lib/src/meta/utils/colors.dart b/mobile-v3/lib/src/meta/utils/colors.dart index 443047ec49..03cae8558e 100644 --- a/mobile-v3/lib/src/meta/utils/colors.dart +++ b/mobile-v3/lib/src/meta/utils/colors.dart @@ -14,6 +14,10 @@ class AppColors { static Color highlightColor = Color(0xffF3F6F8); static Color boldHeadlineColor = Color(0xff6F87A1); static Color secondaryHeadlineColor = Color(0xff6F87A1); + static Color boldHeadlineColor2 = Color(0xff9EA3AA); + static Color secondaryHeadlineColor2 = Color(0xff60646C); + static Color secondaryHeadlineColor3 = Color(0xff7A7F87); + } class AppTheme { From 518e88ca781e3b5a8c5dc10c93784db96266f1c3 Mon Sep 17 00:00:00 2001 From: Peter Kyeyune Date: Thu, 23 Jan 2025 13:53:15 +0300 Subject: [PATCH 02/57] Refactor dashboard and profile pages; implement navigation to ProfilePage and update SettingsWidget layout --- .../app/dashboard/pages/dashboard_page.dart | 15 +- .../src/app/profile/pages/profile_page.dart | 28 ++- .../pages/widgets/settings_widget.dart | 201 +++++++++++++----- 3 files changed, 165 insertions(+), 79 deletions(-) diff --git a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart index c9c4a77146..cbcd25a502 100644 --- a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart +++ b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart @@ -13,6 +13,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; +import 'package:airqo/src/app/profile/pages/profile_page.dart'; import '../models/airquality_response.dart'; @@ -96,13 +97,13 @@ class _DashboardPageState extends State { ), SizedBox(width: 8), GestureDetector( - // onTap: () => Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) { - // return ProfilePage(); - // }, - // ), - // ), + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return ProfilePage(); + }, + ), + ), child: BlocBuilder( builder: (context, state) { if (state is UserLoaded) { diff --git a/mobile-v3/lib/src/app/profile/pages/profile_page.dart b/mobile-v3/lib/src/app/profile/pages/profile_page.dart index ec80c54825..0b7e749377 100644 --- a/mobile-v3/lib/src/app/profile/pages/profile_page.dart +++ b/mobile-v3/lib/src/app/profile/pages/profile_page.dart @@ -1,6 +1,4 @@ import 'package:airqo/src/app/profile/bloc/user_bloc.dart'; -import 'package:airqo/src/app/profile/pages/widgets/devices_widget.dart'; -import 'package:airqo/src/app/profile/pages/widgets/exposure_widget.dart'; import 'package:airqo/src/app/profile/pages/widgets/settings_widget.dart'; import 'package:airqo/src/meta/utils/colors.dart'; import 'package:flutter/material.dart'; @@ -29,7 +27,7 @@ class _ProfilePageState extends State { String firstName = state.model.users[0].firstName; String lastName = state.model.users[0].lastName; return DefaultTabController( - length: 3, + length: 1, child: Scaffold( appBar: AppBar( automaticallyImplyLeading: false, @@ -123,21 +121,21 @@ class _ProfilePageState extends State { ? Colors.white : AppColors.primaryColor, tabs: [ - Tab( - height: 60, - icon: TabIcon( - image: "assets/profile/exposure.svg", - label: "Exposure")), + // Tab( + // height: 60, + // icon: TabIcon( + // image: "assets/profile/exposure.svg", + // label: "Exposure")), // Tab( // height: 60, // icon: TabIcon( // image: "assets/profile/places.svg", // label: "Places")), - Tab( - height: 60, - icon: TabIcon( - image: "assets/profile/devices.svg", - label: "Devices")), + // Tab( + // height: 60, + // icon: TabIcon( + // image: "assets/profile/devices.svg", + // label: "Devices")), Tab( height: 60, icon: TabIcon( @@ -146,9 +144,9 @@ class _ProfilePageState extends State { ]), Expanded( child: TabBarView(children: [ - ExposureWidget(), + // ExposureWidget(), // Container(child: Text("devices")), - DevicesWidget(), + // DevicesWidget(), SettingsWidget() ]), ) diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart index 30ba5db175..2f7fb10ea0 100644 --- a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart +++ b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart @@ -1,68 +1,155 @@ -import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; import 'package:flutter/material.dart'; +import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; +import 'package:flutter_svg/svg.dart'; class SettingsWidget extends StatelessWidget { const SettingsWidget({super.key}); @override Widget build(BuildContext context) { - return Column( - children: [ - SizedBox(height: 8), - SettingsTile( - switchValue: true, - iconPath: "assets/images/shared/location_icon.svg", - title: "Location", - onChanged: (value) { - print(value); - }, - description: - "AirQo to use your precise location to locate the Air Quality of your nearest location"), - SettingsTile( - switchValue: true, - iconPath: "assets/icons/notification.svg", - title: "Notifications", - onChanged: (value) { - print(value); - }, - description: - "AirQo to send you in-app & push notifications & spike alerts."), - SettingsTile( - iconPath: "assets/images/shared/feedback_icon.svg", - title: "Send Feedback", - onChanged: (value) { - print(value); - }, - ), - SettingsTile( - iconPath: "assets/images/shared/airqo_story_icon.svg", - title: "Our Story", - onChanged: (value) { - print(value); - }, - ), - SettingsTile( - iconPath: "assets/images/shared/rate_app_icon.svg", - title: "Rate the App", - onChanged: (value) { - print(value); - }, - ), - SettingsTile( - iconPath: "assets/images/shared/terms_and_privacy.svg", - title: "Terms and Privacy Policy", - onChanged: (value) { - print(value); - }, + final screenHeight = MediaQuery.of(context).size.height; + final screenWidth = MediaQuery.of(context).size.width; + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: screenHeight * 0.02), + + // Location Setting + SettingsTile( + switchValue: true, + iconPath: "assets/images/shared/location_icon.svg", + title: "Location", + onChanged: (value) { + print("Location setting: \$value"); + }, + description: + "AirQo to use your precise location to locate the Air Quality of your nearest location", + ), + + // Notifications Setting + SettingsTile( + switchValue: true, + iconPath: "assets/icons/notification.svg", + title: "Notifications", + onChanged: (value) { + print("Notifications setting: \$value"); + }, + description: + "AirQo to send you in-app & push notifications & spike alerts.", + ), + + // Send Feedback + SettingsTile( + iconPath: "assets/images/shared/feedback_icon.svg", + title: "Send Feedback", + onChanged: (value) { + print("Send Feedback setting: \$value"); + }), + + // Our Story + SettingsTile( + iconPath: "assets/images/shared/airqo_story_icon.svg", + title: "Our Story", + onChanged: (value) { + print("Our Story setting: \$value"); + }, + ), + + // Terms and Privacy Policy + SettingsTile( + iconPath: "assets/images/shared/terms_and_privacy.svg", + title: "Terms and Privacy Policy", + onChanged: (value) { + print("Terms and Privacy Policy setting: \$value"); + }, + ), + + // Logout Button + Padding( + padding: EdgeInsets.symmetric(vertical: screenHeight * 0.02), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + minimumSize: Size.fromHeight(screenHeight * 0.07), + ), + onPressed: () { + print("Logout tapped"); + }, + child: const Text( + "Log out", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + // Delete Account Section + Padding( + padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.04), + child: InkWell( + onTap: () { + print("Delete Account tapped"); + }, + child: Text( + "Delete Account", + style: TextStyle( + color: Colors.red.shade300, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ), + ), + + SizedBox(height: screenHeight * 0.03), + + // App Info + Center( + child: Column( + children: [ + SvgPicture.asset( + "assets/images/shared/logo.svg", + height: screenHeight * 0.05, + ), + SizedBox(height: screenHeight * 0.01), + const Text( + "3.40.1(1)", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + SizedBox(height: screenHeight * 0.005), + const Text( + "A PROJECT BY", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + const Text( + "Makerere University", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.white, + ), + ), + ], + ), + ), + ], ), - SettingsTile( - iconPath: "assets/images/shared/terms_and_privacy.svg", - title: "Logout", - onChanged: (value) { - print(value); - }, - ) - ], + ), ); } } From 7f42f71bebbf3709268002e693254f48f4cc8f46 Mon Sep 17 00:00:00 2001 From: Karagwa Date: Thu, 23 Jan 2025 15:11:33 +0300 Subject: [PATCH 03/57] Made improvements on the guest experience PR --- mobile-v3/lib/main.dart | 2 +- .../src/app/auth/pages/welcome_screen.dart | 4 +--- .../app/dashboard/pages/dashboard_page.dart | 6 +++++- .../app/profile/pages/guest_profile page.dart | 21 +++++++++++++++++-- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/mobile-v3/lib/main.dart b/mobile-v3/lib/main.dart index 8c3aa917ed..d36b5fa091 100644 --- a/mobile-v3/lib/main.dart +++ b/mobile-v3/lib/main.dart @@ -137,7 +137,7 @@ class AirqoMobile extends StatelessWidget { // scaffoldBackgroundColor: Theme.of(context).scaffoldBackgroundColor, // brightness: Brightness.light), title: "AirQo", - home: Decider(), + home: GuestProfilePage(), ); }, ), diff --git a/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart b/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart index b2256097e0..b19faad3f0 100644 --- a/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart +++ b/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart @@ -140,9 +140,7 @@ class _WelcomeScreenState extends State { SizedBox(height: 18), InkWell( - onTap: () => Navigator.of(context).push(MaterialPageRoute( - - builder: (context) => NavPage())), + onTap: () =>context.read().add(UseAsGuest()), child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart index 44c918e599..e22c235614 100644 --- a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart +++ b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart @@ -43,7 +43,11 @@ class _DashboardPageState extends State { userBloc = context.read()..add(LoadUser()); - authBloc = context.read()..add(UseAsGuest()); + final authState = context.read().state; + if (authState is AuthInitial || authState is GuestUser) { + // Only set guest mode if no user is authenticated + context.read().add(UseAsGuest()); + } super.initState(); } diff --git a/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart b/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart index 792b45c7eb..0c996ce8b3 100644 --- a/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart +++ b/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart @@ -16,7 +16,24 @@ class GuestProfilePage extends StatefulWidget { } class _GuestProfilePageState extends State { - get loading => false; + // Declare a loading state variable + bool isLoading = false; + + void handleCreateAccount() async { + setState(() { + isLoading = true; // Set loading to true + }); + + // Simulate a delay for account creation or call an API here + await Future.delayed(Duration(seconds: 2)); + + setState(() { + isLoading = false; // Set loading to false once done + }); + + // Navigate to CreateAccountScreen or handle success + Navigator.of(context).push(MaterialPageRoute(builder: (context) => CreateAccountScreen())); + } @override Widget build(BuildContext context) { @@ -105,7 +122,7 @@ class _GuestProfilePageState extends State { borderRadius: BorderRadius.circular(4)), child: Center( - child: loading + child: isLoading ? Spinner() : Text( "Create Account", From 2f1514180cfada6a353b4ee19685cb791bbc4018 Mon Sep 17 00:00:00 2001 From: Karagwa Date: Thu, 23 Jan 2025 15:12:44 +0300 Subject: [PATCH 04/57] Made improvements on the guest experience PR- removed unused import --- mobile-v3/lib/src/app/profile/pages/guest_profile page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart b/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart index 0c996ce8b3..46dfe2fa93 100644 --- a/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart +++ b/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart @@ -1,5 +1,5 @@ import 'package:airqo/src/app/profile/pages/widgets/guest_settings_widget.dart'; -import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; +//import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; From 5e05dc27076bc39181af5fd6c6a2052f0b41fd8e Mon Sep 17 00:00:00 2001 From: Peter Kyeyune Date: Thu, 23 Jan 2025 15:14:13 +0300 Subject: [PATCH 05/57] Update SettingsWidget layout; adjust padding and styles for improved UI --- .../pages/widgets/settings_widget.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart index 2f7fb10ea0..0f652f6909 100644 --- a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart +++ b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart @@ -12,7 +12,7 @@ class SettingsWidget extends StatelessWidget { return SingleChildScrollView( child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -70,10 +70,10 @@ class SettingsWidget extends StatelessWidget { // Logout Button Padding( - padding: EdgeInsets.symmetric(vertical: screenHeight * 0.02), + padding: EdgeInsets.symmetric(vertical: screenHeight * 0.05), child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, + backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -85,7 +85,7 @@ class SettingsWidget extends StatelessWidget { child: const Text( "Log out", style: TextStyle( - color: Colors.white, + color: Colors.black, fontWeight: FontWeight.bold, ), ), @@ -94,7 +94,7 @@ class SettingsWidget extends StatelessWidget { // Delete Account Section Padding( - padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.04), + padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.3), child: InkWell( onTap: () { print("Delete Account tapped"); @@ -128,7 +128,7 @@ class SettingsWidget extends StatelessWidget { fontSize: 12, ), ), - SizedBox(height: screenHeight * 0.005), + SizedBox(height: screenHeight * 0.01), const Text( "A PROJECT BY", style: TextStyle( @@ -136,11 +136,11 @@ class SettingsWidget extends StatelessWidget { fontSize: 12, ), ), - const Text( - "Makerere University", - style: TextStyle( + Text( + "Makerere University".toUpperCase(), + style: const TextStyle( fontWeight: FontWeight.bold, - fontSize: 12, + fontSize: 20, color: Colors.white, ), ), From b20f83c35ca6a26e9bac6e39f7d26a48b5e2c6fa Mon Sep 17 00:00:00 2001 From: Peter Kyeyune Date: Thu, 23 Jan 2025 15:26:04 +0300 Subject: [PATCH 06/57] Refactor ProfilePage layout; replace Column with SingleChildScrollView for improved scrolling and UI responsiveness --- .../src/app/profile/pages/profile_page.dart | 219 ++++++++++-------- 1 file changed, 116 insertions(+), 103 deletions(-) diff --git a/mobile-v3/lib/src/app/profile/pages/profile_page.dart b/mobile-v3/lib/src/app/profile/pages/profile_page.dart index 0b7e749377..2054bbc8d6 100644 --- a/mobile-v3/lib/src/app/profile/pages/profile_page.dart +++ b/mobile-v3/lib/src/app/profile/pages/profile_page.dart @@ -38,119 +38,132 @@ class _ProfilePageState extends State { SizedBox(width: 16) ], ), - body: Column( - children: [ - SizedBox( - height: 100, - child: Row( - children: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: CircleAvatar( - backgroundColor: Theme.of(context).highlightColor, - child: Center( - child: SvgPicture.asset( - "assets/icons/user_icon.svg"), - ), - radius: 50, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: SingleChildScrollView( + child: Column( + children: [ + SizedBox( + height: 120, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, children: [ - Text( - "${firstName} ${lastName}", - style: TextStyle( - color: AppColors.boldHeadlineColor, - fontSize: 24, - fontWeight: FontWeight.w700, + Container( + margin: + const EdgeInsets.symmetric(horizontal: 16), + child: CircleAvatar( + backgroundColor: + Theme.of(context).highlightColor, + child: Center( + child: SvgPicture.asset( + "assets/icons/user_icon.svg"), + ), + radius: 50, ), ), - Spacer(), - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 32), - height: 50, - child: Center( - child: Text("Edit your profile")), - // child: InkWell( - // onTap: () => Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) => - // EditProfile())), - // child: Text( - // "Edit your profile", - // style: TextStyle( - // fontWeight: FontWeight.w500, - // color: Colors.white, - // ), - // ), - //)), - decoration: BoxDecoration( - color: Theme.of(context).highlightColor, - borderRadius: - BorderRadius.circular(200)), + Text( + "${firstName} ${lastName}", + style: TextStyle( + color: AppColors.boldHeadlineColor, + fontSize: 24, + fontWeight: FontWeight.w700, + ), ), - SizedBox(width: 8), - CircleAvatar( - backgroundColor: - Theme.of(context).highlightColor, - radius: 26, - child: SvgPicture.asset( - "assets/icons/notification.svg")) + SizedBox(height: 8), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 32), + height: 50, + child: Center( + child: Text("Edit your profile")), + // child: InkWell( + // onTap: () => Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => + // EditProfile())), + // child: Text( + // "Edit your profile", + // style: TextStyle( + // fontWeight: FontWeight.w500, + // color: Colors.white, + // ), + // ), + //)), + decoration: BoxDecoration( + color: Theme.of(context) + .highlightColor, + borderRadius: + BorderRadius.circular(200)), + ), + SizedBox(width: 8), + CircleAvatar( + backgroundColor: Theme.of(context) + .highlightColor, + radius: 26, + child: SvgPicture.asset( + "assets/icons/notification.svg")) + ], + ), + ) ], ) ], - ) - ], + ), + ), ), - ), - SizedBox(height: 32), - TabBar( - indicatorSize: TabBarIndicatorSize.tab, - labelColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : AppColors.primaryColor, - overlayColor: - WidgetStatePropertyAll(Colors.transparent), - indicatorColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : AppColors.primaryColor, - tabs: [ - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/exposure.svg", - // label: "Exposure")), - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/places.svg", - // label: "Places")), - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/devices.svg", - // label: "Devices")), - Tab( - height: 60, - icon: TabIcon( - image: "assets/profile/settings.svg", - label: "Settings")), + SizedBox(height: 32), + TabBar( + indicatorSize: TabBarIndicatorSize.tab, + labelColor: + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : AppColors.primaryColor, + overlayColor: + WidgetStatePropertyAll(Colors.transparent), + indicatorColor: + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : AppColors.primaryColor, + tabs: [ + // Tab( + // height: 60, + // icon: TabIcon( + // image: "assets/profile/exposure.svg", + // label: "Exposure")), + // Tab( + // height: 60, + // icon: TabIcon( + // image: "assets/profile/places.svg", + // label: "Places")), + // Tab( + // height: 60, + // icon: TabIcon( + // image: "assets/profile/devices.svg", + // label: "Devices")), + Tab( + height: 60, + icon: TabIcon( + image: "assets/profile/settings.svg", + label: "Settings")), + ]), + Expanded( + child: TabBarView(children: [ + // ExposureWidget(), + // Container(child: Text("devices")), + // DevicesWidget(), + SettingsWidget() ]), - Expanded( - child: TabBarView(children: [ - // ExposureWidget(), - // Container(child: Text("devices")), - // DevicesWidget(), - SettingsWidget() - ]), - ) - ], + ) + ], + ), )), ); } From d35864660084f8764adcf45fc8593e2c889d6ccd Mon Sep 17 00:00:00 2001 From: Peter Kyeyune Date: Thu, 23 Jan 2025 15:35:50 +0300 Subject: [PATCH 07/57] Refactor ProfilePage layout; replace nested SingleChildScrollView with Column for improved performance and UI consistency --- .../src/app/profile/pages/profile_page.dart | 219 ++++++++---------- 1 file changed, 103 insertions(+), 116 deletions(-) diff --git a/mobile-v3/lib/src/app/profile/pages/profile_page.dart b/mobile-v3/lib/src/app/profile/pages/profile_page.dart index 2054bbc8d6..0b7e749377 100644 --- a/mobile-v3/lib/src/app/profile/pages/profile_page.dart +++ b/mobile-v3/lib/src/app/profile/pages/profile_page.dart @@ -38,132 +38,119 @@ class _ProfilePageState extends State { SizedBox(width: 16) ], ), - body: SingleChildScrollView( - child: Column( - children: [ - SizedBox( - height: 120, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, + body: Column( + children: [ + SizedBox( + height: 100, + child: Row( + children: [ + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: CircleAvatar( + backgroundColor: Theme.of(context).highlightColor, + child: Center( + child: SvgPicture.asset( + "assets/icons/user_icon.svg"), + ), + radius: 50, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - margin: - const EdgeInsets.symmetric(horizontal: 16), - child: CircleAvatar( - backgroundColor: - Theme.of(context).highlightColor, - child: Center( - child: SvgPicture.asset( - "assets/icons/user_icon.svg"), - ), - radius: 50, + Text( + "${firstName} ${lastName}", + style: TextStyle( + color: AppColors.boldHeadlineColor, + fontSize: 24, + fontWeight: FontWeight.w700, ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, + Spacer(), + Row( children: [ - Text( - "${firstName} ${lastName}", - style: TextStyle( - color: AppColors.boldHeadlineColor, - fontSize: 24, - fontWeight: FontWeight.w700, - ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 32), + height: 50, + child: Center( + child: Text("Edit your profile")), + // child: InkWell( + // onTap: () => Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => + // EditProfile())), + // child: Text( + // "Edit your profile", + // style: TextStyle( + // fontWeight: FontWeight.w500, + // color: Colors.white, + // ), + // ), + //)), + decoration: BoxDecoration( + color: Theme.of(context).highlightColor, + borderRadius: + BorderRadius.circular(200)), ), - SizedBox(height: 8), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 32), - height: 50, - child: Center( - child: Text("Edit your profile")), - // child: InkWell( - // onTap: () => Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) => - // EditProfile())), - // child: Text( - // "Edit your profile", - // style: TextStyle( - // fontWeight: FontWeight.w500, - // color: Colors.white, - // ), - // ), - //)), - decoration: BoxDecoration( - color: Theme.of(context) - .highlightColor, - borderRadius: - BorderRadius.circular(200)), - ), - SizedBox(width: 8), - CircleAvatar( - backgroundColor: Theme.of(context) - .highlightColor, - radius: 26, - child: SvgPicture.asset( - "assets/icons/notification.svg")) - ], - ), - ) + SizedBox(width: 8), + CircleAvatar( + backgroundColor: + Theme.of(context).highlightColor, + radius: 26, + child: SvgPicture.asset( + "assets/icons/notification.svg")) ], ) ], - ), - ), + ) + ], ), - SizedBox(height: 32), - TabBar( - indicatorSize: TabBarIndicatorSize.tab, - labelColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : AppColors.primaryColor, - overlayColor: - WidgetStatePropertyAll(Colors.transparent), - indicatorColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : AppColors.primaryColor, - tabs: [ - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/exposure.svg", - // label: "Exposure")), - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/places.svg", - // label: "Places")), - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/devices.svg", - // label: "Devices")), - Tab( - height: 60, - icon: TabIcon( - image: "assets/profile/settings.svg", - label: "Settings")), - ]), - Expanded( - child: TabBarView(children: [ - // ExposureWidget(), - // Container(child: Text("devices")), - // DevicesWidget(), - SettingsWidget() + ), + SizedBox(height: 32), + TabBar( + indicatorSize: TabBarIndicatorSize.tab, + labelColor: + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : AppColors.primaryColor, + overlayColor: + WidgetStatePropertyAll(Colors.transparent), + indicatorColor: + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : AppColors.primaryColor, + tabs: [ + // Tab( + // height: 60, + // icon: TabIcon( + // image: "assets/profile/exposure.svg", + // label: "Exposure")), + // Tab( + // height: 60, + // icon: TabIcon( + // image: "assets/profile/places.svg", + // label: "Places")), + // Tab( + // height: 60, + // icon: TabIcon( + // image: "assets/profile/devices.svg", + // label: "Devices")), + Tab( + height: 60, + icon: TabIcon( + image: "assets/profile/settings.svg", + label: "Settings")), ]), - ) - ], - ), + Expanded( + child: TabBarView(children: [ + // ExposureWidget(), + // Container(child: Text("devices")), + // DevicesWidget(), + SettingsWidget() + ]), + ) + ], )), ); } From a2586bb717569ebde9a230403aec7066aac65384 Mon Sep 17 00:00:00 2001 From: Peter Kyeyune Date: Thu, 23 Jan 2025 15:37:08 +0300 Subject: [PATCH 08/57] Remove unused ProfilePage navigation code from DashboardPage --- .../src/app/dashboard/pages/dashboard_page.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart index cbcd25a502..c9c4a77146 100644 --- a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart +++ b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart @@ -13,7 +13,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; -import 'package:airqo/src/app/profile/pages/profile_page.dart'; import '../models/airquality_response.dart'; @@ -97,13 +96,13 @@ class _DashboardPageState extends State { ), SizedBox(width: 8), GestureDetector( - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return ProfilePage(); - }, - ), - ), + // onTap: () => Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) { + // return ProfilePage(); + // }, + // ), + // ), child: BlocBuilder( builder: (context, state) { if (state is UserLoaded) { From e210df6b0a9d23ef559ccf21822d1bb97288a114 Mon Sep 17 00:00:00 2001 From: Karagwa Date: Thu, 23 Jan 2025 15:41:20 +0300 Subject: [PATCH 09/57] Made improvements on the guest experience PR- removed unused import Fixed the theme conditional on main --- mobile-v3/lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile-v3/lib/main.dart b/mobile-v3/lib/main.dart index d36b5fa091..bb412fb5f1 100644 --- a/mobile-v3/lib/main.dart +++ b/mobile-v3/lib/main.dart @@ -124,7 +124,7 @@ class AirqoMobile extends StatelessWidget { return MaterialApp( debugShowCheckedModeBanner: false, - theme: isLightTheme ? AppTheme.darkTheme : AppTheme.lightTheme, + theme: isLightTheme ? AppTheme.lightTheme : AppTheme.darkTheme, // theme: isLightTheme ? ThemeData( // splashColor: Colors.transparent, // highlightColor: Colors.transparent, @@ -137,7 +137,7 @@ class AirqoMobile extends StatelessWidget { // scaffoldBackgroundColor: Theme.of(context).scaffoldBackgroundColor, // brightness: Brightness.light), title: "AirQo", - home: GuestProfilePage(), + home: Decider(), ); }, ), From 4bfd629e30c68e0b7e8e20cb9295975a1a7446af Mon Sep 17 00:00:00 2001 From: Peter Kyeyune Date: Thu, 23 Jan 2025 15:49:16 +0300 Subject: [PATCH 10/57] Add package_info_plus plugin and enhance SettingsWidget with app version display and logout/delete account functionality --- .../plugins/GeneratedPluginRegistrant.java | 5 + .../ios/Runner/GeneratedPluginRegistrant.m | 7 + .../pages/widgets/settings_widget.dart | 138 +++++++++++++++--- mobile-v3/pubspec.lock | 24 +++ mobile-v3/pubspec.yaml | 1 + 5 files changed, 153 insertions(+), 22 deletions(-) diff --git a/mobile-v3/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/mobile-v3/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java index 5f086b275f..2de4621042 100644 --- a/mobile-v3/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/mobile-v3/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -30,6 +30,11 @@ public static void registerWith(@NonNull FlutterEngine flutterEngine) { } catch (Exception e) { Log.e(TAG, "Error registering plugin google_maps_flutter_android, io.flutter.plugins.googlemaps.GoogleMapsPlugin", e); } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e); + } try { flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); } catch (Exception e) { diff --git a/mobile-v3/ios/Runner/GeneratedPluginRegistrant.m b/mobile-v3/ios/Runner/GeneratedPluginRegistrant.m index 523e1cd8f2..e1a144462e 100644 --- a/mobile-v3/ios/Runner/GeneratedPluginRegistrant.m +++ b/mobile-v3/ios/Runner/GeneratedPluginRegistrant.m @@ -18,6 +18,12 @@ @import google_maps_flutter_ios; #endif +#if __has_include() +#import +#else +@import package_info_plus; +#endif + #if __has_include() #import #else @@ -29,6 +35,7 @@ @implementation GeneratedPluginRegistrant + (void)registerWithRegistry:(NSObject*)registry { [ConnectivityPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"ConnectivityPlusPlugin"]]; [FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]]; + [FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]]; [PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]]; } diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart index 0f652f6909..81c1cf1199 100644 --- a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart +++ b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart @@ -1,10 +1,101 @@ import 'package:flutter/material.dart'; import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:package_info/package_info.dart'; -class SettingsWidget extends StatelessWidget { +class SettingsWidget extends StatefulWidget { const SettingsWidget({super.key}); + @override + _SettingsWidgetState createState() => _SettingsWidgetState(); +} + +class _SettingsWidgetState extends State { + String _appVersion = ''; + bool _locationEnabled = true; + bool _notificationsEnabled = true; + + @override + void initState() { + super.initState(); + _getAppVersion(); + } + + Future _getAppVersion() async { + final packageInfo = await PackageInfo.fromPlatform(); + setState(() { + _appVersion = '${packageInfo.version}(${packageInfo.buildNumber})'; + }); + } + + void _showLogoutConfirmation() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Confirm Logout'), + content: const Text('Are you sure you want to log out?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + // TODO: Implement actual logout logic + // e.g., clear user session, revoke tokens + Navigator.of(context).pushReplacementNamed('/login'); + }, + child: const Text('Log Out'), + ), + ], + ), + ); + } + + void _showDeleteAccountDialog() { + final TextEditingController passwordController = TextEditingController(); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Account'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'WARNING: This action cannot be undone. All your data will be permanently deleted.', + style: TextStyle(color: Colors.red), + ), + const SizedBox(height: 16), + TextField( + controller: passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Enter Password to Confirm', + border: OutlineInputBorder(), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + // TODO: Implement actual account deletion logic + // Validate password, call backend deletion endpoint + Navigator.of(context).pushReplacementNamed('/login'); + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + child: const Text('Delete Account'), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; @@ -20,11 +111,14 @@ class SettingsWidget extends StatelessWidget { // Location Setting SettingsTile( - switchValue: true, + switchValue: _locationEnabled, iconPath: "assets/images/shared/location_icon.svg", title: "Location", onChanged: (value) { - print("Location setting: \$value"); + setState(() { + _locationEnabled = value; + }); + print("Location setting: $value"); }, description: "AirQo to use your precise location to locate the Air Quality of your nearest location", @@ -32,11 +126,14 @@ class SettingsWidget extends StatelessWidget { // Notifications Setting SettingsTile( - switchValue: true, + switchValue: _notificationsEnabled, iconPath: "assets/icons/notification.svg", title: "Notifications", onChanged: (value) { - print("Notifications setting: \$value"); + setState(() { + _notificationsEnabled = value; + }); + print("Notifications setting: $value"); }, description: "AirQo to send you in-app & push notifications & spike alerts.", @@ -44,18 +141,19 @@ class SettingsWidget extends StatelessWidget { // Send Feedback SettingsTile( - iconPath: "assets/images/shared/feedback_icon.svg", - title: "Send Feedback", - onChanged: (value) { - print("Send Feedback setting: \$value"); - }), + iconPath: "assets/images/shared/feedback_icon.svg", + title: "Send Feedback", + onChanged: (value) { + print("Send Feedback tapped"); + }, + ), // Our Story SettingsTile( iconPath: "assets/images/shared/airqo_story_icon.svg", title: "Our Story", onChanged: (value) { - print("Our Story setting: \$value"); + print("Our Story tapped"); }, ), @@ -64,7 +162,7 @@ class SettingsWidget extends StatelessWidget { iconPath: "assets/images/shared/terms_and_privacy.svg", title: "Terms and Privacy Policy", onChanged: (value) { - print("Terms and Privacy Policy setting: \$value"); + print("Terms and Privacy Policy tapped"); }, ), @@ -79,9 +177,7 @@ class SettingsWidget extends StatelessWidget { ), minimumSize: Size.fromHeight(screenHeight * 0.07), ), - onPressed: () { - print("Logout tapped"); - }, + onPressed: _showLogoutConfirmation, child: const Text( "Log out", style: TextStyle( @@ -96,9 +192,7 @@ class SettingsWidget extends StatelessWidget { Padding( padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.3), child: InkWell( - onTap: () { - print("Delete Account tapped"); - }, + onTap: _showDeleteAccountDialog, child: Text( "Delete Account", style: TextStyle( @@ -121,9 +215,9 @@ class SettingsWidget extends StatelessWidget { height: screenHeight * 0.05, ), SizedBox(height: screenHeight * 0.01), - const Text( - "3.40.1(1)", - style: TextStyle( + Text( + _appVersion, + style: const TextStyle( color: Colors.grey, fontSize: 12, ), @@ -152,4 +246,4 @@ class SettingsWidget extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/mobile-v3/pubspec.lock b/mobile-v3/pubspec.lock index 1f13bd2d3b..8dd601d822 100644 --- a/mobile-v3/pubspec.lock +++ b/mobile-v3/pubspec.lock @@ -645,6 +645,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790" + url: "https://pub.dev" + source: hosted + version: "8.1.3" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b + url: "https://pub.dev" + source: hosted + version: "3.0.2" path: dependency: transitive description: @@ -978,6 +994,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + url: "https://pub.dev" + source: hosted + version: "5.10.0" xdg_directories: dependency: transitive description: diff --git a/mobile-v3/pubspec.yaml b/mobile-v3/pubspec.yaml index a8f89d228d..6fcb1b26a8 100644 --- a/mobile-v3/pubspec.yaml +++ b/mobile-v3/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: connectivity_plus: ^6.1.0 flutter_loggy: ^2.0.3+1 jwt_decoder: ^2.0.1 + package_info_plus: ^8.1.3 dev_dependencies: flutter_test: From 0af7a80c2a484cecc7e85e4456bf4208422596ae Mon Sep 17 00:00:00 2001 From: Peter Kyeyune Date: Thu, 23 Jan 2025 15:54:55 +0300 Subject: [PATCH 11/57] Update package_info dependency to package_info_plus for improved functionality --- .../lib/src/app/profile/pages/widgets/settings_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart index 81c1cf1199..bdefe87cc9 100644 --- a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart +++ b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; class SettingsWidget extends StatefulWidget { const SettingsWidget({super.key}); @@ -246,4 +246,4 @@ class _SettingsWidgetState extends State { ), ); } -} \ No newline at end of file +} From 28a29c3f67c96e41de7cdb9437782c00cd0f1a27 Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Fri, 24 Jan 2025 05:05:17 +0300 Subject: [PATCH 12/57] fix current org change --- .../common/components/AQNumberCard/index.jsx | 2 +- .../Dropdowns/OrganizationDropdown.jsx | 15 ++++----- .../dataDownload/modules/DataDownload.jsx | 9 +++-- .../src/core/hooks/useGetActiveGroupId.jsx | 33 +++++++++++-------- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/platform/src/common/components/AQNumberCard/index.jsx b/platform/src/common/components/AQNumberCard/index.jsx index e9a7b64672..db48cd24bf 100644 --- a/platform/src/common/components/AQNumberCard/index.jsx +++ b/platform/src/common/components/AQNumberCard/index.jsx @@ -330,7 +330,7 @@ const AQNumberCard = ({ className = '' }) => { [dispatch], ); - if (loading) { + if (loading || isFetchingActiveGroup) { return (
{ setLoading(true); setSelectedGroupId(group._id); try { - const response = await dispatch( + await dispatch( updateUserPreferences({ user_id: userID, group_id: group._id, }), ); - if (response?.payload?.success) { - localStorage.setItem('activeGroup', JSON.stringify(group)); - dispatch(setOrganizationName(group.grp_title)); - } else { - console.warn('Failed to update user preferences'); - } } catch (error) { console.error('Error updating user preferences:', error); } finally { @@ -84,10 +78,15 @@ const OrganizationDropdown = () => { const handleDropdownSelect = useCallback( (group) => { if (group?._id !== activeGroupId) { + // Immediately update organization name + dispatch(setOrganizationName(group.grp_title)); + localStorage.setItem('activeGroup', JSON.stringify(group)); + + // Dispatch preferences update asynchronously handleUpdatePreferences(group); } }, - [activeGroupId, handleUpdatePreferences], + [activeGroupId, handleUpdatePreferences, dispatch], ); if (!activeGroupId || groupList.length === 0) { diff --git a/platform/src/common/components/Modal/dataDownload/modules/DataDownload.jsx b/platform/src/common/components/Modal/dataDownload/modules/DataDownload.jsx index 47b7c9c9db..9faabf3b3f 100644 --- a/platform/src/common/components/Modal/dataDownload/modules/DataDownload.jsx +++ b/platform/src/common/components/Modal/dataDownload/modules/DataDownload.jsx @@ -58,6 +58,7 @@ const DataDownload = ({ onClose }) => { id: activeGroupId, title: groupTitle, groupList, + loading: isFetchingActiveGroup, } = useGetActiveGroup(); const preferencesData = useSelector( (state) => state.defaults.individual_preferences, @@ -128,12 +129,16 @@ const DataDownload = ({ onClose }) => { * Fetch sites summary whenever the selected organization changes. */ useEffect(() => { + if (isFetchingActiveGroup) return; + if (formData.organization) { dispatch( - fetchSitesSummary({ group: formData.organization.name.toLowerCase() }), + fetchSitesSummary({ + group: formData.organization.name.toLowerCase(), + }), ); } - }, [dispatch, formData.organization]); + }, [dispatch, formData.organization, isFetchingActiveGroup]); /** * Clears all selected sites and resets form data. diff --git a/platform/src/core/hooks/useGetActiveGroupId.jsx b/platform/src/core/hooks/useGetActiveGroupId.jsx index fd896c33a6..acea9d2f7d 100644 --- a/platform/src/core/hooks/useGetActiveGroupId.jsx +++ b/platform/src/core/hooks/useGetActiveGroupId.jsx @@ -1,39 +1,44 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; export function useGetActiveGroup() { + const [activeGroup, setActiveGroup] = useState(null); const [loading, setLoading] = useState(true); const userInfo = useSelector((state) => state?.login?.userInfo); const chartData = useSelector((state) => state.chart); - const activeGroupFromStorage = useMemo(() => { - try { - return JSON.parse(localStorage.getItem('activeGroup') || 'null'); - } catch (error) { - console.error('Error parsing activeGroup from local storage:', error); - return null; - } - }, []); + useEffect(() => { + setLoading(false); + }, [userInfo]); useEffect(() => { + setLoading(true); + + const matchingGroup = userInfo?.groups?.find( + (group) => group.grp_title.toLowerCase() === chartData?.organizationName, + ); + + setActiveGroup(matchingGroup); setLoading(false); - }, [userInfo, activeGroupFromStorage]); + }, [chartData?.organizationName]); // If no userInfo or groups, return stored or default values if (!userInfo || !userInfo.groups || !chartData?.organizationName) { return { loading, - id: activeGroupFromStorage?.id || null, - title: activeGroupFromStorage?.grp_title || null, + id: activeGroup?.id || null, + title: activeGroup?.grp_title || null, userID: userInfo?.id || null, groupList: userInfo?.groups || [], }; } // Prioritize stored group if it exists in user's groups - if (activeGroupFromStorage) { + if (chartData.organizationName) { const storedGroupInUserGroups = userInfo.groups.find( - (group) => group._id === activeGroupFromStorage._id, + (group) => + group.grp_title.toLowerCase() === + chartData.organizationName.toLowerCase(), ); if (storedGroupInUserGroups) { From a4f28e9ee9dc696635d42226f3d32cdd8c89803e Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Fri, 24 Jan 2025 05:19:33 +0300 Subject: [PATCH 13/57] lowercase --- platform/src/core/hooks/useGetActiveGroupId.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/platform/src/core/hooks/useGetActiveGroupId.jsx b/platform/src/core/hooks/useGetActiveGroupId.jsx index acea9d2f7d..b58a41afc0 100644 --- a/platform/src/core/hooks/useGetActiveGroupId.jsx +++ b/platform/src/core/hooks/useGetActiveGroupId.jsx @@ -15,7 +15,9 @@ export function useGetActiveGroup() { setLoading(true); const matchingGroup = userInfo?.groups?.find( - (group) => group.grp_title.toLowerCase() === chartData?.organizationName, + (group) => + group.grp_title.toLowerCase() === + chartData?.organizationName.toLowerCase(), ); setActiveGroup(matchingGroup); From 21eabe017eca746d4b1f1eca4cf99bba5fee6080 Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Fri, 24 Jan 2025 05:24:36 +0300 Subject: [PATCH 14/57] id consistency --- platform/src/core/hooks/useGetActiveGroupId.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/src/core/hooks/useGetActiveGroupId.jsx b/platform/src/core/hooks/useGetActiveGroupId.jsx index b58a41afc0..ca8dfbc006 100644 --- a/platform/src/core/hooks/useGetActiveGroupId.jsx +++ b/platform/src/core/hooks/useGetActiveGroupId.jsx @@ -28,9 +28,9 @@ export function useGetActiveGroup() { if (!userInfo || !userInfo.groups || !chartData?.organizationName) { return { loading, - id: activeGroup?.id || null, + id: activeGroup?._id || null, title: activeGroup?.grp_title || null, - userID: userInfo?.id || null, + userID: userInfo?._id || null, groupList: userInfo?.groups || [], }; } From 13c4d1116270cdd6f18abbe0f8e8b3527536e781 Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Fri, 24 Jan 2025 05:25:37 +0300 Subject: [PATCH 15/57] remove unncessaring loading state update --- platform/src/core/hooks/useGetActiveGroupId.jsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/platform/src/core/hooks/useGetActiveGroupId.jsx b/platform/src/core/hooks/useGetActiveGroupId.jsx index ca8dfbc006..3ab1f61849 100644 --- a/platform/src/core/hooks/useGetActiveGroupId.jsx +++ b/platform/src/core/hooks/useGetActiveGroupId.jsx @@ -7,10 +7,6 @@ export function useGetActiveGroup() { const userInfo = useSelector((state) => state?.login?.userInfo); const chartData = useSelector((state) => state.chart); - useEffect(() => { - setLoading(false); - }, [userInfo]); - useEffect(() => { setLoading(true); From 4d78bb3ed13184d0680defc11d7974c5127bc370 Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Fri, 24 Jan 2025 05:30:47 +0300 Subject: [PATCH 16/57] remove duplicate org name matching logic --- .../src/core/hooks/useGetActiveGroupId.jsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/platform/src/core/hooks/useGetActiveGroupId.jsx b/platform/src/core/hooks/useGetActiveGroupId.jsx index 3ab1f61849..0ab5dcba4f 100644 --- a/platform/src/core/hooks/useGetActiveGroupId.jsx +++ b/platform/src/core/hooks/useGetActiveGroupId.jsx @@ -1,6 +1,11 @@ import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; +const findGroupByOrgName = (groups, orgName) => + groups?.find( + (group) => group.grp_title.toLowerCase() === orgName?.toLowerCase(), + ); + export function useGetActiveGroup() { const [activeGroup, setActiveGroup] = useState(null); const [loading, setLoading] = useState(true); @@ -10,10 +15,9 @@ export function useGetActiveGroup() { useEffect(() => { setLoading(true); - const matchingGroup = userInfo?.groups?.find( - (group) => - group.grp_title.toLowerCase() === - chartData?.organizationName.toLowerCase(), + const matchingGroup = findGroupByOrgName( + userInfo?.groups, + chartData?.organizationName, ); setActiveGroup(matchingGroup); @@ -33,10 +37,9 @@ export function useGetActiveGroup() { // Prioritize stored group if it exists in user's groups if (chartData.organizationName) { - const storedGroupInUserGroups = userInfo.groups.find( - (group) => - group.grp_title.toLowerCase() === - chartData.organizationName.toLowerCase(), + const storedGroupInUserGroups = findGroupByOrgName( + userInfo?.groups, + chartData?.organizationName, ); if (storedGroupInUserGroups) { @@ -51,8 +54,9 @@ export function useGetActiveGroup() { } // Find group matching chart organization name - const matchingGroup = userInfo.groups.find( - (group) => group.grp_title === chartData.organizationName, + const matchingGroup = findGroupByOrgName( + userInfo?.groups, + chartData?.organizationName, ); if (matchingGroup) { From 4e49195a42f05238c8fbb486b18069d2df1a2f76 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 08:19:41 +0300 Subject: [PATCH 17/57] Update next platform staging image tag to stage-68854d0b-1737695785 --- k8s/platform/values-stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/platform/values-stage.yaml b/k8s/platform/values-stage.yaml index 878abec559..5c6fd2612b 100644 --- a/k8s/platform/values-stage.yaml +++ b/k8s/platform/values-stage.yaml @@ -2,7 +2,7 @@ replicaCount: 1 image: repository: eu.gcr.io/airqo-250220/airqo-stage-next-platform pullPolicy: Always - tag: stage-87a3d931-1737641477 + tag: stage-68854d0b-1737695785 imagePullSecrets: [] nameOverride: '' fullnameOverride: '' From 80a95de08ecf54260e8cf815e14c29dc136868ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 08:25:58 +0300 Subject: [PATCH 18/57] Update analytics platform production image tag to prod-87212ef4-1737696175 --- k8s/platform/values-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/platform/values-prod.yaml b/k8s/platform/values-prod.yaml index ac1a3d5f24..95c3ae695d 100644 --- a/k8s/platform/values-prod.yaml +++ b/k8s/platform/values-prod.yaml @@ -2,7 +2,7 @@ replicaCount: 1 image: repository: eu.gcr.io/airqo-250220/airqo-next-platform pullPolicy: Always - tag: prod-48adfeaa-1737641861 + tag: prod-87212ef4-1737696175 imagePullSecrets: [] nameOverride: '' fullnameOverride: '' From f9d6fd85ddf5ba292bcb775a835ab30148ba9914 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 11:08:09 +0300 Subject: [PATCH 19/57] commit --- .../app/(authenticated)/clients/columns.tsx | 74 +++++++++ .../(authenticated)/clients/data-table.tsx | 100 +++++++++++++ .../app/(authenticated)/clients/dialogs.tsx | 56 +++++++ .../app/(authenticated)/clients/page.tsx | 98 ++++++++++++ netmanager-app/components/ui/alert-dialog.tsx | 141 ++++++++++++++++++ netmanager-app/components/ui/button.tsx | 22 +-- netmanager-app/core/apis/analytics.ts | 12 +- netmanager-app/package-lock.json | 98 +++++++++++- netmanager-app/package.json | 2 + 9 files changed, 585 insertions(+), 18 deletions(-) create mode 100644 netmanager-app/app/(authenticated)/clients/columns.tsx create mode 100644 netmanager-app/app/(authenticated)/clients/data-table.tsx create mode 100644 netmanager-app/app/(authenticated)/clients/dialogs.tsx create mode 100644 netmanager-app/app/(authenticated)/clients/page.tsx create mode 100644 netmanager-app/components/ui/alert-dialog.tsx diff --git a/netmanager-app/app/(authenticated)/clients/columns.tsx b/netmanager-app/app/(authenticated)/clients/columns.tsx new file mode 100644 index 0000000000..5a223c6ca1 --- /dev/null +++ b/netmanager-app/app/(authenticated)/clients/columns.tsx @@ -0,0 +1,74 @@ +"use client" + +import type { ColumnDef } from "@tanstack/react-table" +import { Button } from "@/components/ui/button" +import { ArrowUpDown, MoreHorizontal } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ) + }, + }, + { + accessorKey: "_id", + header: "Client ID", + }, + { + accessorKey: "ip_address", + header: "Client IP", + }, + { + accessorKey: "isActive", + header: "Status", + cell: ({ row }) => { + const isActive = row.getValue("isActive") + return ( +
+ {isActive ? "Activated" : "Not Activated"} +
+ ) + }, + }, + { + id: "actions", + cell: ({ row }) => { + const client = row.original + + return ( + + + + + + Actions + navigator.clipboard.writeText(client._id)}> + Copy client ID + + + View client details + Update client information + + + ) + }, + }, +] + diff --git a/netmanager-app/app/(authenticated)/clients/data-table.tsx b/netmanager-app/app/(authenticated)/clients/data-table.tsx new file mode 100644 index 0000000000..ecf9395330 --- /dev/null +++ b/netmanager-app/app/(authenticated)/clients/data-table.tsx @@ -0,0 +1,100 @@ +"use client" + +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + getSortedRowModel, + type SortingState, + getFilteredRowModel, + type ColumnFiltersState, +} from "@tanstack/react-table" + +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import React from "react" + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ columns, data }: DataTableProps) { + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState([]) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + }, + }) + + return ( +
+
+ table.getColumn("name")?.setFilterValue(event.target.value)} + className="max-w-sm" + /> +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+ + +
+
+ ) +} + diff --git a/netmanager-app/app/(authenticated)/clients/dialogs.tsx b/netmanager-app/app/(authenticated)/clients/dialogs.tsx new file mode 100644 index 0000000000..43445b6653 --- /dev/null +++ b/netmanager-app/app/(authenticated)/clients/dialogs.tsx @@ -0,0 +1,56 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + } from "@/components/ui/alert-dialog" + + interface DialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onConfirm: () => void + } + + export function ActivateClientDialog({ open, onOpenChange, onConfirm }: DialogProps) { + return ( + + + + Activate Client + + Are you sure you want to activate this client? This action cannot be undone. + + + + Cancel + Activate + + + + ) + } + + export function DeactivateClientDialog({ open, onOpenChange, onConfirm }: DialogProps) { + return ( + + + + Deactivate Client + + Are you sure you want to deactivate this client? This action cannot be undone. + + + + Cancel + Deactivate + + + + ) + } + + \ No newline at end of file diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx new file mode 100644 index 0000000000..dfbfe9dab4 --- /dev/null +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -0,0 +1,98 @@ +"use client" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { DataTable } from "./data-table" +import { columns } from "./columns" +import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" +import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" +import { useToast } from "@/components/ui/use-toast" + +const ClientManagement = () => { + const [clients, setClients] = useState<{ _id: string; isActive: boolean }[]>([]) + const [loading, setLoading] = useState(false) + const [selectedClient, setSelectedClient] = useState(null) + const [activateDialogOpen, setActivateDialogOpen] = useState(false) + const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false) + const { toast } = useToast() + + const fetchClients = async () => { + setLoading(true) + try { + const response = await getClientsApi() + setClients(response.clients) + } catch (error) { + toast({ + title: "Error", + description: "Failed to fetch clients", + variant: "destructive", + }) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchClients() + }, []) + + const handleActivateDeactivate = async (clientId: string, activate: boolean) => { + try { + await activateUserClientApi({ _id: clientId, isActive: activate }) + await fetchClients() + toast({ + title: "Success", + description: `Client ${activate ? "activated" : "deactivated"} successfully`, + }) + } catch (error) { + toast({ + title: "Error", + description: `Failed to ${activate ? "activate" : "deactivate"} client`, + variant: "destructive", + }) + } finally { + setActivateDialogOpen(false) + setDeactivateDialogOpen(false) + } + } + + const activatedClients = clients.filter((client) => client.isActive).length + const deactivatedClients = clients.filter((client) => !client.isActive).length + + return ( +
+
+

Client Management

+ +
+ +
+
+

Activated Clients

+

{activatedClients}

+
+
+

Deactivated Clients

+

{deactivatedClients}

+
+
+ + + + handleActivateDeactivate(selectedClient?._id, true)} + /> + + handleActivateDeactivate(selectedClient?._id, false)} + /> +
+ ) +} + +export default ClientManagement + diff --git a/netmanager-app/components/ui/alert-dialog.tsx b/netmanager-app/components/ui/alert-dialog.tsx new file mode 100644 index 0000000000..25e7b47446 --- /dev/null +++ b/netmanager-app/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/netmanager-app/components/ui/button.tsx b/netmanager-app/components/ui/button.tsx index f2329bdfe6..36496a2872 100644 --- a/netmanager-app/components/ui/button.tsx +++ b/netmanager-app/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", @@ -31,26 +31,26 @@ const buttonVariants = cva( size: "default", }, } -); +) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean; + asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; + const Comp = asChild ? Slot : "button" return ( - ); + ) } -); -Button.displayName = "Button"; +) +Button.displayName = "Button" -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/netmanager-app/core/apis/analytics.ts b/netmanager-app/core/apis/analytics.ts index 9f189c4cbc..b760d9ab6a 100644 --- a/netmanager-app/core/apis/analytics.ts +++ b/netmanager-app/core/apis/analytics.ts @@ -1,5 +1,5 @@ import createAxiosInstance from "./axiosConfig"; -import { ANALYTICS_MGT_URL } from "@/core/urls"; +import { ANALYTICS_MGT_URL, USERS_MGT_URL } from "@/core/urls"; const axiosInstance = createAxiosInstance(); @@ -26,3 +26,13 @@ export const dataExport = async (data: DataExportForm) => { { headers } ); }; + +export const getClientsApi = async () => { + return axiosInstance.get(`${USERS_MGT_URL}/clients`).then((response) => response.data); +}; + +export const activateUserClientApi = async (data: { _id: string; isActive: boolean }) => { + return axiosInstance + .put(`${USERS_MGT_URL}/clients/activate`, data) + .then((response) => response.data); +}; diff --git a/netmanager-app/package-lock.json b/netmanager-app/package-lock.json index 70377ac0f0..fecd809959 100644 --- a/netmanager-app/package-lock.json +++ b/netmanager-app/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", @@ -23,6 +24,7 @@ "@reduxjs/toolkit": "^2.5.0", "@tanstack/react-query": "^5.62.11", "@tanstack/react-query-devtools": "^5.62.11", + "@tanstack/react-table": "^8.20.6", "@types/leaflet": "^1.9.15", "@types/react-redux": "^7.1.34", "apexcharts": "^4.3.0", @@ -513,6 +515,33 @@ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.5.tgz", + "integrity": "sha512-1Y2sI17QzSZP58RjGtrklfSGIf3AF7U/HkD3aAcAnhOUJrm7+7GG1wRDFaUlSe0nW5B/t4mYd/+7RNbP2Wexug==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.5", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", @@ -643,14 +672,14 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", - "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz", + "integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", @@ -659,8 +688,34 @@ "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-slot": "1.1.1", "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.4.tgz", + "integrity": "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -1467,6 +1522,37 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-table": { + "version": "8.20.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", + "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", diff --git a/netmanager-app/package.json b/netmanager-app/package.json index ac64760639..d252c5532a 100644 --- a/netmanager-app/package.json +++ b/netmanager-app/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", @@ -24,6 +25,7 @@ "@reduxjs/toolkit": "^2.5.0", "@tanstack/react-query": "^5.62.11", "@tanstack/react-query-devtools": "^5.62.11", + "@tanstack/react-table": "^8.20.6", "@types/leaflet": "^1.9.15", "@types/react-redux": "^7.1.34", "apexcharts": "^4.3.0", From 3cabe59b3349bb03070abaf31a63ba2ea6da6522 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 11:17:12 +0300 Subject: [PATCH 20/57] Clients Activation --- .../app/(authenticated)/clients/columns.tsx | 23 +++- .../(authenticated)/clients/data-table.tsx | 13 ++- .../app/(authenticated)/clients/dialogs.tsx | 110 +++++++++--------- .../app/(authenticated)/clients/page.tsx | 29 ++++- netmanager-app/app/types/clients.ts | 27 +++++ 5 files changed, 134 insertions(+), 68 deletions(-) create mode 100644 netmanager-app/app/types/clients.ts diff --git a/netmanager-app/app/(authenticated)/clients/columns.tsx b/netmanager-app/app/(authenticated)/clients/columns.tsx index 5a223c6ca1..ac2a1a57c2 100644 --- a/netmanager-app/app/(authenticated)/clients/columns.tsx +++ b/netmanager-app/app/(authenticated)/clients/columns.tsx @@ -11,8 +11,14 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import type { Client } from "@/app/types/clients" -export const columns: ColumnDef[] = [ +interface ColumnProps { + onActivate: (client: Client) => void + onDeactivate: (client: Client) => void +} + +export const columns = ({ onActivate, onDeactivate }: ColumnProps): ColumnDef[] => [ { accessorKey: "name", header: ({ column }) => { @@ -29,8 +35,16 @@ export const columns: ColumnDef[] = [ header: "Client ID", }, { - accessorKey: "ip_address", - header: "Client IP", + accessorKey: "user.email", + header: "User Email", + }, + { + accessorKey: "access_token.expires", + header: "Token Expiry", + cell: ({ row }) => { + const expires = new Date(row.getValue("access_token.expires")) + return expires.toLocaleDateString() + }, }, { accessorKey: "isActive", @@ -63,6 +77,9 @@ export const columns: ColumnDef[] = [ Copy client ID + (client.isActive ? onDeactivate(client) : onActivate(client))}> + {client.isActive ? "Deactivate" : "Activate"} + View client details Update client information diff --git a/netmanager-app/app/(authenticated)/clients/data-table.tsx b/netmanager-app/app/(authenticated)/clients/data-table.tsx index ecf9395330..716e5a6389 100644 --- a/netmanager-app/app/(authenticated)/clients/data-table.tsx +++ b/netmanager-app/app/(authenticated)/clients/data-table.tsx @@ -17,19 +17,22 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import React from "react" +import type { Client } from "@/app/types/clients" -interface DataTableProps { - columns: ColumnDef[] - data: TData[] +interface DataTableProps { + columns: ColumnDef[] + data: Client[] + onActivate: (client: Client) => void + onDeactivate: (client: Client) => void } -export function DataTable({ columns, data }: DataTableProps) { +export function DataTable({ columns, data, onActivate, onDeactivate }: DataTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) const table = useReactTable({ data, - columns, + columns: columns({ onActivate, onDeactivate }), getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), onSortingChange: setSorting, diff --git a/netmanager-app/app/(authenticated)/clients/dialogs.tsx b/netmanager-app/app/(authenticated)/clients/dialogs.tsx index 43445b6653..9cfd7d51b1 100644 --- a/netmanager-app/app/(authenticated)/clients/dialogs.tsx +++ b/netmanager-app/app/(authenticated)/clients/dialogs.tsx @@ -1,56 +1,56 @@ import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - } from "@/components/ui/alert-dialog" - - interface DialogProps { - open: boolean - onOpenChange: (open: boolean) => void - onConfirm: () => void - } - - export function ActivateClientDialog({ open, onOpenChange, onConfirm }: DialogProps) { - return ( - - - - Activate Client - - Are you sure you want to activate this client? This action cannot be undone. - - - - Cancel - Activate - - - - ) - } - - export function DeactivateClientDialog({ open, onOpenChange, onConfirm }: DialogProps) { - return ( - - - - Deactivate Client - - Are you sure you want to deactivate this client? This action cannot be undone. - - - - Cancel - Deactivate - - - - ) - } - - \ No newline at end of file + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" + +interface DialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onConfirm: () => void + clientName?: string +} + +export function ActivateClientDialog({ open, onOpenChange, onConfirm, clientName }: DialogProps) { + return ( + + + + Activate Client + + Are you sure you want to activate the client {clientName}? This action cannot be undone. + + + + Cancel + Activate + + + + ) +} + +export function DeactivateClientDialog({ open, onOpenChange, onConfirm, clientName }: DialogProps) { + return ( + + + + Deactivate Client + + Are you sure you want to deactivate the client {clientName}? This action cannot be undone. + + + + Cancel + Deactivate + + + + ) +} + diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index dfbfe9dab4..adcce2d85f 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -7,11 +7,12 @@ import { columns } from "./columns" import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" +import type { Client } from "@/app/types/clients" const ClientManagement = () => { - const [clients, setClients] = useState<{ _id: string; isActive: boolean }[]>([]) + const [clients, setClients] = useState([]) const [loading, setLoading] = useState(false) - const [selectedClient, setSelectedClient] = useState(null) + const [selectedClient, setSelectedClient] = useState(null) const [activateDialogOpen, setActivateDialogOpen] = useState(false) const [deactivateDialogOpen, setDeactivateDialogOpen] = useState(false) const { toast } = useToast() @@ -53,12 +54,23 @@ const ClientManagement = () => { } finally { setActivateDialogOpen(false) setDeactivateDialogOpen(false) + setSelectedClient(null) } } const activatedClients = clients.filter((client) => client.isActive).length const deactivatedClients = clients.filter((client) => !client.isActive).length + const handleActivateClick = (client: Client) => { + setSelectedClient(client) + setActivateDialogOpen(true) + } + + const handleDeactivateClick = (client: Client) => { + setSelectedClient(client) + setDeactivateDialogOpen(true) + } + return (
@@ -77,18 +89,25 @@ const ClientManagement = () => {
- + handleActivateDeactivate(selectedClient?._id, true)} + onConfirm={() => selectedClient && handleActivateDeactivate(selectedClient._id, true)} + clientName={selectedClient?.name} /> handleActivateDeactivate(selectedClient?._id, false)} + onConfirm={() => selectedClient && handleActivateDeactivate(selectedClient._id, false)} + clientName={selectedClient?.name} />
) diff --git a/netmanager-app/app/types/clients.ts b/netmanager-app/app/types/clients.ts new file mode 100644 index 0000000000..9e0235a51c --- /dev/null +++ b/netmanager-app/app/types/clients.ts @@ -0,0 +1,27 @@ +import { UserDetails } from './users'; + + export interface AccessToken { + _id: string + permissions: string[] + scopes: string[] + expiredEmailSent: boolean + token: string + client_id: string + name: string + expires: string + createdAt: string + updatedAt: string + __v: number + } + + export interface Client { + _id: string + isActive: boolean + ip_addresses: string[] + name: string + client_secret: string + user: UserDetails + access_token: AccessToken + } + + \ No newline at end of file From dde8e137b3d36503f4c1ccc11a0ec2d554b7cadd Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 11:28:09 +0300 Subject: [PATCH 21/57] commit --- .../app/(authenticated)/clients/columns.tsx | 15 ++++++- .../app/(authenticated)/clients/page.tsx | 40 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/netmanager-app/app/(authenticated)/clients/columns.tsx b/netmanager-app/app/(authenticated)/clients/columns.tsx index ac2a1a57c2..398a793ae3 100644 --- a/netmanager-app/app/(authenticated)/clients/columns.tsx +++ b/netmanager-app/app/(authenticated)/clients/columns.tsx @@ -42,8 +42,19 @@ export const columns = ({ onActivate, onDeactivate }: ColumnProps): ColumnDef { - const expires = new Date(row.getValue("access_token.expires")) - return expires.toLocaleDateString() + const accessToken = row.original.access_token + if (!accessToken || !accessToken.expires) return "N/A" + const expires = new Date(accessToken.expires) + if (isNaN(expires.getTime())) return "Invalid Date" + + const now = new Date() + const diffTime = expires.getTime() - now.getTime() + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + + if (diffDays < 0) return "Expired" + if (diffDays === 0) return "Expires today" + if (diffDays === 1) return "Expires tomorrow" + return `Expires in ${diffDays} days` }, }, { diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index adcce2d85f..ae10c6142a 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -9,6 +9,21 @@ import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" import type { Client } from "@/app/types/clients" +const formatDate = (dateString: string | undefined): string => { + if (!dateString) return "N/A" + const date = new Date(dateString) + if (isNaN(date.getTime())) return "Invalid Date" + + const now = new Date() + const diffTime = date.getTime() - now.getTime() + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + + if (diffDays < 0) return "Expired" + if (diffDays === 0) return "Expires today" + if (diffDays === 1) return "Expires tomorrow" + return `Expires in ${diffDays} days` +} + const ClientManagement = () => { const [clients, setClients] = useState([]) const [loading, setLoading] = useState(false) @@ -71,6 +86,22 @@ const ClientManagement = () => { setDeactivateDialogOpen(true) } + const nearestExpiringToken = clients + .filter((client) => client.access_token && client.access_token.expires) + .filter((client) => { + const expiryDate = new Date(client.access_token.expires) + return expiryDate > new Date() + }) + .sort((a, b) => { + const dateA = new Date(a.access_token.expires).getTime() + const dateB = new Date(b.access_token.expires).getTime() + return dateA - dateB + })[0] + + const nearestExpiryDate = nearestExpiringToken + ? formatDate(nearestExpiringToken.access_token.expires) + : "No upcoming expiries" + return (
@@ -78,7 +109,7 @@ const ClientManagement = () => {
-
+

Activated Clients

{activatedClients}

@@ -87,6 +118,13 @@ const ClientManagement = () => {

Deactivated Clients

{deactivatedClients}

+
+

Nearest Token Expiry

+

+ {nearestExpiringToken ? formatDate(nearestExpiringToken.access_token.expires) : "No upcoming expiries"} +

+

{nearestExpiringToken?.name || "N/A"}

+
Date: Fri, 24 Jan 2025 11:40:23 +0300 Subject: [PATCH 22/57] Refactor ProfilePage layout for improved readability and responsiveness; update SettingsWidget state creation method --- .../src/app/profile/pages/profile_page.dart | 125 ++++++++++-------- .../pages/widgets/settings_widget.dart | 3 +- 2 files changed, 68 insertions(+), 60 deletions(-) diff --git a/mobile-v3/lib/src/app/profile/pages/profile_page.dart b/mobile-v3/lib/src/app/profile/pages/profile_page.dart index 0b7e749377..d82cecc6d2 100644 --- a/mobile-v3/lib/src/app/profile/pages/profile_page.dart +++ b/mobile-v3/lib/src/app/profile/pages/profile_page.dart @@ -42,69 +42,78 @@ class _ProfilePageState extends State { children: [ SizedBox( height: 100, - child: Row( - children: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: CircleAvatar( - backgroundColor: Theme.of(context).highlightColor, - child: Center( - child: SvgPicture.asset( - "assets/icons/user_icon.svg"), - ), - radius: 50, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: LayoutBuilder( + builder: (context, constraints) { + return Row( children: [ - Text( - "${firstName} ${lastName}", - style: TextStyle( - color: AppColors.boldHeadlineColor, - fontSize: 24, - fontWeight: FontWeight.w700, + Container( + margin: + const EdgeInsets.symmetric(horizontal: 16), + child: CircleAvatar( + backgroundColor: + Theme.of(context).highlightColor, + child: Center( + child: SvgPicture.asset( + "assets/icons/user_icon.svg"), + ), + radius: 50, ), ), - Spacer(), - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 32), - height: 50, - child: Center( - child: Text("Edit your profile")), - // child: InkWell( - // onTap: () => Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) => - // EditProfile())), - // child: Text( - // "Edit your profile", - // style: TextStyle( - // fontWeight: FontWeight.w500, - // color: Colors.white, - // ), - // ), - //)), - decoration: BoxDecoration( - color: Theme.of(context).highlightColor, - borderRadius: - BorderRadius.circular(200)), - ), - SizedBox(width: 8), - CircleAvatar( - backgroundColor: - Theme.of(context).highlightColor, - radius: 26, - child: SvgPicture.asset( - "assets/icons/notification.svg")) - ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${firstName} ${lastName}", + style: TextStyle( + color: AppColors.boldHeadlineColor, + fontSize: 24, + fontWeight: FontWeight.w700, + ), + ), + Spacer(), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 32), + height: 50, + child: Center( + child: Text("Edit your profile")), + // child: InkWell( + // onTap: () => Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => + // EditProfile())), + // child: Text( + // "Edit your profile", + // style: TextStyle( + // fontWeight: FontWeight.w500, + // color: Colors.white, + // ), + // ), + //)), + decoration: BoxDecoration( + color: Theme.of(context) + .highlightColor, + borderRadius: + BorderRadius.circular(200)), + ), + SizedBox(width: 8), + CircleAvatar( + backgroundColor: + Theme.of(context).highlightColor, + radius: 26, + child: SvgPicture.asset( + "assets/icons/notification.svg")) + ], + ) + ], + ), ) ], - ) - ], + ); + }, ), ), SizedBox(height: 32), diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart index bdefe87cc9..1a02748f34 100644 --- a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart +++ b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart @@ -7,9 +7,8 @@ class SettingsWidget extends StatefulWidget { const SettingsWidget({super.key}); @override - _SettingsWidgetState createState() => _SettingsWidgetState(); + State createState() => _SettingsWidgetState(); } - class _SettingsWidgetState extends State { String _appVersion = ''; bool _locationEnabled = true; From 7bc0bc354190c6adc0c1b547bfb93359fdb0fda5 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 11:55:57 +0300 Subject: [PATCH 23/57] commit --- .../app/(authenticated)/clients/page.tsx | 3 +- netmanager-app/app/pageAccess.tsx | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 netmanager-app/app/pageAccess.tsx diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index ae10c6142a..b8c0f38088 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -8,6 +8,7 @@ import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" import type { Client } from "@/app/types/clients" +import withPermission from "@/app/pageAccess" const formatDate = (dateString: string | undefined): string => { if (!dateString) return "N/A" @@ -151,5 +152,5 @@ const ClientManagement = () => { ) } -export default ClientManagement +export default withPermission(ClientManagement, 'CREATE_UPDATE_AND_DELETE_NETWORK_DEVICES'); diff --git a/netmanager-app/app/pageAccess.tsx b/netmanager-app/app/pageAccess.tsx new file mode 100644 index 0000000000..4ca6d4dca3 --- /dev/null +++ b/netmanager-app/app/pageAccess.tsx @@ -0,0 +1,38 @@ +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useAppSelector } from "@/core/redux/hooks"; +import type { RootState } from "@/core/redux/store"; + +const withPermission =

( + Component: React.ComponentType

, + requiredPermission: string +): React.FC

=> { + const WithPermission: React.FC

= (props) => { + const router = useRouter(); + const currentRole = useAppSelector( + (state: RootState) => state.user.currentRole + ); + const [hasPermission, setHasPermission] = useState(false); + + useEffect(() => { + if (currentRole) { + const permissionExists = currentRole.permissions.includes( + requiredPermission + ); + setHasPermission(permissionExists); + + if (!permissionExists) { + router.push("/permission-denied"); + } + } + }, [currentRole, requiredPermission, router]); + + if (!hasPermission) return null; + + return ; + }; + + return WithPermission; +}; + +export default withPermission; From cf84b1a2a9826c4fcd971cb72abe1ad6ca83a894 Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Fri, 24 Jan 2025 12:19:48 +0300 Subject: [PATCH 24/57] Sidebar Client --- .../app/(authenticated)/clients/page.tsx | 3 +- netmanager-app/app/pageAccess.tsx | 38 ------------------- netmanager-app/components/sidebar.tsx | 16 ++++++++ 3 files changed, 17 insertions(+), 40 deletions(-) delete mode 100644 netmanager-app/app/pageAccess.tsx diff --git a/netmanager-app/app/(authenticated)/clients/page.tsx b/netmanager-app/app/(authenticated)/clients/page.tsx index b8c0f38088..f5aa65dcaa 100644 --- a/netmanager-app/app/(authenticated)/clients/page.tsx +++ b/netmanager-app/app/(authenticated)/clients/page.tsx @@ -8,7 +8,6 @@ import { ActivateClientDialog, DeactivateClientDialog } from "./dialogs" import { getClientsApi, activateUserClientApi } from "@/core/apis/analytics" import { useToast } from "@/components/ui/use-toast" import type { Client } from "@/app/types/clients" -import withPermission from "@/app/pageAccess" const formatDate = (dateString: string | undefined): string => { if (!dateString) return "N/A" @@ -152,5 +151,5 @@ const ClientManagement = () => { ) } -export default withPermission(ClientManagement, 'CREATE_UPDATE_AND_DELETE_NETWORK_DEVICES'); +export default ClientManagement; diff --git a/netmanager-app/app/pageAccess.tsx b/netmanager-app/app/pageAccess.tsx deleted file mode 100644 index 4ca6d4dca3..0000000000 --- a/netmanager-app/app/pageAccess.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { useAppSelector } from "@/core/redux/hooks"; -import type { RootState } from "@/core/redux/store"; - -const withPermission =

( - Component: React.ComponentType

, - requiredPermission: string -): React.FC

=> { - const WithPermission: React.FC

= (props) => { - const router = useRouter(); - const currentRole = useAppSelector( - (state: RootState) => state.user.currentRole - ); - const [hasPermission, setHasPermission] = useState(false); - - useEffect(() => { - if (currentRole) { - const permissionExists = currentRole.permissions.includes( - requiredPermission - ); - setHasPermission(permissionExists); - - if (!permissionExists) { - router.push("/permission-denied"); - } - } - }, [currentRole, requiredPermission, router]); - - if (!hasPermission) return null; - - return ; - }; - - return WithPermission; -}; - -export default withPermission; diff --git a/netmanager-app/components/sidebar.tsx b/netmanager-app/components/sidebar.tsx index c419bf1987..2dc2b65a10 100644 --- a/netmanager-app/components/sidebar.tsx +++ b/netmanager-app/components/sidebar.tsx @@ -18,6 +18,7 @@ import { Map, ChevronDown, Check, + Hospital, PlusCircle, MonitorSmartphone, LogOut, @@ -210,6 +211,21 @@ const Sidebar = () => { + +

  • + + + Clients + +
  • +
  • Date: Fri, 24 Jan 2025 14:42:55 +0300 Subject: [PATCH 25/57] Trigger Website Build to apply new app engine configs --- website2/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website2/README.md b/website2/README.md index e215bc4ccf..cf37132405 100644 --- a/website2/README.md +++ b/website2/README.md @@ -20,7 +20,7 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. -## Learn More +## Learn More. To learn more about Next.js, take a look at the following resources: From 9ddb4be8c76f8d9d9267432d4e70cbb4043f8f1c Mon Sep 17 00:00:00 2001 From: Benjamin Ssempala <86492979+BenjaminSsempala@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:50:03 +0300 Subject: [PATCH 26/57] Fix outdated config variable --- .github/workflows/deploy-frontend-pr-previews.yml | 4 ++-- .github/workflows/deploy-frontends-to-production.yml | 4 ++-- .github/workflows/deploy-frontends-to-staging.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-frontend-pr-previews.yml b/.github/workflows/deploy-frontend-pr-previews.yml index 068e0d9740..4907babcd8 100644 --- a/.github/workflows/deploy-frontend-pr-previews.yml +++ b/.github/workflows/deploy-frontend-pr-previews.yml @@ -539,7 +539,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period: 120s" >> app.yaml + echo " cool_down_period_sec: 120s" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 3" >> app.yaml echo " cpu_utilization:" >> app.yaml @@ -653,7 +653,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period: 120s" >> app.yaml + echo " cool_down_period_sec: 120s" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 3" >> app.yaml echo " cpu_utilization:" >> app.yaml diff --git a/.github/workflows/deploy-frontends-to-production.yml b/.github/workflows/deploy-frontends-to-production.yml index 85f33552a3..7e339a6b1d 100644 --- a/.github/workflows/deploy-frontends-to-production.yml +++ b/.github/workflows/deploy-frontends-to-production.yml @@ -175,7 +175,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period: 120s" >> app.yaml + echo " cool_down_period_sec: 120s" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 10" >> app.yaml echo " cpu_utilization:" >> app.yaml @@ -278,7 +278,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period: 80s" >> app.yaml + echo " cool_down_period_sec: 80s" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 10" >> app.yaml echo " cpu_utilization:" >> app.yaml diff --git a/.github/workflows/deploy-frontends-to-staging.yml b/.github/workflows/deploy-frontends-to-staging.yml index c7a478ea62..f77138409b 100644 --- a/.github/workflows/deploy-frontends-to-staging.yml +++ b/.github/workflows/deploy-frontends-to-staging.yml @@ -230,7 +230,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period: 120s" >> app.yaml + echo " cool_down_period_sec: 120s" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 5" >> app.yaml echo " cpu_utilization:" >> app.yaml @@ -330,7 +330,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period: 120s" >> app.yaml + echo " cool_down_period_sec: 120s" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 5" >> app.yaml echo " cpu_utilization:" >> app.yaml From acedbc40e7cf571612784bbe9aab99b988d39bc7 Mon Sep 17 00:00:00 2001 From: Benjamin Ssempala <86492979+BenjaminSsempala@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:51:14 +0300 Subject: [PATCH 27/57] remove s --- .github/workflows/deploy-frontend-pr-previews.yml | 2 +- .github/workflows/deploy-frontends-to-production.yml | 2 +- .github/workflows/deploy-frontends-to-staging.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-frontend-pr-previews.yml b/.github/workflows/deploy-frontend-pr-previews.yml index 4907babcd8..fbfc2b924a 100644 --- a/.github/workflows/deploy-frontend-pr-previews.yml +++ b/.github/workflows/deploy-frontend-pr-previews.yml @@ -653,7 +653,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period_sec: 120s" >> app.yaml + echo " cool_down_period_sec: 120" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 3" >> app.yaml echo " cpu_utilization:" >> app.yaml diff --git a/.github/workflows/deploy-frontends-to-production.yml b/.github/workflows/deploy-frontends-to-production.yml index 7e339a6b1d..550fd2493b 100644 --- a/.github/workflows/deploy-frontends-to-production.yml +++ b/.github/workflows/deploy-frontends-to-production.yml @@ -278,7 +278,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period_sec: 80s" >> app.yaml + echo " cool_down_period_sec: 80" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 10" >> app.yaml echo " cpu_utilization:" >> app.yaml diff --git a/.github/workflows/deploy-frontends-to-staging.yml b/.github/workflows/deploy-frontends-to-staging.yml index f77138409b..746b37e889 100644 --- a/.github/workflows/deploy-frontends-to-staging.yml +++ b/.github/workflows/deploy-frontends-to-staging.yml @@ -230,7 +230,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period_sec: 120s" >> app.yaml + echo " cool_down_period_sec: 120" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 5" >> app.yaml echo " cpu_utilization:" >> app.yaml From f1a33e99325b8c1934532f9ef16ad823de904727 Mon Sep 17 00:00:00 2001 From: Benjamin Ssempala <86492979+BenjaminSsempala@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:53:40 +0300 Subject: [PATCH 28/57] remove s --- .github/workflows/deploy-frontend-pr-previews.yml | 2 +- .github/workflows/deploy-frontends-to-production.yml | 2 +- .github/workflows/deploy-frontends-to-staging.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-frontend-pr-previews.yml b/.github/workflows/deploy-frontend-pr-previews.yml index fbfc2b924a..976c0a51b0 100644 --- a/.github/workflows/deploy-frontend-pr-previews.yml +++ b/.github/workflows/deploy-frontend-pr-previews.yml @@ -539,7 +539,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period_sec: 120s" >> app.yaml + echo " cool_down_period_sec: 120" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 3" >> app.yaml echo " cpu_utilization:" >> app.yaml diff --git a/.github/workflows/deploy-frontends-to-production.yml b/.github/workflows/deploy-frontends-to-production.yml index 550fd2493b..80cc3ccd66 100644 --- a/.github/workflows/deploy-frontends-to-production.yml +++ b/.github/workflows/deploy-frontends-to-production.yml @@ -175,7 +175,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period_sec: 120s" >> app.yaml + echo " cool_down_period_sec: 120" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 10" >> app.yaml echo " cpu_utilization:" >> app.yaml diff --git a/.github/workflows/deploy-frontends-to-staging.yml b/.github/workflows/deploy-frontends-to-staging.yml index 746b37e889..52154832fc 100644 --- a/.github/workflows/deploy-frontends-to-staging.yml +++ b/.github/workflows/deploy-frontends-to-staging.yml @@ -330,7 +330,7 @@ jobs: echo "includes:" >> app.yaml echo " - .env.yaml" >> app.yaml echo "automatic_scaling:" >> app.yaml - echo " cool_down_period_sec: 120s" >> app.yaml + echo " cool_down_period_sec: 120" >> app.yaml echo " min_num_instances: 1" >> app.yaml echo " max_num_instances: 5" >> app.yaml echo " cpu_utilization:" >> app.yaml From 0985d174ca4ddf7f227f84707bd5fb7ff3445355 Mon Sep 17 00:00:00 2001 From: Benjamin Ssempala <86492979+BenjaminSsempala@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:04:31 +0300 Subject: [PATCH 29/57] Trigger Website Deployment --- website2/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website2/README.md b/website2/README.md index cf37132405..afd3a53343 100644 --- a/website2/README.md +++ b/website2/README.md @@ -1,6 +1,6 @@ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). -## Getting Started +## Getting Started. First, run the development server: From bee8d6d8d57ec8273105002832f63fd1d8c23d8e Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sun, 26 Jan 2025 19:59:11 +0300 Subject: [PATCH 30/57] fix file link on resources page --- website2/src/app/clean-air-forum/layout.tsx | 54 +++--- website2/src/hooks/useResourceFilter.ts | 41 ++++ website2/src/types/index.ts | 21 +++ website2/src/utils/string-utils.ts | 6 + .../src/views/cleanairforum/TabNavigation.tsx | 40 ++-- .../cleanairforum/resources/ResourceCard.tsx | 60 ++++++ .../cleanairforum/resources/ResourcePage.tsx | 175 +++++------------- 7 files changed, 225 insertions(+), 172 deletions(-) create mode 100644 website2/src/hooks/useResourceFilter.ts create mode 100644 website2/src/utils/string-utils.ts create mode 100644 website2/src/views/cleanairforum/resources/ResourceCard.tsx diff --git a/website2/src/app/clean-air-forum/layout.tsx b/website2/src/app/clean-air-forum/layout.tsx index 54f43a9fd1..8241bf6c22 100644 --- a/website2/src/app/clean-air-forum/layout.tsx +++ b/website2/src/app/clean-air-forum/layout.tsx @@ -1,7 +1,6 @@ 'use client'; import React, { ReactNode } from 'react'; -import { Suspense } from 'react'; import Footer from '@/components/layouts/Footer'; import Navbar from '@/components/layouts/Navbar'; @@ -17,37 +16,40 @@ type CleanAirLayoutProps = { const CleanAirLayout: React.FC = ({ children }) => { // Using the `useForumEvents` hook - const { forumEvents } = useForumEvents(); + const { forumEvents, isLoading } = useForumEvents(); // Extract the first event (if available) const eventData = forumEvents?.[0] || null; + // Loading state + if (isLoading) { + return ; + } + return ( - }> -
    - {/* Navbar */} -
    - -
    - - {/* Banner Section */} - - - {/* Main Content */} -
    {children}
    - - {/* Newsletter Section */} -
    - -
    - - {/* Footer */} -
    -
    -
    -
    -
    +
    + {/* Navbar */} +
    + +
    + + {/* Banner Section */} + + + {/* Main Content */} +
    {children}
    + + {/* Newsletter Section */} +
    + +
    + + {/* Footer */} +
    +
    +
    +
    ); }; diff --git a/website2/src/hooks/useResourceFilter.ts b/website2/src/hooks/useResourceFilter.ts new file mode 100644 index 0000000000..30f90ab8bc --- /dev/null +++ b/website2/src/hooks/useResourceFilter.ts @@ -0,0 +1,41 @@ +import { useMemo, useState } from 'react'; + +import type { Resource } from '@/types/index'; + +export const useResourceFilter = ( + resources: Resource[], + itemsPerPage: number, +) => { + const [selectedCategory, setSelectedCategory] = useState('All'); + const [currentPage, setCurrentPage] = useState(1); + + const filteredResources = useMemo(() => { + if (selectedCategory === 'All') return resources; + return resources.filter( + (resource) => + resource.resource_category === + selectedCategory.toLowerCase().replace(' ', '_'), + ); + }, [resources, selectedCategory]); + + const totalPages = Math.ceil(filteredResources.length / itemsPerPage); + + const paginatedResources = useMemo( + () => + filteredResources.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage, + ), + [filteredResources, currentPage, itemsPerPage], + ); + + return { + selectedCategory, + setSelectedCategory, + currentPage, + setCurrentPage, + filteredResources, + paginatedResources, + totalPages, + }; +}; diff --git a/website2/src/types/index.ts b/website2/src/types/index.ts index 85578ff142..b04173d81a 100644 --- a/website2/src/types/index.ts +++ b/website2/src/types/index.ts @@ -9,3 +9,24 @@ export interface PressArticle { website_category: string; article_tag: string; } + +export interface Resource { + id: number; + resource_file_url: string; + created: string; + modified: string; + is_deleted: boolean; + resource_title: string; + resource_link: string | null; + resource_file: string; + author_title: string; + resource_category: + | 'workshop_report' + | 'technical_report' + | 'toolkit' + | 'research_publication'; + resource_authors: string; + order: number; +} + +export type ResourceCategory = Resource['resource_category']; diff --git a/website2/src/utils/string-utils.ts b/website2/src/utils/string-utils.ts new file mode 100644 index 0000000000..6dd4fb6dd5 --- /dev/null +++ b/website2/src/utils/string-utils.ts @@ -0,0 +1,6 @@ +export const formatString = (category: string): string => { + return category + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +}; diff --git a/website2/src/views/cleanairforum/TabNavigation.tsx b/website2/src/views/cleanairforum/TabNavigation.tsx index d51687e4bd..28b50e024f 100644 --- a/website2/src/views/cleanairforum/TabNavigation.tsx +++ b/website2/src/views/cleanairforum/TabNavigation.tsx @@ -1,31 +1,33 @@ 'use client'; -import { usePathname, useRouter } from 'next/navigation'; -import React, { useState } from 'react'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import type React from 'react'; +import { useEffect, useState } from 'react'; + +const tabs = [ + { label: 'About', value: '/clean-air-network' }, + { label: 'Membership', value: '/clean-air-network/membership' }, + { label: 'Events', value: '/clean-air-network/events' }, + { label: 'Resources', value: '/clean-air-network/resources' }, +]; const TabNavigation: React.FC = () => { - const router = useRouter(); const pathname = usePathname(); const [activeTab, setActiveTab] = useState(pathname); - const tabs = [ - { label: 'About', value: '/clean-air-network' }, - { label: 'Membership', value: '/clean-air-network/membership' }, - { label: 'Events', value: '/clean-air-network/events' }, - { label: 'Resources', value: '/clean-air-network/resources' }, - ]; - - const handleTabClick = (value: string) => { - setActiveTab(value); - router.push(value); - }; + useEffect(() => { + setActiveTab(pathname); + }, [pathname]); return ( -
    +
    + ); }; diff --git a/website2/src/views/cleanairforum/resources/ResourceCard.tsx b/website2/src/views/cleanairforum/resources/ResourceCard.tsx new file mode 100644 index 0000000000..4fedfcb1c9 --- /dev/null +++ b/website2/src/views/cleanairforum/resources/ResourceCard.tsx @@ -0,0 +1,60 @@ +import type React from 'react'; + +import { CustomButton } from '@/components/ui'; +import type { Resource } from '@/types/index'; +import { formatString } from '@/utils/string-utils'; + +interface ResourceCardProps { + resource: Resource; +} + +const ResourceCard: React.FC = ({ resource }) => ( +
    +

    + {formatString(resource.resource_category)} +

    +

    + {resource.resource_title} +

    +
    +

    {resource.author_title}

    +

    {resource.resource_authors}

    +
    +
    + {resource.resource_link && ( + { + if (resource.resource_link) { + window.open( + resource.resource_link, + '_blank', + 'noopener,noreferrer', + ); + } + }} + > + Read action plan → + + )} + {resource.resource_file && resource.resource_file_url && ( + { + if (resource.resource_file_url) { + window.open( + resource.resource_file_url, + '_blank', + 'noopener,noreferrer', + ); + } + }} + > + Download + + )} +
    +
    +); + +export default ResourceCard; diff --git a/website2/src/views/cleanairforum/resources/ResourcePage.tsx b/website2/src/views/cleanairforum/resources/ResourcePage.tsx index a2b8e7295d..0d31955895 100644 --- a/website2/src/views/cleanairforum/resources/ResourcePage.tsx +++ b/website2/src/views/cleanairforum/resources/ResourcePage.tsx @@ -1,10 +1,9 @@ 'use client'; import Image from 'next/image'; -import React, { useMemo, useState } from 'react'; +import type React from 'react'; import { - CustomButton, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -13,73 +12,48 @@ import { Pagination, } from '@/components/ui'; import { useCleanAirResources } from '@/hooks/useApiHooks'; - -const ResourcePage = () => { +import { useResourceFilter } from '@/hooks/useResourceFilter'; +import type { Resource } from '@/types/index'; + +import ResourceCard from './ResourceCard'; + +const categories = [ + 'All', + 'Toolkit', + 'Technical Report', + 'Workshop Report', + 'Research Publication', +]; + +const LoadingSkeleton = ({ itemsPerPage }: { itemsPerPage: number }) => ( +
    + {Array.from({ length: itemsPerPage }).map((_, index) => ( +
    +
    +
    +
    +
    +
    +
    + ))} +
    +); + +const ResourcePage: React.FC = () => { const { cleanAirResources, isLoading, isError } = useCleanAirResources(); - const [selectedCategory, setSelectedCategory] = useState('All'); - const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 3; - // Categories for the dropdown filter - const categories = [ - 'All', - 'Toolkits', - 'Technical Reports', - 'Workshop Reports', - 'Research Publications', - ]; - - // Map and filter resources based on the selected category - const filteredResources = useMemo(() => { - if (selectedCategory === 'All') return cleanAirResources || []; - - const categoryMap: { [key: string]: string } = { - Toolkits: 'toolkit', - 'Technical Reports': 'technical_report', - 'Workshop Reports': 'workshop_report', - 'Research Publications': 'research_publication', - }; - - return ( - cleanAirResources?.filter( - (resource: any) => - resource.resource_category === categoryMap[selectedCategory], - ) || [] - ); - }, [cleanAirResources, selectedCategory]); - - // Pagination logic - const totalPages = Math.ceil(filteredResources.length / itemsPerPage); - const paginatedResources = useMemo( - () => - filteredResources.slice( - (currentPage - 1) * itemsPerPage, - currentPage * itemsPerPage, - ), - [filteredResources, currentPage, itemsPerPage], - ); - - // Loading Skeleton Component - const LoadingSkeleton = () => ( -
    - {Array.from({ length: itemsPerPage }).map((_, index) => ( -
    -
    -
    -
    -
    -
    -
    - ))} -
    - ); + const { + selectedCategory, + setSelectedCategory, + currentPage, + setCurrentPage, + paginatedResources, + totalPages, + } = useResourceFilter(cleanAirResources || [], itemsPerPage); return (
    - {/* Main banner section */}
    @@ -89,19 +63,18 @@ const ResourcePage = () => { width={800} height={400} className="rounded-lg object-contain w-full" + priority />
    - {/* Resource Filter and List */}

    Resource Center

    - {/* Filter Dropdown */}
    - {/* Loading and Error States */} - {isLoading && } + {isLoading && } {isError && ( )} - {/* Resource Cards */} {!isLoading && !isError && paginatedResources.length === 0 && ( )} {!isLoading && !isError && paginatedResources.length > 0 && (
    - {paginatedResources.map((resource: any, index: any) => ( -
    -

    - {resource.resource_category.toUpperCase()} -

    -

    - {resource.resource_title} -

    -
    -

    Created by

    -

    {resource.resource_authors}

    -
    -
    - {/* Read Action Plan Button */} - {resource.resource_link && ( - - window.open( - resource.resource_link, - '_blank', - 'noopener,noreferrer', - ) - } - > - Read action plan → - - )} - - {/* Download Button */} - {resource.resource_file && ( - - window.open( - resource.resource_file, - '_blank', - 'noopener,noreferrer', - ) - } - > - Download - - )} -
    -
    + {paginatedResources.map((resource: Resource) => ( + ))}
    )} - {/* Pagination */} - {!isLoading && - !isError && - filteredResources.length > itemsPerPage && ( - - )} + {!isLoading && !isError && totalPages > 1 && ( + + )}
    From 219d2b4aa9d53247d109334a040b5a9917866397 Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sun, 26 Jan 2025 20:00:23 +0300 Subject: [PATCH 31/57] text change --- website2/src/views/cleanairforum/resources/ResourceCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website2/src/views/cleanairforum/resources/ResourceCard.tsx b/website2/src/views/cleanairforum/resources/ResourceCard.tsx index 4fedfcb1c9..adb27ee6aa 100644 --- a/website2/src/views/cleanairforum/resources/ResourceCard.tsx +++ b/website2/src/views/cleanairforum/resources/ResourceCard.tsx @@ -34,7 +34,7 @@ const ResourceCard: React.FC = ({ resource }) => ( } }} > - Read action plan → + Read more → )} {resource.resource_file && resource.resource_file_url && ( From f80b4e5e53b11dada64925fa8c9997d96b07279f Mon Sep 17 00:00:00 2001 From: Ochieng Paul Date: Sun, 26 Jan 2025 21:26:59 +0300 Subject: [PATCH 32/57] set up main config settings for whole website --- website2/src/app/legal/layout.tsx | 5 +- .../components/dialogs/EngagementDialog.tsx | 5 +- .../src/components/layouts/ActionButtons.tsx | 5 +- .../src/components/layouts/ActionButtons2.tsx | 6 +- website2/src/components/layouts/Footer.tsx | 4 +- website2/src/components/layouts/Highlight.tsx | 6 +- website2/src/components/layouts/Navbar.tsx | 5 +- .../src/components/layouts/NewsLetter.tsx | 5 +- .../components/layouts/NotificationBanner.tsx | 57 +-- website2/src/components/ui/Pagination.tsx | 61 ++- website2/src/configs/mainConfigs.ts | 13 + website2/src/views/about/AboutPage.tsx | 417 +++++++++--------- website2/src/views/careers/CareerPage.tsx | 215 ++++----- website2/src/views/careers/DetailsPage.tsx | 7 +- .../src/views/cleanairforum/FeaturedEvent.tsx | 9 +- .../views/cleanairforum/PaginatedSection.tsx | 3 +- .../views/cleanairforum/RegisterBanner.tsx | 3 +- .../src/views/cleanairforum/TabNavigation.tsx | 20 +- .../cleanairforum/resources/ResourcePage.tsx | 1 + .../src/views/events/EventCardsSection.tsx | 5 +- website2/src/views/events/EventPage.tsx | 21 +- website2/src/views/events/SingleEvent.tsx | 341 +++++++------- .../views/home/AnalyticsContentSection.tsx | 5 +- .../src/views/home/AppDownloadSection.tsx | 6 +- website2/src/views/home/FeaturedCarousel.tsx | 5 +- website2/src/views/home/HomePlayerSection.tsx | 5 +- website2/src/views/home/HomeStatsSection.tsx | 3 +- website2/src/views/legal/AirQoDataPage.tsx | 2 +- website2/src/views/legal/PP_Page.tsx | 2 +- website2/src/views/legal/PRP_Page.tsx | 2 +- website2/src/views/legal/TOSPage.tsx | 2 +- website2/src/views/legal/Tabsection.tsx | 46 +- website2/src/views/press/PressPage.tsx | 10 +- .../src/views/publications/ResourcePage.tsx | 6 +- .../solutions/AfricanCities/AfricanCities.tsx | 3 +- .../AfricanCities/AfricanCityPage.tsx | 5 +- .../solutions/communities/CommunityPage.tsx | 11 +- website2/tsconfig.json | 3 +- 38 files changed, 738 insertions(+), 592 deletions(-) create mode 100644 website2/src/configs/mainConfigs.ts diff --git a/website2/src/app/legal/layout.tsx b/website2/src/app/legal/layout.tsx index d712dc909d..09908370d0 100644 --- a/website2/src/app/legal/layout.tsx +++ b/website2/src/app/legal/layout.tsx @@ -2,6 +2,7 @@ import { Metadata } from 'next'; import React from 'react'; import MainLayout from '@/components/layouts/MainLayout'; +import mainConfig from '@/configs/mainConfigs'; import TabSection from '@/views/legal/Tabsection'; export const metadata: Metadata = { @@ -51,7 +52,9 @@ const LegalPageLayout: React.FC = ({ children }) => { return ( -
    {children}
    +
    + {children} +
    ); }; diff --git a/website2/src/components/dialogs/EngagementDialog.tsx b/website2/src/components/dialogs/EngagementDialog.tsx index 1aab405e97..0ad66e1528 100644 --- a/website2/src/components/dialogs/EngagementDialog.tsx +++ b/website2/src/components/dialogs/EngagementDialog.tsx @@ -5,6 +5,7 @@ import React, { useState } from 'react'; import { FiArrowLeft } from 'react-icons/fi'; import { Dialog, DialogContent } from '@/components/ui/dialog'; +import mainConfig from '@/configs/mainConfigs'; import { useDispatch, useSelector } from '@/hooks'; import { postContactUs } from '@/services/externalService'; import { closeModal } from '@/store/slices/modalSlice'; @@ -349,7 +350,9 @@ const EngagementDialog = () => { return ( - +
    {/* Left Side - Breadcrumb and Text with Animation */} { const router = useRouter(); const dispatch = useDispatch(); return ( -
    +
    {/* Card 1 */} { diff --git a/website2/src/components/layouts/ActionButtons2.tsx b/website2/src/components/layouts/ActionButtons2.tsx index 67883d54eb..ae6f58f9ec 100644 --- a/website2/src/components/layouts/ActionButtons2.tsx +++ b/website2/src/components/layouts/ActionButtons2.tsx @@ -1,11 +1,15 @@ 'use client'; import React from 'react'; +import mainConfig from '@/configs/mainConfigs'; + import { CustomButton } from '../ui'; const ActionButtons2 = () => { return ( -
    +
    {/* Card 1 */} { diff --git a/website2/src/components/layouts/Footer.tsx b/website2/src/components/layouts/Footer.tsx index 1bff44d4c6..24c76d3f8e 100644 --- a/website2/src/components/layouts/Footer.tsx +++ b/website2/src/components/layouts/Footer.tsx @@ -8,6 +8,8 @@ import { FaYoutube, } from 'react-icons/fa'; +import mainConfig from '@/configs/mainConfigs'; + import CountrySelectorDialog from '../sections/footer/CountrySelectorDialog'; import MonitorDisplay from '../sections/footer/MonitorDisplay'; import ScrollToTopButton from './ScrollToTopButton'; @@ -17,7 +19,7 @@ const Footer = () => { return (