From 22b50881d0c63ae6a2103d6e52b89f4aa5db6946 Mon Sep 17 00:00:00 2001 From: Patricia Anugrah Setiani Date: Mon, 10 May 2021 22:18:05 +0700 Subject: [PATCH 01/45] [RED] add additional tests in profile_test --- test/profile_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/profile_test.dart b/test/profile_test.dart index e1f6240..e7ecf76 100644 --- a/test/profile_test.dart +++ b/test/profile_test.dart @@ -400,5 +400,19 @@ void main() { await tester.pumpAndSettle(); expect(find.text('*Wajib diisi'), findsNWidgets(3)); }); + + testWidgets('Change Profile Picture Title -- Positive', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + expect(find.text('Ubah Foto Profil'), findsOneWidget); + }); + + testWidgets('Change Profile Picture Title -- Negative', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + expect(find.text('Edit PFP'), findsNothing); + }); }); } -- GitLab From 4b5c2bc973e62a40c371232d1ee412e427a82558 Mon Sep 17 00:00:00 2001 From: Patricia Anugrah Setiani Date: Tue, 11 May 2021 06:03:23 +0700 Subject: [PATCH 02/45] [GREEN] add upload profile in edit profile page --- lib/page/profile/edit_profile.dart | 93 ++++++++++++++++++++++++------ test/profile_test.dart | 6 +- 2 files changed, 78 insertions(+), 21 deletions(-) diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index af2aabb..8259eb9 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:bisaGo/bloc/new_user_bloc.dart'; import 'package:bisaGo/component/bisago_appbar.dart'; @@ -15,6 +16,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; +import 'package:image_picker/image_picker.dart'; class EditProfile extends StatefulWidget { final UserModel user; @@ -28,6 +30,25 @@ class _EditProfileState extends State { final GlobalKey _formKey = GlobalKey(); UserModel user; NewUserBloc bloc; + final picker = ImagePicker(); + File _image; + + Future _getImage() async { + final image = await picker.getImage(source: ImageSource.gallery); + return File(image.path); + } + + Future _getCameraImage() async { + final image = + await picker.getImage(source: ImageSource.camera, imageQuality: 50); + return File(image.path); + } + + Future _clearImage() async { + setState(() { + _image = null; + }); + } _EditProfileState(this.user); @@ -88,30 +109,37 @@ class _EditProfileState extends State { child: ListBody( children: [ CircleAvatar( - key: Key('Avatar ${name.split(' ')[0]}'), + //key: Key('Avatar ${name.split(' ')[0]}'), radius: 50, backgroundColor: white, - child: Text( - user.name.substring(0, 1), - style: const TextStyle( - fontSize: 45, - fontWeight: FontWeight.w900, - color: darkGreen, - fontFamily: 'Comfortaa', + child: ClipOval( + child: (_image != null) + ? Image.file(_image) + : Text(user.name.substring(0, 1), + style: const TextStyle( + fontSize: 45, + fontWeight: FontWeight.w900, + color: darkGreen, + fontFamily: 'Comfortaa', + ) + ), ), - ), ), Padding( - padding: const EdgeInsets.only(top: doubleSpace), - child: Text( - 'Halo, ${name.split(' ')[0]}!', - style: const TextStyle( - fontSize: 26, - fontWeight: FontWeight.w900, - color: Colors.black, - fontFamily: 'Comfortaa', + padding: const EdgeInsets.symmetric(vertical: doubleSpace), + child: InkWell( + child: Text( + 'Ubah Foto Profil', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w900, + color: Colors.blue, + fontFamily: 'Comfortaa', + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, + onTap: () { _showPhotoSelectionDialog(); + }, ), ), CustomTextField( @@ -320,4 +348,33 @@ class _EditProfileState extends State { Navigator.pop(context); Navigator.of(context).push(route); } + + Future _showPhotoSelectionDialog() async { + await showDialog( + context: context, + builder: (_) => SimpleDialog( + title: Text('Pilih Foto'), + children: [ + SimpleDialogOption( + child: Text('Dari Gallery'), + onPressed: () async { + final imageSelected = await _getImage(); + setState((){ + _image = imageSelected; + }); + }, + ), + SimpleDialogOption( + child: Text('Dari Kamera'), + onPressed: () async { + final imageSelected = await _getCameraImage(); + setState((){ + _image = imageSelected; + }); + }, + ), + ], + ), + ); + } } diff --git a/test/profile_test.dart b/test/profile_test.dart index e7ecf76..bd66992 100644 --- a/test/profile_test.dart +++ b/test/profile_test.dart @@ -222,14 +222,14 @@ void main() { testWidgets('New Edit Profile Page Widget Test -- Positive', (WidgetTester tester) async { - final userAvatarKey = Key('Avatar test'); + //final userAvatarKey = Key('Avatar test'); final name = 'test'; - + await tester.pumpWidget( MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); await tester.pumpAndSettle(); expect(find.byType(EditProfile), findsOneWidget); - expect(find.byKey(userAvatarKey), findsOneWidget); + //expect(find.byKey(userAvatarKey), findsOneWidget); expect(find.text(name), findsOneWidget); }); -- GitLab From d460a2ff9962758fde686bb776ca7cc51e1d003d Mon Sep 17 00:00:00 2001 From: Patricia Anugrah Setiani Date: Tue, 11 May 2021 06:12:45 +0700 Subject: [PATCH 03/45] [CHORES] remove unused elements in edit profile page --- lib/page/profile/edit_profile.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index 8259eb9..669fbbb 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -44,11 +44,11 @@ class _EditProfileState extends State { return File(image.path); } - Future _clearImage() async { - setState(() { - _image = null; - }); - } + // Future _clearImage() async { + // setState(() { + // _image = null; + // }); + // } _EditProfileState(this.user); @@ -67,7 +67,7 @@ class _EditProfileState extends State { @override Widget build(BuildContext context) { - final name = user.name; + // final name = user.name; return Scaffold( appBar: PreferredSize( child: BisaGoAppBar( @@ -109,7 +109,7 @@ class _EditProfileState extends State { child: ListBody( children: [ CircleAvatar( - //key: Key('Avatar ${name.split(' ')[0]}'), + // key: Key('Avatar ${name.split(' ')[0]}'), radius: 50, backgroundColor: white, child: ClipOval( -- GitLab From 3263cc5ca6f614dfde88c867a29f4f2e2d495ef4 Mon Sep 17 00:00:00 2001 From: Patricia Anugrah Setiani Date: Sun, 16 May 2021 07:29:09 +0700 Subject: [PATCH 04/45] [REFACTOR] fix simple dialog and circle avatar image box size --- lib/page/profile/edit_profile.dart | 33 ++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index 669fbbb..fe22ef3 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -33,7 +33,7 @@ class _EditProfileState extends State { final picker = ImagePicker(); File _image; - Future _getImage() async { + Future _getGalleryImage() async { final image = await picker.getImage(source: ImageSource.gallery); return File(image.path); } @@ -44,11 +44,11 @@ class _EditProfileState extends State { return File(image.path); } - // Future _clearImage() async { - // setState(() { - // _image = null; - // }); - // } + Future _clearImage() async { + setState(() { + _image = null; + }); + } _EditProfileState(this.user); @@ -114,7 +114,11 @@ class _EditProfileState extends State { backgroundColor: white, child: ClipOval( child: (_image != null) - ? Image.file(_image) + ? SizedBox( + width: 100, + height: 100, + child: Image.file(_image, fit: BoxFit.cover), + ) : Text(user.name.substring(0, 1), style: const TextStyle( fontSize: 45, @@ -358,10 +362,11 @@ class _EditProfileState extends State { SimpleDialogOption( child: Text('Dari Gallery'), onPressed: () async { - final imageSelected = await _getImage(); + final imageSelected = await _getGalleryImage(); setState((){ _image = imageSelected; }); + Navigator.pop(context); }, ), SimpleDialogOption( @@ -371,6 +376,18 @@ class _EditProfileState extends State { setState((){ _image = imageSelected; }); + Navigator.pop(context); + }, + ), + SimpleDialogOption( + child: Text('Hapus Foto', + style: TextStyle( + color: Colors.red[800], + ), + ), + onPressed: () async { + Navigator.pop(context); + _clearImage(); }, ), ], -- GitLab From 1600f0f592752049811bae59ffdf6073b1178483 Mon Sep 17 00:00:00 2001 From: Patricia Anugrah Setiani Date: Sun, 16 May 2021 07:38:25 +0700 Subject: [PATCH 05/45] [CHORES] add await --- lib/page/profile/edit_profile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index fe22ef3..f649ad9 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -387,7 +387,7 @@ class _EditProfileState extends State { ), onPressed: () async { Navigator.pop(context); - _clearImage(); + await _clearImage(); }, ), ], -- GitLab From 528e06c91effdc1e5f99d76e2a8587918b4bc563 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sat, 22 May 2021 09:28:52 +0700 Subject: [PATCH 06/45] [RED] Implement test for fcm repository --- .../cloud_messaging_repository.dart | 10 +++++++ test/cloud_messaging_test.dart | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 lib/repository/cloud_messaging_repository.dart create mode 100644 test/cloud_messaging_test.dart diff --git a/lib/repository/cloud_messaging_repository.dart b/lib/repository/cloud_messaging_repository.dart new file mode 100644 index 0000000..1e8d160 --- /dev/null +++ b/lib/repository/cloud_messaging_repository.dart @@ -0,0 +1,10 @@ +abstract class BaseCloudMessagingRepository { + Future sendFCMToken(String fcmToken, String token); +} + +class CloudMessagingRepository implements BaseCloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return false; + } +} diff --git a/test/cloud_messaging_test.dart b/test/cloud_messaging_test.dart new file mode 100644 index 0000000..0e99bce --- /dev/null +++ b/test/cloud_messaging_test.dart @@ -0,0 +1,26 @@ +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/mockito.dart'; + +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + +void main() { + setUpAll(() { + final _getIt = GetIt.instance; + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); + }); + testWidgets('Generate fcm token', (WidgetTester tester) async { + final generatedUrl = + await MockCloudMessagingRepository().sendFCMToken('fcmToken', 'token'); + + expect(generatedUrl, true); + }); +} -- GitLab From 982883b5c224439a2f2fe1f4ceeb6e2c10f67944 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sat, 22 May 2021 10:18:20 +0700 Subject: [PATCH 07/45] [GREEN] Implement fcm getToken() --- android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 4 ++ lib/get_it.dart | 9 ++- lib/main_dev.dart | 11 ++++ lib/page/dashboard/dashboard.dart | 59 ++++++++++++++----- .../cloud_messaging_repository.dart | 16 ++++- pubspec.yaml | 1 + 7 files changed, 86 insertions(+), 18 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 949d735..13a3598 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -83,7 +83,9 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - implementation 'com.google.firebase:firebase-analytics:17.2.2' + implementation 'com.google.firebase:firebase-analytics:' + implementation 'com.google.firebase:firebase-messaging:' + implementation 'com.google.firebase:firebase-bom:27.0.0' } apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7e0e18f..9520df8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -33,6 +33,10 @@ + + + + diff --git a/lib/get_it.dart b/lib/get_it.dart index aec9e26..0aa444d 100644 --- a/lib/get_it.dart +++ b/lib/get_it.dart @@ -1,3 +1,4 @@ +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/kegiatan_repository.dart'; import 'package:bisaGo/repository/kegiatan_terdekat_repository.dart'; import 'package:bisaGo/repository/komentar_posting_kegiatan_repository.dart'; @@ -26,8 +27,8 @@ class AppGetIt { () => KomentarPostingRepository()); _getIt.registerLazySingleton( () => KomentarPostingKegiatanRepository()); - _getIt.registerLazySingleton( - () => LokasiRepository()); + _getIt + .registerLazySingleton(() => LokasiRepository()); _getIt.registerLazySingleton( () => LayananRepository()); _getIt.registerLazySingleton( @@ -36,5 +37,9 @@ class AppGetIt { () => KegiatanTerdekatRepository()); _getIt.registerLazySingleton( () => DynamicLinksServiceRepository()); + _getIt.registerLazySingleton( + () => DynamicLinksServiceRepository()); + _getIt.registerLazySingleton( + () => CloudMessagingRepository()); } } diff --git a/lib/main_dev.dart b/lib/main_dev.dart index 564a455..aa484a7 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -1,3 +1,5 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:bisaGo/app.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -7,11 +9,20 @@ import 'flavor/flavor.dart'; import 'globalnetwork.dart'; import 'package:bisaGo/get_it.dart'; +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // If you're going to use other Firebase services in the background, such as Firestore, + // make sure you call `initializeApp` before using other Firebase services. + await Firebase.initializeApp(); + + print('Handling a background message: ${message.messageId}'); +} + Future main() async { AppGetIt().initialize(); await DotEnv().load('.env'); getDioInstance('build'); await initializeDateFormatting('id_ID', null); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); Intl.defaultLocale = 'id_ID'; dio.options.receiveTimeout = 15000; ApiFlavor.flavor = BuildFlavor.development.toString(); diff --git a/lib/page/dashboard/dashboard.dart b/lib/page/dashboard/dashboard.dart index 8e70ad4..5d527da 100644 --- a/lib/page/dashboard/dashboard.dart +++ b/lib/page/dashboard/dashboard.dart @@ -11,7 +11,9 @@ import 'package:bisaGo/page/filter_fasilitas/postingan/detail_post.dart'; import 'package:bisaGo/repository/komentar_repository.dart'; import 'package:bisaGo/utils/custom_dashboard_location_button.dart'; import 'package:bisaGo/utils/location_turn_on_dialog.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:geolocator/geolocator.dart'; @@ -38,6 +40,10 @@ class DashboardState extends State { LokasiResponseBloc bloc = LokasiResponseBloc(); KegiatanTerdekatBloc blocKegiatanTerdekat = KegiatanTerdekatBloc(); + FirebaseMessaging _firebaseMessaging; + + bool _initialized = false; + @override void initState() { super.initState(); @@ -45,6 +51,31 @@ class DashboardState extends State { geolocator = Geolocator()..forceAndroidLocationManager; initDynamicLinks(); setInitialLocation(); + + if (!_initialized) { + _initialized = true; + _setupFirebase(); + } + } + + void _setupFirebase() async { + await Firebase.initializeApp(); + _firebaseMessaging = FirebaseMessaging.instance; + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + print('Got a message whilst in the foreground!'); + print('Message data: ${message.data}'); + + if (message.notification != null) { + print('Message also contained a notification: ${message.notification}'); + } + }); + _requestFCMToken(); + } + + void _requestFCMToken() async { + final token = await _firebaseMessaging.getToken(); + print(token); } void _navigateToPencarianPage(BuildContext context) { @@ -368,20 +399,20 @@ class DashboardState extends State { ..name = namaLokasi; final fasilitasRoute = MaterialPageRoute( builder: (BuildContext context) => DetailPostKegiatanPage( - lokasi: lokasi, - kegiatan: KegiatanModel( - id: kegiatan.id, - placeId: lokasi.placeId, - creator: kegiatan.creator, - namaKegiatan: kegiatan.namaKegiatan, - penyelenggara: kegiatan.penyelenggara, - narahubung: kegiatan.narahubung, - deskripsi: kegiatan.deskripsi, - timeStart: kegiatan.timeStart, - timeEnd: kegiatan.timeEnd, - image: kegiatan.image, - ), - )); + lokasi: lokasi, + kegiatan: KegiatanModel( + id: kegiatan.id, + placeId: lokasi.placeId, + creator: kegiatan.creator, + namaKegiatan: kegiatan.namaKegiatan, + penyelenggara: kegiatan.penyelenggara, + narahubung: kegiatan.narahubung, + deskripsi: kegiatan.deskripsi, + timeStart: kegiatan.timeStart, + timeEnd: kegiatan.timeEnd, + image: kegiatan.image, + ), + )); await Navigator.of(context).push(fasilitasRoute); } diff --git a/lib/repository/cloud_messaging_repository.dart b/lib/repository/cloud_messaging_repository.dart index 1e8d160..e45573b 100644 --- a/lib/repository/cloud_messaging_repository.dart +++ b/lib/repository/cloud_messaging_repository.dart @@ -1,3 +1,8 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:bisaGo/flavor/flavor.dart'; + abstract class BaseCloudMessagingRepository { Future sendFCMToken(String fcmToken, String token); } @@ -5,6 +10,15 @@ abstract class BaseCloudMessagingRepository { class CloudMessagingRepository implements BaseCloudMessagingRepository { @override Future sendFCMToken(String fcmToken, String token) async { - return false; + try { + await http.post( + '${ApiFlavor.getBaseUrl()}/', + headers: {'Authorization': token, 'content-type': 'application/json'}, + body: json.encode({'token': fcmToken}), + ); + return true; + } catch (_) { + return false; + } } } diff --git a/pubspec.yaml b/pubspec.yaml index 59f1b2b..ab35153 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: firebase_core: ^0.7.0 firebase_core_platform_interface: ^3.0.1 firebase_dynamic_links: ^0.7.0+1 + firebase_messaging: ^8.0.0-dev.15 carousel_slider: ^3.0.0 dev_dependencies: -- GitLab From e7f00baf3e91a4084c632f8a56e6b46355b74a13 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sat, 22 May 2021 10:52:39 +0700 Subject: [PATCH 08/45] [GREEN] Implement fcm bloc --- lib/bloc/cloud_messaging_bloc.dart | 26 +++++++++++++++++++ .../cloud_messaging_repository.dart | 12 ++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 lib/bloc/cloud_messaging_bloc.dart diff --git a/lib/bloc/cloud_messaging_bloc.dart b/lib/bloc/cloud_messaging_bloc.dart new file mode 100644 index 0000000..b91d380 --- /dev/null +++ b/lib/bloc/cloud_messaging_bloc.dart @@ -0,0 +1,26 @@ +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; +import 'package:get_it/get_it.dart'; +import 'package:http/http.dart'; + +class CloudMessagingBloc { + CloudMessagingRepository _cloudMessagingRepository; + + CloudMessagingBloc() { + _cloudMessagingRepository = + GetIt.instance.get(); + } + + Future sendFCMToken( + String fcmToken, + String token, + ) async { + try { + return await _cloudMessagingRepository.sendFCMToken( + fcmToken, + token, + ); + } catch (e) { + return Response('Failed to add komentar', 400); + } + } +} diff --git a/lib/repository/cloud_messaging_repository.dart b/lib/repository/cloud_messaging_repository.dart index e45573b..49ea45c 100644 --- a/lib/repository/cloud_messaging_repository.dart +++ b/lib/repository/cloud_messaging_repository.dart @@ -12,9 +12,15 @@ class CloudMessagingRepository implements BaseCloudMessagingRepository { Future sendFCMToken(String fcmToken, String token) async { try { await http.post( - '${ApiFlavor.getBaseUrl()}/', - headers: {'Authorization': token, 'content-type': 'application/json'}, - body: json.encode({'token': fcmToken}), + '${ApiFlavor.getBaseUrl()}/notification/', + headers: { + 'Authorization': 'token $token', + 'content-type': 'application/json' + }, + body: json.encode({ + 'token': fcmToken, + 'type': 'android', + }), ); return true; } catch (_) { -- GitLab From 061effe878e2d175dae09f5c21aa1649ede8cba9 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sat, 22 May 2021 10:53:19 +0700 Subject: [PATCH 09/45] [CHORES] Delete duplicate line --- lib/get_it.dart | 2 -- lib/page/dashboard/dashboard.dart | 12 ++++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/get_it.dart b/lib/get_it.dart index 0aa444d..18eee1b 100644 --- a/lib/get_it.dart +++ b/lib/get_it.dart @@ -37,8 +37,6 @@ class AppGetIt { () => KegiatanTerdekatRepository()); _getIt.registerLazySingleton( () => DynamicLinksServiceRepository()); - _getIt.registerLazySingleton( - () => DynamicLinksServiceRepository()); _getIt.registerLazySingleton( () => CloudMessagingRepository()); } diff --git a/lib/page/dashboard/dashboard.dart b/lib/page/dashboard/dashboard.dart index 5d527da..4de3408 100644 --- a/lib/page/dashboard/dashboard.dart +++ b/lib/page/dashboard/dashboard.dart @@ -1,3 +1,4 @@ +import 'package:bisaGo/bloc/cloud_messaging_bloc.dart'; import 'package:bisaGo/bloc/lokasi_response_bloc.dart'; import 'package:bisaGo/model/komentar.dart'; import 'package:bisaGo/model/lokasi.dart'; @@ -23,6 +24,7 @@ import 'package:bisaGo/component/bisago_drawer.dart'; import 'package:bisaGo/config/styles.dart'; import 'package:bisaGo/page/pencarian/pencarian.dart'; import 'package:google_maps_webservice/places.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class Dashboard extends StatefulWidget { const Dashboard({Key key}) : super(key: key); @@ -39,6 +41,7 @@ class DashboardState extends State { LokasiResponseBloc bloc = LokasiResponseBloc(); KegiatanTerdekatBloc blocKegiatanTerdekat = KegiatanTerdekatBloc(); + CloudMessagingBloc cloudMessagingBloc = CloudMessagingBloc(); FirebaseMessaging _firebaseMessaging; @@ -74,8 +77,13 @@ class DashboardState extends State { } void _requestFCMToken() async { - final token = await _firebaseMessaging.getToken(); - print(token); + final fcmToken = await _firebaseMessaging.getToken(); + final sharedPreferences = await SharedPreferences.getInstance(); + final token = sharedPreferences.getString('token'); + + if (token != null) { + await cloudMessagingBloc.sendFCMToken(fcmToken, token); + } } void _navigateToPencarianPage(BuildContext context) { -- GitLab From 61e51b86b5981e96dbdc7226a2eaff3236197746 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sat, 22 May 2021 11:10:26 +0700 Subject: [PATCH 10/45] [CHORES] Add mock fcm repository to several files --- test/cloud_messaging_test.dart | 4 ++-- test/custom_kegiatan_terdekat_button_test.dart | 11 +++++++++++ test/mock_test.dart | 11 +++++++++++ test/widget_test.dart | 11 +++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/test/cloud_messaging_test.dart b/test/cloud_messaging_test.dart index 0e99bce..0f41541 100644 --- a/test/cloud_messaging_test.dart +++ b/test/cloud_messaging_test.dart @@ -18,9 +18,9 @@ void main() { () => MockCloudMessagingRepository()); }); testWidgets('Generate fcm token', (WidgetTester tester) async { - final generatedUrl = + final result = await MockCloudMessagingRepository().sendFCMToken('fcmToken', 'token'); - expect(generatedUrl, true); + expect(result, true); }); } diff --git a/test/custom_kegiatan_terdekat_button_test.dart b/test/custom_kegiatan_terdekat_button_test.dart index b59a76e..c8b5511 100644 --- a/test/custom_kegiatan_terdekat_button_test.dart +++ b/test/custom_kegiatan_terdekat_button_test.dart @@ -1,6 +1,7 @@ import 'package:bisaGo/model/kegiatan.dart'; import 'package:bisaGo/model/lokasi.dart'; import 'package:bisaGo/page/dashboard/dashboard.dart'; +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/kegiatan_terdekat_repository.dart'; import 'package:bisaGo/repository/lokasi_repository.dart'; import 'package:flutter/material.dart'; @@ -41,6 +42,14 @@ class MockLokasi extends Fake implements LokasiRepository { } } +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + void main() { // final mockLokasi = { // 'name': 'Margo City', @@ -68,6 +77,8 @@ void main() { _getIt.registerLazySingleton( () => MockKegiatanTerdekat()); _getIt.registerLazySingleton(() => MockLokasi()); + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); }); testWidgets('Detail Post Kegiatan Page - Positive Test', diff --git a/test/mock_test.dart b/test/mock_test.dart index 032df7c..32c7466 100644 --- a/test/mock_test.dart +++ b/test/mock_test.dart @@ -1,4 +1,5 @@ import 'package:bisaGo/model/kegiatan.dart'; +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/kegiatan_terdekat_repository.dart'; import 'package:bisaGo/repository/lokasi_repository.dart'; import 'package:flutter/material.dart'; @@ -45,6 +46,14 @@ class MockKegiatanTerdekatRepository extends Fake } } +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + void main() { group('Dashboard navigation tests', () { NavigatorObserver mockObserver; @@ -74,6 +83,8 @@ void main() { () => MockLokasiRepository()); _getIt.registerLazySingleton( () => MockKegiatanTerdekatRepository()); + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); }); Future _buildDashboardPage(WidgetTester tester) async { diff --git a/test/widget_test.dart b/test/widget_test.dart index bac647f..69c7052 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -2,6 +2,7 @@ import 'package:bisaGo/model/kegiatan.dart'; import 'package:bisaGo/model/komunitas.dart'; import 'package:bisaGo/model/lokasi.dart'; import 'package:bisaGo/model/sekolah.dart'; +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/kegiatan_terdekat_repository.dart'; import 'package:bisaGo/repository/komunitas_repository.dart'; import 'package:bisaGo/repository/lokasi_repository.dart'; @@ -88,6 +89,14 @@ class MockKegiatanTerdekatRepository extends Fake } } +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + void main() { setUpAll(() { final _getIt = GetIt.instance; @@ -99,6 +108,8 @@ void main() { () => MockLokasiRepository()); _getIt.registerLazySingleton( () => MockKegiatanTerdekatRepository()); + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); }); testWidgets('finds a text field in dashboard', (WidgetTester tester) async { final containerTextField = Key('Container Text Field'); -- GitLab From 81889682d70081632d52b63fc33a1f9345b4b31e Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sun, 23 May 2021 05:13:19 +0700 Subject: [PATCH 11/45] [GREEN] Implement receive notification logic --- lib/page/dashboard/dashboard.dart | 49 +++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/page/dashboard/dashboard.dart b/lib/page/dashboard/dashboard.dart index 4de3408..e1daa0f 100644 --- a/lib/page/dashboard/dashboard.dart +++ b/lib/page/dashboard/dashboard.dart @@ -12,6 +12,7 @@ import 'package:bisaGo/page/filter_fasilitas/postingan/detail_post.dart'; import 'package:bisaGo/repository/komentar_repository.dart'; import 'package:bisaGo/utils/custom_dashboard_location_button.dart'; import 'package:bisaGo/utils/location_turn_on_dialog.dart'; +import 'package:dropdown_banner/dropdown_banner.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; @@ -26,6 +27,8 @@ import 'package:bisaGo/page/pencarian/pencarian.dart'; import 'package:google_maps_webservice/places.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../filter_fasilitas/postingan/detail_post.dart'; + class Dashboard extends StatefulWidget { const Dashboard({Key key}) : super(key: key); @override @@ -47,6 +50,8 @@ class DashboardState extends State { bool _initialized = false; + DateTime _lastNotification; + @override void initState() { super.initState(); @@ -66,13 +71,48 @@ class DashboardState extends State { _firebaseMessaging = FirebaseMessaging.instance; FirebaseMessaging.onMessage.listen((RemoteMessage message) { - print('Got a message whilst in the foreground!'); - print('Message data: ${message.data}'); + final now = DateTime.now(); - if (message.notification != null) { - print('Message also contained a notification: ${message.notification}'); + if (_lastNotification != null && + _lastNotification.add(Duration(seconds: 10)).isAfter(now)) return; + + _lastNotification = now; + final data = message.data; + final String msg = data['message']; + DropdownBanner.showBanner( + text: msg, + color: Color(0xFF003566), + textStyle: TextStyle( + color: Colors.white, + height: 2.1, + ), + duration: Duration(seconds: 8), + tapCallback: () { + if (data['type'] == 'fasilitas') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailFasilitasPage(context, placeId, id); + } else if (data['type'] == 'kegiatan') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailKegiatanPage(context, placeId, id); + } + }); + }); + + FirebaseMessaging.onMessageOpenedApp.listen((message) { + final data = message.data; + if (data['type'] == 'fasilitas') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailFasilitasPage(context, placeId, id); + } else if (data['type'] == 'kegiatan') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailKegiatanPage(context, placeId, id); } }); + _requestFCMToken(); } @@ -80,7 +120,6 @@ class DashboardState extends State { final fcmToken = await _firebaseMessaging.getToken(); final sharedPreferences = await SharedPreferences.getInstance(); final token = sharedPreferences.getString('token'); - if (token != null) { await cloudMessagingBloc.sendFCMToken(fcmToken, token); } -- GitLab From ea73f9b5848e6a1e097394e55b550d15baa6d55a Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sun, 23 May 2021 05:15:54 +0700 Subject: [PATCH 12/45] [GREEN] Implement banner for foreground notification --- lib/app.dart | 8 +++++++- pubspec.yaml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/app.dart b/lib/app.dart index 11dd997..c572bb9 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,3 +1,4 @@ +import 'package:dropdown_banner/dropdown_banner.dart'; import 'package:flutter/material.dart'; import 'package:bisaGo/config/styles.dart'; import 'package:bisaGo/page/dashboard/dashboard.dart'; @@ -7,6 +8,8 @@ class BisaGo extends StatelessWidget { @override Widget build(BuildContext context) { + final navigatorKey = GlobalKey(); + return MaterialApp( title: 'bisaGo', theme: ThemeData( @@ -14,7 +17,10 @@ class BisaGo extends StatelessWidget { primaryColor: greenPrimary, backgroundColor: Colors.white, ), - home: Dashboard(), + home: DropdownBanner( + navigatorKey: navigatorKey, + child: Dashboard(), + ), ); } } diff --git a/pubspec.yaml b/pubspec.yaml index ab35153..c7a2c05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: firebase_dynamic_links: ^0.7.0+1 firebase_messaging: ^8.0.0-dev.15 carousel_slider: ^3.0.0 + dropdown_banner: ^1.4.0 dev_dependencies: flutter_test: -- GitLab From 562c36363bd4bee788a7cddcb3b75ab7485f2da8 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sun, 23 May 2021 05:16:25 +0700 Subject: [PATCH 13/45] [CHORES] Remove WillPopScope --- lib/page/filter_fasilitas/kegiatan.dart | 147 ++-- lib/page/filter_fasilitas/komentar.dart | 2 +- .../postingan/detail_post.dart | 637 +++++++------- .../postingan/detail_post_kegiatan.dart | 795 +++++++++--------- 4 files changed, 778 insertions(+), 803 deletions(-) diff --git a/lib/page/filter_fasilitas/kegiatan.dart b/lib/page/filter_fasilitas/kegiatan.dart index d668443..ef3c723 100644 --- a/lib/page/filter_fasilitas/kegiatan.dart +++ b/lib/page/filter_fasilitas/kegiatan.dart @@ -17,29 +17,27 @@ class Kegiatan extends StatefulWidget { } class _KegiatanState extends State { - @override Widget build(BuildContext context) { return InkWell( key: Key(widget.kegiatan.namaKegiatan), onTap: () { - Navigator.of(context).pushReplacement(MaterialPageRoute( + Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) => DetailPostKegiatanPage( - lokasi: widget.lokasi, - kegiatan: KegiatanModel( - id: widget.kegiatan.id, - placeId: widget.kegiatan.placeId, - creator: widget.kegiatan.creator, - namaKegiatan: widget.kegiatan.namaKegiatan, - penyelenggara: widget.kegiatan.penyelenggara, - narahubung: widget.kegiatan.narahubung, - deskripsi: widget.kegiatan.deskripsi, - timeStart: widget.kegiatan.timeStart, - timeEnd: widget.kegiatan.timeEnd, - image: widget.kegiatan.image, - ), - ) - )); + lokasi: widget.lokasi, + kegiatan: KegiatanModel( + id: widget.kegiatan.id, + placeId: widget.kegiatan.placeId, + creator: widget.kegiatan.creator, + namaKegiatan: widget.kegiatan.namaKegiatan, + penyelenggara: widget.kegiatan.penyelenggara, + narahubung: widget.kegiatan.narahubung, + deskripsi: widget.kegiatan.deskripsi, + timeStart: widget.kegiatan.timeStart, + timeEnd: widget.kegiatan.timeEnd, + image: widget.kegiatan.image, + ), + ))); }, child: Container( margin: const EdgeInsets.only(bottom: regularSpace), @@ -72,59 +70,68 @@ class _KegiatanState extends State { ), ), Container( - margin: const EdgeInsets.only(bottom: regularSpace), - child: SizedBox( - width: MediaQuery.of(context).size.width, - height: 160, - child: CarouselSlider( - options: CarouselOptions( - aspectRatio: 1.0, - enlargeCenterPage: true, - enableInfiniteScroll: false, - initialPage: 0, - autoPlay: true, - ), - items: widget.kegiatan.image.map((item) => Container( - child: Container( - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(20)), - child: Stack( - children: [ - Image.network(item, fit: BoxFit.cover, width: 1000.0), - Positioned( - bottom: 0.0, - left: 0.0, - right: 0.0, + margin: const EdgeInsets.only(bottom: regularSpace), + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: 160, + child: CarouselSlider( + options: CarouselOptions( + aspectRatio: 1.0, + enlargeCenterPage: true, + enableInfiniteScroll: false, + initialPage: 0, + autoPlay: true, + ), + items: widget.kegiatan.image + .map((item) => Container( child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromARGB(200, 0, 0, 0), - Color.fromARGB(0, 0, 0, 0) - ], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - child: Text( - '#${widget.kegiatan.image.indexOf(item)+1}', - style: TextStyle( - color: Colors.white, - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), - ), + child: ClipRRect( + borderRadius: BorderRadius.all( + Radius.circular(20)), + child: Stack( + children: [ + Image.network(item, + fit: BoxFit.cover, + width: 1000.0), + Positioned( + bottom: 0.0, + left: 0.0, + right: 0.0, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color.fromARGB( + 200, 0, 0, 0), + Color.fromARGB( + 0, 0, 0, 0) + ], + begin: Alignment + .bottomCenter, + end: Alignment.topCenter, + ), + ), + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 20.0), + child: Text( + '#${widget.kegiatan.image.indexOf(item) + 1}', + style: TextStyle( + color: Colors.white, + fontSize: 20.0, + fontWeight: + FontWeight.bold, + ), + ), + ), + ), + ], + )), ), - ), - ], - ) - ), - ), - )).toList(), - ), - ) - ), + )) + .toList(), + ), + )), Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, @@ -148,5 +155,5 @@ class _KegiatanState extends State { ], ), )); - } -} \ No newline at end of file + } +} diff --git a/lib/page/filter_fasilitas/komentar.dart b/lib/page/filter_fasilitas/komentar.dart index dda5b47..d7c53f2 100644 --- a/lib/page/filter_fasilitas/komentar.dart +++ b/lib/page/filter_fasilitas/komentar.dart @@ -37,7 +37,7 @@ class _KomentarState extends State { return InkWell( key: Key('Fasilitas ${fasilitas[widget.komentar.tag]}'), onTap: () { - Navigator.of(context).pushReplacement(MaterialPageRoute( + Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) => DetailPostPage( lokasi: widget.lokasi, komentar: KomentarModel( diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index af1d54d..39ccab1 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -54,357 +54,345 @@ class _DetailPostPageState extends State { @override Widget build(BuildContext context) { var namaLokasi = widget.komentar.namaLokasi ?? 'Invalid Lokasi Name'; - return WillPopScope( - onWillPop: () => Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (BuildContext context) => Fasilitas( - lokasi: widget.lokasi, - ))), - child: Scaffold( - appBar: BisaGoAppBar( - title: namaLokasi, - key: Key('appbar-text-$namaLokasi'), - actions: [ - InkWell( - onTap: () async { - final link = await DynamicLinksServiceRepository() - .createDynamicLinkForFasilitas( - widget.komentar.id, - widget.lokasi.placeId, - ); - await Share.share(ShareUtils.getFormattedMessageFasilitas( - widget.komentar, widget.lokasi, link)); - }, - child: const Padding( - padding: EdgeInsets.all(doubleSpace), - child: Icon(Icons.share), - ), + return Scaffold( + appBar: BisaGoAppBar( + title: namaLokasi, + key: Key('appbar-text-$namaLokasi'), + actions: [ + InkWell( + onTap: () async { + final link = await DynamicLinksServiceRepository() + .createDynamicLinkForFasilitas( + widget.komentar.id, + widget.lokasi.placeId, + ); + await Share.share(ShareUtils.getFormattedMessageFasilitas( + widget.komentar, widget.lokasi, link)); + }, + child: const Padding( + padding: EdgeInsets.all(doubleSpace), + child: Icon(Icons.share), ), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - Container( - key: const Key('Text Jenis Fasilitas'), - margin: const EdgeInsets.symmetric( - vertical: 10.0, horizontal: 15.0), - alignment: Alignment.centerLeft, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.6, - child: Text( - fasilitas[widget.komentar.tag], - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - letterSpacing: -0.3, - color: Colors.black, - fontFamily: 'Comfortaa', - ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + key: const Key('Text Jenis Fasilitas'), + margin: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), + alignment: Alignment.centerLeft, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.6, + child: Text( + fasilitas[widget.komentar.tag], + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.w800, + letterSpacing: -0.3, + color: Colors.black, + fontFamily: 'Comfortaa', ), ), - PopupMenuButton( - key: const Key('Button Ubah Informasi'), - elevation: 4.0, - offset: const Offset(0.0, 40.0), - itemBuilder: (BuildContext context) { - final choices = ['Ubah Informasi']; - return choices.map((String choice) { - return PopupMenuItem( - key: Key(choice), - child: ElevatedButton( - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.symmetric( - vertical: 0, horizontal: 0)), - backgroundColor: - MaterialStateProperty.all(Colors.white), - foregroundColor: - MaterialStateProperty.all(Colors.black), - elevation: MaterialStateProperty.all(0)), - onPressed: _updateInformasi, - child: SizedBox( - width: double.infinity, - child: Text(choice), - ), + ), + PopupMenuButton( + key: const Key('Button Ubah Informasi'), + elevation: 4.0, + offset: const Offset(0.0, 40.0), + itemBuilder: (BuildContext context) { + final choices = ['Ubah Informasi']; + return choices.map((String choice) { + return PopupMenuItem( + key: Key(choice), + child: ElevatedButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.symmetric( + vertical: 0, horizontal: 0)), + backgroundColor: + MaterialStateProperty.all(Colors.white), + foregroundColor: + MaterialStateProperty.all(Colors.black), + elevation: MaterialStateProperty.all(0)), + onPressed: _updateInformasi, + child: SizedBox( + width: double.infinity, + child: Text(choice), ), - ); - }).toList(); - }, - ), - ], - ), + ), + ); + }).toList(); + }, + ), + ], ), - Container( - key: const Key('Text Jumlah'), - width: MediaQuery.of(context).size.width, - color: red, - padding: const EdgeInsets.symmetric( - vertical: regularSpace, horizontal: doubleSpace), - child: Text( - 'Tersedia sebanyak ${widget.komentar.jumlah} ' - 'unit fasilitas.', - style: const TextStyle( - fontSize: 16, - color: Colors.white, - fontFamily: 'Comfortaa', - ), - )), - Container( - margin: const EdgeInsets.all(doubleSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 150, - child: ImageHolder( - url: widget.komentar.image, - fasilitas: widget.komentar.tag)), - const SizedBox( - height: 10, - ), - // Wrap( - // alignment: WrapAlignment.start, - // direction: Axis.horizontal, - // crossAxisAlignment: WrapCrossAlignment.start, - // children: widget.komentar.tag - // .map((tag) => - // _createTagContainer(getTag(tag))) - // .toList(), - // ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + Container( + key: const Key('Text Jumlah'), + width: MediaQuery.of(context).size.width, + color: red, + padding: const EdgeInsets.symmetric( + vertical: regularSpace, horizontal: doubleSpace), + child: Text( + 'Tersedia sebanyak ${widget.komentar.jumlah} ' + 'unit fasilitas.', + style: const TextStyle( + fontSize: 16, + color: Colors.white, + fontFamily: 'Comfortaa', + ), + )), + Container( + margin: const EdgeInsets.all(doubleSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 150, + child: ImageHolder( + url: widget.komentar.image, + fasilitas: widget.komentar.tag)), + const SizedBox( + height: 10, + ), + // Wrap( + // alignment: WrapAlignment.start, + // direction: Axis.horizontal, + // crossAxisAlignment: WrapCrossAlignment.start, + // children: widget.komentar.tag + // .map((tag) => + // _createTagContainer(getTag(tag))) + // .toList(), + // ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Dapat digunakan oleh', + style: TextStyle(fontSize: 16), + textAlign: TextAlign.left, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _showIfContains('DF'), + _showIfContains('DI'), + _showIfContains('DM'), + _showIfContains('DS'), + ], + ) + ], + ), + Container( + key: const Key('desc'), + decoration: BoxDecoration( + color: gray, + boxShadow: regularShadow, + borderRadius: regularBorderRadius), + padding: const EdgeInsets.all(doubleSpace), + margin: const EdgeInsets.symmetric(vertical: doubleSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Dapat digunakan oleh', - style: TextStyle(fontSize: 16), - textAlign: TextAlign.left, - ), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _showIfContains('DF'), - _showIfContains('DI'), - _showIfContains('DM'), - _showIfContains('DS'), - ], - ) - ], - ), - Container( - key: const Key('desc'), - decoration: BoxDecoration( - color: gray, - boxShadow: regularShadow, - borderRadius: regularBorderRadius), - padding: const EdgeInsets.all(doubleSpace), - margin: const EdgeInsets.symmetric(vertical: doubleSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Expanded( - child: Text( - 'Cara menggunakan', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w800), - ), + const Expanded( + child: Text( + 'Cara menggunakan', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w800), ), - ], - ), - Container( - margin: const EdgeInsets.symmetric( - vertical: regularSpace), - child: Text( - widget.komentar.deskripsi, - key: const Key('Text Cara Menggunakan'), - style: const TextStyle(fontSize: 16), ), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Text( - 'informasi ditambahkan oleh ', - style: TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - fontWeight: FontWeight.w200, - ), + ], ), Container( - padding: EdgeInsets.zero, - constraints: BoxConstraints( - maxWidth: - MediaQuery.of(context).size.width * 0.3), + margin: const EdgeInsets.symmetric( + vertical: regularSpace), child: Text( - '${widget.komentar.creator} ', - key: Key('creator-${widget.komentar.creator}'), - overflow: TextOverflow.fade, - softWrap: false, - style: const TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - ), + widget.komentar.deskripsi, + key: const Key('Text Cara Menggunakan'), + style: const TextStyle(fontSize: 16), ), ), - Text( - '(${DateFormat('dd MMM yyy').format(widget.komentar.dateTime)})', - key: const Key('timestamp'), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text( + 'informasi ditambahkan oleh ', + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w200, + ), + ), + Container( + padding: EdgeInsets.zero, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.3), + child: Text( + '${widget.komentar.creator} ', + key: Key('creator-${widget.komentar.creator}'), + overflow: TextOverflow.fade, + softWrap: false, style: const TextStyle( fontSize: 12, fontStyle: FontStyle.italic, ), ), - ], - ), - const SizedBox( - height: regularSpace, - ), - const Divider( - color: grayPrimary, - thickness: 1.0, - ), - Container( - key: const Key('Komentar'), - padding: - const EdgeInsets.symmetric(vertical: regularSpace), - child: const Text( - 'Komentar', - style: TextStyle( - fontSize: 20, fontWeight: FontWeight.w800), - )), - StreamBuilder( - stream: _bloc.komentarPostingListStream, - builder: (context, snapshot) { - if (snapshot.hasData) { - switch (snapshot.data.status) { - case Status.loading: + ), + Text( + '(${DateFormat('dd MMM yyy').format(widget.komentar.dateTime)})', + key: const Key('timestamp'), + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + const SizedBox( + height: regularSpace, + ), + const Divider( + color: grayPrimary, + thickness: 1.0, + ), + Container( + key: const Key('Komentar'), + padding: + const EdgeInsets.symmetric(vertical: regularSpace), + child: const Text( + 'Komentar', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w800), + )), + StreamBuilder( + stream: _bloc.komentarPostingListStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + switch (snapshot.data.status) { + case Status.loading: + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + greenPrimary), + ), + ); + break; + case Status.completed: + allKomentarPostingFromApi = + snapshot.data.data.allKomentar; + if (allKomentarPostingFromApi.isEmpty) { return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - greenPrimary), - ), - ); - break; - case Status.completed: - allKomentarPostingFromApi = - snapshot.data.data.allKomentar; - if (allKomentarPostingFromApi.isEmpty) { - return const Center( - child: Text('Tidak ada Komentar')); - } else { - return Column( - children: allKomentarPostingFromApi - .map((k) => - komentarPlaceHolder(k.creator, - k.dateTime, k.deskripsi)) - .toList()); - } - break; - case Status.error: - return Center( - child: Text(snapshot.data.data.toString()), - ); - break; - } + child: Text('Tidak ada Komentar')); + } else { + return Column( + children: allKomentarPostingFromApi + .map((k) => komentarPlaceHolder( + k.creator, k.dateTime, k.deskripsi)) + .toList()); + } + break; + case Status.error: + return Center( + child: Text(snapshot.data.data.toString()), + ); + break; } - return Container(); - }), - const SizedBox(height: regularSpace), - Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - key: const Key('Text Field Komentar'), - keyboardType: TextInputType.multiline, - maxLines: null, - minLines: 3, - validator: FieldValidator.validateInfo, - controller: komentarController, - style: const TextStyle( - fontSize: 18, - ), - decoration: InputDecoration( - hintStyle: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 15), - hintText: 'Tulis komentar...', - contentPadding: const EdgeInsets.all(8.0), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), + } + return Container(); + }), + const SizedBox(height: regularSpace), + Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + key: const Key('Text Field Komentar'), + keyboardType: TextInputType.multiline, + maxLines: null, + minLines: 3, + validator: FieldValidator.validateInfo, + controller: komentarController, + style: const TextStyle( + fontSize: 18, + ), + decoration: InputDecoration( + hintStyle: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 15), + hintText: 'Tulis komentar...', + contentPadding: const EdgeInsets.all(8.0), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, ), ), ), - Container( - key: const Key('tambah komentar'), - padding: - const EdgeInsets.only(top: doubleSpace), - alignment: Alignment.center, - child: ButtonTheme( - minWidth: double.infinity, - height: 40, - child: ElevatedButton( - key: const Key('Button Tambah Komentar'), - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(vertical: 13)), - elevation: MaterialStateProperty.all(0.0), - backgroundColor: - MaterialStateProperty.all( - greenPrimary), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(10)))), - ), - onPressed: () { - _checkLoginStatus(); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon( - Icons.add, - size: 30, - color: Colors.white, - ), - const SizedBox(width: 5.0), - const Text( - 'Tambah Komentar', - style: TextStyle( - fontSize: 20, - color: Colors.white, - fontWeight: FontWeight.bold), - ), - ], - ), + ), + Container( + key: const Key('tambah komentar'), + padding: const EdgeInsets.only(top: doubleSpace), + alignment: Alignment.center, + child: ButtonTheme( + minWidth: double.infinity, + height: 40, + child: ElevatedButton( + key: const Key('Button Tambah Komentar'), + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.symmetric(vertical: 13)), + elevation: MaterialStateProperty.all(0.0), + backgroundColor: + MaterialStateProperty.all(greenPrimary), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10)))), ), - )), - ], - )), - ], - ), + onPressed: () { + _checkLoginStatus(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.add, + size: 30, + color: Colors.white, + ), + const SizedBox(width: 5.0), + const Text( + 'Tambah Komentar', + style: TextStyle( + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold), + ), + ], + ), + ), + )), + ], + )), + ], ), - ], - ), + ), + ], ), ), ); @@ -440,9 +428,6 @@ class _DetailPostPageState extends State { newKomentarPostingData, _namaLokasi, widget.komentar.id); if (response['response'] == 'komentar added') { successDialog(context); - Timer(const Duration(seconds: 2), () { - Navigator.pop(context); - }); await _bloc.fetchKomentarPostingList(_namaLokasi, widget.komentar.id); komentarController.clear(); } else { diff --git a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart index fbc07c0..691db2d 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart @@ -48,436 +48,422 @@ class _DetailPostKegiatanPageState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () => Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (BuildContext context) => Fasilitas( - lokasi: widget.lokasi, - ))), - child: Scaffold( - appBar: BisaGoAppBar( - title: widget.lokasi.name, - key: Key('appbar-text-${widget.kegiatan.placeId}'), - actions: [ - InkWell( - onTap: () async { - final link = await DynamicLinksServiceRepository() - .createDynamicLinkForKegiatan( - widget.kegiatan.id, - widget.lokasi.placeId, - ); - await Share.share(ShareUtils.getFormattedMessageKegiatan( - widget.kegiatan, widget.lokasi, link)); - }, - child: const Padding( - padding: EdgeInsets.all(doubleSpace), - child: Icon(Icons.share), - ), + return Scaffold( + appBar: BisaGoAppBar( + title: widget.lokasi.name, + key: Key('appbar-text-${widget.kegiatan.placeId}'), + actions: [ + InkWell( + onTap: () async { + final link = await DynamicLinksServiceRepository() + .createDynamicLinkForKegiatan( + widget.kegiatan.id, + widget.lokasi.placeId, + ); + await Share.share(ShareUtils.getFormattedMessageKegiatan( + widget.kegiatan, widget.lokasi, link)); + }, + child: const Padding( + padding: EdgeInsets.all(doubleSpace), + child: Icon(Icons.share), ), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - Container( - key: const Key('Text Judul Kegiatan'), - margin: const EdgeInsets.symmetric( - vertical: 10.0, horizontal: 15.0), - alignment: Alignment.centerLeft, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.6, - // ganti alias tambahan - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: regularSpace), - Text( - widget.kegiatan - .namaKegiatan, // ganti nama kegiatan // sudah - style: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.w800, - letterSpacing: 0.3, - fontFamily: 'Muli', - // color: Colors.black, - // fontFamily: 'Comfortaa', - ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + key: const Key('Text Judul Kegiatan'), + margin: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), + alignment: Alignment.centerLeft, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.6, + // ganti alias tambahan + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: regularSpace), + Text( + widget.kegiatan + .namaKegiatan, // ganti nama kegiatan // sudah + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.w800, + letterSpacing: 0.3, + fontFamily: 'Muli', + // color: Colors.black, + // fontFamily: 'Comfortaa', ), - Text( - 'oleh ${widget.kegiatan.penyelenggara}', // ganti format ${nama penyelenggara} // sudah - style: TextStyle(fontSize: 16), - textAlign: TextAlign.left, - ) - ], - ), + ), + Text( + 'oleh ${widget.kegiatan.penyelenggara}', // ganti format ${nama penyelenggara} // sudah + style: TextStyle(fontSize: 16), + textAlign: TextAlign.left, + ) + ], ), - // PopupMenuButton( - // key: const Key('Button Ubah Informasi'), - // elevation: 4.0, - // offset: const Offset(0.0, 40.0), - // itemBuilder: (BuildContext context) { - // final choices = ['Ubah Informasi']; - // return choices.map((String choice) { - // return PopupMenuItem( - // key: Key(choice), - // child: ElevatedButton( - // style: ButtonStyle( - // padding: MaterialStateProperty.all( - // EdgeInsets.symmetric( - // vertical: 0, horizontal: 0)), - // backgroundColor: - // MaterialStateProperty.all(Colors.white), - // foregroundColor: - // MaterialStateProperty.all(Colors.black), - // elevation: MaterialStateProperty.all(0)), - // onPressed: _updateInformasi, - // child: SizedBox( - // width: double.infinity, - // child: Text(choice), - // ), - // ), - // ); - // }).toList(); - // }, - // ), - ], - ), + ), + // PopupMenuButton( + // key: const Key('Button Ubah Informasi'), + // elevation: 4.0, + // offset: const Offset(0.0, 40.0), + // itemBuilder: (BuildContext context) { + // final choices = ['Ubah Informasi']; + // return choices.map((String choice) { + // return PopupMenuItem( + // key: Key(choice), + // child: ElevatedButton( + // style: ButtonStyle( + // padding: MaterialStateProperty.all( + // EdgeInsets.symmetric( + // vertical: 0, horizontal: 0)), + // backgroundColor: + // MaterialStateProperty.all(Colors.white), + // foregroundColor: + // MaterialStateProperty.all(Colors.black), + // elevation: MaterialStateProperty.all(0)), + // onPressed: _updateInformasi, + // child: SizedBox( + // width: double.infinity, + // child: Text(choice), + // ), + // ), + // ); + // }).toList(); + // }, + // ), + ], ), - Container( - margin: const EdgeInsets.all(doubleSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width, - height: 160, - child: CarouselSlider( - options: CarouselOptions( - aspectRatio: 1.0, - enlargeCenterPage: true, - enableInfiniteScroll: false, - initialPage: 0, - autoPlay: true, - ), - items: widget.kegiatan.image - .map((item) => Container( - child: Container( - child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(20)), - child: Stack( - children: [ - Image.network(item, - fit: BoxFit.cover, - width: 1000.0), - Positioned( - bottom: 0.0, - left: 0.0, - right: 0.0, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromARGB( - 200, 0, 0, 0), - Color.fromARGB(0, 0, 0, 0) - ], - begin: - Alignment.bottomCenter, - end: Alignment.topCenter, - ), + ), + Container( + margin: const EdgeInsets.all(doubleSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + height: 160, + child: CarouselSlider( + options: CarouselOptions( + aspectRatio: 1.0, + enlargeCenterPage: true, + enableInfiniteScroll: false, + initialPage: 0, + autoPlay: true, + ), + items: widget.kegiatan.image + .map((item) => Container( + child: Container( + child: ClipRRect( + borderRadius: + BorderRadius.all(Radius.circular(20)), + child: Stack( + children: [ + Image.network(item, + fit: BoxFit.cover, width: 1000.0), + Positioned( + bottom: 0.0, + left: 0.0, + right: 0.0, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color.fromARGB( + 200, 0, 0, 0), + Color.fromARGB(0, 0, 0, 0) + ], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, ), - padding: EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 20.0), - child: Text( - '#${widget.kegiatan.image.indexOf(item) + 1}', - style: TextStyle( - color: Colors.white, - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), + ), + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 20.0), + child: Text( + '#${widget.kegiatan.image.indexOf(item) + 1}', + style: TextStyle( + color: Colors.white, + fontSize: 20.0, + fontWeight: FontWeight.bold, ), ), ), - ], - )), - ), - )) - .toList(), - ), - ), - const SizedBox( - height: 10, - ), - Container( - key: const Key('desc'), - // decoration: BoxDecoration( - // color: gray, - // boxShadow: regularShadow, - // borderRadius: regularBorderRadius), - padding: const EdgeInsets.all(doubleSpace), - margin: const EdgeInsets.symmetric(vertical: doubleSpace), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // children: [ - // const Expanded( - // child: Text( - // 'Cara menggunakan', - // style: TextStyle( - // fontSize: 20, - // fontWeight: FontWeight.w800), - // ), - // ), - // ], - // ), - // Container( - // margin: const EdgeInsets.symmetric( - // vertical: regularSpace - // ), - // ganti alias tambahan - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - // mainAxisAlignment: - // MainAxisAlignment.center, - children: [ - const Icon( - Icons.access_time, - size: 20, - color: Colors.green, - ), - const SizedBox(width: 7.0), - const Text( - 'Senin, 3 Mei 2021', // ganti format widget.x.tanggalpelaksanaan - style: TextStyle(fontSize: 16), - key: Key('Text Waktu Pelaksanaan'), - ), - ], - ), - SizedBox(height: 40), - Text( - widget.kegiatan - .deskripsi, // ganti format widget.x.deskripsi kegiatan // sudah - style: const TextStyle(fontSize: 16), - key: const Key('Text Deskripsi Kegiatan'), - ), - SizedBox(height: 40), - Row( - // mainAxisAlignment: - // MainAxisAlignment.center, - children: [ - const Icon( - Icons.call, - size: 20, - color: Colors.green, - ), - const SizedBox(width: 7.0), - Text( - widget.kegiatan - .narahubung, // ganti format narahubung // sudah - style: TextStyle(fontSize: 16), - key: Key('Text Narahubung'), - ), - ], - ), - ], - ), - // ), - // ], - // ), + ), + ], + )), + ), + )) + .toList(), ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Text( - 'informasi ditambahkan oleh ', - style: TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - fontWeight: FontWeight.w200, - ), - ), - Container( - padding: EdgeInsets.zero, - constraints: BoxConstraints( - maxWidth: - MediaQuery.of(context).size.width * 0.3), - child: Text( - widget.kegiatan - .creator, // ganti format ${widget.x.creator} // sudah - key: Key( - 'Creator info kegiatan'), // ganti format key creator-${widget.x.creator} - overflow: TextOverflow.fade, - softWrap: false, - style: const TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, + ), + const SizedBox( + height: 10, + ), + Container( + key: const Key('desc'), + // decoration: BoxDecoration( + // color: gray, + // boxShadow: regularShadow, + // borderRadius: regularBorderRadius), + padding: const EdgeInsets.all(doubleSpace), + margin: const EdgeInsets.symmetric(vertical: doubleSpace), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // children: [ + // const Expanded( + // child: Text( + // 'Cara menggunakan', + // style: TextStyle( + // fontSize: 20, + // fontWeight: FontWeight.w800), + // ), + // ), + // ], + // ), + // Container( + // margin: const EdgeInsets.symmetric( + // vertical: regularSpace + // ), + // ganti alias tambahan + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + // mainAxisAlignment: + // MainAxisAlignment.center, + children: [ + const Icon( + Icons.access_time, + size: 20, + color: Colors.green, ), - ), + const SizedBox(width: 7.0), + const Text( + 'Senin, 3 Mei 2021', // ganti format widget.x.tanggalpelaksanaan + style: TextStyle(fontSize: 16), + key: Key('Text Waktu Pelaksanaan'), + ), + ], ), + SizedBox(height: 40), Text( - '22 April 2021', - // '(${DateFormat('dd MMM yyy').format(widget.kegiatan.dateTime)})', // ganti format (${DateFormat('dd MMM yyy').format(widget.x.dateTime)}) - key: const Key('timestamp'), + widget.kegiatan + .deskripsi, // ganti format widget.x.deskripsi kegiatan // sudah + style: const TextStyle(fontSize: 16), + key: const Key('Text Deskripsi Kegiatan'), + ), + SizedBox(height: 40), + Row( + // mainAxisAlignment: + // MainAxisAlignment.center, + children: [ + const Icon( + Icons.call, + size: 20, + color: Colors.green, + ), + const SizedBox(width: 7.0), + Text( + widget.kegiatan + .narahubung, // ganti format narahubung // sudah + style: TextStyle(fontSize: 16), + key: Key('Text Narahubung'), + ), + ], + ), + ], + ), + // ), + // ], + // ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text( + 'informasi ditambahkan oleh ', + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w200, + ), + ), + Container( + padding: EdgeInsets.zero, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.3), + child: Text( + widget.kegiatan + .creator, // ganti format ${widget.x.creator} // sudah + key: Key( + 'Creator info kegiatan'), // ganti format key creator-${widget.x.creator} + overflow: TextOverflow.fade, + softWrap: false, style: const TextStyle( fontSize: 12, fontStyle: FontStyle.italic, ), ), - ], - ), - const SizedBox( - height: regularSpace, - ), - const Divider( - color: grayPrimary, - thickness: 1.0, - ), - Container( - key: const Key('Komentar'), - padding: - const EdgeInsets.symmetric(vertical: regularSpace), - child: const Text( - 'Komentar', - style: TextStyle( - fontSize: 20, fontWeight: FontWeight.w800), ), + Text( + '22 April 2021', + // '(${DateFormat('dd MMM yyy').format(widget.kegiatan.dateTime)})', // ganti format (${DateFormat('dd MMM yyy').format(widget.x.dateTime)}) + key: const Key('timestamp'), + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + const SizedBox( + height: regularSpace, + ), + const Divider( + color: grayPrimary, + thickness: 1.0, + ), + Container( + key: const Key('Komentar'), + padding: const EdgeInsets.symmetric(vertical: regularSpace), + child: const Text( + 'Komentar', + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.w800), ), - StreamBuilder( - stream: _bloc.komentarPostingKegiatanListStream, - builder: (context, snapshot) { - if (snapshot.hasData) { - switch (snapshot.data.status) { - case Status.loading: + ), + StreamBuilder( + stream: _bloc.komentarPostingKegiatanListStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + switch (snapshot.data.status) { + case Status.loading: + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + greenPrimary), + ), + ); + break; + case Status.completed: + allKomentarPositngKegiatanFromApi = snapshot + .data + .data + .allKomentarKegiatan; // kalo error, brarti allKegiatan + if (allKomentarPositngKegiatanFromApi.isEmpty) { return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - greenPrimary), - ), - ); - break; - case Status.completed: - allKomentarPositngKegiatanFromApi = snapshot - .data - .data - .allKomentarKegiatan; // kalo error, brarti allKegiatan - if (allKomentarPositngKegiatanFromApi.isEmpty) { - return const Center( - child: Text('Tidak ada komentar')); - } else { - return Column( - children: - allKomentarPositngKegiatanFromApi - .map((k) => - komentarKegiatanPlaceHolder( - k.creator, - k.created, - k.deskripsi)) - .toList()); - } - break; - case Status.error: - return Center( - child: Text(snapshot.data.data.toString()), - ); - break; - } + child: Text('Tidak ada komentar')); + } else { + return Column( + children: allKomentarPositngKegiatanFromApi + .map((k) => + komentarKegiatanPlaceHolder( + k.creator, + k.created, + k.deskripsi)) + .toList()); + } + break; + case Status.error: + return Center( + child: Text(snapshot.data.data.toString()), + ); + break; } - return Container(); - }), - const SizedBox(height: regularSpace), - Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - key: const Key('Text Field Komentar'), - keyboardType: TextInputType.multiline, - maxLines: null, - minLines: 3, - validator: FieldValidator.validateInfo, - controller: komentarKegiatanController, - style: const TextStyle( - fontSize: 18, - ), - decoration: InputDecoration( - hintStyle: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 15), - hintText: 'Tulis komentar...', - contentPadding: const EdgeInsets.all(8.0), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), + } + return Container(); + }), + const SizedBox(height: regularSpace), + Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + key: const Key('Text Field Komentar'), + keyboardType: TextInputType.multiline, + maxLines: null, + minLines: 3, + validator: FieldValidator.validateInfo, + controller: komentarKegiatanController, + style: const TextStyle( + fontSize: 18, + ), + decoration: InputDecoration( + hintStyle: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 15), + hintText: 'Tulis komentar...', + contentPadding: const EdgeInsets.all(8.0), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, ), ), ), - Container( - key: const Key('Tambah Komentar'), - padding: - const EdgeInsets.only(top: doubleSpace), - alignment: Alignment.center, - child: ButtonTheme( - minWidth: double.infinity, - height: 40, - child: ElevatedButton( - key: const Key('Button Tambah Komentar'), - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(vertical: 13)), - elevation: MaterialStateProperty.all(0.0), - backgroundColor: - MaterialStateProperty.all( - greenPrimary), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(10)))), - ), - onPressed: () { - _checkLoginStatus(); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon( - Icons.add, - size: 30, - color: Colors.white, - ), - const SizedBox(width: 5.0), - const Text( - 'Tambah Komentar', - style: TextStyle( - fontSize: 20, - color: Colors.white, - fontWeight: FontWeight.bold), - ), - ], - ), + ), + Container( + key: const Key('Tambah Komentar'), + padding: const EdgeInsets.only(top: doubleSpace), + alignment: Alignment.center, + child: ButtonTheme( + minWidth: double.infinity, + height: 40, + child: ElevatedButton( + key: const Key('Button Tambah Komentar'), + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.symmetric(vertical: 13)), + elevation: MaterialStateProperty.all(0.0), + backgroundColor: + MaterialStateProperty.all(greenPrimary), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10)))), ), - )), - ], - )), - ], - ), + onPressed: () { + _checkLoginStatus(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.add, + size: 30, + color: Colors.white, + ), + const SizedBox(width: 5.0), + const Text( + 'Tambah Komentar', + style: TextStyle( + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold), + ), + ], + ), + ), + )), + ], + )), + ], ), - ], - ), + ), + ], ), ), ); @@ -513,9 +499,6 @@ class _DetailPostKegiatanPageState extends State { if (response['response'] == 'komentar kegiatan added') { successDialog(context); - Timer(const Duration(seconds: 2), () { - Navigator.pop(context); - }); await _bloc.fetchKomentarPostingKegiatanList( _placeId, widget.kegiatan.id); // ganti widget.x.id // sudah komentarKegiatanController.clear(); -- GitLab From f66e1d9ed1c6bac6fe4f0fc60e94cd7f7e520c83 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Sun, 23 May 2021 18:04:56 +0700 Subject: [PATCH 14/45] [CHORES] Remove unused import --- lib/page/filter_fasilitas/postingan/detail_post.dart | 1 - lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart | 2 -- 2 files changed, 3 deletions(-) diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index 39ccab1..e17b8e5 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bisaGo/config/strings.dart'; import 'package:bisaGo/model/lokasi.dart'; -import 'package:bisaGo/page/filter_fasilitas/fasilitas.dart'; import 'package:bisaGo/page/updateInformasi/update_informasi.dart'; import 'package:bisaGo/repository/dynamic_links_service_repository.dart'; import 'package:bisaGo/utils/share_utils.dart'; diff --git a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart index 691db2d..2054000 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart @@ -19,8 +19,6 @@ import 'package:bisaGo/page/login/login.dart'; import 'package:share/share.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../fasilitas.dart'; - class DetailPostKegiatanPage extends StatefulWidget { final Lokasi lokasi; final KegiatanModel kegiatan; // ganti model x // sudah -- GitLab From 7cfb743dfb4bee18afb5ba5476674a4e6bbb8f16 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Mon, 24 May 2021 20:50:03 +0700 Subject: [PATCH 15/45] [CHORE] fix can't show notification icon set default firebase messaging default notification icon --- android/app/src/main/AndroidManifest.xml | 8 ++++++++ .../main/res/mipmap-hdpi/transparent_icon.png | Bin 0 -> 1960 bytes android/app/src/main/res/values/colors.xml | 1 + 3 files changed, 9 insertions(+) create mode 100644 android/app/src/main/res/mipmap-hdpi/transparent_icon.png diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7e0e18f..4207a35 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -41,5 +41,13 @@ android:value="2" /> + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/transparent_icon.png b/android/app/src/main/res/mipmap-hdpi/transparent_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b68a7d08ba59bd73f753fe67c2cdebc9b4bb3689 GIT binary patch literal 1960 zcmV;Z2UqxsP)!+u3R_%QsGN=)I?9KY^vWrsWZQl+#>(O+`=4^ij#ix zs5;Rsu0DSG_!Y-RhbN{W=2we7huW^*c&7Ud?#i zjKZHD{ycBxV>uIdB*BsJ%>6YJZx8h2CBFUZr|<|=qFMmZI<)l%|9%jc{&5M{{nv5g z>IrBHH3tH9a;rRH_u1Vi-*&xyWP@YF_Xr_Go-cpR>b2f&A85z5!D}eKT#UHFxK9Cm zn-D@oqnMWDOhR>6HGD%poWFG*Ii4Kw5`X`hm*h;s%-js(ZSk8QXI_n6gObZ7=p5`s z`#?J!7aWK#hz2#%aPtG8{A|1MN-CWi6Dhy2yA;F$t2wb+T zv+Qp1)#96~0@d?!t308otq2_h9nh8OkajW+?vLH*9qk1*(K@+VzAU{W9aFPYINW*| zub+4w*|pi|8R>zi+XGJGKnNju4SDePcp;j_)xmR76I~Tq5E&^q%jJSi_$eWTXdT+I zvAbiayjBTanGWd{={WnzSqx4LLS3jvsx9^DCl-*)=K^QqT!au3bu{Wn)I=>6ofU9( zxsdEkUcwvszm#>9;e)FmOmT%=n3ylFZ@txu98V6Yk*22FQlF+0ZAd(t2wko2DZwOQ zcxo8gwb`I1+Cj*de@CTN3IC)Y+fQ!?FY#L)7aXYQtoTB-h;f0Llm$vYuQ9LN)NE=r zH=9rCEA$7bMAdbl*1>tn3C_sf5NyIyLI~ka{GiL{f~(5~&d6mfen9!+dH_f%PMHi| z*OqF3ka9ewVsd5@ssh#X)JSv7I?7OdxfoGWRD{B$2-R2VF*G@ZW0#I$XU$Hmwyy?n zpuYvBCYZ8&slw?=`-Mrq7y2BMTxUg92J(Z?z8>wG}m= zyLryn?r&d^o8|I5Gk2glqS>J>)qXd)XG(DjCTAu=jr8)O2PCt|*32!;L3>ndet7TY zqL+`&&d-7xX$)uNq(DG2lT0Aw%Qu_as>Z&WeW<)xiQE3$ur^rnqVdJFH_P8d?`ZG; z1VjiShZ+vS+v^3DsQ1-lueMFkO+#bTs2-RrU#1bvLi8@rt{t6&oqYSDt$nW6M+}aGKLD?-c$k21E2l1Bp2l{dnhvuGUczeCz zjeLz@65`L@Ja@t6bCq(1T-d|&89au)9RnTXyu>TJKkr6XWmd7=EMFE}lUtwL)aCDb zG6Ai!t@=gA>5MNXW+tGk(9KhcmU9w!YSV|Cnnv%ABE2%*M+hODi96{$??hfj9>O!j zv%|B(M@wo;ur_xsLi8cEA(Y$$pfcDTN|OffajKv{EC5 z)oaDf+zhNzB7!CCpK=2lA2c(^a|Tp))pLXpvV8aQ>OIAKU^r?(XlCfP(9F;yTP#~p-&_xOqZ`%r)o5&K zL|T3tBJ>ff2q7dqD;#@E_M)Y&1y%J`XuH~m+KyV7o6Kmu*$6=r3Ij7}iZqdRH|kvT z|DK=i8|&-u9qs*meqkOR{*IC4JmY?^0P`Kq5EY&iJ0iRlUSPxg=br~LOu?vB7v zW7y3b`I;vZP5?F17i9UeZOP8$+*e9o*(aEVHU0Jg^p? z75;vhJ`BGNIYx!GBNx37q ze=x%-!!x>a-O9x3#AgX1L|dx;eqay4w+JCb@6<0zElvF)HPRR2EOEbF>^vBTVHk#C u7=~dOhG7_nVHk#C7=~dOhG7`y8|*(S8W3s>-LtL$0000 #3A903A + #00FF00 -- GitLab From b9ccf68b7a22ee403059ea7f9ac8ab3482ee869c Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Mon, 24 May 2021 22:08:35 +0700 Subject: [PATCH 16/45] [CHORE] replaced default transparent icon with new icon --- .../main/res/mipmap-hdpi/transparent_icon.png | Bin 1960 -> 20706 bytes .../main/res/mipmap-mdpi/transparent_icon.png | Bin 0 -> 20706 bytes .../res/mipmap-xhdpi/transparent_icon.png | Bin 0 -> 20706 bytes .../res/mipmap-xxhdpi/transparent_icon.png | Bin 0 -> 20706 bytes .../res/mipmap-xxxhdpi/transparent_icon.png | Bin 0 -> 20706 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 android/app/src/main/res/mipmap-mdpi/transparent_icon.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/transparent_icon.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/transparent_icon.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/transparent_icon.png diff --git a/android/app/src/main/res/mipmap-hdpi/transparent_icon.png b/android/app/src/main/res/mipmap-hdpi/transparent_icon.png index b68a7d08ba59bd73f753fe67c2cdebc9b4bb3689..f8fddc349c3a2e79088ca668cc9455ba3f3ae59e 100644 GIT binary patch literal 20706 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 Date: Mon, 24 May 2021 22:19:23 +0700 Subject: [PATCH 17/45] [CHORE] change accent color same as icon color --- android/app/src/main/res/values/colors.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index a3ce57c..368aefd 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,4 +1,4 @@ #3A903A - #00FF00 + #3A903A -- GitLab From 1117de4f9175457a85042d5387af8e2daba00613 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Mon, 24 May 2021 23:39:13 +0700 Subject: [PATCH 18/45] [CHORE] changelogs.txt 3.4.0 - Notification when new komentar is posted --- android/fastlane/metadata/android/id/changelogs/changelogs.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/fastlane/metadata/android/id/changelogs/changelogs.txt b/android/fastlane/metadata/android/id/changelogs/changelogs.txt index 659bd95..6e0cd42 100644 --- a/android/fastlane/metadata/android/id/changelogs/changelogs.txt +++ b/android/fastlane/metadata/android/id/changelogs/changelogs.txt @@ -1,3 +1,6 @@ +3.4.0: +- Notification when new komentar is posted + 3.3.0: - New feature for add kegiatan - Fix bugs -- GitLab From 2fe5056ade71d74dcdb731cb855e20e22787dd81 Mon Sep 17 00:00:00 2001 From: Patricia Anugrah Setiani Date: Tue, 25 May 2021 08:30:11 +0700 Subject: [PATCH 19/45] check error pbi 10a --- lib/bloc/new_user_bloc.dart | 8 ++ lib/bloc/user_bloc.dart | 9 ++ lib/model/new_user.dart | 2 + lib/model/new_user.g.dart | 2 + lib/model/user.dart | 4 +- lib/model/user.g.dart | 2 + lib/page/profile/edit_profile.dart | 131 ++++++++++++++++++++++++++-- lib/page/profile/profile.dart | 26 ++++-- lib/repository/user_repository.dart | 39 +++++++++ 9 files changed, 205 insertions(+), 18 deletions(-) diff --git a/lib/bloc/new_user_bloc.dart b/lib/bloc/new_user_bloc.dart index 90b1b2d..8d62e78 100644 --- a/lib/bloc/new_user_bloc.dart +++ b/lib/bloc/new_user_bloc.dart @@ -31,6 +31,14 @@ class NewUserBloc { } } + Future updateUserPFP(Map newUserData) async { + try { + return await _userRepository.updateUserProfile(newUserData); + } catch (_) { + return Response('Failed to update user', 400); + } + } + void dispose() { _userController?.close(); } diff --git a/lib/bloc/user_bloc.dart b/lib/bloc/user_bloc.dart index b6b1554..8d4c38d 100644 --- a/lib/bloc/user_bloc.dart +++ b/lib/bloc/user_bloc.dart @@ -4,6 +4,7 @@ import 'package:bisaGo/network/data/network_model.dart'; import 'package:bisaGo/repository/user_repository.dart'; import 'package:bisaGo/model/user.dart'; import 'package:get_it/get_it.dart'; +import 'package:http/http.dart'; class UserBloc { UserRepository _userRepository; @@ -41,6 +42,14 @@ class UserBloc { } } + Future updateUserProfile(Map newUserData) async { + try { + return await _userRepository.updateUserProfile(newUserData); + } catch (e) { + return Response('Failed to update user', 400); + } + } + void dispose() { _userController?.close(); } diff --git a/lib/model/new_user.dart b/lib/model/new_user.dart index 34c1a52..c7173a0 100644 --- a/lib/model/new_user.dart +++ b/lib/model/new_user.dart @@ -15,6 +15,7 @@ class NewUser { String alamat; @JsonKey(name: 'phone_number') String phoneNumber; + String foto; NewUser({ this.name, @@ -26,6 +27,7 @@ class NewUser { this.pekerjaan, this.alamat, this.phoneNumber, + this.foto }); factory NewUser.fromJson(Map json) => diff --git a/lib/model/new_user.g.dart b/lib/model/new_user.g.dart index 28a0aa4..8628dbe 100644 --- a/lib/model/new_user.g.dart +++ b/lib/model/new_user.g.dart @@ -17,6 +17,7 @@ NewUser _$NewUserFromJson(Map json) { pekerjaan: json['pekerjaan'] as String, alamat: json['alamat'] as String, phoneNumber: json['phone_number'] as String, + foto: json['foto'] as String, ); } @@ -30,4 +31,5 @@ Map _$NewUserToJson(NewUser instance) => { 'pekerjaan': instance.pekerjaan, 'alamat': instance.alamat, 'phone_number': instance.phoneNumber, + 'foto': instance.foto, }; diff --git a/lib/model/user.dart b/lib/model/user.dart index 09a6a65..0afcec2 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -23,6 +23,7 @@ class UserModel { String pekerjaan; String alamat; String token; + String foto; UserModel( {this.is_login, @@ -35,7 +36,8 @@ class UserModel { this.disabilitas, this.pekerjaan, this.alamat, - this.token}); + this.token, + this.foto}); factory UserModel.fromJson(Map json) => _$UserModelFromJson(json); diff --git a/lib/model/user.g.dart b/lib/model/user.g.dart index c3527bc..90ad6ba 100644 --- a/lib/model/user.g.dart +++ b/lib/model/user.g.dart @@ -32,6 +32,7 @@ UserModel _$UserModelFromJson(Map json) { pekerjaan: json['pekerjaan'] as String, alamat: json['alamat'] as String, token: json['token'] as String, + foto: json['foto'] as String, ); } @@ -47,4 +48,5 @@ Map _$UserModelToJson(UserModel instance) => { 'pekerjaan': instance.pekerjaan, 'alamat': instance.alamat, 'token': instance.token, + 'foto': instance.foto, }; diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index f649ad9..7de85a3 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:io'; +import 'package:dio/dio.dart'; +//import 'package:bisaGo/bloc/user_bloc.dart'; import 'package:bisaGo/bloc/new_user_bloc.dart'; import 'package:bisaGo/component/bisago_appbar.dart'; import 'package:bisaGo/config/strings.dart'; @@ -30,6 +32,7 @@ class _EditProfileState extends State { final GlobalKey _formKey = GlobalKey(); UserModel user; NewUserBloc bloc; + //UserBloc bloc; final picker = ImagePicker(); File _image; @@ -63,11 +66,11 @@ class _EditProfileState extends State { tanggalLahir = user.tanggalLahir; jenisDisabilitasValue = user.disabilitas; pekerjaanValue = user.pekerjaan; + //fotoValue = user.foto; } @override Widget build(BuildContext context) { - // final name = user.name; return Scaffold( appBar: PreferredSize( child: BisaGoAppBar( @@ -109,7 +112,7 @@ class _EditProfileState extends State { child: ListBody( children: [ CircleAvatar( - // key: Key('Avatar ${name.split(' ')[0]}'), + key: Key('Avatar ${user.name.split(' ')[0]}'), radius: 50, backgroundColor: white, child: ClipOval( @@ -142,7 +145,8 @@ class _EditProfileState extends State { ), textAlign: TextAlign.center, ), - onTap: () { _showPhotoSelectionDialog(); + onTap: () { + _showPhotoSelectionDialog(); }, ), ), @@ -276,8 +280,32 @@ class _EditProfileState extends State { String tanggalLahir; String jenisDisabilitasValue; String pekerjaanValue; + Map newUserData = {}; + + dynamic fotoValidate() { + dynamic fotoVal = ''; + if (_image != null) { + final fileName = _image.path.split('/').last; + fotoVal = MultipartFile.fromFile( + _image.path, + filename: fileName, + ); + } + return fotoVal; + } Future _validateLoginInput() async { + dynamic fotoValidate = ''; + //fotoValue['foto'] = ''; + if (_image != null) { + //final fileName = _image.path.split('/').last; + //fotoValue['foto'] = await MultipartFile.fromFile( + //_image.path, + //filename: fileName, + //); + fotoValidate = _image.path; + } + final form = _formKey.currentState; if (_formKey.currentState.validate()) { form.save(); @@ -290,13 +318,77 @@ class _EditProfileState extends State { jenisKelamin: jenisKelaminValue ?? '-', disabilitas: jenisDisabilitasValue, pekerjaan: pekerjaanValue ?? '-', - alamat: alamatController.text.toString()); - await _updateUser(newUser); + alamat: alamatController.text.toString(), + foto: fotoValidate //_image.path //fotoValue['foto'] + ); + + newUserData['name'] = newUser.name; + newUserData['password'] = newUser.password; + newUserData['email'] = newUser.email; + newUserData['phoneNumber'] = newUser.phoneNumber; + newUserData['tanggalLahir'] = newUser.tanggalLahir; + newUserData['jenisKelamin'] = newUser.jenisKelamin; + newUserData['disabilitas'] = newUser.disabilitas; + newUserData['pekerjaan'] = newUser.pekerjaan; + newUserData['alamat'] = newUser.alamat; + newUserData['foto'] = ''; + if (_image != null) { + final fileName = _image.path.split('/').last; + newUserData['foto'] = await MultipartFile.fromFile( + newUser.foto, + filename: fileName, + ); + } + + //await updateUser(newUser); + await updatePPUser(); } else { failedDialog(context); } } +/* + Future _validateLoginInput() async { + final form = _formKey.currentState; + if (_formKey.currentState.validate()) { + form.save(); + newUserData['name'] = nameController.text.toString(); + newUserData['phoneNumber'] = phoneController.text.toString(); + newUserData['tanggalLahir'] = tanggalLahir; + newUserData['jenisKelamin'] = jenisKelaminValue; + newUserData['disabilitas'] = jenisDisabilitasValue; + newUserData['pekerjaan'] = pekerjaanValue; + newUserData['alamat'] = alamatController.text.toString(); + newUserData['foto'] = ''; + if (_image != null) { + final fileName = _image.path.split('/').last; + newUserData['foto'] = await MultipartFile.fromFile( + _image.path, + filename: fileName, + ); + } + await _updateUser(); + } else { + failedDialog(context); + } + }*/ + Future updatePPUser() async { + bloc = NewUserBloc(); + //bloc = UserBloc(user.email); + //final response = await bloc.updateUserProfile(newUserData); + final response = await bloc.updateUserPFP(newUserData); + if (response.statusCode == 200) { + successDialog(context); + Timer(const Duration(seconds: 2), () { + //_navigateToProfile(context, newUser); + _navigateToProfile(context); + }); + } else { + failedDialog(context); + } + bloc.dispose(); + } +/* Future _updateUser(NewUser newUser) async { bloc = NewUserBloc(); final response = await bloc.updateUser(newUser); @@ -310,7 +402,7 @@ class _EditProfileState extends State { } bloc.dispose(); } - +*/ void successDialog(BuildContext context) { const alertDialog = AlertDialog( title: Text('Update profile berhasil!'), @@ -335,6 +427,26 @@ class _EditProfileState extends State { }); } + void _navigateToProfile(BuildContext context) { + final userNew = UserModel( + username: newUserData['email'], + name: newUserData['name'], + email: newUserData['email'], + tanggalLahir: newUserData['tanggalLahir'], + phoneNumber: newUserData['phoneNumber'], + jenisKelamin: newUserData['jenisKelamin'], + disabilitas: newUserData['disabilitas'], + pekerjaan: newUserData['pekerjaan'], + alamat: newUserData['alamat'], + foto: newUserData['foto']); + final route = + MaterialPageRoute(builder: (_) => Profile(email: userNew.email)); + Navigator.of(context).pop(Navigator.pop(context)); + Navigator.pop(context); + Navigator.of(context).push(route); + } + + /* void _navigateToProfile(BuildContext context, NewUser newUser) { final userNew = UserModel( username: newUser.email, @@ -345,15 +457,16 @@ class _EditProfileState extends State { jenisKelamin: newUser.jenisKelamin, disabilitas: newUser.disabilitas, pekerjaan: newUser.pekerjaan, - alamat: newUser.alamat); + alamat: newUser.alamat, + foto: newUser.foto); final route = MaterialPageRoute(builder: (_) => Profile(email: userNew.email)); Navigator.of(context).pop(Navigator.pop(context)); Navigator.pop(context); Navigator.of(context).push(route); } - - Future _showPhotoSelectionDialog() async { + */ + Future _showPhotoSelectionDialog() async { await showDialog( context: context, builder: (_) => SimpleDialog( diff --git a/lib/page/profile/profile.dart b/lib/page/profile/profile.dart index 0903be1..98bbf2d 100644 --- a/lib/page/profile/profile.dart +++ b/lib/page/profile/profile.dart @@ -5,6 +5,7 @@ import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/profile/edit_profile.dart'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:bisaGo/component/image_holder.dart'; class Profile extends StatefulWidget { final String email; @@ -93,13 +94,22 @@ class _ProfileState extends State { key: Key('Avatar ${user.name.split(' ')[0]}'), radius: 50, backgroundColor: white, - child: Text( - user.name.substring(0, 1), - style: const TextStyle( - fontSize: 45, - fontWeight: FontWeight.w900, - color: darkGreen, - fontFamily: 'Comfortaa', + child: ClipOval( + child: /*(user.foto != '') + ? SizedBox( + width: 100, + height: 100, + child: ImageHolder( + url: user.foto, + ), + ) + :*/ Text(user.name.substring(0, 1), + style: const TextStyle( + fontSize: 45, + fontWeight: FontWeight.w900, + color: darkGreen, + fontFamily: 'Comfortaa', + ) ), ), ), @@ -120,7 +130,7 @@ class _ProfileState extends State { Text( user.email, style: const TextStyle( - fontSize: 20, + fontSize: 16, fontWeight: FontWeight.w500, color: Colors.white, fontFamily: 'Comfortaa', diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart index 4ced6b3..8a7ae33 100644 --- a/lib/repository/user_repository.dart +++ b/lib/repository/user_repository.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:bisaGo/flavor/flavor.dart'; import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/model/user.dart'; @@ -8,6 +9,7 @@ abstract class BaseUserRepository { Future fetchUserDetail(String email); Future createUser(NewUser newUser); Future updateUser(NewUser newUser); + Future updateUserProfile(Map newUserData); } class UserRepository implements BaseUserRepository { final NetworkInterface _network = NetworkInterface(); @@ -30,6 +32,16 @@ class UserRepository implements BaseUserRepository { return response; } + @override + Future updateUserProfile(Map newUserData) async { + final response = await post( + '${ApiFlavor.getBaseUrl()}/api/update-user/', + body: json.encode(newUserData) + ); + return response; + } + + @override Future updateUser(NewUser newUser) async { final response = await post( @@ -38,4 +50,31 @@ class UserRepository implements BaseUserRepository { ); return response; } + /* + @override + Future updateUser(NewUser newUser) async { + dynamic file; + Uri apiUrl = Uri.parse('${ApiFlavor.getBaseUrl()}/api/update-user/'); + final imageUploadRequest = MultipartRequest('POST', apiUrl); + if (newUser.foto != '') { + final fileName = newUser.foto.split('/').last; + file = await MultipartFile.fromPath( + newUser.foto, + fileName + ); + imageUploadRequest.files.add(file); + } + imageUploadRequest.fields['name'] = newUser.name; + imageUploadRequest.fields['password'] = newUser.password; + imageUploadRequest.fields['email'] = newUser.email; + imageUploadRequest.fields['tanggalLahir'] = newUser.tanggalLahir; + imageUploadRequest.fields['jenisKelamin'] = newUser.jenisKelamin; + imageUploadRequest.fields['disabilitas'] = newUser.disabilitas; + imageUploadRequest.fields['pekerjaan'] = newUser.pekerjaan; + imageUploadRequest.fields['alamat'] = newUser.alamat; + + final streamedResponse = await imageUploadRequest.send(); + final response = await Response.fromStream(streamedResponse); + return response; + }*/ } -- GitLab From 5c3bef2de261bee6895274e95d7269e34fdd58c4 Mon Sep 17 00:00:00 2001 From: Patricia Anugrah Setiani Date: Tue, 25 May 2021 10:03:49 +0700 Subject: [PATCH 20/45] check pbi 10a --- lib/page/profile/edit_profile.dart | 71 +---------------------------- lib/repository/user_repository.dart | 27 ----------- 2 files changed, 2 insertions(+), 96 deletions(-) diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index 68a9bc6..ab3b267 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -299,13 +299,7 @@ class _EditProfileState extends State { Future _validateLoginInput() async { dynamic fotoValidate = ''; - //fotoValue['foto'] = ''; if (_image != null) { - //final fileName = _image.path.split('/').last; - //fotoValue['foto'] = await MultipartFile.fromFile( - //_image.path, - //filename: fileName, - //); fotoValidate = _image.path; } @@ -322,7 +316,7 @@ class _EditProfileState extends State { disabilitas: jenisDisabilitasValue, pekerjaan: pekerjaanValue ?? '-', alamat: alamatController.text.toString(), - foto: fotoValidate //_image.path //fotoValue['foto'] + foto: fotoValidate ); newUserData['name'] = newUser.name; @@ -349,36 +343,9 @@ class _EditProfileState extends State { failedDialog(context); } } -/* - Future _validateLoginInput() async { - final form = _formKey.currentState; - if (_formKey.currentState.validate()) { - form.save(); - newUserData['name'] = nameController.text.toString(); - newUserData['phoneNumber'] = phoneController.text.toString(); - newUserData['tanggalLahir'] = tanggalLahir; - newUserData['jenisKelamin'] = jenisKelaminValue; - newUserData['disabilitas'] = jenisDisabilitasValue; - newUserData['pekerjaan'] = pekerjaanValue; - newUserData['alamat'] = alamatController.text.toString(); - newUserData['foto'] = ''; - if (_image != null) { - final fileName = _image.path.split('/').last; - newUserData['foto'] = await MultipartFile.fromFile( - _image.path, - filename: fileName, - ); - } - await _updateUser(); - } else { - failedDialog(context); - } - }*/ Future updatePPUser() async { bloc = NewUserBloc(); - //bloc = UserBloc(user.email); - //final response = await bloc.updateUserProfile(newUserData); final response = await bloc.updateUserPFP(newUserData); if (response.statusCode == 200) { successDialog(context); @@ -391,21 +358,7 @@ class _EditProfileState extends State { } bloc.dispose(); } -/* - Future _updateUser(NewUser newUser) async { - bloc = NewUserBloc(); - final response = await bloc.updateUser(newUser); - if (response.statusCode == 200) { - successDialog(context); - Timer(const Duration(seconds: 2), () { - _navigateToProfile(context, newUser); - }); - } else { - failedDialog(context); - } - bloc.dispose(); - } -*/ + void successDialog(BuildContext context) { const alertDialog = AlertDialog( title: Text('Update profile berhasil!'), @@ -449,26 +402,6 @@ class _EditProfileState extends State { Navigator.of(context).push(route); } - /* - void _navigateToProfile(BuildContext context, NewUser newUser) { - final userNew = UserModel( - username: newUser.email, - name: newUser.name, - email: newUser.email, - tanggalLahir: newUser.tanggalLahir, - phoneNumber: newUser.phoneNumber, - jenisKelamin: newUser.jenisKelamin, - disabilitas: newUser.disabilitas, - pekerjaan: newUser.pekerjaan, - alamat: newUser.alamat, - foto: newUser.foto); - final route = - MaterialPageRoute(builder: (_) => Profile(email: userNew.email)); - Navigator.of(context).pop(Navigator.pop(context)); - Navigator.pop(context); - Navigator.of(context).push(route); - } - */ Future _showPhotoSelectionDialog() async { await showDialog( context: context, diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart index 8a7ae33..12f958a 100644 --- a/lib/repository/user_repository.dart +++ b/lib/repository/user_repository.dart @@ -50,31 +50,4 @@ class UserRepository implements BaseUserRepository { ); return response; } - /* - @override - Future updateUser(NewUser newUser) async { - dynamic file; - Uri apiUrl = Uri.parse('${ApiFlavor.getBaseUrl()}/api/update-user/'); - final imageUploadRequest = MultipartRequest('POST', apiUrl); - if (newUser.foto != '') { - final fileName = newUser.foto.split('/').last; - file = await MultipartFile.fromPath( - newUser.foto, - fileName - ); - imageUploadRequest.files.add(file); - } - imageUploadRequest.fields['name'] = newUser.name; - imageUploadRequest.fields['password'] = newUser.password; - imageUploadRequest.fields['email'] = newUser.email; - imageUploadRequest.fields['tanggalLahir'] = newUser.tanggalLahir; - imageUploadRequest.fields['jenisKelamin'] = newUser.jenisKelamin; - imageUploadRequest.fields['disabilitas'] = newUser.disabilitas; - imageUploadRequest.fields['pekerjaan'] = newUser.pekerjaan; - imageUploadRequest.fields['alamat'] = newUser.alamat; - - final streamedResponse = await imageUploadRequest.send(); - final response = await Response.fromStream(streamedResponse); - return response; - }*/ } -- GitLab From cd2abc31d374fd03c73e4e2e6f67f4506383d51c Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Tue, 25 May 2021 16:33:17 +0700 Subject: [PATCH 21/45] [CHORES] Fix edit profile implementation & View others profile --- lib/bloc/new_user_bloc.dart | 6 +- lib/model/kegiatan.dart | 32 ++-- lib/model/kegiatan.g.dart | 2 + lib/model/komentar.dart | 28 ++-- lib/model/komentar.g.dart | 2 + lib/model/komentar_posting.dart | 10 +- lib/model/komentar_posting.g.dart | 2 + lib/model/komentar_posting_kegiatan.dart | 15 +- lib/model/komentar_posting_kegiatan.g.dart | 2 + lib/model/new_user.dart | 4 +- lib/model/new_user.g.dart | 2 + lib/model/user.dart | 29 ++-- lib/model/user.g.dart | 2 + lib/page/filter_fasilitas/kegiatan.dart | 146 +++++++++-------- lib/page/filter_fasilitas/komentar.dart | 1 + .../postingan/detail_post.dart | 92 ++++++++--- .../postingan/detail_post_kegiatan.dart | 7 +- lib/page/profile/edit_profile.dart | 155 +++++++++++------- lib/page/profile/profile.dart | 88 +++++----- lib/repository/komentar_repository.dart | 16 +- lib/repository/user_repository.dart | 17 +- test/detail_post_test.dart | 12 +- test/model_test.dart | 32 ++-- test/profile_test.dart | 14 +- test/user_test.dart | 26 +-- 25 files changed, 455 insertions(+), 287 deletions(-) diff --git a/lib/bloc/new_user_bloc.dart b/lib/bloc/new_user_bloc.dart index 8d62e78..4f695b1 100644 --- a/lib/bloc/new_user_bloc.dart +++ b/lib/bloc/new_user_bloc.dart @@ -33,8 +33,10 @@ class NewUserBloc { Future updateUserPFP(Map newUserData) async { try { - return await _userRepository.updateUserProfile(newUserData); - } catch (_) { + await _userRepository.updateUserProfile(newUserData); + return Response('Success', 200); + } catch (e) { + print(e); return Response('Failed to update user', 400); } } diff --git a/lib/model/kegiatan.dart b/lib/model/kegiatan.dart index 241c395..0903c22 100644 --- a/lib/model/kegiatan.dart +++ b/lib/model/kegiatan.dart @@ -15,6 +15,8 @@ class KegiatanModel { @JsonKey(name: 'place_id') final String placeId; final String creator; + @JsonKey(name: 'crator_email') + final String creatorEmail; @JsonKey(name: 'nama_kegiatan') final String namaKegiatan; final String penyelenggara; @@ -26,21 +28,21 @@ class KegiatanModel { final DateTime timeEnd; List image; - KegiatanModel( - { - this.id, - this.placeId, - this.creator, - this.namaKegiatan, - this.penyelenggara, - this.narahubung, - this.deskripsi, - this.timeStart, - this.timeEnd, - this.image, - } - ); + KegiatanModel({ + this.id, + this.placeId, + this.creator, + this.namaKegiatan, + this.penyelenggara, + this.narahubung, + this.deskripsi, + this.timeStart, + this.timeEnd, + this.image, + this.creatorEmail, + }); - factory KegiatanModel.fromJson(Map json) => _$KegiatanModelFromJson(json); + factory KegiatanModel.fromJson(Map json) => + _$KegiatanModelFromJson(json); Map toJson() => _$KegiatanModelToJson(this); } diff --git a/lib/model/kegiatan.g.dart b/lib/model/kegiatan.g.dart index f3e23ad..0c9e114 100644 --- a/lib/model/kegiatan.g.dart +++ b/lib/model/kegiatan.g.dart @@ -33,6 +33,7 @@ KegiatanModel _$KegiatanModelFromJson(Map json) { timeStart: CustomSerializer.stringToDateTime(json['time_start'] as String), timeEnd: CustomSerializer.stringToDateTime(json['time_end'] as String), image: (json['image'] as List)?.map((e) => e as String)?.toList(), + creatorEmail: json['crator_email'] as String, ); } @@ -41,6 +42,7 @@ Map _$KegiatanModelToJson(KegiatanModel instance) => 'id': instance.id, 'place_id': instance.placeId, 'creator': instance.creator, + 'crator_email': instance.creatorEmail, 'nama_kegiatan': instance.namaKegiatan, 'penyelenggara': instance.penyelenggara, 'narahubung': instance.narahubung, diff --git a/lib/model/komentar.dart b/lib/model/komentar.dart index 3a65b93..1efb152 100644 --- a/lib/model/komentar.dart +++ b/lib/model/komentar.dart @@ -25,19 +25,23 @@ class KomentarModel { final bool isVerified; final int jumlah; final int rating; + @JsonKey(name: 'creator_email') + final String creatorEmail; - KomentarModel( - {this.id, - this.namaLokasi, - this.deskripsi, - this.creator, - this.dateTime, - this.tag, - this.disabilitas, - this.image, - this.isVerified, - this.jumlah, - this.rating}); + KomentarModel({ + this.id, + this.namaLokasi, + this.deskripsi, + this.creator, + this.dateTime, + this.tag, + this.disabilitas, + this.image, + this.isVerified, + this.jumlah, + this.rating, + this.creatorEmail, + }); factory KomentarModel.fromJson(Map json) => _$KomentarModelFromJson(json); diff --git a/lib/model/komentar.g.dart b/lib/model/komentar.g.dart index 2755f33..314c20c 100644 --- a/lib/model/komentar.g.dart +++ b/lib/model/komentar.g.dart @@ -35,6 +35,7 @@ KomentarModel _$KomentarModelFromJson(Map json) { isVerified: json['is_verified'] as bool, jumlah: json['jumlah'] as int, rating: json['rating'] as int, + creatorEmail: json['creator_email'] as String, ); } @@ -51,4 +52,5 @@ Map _$KomentarModelToJson(KomentarModel instance) => 'is_verified': instance.isVerified, 'jumlah': instance.jumlah, 'rating': instance.rating, + 'creator_email': instance.creatorEmail, }; diff --git a/lib/model/komentar_posting.dart b/lib/model/komentar_posting.dart index 04ffea6..eebf956 100644 --- a/lib/model/komentar_posting.dart +++ b/lib/model/komentar_posting.dart @@ -16,8 +16,16 @@ class KomentarPostingModel { final String creator; @JsonKey(name: 'date_time', fromJson: CustomSerializer.stringToDateTime) final DateTime dateTime; + @JsonKey(name: 'creator_email') + final String creatorEmail; - KomentarPostingModel({this.id, this.deskripsi, this.creator, this.dateTime}); + KomentarPostingModel({ + this.id, + this.deskripsi, + this.creator, + this.dateTime, + this.creatorEmail, + }); factory KomentarPostingModel.fromJson(Map json) => _$KomentarPostingModelFromJson(json); diff --git a/lib/model/komentar_posting.g.dart b/lib/model/komentar_posting.g.dart index 565bcd3..f586849 100644 --- a/lib/model/komentar_posting.g.dart +++ b/lib/model/komentar_posting.g.dart @@ -28,6 +28,7 @@ KomentarPostingModel _$KomentarPostingModelFromJson(Map json) { deskripsi: json['deskripsi'] as String, creator: json['creator'] as String, dateTime: CustomSerializer.stringToDateTime(json['date_time'] as String), + creatorEmail: json['creator_email'] as String, ); } @@ -38,4 +39,5 @@ Map _$KomentarPostingModelToJson( 'deskripsi': instance.deskripsi, 'creator': instance.creator, 'date_time': instance.dateTime?.toIso8601String(), + 'creator_email': instance.creatorEmail, }; diff --git a/lib/model/komentar_posting_kegiatan.dart b/lib/model/komentar_posting_kegiatan.dart index 78e18d7..d14c553 100644 --- a/lib/model/komentar_posting_kegiatan.dart +++ b/lib/model/komentar_posting_kegiatan.dart @@ -13,16 +13,19 @@ class KomentarPostingKegiatanList { class KomentarPostingKegiatanModel { final int id; final String creator; + @JsonKey(name: 'crator_email') + final String creatorEmail; final String deskripsi; @JsonKey(name: 'created', fromJson: CustomSerializer.stringToDateTime) final DateTime created; - KomentarPostingKegiatanModel( - {this.id, - this.creator, - this.deskripsi, - this.created} - ); + KomentarPostingKegiatanModel({ + this.id, + this.creator, + this.deskripsi, + this.created, + this.creatorEmail, + }); factory KomentarPostingKegiatanModel.fromJson(Map json) => _$KomentarPostingKegiatanModelFromJson(json); diff --git a/lib/model/komentar_posting_kegiatan.g.dart b/lib/model/komentar_posting_kegiatan.g.dart index 3b73f1a..59124a1 100644 --- a/lib/model/komentar_posting_kegiatan.g.dart +++ b/lib/model/komentar_posting_kegiatan.g.dart @@ -30,6 +30,7 @@ KomentarPostingKegiatanModel _$KomentarPostingKegiatanModelFromJson( creator: json['creator'] as String, deskripsi: json['deskripsi'] as String, created: CustomSerializer.stringToDateTime(json['created'] as String), + creatorEmail: json['crator_email'] as String, ); } @@ -38,6 +39,7 @@ Map _$KomentarPostingKegiatanModelToJson( { 'id': instance.id, 'creator': instance.creator, + 'crator_email': instance.creatorEmail, 'deskripsi': instance.deskripsi, 'created': instance.created?.toIso8601String(), }; diff --git a/lib/model/new_user.dart b/lib/model/new_user.dart index c7173a0..13301af 100644 --- a/lib/model/new_user.dart +++ b/lib/model/new_user.dart @@ -16,6 +16,7 @@ class NewUser { @JsonKey(name: 'phone_number') String phoneNumber; String foto; + bool seen; NewUser({ this.name, @@ -27,7 +28,8 @@ class NewUser { this.pekerjaan, this.alamat, this.phoneNumber, - this.foto + this.foto, + this.seen, }); factory NewUser.fromJson(Map json) => diff --git a/lib/model/new_user.g.dart b/lib/model/new_user.g.dart index 8628dbe..c0e14ca 100644 --- a/lib/model/new_user.g.dart +++ b/lib/model/new_user.g.dart @@ -18,6 +18,7 @@ NewUser _$NewUserFromJson(Map json) { alamat: json['alamat'] as String, phoneNumber: json['phone_number'] as String, foto: json['foto'] as String, + seen: json['seen'] as bool, ); } @@ -32,4 +33,5 @@ Map _$NewUserToJson(NewUser instance) => { 'alamat': instance.alamat, 'phone_number': instance.phoneNumber, 'foto': instance.foto, + 'seen': instance.seen, }; diff --git a/lib/model/user.dart b/lib/model/user.dart index 0afcec2..fc8f22d 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -24,20 +24,23 @@ class UserModel { String alamat; String token; String foto; + bool seen; - UserModel( - {this.is_login, - this.username, - this.name, - this.email, - this.tanggalLahir, - this.phoneNumber, - this.jenisKelamin, - this.disabilitas, - this.pekerjaan, - this.alamat, - this.token, - this.foto}); + UserModel({ + this.is_login, + this.username, + this.name, + this.email, + this.tanggalLahir, + this.phoneNumber, + this.jenisKelamin, + this.disabilitas, + this.pekerjaan, + this.alamat, + this.token, + this.foto, + this.seen, + }); factory UserModel.fromJson(Map json) => _$UserModelFromJson(json); diff --git a/lib/model/user.g.dart b/lib/model/user.g.dart index 90ad6ba..23491c5 100644 --- a/lib/model/user.g.dart +++ b/lib/model/user.g.dart @@ -33,6 +33,7 @@ UserModel _$UserModelFromJson(Map json) { alamat: json['alamat'] as String, token: json['token'] as String, foto: json['foto'] as String, + seen: json['seen'] as bool, ); } @@ -49,4 +50,5 @@ Map _$UserModelToJson(UserModel instance) => { 'alamat': instance.alamat, 'token': instance.token, 'foto': instance.foto, + 'seen': instance.seen, }; diff --git a/lib/page/filter_fasilitas/kegiatan.dart b/lib/page/filter_fasilitas/kegiatan.dart index d668443..ebc321f 100644 --- a/lib/page/filter_fasilitas/kegiatan.dart +++ b/lib/page/filter_fasilitas/kegiatan.dart @@ -17,7 +17,6 @@ class Kegiatan extends StatefulWidget { } class _KegiatanState extends State { - @override Widget build(BuildContext context) { return InkWell( @@ -25,21 +24,21 @@ class _KegiatanState extends State { onTap: () { Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (BuildContext context) => DetailPostKegiatanPage( - lokasi: widget.lokasi, - kegiatan: KegiatanModel( - id: widget.kegiatan.id, - placeId: widget.kegiatan.placeId, - creator: widget.kegiatan.creator, - namaKegiatan: widget.kegiatan.namaKegiatan, - penyelenggara: widget.kegiatan.penyelenggara, - narahubung: widget.kegiatan.narahubung, - deskripsi: widget.kegiatan.deskripsi, - timeStart: widget.kegiatan.timeStart, - timeEnd: widget.kegiatan.timeEnd, - image: widget.kegiatan.image, - ), - ) - )); + lokasi: widget.lokasi, + kegiatan: KegiatanModel( + id: widget.kegiatan.id, + placeId: widget.kegiatan.placeId, + creator: widget.kegiatan.creator, + namaKegiatan: widget.kegiatan.namaKegiatan, + penyelenggara: widget.kegiatan.penyelenggara, + narahubung: widget.kegiatan.narahubung, + deskripsi: widget.kegiatan.deskripsi, + timeStart: widget.kegiatan.timeStart, + timeEnd: widget.kegiatan.timeEnd, + image: widget.kegiatan.image, + creatorEmail: widget.kegiatan.creatorEmail, + ), + ))); }, child: Container( margin: const EdgeInsets.only(bottom: regularSpace), @@ -72,59 +71,68 @@ class _KegiatanState extends State { ), ), Container( - margin: const EdgeInsets.only(bottom: regularSpace), - child: SizedBox( - width: MediaQuery.of(context).size.width, - height: 160, - child: CarouselSlider( - options: CarouselOptions( - aspectRatio: 1.0, - enlargeCenterPage: true, - enableInfiniteScroll: false, - initialPage: 0, - autoPlay: true, - ), - items: widget.kegiatan.image.map((item) => Container( - child: Container( - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(20)), - child: Stack( - children: [ - Image.network(item, fit: BoxFit.cover, width: 1000.0), - Positioned( - bottom: 0.0, - left: 0.0, - right: 0.0, + margin: const EdgeInsets.only(bottom: regularSpace), + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: 160, + child: CarouselSlider( + options: CarouselOptions( + aspectRatio: 1.0, + enlargeCenterPage: true, + enableInfiniteScroll: false, + initialPage: 0, + autoPlay: true, + ), + items: widget.kegiatan.image + .map((item) => Container( child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromARGB(200, 0, 0, 0), - Color.fromARGB(0, 0, 0, 0) - ], - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - ), - ), - padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - child: Text( - '#${widget.kegiatan.image.indexOf(item)+1}', - style: TextStyle( - color: Colors.white, - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), - ), + child: ClipRRect( + borderRadius: BorderRadius.all( + Radius.circular(20)), + child: Stack( + children: [ + Image.network(item, + fit: BoxFit.cover, + width: 1000.0), + Positioned( + bottom: 0.0, + left: 0.0, + right: 0.0, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color.fromARGB( + 200, 0, 0, 0), + Color.fromARGB( + 0, 0, 0, 0) + ], + begin: Alignment + .bottomCenter, + end: Alignment.topCenter, + ), + ), + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 20.0), + child: Text( + '#${widget.kegiatan.image.indexOf(item) + 1}', + style: TextStyle( + color: Colors.white, + fontSize: 20.0, + fontWeight: + FontWeight.bold, + ), + ), + ), + ), + ], + )), ), - ), - ], - ) - ), - ), - )).toList(), - ), - ) - ), + )) + .toList(), + ), + )), Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, @@ -148,5 +156,5 @@ class _KegiatanState extends State { ], ), )); - } -} \ No newline at end of file + } +} diff --git a/lib/page/filter_fasilitas/komentar.dart b/lib/page/filter_fasilitas/komentar.dart index dda5b47..345a7d2 100644 --- a/lib/page/filter_fasilitas/komentar.dart +++ b/lib/page/filter_fasilitas/komentar.dart @@ -51,6 +51,7 @@ class _KomentarState extends State { tag: widget.komentar.tag, disabilitas: widget.komentar.disabilitas, jumlah: widget.komentar.jumlah, + creatorEmail: widget.komentar.creatorEmail, ), ))); }, diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index af1d54d..c79d026 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bisaGo/config/strings.dart'; import 'package:bisaGo/model/lokasi.dart'; import 'package:bisaGo/page/filter_fasilitas/fasilitas.dart'; +import 'package:bisaGo/page/profile/profile.dart'; import 'package:bisaGo/page/updateInformasi/update_informasi.dart'; import 'package:bisaGo/repository/dynamic_links_service_repository.dart'; import 'package:bisaGo/utils/share_utils.dart'; @@ -244,14 +245,27 @@ class _DetailPostPageState extends State { constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.3), - child: Text( - '${widget.komentar.creator} ', - key: Key('creator-${widget.komentar.creator}'), - overflow: TextOverflow.fade, - softWrap: false, - style: const TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => Profile( + email: widget.komentar.creatorEmail, + isPublic: true, + ), + ), + ); + }, + child: Text( + '${widget.komentar.creator} ', + key: Key('creator-${widget.komentar.creator}'), + overflow: TextOverflow.fade, + softWrap: false, + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), ), ), ), @@ -303,9 +317,14 @@ class _DetailPostPageState extends State { } else { return Column( children: allKomentarPostingFromApi - .map((k) => - komentarPlaceHolder(k.creator, - k.dateTime, k.deskripsi)) + .map( + (k) => komentarPlaceHolder( + k.creator, + k.dateTime, + k.deskripsi, + k.creatorEmail, + ), + ) .toList()); } break; @@ -474,18 +493,36 @@ class _DetailPostPageState extends State { }); } - Widget komentarPlaceHolder(String name, DateTime date, String description) { + Widget komentarPlaceHolder( + String name, + DateTime date, + String description, + String email, + ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - CircleAvatar( - backgroundColor: greenPrimary, - child: Text( - _creatorInitials(name), - style: const TextStyle( - color: Colors.white, + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => Profile( + email: email, + isPublic: true, + ), + ), + ); + }, + child: CircleAvatar( + backgroundColor: greenPrimary, + child: Text( + _creatorInitials(name), + style: const TextStyle( + color: Colors.white, + ), ), ), ), @@ -494,9 +531,22 @@ class _DetailPostPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - name, - style: const TextStyle(fontSize: 18), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => Profile( + email: email, + isPublic: true, + ), + ), + ); + }, + child: Text( + name, + style: const TextStyle(fontSize: 18), + ), ), Text('${DateFormat('dd MMMM yyy hh:mm').format(date)}', style: const TextStyle(color: grayPrimary, fontSize: 14)) diff --git a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart index fbc07c0..d6e186a 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart @@ -376,9 +376,10 @@ class _DetailPostKegiatanPageState extends State { allKomentarPositngKegiatanFromApi .map((k) => komentarKegiatanPlaceHolder( - k.creator, - k.created, - k.deskripsi)) + k.creator, + k.created, + k.deskripsi, + )) .toList()); } break; diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index ab3b267..f349b96 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -35,6 +35,7 @@ class _EditProfileState extends State { //UserBloc bloc; final picker = ImagePicker(); File _image; + bool _rahasiakanData; Future _getGalleryImage() async { final image = await picker.getImage(source: ImageSource.gallery); @@ -47,11 +48,11 @@ class _EditProfileState extends State { return File(image.path); } - Future _clearImage() async { - setState(() { - _image = null; - }); - } + Future _clearImage() async { + setState(() { + _image = null; + }); + } _EditProfileState(this.user); @@ -66,7 +67,7 @@ class _EditProfileState extends State { tanggalLahir = user.tanggalLahir; jenisDisabilitasValue = user.disabilitas; pekerjaanValue = user.pekerjaan; - //fotoValue = user.foto; + _rahasiakanData = user.seen; } @override @@ -117,25 +118,28 @@ class _EditProfileState extends State { radius: 50, backgroundColor: white, child: ClipOval( - child: (_image != null) - ? SizedBox( - width: 100, - height: 100, - child: Image.file(_image, fit: BoxFit.cover), - ) - : Text(user.name.substring(0, 1), - style: const TextStyle( - fontSize: 45, - fontWeight: FontWeight.w900, - color: darkGreen, - fontFamily: 'Comfortaa', - ) - ), - ), + child: (_image != null) + ? SizedBox( + width: 100, + height: 100, + child: Image.file(_image, fit: BoxFit.cover), + ) + : Text(user.name.substring(0, 1), + style: const TextStyle( + fontSize: 45, + fontWeight: FontWeight.w900, + color: darkGreen, + fontFamily: 'Comfortaa', + )), + ), ), Padding( - padding: const EdgeInsets.symmetric(vertical: doubleSpace), + padding: + const EdgeInsets.symmetric(vertical: doubleSpace), child: InkWell( + onTap: () { + _showPhotoSelectionDialog(); + }, child: Text( 'Ubah Foto Profil', style: const TextStyle( @@ -146,9 +150,50 @@ class _EditProfileState extends State { ), textAlign: TextAlign.center, ), - onTap: () { - _showPhotoSelectionDialog(); - }, + ), + ), + Container( + margin: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Rahasiakan data?', + key: Key('Rahasiakan data?'), + style: const TextStyle( + fontSize: 18, + color: Colors.black, + fontFamily: 'Muli', + ), + ), + Center( + child: Text( + 'Data yang akan dirahasiakan adalah: nomor telepon, email, dan alamat.', + style: TextStyle( + fontSize: 10.0, + ), + ), + ), + ], + ), + ), + Switch( + value: _rahasiakanData, + focusColor: Colors.green, + hoverColor: Colors.green, + activeColor: Colors.green, + activeTrackColor: Colors.green, + onChanged: (value) { + setState(() { + _rahasiakanData = value; + }); + }, + ), + ], ), ), CustomTextField( @@ -307,36 +352,27 @@ class _EditProfileState extends State { if (_formKey.currentState.validate()) { form.save(); newUser = NewUser( - name: nameController.text.toString(), - password: '', - email: emailController.text.toString(), - phoneNumber: phoneController.text.toString(), - tanggalLahir: tanggalLahir, - jenisKelamin: jenisKelaminValue ?? '-', - disabilitas: jenisDisabilitasValue, - pekerjaan: pekerjaanValue ?? '-', - alamat: alamatController.text.toString(), - foto: fotoValidate - ); - - newUserData['name'] = newUser.name; - newUserData['password'] = newUser.password; - newUserData['email'] = newUser.email; - newUserData['phoneNumber'] = newUser.phoneNumber; - newUserData['tanggalLahir'] = newUser.tanggalLahir; - newUserData['jenisKelamin'] = newUser.jenisKelamin; - newUserData['disabilitas'] = newUser.disabilitas; - newUserData['pekerjaan'] = newUser.pekerjaan; - newUserData['alamat'] = newUser.alamat; - newUserData['foto'] = ''; + name: nameController.text.toString(), + password: '', + email: emailController.text.toString(), + phoneNumber: phoneController.text.toString(), + tanggalLahir: tanggalLahir, + jenisKelamin: jenisKelaminValue ?? '-', + disabilitas: jenisDisabilitasValue, + pekerjaan: pekerjaanValue ?? '-', + alamat: alamatController.text.toString(), + foto: fotoValidate, + seen: _rahasiakanData, + ); + newUserData = newUser.toJson(); if (_image != null) { final fileName = _image.path.split('/').last; newUserData['foto'] = await MultipartFile.fromFile( - newUser.foto, + _image.path, filename: fileName, ); } - + //await updateUser(newUser); await updatePPUser(); } else { @@ -401,7 +437,7 @@ class _EditProfileState extends State { Navigator.pop(context); Navigator.of(context).push(route); } - + Future _showPhotoSelectionDialog() async { await showDialog( context: context, @@ -409,35 +445,36 @@ class _EditProfileState extends State { title: Text('Pilih Foto'), children: [ SimpleDialogOption( - child: Text('Dari Gallery'), onPressed: () async { final imageSelected = await _getGalleryImage(); - setState((){ + setState(() { _image = imageSelected; }); Navigator.pop(context); }, + child: Text('Dari Gallery'), ), SimpleDialogOption( - child: Text('Dari Kamera'), onPressed: () async { final imageSelected = await _getCameraImage(); - setState((){ + setState(() { _image = imageSelected; }); Navigator.pop(context); }, + child: Text('Dari Kamera'), ), SimpleDialogOption( - child: Text('Hapus Foto', - style: TextStyle( - color: Colors.red[800], - ), - ), onPressed: () async { Navigator.pop(context); await _clearImage(); }, + child: Text( + 'Hapus Foto', + style: TextStyle( + color: Colors.red[800], + ), + ), ), ], ), diff --git a/lib/page/profile/profile.dart b/lib/page/profile/profile.dart index d531d12..68ad2a3 100644 --- a/lib/page/profile/profile.dart +++ b/lib/page/profile/profile.dart @@ -1,16 +1,21 @@ import 'package:bisaGo/bloc/user_bloc.dart'; import 'package:bisaGo/component/bisago_appbar.dart'; +import 'package:bisaGo/component/image_holder.dart'; import 'package:bisaGo/config/styles.dart'; import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/profile/edit_profile.dart'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:bisaGo/component/image_holder.dart'; class Profile extends StatefulWidget { final String email; + final bool isPublic; - const Profile({@required this.email, Key key}) : super(key: key); + const Profile({ + @required this.email, + Key key, + this.isPublic = false, + }) : super(key: key); @override _ProfileState createState() => _ProfileState(email); } @@ -39,19 +44,20 @@ class _ProfileState extends State { child: const Icon(Icons.arrow_back_ios), ), actions: [ - Padding( - padding: const EdgeInsets.all(doubleSpace), - child: InkWell( - key: const Key('Edit User Profile'), - onTap: () { - _navigateToEditProfile(context); - }, - child: const Text( - 'Edit', - style: TextStyle(fontSize: 15), + if (!widget.isPublic) + Padding( + padding: const EdgeInsets.all(doubleSpace), + child: InkWell( + key: const Key('Edit User Profile'), + onTap: () { + _navigateToEditProfile(context); + }, + child: const Text( + 'Edit', + style: TextStyle(fontSize: 15), + ), ), - ), - ) + ) ], ), ), @@ -99,35 +105,35 @@ class _ProfileState extends State { radius: 50, backgroundColor: white, child: ClipOval( - child: /*(user.foto != '') - ? SizedBox( - width: 100, - height: 100, - child: ImageHolder( - url: user.foto, - ), - ) - :*/ Text(user.name.substring(0, 1), - style: const TextStyle( - fontSize: 45, - fontWeight: FontWeight.w900, - color: darkGreen, - fontFamily: 'Comfortaa', - ) - ), + child: (user.foto != null) + ? SizedBox( + width: 100, + height: 100, + child: ImageHolder( + url: user.foto, + ), + ) + : Text(user.name.substring(0, 1), + style: const TextStyle( + fontSize: 45, + fontWeight: FontWeight.w900, + color: darkGreen, + fontFamily: 'Comfortaa', + )), ), ), ), - Text( - 'Halo, ${user.name.split(' ')[0]}!', - style: const TextStyle( - fontSize: 26, - fontWeight: FontWeight.w900, - color: Colors.white, - fontFamily: 'Comfortaa', + if (!widget.isPublic) + Text( + 'Halo, ${user.name.split(' ')[0]}!', + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.w900, + color: Colors.white, + fontFamily: 'Comfortaa', + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ), Text( user.email, style: const TextStyle( @@ -147,7 +153,9 @@ class _ProfileState extends State { key: const Key('Card Profile Page'), alignment: Alignment.topCenter, padding: EdgeInsets.only( - top: MediaQuery.of(context).size.height * .32, + top: widget.isPublic + ? MediaQuery.of(context).size.height * .32 - 30 + : MediaQuery.of(context).size.height * .32, left: tripleSpace, right: tripleSpace, bottom: tripleSpace), diff --git a/lib/repository/komentar_repository.dart b/lib/repository/komentar_repository.dart index cf34a3e..9d18614 100644 --- a/lib/repository/komentar_repository.dart +++ b/lib/repository/komentar_repository.dart @@ -7,8 +7,7 @@ abstract class BaseKomentarRepository { Map newKomentarData, String namaLokasi); Future updateKomentar( Map newKomentarData, String placeId, int id); - Future fetchDetailFasilitas( - String placeId, int fasilitasId); + Future fetchDetailFasilitas(String placeId, int fasilitasId); } class KomentarRepository implements BaseKomentarRepository { @@ -35,10 +34,11 @@ class KomentarRepository implements BaseKomentarRepository { } @override - Future updateKomentar(Map newKomentarData, - String placeId, int id) async { + Future updateKomentar( + Map newKomentarData, String placeId, int id) async { final response = await _network.put( - url: '/informasi-fasilitas/lokasi/update-fasilitas/$placeId/${id.toString()}/', + url: + '/informasi-fasilitas/lokasi/update-fasilitas/$placeId/${id.toString()}/', bodyParams: newKomentarData, isLogin: true, ); @@ -46,8 +46,10 @@ class KomentarRepository implements BaseKomentarRepository { } @override - Future fetchDetailFasilitas(String placeId, int fasilitasId) async { - final url = '/informasi-fasilitas/lokasi/detail-fasilitas/$placeId/$fasilitasId/'; + Future fetchDetailFasilitas( + String placeId, int fasilitasId) async { + final url = + '/informasi-fasilitas/lokasi/detail-fasilitas/$placeId/$fasilitasId/'; final response = await _network.get(url: url, isLogin: false); return KomentarModel.fromJson(response); } diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart index 12f958a..3dadee3 100644 --- a/lib/repository/user_repository.dart +++ b/lib/repository/user_repository.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:bisaGo/flavor/flavor.dart'; import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/model/user.dart'; @@ -11,13 +10,14 @@ abstract class BaseUserRepository { Future updateUser(NewUser newUser); Future updateUserProfile(Map newUserData); } + class UserRepository implements BaseUserRepository { final NetworkInterface _network = NetworkInterface(); @override Future fetchUserDetail(String email) async { final response = - await _network.get(url: '/api/user-detail/$email', isLogin: true); + await _network.get(url: '/api/user/$email/', isLogin: true); final data = [response]; return User( data.map((user) => UserModel.fromJson(user)).toList()); @@ -33,15 +33,18 @@ class UserRepository implements BaseUserRepository { } @override - Future updateUserProfile(Map newUserData) async { - final response = await post( - '${ApiFlavor.getBaseUrl()}/api/update-user/', - body: json.encode(newUserData) + Future updateUserProfile( + Map newUserData, + ) async { + final String email = newUserData['email']; + final response = await _network.put( + url: '/api/user/$email/', + bodyParams: newUserData, + isLogin: true, ); return response; } - @override Future updateUser(NewUser newUser) async { final response = await post( diff --git a/test/detail_post_test.dart b/test/detail_post_test.dart index 0d6625d..8a1e83d 100644 --- a/test/detail_post_test.dart +++ b/test/detail_post_test.dart @@ -66,13 +66,21 @@ void main() { testWidgets('Create a komentar placeholder', (WidgetTester tester) async { final detailPostState = detailPostPage.createState(); detailPostState.komentarPlaceHolder( - 'Halo', DateTime(2020, 1, 1), 'This is a test'); + 'Halo', + DateTime(2020, 1, 1), + 'This is a test', + 'test@email.com', + ); }); testWidgets('Create a komentar placeholder', (WidgetTester tester) async { final detailPostState = detailPostPage.createState(); detailPostState.komentarPlaceHolder( - 'Halo', DateTime(2020, 1, 1), 'This is a test'); + 'Halo', + DateTime(2020, 1, 1), + 'This is a test', + 'test@email.com', + ); }); test('Should decrypt tag code', () { diff --git a/test/model_test.dart b/test/model_test.dart index 9978873..aabb467 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -9,26 +9,30 @@ void main() { 'id': 1, 'deskripsi': 'This is a test', 'creator': 'Test', - 'date_time': '2020-11-18 00:13:52.939668' + 'date_time': '2020-11-18 00:13:52.939668', + 'creator_email': 'test@email.com' }; final returnKomentarPostingData = { 'id': 1, 'deskripsi': 'This is a test', 'creator': 'Test', - 'date_time': '2020-11-18T00:13:00.000' + 'date_time': '2020-11-18T00:13:00.000', + 'creator_email': 'test@email.com' }; final userData = { 'is_login': true, - 'username': 'test@gmail.com', + 'username': 'test@email.com', 'name': 'test', - 'email': 'test@gmail.com', + 'email': 'test@email.com', 'tanggal_lahir': '2000-01-01', 'phone_number': '081234567898', 'jenis_kelamin': 'Laki-Laki', 'disabilitas': 'Tidak Memiliki Disabilitas', 'pekerjaan': 'Mahasiswa', 'alamat': 'Tidak Tahu', - 'token': null + 'token': null, + 'seen': true, + 'foto': '', }; final komentarData = { 'id': 1, @@ -42,7 +46,8 @@ void main() { 'disabilitas': ['DF'], 'jumlah': 1, 'image': 'static/img/2669211407.jpg', - 'is_verified': false + 'is_verified': false, + 'creator_email': 'test@email.com' }; final returnKomentarData = { 'id': 1, @@ -56,11 +61,12 @@ void main() { 'disabilitas': ['DF'], 'jumlah': 1, 'image': 'static/img/2669211407.jpg', - 'is_verified': false + 'is_verified': false, + 'creator_email': 'test@email.com', }; final newUserData = { 'name': 'test', - 'email': 'test@gmail.com', + 'email': 'test@email.com', 'password': '2139809348143123', 'tanggal_lahir': '2000-01-01', 'phone_number': '081234567898', @@ -74,10 +80,12 @@ void main() { final komentarPostingModel = KomentarPostingModel.fromJson(komentarPostingData); final komentarPostingWithConstructor = KomentarPostingModel( - id: 2, - deskripsi: 'This is a test', - creator: 'Test', - dateTime: DateTime.now()); + id: 2, + deskripsi: 'This is a test', + creator: 'Test', + dateTime: DateTime.now(), + creatorEmail: 'test@email.com', + ); expect(komentarPostingModel, isInstanceOf()); expect( komentarPostingWithConstructor, isInstanceOf()); diff --git a/test/profile_test.dart b/test/profile_test.dart index bd66992..bb2699f 100644 --- a/test/profile_test.dart +++ b/test/profile_test.dart @@ -22,7 +22,8 @@ class MockUserRepository extends Fake implements UserRepository { 'jenis_kelamin': 'Laki-Laki', 'disabilitas': 'Tidak memiliki disabilitas', 'pekerjaan': 'Pelajar', - 'alamat': 'Tidak Tahu' + 'alamat': 'Tidak Tahu', + 'seen': true, }; @override @@ -166,7 +167,8 @@ void main() { 'jenis_kelamin': 'Laki-laki', 'disabilitas': 'Tidak memiliki disabilitas', 'pekerjaan': 'Pelajar', - 'alamat': 'Tidak Tahu' + 'alamat': 'Tidak Tahu', + 'seen': true, }; final userData2 = { @@ -177,7 +179,8 @@ void main() { 'jenis_kelamin': 'Laki-laki', 'disabilitas': 'Tidak memiliki disabilitas', 'pekerjaan': 'Pelajar', - 'alamat': '' + 'alamat': '', + 'seen': true, }; testWidgets('Edit Profile Page Widget Test -- Positive', @@ -224,7 +227,7 @@ void main() { (WidgetTester tester) async { //final userAvatarKey = Key('Avatar test'); final name = 'test'; - + await tester.pumpWidget( MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); await tester.pumpAndSettle(); @@ -354,7 +357,8 @@ void main() { await tester.tap(find.byKey(Key('Button Simpan'))); await tester.pumpAndSettle(); - expect(find.text('Update profile tidak berhasil!', skipOffstage: false), findsOneWidget); + expect(find.text('Update profile tidak berhasil!', skipOffstage: false), + findsOneWidget); }); testWidgets('Require all text fields to be filled -- Positive', diff --git a/test/user_test.dart b/test/user_test.dart index 4703a5b..3078282 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -3,17 +3,19 @@ import 'package:flutter_test/flutter_test.dart'; void main() { final userData = { - 'is_login':true, - 'username':'test@gmail.com', - 'name':'test', - 'email':'test@gmail.com', - 'tanggal_lahir':'2000-01-01', - 'phone_number':'081234567898', - 'jenis_kelamin':'Laki-Laki', - 'disabilitas':'Tidak Memiliki Disabilitas', - 'pekerjaan':'Mahasiswa', - 'alamat':'Tidak Tahu', - 'token':null + 'is_login': true, + 'username': 'test@gmail.com', + 'name': 'test', + 'email': 'test@gmail.com', + 'tanggal_lahir': '2000-01-01', + 'phone_number': '081234567898', + 'jenis_kelamin': 'Laki-Laki', + 'disabilitas': 'Tidak Memiliki Disabilitas', + 'pekerjaan': 'Mahasiswa', + 'alamat': 'Tidak Tahu', + 'token': null, + 'seen': true, + 'foto': '', }; test('User Model', () { @@ -22,4 +24,4 @@ void main() { expect(user.user.length, 1); expect(userModel.first.toJson(), userData); }); -} \ No newline at end of file +} -- GitLab From 7ea7884efb53c46301c654e5be301c61f8f220a8 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Thu, 27 May 2021 13:38:53 +0700 Subject: [PATCH 22/45] [CHORES] Change main and main_staging --- lib/main.dart | 15 +++++++++++++-- lib/main_staging.dart | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 1d165ee..aa484a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ -import 'package:bisaGo/get_it.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:bisaGo/app.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -6,14 +7,24 @@ import 'package:intl/intl.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'flavor/flavor.dart'; import 'globalnetwork.dart'; +import 'package:bisaGo/get_it.dart'; + +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // If you're going to use other Firebase services in the background, such as Firestore, + // make sure you call `initializeApp` before using other Firebase services. + await Firebase.initializeApp(); + + print('Handling a background message: ${message.messageId}'); +} Future main() async { AppGetIt().initialize(); await DotEnv().load('.env'); getDioInstance('build'); await initializeDateFormatting('id_ID', null); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); Intl.defaultLocale = 'id_ID'; dio.options.receiveTimeout = 15000; - ApiFlavor.flavor = BuildFlavor.production.toString(); + ApiFlavor.flavor = BuildFlavor.development.toString(); runApp(BisaGo()); } diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 564412c..aa484a7 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -1,3 +1,5 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:bisaGo/app.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -7,13 +9,22 @@ import 'flavor/flavor.dart'; import 'globalnetwork.dart'; import 'package:bisaGo/get_it.dart'; +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // If you're going to use other Firebase services in the background, such as Firestore, + // make sure you call `initializeApp` before using other Firebase services. + await Firebase.initializeApp(); + + print('Handling a background message: ${message.messageId}'); +} + Future main() async { AppGetIt().initialize(); await DotEnv().load('.env'); getDioInstance('build'); await initializeDateFormatting('id_ID', null); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); Intl.defaultLocale = 'id_ID'; dio.options.receiveTimeout = 15000; - ApiFlavor.flavor = BuildFlavor.staging.toString(); + ApiFlavor.flavor = BuildFlavor.development.toString(); runApp(BisaGo()); -} \ No newline at end of file +} -- GitLab From d89cf6ca1cabadadebbc64020b058e7d33f9e0d5 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Mon, 31 May 2021 13:32:46 +0700 Subject: [PATCH 23/45] [CHORES] Add hidden fields in user model --- lib/model/user.dart | 2 ++ lib/model/user.g.dart | 4 +++- lib/page/profile/edit_profile.dart | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/model/user.dart b/lib/model/user.dart index fc8f22d..3fd8170 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -25,6 +25,8 @@ class UserModel { String token; String foto; bool seen; + @JsonKey(name: 'hidden_fields') + List hiddenFields; UserModel({ this.is_login, diff --git a/lib/model/user.g.dart b/lib/model/user.g.dart index 23491c5..34dc01b 100644 --- a/lib/model/user.g.dart +++ b/lib/model/user.g.dart @@ -34,7 +34,8 @@ UserModel _$UserModelFromJson(Map json) { token: json['token'] as String, foto: json['foto'] as String, seen: json['seen'] as bool, - ); + )..hiddenFields = + (json['hidden_fields'] as List)?.map((e) => e as String)?.toList(); } Map _$UserModelToJson(UserModel instance) => { @@ -51,4 +52,5 @@ Map _$UserModelToJson(UserModel instance) => { 'token': instance.token, 'foto': instance.foto, 'seen': instance.seen, + 'hidden_fields': instance.hiddenFields, }; diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index f349b96..1d4ca8b 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -172,7 +172,7 @@ class _EditProfileState extends State { ), Center( child: Text( - 'Data yang akan dirahasiakan adalah: nomor telepon, email, dan alamat.', + 'Data yang akan dirahasiakan adalah: ${widget.user.hiddenFields.join(', ')}', style: TextStyle( fontSize: 10.0, ), -- GitLab From defd63beceaaddecaa8263023b7ef0d0246fa8ab Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Mon, 31 May 2021 14:38:41 +0700 Subject: [PATCH 24/45] [CHORES] Add organisasi field to register page --- lib/bloc/new_user_bloc.dart | 10 ++--- lib/model/new_user.dart | 3 ++ lib/model/new_user.g.dart | 2 + lib/model/user.dart | 3 ++ lib/model/user.g.dart | 2 + lib/network/network_interface.dart | 12 +++--- .../postingan/detail_post.dart | 3 -- .../postingan/detail_post_kegiatan.dart | 3 -- lib/page/profile/edit_profile.dart | 21 +++++++--- lib/page/profile/profile.dart | 20 ++++++++++ lib/page/registrasi/registrasi.dart | 39 ++++++++++++------- lib/repository/user_repository.dart | 11 +++--- lib/utils/validator.dart | 8 ++++ test/model_test.dart | 3 ++ test/profile_test.dart | 9 ++++- test/registrasi_test.dart | 7 ++-- test/user_test.dart | 2 + 17 files changed, 112 insertions(+), 46 deletions(-) diff --git a/lib/bloc/new_user_bloc.dart b/lib/bloc/new_user_bloc.dart index 4f695b1..f5c31f8 100644 --- a/lib/bloc/new_user_bloc.dart +++ b/lib/bloc/new_user_bloc.dart @@ -15,11 +15,12 @@ class NewUserBloc { _userRepository = GetIt.instance.get(); } - Future registerNewUser(NewUser newUser) async { + Future registerNewUser(NewUser newUser) async { try { - return await _userRepository.createUser(newUser); + await _userRepository.createUser(newUser); + return true; } catch (_) { - return Response('Failed to register user', 400); + return false; } } @@ -35,8 +36,7 @@ class NewUserBloc { try { await _userRepository.updateUserProfile(newUserData); return Response('Success', 200); - } catch (e) { - print(e); + } catch (_) { return Response('Failed to update user', 400); } } diff --git a/lib/model/new_user.dart b/lib/model/new_user.dart index 13301af..6e2c960 100644 --- a/lib/model/new_user.dart +++ b/lib/model/new_user.dart @@ -17,6 +17,8 @@ class NewUser { String phoneNumber; String foto; bool seen; + @JsonKey(name: 'organisasi_komunitas') + String organisasiKomunitas; NewUser({ this.name, @@ -30,6 +32,7 @@ class NewUser { this.phoneNumber, this.foto, this.seen, + this.organisasiKomunitas, }); factory NewUser.fromJson(Map json) => diff --git a/lib/model/new_user.g.dart b/lib/model/new_user.g.dart index c0e14ca..0899a76 100644 --- a/lib/model/new_user.g.dart +++ b/lib/model/new_user.g.dart @@ -19,6 +19,7 @@ NewUser _$NewUserFromJson(Map json) { phoneNumber: json['phone_number'] as String, foto: json['foto'] as String, seen: json['seen'] as bool, + organisasiKomunitas: json['organisasi_komunitas'] as String, ); } @@ -34,4 +35,5 @@ Map _$NewUserToJson(NewUser instance) => { 'phone_number': instance.phoneNumber, 'foto': instance.foto, 'seen': instance.seen, + 'organisasi_komunitas': instance.organisasiKomunitas, }; diff --git a/lib/model/user.dart b/lib/model/user.dart index 3fd8170..4840005 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -27,6 +27,8 @@ class UserModel { bool seen; @JsonKey(name: 'hidden_fields') List hiddenFields; + @JsonKey(name: 'organisasi_komunitas') + String organisasiKomunitas; UserModel({ this.is_login, @@ -42,6 +44,7 @@ class UserModel { this.token, this.foto, this.seen, + this.organisasiKomunitas, }); factory UserModel.fromJson(Map json) => diff --git a/lib/model/user.g.dart b/lib/model/user.g.dart index 34dc01b..d5be823 100644 --- a/lib/model/user.g.dart +++ b/lib/model/user.g.dart @@ -34,6 +34,7 @@ UserModel _$UserModelFromJson(Map json) { token: json['token'] as String, foto: json['foto'] as String, seen: json['seen'] as bool, + organisasiKomunitas: json['organisasi_komunitas'] as String, )..hiddenFields = (json['hidden_fields'] as List)?.map((e) => e as String)?.toList(); } @@ -53,4 +54,5 @@ Map _$UserModelToJson(UserModel instance) => { 'foto': instance.foto, 'seen': instance.seen, 'hidden_fields': instance.hiddenFields, + 'organisasi_komunitas': instance.organisasiKomunitas, }; diff --git a/lib/network/network_interface.dart b/lib/network/network_interface.dart index 4641b1d..6fe64a3 100644 --- a/lib/network/network_interface.dart +++ b/lib/network/network_interface.dart @@ -15,10 +15,12 @@ class NetworkInterface { }) async { var responseJson; try { - final sharedPreferences = await SharedPreferences.getInstance(); - dio.options.headers['Authorization'] = - 'Token ${sharedPreferences.getString('token')}'; - dio.options.headers['content-type'] = 'application/json'; + if (isLogin) { + final sharedPreferences = await SharedPreferences.getInstance(); + dio.options.headers['Authorization'] = + 'Token ${sharedPreferences.getString('token')}'; + dio.options.headers['content-type'] = 'application/json'; + } final response = await dio.post( '${ApiFlavor.getBaseUrl()}$url', data: formData ? FormData.fromMap(bodyParams) : json.encode(bodyParams), @@ -43,7 +45,7 @@ class NetworkInterface { if (isLogin) { final sharedPreferences = await SharedPreferences.getInstance(); dio.options.headers['Authorization'] = - 'Token ${sharedPreferences.getString('token')}'; + 'Token ${sharedPreferences.getString('token')}'; } dio.options.headers['content-type'] = 'application/json'; final response = await dio.put( diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index c79d026..2072fe9 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -459,9 +459,6 @@ class _DetailPostPageState extends State { newKomentarPostingData, _namaLokasi, widget.komentar.id); if (response['response'] == 'komentar added') { successDialog(context); - Timer(const Duration(seconds: 2), () { - Navigator.pop(context); - }); await _bloc.fetchKomentarPostingList(_namaLokasi, widget.komentar.id); komentarController.clear(); } else { diff --git a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart index d6e186a..5af0566 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart @@ -514,9 +514,6 @@ class _DetailPostKegiatanPageState extends State { if (response['response'] == 'komentar kegiatan added') { successDialog(context); - Timer(const Duration(seconds: 2), () { - Navigator.pop(context); - }); await _bloc.fetchKomentarPostingKegiatanList( _placeId, widget.kegiatan.id); // ganti widget.x.id // sudah komentarKegiatanController.clear(); diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index 1d4ca8b..f1804b0 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -7,7 +7,6 @@ import 'package:bisaGo/bloc/new_user_bloc.dart'; import 'package:bisaGo/component/bisago_appbar.dart'; import 'package:bisaGo/config/strings.dart'; import 'package:bisaGo/config/styles.dart'; -import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/profile/profile.dart'; import 'package:bisaGo/utils/custom_button.dart'; @@ -63,8 +62,11 @@ class _EditProfileState extends State { phoneController.text = user.phoneNumber; emailController.text = user.email; alamatController.text = user.alamat; + organisasiController.text = user.organisasiKomunitas; jenisKelaminValue = user.jenisKelamin; - tanggalLahir = user.tanggalLahir; + tanggalLahir = (user.tanggalLahir == '-' + ? '${DateFormat('yyyy-MM-dd').format(DateTime.now())}' + : user.tanggalLahir); jenisDisabilitasValue = user.disabilitas; pekerjaanValue = user.pekerjaan; _rahasiakanData = user.seen; @@ -299,6 +301,13 @@ class _EditProfileState extends State { }); }, ), + CustomTextField( + title: 'Organisasi / Komunitasi', + required: true, + key: const Key('Text Field Organisasi'), + controller: organisasiController, + validator: FieldValidator.validateOrganisasi, + ), Container( margin: const EdgeInsets.fromLTRB( 0, tripleSpace, 0, regularSpace), @@ -319,11 +328,13 @@ class _EditProfileState extends State { ); } - NewUser newUser; + UserModel newUser; TextEditingController nameController = TextEditingController(); TextEditingController phoneController = TextEditingController(); TextEditingController emailController = TextEditingController(); TextEditingController alamatController = TextEditingController(); + TextEditingController organisasiController = TextEditingController(); + String jenisKelaminValue; String tanggalLahir; String jenisDisabilitasValue; @@ -351,9 +362,8 @@ class _EditProfileState extends State { final form = _formKey.currentState; if (_formKey.currentState.validate()) { form.save(); - newUser = NewUser( + newUser = UserModel( name: nameController.text.toString(), - password: '', email: emailController.text.toString(), phoneNumber: phoneController.text.toString(), tanggalLahir: tanggalLahir, @@ -363,6 +373,7 @@ class _EditProfileState extends State { alamat: alamatController.text.toString(), foto: fotoValidate, seen: _rahasiakanData, + organisasiKomunitas: organisasiController.text, ); newUserData = newUser.toJson(); if (_image != null) { diff --git a/lib/page/profile/profile.dart b/lib/page/profile/profile.dart index 68ad2a3..a023513 100644 --- a/lib/page/profile/profile.dart +++ b/lib/page/profile/profile.dart @@ -304,6 +304,26 @@ class _ProfileState extends State { textAlign: TextAlign.left, ), ), + ListTile( + leading: const Icon( + Icons.people, + size: 28, + color: darkGreen, + ), + title: Text( + user.organisasiKomunitas == '' || + user.organisasiKomunitas == null + ? '-' + : user.organisasiKomunitas, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Colors.black, + fontFamily: 'Comfortaa', + ), + textAlign: TextAlign.left, + ), + ), ], ), ], diff --git a/lib/page/registrasi/registrasi.dart b/lib/page/registrasi/registrasi.dart index 7c64eca..87f211e 100644 --- a/lib/page/registrasi/registrasi.dart +++ b/lib/page/registrasi/registrasi.dart @@ -197,6 +197,11 @@ class RegistrasiState extends State { }); }, ), + CustomTextField( + title: 'Organisasi / Komunitasi', + key: const Key('Text Field Organisasi'), + controller: organisasiController, + ), CustomTextField( title: 'Password', required: true, @@ -258,17 +263,20 @@ class RegistrasiState extends State { if (_validateTanggalLahir(context) && _formKey.currentState.validate()) { form.save(); newUser = NewUser( - name: nameController.text.toString(), - password: passwordController.text.toString(), - email: emailController.text.toString(), - phoneNumber: phoneController.text.toString(), - tanggalLahir: tanggalLahir, - jenisKelamin: _jenisKelaminValue ?? '-', - disabilitas: jenisDisabilitas, - pekerjaan: _pekerjaanValue ?? '-', - alamat: alamatController.text.toString() == '' - ? '-' - : alamatController.text.toString()); + name: nameController.text.toString(), + password: passwordController.text.toString(), + email: emailController.text.toString(), + phoneNumber: phoneController.text.toString(), + tanggalLahir: tanggalLahir, + jenisKelamin: _jenisKelaminValue ?? '-', + disabilitas: jenisDisabilitas, + pekerjaan: _pekerjaanValue ?? '-', + alamat: alamatController.text.toString() == '' + ? '-' + : alamatController.text.toString(), + organisasiKomunitas: + organisasiController.text == '' ? '-' : organisasiController.text, + ); await createUser(newUser); } } @@ -294,6 +302,8 @@ class RegistrasiState extends State { TextEditingController emailController = TextEditingController(); TextEditingController alamatController = TextEditingController(); TextEditingController passwordController = TextEditingController(); + TextEditingController organisasiController = TextEditingController(); + String _jenisKelaminValue; String _pekerjaanValue; String jenisDisabilitas = '-'; @@ -302,11 +312,10 @@ class RegistrasiState extends State { Future createUser(NewUser newUser) async { bloc = NewUserBloc(); final response = await bloc.registerNewUser(newUser); - if (response.statusCode == 201) { + if (response) { successDialog(context); - Timer(const Duration(seconds: 2), () { - _navigateToDashboard(context); - }); + await Future.delayed(Duration(seconds: 2)); + _navigateToDashboard(context); } else { failedDialog(context); } diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart index 3dadee3..d47c809 100644 --- a/lib/repository/user_repository.dart +++ b/lib/repository/user_repository.dart @@ -6,7 +6,7 @@ import 'package:http/http.dart'; abstract class BaseUserRepository { Future fetchUserDetail(String email); - Future createUser(NewUser newUser); + Future createUser(NewUser newUser); Future updateUser(NewUser newUser); Future updateUserProfile(Map newUserData); } @@ -24,10 +24,11 @@ class UserRepository implements BaseUserRepository { } @override - Future createUser(NewUser newUser) async { - final response = await post( - '${ApiFlavor.getBaseUrl()}/api/register/', - body: newUser.toJson(), + Future createUser(NewUser newUser) async { + final response = await _network.post( + url: '/api/user/register/', + isLogin: false, + bodyParams: newUser.toJson(), ); return response; } diff --git a/lib/utils/validator.dart b/lib/utils/validator.dart index b83b22c..e1d845c 100644 --- a/lib/utils/validator.dart +++ b/lib/utils/validator.dart @@ -34,6 +34,14 @@ class FieldValidator { } } + static String validateOrganisasi(String value) { + if (value.isEmpty) { + return '*Wajib diisi'; + } else { + return null; + } + } + static String validateEmail(String value) { const pattern = r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'; diff --git a/test/model_test.dart b/test/model_test.dart index aabb467..9261956 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -33,6 +33,8 @@ void main() { 'token': null, 'seen': true, 'foto': '', + 'hidden_fields': [], + 'organisasi_komunitas': 'Organisasi', }; final komentarData = { 'id': 1, @@ -74,6 +76,7 @@ void main() { 'disabilitas': 'Tidak Memiliki Disabilitas', 'pekerjaan': 'Mahasiswa', 'alamat': 'Tidak Tahu', + 'hidden_fields': [], }; test('Komentar Posting fromJson and toJson', () { diff --git a/test/profile_test.dart b/test/profile_test.dart index bb2699f..bf6512a 100644 --- a/test/profile_test.dart +++ b/test/profile_test.dart @@ -23,6 +23,7 @@ class MockUserRepository extends Fake implements UserRepository { 'disabilitas': 'Tidak memiliki disabilitas', 'pekerjaan': 'Pelajar', 'alamat': 'Tidak Tahu', + 'hidden_fields': [], 'seen': true, }; @@ -169,6 +170,7 @@ void main() { 'pekerjaan': 'Pelajar', 'alamat': 'Tidak Tahu', 'seen': true, + 'hidden_fields': [], }; final userData2 = { @@ -181,6 +183,7 @@ void main() { 'pekerjaan': 'Pelajar', 'alamat': '', 'seen': true, + 'hidden_fields': [], }; testWidgets('Edit Profile Page Widget Test -- Positive', @@ -366,6 +369,7 @@ void main() { final namaKey = find.byKey(Key('Text Field Nama')); final nomorTeleponKey = find.byKey(Key('Text Field Nomor Telepon')); final alamatKey = find.byKey(Key('Text Field Alamat')); + final organisasiKey = find.byKey(Key('Text Field Organisasi')); await tester.pumpWidget( MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); @@ -374,6 +378,7 @@ void main() { await tester.enterText(namaKey, 'Ardian'); await tester.enterText(nomorTeleponKey, '085811111111'); await tester.enterText(alamatKey, ' Bekasi'); + await tester.enterText(organisasiKey, 'Olahraga'); await tester .ensureVisible(find.byKey(Key('Button Simpan'), skipOffstage: false)); @@ -388,6 +393,7 @@ void main() { final namaKey = find.byKey(Key('Text Field Nama')); final nomorTeleponKey = find.byKey(Key('Text Field Nomor Telepon')); final alamatKey = find.byKey(Key('Text Field Alamat')); + final organisasiKey = find.byKey(Key('Text Field Organisasi')); await tester.pumpWidget( MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); @@ -396,13 +402,14 @@ void main() { await tester.enterText(namaKey, ''); await tester.enterText(nomorTeleponKey, ''); await tester.enterText(alamatKey, ''); + await tester.enterText(organisasiKey, ''); await tester .ensureVisible(find.byKey(Key('Button Simpan'), skipOffstage: false)); await tester.pumpAndSettle(); await tester.tap(find.byKey(Key('Button Simpan'))); await tester.pumpAndSettle(); - expect(find.text('*Wajib diisi'), findsNWidgets(3)); + expect(find.text('*Wajib diisi'), findsNWidgets(4)); }); testWidgets('Change Profile Picture Title -- Positive', diff --git a/test/registrasi_test.dart b/test/registrasi_test.dart index 9fed8a6..307118a 100644 --- a/test/registrasi_test.dart +++ b/test/registrasi_test.dart @@ -20,7 +20,8 @@ class MockUserRepository extends Fake implements UserRepository { void main() { setUpAll(() { final _getIt = GetIt.instance; - _getIt.registerLazySingleton(() => MockUserRepository()); + _getIt + .registerLazySingleton(() => MockUserRepository()); }); testWidgets('Find Name Text Field', (WidgetTester tester) async { final textFieldKey = Key('Text Field Nama'); @@ -96,7 +97,7 @@ void main() { final pekerjaanKey = find.byKey(Key('Dropdown Pekerjaan')); final password = find.byKey(Key('Text Field Password')); final konfirmasiPassword = - find.byKey(Key('Text Field Konfirmasi Password')); + find.byKey(Key('Text Field Konfirmasi Password')); await tester.enterText(nama, 'nama'); await tester.enterText(noTelp, '08123456789'); @@ -137,8 +138,6 @@ void main() { await tester .ensureVisible(find.byKey(daftarButtonKey, skipOffstage: false)); - await tester.pumpAndSettle(const Duration(seconds: 1)); - await tester.tap(find.byKey(daftarButtonKey)); await tester.pumpAndSettle(); }); diff --git a/test/user_test.dart b/test/user_test.dart index 3078282..14df3fc 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -16,6 +16,8 @@ void main() { 'token': null, 'seen': true, 'foto': '', + 'hidden_fields': [], + 'organisasi_komunitas': 'Organisasi', }; test('User Model', () { -- GitLab From cd23fb24c444c776ba0aa17fdc22a84953365a89 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Mon, 31 May 2021 14:49:32 +0700 Subject: [PATCH 25/45] [CHORES] Fix pilih disabilitas --- lib/bloc/new_user_bloc.dart | 7 ++-- lib/page/login/login.dart | 24 +++++++------- lib/page/login/pilih_disabilitas.dart | 7 ++-- lib/repository/user_repository.dart | 13 ++++---- ...si_informasi_layanan_disabilitas_test.dart | 32 +++++++++---------- 5 files changed, 41 insertions(+), 42 deletions(-) diff --git a/lib/bloc/new_user_bloc.dart b/lib/bloc/new_user_bloc.dart index f5c31f8..c030b70 100644 --- a/lib/bloc/new_user_bloc.dart +++ b/lib/bloc/new_user_bloc.dart @@ -24,11 +24,12 @@ class NewUserBloc { } } - Future updateUser(NewUser newUser) async { + Future updateUser(NewUser newUser) async { try { - return await _userRepository.updateUser(newUser); + await _userRepository.updateUser(newUser); + return true; } catch (_) { - return Response('Failed to update user', 400); + return false; } } diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart index 5749677..f646e4f 100644 --- a/lib/page/login/login.dart +++ b/lib/page/login/login.dart @@ -159,12 +159,12 @@ class LoginState extends State { height: 64.0, child: TextButton( style: TextButton.styleFrom( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0), - side: const BorderSide(color: greenPrimary)), - padding: const EdgeInsets.symmetric(vertical: 10.0) - ), + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + side: const BorderSide(color: greenPrimary)), + padding: + const EdgeInsets.symmetric(vertical: 10.0)), onPressed: () async { Navigator.of(context).pop(true); await _updateUser(newUser); @@ -189,11 +189,11 @@ class LoginState extends State { height: 64.0, child: TextButton( style: TextButton.styleFrom( - backgroundColor: greenPrimary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0)), - padding: const EdgeInsets.symmetric(vertical: 10.0) - ), + backgroundColor: greenPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0)), + padding: + const EdgeInsets.symmetric(vertical: 10.0)), onPressed: () { Navigator.of(context).pop(true); _navigateToPilihDisabilitas(context); @@ -246,7 +246,7 @@ class LoginState extends State { Future _updateUser(NewUser newUser) async { _newUserBloc = NewUserBloc(); final response = await _newUserBloc.updateUser(newUser); - if (response.statusCode == 200) { + if (response) { successUserUpdateDialog(context); } else { failedUserUpdateDialog(context); diff --git a/lib/page/login/pilih_disabilitas.dart b/lib/page/login/pilih_disabilitas.dart index 758a125..ac7c034 100644 --- a/lib/page/login/pilih_disabilitas.dart +++ b/lib/page/login/pilih_disabilitas.dart @@ -119,11 +119,10 @@ class _PilihDisabilitasState extends State { newUser.disabilitas = _disabilitas; bloc = NewUserBloc(); final response = await bloc.updateUser(newUser); - if (response.statusCode == 200) { + if (response) { successDialog(context); - Timer(const Duration(seconds: 2), () { - _navigateToDashboard(context); - }); + await Future.delayed(Duration(seconds: 2)); + _navigateToDashboard(context); } else { failedDialog(context); } diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart index d47c809..3126c97 100644 --- a/lib/repository/user_repository.dart +++ b/lib/repository/user_repository.dart @@ -1,13 +1,11 @@ -import 'package:bisaGo/flavor/flavor.dart'; import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/network/network_interface.dart'; -import 'package:http/http.dart'; abstract class BaseUserRepository { Future fetchUserDetail(String email); Future createUser(NewUser newUser); - Future updateUser(NewUser newUser); + Future updateUser(NewUser newUser); Future updateUserProfile(Map newUserData); } @@ -47,10 +45,11 @@ class UserRepository implements BaseUserRepository { } @override - Future updateUser(NewUser newUser) async { - final response = await post( - '${ApiFlavor.getBaseUrl()}/api/update-user/', - body: newUser.toJson(), + Future updateUser(NewUser newUser) async { + final response = await _network.put( + url: '/api/user/${newUser.email}/', + bodyParams: newUser.toJson(), + isLogin: true, ); return response; } diff --git a/test/registrasi_informasi_layanan_disabilitas_test.dart b/test/registrasi_informasi_layanan_disabilitas_test.dart index c84ca84..e2daf2c 100644 --- a/test/registrasi_informasi_layanan_disabilitas_test.dart +++ b/test/registrasi_informasi_layanan_disabilitas_test.dart @@ -43,21 +43,21 @@ void main() { await tester.tap(find.byKey(backButtonKey)); }); - testWidgets('create penyandang method test', (WidgetTester tester) async { - final registrasiPage = RegistrasiInformasiLayananDisabilitas(); - final registrasiState = registrasiPage.createState(); - var textEditingController = TextEditingController(); - textEditingController.text = 'This is a test'; - registrasiState.namaPenyandangController = textEditingController; - registrasiState.alamatController = textEditingController; - registrasiState.ttlController = textEditingController; - registrasiState.tanggalLahir = '2000-01-01'; - registrasiState.phoneController = textEditingController; - registrasiState.emailController = textEditingController; - registrasiState.namaWaliController = textEditingController; - registrasiState.alamatWaliController = textEditingController; - registrasiState.phoneNumbOrtuController = textEditingController; + // testWidgets('create penyandang method test', (WidgetTester tester) async { + // final registrasiPage = RegistrasiInformasiLayananDisabilitas(); + // final registrasiState = registrasiPage.createState(); + // var textEditingController = TextEditingController(); + // textEditingController.text = 'This is a test'; + // registrasiState.namaPenyandangController = textEditingController; + // registrasiState.alamatController = textEditingController; + // registrasiState.ttlController = textEditingController; + // registrasiState.tanggalLahir = '2000-01-01'; + // registrasiState.phoneController = textEditingController; + // registrasiState.emailController = textEditingController; + // registrasiState.namaWaliController = textEditingController; + // registrasiState.alamatWaliController = textEditingController; + // registrasiState.phoneNumbOrtuController = textEditingController; - registrasiState.createPenyandang(); - }); + // registrasiState.createPenyandang(); + // }); } -- GitLab From f7061b7b92196782d2758d8e58e31165fb714ff8 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Mon, 31 May 2021 15:12:09 +0700 Subject: [PATCH 26/45] [CHORES] Trigger request fcm token on login --- lib/page/login/login.dart | 46 ++++++++++++++++++++++------- lib/repository/user_repository.dart | 3 +- test/login_test.dart | 12 ++++++++ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart index 5749677..fcd2dfd 100644 --- a/lib/page/login/login.dart +++ b/lib/page/login/login.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:bisaGo/bloc/cloud_messaging_bloc.dart'; import 'package:bisaGo/bloc/new_user_bloc.dart'; import 'package:bisaGo/bloc/user_bloc.dart'; import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/page/dashboard/dashboard.dart'; import 'package:bisaGo/page/login/pilih_disabilitas.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -35,6 +38,10 @@ class LoginState extends State { GoogleSignInAccount _currentUser; SharedPreferences sharedPreferences; + FirebaseMessaging _firebaseMessaging; + + CloudMessagingBloc cloudMessagingBloc = CloudMessagingBloc(); + @override void initState() { super.initState(); @@ -159,12 +166,12 @@ class LoginState extends State { height: 64.0, child: TextButton( style: TextButton.styleFrom( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0), - side: const BorderSide(color: greenPrimary)), - padding: const EdgeInsets.symmetric(vertical: 10.0) - ), + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + side: const BorderSide(color: greenPrimary)), + padding: + const EdgeInsets.symmetric(vertical: 10.0)), onPressed: () async { Navigator.of(context).pop(true); await _updateUser(newUser); @@ -189,11 +196,11 @@ class LoginState extends State { height: 64.0, child: TextButton( style: TextButton.styleFrom( - backgroundColor: greenPrimary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0)), - padding: const EdgeInsets.symmetric(vertical: 10.0) - ), + backgroundColor: greenPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0)), + padding: + const EdgeInsets.symmetric(vertical: 10.0)), onPressed: () { Navigator.of(context).pop(true); _navigateToPilihDisabilitas(context); @@ -286,12 +293,15 @@ class LoginState extends State { final response = await http .post('${ApiFlavor.getBaseUrl()}/api-token-auth/', body: data); if (response.statusCode == 200) { + print('here'); final tokenMap = jsonDecode(response.body); setState(() { sharedPreferences ..setString('token', tokenMap['token']) ..setString('email', email); }); + await _requestFCMToken(); + successDialog(context); _navigateToDashboard(context); } else { @@ -372,6 +382,7 @@ class LoginState extends State { await googleSignInAccount.authentication; final token = googleSignInAuthentication.accessToken; + print(token); await login( _currentUser.email, '', 'true', token, _currentUser.displayName); sharedPreferences = await SharedPreferences.getInstance(); @@ -384,6 +395,19 @@ class LoginState extends State { } } + Future _requestFCMToken() async { + await Firebase.initializeApp(); + + _firebaseMessaging = FirebaseMessaging.instance; + + final fcmToken = await _firebaseMessaging.getToken(); + final sharedPreferences = await SharedPreferences.getInstance(); + final token = sharedPreferences.getString('token'); + if (token != null) { + await cloudMessagingBloc.sendFCMToken(fcmToken, token); + } + } + Future _handleSignOut() async { await _googleSignIn.signOut(); } diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart index 4ced6b3..dd7caf0 100644 --- a/lib/repository/user_repository.dart +++ b/lib/repository/user_repository.dart @@ -9,13 +9,14 @@ abstract class BaseUserRepository { Future createUser(NewUser newUser); Future updateUser(NewUser newUser); } + class UserRepository implements BaseUserRepository { final NetworkInterface _network = NetworkInterface(); @override Future fetchUserDetail(String email) async { final response = - await _network.get(url: '/api/user-detail/$email', isLogin: true); + await _network.get(url: '/api/user/$email/', isLogin: true); final data = [response]; return User( data.map((user) => UserModel.fromJson(user)).toList()); diff --git a/test/login_test.dart b/test/login_test.dart index dbea2e8..2573a52 100644 --- a/test/login_test.dart +++ b/test/login_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bisaGo/model/user.dart'; +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/user_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,6 +14,14 @@ import 'package:shared_preferences/shared_preferences.dart'; class MockNetwork extends Mock implements NetworkInterface {} +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + class MockUserRepository extends Fake implements UserRepository { final userData = { 'is_login': true, @@ -41,6 +50,9 @@ void main() { .registerLazySingleton(() => MockUserRepository()); SharedPreferences.setMockInitialValues( {'email': 'test@gmail.com', 'token': 'token'}); + + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); // mockNetwork = MockNetwork(); // when(mockNetwork.get(isLogin: false, url: anyNamed('url'))) // .thenAnswer((_) async { -- GitLab From bcb608274d50de21ccb653f883e6b43451148ae8 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Mon, 31 May 2021 15:44:29 +0700 Subject: [PATCH 27/45] [CHORES] Change date format --- lib/config/custom_serializer.dart | 2 +- lib/page/filter_fasilitas/postingan/detail_post.dart | 2 +- lib/page/login/login.dart | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/config/custom_serializer.dart b/lib/config/custom_serializer.dart index 00ed551..3f2f901 100644 --- a/lib/config/custom_serializer.dart +++ b/lib/config/custom_serializer.dart @@ -2,6 +2,6 @@ import 'package:intl/intl.dart'; class CustomSerializer { static DateTime stringToDateTime(String date) { - return DateFormat('yyyy-MM-dd hh:mm').parse(date); + return DateFormat('dd-MM-yyyy hh:mm:ss').parse(date); } } diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index e17b8e5..c5dac12 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -482,7 +482,7 @@ class _DetailPostPageState extends State { name, style: const TextStyle(fontSize: 18), ), - Text('${DateFormat('dd MMMM yyy hh:mm').format(date)}', + Text('${DateFormat('dd MMMM yyyy hh:mm').format(date)}', style: const TextStyle(color: grayPrimary, fontSize: 14)) ], ), diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart index fcd2dfd..67aaa2e 100644 --- a/lib/page/login/login.dart +++ b/lib/page/login/login.dart @@ -293,7 +293,6 @@ class LoginState extends State { final response = await http .post('${ApiFlavor.getBaseUrl()}/api-token-auth/', body: data); if (response.statusCode == 200) { - print('here'); final tokenMap = jsonDecode(response.body); setState(() { sharedPreferences @@ -382,7 +381,6 @@ class LoginState extends State { await googleSignInAccount.authentication; final token = googleSignInAuthentication.accessToken; - print(token); await login( _currentUser.email, '', 'true', token, _currentUser.displayName); sharedPreferences = await SharedPreferences.getInstance(); -- GitLab From 3f4ddbbd075d937464f903b0e88bf9ed131bb3eb Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Mon, 31 May 2021 16:26:56 +0700 Subject: [PATCH 28/45] [CHORES] Change dropdown banner to flushbar --- lib/app.dart | 8 +----- lib/page/dashboard/dashboard.dart | 41 ++++++++++++++++--------------- pubspec.yaml | 2 +- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index c572bb9..11dd997 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,4 +1,3 @@ -import 'package:dropdown_banner/dropdown_banner.dart'; import 'package:flutter/material.dart'; import 'package:bisaGo/config/styles.dart'; import 'package:bisaGo/page/dashboard/dashboard.dart'; @@ -8,8 +7,6 @@ class BisaGo extends StatelessWidget { @override Widget build(BuildContext context) { - final navigatorKey = GlobalKey(); - return MaterialApp( title: 'bisaGo', theme: ThemeData( @@ -17,10 +14,7 @@ class BisaGo extends StatelessWidget { primaryColor: greenPrimary, backgroundColor: Colors.white, ), - home: DropdownBanner( - navigatorKey: navigatorKey, - child: Dashboard(), - ), + home: Dashboard(), ); } } diff --git a/lib/page/dashboard/dashboard.dart b/lib/page/dashboard/dashboard.dart index e1daa0f..4376297 100644 --- a/lib/page/dashboard/dashboard.dart +++ b/lib/page/dashboard/dashboard.dart @@ -12,10 +12,10 @@ import 'package:bisaGo/page/filter_fasilitas/postingan/detail_post.dart'; import 'package:bisaGo/repository/komentar_repository.dart'; import 'package:bisaGo/utils/custom_dashboard_location_button.dart'; import 'package:bisaGo/utils/location_turn_on_dialog.dart'; -import 'package:dropdown_banner/dropdown_banner.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:geolocator/geolocator.dart'; @@ -79,25 +79,26 @@ class DashboardState extends State { _lastNotification = now; final data = message.data; final String msg = data['message']; - DropdownBanner.showBanner( - text: msg, - color: Color(0xFF003566), - textStyle: TextStyle( - color: Colors.white, - height: 2.1, - ), - duration: Duration(seconds: 8), - tapCallback: () { - if (data['type'] == 'fasilitas') { - final String placeId = data['place_id']; - final id = int.parse(data['id']); - _navigateToDetailFasilitasPage(context, placeId, id); - } else if (data['type'] == 'kegiatan') { - final String placeId = data['place_id']; - final id = int.parse(data['id']); - _navigateToDetailKegiatanPage(context, placeId, id); - } - }); + final String title = data['title']; + Flushbar( + title: title, + message: '"$msg"', + duration: Duration(seconds: 8), + backgroundColor: Color(0xFF003566), + onTap: (_) { + if (data['type'] == 'fasilitas') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailFasilitasPage(context, placeId, id); + } else if (data['type'] == 'kegiatan') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailKegiatanPage(context, placeId, id); + } + }, + flushbarStyle: FlushbarStyle.GROUNDED, + flushbarPosition: FlushbarPosition.TOP, + ).show(context); }); FirebaseMessaging.onMessageOpenedApp.listen((message) { diff --git a/pubspec.yaml b/pubspec.yaml index c7a2c05..14d76cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,7 +57,7 @@ dependencies: firebase_dynamic_links: ^0.7.0+1 firebase_messaging: ^8.0.0-dev.15 carousel_slider: ^3.0.0 - dropdown_banner: ^1.4.0 + flushbar: ^1.10.4 dev_dependencies: flutter_test: -- GitLab From 1bc264eee520e1634cca0508ae4732222a92c326 Mon Sep 17 00:00:00 2001 From: Yoga Pratama Date: Mon, 31 May 2021 17:40:48 +0700 Subject: [PATCH 29/45] [CHORES] Change date_time field to created --- lib/config/custom_serializer.dart | 2 +- lib/model/komentar_posting.dart | 11 ++++++++--- lib/model/komentar_posting.g.dart | 4 ++-- lib/model/komentar_posting_kegiatan.dart | 13 +++++++------ lib/model/komentar_posting_kegiatan.g.dart | 2 +- lib/page/dashboard/dashboard.dart | 2 +- .../filter_fasilitas/postingan/detail_post.dart | 2 +- lib/repository/kegiatan_repository.dart | 8 +++++--- test/model_test.dart | 8 ++++---- 9 files changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/config/custom_serializer.dart b/lib/config/custom_serializer.dart index 3f2f901..458c5a4 100644 --- a/lib/config/custom_serializer.dart +++ b/lib/config/custom_serializer.dart @@ -2,6 +2,6 @@ import 'package:intl/intl.dart'; class CustomSerializer { static DateTime stringToDateTime(String date) { - return DateFormat('dd-MM-yyyy hh:mm:ss').parse(date); + return DateFormat('dd-MM-yyyy hh:mm').parse(date); } } diff --git a/lib/model/komentar_posting.dart b/lib/model/komentar_posting.dart index 04ffea6..465d069 100644 --- a/lib/model/komentar_posting.dart +++ b/lib/model/komentar_posting.dart @@ -14,10 +14,15 @@ class KomentarPostingModel { final int id; final String deskripsi; final String creator; - @JsonKey(name: 'date_time', fromJson: CustomSerializer.stringToDateTime) - final DateTime dateTime; + @JsonKey(fromJson: CustomSerializer.stringToDateTime) + final DateTime created; - KomentarPostingModel({this.id, this.deskripsi, this.creator, this.dateTime}); + KomentarPostingModel({ + this.id, + this.deskripsi, + this.creator, + this.created, + }); factory KomentarPostingModel.fromJson(Map json) => _$KomentarPostingModelFromJson(json); diff --git a/lib/model/komentar_posting.g.dart b/lib/model/komentar_posting.g.dart index 565bcd3..0670572 100644 --- a/lib/model/komentar_posting.g.dart +++ b/lib/model/komentar_posting.g.dart @@ -27,7 +27,7 @@ KomentarPostingModel _$KomentarPostingModelFromJson(Map json) { id: json['id'] as int, deskripsi: json['deskripsi'] as String, creator: json['creator'] as String, - dateTime: CustomSerializer.stringToDateTime(json['date_time'] as String), + created: CustomSerializer.stringToDateTime(json['created'] as String), ); } @@ -37,5 +37,5 @@ Map _$KomentarPostingModelToJson( 'id': instance.id, 'deskripsi': instance.deskripsi, 'creator': instance.creator, - 'date_time': instance.dateTime?.toIso8601String(), + 'created': instance.created?.toIso8601String(), }; diff --git a/lib/model/komentar_posting_kegiatan.dart b/lib/model/komentar_posting_kegiatan.dart index 78e18d7..1269bd4 100644 --- a/lib/model/komentar_posting_kegiatan.dart +++ b/lib/model/komentar_posting_kegiatan.dart @@ -1,3 +1,4 @@ +import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:bisaGo/config/custom_serializer.dart'; @@ -14,18 +15,18 @@ class KomentarPostingKegiatanModel { final int id; final String creator; final String deskripsi; - @JsonKey(name: 'created', fromJson: CustomSerializer.stringToDateTime) + @JsonKey(name: 'created', fromJson: _stringToDateTime) final DateTime created; KomentarPostingKegiatanModel( - {this.id, - this.creator, - this.deskripsi, - this.created} - ); + {this.id, this.creator, this.deskripsi, this.created}); factory KomentarPostingKegiatanModel.fromJson(Map json) => _$KomentarPostingKegiatanModelFromJson(json); Map toJson() => _$KomentarPostingKegiatanModelToJson(this); } + +DateTime _stringToDateTime(String date) { + return DateFormat('yyy-MM-dd hh:mm').parse(date); +} diff --git a/lib/model/komentar_posting_kegiatan.g.dart b/lib/model/komentar_posting_kegiatan.g.dart index 3b73f1a..4959621 100644 --- a/lib/model/komentar_posting_kegiatan.g.dart +++ b/lib/model/komentar_posting_kegiatan.g.dart @@ -29,7 +29,7 @@ KomentarPostingKegiatanModel _$KomentarPostingKegiatanModelFromJson( id: json['id'] as int, creator: json['creator'] as String, deskripsi: json['deskripsi'] as String, - created: CustomSerializer.stringToDateTime(json['created'] as String), + created: _stringToDateTime(json['created'] as String), ); } diff --git a/lib/page/dashboard/dashboard.dart b/lib/page/dashboard/dashboard.dart index 4376297..f6915b4 100644 --- a/lib/page/dashboard/dashboard.dart +++ b/lib/page/dashboard/dashboard.dart @@ -82,7 +82,7 @@ class DashboardState extends State { final String title = data['title']; Flushbar( title: title, - message: '"$msg"', + message: msg, duration: Duration(seconds: 8), backgroundColor: Color(0xFF003566), onTap: (_) { diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index c5dac12..c8abdb5 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -296,7 +296,7 @@ class _DetailPostPageState extends State { return Column( children: allKomentarPostingFromApi .map((k) => komentarPlaceHolder( - k.creator, k.dateTime, k.deskripsi)) + k.creator, k.created, k.deskripsi)) .toList()); } break; diff --git a/lib/repository/kegiatan_repository.dart b/lib/repository/kegiatan_repository.dart index e8fdb53..b1f969a 100644 --- a/lib/repository/kegiatan_repository.dart +++ b/lib/repository/kegiatan_repository.dart @@ -55,10 +55,12 @@ class KegiatanRepository implements BaseKegiatanRepository { ); return response; } - + @override - Future fetchDetailKegiatan(String placeId, int kegiatanId) async { - final url = '/informasi-fasilitas/lokasi/detail-kegiatan/$placeId/$kegiatanId/'; + Future fetchDetailKegiatan( + String placeId, int kegiatanId) async { + final url = + '/informasi-fasilitas/lokasi/detail-kegiatan/$placeId/$kegiatanId/'; final response = await _network.get(url: url, isLogin: false); var kegiatan = KegiatanModel.fromJson(response); kegiatan.image = await fetchImages(kegiatan.placeId, kegiatan.id); diff --git a/test/model_test.dart b/test/model_test.dart index 9978873..cece1d5 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -9,13 +9,13 @@ void main() { 'id': 1, 'deskripsi': 'This is a test', 'creator': 'Test', - 'date_time': '2020-11-18 00:13:52.939668' + 'created': '18-11-2020 00:13:52' }; final returnKomentarPostingData = { 'id': 1, 'deskripsi': 'This is a test', 'creator': 'Test', - 'date_time': '2020-11-18T00:13:00.000' + 'created': '2020-11-18T00:13:00.000' }; final userData = { 'is_login': true, @@ -36,7 +36,7 @@ void main() { 'deskripsi': 'Ada toilet khusus disabilitas terletak di lantai 2 dekat kintan', 'creator': '', - 'date_time': '2020-11-18 00:13:52.939668', + 'date_time': '18-11-2020 00:13:52', 'rating': 3, 'tag': 'KR', 'disabilitas': ['DF'], @@ -77,7 +77,7 @@ void main() { id: 2, deskripsi: 'This is a test', creator: 'Test', - dateTime: DateTime.now()); + created: DateTime.now()); expect(komentarPostingModel, isInstanceOf()); expect( komentarPostingWithConstructor, isInstanceOf()); -- GitLab From 210142da1e678a23cbfba6ffd18a6515cba7d427 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Mon, 31 May 2021 20:57:38 +0700 Subject: [PATCH 30/45] [CHORES] fix linter and add changelogs 3.4.0 --- .../android/id/changelogs/changelogs.txt | 22 +++++++++---------- lib/model/komentar_posting_kegiatan.dart | 1 - 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/android/fastlane/metadata/android/id/changelogs/changelogs.txt b/android/fastlane/metadata/android/id/changelogs/changelogs.txt index 659bd95..6264239 100644 --- a/android/fastlane/metadata/android/id/changelogs/changelogs.txt +++ b/android/fastlane/metadata/android/id/changelogs/changelogs.txt @@ -1,20 +1,20 @@ +3.4.0: +- Notifikasi pengguna untuk komentar baru + 3.3.0: -- New feature for add kegiatan -- Fix bugs +- Kegiatan disabilitas di suatu lokasi +- Perbaikan bugs 3.2.1: -- Fix cant update fasilitas +- Perbaikan masalah update fasilitas 3.2.0: -- New feature share fasilitas as a link +- Tersedia fitur membagikan informasi fasilitas/kegiatan disabilitas kepada orang lain 3.1.2: -- Fix google oauth +- Perbaikan masuk dengan Google 3.1.1: -- Add dialog to turn on Location services upon starting the app -- New feature for layanan search -- Add feature to upload image for a fasilitas straight from the camera -- Add default image for facilities -- Add search history feature -- Now all fields are required in edit profile +- Tersedia fitur pencarian layanan +- Tersedia pilihan kamera ketika menambahkan gambar fasilitas +- Tersedia fitur riwayat pencarian diff --git a/lib/model/komentar_posting_kegiatan.dart b/lib/model/komentar_posting_kegiatan.dart index 1269bd4..4f668c1 100644 --- a/lib/model/komentar_posting_kegiatan.dart +++ b/lib/model/komentar_posting_kegiatan.dart @@ -1,6 +1,5 @@ import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:bisaGo/config/custom_serializer.dart'; part 'komentar_posting_kegiatan.g.dart'; -- GitLab From b81f13b971a81a87adbb5c610e1618570fbabe65 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Tue, 1 Jun 2021 00:46:11 +0700 Subject: [PATCH 31/45] Merge branch 'development' into 'PBI-10-edit_profile' --- android/app/build.gradle | 4 +- .../main/res/mipmap-hdpi/transparent_icon.png | Bin 0 -> 20706 bytes .../main/res/mipmap-mdpi/transparent_icon.png | Bin 0 -> 20706 bytes .../res/mipmap-xhdpi/transparent_icon.png | Bin 0 -> 20706 bytes .../res/mipmap-xxhdpi/transparent_icon.png | Bin 0 -> 20706 bytes .../res/mipmap-xxxhdpi/transparent_icon.png | Bin 0 -> 20706 bytes android/app/src/main/res/values/colors.xml | 1 + .../android/id/changelogs/changelogs.txt | 22 +- lib/bloc/cloud_messaging_bloc.dart | 26 + lib/config/custom_serializer.dart | 2 +- lib/get_it.dart | 7 +- lib/main.dart | 15 +- lib/main_dev.dart | 11 + lib/main_staging.dart | 15 +- lib/model/kegiatan.dart | 2 +- lib/model/kegiatan.g.dart | 4 +- lib/model/komentar_posting.dart | 6 +- lib/model/komentar_posting.g.dart | 4 +- lib/model/komentar_posting_kegiatan.dart | 10 +- lib/model/komentar_posting_kegiatan.g.dart | 6 +- lib/page/dashboard/dashboard.dart | 107 ++- lib/page/filter_fasilitas/kegiatan.dart | 2 +- lib/page/filter_fasilitas/komentar.dart | 2 +- .../postingan/detail_post.dart | 679 ++++++++------- .../postingan/detail_post_kegiatan.dart | 796 +++++++++--------- lib/page/login/login.dart | 22 + lib/page/profile/edit_profile.dart | 57 +- .../cloud_messaging_repository.dart | 30 + lib/repository/kegiatan_repository.dart | 8 +- pubspec.yaml | 2 + test/cloud_messaging_test.dart | 26 + .../custom_kegiatan_terdekat_button_test.dart | 11 + test/login_test.dart | 12 + test/mock_test.dart | 11 + test/model_test.dart | 13 +- test/widget_test.dart | 11 + 36 files changed, 1089 insertions(+), 835 deletions(-) create mode 100644 android/app/src/main/res/mipmap-hdpi/transparent_icon.png create mode 100644 android/app/src/main/res/mipmap-mdpi/transparent_icon.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/transparent_icon.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/transparent_icon.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/transparent_icon.png create mode 100644 lib/bloc/cloud_messaging_bloc.dart create mode 100644 lib/repository/cloud_messaging_repository.dart create mode 100644 test/cloud_messaging_test.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 949d735..13a3598 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -83,7 +83,9 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - implementation 'com.google.firebase:firebase-analytics:17.2.2' + implementation 'com.google.firebase:firebase-analytics:' + implementation 'com.google.firebase:firebase-messaging:' + implementation 'com.google.firebase:firebase-bom:27.0.0' } apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/transparent_icon.png b/android/app/src/main/res/mipmap-hdpi/transparent_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f8fddc349c3a2e79088ca668cc9455ba3f3ae59e GIT binary patch literal 20706 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 #3A903A + #3A903A diff --git a/android/fastlane/metadata/android/id/changelogs/changelogs.txt b/android/fastlane/metadata/android/id/changelogs/changelogs.txt index 659bd95..6264239 100644 --- a/android/fastlane/metadata/android/id/changelogs/changelogs.txt +++ b/android/fastlane/metadata/android/id/changelogs/changelogs.txt @@ -1,20 +1,20 @@ +3.4.0: +- Notifikasi pengguna untuk komentar baru + 3.3.0: -- New feature for add kegiatan -- Fix bugs +- Kegiatan disabilitas di suatu lokasi +- Perbaikan bugs 3.2.1: -- Fix cant update fasilitas +- Perbaikan masalah update fasilitas 3.2.0: -- New feature share fasilitas as a link +- Tersedia fitur membagikan informasi fasilitas/kegiatan disabilitas kepada orang lain 3.1.2: -- Fix google oauth +- Perbaikan masuk dengan Google 3.1.1: -- Add dialog to turn on Location services upon starting the app -- New feature for layanan search -- Add feature to upload image for a fasilitas straight from the camera -- Add default image for facilities -- Add search history feature -- Now all fields are required in edit profile +- Tersedia fitur pencarian layanan +- Tersedia pilihan kamera ketika menambahkan gambar fasilitas +- Tersedia fitur riwayat pencarian diff --git a/lib/bloc/cloud_messaging_bloc.dart b/lib/bloc/cloud_messaging_bloc.dart new file mode 100644 index 0000000..b91d380 --- /dev/null +++ b/lib/bloc/cloud_messaging_bloc.dart @@ -0,0 +1,26 @@ +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; +import 'package:get_it/get_it.dart'; +import 'package:http/http.dart'; + +class CloudMessagingBloc { + CloudMessagingRepository _cloudMessagingRepository; + + CloudMessagingBloc() { + _cloudMessagingRepository = + GetIt.instance.get(); + } + + Future sendFCMToken( + String fcmToken, + String token, + ) async { + try { + return await _cloudMessagingRepository.sendFCMToken( + fcmToken, + token, + ); + } catch (e) { + return Response('Failed to add komentar', 400); + } + } +} diff --git a/lib/config/custom_serializer.dart b/lib/config/custom_serializer.dart index 00ed551..458c5a4 100644 --- a/lib/config/custom_serializer.dart +++ b/lib/config/custom_serializer.dart @@ -2,6 +2,6 @@ import 'package:intl/intl.dart'; class CustomSerializer { static DateTime stringToDateTime(String date) { - return DateFormat('yyyy-MM-dd hh:mm').parse(date); + return DateFormat('dd-MM-yyyy hh:mm').parse(date); } } diff --git a/lib/get_it.dart b/lib/get_it.dart index aec9e26..18eee1b 100644 --- a/lib/get_it.dart +++ b/lib/get_it.dart @@ -1,3 +1,4 @@ +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/kegiatan_repository.dart'; import 'package:bisaGo/repository/kegiatan_terdekat_repository.dart'; import 'package:bisaGo/repository/komentar_posting_kegiatan_repository.dart'; @@ -26,8 +27,8 @@ class AppGetIt { () => KomentarPostingRepository()); _getIt.registerLazySingleton( () => KomentarPostingKegiatanRepository()); - _getIt.registerLazySingleton( - () => LokasiRepository()); + _getIt + .registerLazySingleton(() => LokasiRepository()); _getIt.registerLazySingleton( () => LayananRepository()); _getIt.registerLazySingleton( @@ -36,5 +37,7 @@ class AppGetIt { () => KegiatanTerdekatRepository()); _getIt.registerLazySingleton( () => DynamicLinksServiceRepository()); + _getIt.registerLazySingleton( + () => CloudMessagingRepository()); } } diff --git a/lib/main.dart b/lib/main.dart index 1d165ee..aa484a7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ -import 'package:bisaGo/get_it.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:bisaGo/app.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -6,14 +7,24 @@ import 'package:intl/intl.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'flavor/flavor.dart'; import 'globalnetwork.dart'; +import 'package:bisaGo/get_it.dart'; + +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // If you're going to use other Firebase services in the background, such as Firestore, + // make sure you call `initializeApp` before using other Firebase services. + await Firebase.initializeApp(); + + print('Handling a background message: ${message.messageId}'); +} Future main() async { AppGetIt().initialize(); await DotEnv().load('.env'); getDioInstance('build'); await initializeDateFormatting('id_ID', null); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); Intl.defaultLocale = 'id_ID'; dio.options.receiveTimeout = 15000; - ApiFlavor.flavor = BuildFlavor.production.toString(); + ApiFlavor.flavor = BuildFlavor.development.toString(); runApp(BisaGo()); } diff --git a/lib/main_dev.dart b/lib/main_dev.dart index 564a455..aa484a7 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -1,3 +1,5 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:bisaGo/app.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -7,11 +9,20 @@ import 'flavor/flavor.dart'; import 'globalnetwork.dart'; import 'package:bisaGo/get_it.dart'; +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // If you're going to use other Firebase services in the background, such as Firestore, + // make sure you call `initializeApp` before using other Firebase services. + await Firebase.initializeApp(); + + print('Handling a background message: ${message.messageId}'); +} + Future main() async { AppGetIt().initialize(); await DotEnv().load('.env'); getDioInstance('build'); await initializeDateFormatting('id_ID', null); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); Intl.defaultLocale = 'id_ID'; dio.options.receiveTimeout = 15000; ApiFlavor.flavor = BuildFlavor.development.toString(); diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 564412c..aa484a7 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -1,3 +1,5 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:bisaGo/app.dart'; import 'package:intl/date_symbol_data_local.dart'; @@ -7,13 +9,22 @@ import 'flavor/flavor.dart'; import 'globalnetwork.dart'; import 'package:bisaGo/get_it.dart'; +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + // If you're going to use other Firebase services in the background, such as Firestore, + // make sure you call `initializeApp` before using other Firebase services. + await Firebase.initializeApp(); + + print('Handling a background message: ${message.messageId}'); +} + Future main() async { AppGetIt().initialize(); await DotEnv().load('.env'); getDioInstance('build'); await initializeDateFormatting('id_ID', null); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); Intl.defaultLocale = 'id_ID'; dio.options.receiveTimeout = 15000; - ApiFlavor.flavor = BuildFlavor.staging.toString(); + ApiFlavor.flavor = BuildFlavor.development.toString(); runApp(BisaGo()); -} \ No newline at end of file +} diff --git a/lib/model/kegiatan.dart b/lib/model/kegiatan.dart index 0903c22..17faede 100644 --- a/lib/model/kegiatan.dart +++ b/lib/model/kegiatan.dart @@ -15,7 +15,7 @@ class KegiatanModel { @JsonKey(name: 'place_id') final String placeId; final String creator; - @JsonKey(name: 'crator_email') + @JsonKey(name: 'creator_email') final String creatorEmail; @JsonKey(name: 'nama_kegiatan') final String namaKegiatan; diff --git a/lib/model/kegiatan.g.dart b/lib/model/kegiatan.g.dart index 0c9e114..ab0f845 100644 --- a/lib/model/kegiatan.g.dart +++ b/lib/model/kegiatan.g.dart @@ -33,7 +33,7 @@ KegiatanModel _$KegiatanModelFromJson(Map json) { timeStart: CustomSerializer.stringToDateTime(json['time_start'] as String), timeEnd: CustomSerializer.stringToDateTime(json['time_end'] as String), image: (json['image'] as List)?.map((e) => e as String)?.toList(), - creatorEmail: json['crator_email'] as String, + creatorEmail: json['creator_email'] as String, ); } @@ -42,7 +42,7 @@ Map _$KegiatanModelToJson(KegiatanModel instance) => 'id': instance.id, 'place_id': instance.placeId, 'creator': instance.creator, - 'crator_email': instance.creatorEmail, + 'creator_email': instance.creatorEmail, 'nama_kegiatan': instance.namaKegiatan, 'penyelenggara': instance.penyelenggara, 'narahubung': instance.narahubung, diff --git a/lib/model/komentar_posting.dart b/lib/model/komentar_posting.dart index eebf956..c7064e0 100644 --- a/lib/model/komentar_posting.dart +++ b/lib/model/komentar_posting.dart @@ -14,17 +14,17 @@ class KomentarPostingModel { final int id; final String deskripsi; final String creator; - @JsonKey(name: 'date_time', fromJson: CustomSerializer.stringToDateTime) - final DateTime dateTime; @JsonKey(name: 'creator_email') final String creatorEmail; + @JsonKey(fromJson: CustomSerializer.stringToDateTime) + final DateTime created; KomentarPostingModel({ this.id, this.deskripsi, this.creator, - this.dateTime, this.creatorEmail, + this.created, }); factory KomentarPostingModel.fromJson(Map json) => diff --git a/lib/model/komentar_posting.g.dart b/lib/model/komentar_posting.g.dart index f586849..5323425 100644 --- a/lib/model/komentar_posting.g.dart +++ b/lib/model/komentar_posting.g.dart @@ -27,8 +27,8 @@ KomentarPostingModel _$KomentarPostingModelFromJson(Map json) { id: json['id'] as int, deskripsi: json['deskripsi'] as String, creator: json['creator'] as String, - dateTime: CustomSerializer.stringToDateTime(json['date_time'] as String), creatorEmail: json['creator_email'] as String, + created: CustomSerializer.stringToDateTime(json['created'] as String), ); } @@ -38,6 +38,6 @@ Map _$KomentarPostingModelToJson( 'id': instance.id, 'deskripsi': instance.deskripsi, 'creator': instance.creator, - 'date_time': instance.dateTime?.toIso8601String(), 'creator_email': instance.creatorEmail, + 'created': instance.created?.toIso8601String(), }; diff --git a/lib/model/komentar_posting_kegiatan.dart b/lib/model/komentar_posting_kegiatan.dart index d14c553..19ded6e 100644 --- a/lib/model/komentar_posting_kegiatan.dart +++ b/lib/model/komentar_posting_kegiatan.dart @@ -1,5 +1,5 @@ +import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:bisaGo/config/custom_serializer.dart'; part 'komentar_posting_kegiatan.g.dart'; @@ -13,10 +13,10 @@ class KomentarPostingKegiatanList { class KomentarPostingKegiatanModel { final int id; final String creator; - @JsonKey(name: 'crator_email') + @JsonKey(name: 'creator_email') final String creatorEmail; final String deskripsi; - @JsonKey(name: 'created', fromJson: CustomSerializer.stringToDateTime) + @JsonKey(name: 'created', fromJson: _stringToDateTime) final DateTime created; KomentarPostingKegiatanModel({ @@ -32,3 +32,7 @@ class KomentarPostingKegiatanModel { Map toJson() => _$KomentarPostingKegiatanModelToJson(this); } + +DateTime _stringToDateTime(String date) { + return DateFormat('yyy-MM-dd hh:mm').parse(date); +} diff --git a/lib/model/komentar_posting_kegiatan.g.dart b/lib/model/komentar_posting_kegiatan.g.dart index 59124a1..f1108b5 100644 --- a/lib/model/komentar_posting_kegiatan.g.dart +++ b/lib/model/komentar_posting_kegiatan.g.dart @@ -29,8 +29,8 @@ KomentarPostingKegiatanModel _$KomentarPostingKegiatanModelFromJson( id: json['id'] as int, creator: json['creator'] as String, deskripsi: json['deskripsi'] as String, - created: CustomSerializer.stringToDateTime(json['created'] as String), - creatorEmail: json['crator_email'] as String, + created: _stringToDateTime(json['created'] as String), + creatorEmail: json['creator_email'] as String, ); } @@ -39,7 +39,7 @@ Map _$KomentarPostingKegiatanModelToJson( { 'id': instance.id, 'creator': instance.creator, - 'crator_email': instance.creatorEmail, + 'creator_email': instance.creatorEmail, 'deskripsi': instance.deskripsi, 'created': instance.created?.toIso8601String(), }; diff --git a/lib/page/dashboard/dashboard.dart b/lib/page/dashboard/dashboard.dart index 8e70ad4..f6915b4 100644 --- a/lib/page/dashboard/dashboard.dart +++ b/lib/page/dashboard/dashboard.dart @@ -1,3 +1,4 @@ +import 'package:bisaGo/bloc/cloud_messaging_bloc.dart'; import 'package:bisaGo/bloc/lokasi_response_bloc.dart'; import 'package:bisaGo/model/komentar.dart'; import 'package:bisaGo/model/lokasi.dart'; @@ -11,7 +12,10 @@ import 'package:bisaGo/page/filter_fasilitas/postingan/detail_post.dart'; import 'package:bisaGo/repository/komentar_repository.dart'; import 'package:bisaGo/utils/custom_dashboard_location_button.dart'; import 'package:bisaGo/utils/location_turn_on_dialog.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:geolocator/geolocator.dart'; @@ -21,6 +25,9 @@ import 'package:bisaGo/component/bisago_drawer.dart'; import 'package:bisaGo/config/styles.dart'; import 'package:bisaGo/page/pencarian/pencarian.dart'; import 'package:google_maps_webservice/places.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../filter_fasilitas/postingan/detail_post.dart'; class Dashboard extends StatefulWidget { const Dashboard({Key key}) : super(key: key); @@ -37,6 +44,13 @@ class DashboardState extends State { LokasiResponseBloc bloc = LokasiResponseBloc(); KegiatanTerdekatBloc blocKegiatanTerdekat = KegiatanTerdekatBloc(); + CloudMessagingBloc cloudMessagingBloc = CloudMessagingBloc(); + + FirebaseMessaging _firebaseMessaging; + + bool _initialized = false; + + DateTime _lastNotification; @override void initState() { @@ -45,6 +59,71 @@ class DashboardState extends State { geolocator = Geolocator()..forceAndroidLocationManager; initDynamicLinks(); setInitialLocation(); + + if (!_initialized) { + _initialized = true; + _setupFirebase(); + } + } + + void _setupFirebase() async { + await Firebase.initializeApp(); + _firebaseMessaging = FirebaseMessaging.instance; + + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + final now = DateTime.now(); + + if (_lastNotification != null && + _lastNotification.add(Duration(seconds: 10)).isAfter(now)) return; + + _lastNotification = now; + final data = message.data; + final String msg = data['message']; + final String title = data['title']; + Flushbar( + title: title, + message: msg, + duration: Duration(seconds: 8), + backgroundColor: Color(0xFF003566), + onTap: (_) { + if (data['type'] == 'fasilitas') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailFasilitasPage(context, placeId, id); + } else if (data['type'] == 'kegiatan') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailKegiatanPage(context, placeId, id); + } + }, + flushbarStyle: FlushbarStyle.GROUNDED, + flushbarPosition: FlushbarPosition.TOP, + ).show(context); + }); + + FirebaseMessaging.onMessageOpenedApp.listen((message) { + final data = message.data; + if (data['type'] == 'fasilitas') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailFasilitasPage(context, placeId, id); + } else if (data['type'] == 'kegiatan') { + final String placeId = data['place_id']; + final id = int.parse(data['id']); + _navigateToDetailKegiatanPage(context, placeId, id); + } + }); + + _requestFCMToken(); + } + + void _requestFCMToken() async { + final fcmToken = await _firebaseMessaging.getToken(); + final sharedPreferences = await SharedPreferences.getInstance(); + final token = sharedPreferences.getString('token'); + if (token != null) { + await cloudMessagingBloc.sendFCMToken(fcmToken, token); + } } void _navigateToPencarianPage(BuildContext context) { @@ -368,20 +447,20 @@ class DashboardState extends State { ..name = namaLokasi; final fasilitasRoute = MaterialPageRoute( builder: (BuildContext context) => DetailPostKegiatanPage( - lokasi: lokasi, - kegiatan: KegiatanModel( - id: kegiatan.id, - placeId: lokasi.placeId, - creator: kegiatan.creator, - namaKegiatan: kegiatan.namaKegiatan, - penyelenggara: kegiatan.penyelenggara, - narahubung: kegiatan.narahubung, - deskripsi: kegiatan.deskripsi, - timeStart: kegiatan.timeStart, - timeEnd: kegiatan.timeEnd, - image: kegiatan.image, - ), - )); + lokasi: lokasi, + kegiatan: KegiatanModel( + id: kegiatan.id, + placeId: lokasi.placeId, + creator: kegiatan.creator, + namaKegiatan: kegiatan.namaKegiatan, + penyelenggara: kegiatan.penyelenggara, + narahubung: kegiatan.narahubung, + deskripsi: kegiatan.deskripsi, + timeStart: kegiatan.timeStart, + timeEnd: kegiatan.timeEnd, + image: kegiatan.image, + ), + )); await Navigator.of(context).push(fasilitasRoute); } diff --git a/lib/page/filter_fasilitas/kegiatan.dart b/lib/page/filter_fasilitas/kegiatan.dart index ebc321f..8c3fcf2 100644 --- a/lib/page/filter_fasilitas/kegiatan.dart +++ b/lib/page/filter_fasilitas/kegiatan.dart @@ -22,7 +22,7 @@ class _KegiatanState extends State { return InkWell( key: Key(widget.kegiatan.namaKegiatan), onTap: () { - Navigator.of(context).pushReplacement(MaterialPageRoute( + Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) => DetailPostKegiatanPage( lokasi: widget.lokasi, kegiatan: KegiatanModel( diff --git a/lib/page/filter_fasilitas/komentar.dart b/lib/page/filter_fasilitas/komentar.dart index 345a7d2..2165549 100644 --- a/lib/page/filter_fasilitas/komentar.dart +++ b/lib/page/filter_fasilitas/komentar.dart @@ -37,7 +37,7 @@ class _KomentarState extends State { return InkWell( key: Key('Fasilitas ${fasilitas[widget.komentar.tag]}'), onTap: () { - Navigator.of(context).pushReplacement(MaterialPageRoute( + Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) => DetailPostPage( lokasi: widget.lokasi, komentar: KomentarModel( diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index 2072fe9..6f57b2d 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bisaGo/config/strings.dart'; import 'package:bisaGo/model/lokasi.dart'; -import 'package:bisaGo/page/filter_fasilitas/fasilitas.dart'; import 'package:bisaGo/page/profile/profile.dart'; import 'package:bisaGo/page/updateInformasi/update_informasi.dart'; import 'package:bisaGo/repository/dynamic_links_service_repository.dart'; @@ -26,9 +25,11 @@ import 'package:shared_preferences/shared_preferences.dart'; class DetailPostPage extends StatefulWidget { final Lokasi lokasi; final KomentarModel komentar; + const DetailPostPage( {@required this.komentar, @required this.lokasi, Key key}) : super(key: key); + @override _DetailPostPageState createState() => _DetailPostPageState(); } @@ -43,6 +44,7 @@ class _DetailPostPageState extends State { int dislike; int alreadyLikeFlag; int alreadyDislikeFlag; + @override void initState() { alreadyLikeFlag = 0; @@ -55,375 +57,364 @@ class _DetailPostPageState extends State { @override Widget build(BuildContext context) { var namaLokasi = widget.komentar.namaLokasi ?? 'Invalid Lokasi Name'; - return WillPopScope( - onWillPop: () => Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (BuildContext context) => Fasilitas( - lokasi: widget.lokasi, - ))), - child: Scaffold( - appBar: BisaGoAppBar( - title: namaLokasi, - key: Key('appbar-text-$namaLokasi'), - actions: [ - InkWell( - onTap: () async { - final link = await DynamicLinksServiceRepository() - .createDynamicLinkForFasilitas( - widget.komentar.id, - widget.lokasi.placeId, - ); - await Share.share(ShareUtils.getFormattedMessageFasilitas( - widget.komentar, widget.lokasi, link)); - }, - child: const Padding( - padding: EdgeInsets.all(doubleSpace), - child: Icon(Icons.share), - ), + return Scaffold( + appBar: BisaGoAppBar( + title: namaLokasi, + key: Key('appbar-text-$namaLokasi'), + actions: [ + InkWell( + onTap: () async { + final link = await DynamicLinksServiceRepository() + .createDynamicLinkForFasilitas( + widget.komentar.id, + widget.lokasi.placeId, + ); + await Share.share(ShareUtils.getFormattedMessageFasilitas( + widget.komentar, widget.lokasi, link)); + }, + child: const Padding( + padding: EdgeInsets.all(doubleSpace), + child: Icon(Icons.share), ), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - Container( - key: const Key('Text Jenis Fasilitas'), - margin: const EdgeInsets.symmetric( - vertical: 10.0, horizontal: 15.0), - alignment: Alignment.centerLeft, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.6, - child: Text( - fasilitas[widget.komentar.tag], - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.w800, - letterSpacing: -0.3, - color: Colors.black, - fontFamily: 'Comfortaa', - ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + key: const Key('Text Jenis Fasilitas'), + margin: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), + alignment: Alignment.centerLeft, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.6, + child: Text( + fasilitas[widget.komentar.tag], + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.w800, + letterSpacing: -0.3, + color: Colors.black, + fontFamily: 'Comfortaa', ), ), - PopupMenuButton( - key: const Key('Button Ubah Informasi'), - elevation: 4.0, - offset: const Offset(0.0, 40.0), - itemBuilder: (BuildContext context) { - final choices = ['Ubah Informasi']; - return choices.map((String choice) { - return PopupMenuItem( - key: Key(choice), - child: ElevatedButton( - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.symmetric( - vertical: 0, horizontal: 0)), - backgroundColor: - MaterialStateProperty.all(Colors.white), - foregroundColor: - MaterialStateProperty.all(Colors.black), - elevation: MaterialStateProperty.all(0)), - onPressed: _updateInformasi, - child: SizedBox( - width: double.infinity, - child: Text(choice), - ), + ), + PopupMenuButton( + key: const Key('Button Ubah Informasi'), + elevation: 4.0, + offset: const Offset(0.0, 40.0), + itemBuilder: (BuildContext context) { + final choices = ['Ubah Informasi']; + return choices.map((String choice) { + return PopupMenuItem( + key: Key(choice), + child: ElevatedButton( + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.symmetric( + vertical: 0, horizontal: 0)), + backgroundColor: + MaterialStateProperty.all(Colors.white), + foregroundColor: + MaterialStateProperty.all(Colors.black), + elevation: MaterialStateProperty.all(0)), + onPressed: _updateInformasi, + child: SizedBox( + width: double.infinity, + child: Text(choice), ), - ); - }).toList(); - }, - ), - ], - ), + ), + ); + }).toList(); + }, + ), + ], ), - Container( - key: const Key('Text Jumlah'), - width: MediaQuery.of(context).size.width, - color: red, - padding: const EdgeInsets.symmetric( - vertical: regularSpace, horizontal: doubleSpace), - child: Text( - 'Tersedia sebanyak ${widget.komentar.jumlah} ' - 'unit fasilitas.', - style: const TextStyle( - fontSize: 16, - color: Colors.white, - fontFamily: 'Comfortaa', - ), - )), - Container( - margin: const EdgeInsets.all(doubleSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 150, - child: ImageHolder( - url: widget.komentar.image, - fasilitas: widget.komentar.tag)), - const SizedBox( - height: 10, - ), - // Wrap( - // alignment: WrapAlignment.start, - // direction: Axis.horizontal, - // crossAxisAlignment: WrapCrossAlignment.start, - // children: widget.komentar.tag - // .map((tag) => - // _createTagContainer(getTag(tag))) - // .toList(), - // ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + Container( + key: const Key('Text Jumlah'), + width: MediaQuery.of(context).size.width, + color: red, + padding: const EdgeInsets.symmetric( + vertical: regularSpace, horizontal: doubleSpace), + child: Text( + 'Tersedia sebanyak ${widget.komentar.jumlah} ' + 'unit fasilitas.', + style: const TextStyle( + fontSize: 16, + color: Colors.white, + fontFamily: 'Comfortaa', + ), + )), + Container( + margin: const EdgeInsets.all(doubleSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 150, + child: ImageHolder( + url: widget.komentar.image, + fasilitas: widget.komentar.tag)), + const SizedBox( + height: 10, + ), + // Wrap( + // alignment: WrapAlignment.start, + // direction: Axis.horizontal, + // crossAxisAlignment: WrapCrossAlignment.start, + // children: widget.komentar.tag + // .map((tag) => + // _createTagContainer(getTag(tag))) + // .toList(), + // ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Dapat digunakan oleh', + style: TextStyle(fontSize: 16), + textAlign: TextAlign.left, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _showIfContains('DF'), + _showIfContains('DI'), + _showIfContains('DM'), + _showIfContains('DS'), + ], + ) + ], + ), + Container( + key: const Key('desc'), + decoration: BoxDecoration( + color: gray, + boxShadow: regularShadow, + borderRadius: regularBorderRadius), + padding: const EdgeInsets.all(doubleSpace), + margin: const EdgeInsets.symmetric(vertical: doubleSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Dapat digunakan oleh', - style: TextStyle(fontSize: 16), - textAlign: TextAlign.left, - ), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _showIfContains('DF'), - _showIfContains('DI'), - _showIfContains('DM'), - _showIfContains('DS'), - ], - ) - ], - ), - Container( - key: const Key('desc'), - decoration: BoxDecoration( - color: gray, - boxShadow: regularShadow, - borderRadius: regularBorderRadius), - padding: const EdgeInsets.all(doubleSpace), - margin: const EdgeInsets.symmetric(vertical: doubleSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Expanded( - child: Text( - 'Cara menggunakan', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w800), - ), + const Expanded( + child: Text( + 'Cara menggunakan', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w800), ), - ], - ), - Container( - margin: const EdgeInsets.symmetric( - vertical: regularSpace), - child: Text( - widget.komentar.deskripsi, - key: const Key('Text Cara Menggunakan'), - style: const TextStyle(fontSize: 16), ), + ], + ), + Container( + margin: const EdgeInsets.symmetric( + vertical: regularSpace), + child: Text( + widget.komentar.deskripsi, + key: const Key('Text Cara Menggunakan'), + style: const TextStyle(fontSize: 16), ), - ], - ), + ), + ], ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Text( - 'informasi ditambahkan oleh ', - style: TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - fontWeight: FontWeight.w200, - ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text( + 'informasi ditambahkan oleh ', + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w200, ), - Container( - padding: EdgeInsets.zero, - constraints: BoxConstraints( - maxWidth: - MediaQuery.of(context).size.width * 0.3), - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => Profile( - email: widget.komentar.creatorEmail, - isPublic: true, - ), + ), + Container( + padding: EdgeInsets.zero, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.3), + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => Profile( + email: widget.komentar.creatorEmail, + isPublic: true, ), - ); - }, - child: Text( - '${widget.komentar.creator} ', - key: Key('creator-${widget.komentar.creator}'), - overflow: TextOverflow.fade, - softWrap: false, - style: const TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, ), + ); + }, + child: Text( + '${widget.komentar.creator} ', + key: Key('creator-${widget.komentar.creator}'), + overflow: TextOverflow.fade, + softWrap: false, + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, ), ), ), - Text( - '(${DateFormat('dd MMM yyy').format(widget.komentar.dateTime)})', - key: const Key('timestamp'), - style: const TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - ), + ), + Text( + '(${DateFormat('dd MMM yyy').format(widget.komentar.dateTime)})', + key: const Key('timestamp'), + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, ), - ], - ), - const SizedBox( - height: regularSpace, - ), - const Divider( - color: grayPrimary, - thickness: 1.0, - ), - Container( - key: const Key('Komentar'), - padding: - const EdgeInsets.symmetric(vertical: regularSpace), - child: const Text( - 'Komentar', - style: TextStyle( - fontSize: 20, fontWeight: FontWeight.w800), - )), - StreamBuilder( - stream: _bloc.komentarPostingListStream, - builder: (context, snapshot) { - if (snapshot.hasData) { - switch (snapshot.data.status) { - case Status.loading: + ), + ], + ), + const SizedBox( + height: regularSpace, + ), + const Divider( + color: grayPrimary, + thickness: 1.0, + ), + Container( + key: const Key('Komentar'), + padding: + const EdgeInsets.symmetric(vertical: regularSpace), + child: const Text( + 'Komentar', + style: TextStyle( + fontSize: 20, fontWeight: FontWeight.w800), + )), + StreamBuilder( + stream: _bloc.komentarPostingListStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + switch (snapshot.data.status) { + case Status.loading: + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + greenPrimary), + ), + ); + break; + case Status.completed: + allKomentarPostingFromApi = + snapshot.data.data.allKomentar; + if (allKomentarPostingFromApi.isEmpty) { return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - greenPrimary), - ), - ); - break; - case Status.completed: - allKomentarPostingFromApi = - snapshot.data.data.allKomentar; - if (allKomentarPostingFromApi.isEmpty) { - return const Center( - child: Text('Tidak ada Komentar')); - } else { - return Column( - children: allKomentarPostingFromApi - .map( - (k) => komentarPlaceHolder( - k.creator, - k.dateTime, - k.deskripsi, - k.creatorEmail, - ), - ) - .toList()); - } - break; - case Status.error: - return Center( - child: Text(snapshot.data.data.toString()), - ); - break; - } + child: Text('Tidak ada Komentar')); + } else { + return Column( + children: allKomentarPostingFromApi + .map( + (k) => komentarPlaceHolder( + k.creator, + k.created, + k.deskripsi, + k.creatorEmail, + ), + ) + .toList()); + } + break; + case Status.error: + return Center( + child: Text(snapshot.data.data.toString()), + ); + break; } - return Container(); - }), - const SizedBox(height: regularSpace), - Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - key: const Key('Text Field Komentar'), - keyboardType: TextInputType.multiline, - maxLines: null, - minLines: 3, - validator: FieldValidator.validateInfo, - controller: komentarController, - style: const TextStyle( - fontSize: 18, - ), - decoration: InputDecoration( - hintStyle: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 15), - hintText: 'Tulis komentar...', - contentPadding: const EdgeInsets.all(8.0), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), + } + return Container(); + }), + const SizedBox(height: regularSpace), + Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + key: const Key('Text Field Komentar'), + keyboardType: TextInputType.multiline, + maxLines: null, + minLines: 3, + validator: FieldValidator.validateInfo, + controller: komentarController, + style: const TextStyle( + fontSize: 18, + ), + decoration: InputDecoration( + hintStyle: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 15), + hintText: 'Tulis komentar...', + contentPadding: const EdgeInsets.all(8.0), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, ), ), ), - Container( - key: const Key('tambah komentar'), - padding: - const EdgeInsets.only(top: doubleSpace), - alignment: Alignment.center, - child: ButtonTheme( - minWidth: double.infinity, - height: 40, - child: ElevatedButton( - key: const Key('Button Tambah Komentar'), - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(vertical: 13)), - elevation: MaterialStateProperty.all(0.0), - backgroundColor: - MaterialStateProperty.all( - greenPrimary), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(10)))), - ), - onPressed: () { - _checkLoginStatus(); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon( - Icons.add, - size: 30, - color: Colors.white, - ), - const SizedBox(width: 5.0), - const Text( - 'Tambah Komentar', - style: TextStyle( - fontSize: 20, - color: Colors.white, - fontWeight: FontWeight.bold), - ), - ], - ), + ), + Container( + key: const Key('tambah komentar'), + padding: const EdgeInsets.only(top: doubleSpace), + alignment: Alignment.center, + child: ButtonTheme( + minWidth: double.infinity, + height: 40, + child: ElevatedButton( + key: const Key('Button Tambah Komentar'), + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.symmetric(vertical: 13)), + elevation: MaterialStateProperty.all(0.0), + backgroundColor: + MaterialStateProperty.all(greenPrimary), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10)))), ), - )), - ], - )), - ], - ), + onPressed: () { + _checkLoginStatus(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.add, + size: 30, + color: Colors.white, + ), + const SizedBox(width: 5.0), + const Text( + 'Tambah Komentar', + style: TextStyle( + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold), + ), + ], + ), + ), + )), + ], + )), + ], ), - ], - ), + ), + ], ), ), ); @@ -545,7 +536,7 @@ class _DetailPostPageState extends State { style: const TextStyle(fontSize: 18), ), ), - Text('${DateFormat('dd MMMM yyy hh:mm').format(date)}', + Text('${DateFormat('dd MMMM yyyy hh:mm').format(date)}', style: const TextStyle(color: grayPrimary, fontSize: 14)) ], ), diff --git a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart index 5af0566..08ded62 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart @@ -19,8 +19,6 @@ import 'package:bisaGo/page/login/login.dart'; import 'package:share/share.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../fasilitas.dart'; - class DetailPostKegiatanPage extends StatefulWidget { final Lokasi lokasi; final KegiatanModel kegiatan; // ganti model x // sudah @@ -48,437 +46,423 @@ class _DetailPostKegiatanPageState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () => Navigator.of(context).pushReplacement(MaterialPageRoute( - builder: (BuildContext context) => Fasilitas( - lokasi: widget.lokasi, - ))), - child: Scaffold( - appBar: BisaGoAppBar( - title: widget.lokasi.name, - key: Key('appbar-text-${widget.kegiatan.placeId}'), - actions: [ - InkWell( - onTap: () async { - final link = await DynamicLinksServiceRepository() - .createDynamicLinkForKegiatan( - widget.kegiatan.id, - widget.lokasi.placeId, - ); - await Share.share(ShareUtils.getFormattedMessageKegiatan( - widget.kegiatan, widget.lokasi, link)); - }, - child: const Padding( - padding: EdgeInsets.all(doubleSpace), - child: Icon(Icons.share), - ), + print('asd ${widget.kegiatan.toJson()}'); + return Scaffold( + appBar: BisaGoAppBar( + title: widget.lokasi.name, + key: Key('appbar-text-${widget.kegiatan.placeId}'), + actions: [ + InkWell( + onTap: () async { + final link = await DynamicLinksServiceRepository() + .createDynamicLinkForKegiatan( + widget.kegiatan.id, + widget.lokasi.placeId, + ); + await Share.share(ShareUtils.getFormattedMessageKegiatan( + widget.kegiatan, widget.lokasi, link)); + }, + child: const Padding( + padding: EdgeInsets.all(doubleSpace), + child: Icon(Icons.share), ), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - Container( - key: const Key('Text Judul Kegiatan'), - margin: const EdgeInsets.symmetric( - vertical: 10.0, horizontal: 15.0), - alignment: Alignment.centerLeft, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: MediaQuery.of(context).size.width * 0.6, - // ganti alias tambahan - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: regularSpace), - Text( - widget.kegiatan - .namaKegiatan, // ganti nama kegiatan // sudah - style: const TextStyle( - fontSize: 30, - fontWeight: FontWeight.w800, - letterSpacing: 0.3, - fontFamily: 'Muli', - // color: Colors.black, - // fontFamily: 'Comfortaa', - ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + key: const Key('Text Judul Kegiatan'), + margin: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), + alignment: Alignment.centerLeft, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.6, + // ganti alias tambahan + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: regularSpace), + Text( + widget.kegiatan + .namaKegiatan, // ganti nama kegiatan // sudah + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.w800, + letterSpacing: 0.3, + fontFamily: 'Muli', + // color: Colors.black, + // fontFamily: 'Comfortaa', ), - Text( - 'oleh ${widget.kegiatan.penyelenggara}', // ganti format ${nama penyelenggara} // sudah - style: TextStyle(fontSize: 16), - textAlign: TextAlign.left, - ) - ], - ), + ), + Text( + 'oleh ${widget.kegiatan.penyelenggara}', // ganti format ${nama penyelenggara} // sudah + style: TextStyle(fontSize: 16), + textAlign: TextAlign.left, + ) + ], ), - // PopupMenuButton( - // key: const Key('Button Ubah Informasi'), - // elevation: 4.0, - // offset: const Offset(0.0, 40.0), - // itemBuilder: (BuildContext context) { - // final choices = ['Ubah Informasi']; - // return choices.map((String choice) { - // return PopupMenuItem( - // key: Key(choice), - // child: ElevatedButton( - // style: ButtonStyle( - // padding: MaterialStateProperty.all( - // EdgeInsets.symmetric( - // vertical: 0, horizontal: 0)), - // backgroundColor: - // MaterialStateProperty.all(Colors.white), - // foregroundColor: - // MaterialStateProperty.all(Colors.black), - // elevation: MaterialStateProperty.all(0)), - // onPressed: _updateInformasi, - // child: SizedBox( - // width: double.infinity, - // child: Text(choice), - // ), - // ), - // ); - // }).toList(); - // }, - // ), - ], - ), + ), + // PopupMenuButton( + // key: const Key('Button Ubah Informasi'), + // elevation: 4.0, + // offset: const Offset(0.0, 40.0), + // itemBuilder: (BuildContext context) { + // final choices = ['Ubah Informasi']; + // return choices.map((String choice) { + // return PopupMenuItem( + // key: Key(choice), + // child: ElevatedButton( + // style: ButtonStyle( + // padding: MaterialStateProperty.all( + // EdgeInsets.symmetric( + // vertical: 0, horizontal: 0)), + // backgroundColor: + // MaterialStateProperty.all(Colors.white), + // foregroundColor: + // MaterialStateProperty.all(Colors.black), + // elevation: MaterialStateProperty.all(0)), + // onPressed: _updateInformasi, + // child: SizedBox( + // width: double.infinity, + // child: Text(choice), + // ), + // ), + // ); + // }).toList(); + // }, + // ), + ], ), - Container( - margin: const EdgeInsets.all(doubleSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width, - height: 160, - child: CarouselSlider( - options: CarouselOptions( - aspectRatio: 1.0, - enlargeCenterPage: true, - enableInfiniteScroll: false, - initialPage: 0, - autoPlay: true, - ), - items: widget.kegiatan.image - .map((item) => Container( - child: Container( - child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(20)), - child: Stack( - children: [ - Image.network(item, - fit: BoxFit.cover, - width: 1000.0), - Positioned( - bottom: 0.0, - left: 0.0, - right: 0.0, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color.fromARGB( - 200, 0, 0, 0), - Color.fromARGB(0, 0, 0, 0) - ], - begin: - Alignment.bottomCenter, - end: Alignment.topCenter, - ), + ), + Container( + margin: const EdgeInsets.all(doubleSpace), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + height: 160, + child: CarouselSlider( + options: CarouselOptions( + aspectRatio: 1.0, + enlargeCenterPage: true, + enableInfiniteScroll: false, + initialPage: 0, + autoPlay: true, + ), + items: widget.kegiatan.image + .map((item) => Container( + child: Container( + child: ClipRRect( + borderRadius: + BorderRadius.all(Radius.circular(20)), + child: Stack( + children: [ + Image.network(item, + fit: BoxFit.cover, width: 1000.0), + Positioned( + bottom: 0.0, + left: 0.0, + right: 0.0, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color.fromARGB( + 200, 0, 0, 0), + Color.fromARGB(0, 0, 0, 0) + ], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, ), - padding: EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 20.0), - child: Text( - '#${widget.kegiatan.image.indexOf(item) + 1}', - style: TextStyle( - color: Colors.white, - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), + ), + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 20.0), + child: Text( + '#${widget.kegiatan.image.indexOf(item) + 1}', + style: TextStyle( + color: Colors.white, + fontSize: 20.0, + fontWeight: FontWeight.bold, ), ), ), - ], - )), - ), - )) - .toList(), - ), - ), - const SizedBox( - height: 10, - ), - Container( - key: const Key('desc'), - // decoration: BoxDecoration( - // color: gray, - // boxShadow: regularShadow, - // borderRadius: regularBorderRadius), - padding: const EdgeInsets.all(doubleSpace), - margin: const EdgeInsets.symmetric(vertical: doubleSpace), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // children: [ - // const Expanded( - // child: Text( - // 'Cara menggunakan', - // style: TextStyle( - // fontSize: 20, - // fontWeight: FontWeight.w800), - // ), - // ), - // ], - // ), - // Container( - // margin: const EdgeInsets.symmetric( - // vertical: regularSpace - // ), - // ganti alias tambahan - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - // mainAxisAlignment: - // MainAxisAlignment.center, - children: [ - const Icon( - Icons.access_time, - size: 20, - color: Colors.green, - ), - const SizedBox(width: 7.0), - const Text( - 'Senin, 3 Mei 2021', // ganti format widget.x.tanggalpelaksanaan - style: TextStyle(fontSize: 16), - key: Key('Text Waktu Pelaksanaan'), - ), - ], - ), - SizedBox(height: 40), - Text( - widget.kegiatan - .deskripsi, // ganti format widget.x.deskripsi kegiatan // sudah - style: const TextStyle(fontSize: 16), - key: const Key('Text Deskripsi Kegiatan'), - ), - SizedBox(height: 40), - Row( - // mainAxisAlignment: - // MainAxisAlignment.center, - children: [ - const Icon( - Icons.call, - size: 20, - color: Colors.green, - ), - const SizedBox(width: 7.0), - Text( - widget.kegiatan - .narahubung, // ganti format narahubung // sudah - style: TextStyle(fontSize: 16), - key: Key('Text Narahubung'), - ), - ], - ), - ], - ), - // ), - // ], - // ), + ), + ], + )), + ), + )) + .toList(), ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Text( - 'informasi ditambahkan oleh ', - style: TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, - fontWeight: FontWeight.w200, - ), - ), - Container( - padding: EdgeInsets.zero, - constraints: BoxConstraints( - maxWidth: - MediaQuery.of(context).size.width * 0.3), - child: Text( - widget.kegiatan - .creator, // ganti format ${widget.x.creator} // sudah - key: Key( - 'Creator info kegiatan'), // ganti format key creator-${widget.x.creator} - overflow: TextOverflow.fade, - softWrap: false, - style: const TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, + ), + const SizedBox( + height: 10, + ), + Container( + key: const Key('desc'), + // decoration: BoxDecoration( + // color: gray, + // boxShadow: regularShadow, + // borderRadius: regularBorderRadius), + padding: const EdgeInsets.all(doubleSpace), + margin: const EdgeInsets.symmetric(vertical: doubleSpace), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // children: [ + // const Expanded( + // child: Text( + // 'Cara menggunakan', + // style: TextStyle( + // fontSize: 20, + // fontWeight: FontWeight.w800), + // ), + // ), + // ], + // ), + // Container( + // margin: const EdgeInsets.symmetric( + // vertical: regularSpace + // ), + // ganti alias tambahan + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + // mainAxisAlignment: + // MainAxisAlignment.center, + children: [ + const Icon( + Icons.access_time, + size: 20, + color: Colors.green, ), - ), + const SizedBox(width: 7.0), + const Text( + 'Senin, 3 Mei 2021', // ganti format widget.x.tanggalpelaksanaan + style: TextStyle(fontSize: 16), + key: Key('Text Waktu Pelaksanaan'), + ), + ], ), + SizedBox(height: 40), Text( - '22 April 2021', - // '(${DateFormat('dd MMM yyy').format(widget.kegiatan.dateTime)})', // ganti format (${DateFormat('dd MMM yyy').format(widget.x.dateTime)}) - key: const Key('timestamp'), + widget.kegiatan + .deskripsi, // ganti format widget.x.deskripsi kegiatan // sudah + style: const TextStyle(fontSize: 16), + key: const Key('Text Deskripsi Kegiatan'), + ), + SizedBox(height: 40), + Row( + // mainAxisAlignment: + // MainAxisAlignment.center, + children: [ + const Icon( + Icons.call, + size: 20, + color: Colors.green, + ), + const SizedBox(width: 7.0), + Text( + widget.kegiatan + .narahubung, // ganti format narahubung // sudah + style: TextStyle(fontSize: 16), + key: Key('Text Narahubung'), + ), + ], + ), + ], + ), + // ), + // ], + // ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text( + 'informasi ditambahkan oleh ', + style: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w200, + ), + ), + Container( + padding: EdgeInsets.zero, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.3), + child: Text( + widget.kegiatan + .creator, // ganti format ${widget.x.creator} // sudah + key: Key( + 'Creator info kegiatan'), // ganti format key creator-${widget.x.creator} + overflow: TextOverflow.fade, + softWrap: false, style: const TextStyle( fontSize: 12, fontStyle: FontStyle.italic, ), ), - ], - ), - const SizedBox( - height: regularSpace, - ), - const Divider( - color: grayPrimary, - thickness: 1.0, - ), - Container( - key: const Key('Komentar'), - padding: - const EdgeInsets.symmetric(vertical: regularSpace), - child: const Text( - 'Komentar', - style: TextStyle( - fontSize: 20, fontWeight: FontWeight.w800), ), + Text( + '22 April 2021', + // '(${DateFormat('dd MMM yyy').format(widget.kegiatan.dateTime)})', // ganti format (${DateFormat('dd MMM yyy').format(widget.x.dateTime)}) + key: const Key('timestamp'), + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + const SizedBox( + height: regularSpace, + ), + const Divider( + color: grayPrimary, + thickness: 1.0, + ), + Container( + key: const Key('Komentar'), + padding: const EdgeInsets.symmetric(vertical: regularSpace), + child: const Text( + 'Komentar', + style: + TextStyle(fontSize: 20, fontWeight: FontWeight.w800), ), - StreamBuilder( - stream: _bloc.komentarPostingKegiatanListStream, - builder: (context, snapshot) { - if (snapshot.hasData) { - switch (snapshot.data.status) { - case Status.loading: + ), + StreamBuilder( + stream: _bloc.komentarPostingKegiatanListStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + switch (snapshot.data.status) { + case Status.loading: + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + greenPrimary), + ), + ); + break; + case Status.completed: + allKomentarPositngKegiatanFromApi = snapshot + .data + .data + .allKomentarKegiatan; // kalo error, brarti allKegiatan + if (allKomentarPositngKegiatanFromApi.isEmpty) { return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - greenPrimary), - ), - ); - break; - case Status.completed: - allKomentarPositngKegiatanFromApi = snapshot - .data - .data - .allKomentarKegiatan; // kalo error, brarti allKegiatan - if (allKomentarPositngKegiatanFromApi.isEmpty) { - return const Center( - child: Text('Tidak ada komentar')); - } else { - return Column( - children: - allKomentarPositngKegiatanFromApi - .map((k) => - komentarKegiatanPlaceHolder( - k.creator, - k.created, - k.deskripsi, - )) - .toList()); - } - break; - case Status.error: - return Center( - child: Text(snapshot.data.data.toString()), - ); - break; - } + child: Text('Tidak ada komentar')); + } else { + return Column( + children: allKomentarPositngKegiatanFromApi + .map((k) => + komentarKegiatanPlaceHolder( + k.creator, + k.created, + k.deskripsi)) + .toList()); + } + break; + case Status.error: + return Center( + child: Text(snapshot.data.data.toString()), + ); + break; } - return Container(); - }), - const SizedBox(height: regularSpace), - Form( - key: _formKey, - child: Column( - children: [ - TextFormField( - key: const Key('Text Field Komentar'), - keyboardType: TextInputType.multiline, - maxLines: null, - minLines: 3, - validator: FieldValidator.validateInfo, - controller: komentarKegiatanController, - style: const TextStyle( - fontSize: 18, - ), - decoration: InputDecoration( - hintStyle: const TextStyle( - fontWeight: FontWeight.bold, fontSize: 15), - hintText: 'Tulis komentar...', - contentPadding: const EdgeInsets.all(8.0), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), + } + return Container(); + }), + const SizedBox(height: regularSpace), + Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + key: const Key('Text Field Komentar'), + keyboardType: TextInputType.multiline, + maxLines: null, + minLines: 3, + validator: FieldValidator.validateInfo, + controller: komentarKegiatanController, + style: const TextStyle( + fontSize: 18, + ), + decoration: InputDecoration( + hintStyle: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 15), + hintText: 'Tulis komentar...', + contentPadding: const EdgeInsets.all(8.0), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, ), ), ), - Container( - key: const Key('Tambah Komentar'), - padding: - const EdgeInsets.only(top: doubleSpace), - alignment: Alignment.center, - child: ButtonTheme( - minWidth: double.infinity, - height: 40, - child: ElevatedButton( - key: const Key('Button Tambah Komentar'), - style: ButtonStyle( - padding: MaterialStateProperty.all( - EdgeInsets.symmetric(vertical: 13)), - elevation: MaterialStateProperty.all(0.0), - backgroundColor: - MaterialStateProperty.all( - greenPrimary), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(10)))), - ), - onPressed: () { - _checkLoginStatus(); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon( - Icons.add, - size: 30, - color: Colors.white, - ), - const SizedBox(width: 5.0), - const Text( - 'Tambah Komentar', - style: TextStyle( - fontSize: 20, - color: Colors.white, - fontWeight: FontWeight.bold), - ), - ], - ), + ), + Container( + key: const Key('Tambah Komentar'), + padding: const EdgeInsets.only(top: doubleSpace), + alignment: Alignment.center, + child: ButtonTheme( + minWidth: double.infinity, + height: 40, + child: ElevatedButton( + key: const Key('Button Tambah Komentar'), + style: ButtonStyle( + padding: MaterialStateProperty.all( + EdgeInsets.symmetric(vertical: 13)), + elevation: MaterialStateProperty.all(0.0), + backgroundColor: + MaterialStateProperty.all(greenPrimary), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10)))), ), - )), - ], - )), - ], - ), + onPressed: () { + _checkLoginStatus(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.add, + size: 30, + color: Colors.white, + ), + const SizedBox(width: 5.0), + const Text( + 'Tambah Komentar', + style: TextStyle( + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold), + ), + ], + ), + ), + )), + ], + )), + ], ), - ], - ), + ), + ], ), ), ); diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart index f646e4f..5af4cc2 100644 --- a/lib/page/login/login.dart +++ b/lib/page/login/login.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:bisaGo/bloc/cloud_messaging_bloc.dart'; import 'package:bisaGo/bloc/new_user_bloc.dart'; import 'package:bisaGo/bloc/user_bloc.dart'; import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/page/dashboard/dashboard.dart'; import 'package:bisaGo/page/login/pilih_disabilitas.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -35,6 +38,10 @@ class LoginState extends State { GoogleSignInAccount _currentUser; SharedPreferences sharedPreferences; + FirebaseMessaging _firebaseMessaging; + + CloudMessagingBloc cloudMessagingBloc = CloudMessagingBloc(); + @override void initState() { super.initState(); @@ -292,6 +299,8 @@ class LoginState extends State { ..setString('token', tokenMap['token']) ..setString('email', email); }); + await _requestFCMToken(); + successDialog(context); _navigateToDashboard(context); } else { @@ -384,6 +393,19 @@ class LoginState extends State { } } + Future _requestFCMToken() async { + await Firebase.initializeApp(); + + _firebaseMessaging = FirebaseMessaging.instance; + + final fcmToken = await _firebaseMessaging.getToken(); + final sharedPreferences = await SharedPreferences.getInstance(); + final token = sharedPreferences.getString('token'); + if (token != null) { + await cloudMessagingBloc.sendFCMToken(fcmToken, token); + } + } + Future _handleSignOut() async { await _googleSignIn.signOut(); } diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index f1804b0..a186384 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'package:bisaGo/component/image_holder.dart'; import 'package:dio/dio.dart'; //import 'package:bisaGo/bloc/user_bloc.dart'; @@ -23,6 +24,7 @@ class EditProfile extends StatefulWidget { final UserModel user; const EditProfile({@required this.user, Key key}) : super(key: key); + @override _EditProfileState createState() => _EditProfileState(user); } @@ -31,9 +33,11 @@ class _EditProfileState extends State { final GlobalKey _formKey = GlobalKey(); UserModel user; NewUserBloc bloc; + //UserBloc bloc; final picker = ImagePicker(); File _image; + String _imageUrl; bool _rahasiakanData; Future _getGalleryImage() async { @@ -50,6 +54,7 @@ class _EditProfileState extends State { Future _clearImage() async { setState(() { _image = null; + _imageUrl = null; }); } @@ -70,6 +75,7 @@ class _EditProfileState extends State { jenisDisabilitasValue = user.disabilitas; pekerjaanValue = user.pekerjaan; _rahasiakanData = user.seen; + _imageUrl = user.foto; } @override @@ -126,13 +132,21 @@ class _EditProfileState extends State { height: 100, child: Image.file(_image, fit: BoxFit.cover), ) - : Text(user.name.substring(0, 1), - style: const TextStyle( - fontSize: 45, - fontWeight: FontWeight.w900, - color: darkGreen, - fontFamily: 'Comfortaa', - )), + : ((_imageUrl) == null + ? Text(user.name.substring(0, 1), + style: const TextStyle( + fontSize: 45, + fontWeight: FontWeight.w900, + color: darkGreen, + fontFamily: 'Comfortaa', + )) + : SizedBox( + width: 100, + height: 100, + child: ImageHolder( + url: _imageUrl, + ), + )), ), ), Padding( @@ -354,9 +368,11 @@ class _EditProfileState extends State { } Future _validateLoginInput() async { - dynamic fotoValidate = ''; + var validatedPhoto = ''; if (_image != null) { - fotoValidate = _image.path; + validatedPhoto = _image.path; + } else if (_imageUrl == null) { + validatedPhoto = null; } final form = _formKey.currentState; @@ -371,7 +387,7 @@ class _EditProfileState extends State { disabilitas: jenisDisabilitasValue, pekerjaan: pekerjaanValue ?? '-', alamat: alamatController.text.toString(), - foto: fotoValidate, + foto: validatedPhoto, seen: _rahasiakanData, organisasiKomunitas: organisasiController.text, ); @@ -385,20 +401,20 @@ class _EditProfileState extends State { } //await updateUser(newUser); - await updatePPUser(); + await updatePPUser(newUserData); } else { failedDialog(context); } } - Future updatePPUser() async { + Future updatePPUser(Map newUserData) async { bloc = NewUserBloc(); final response = await bloc.updateUserPFP(newUserData); if (response.statusCode == 200) { successDialog(context); Timer(const Duration(seconds: 2), () { //_navigateToProfile(context, newUser); - _navigateToProfile(context); + _navigateToProfile(context, newUserData['email']); }); } else { failedDialog(context); @@ -430,20 +446,9 @@ class _EditProfileState extends State { }); } - void _navigateToProfile(BuildContext context) { - final userNew = UserModel( - username: newUserData['email'], - name: newUserData['name'], - email: newUserData['email'], - tanggalLahir: newUserData['tanggalLahir'], - phoneNumber: newUserData['phoneNumber'], - jenisKelamin: newUserData['jenisKelamin'], - disabilitas: newUserData['disabilitas'], - pekerjaan: newUserData['pekerjaan'], - alamat: newUserData['alamat'], - foto: newUserData['foto']); + void _navigateToProfile(BuildContext context, String email) { final route = - MaterialPageRoute(builder: (_) => Profile(email: userNew.email)); + MaterialPageRoute(builder: (_) => Profile(email: email)); Navigator.of(context).pop(Navigator.pop(context)); Navigator.pop(context); Navigator.of(context).push(route); diff --git a/lib/repository/cloud_messaging_repository.dart b/lib/repository/cloud_messaging_repository.dart new file mode 100644 index 0000000..49ea45c --- /dev/null +++ b/lib/repository/cloud_messaging_repository.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:bisaGo/flavor/flavor.dart'; + +abstract class BaseCloudMessagingRepository { + Future sendFCMToken(String fcmToken, String token); +} + +class CloudMessagingRepository implements BaseCloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + try { + await http.post( + '${ApiFlavor.getBaseUrl()}/notification/', + headers: { + 'Authorization': 'token $token', + 'content-type': 'application/json' + }, + body: json.encode({ + 'token': fcmToken, + 'type': 'android', + }), + ); + return true; + } catch (_) { + return false; + } + } +} diff --git a/lib/repository/kegiatan_repository.dart b/lib/repository/kegiatan_repository.dart index e8fdb53..b1f969a 100644 --- a/lib/repository/kegiatan_repository.dart +++ b/lib/repository/kegiatan_repository.dart @@ -55,10 +55,12 @@ class KegiatanRepository implements BaseKegiatanRepository { ); return response; } - + @override - Future fetchDetailKegiatan(String placeId, int kegiatanId) async { - final url = '/informasi-fasilitas/lokasi/detail-kegiatan/$placeId/$kegiatanId/'; + Future fetchDetailKegiatan( + String placeId, int kegiatanId) async { + final url = + '/informasi-fasilitas/lokasi/detail-kegiatan/$placeId/$kegiatanId/'; final response = await _network.get(url: url, isLogin: false); var kegiatan = KegiatanModel.fromJson(response); kegiatan.image = await fetchImages(kegiatan.placeId, kegiatan.id); diff --git a/pubspec.yaml b/pubspec.yaml index 59f1b2b..14d76cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,7 +55,9 @@ dependencies: firebase_core: ^0.7.0 firebase_core_platform_interface: ^3.0.1 firebase_dynamic_links: ^0.7.0+1 + firebase_messaging: ^8.0.0-dev.15 carousel_slider: ^3.0.0 + flushbar: ^1.10.4 dev_dependencies: flutter_test: diff --git a/test/cloud_messaging_test.dart b/test/cloud_messaging_test.dart new file mode 100644 index 0000000..0f41541 --- /dev/null +++ b/test/cloud_messaging_test.dart @@ -0,0 +1,26 @@ +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mockito/mockito.dart'; + +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + +void main() { + setUpAll(() { + final _getIt = GetIt.instance; + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); + }); + testWidgets('Generate fcm token', (WidgetTester tester) async { + final result = + await MockCloudMessagingRepository().sendFCMToken('fcmToken', 'token'); + + expect(result, true); + }); +} diff --git a/test/custom_kegiatan_terdekat_button_test.dart b/test/custom_kegiatan_terdekat_button_test.dart index b59a76e..c8b5511 100644 --- a/test/custom_kegiatan_terdekat_button_test.dart +++ b/test/custom_kegiatan_terdekat_button_test.dart @@ -1,6 +1,7 @@ import 'package:bisaGo/model/kegiatan.dart'; import 'package:bisaGo/model/lokasi.dart'; import 'package:bisaGo/page/dashboard/dashboard.dart'; +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/kegiatan_terdekat_repository.dart'; import 'package:bisaGo/repository/lokasi_repository.dart'; import 'package:flutter/material.dart'; @@ -41,6 +42,14 @@ class MockLokasi extends Fake implements LokasiRepository { } } +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + void main() { // final mockLokasi = { // 'name': 'Margo City', @@ -68,6 +77,8 @@ void main() { _getIt.registerLazySingleton( () => MockKegiatanTerdekat()); _getIt.registerLazySingleton(() => MockLokasi()); + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); }); testWidgets('Detail Post Kegiatan Page - Positive Test', diff --git a/test/login_test.dart b/test/login_test.dart index dbea2e8..2573a52 100644 --- a/test/login_test.dart +++ b/test/login_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bisaGo/model/user.dart'; +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/user_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,6 +14,14 @@ import 'package:shared_preferences/shared_preferences.dart'; class MockNetwork extends Mock implements NetworkInterface {} +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + class MockUserRepository extends Fake implements UserRepository { final userData = { 'is_login': true, @@ -41,6 +50,9 @@ void main() { .registerLazySingleton(() => MockUserRepository()); SharedPreferences.setMockInitialValues( {'email': 'test@gmail.com', 'token': 'token'}); + + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); // mockNetwork = MockNetwork(); // when(mockNetwork.get(isLogin: false, url: anyNamed('url'))) // .thenAnswer((_) async { diff --git a/test/mock_test.dart b/test/mock_test.dart index 032df7c..32c7466 100644 --- a/test/mock_test.dart +++ b/test/mock_test.dart @@ -1,4 +1,5 @@ import 'package:bisaGo/model/kegiatan.dart'; +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/kegiatan_terdekat_repository.dart'; import 'package:bisaGo/repository/lokasi_repository.dart'; import 'package:flutter/material.dart'; @@ -45,6 +46,14 @@ class MockKegiatanTerdekatRepository extends Fake } } +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + void main() { group('Dashboard navigation tests', () { NavigatorObserver mockObserver; @@ -74,6 +83,8 @@ void main() { () => MockLokasiRepository()); _getIt.registerLazySingleton( () => MockKegiatanTerdekatRepository()); + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); }); Future _buildDashboardPage(WidgetTester tester) async { diff --git a/test/model_test.dart b/test/model_test.dart index 9261956..8623d86 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -9,15 +9,15 @@ void main() { 'id': 1, 'deskripsi': 'This is a test', 'creator': 'Test', - 'date_time': '2020-11-18 00:13:52.939668', - 'creator_email': 'test@email.com' + 'creator_email': 'test@email.com', + 'created': '18-11-2020 00:13:52' }; final returnKomentarPostingData = { 'id': 1, 'deskripsi': 'This is a test', 'creator': 'Test', - 'date_time': '2020-11-18T00:13:00.000', - 'creator_email': 'test@email.com' + 'creator_email': 'test@email.com', + 'created': '2020-11-18T00:13:00.000' }; final userData = { 'is_login': true, @@ -42,7 +42,7 @@ void main() { 'deskripsi': 'Ada toilet khusus disabilitas terletak di lantai 2 dekat kintan', 'creator': '', - 'date_time': '2020-11-18 00:13:52.939668', + 'date_time': '18-11-2020 00:13:52', 'rating': 3, 'tag': 'KR', 'disabilitas': ['DF'], @@ -86,9 +86,8 @@ void main() { id: 2, deskripsi: 'This is a test', creator: 'Test', - dateTime: DateTime.now(), creatorEmail: 'test@email.com', - ); + created: DateTime.now()); expect(komentarPostingModel, isInstanceOf()); expect( komentarPostingWithConstructor, isInstanceOf()); diff --git a/test/widget_test.dart b/test/widget_test.dart index bac647f..69c7052 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -2,6 +2,7 @@ import 'package:bisaGo/model/kegiatan.dart'; import 'package:bisaGo/model/komunitas.dart'; import 'package:bisaGo/model/lokasi.dart'; import 'package:bisaGo/model/sekolah.dart'; +import 'package:bisaGo/repository/cloud_messaging_repository.dart'; import 'package:bisaGo/repository/kegiatan_terdekat_repository.dart'; import 'package:bisaGo/repository/komunitas_repository.dart'; import 'package:bisaGo/repository/lokasi_repository.dart'; @@ -88,6 +89,14 @@ class MockKegiatanTerdekatRepository extends Fake } } +class MockCloudMessagingRepository extends Fake + implements CloudMessagingRepository { + @override + Future sendFCMToken(String fcmToken, String token) async { + return Future.value(true); + } +} + void main() { setUpAll(() { final _getIt = GetIt.instance; @@ -99,6 +108,8 @@ void main() { () => MockLokasiRepository()); _getIt.registerLazySingleton( () => MockKegiatanTerdekatRepository()); + _getIt.registerLazySingleton( + () => MockCloudMessagingRepository()); }); testWidgets('finds a text field in dashboard', (WidgetTester tester) async { final containerTextField = Key('Container Text Field'); -- GitLab From 7de7982a5bef6c73b20ee82b41d2697ecd89eba3 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Tue, 1 Jun 2021 00:56:41 +0700 Subject: [PATCH 32/45] [CHORE] fix widget.komentar.deskripsi on creator --- lib/page/filter_fasilitas/postingan/detail_post.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index 648ba07..6f57b2d 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -253,9 +253,14 @@ class _DetailPostPageState extends State { ); }, child: Text( - widget.komentar.deskripsi, - key: const Key('Text Cara Menggunakan'), - style: const TextStyle(fontSize: 16), + '${widget.komentar.creator} ', + key: Key('creator-${widget.komentar.creator}'), + overflow: TextOverflow.fade, + softWrap: false, + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), ), ), ), -- GitLab From f0b6c945a45da33e6efd73b4439e91c7d01978cb Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Tue, 1 Jun 2021 01:04:58 +0700 Subject: [PATCH 33/45] [CHORE] changelogs 3.5.0 --- android/fastlane/metadata/android/id/changelogs/changelogs.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/fastlane/metadata/android/id/changelogs/changelogs.txt b/android/fastlane/metadata/android/id/changelogs/changelogs.txt index 6264239..ba07351 100644 --- a/android/fastlane/metadata/android/id/changelogs/changelogs.txt +++ b/android/fastlane/metadata/android/id/changelogs/changelogs.txt @@ -1,3 +1,6 @@ +3.5.0: +- Pengguna dapat mengganti foto profil + 3.4.0: - Notifikasi pengguna untuk komentar baru -- GitLab From 26b89a4465dffb11ea6eec9d5ca9e2500355b6f4 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Wed, 2 Jun 2021 19:42:31 +0700 Subject: [PATCH 34/45] [CHORES] refactors and fix bugs: - fix bug select dissability remove profile picture - removed new_user_bloc (duplicate with user_bloc) and NewUser model - added new model DetailUserModel, RegisterUserModel, UpdateUsermodel - now shared preferences store user json with key 'user' --- lib/bloc/new_user_bloc.dart | 48 ------ lib/bloc/user_bloc.dart | 50 ++++--- lib/component/bisago_drawer.dart | 5 +- lib/model/new_user.dart | 41 ------ lib/model/new_user.g.dart | 39 ----- lib/model/user.dart | 139 ++++++++++++++---- lib/model/user.g.dart | 98 ++++++++---- lib/network/network_interface.dart | 30 ++-- .../postingan/detail_post_kegiatan.dart | 1 - lib/page/login/login.dart | 88 ++++------- lib/page/login/pilih_disabilitas.dart | 30 ++-- lib/page/profile/edit_profile.dart | 105 +++++++------ lib/page/profile/profile.dart | 60 +++++--- lib/page/registrasi/registrasi.dart | 54 +++---- lib/repository/user_repository.dart | 43 ++---- lib/utils/datetime_utils.dart | 27 ++++ test/login_test.dart | 6 +- test/model_test.dart | 17 +-- test/pilih_disabilitas_test.dart | 20 +-- test/profile_test.dart | 55 +++---- test/registrasi_test.dart | 11 +- test/user_test.dart | 10 +- 22 files changed, 477 insertions(+), 500 deletions(-) delete mode 100644 lib/bloc/new_user_bloc.dart delete mode 100644 lib/model/new_user.dart delete mode 100644 lib/model/new_user.g.dart create mode 100644 lib/utils/datetime_utils.dart diff --git a/lib/bloc/new_user_bloc.dart b/lib/bloc/new_user_bloc.dart deleted file mode 100644 index c030b70..0000000 --- a/lib/bloc/new_user_bloc.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:async'; - -import 'package:bisaGo/model/new_user.dart'; -import 'package:bisaGo/network/data/network_model.dart'; -import 'package:bisaGo/repository/user_repository.dart'; -import 'package:get_it/get_it.dart'; -import 'package:http/http.dart'; - -class NewUserBloc { - UserRepository _userRepository; - StreamController _userController; - - NewUserBloc() { - _userController = StreamController>(); - _userRepository = GetIt.instance.get(); - } - - Future registerNewUser(NewUser newUser) async { - try { - await _userRepository.createUser(newUser); - return true; - } catch (_) { - return false; - } - } - - Future updateUser(NewUser newUser) async { - try { - await _userRepository.updateUser(newUser); - return true; - } catch (_) { - return false; - } - } - - Future updateUserPFP(Map newUserData) async { - try { - await _userRepository.updateUserProfile(newUserData); - return Response('Success', 200); - } catch (_) { - return Response('Failed to update user', 400); - } - } - - void dispose() { - _userController?.close(); - } -} diff --git a/lib/bloc/user_bloc.dart b/lib/bloc/user_bloc.dart index 8d4c38d..de113fa 100644 --- a/lib/bloc/user_bloc.dart +++ b/lib/bloc/user_bloc.dart @@ -4,49 +4,55 @@ import 'package:bisaGo/network/data/network_model.dart'; import 'package:bisaGo/repository/user_repository.dart'; import 'package:bisaGo/model/user.dart'; import 'package:get_it/get_it.dart'; -import 'package:http/http.dart'; class UserBloc { UserRepository _userRepository; StreamController _userController; - List userFromApi; + String email; - StreamSink> get userSink => _userController.sink; - Stream> get userStream => _userController.stream; + StreamSink> get userSink => + _userController.sink; - UserBloc(String email) { - _userController = StreamController>(); + Stream> get userStream => + _userController.stream; + + UserBloc({this.email}) { + _userController = StreamController>(); _userRepository = GetIt.instance.get(); - fetchUserDetail(email); } - Future fetchUserDetail(String email) async { + Future fetchUserDetail() async { userSink.add(NetworkModel.loading('Getting user')); + final response = await getUserDetail(email); + if (response is DetailUserModel) { + userSink.add(NetworkModel.completed(response)); + } else { + userSink.add(NetworkModel.error(response.toString())); + } + } + + Future getUserDetail(String email) async { try { - final userResponse = await _userRepository.fetchUserDetail(email); - userFromApi = List.from(userResponse.user); - userSink.add(NetworkModel.completed(userResponse)); + return await _userRepository.fetchUserDetail(email); } catch (e) { - if (!_userController.isClosed) { - userSink.add(NetworkModel.error(e.toString())); - } + return e; } } - Future fetchUser(String email) async { + Future updateUser(UpdateUserModel updateUser) async { try { - await fetchUserDetail(email); - return userFromApi.first; - } catch (e) { + return await _userRepository.updateUser(updateUser, email); + } catch (_) { return null; } } - Future updateUserProfile(Map newUserData) async { + Future registerNewUser(RegisterUserModel registerUserModel) async { try { - return await _userRepository.updateUserProfile(newUserData); - } catch (e) { - return Response('Failed to update user', 400); + await _userRepository.registerUser(registerUserModel); + return true; + } catch (_) { + return false; } } diff --git a/lib/component/bisago_drawer.dart b/lib/component/bisago_drawer.dart index 7e45592..05a78c6 100644 --- a/lib/component/bisago_drawer.dart +++ b/lib/component/bisago_drawer.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:bisaGo/page/informasi/list_sekolah.dart'; import 'package:bisaGo/page/profile/profile.dart'; import 'package:bisaGo/page/tentang_disabilitas/tentang_disabilitas.dart'; @@ -168,7 +170,8 @@ class BisaGoDrawer extends StatelessWidget { if (sharedPreferences.getString('token') == null) { return 'Selamat datang ke BisaGo!'; } else { - return sharedPreferences.getString('email'); + final userJson = jsonDecode(sharedPreferences.getString('user')); + return userJson['email']; } } diff --git a/lib/model/new_user.dart b/lib/model/new_user.dart deleted file mode 100644 index 6e2c960..0000000 --- a/lib/model/new_user.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; -part 'new_user.g.dart'; - -@JsonSerializable() -class NewUser { - String name; - String password; - String email; - @JsonKey(name: 'tanggal_lahir') - String tanggalLahir; - @JsonKey(name: 'jenis_kelamin') - String jenisKelamin; - String disabilitas; - String pekerjaan; - String alamat; - @JsonKey(name: 'phone_number') - String phoneNumber; - String foto; - bool seen; - @JsonKey(name: 'organisasi_komunitas') - String organisasiKomunitas; - - NewUser({ - this.name, - this.password, - this.email, - this.tanggalLahir, - this.jenisKelamin, - this.disabilitas, - this.pekerjaan, - this.alamat, - this.phoneNumber, - this.foto, - this.seen, - this.organisasiKomunitas, - }); - - factory NewUser.fromJson(Map json) => - _$NewUserFromJson(json); - Map toJson() => _$NewUserToJson(this); -} diff --git a/lib/model/new_user.g.dart b/lib/model/new_user.g.dart deleted file mode 100644 index 0899a76..0000000 --- a/lib/model/new_user.g.dart +++ /dev/null @@ -1,39 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'new_user.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -NewUser _$NewUserFromJson(Map json) { - return NewUser( - name: json['name'] as String, - password: json['password'] as String, - email: json['email'] as String, - tanggalLahir: json['tanggal_lahir'] as String, - jenisKelamin: json['jenis_kelamin'] as String, - disabilitas: json['disabilitas'] as String, - pekerjaan: json['pekerjaan'] as String, - alamat: json['alamat'] as String, - phoneNumber: json['phone_number'] as String, - foto: json['foto'] as String, - seen: json['seen'] as bool, - organisasiKomunitas: json['organisasi_komunitas'] as String, - ); -} - -Map _$NewUserToJson(NewUser instance) => { - 'name': instance.name, - 'password': instance.password, - 'email': instance.email, - 'tanggal_lahir': instance.tanggalLahir, - 'jenis_kelamin': instance.jenisKelamin, - 'disabilitas': instance.disabilitas, - 'pekerjaan': instance.pekerjaan, - 'alamat': instance.alamat, - 'phone_number': instance.phoneNumber, - 'foto': instance.foto, - 'seen': instance.seen, - 'organisasi_komunitas': instance.organisasiKomunitas, - }; diff --git a/lib/model/user.dart b/lib/model/user.dart index 4840005..8118c25 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -1,18 +1,9 @@ import 'package:json_annotation/json_annotation.dart'; -part 'user.g.dart'; -@JsonSerializable() -class User { - final List user; - User(this.user); -} +part 'user.g.dart'; -@JsonSerializable() -class UserModel { - bool is_login; - String username; +abstract class BaseUserModel { String name; - String email; @JsonKey(name: 'tanggal_lahir') String tanggalLahir; @JsonKey(name: 'phone_number') @@ -22,32 +13,130 @@ class UserModel { String disabilitas; String pekerjaan; String alamat; - String token; - String foto; - bool seen; - @JsonKey(name: 'hidden_fields') - List hiddenFields; + dynamic foto; @JsonKey(name: 'organisasi_komunitas') String organisasiKomunitas; - UserModel({ - this.is_login, - this.username, + BaseUserModel({ this.name, - this.email, this.tanggalLahir, this.phoneNumber, this.jenisKelamin, this.disabilitas, this.pekerjaan, this.alamat, - this.token, this.foto, - this.seen, this.organisasiKomunitas, }); +} + +@JsonSerializable() +class RegisterUserModel extends BaseUserModel { + String email; + String password; + + RegisterUserModel({ + this.email, + this.password, + name, + tanggalLahir, + phoneNumber, + jenisKelamin, + disabilitas, + pekerjaan, + alamat, + foto, + organisasiKomunitas, + }) : super( + name: name, + tanggalLahir: tanggalLahir, + phoneNumber: phoneNumber, + jenisKelamin: jenisKelamin, + disabilitas: disabilitas, + pekerjaan: pekerjaan, + alamat: alamat, + foto: foto, + organisasiKomunitas: organisasiKomunitas); + + factory RegisterUserModel.fromJson(Map json) => + _$RegisterUserModelFromJson(json); + + Map toJson() => _$RegisterUserModelToJson(this); +} + +@JsonSerializable() +class UpdateUserModel extends BaseUserModel { + bool seen; + + UpdateUserModel({ + this.seen, + name, + tanggalLahir, + phoneNumber, + jenisKelamin, + disabilitas, + pekerjaan, + alamat, + foto, + organisasiKomunitas, + }) : super( + name: name, + tanggalLahir: tanggalLahir, + phoneNumber: phoneNumber, + jenisKelamin: jenisKelamin, + disabilitas: disabilitas, + pekerjaan: pekerjaan, + alamat: alamat, + foto: foto, + organisasiKomunitas: organisasiKomunitas); + + factory UpdateUserModel.fromJson(Map json) => + _$UpdateUserModelFromJson(json); + + Map toJson() => _$UpdateUserModelToJson(this); +} + +@JsonSerializable() +class DetailUserModel extends BaseUserModel { + String username; + String email; + bool seen; + @JsonKey(name: 'hidden_fields') + List hiddenFields; + + DetailUserModel({ + this.username, + this.email, + this.seen, + name, + tanggalLahir, + phoneNumber, + jenisKelamin, + disabilitas, + pekerjaan, + alamat, + foto, + organisasiKomunitas, + }) : super( + name: name, + tanggalLahir: tanggalLahir, + phoneNumber: phoneNumber, + jenisKelamin: jenisKelamin, + disabilitas: disabilitas, + pekerjaan: pekerjaan, + alamat: alamat, + foto: foto, + organisasiKomunitas: organisasiKomunitas); + + UpdateUserModel toUpdateUserModel() { + var thisData = toJson(); + thisData.remove('username'); + thisData.remove('email'); + return UpdateUserModel.fromJson(thisData); + } + + factory DetailUserModel.fromJson(Map json) => + _$DetailUserModelFromJson(json); - factory UserModel.fromJson(Map json) => - _$UserModelFromJson(json); - Map toJson() => _$UserModelToJson(this); + Map toJson() => _$DetailUserModelToJson(this); } diff --git a/lib/model/user.g.dart b/lib/model/user.g.dart index d5be823..f4aac33 100644 --- a/lib/model/user.g.dart +++ b/lib/model/user.g.dart @@ -6,53 +6,97 @@ part of 'user.dart'; // JsonSerializableGenerator // ************************************************************************** -User _$UserFromJson(Map json) { - return User( - (json['user'] as List) - ?.map((e) => - e == null ? null : UserModel.fromJson(e as Map)) - ?.toList(), +RegisterUserModel _$RegisterUserModelFromJson(Map json) { + return RegisterUserModel( + email: json['email'] as String, + password: json['password'] as String, + name: json['name'], + tanggalLahir: json['tanggal_lahir'], + phoneNumber: json['phone_number'], + jenisKelamin: json['jenis_kelamin'], + disabilitas: json['disabilitas'], + pekerjaan: json['pekerjaan'], + alamat: json['alamat'], + foto: json['foto'], + organisasiKomunitas: json['organisasi_komunitas'], ); } -Map _$UserToJson(User instance) => { - 'user': instance.user, +Map _$RegisterUserModelToJson(RegisterUserModel instance) => + { + 'name': instance.name, + 'tanggal_lahir': instance.tanggalLahir, + 'phone_number': instance.phoneNumber, + 'jenis_kelamin': instance.jenisKelamin, + 'disabilitas': instance.disabilitas, + 'pekerjaan': instance.pekerjaan, + 'alamat': instance.alamat, + 'foto': instance.foto, + 'organisasi_komunitas': instance.organisasiKomunitas, + 'email': instance.email, + 'password': instance.password, }; -UserModel _$UserModelFromJson(Map json) { - return UserModel( - is_login: json['is_login'] as bool, +UpdateUserModel _$UpdateUserModelFromJson(Map json) { + return UpdateUserModel( + seen: json['seen'] as bool, + name: json['name'], + tanggalLahir: json['tanggal_lahir'], + phoneNumber: json['phone_number'], + jenisKelamin: json['jenis_kelamin'], + disabilitas: json['disabilitas'], + pekerjaan: json['pekerjaan'], + alamat: json['alamat'], + foto: json['foto'], + organisasiKomunitas: json['organisasi_komunitas'], + ); +} + +Map _$UpdateUserModelToJson(UpdateUserModel instance) => + { + 'name': instance.name, + 'tanggal_lahir': instance.tanggalLahir, + 'phone_number': instance.phoneNumber, + 'jenis_kelamin': instance.jenisKelamin, + 'disabilitas': instance.disabilitas, + 'pekerjaan': instance.pekerjaan, + 'alamat': instance.alamat, + 'foto': instance.foto, + 'organisasi_komunitas': instance.organisasiKomunitas, + 'seen': instance.seen, + }; + +DetailUserModel _$DetailUserModelFromJson(Map json) { + return DetailUserModel( username: json['username'] as String, - name: json['name'] as String, email: json['email'] as String, - tanggalLahir: json['tanggal_lahir'] as String, - phoneNumber: json['phone_number'] as String, - jenisKelamin: json['jenis_kelamin'] as String, - disabilitas: json['disabilitas'] as String, - pekerjaan: json['pekerjaan'] as String, - alamat: json['alamat'] as String, - token: json['token'] as String, - foto: json['foto'] as String, seen: json['seen'] as bool, - organisasiKomunitas: json['organisasi_komunitas'] as String, + name: json['name'], + tanggalLahir: json['tanggal_lahir'], + phoneNumber: json['phone_number'], + jenisKelamin: json['jenis_kelamin'], + disabilitas: json['disabilitas'], + pekerjaan: json['pekerjaan'], + alamat: json['alamat'], + foto: json['foto'], + organisasiKomunitas: json['organisasi_komunitas'], )..hiddenFields = (json['hidden_fields'] as List)?.map((e) => e as String)?.toList(); } -Map _$UserModelToJson(UserModel instance) => { - 'is_login': instance.is_login, - 'username': instance.username, +Map _$DetailUserModelToJson(DetailUserModel instance) => + { 'name': instance.name, - 'email': instance.email, 'tanggal_lahir': instance.tanggalLahir, 'phone_number': instance.phoneNumber, 'jenis_kelamin': instance.jenisKelamin, 'disabilitas': instance.disabilitas, 'pekerjaan': instance.pekerjaan, 'alamat': instance.alamat, - 'token': instance.token, 'foto': instance.foto, + 'organisasi_komunitas': instance.organisasiKomunitas, + 'username': instance.username, + 'email': instance.email, 'seen': instance.seen, 'hidden_fields': instance.hiddenFields, - 'organisasi_komunitas': instance.organisasiKomunitas, }; diff --git a/lib/network/network_interface.dart b/lib/network/network_interface.dart index 6fe64a3..8bf0898 100644 --- a/lib/network/network_interface.dart +++ b/lib/network/network_interface.dart @@ -7,6 +7,10 @@ import 'package:bisaGo/flavor/flavor.dart'; import 'package:shared_preferences/shared_preferences.dart'; class NetworkInterface { + NetworkInterface() { + dio.options.headers['content-type'] = 'application/json'; + } + Future post({ String url, Map bodyParams, @@ -15,12 +19,7 @@ class NetworkInterface { }) async { var responseJson; try { - if (isLogin) { - final sharedPreferences = await SharedPreferences.getInstance(); - dio.options.headers['Authorization'] = - 'Token ${sharedPreferences.getString('token')}'; - dio.options.headers['content-type'] = 'application/json'; - } + await setToken(isLogin); final response = await dio.post( '${ApiFlavor.getBaseUrl()}$url', data: formData ? FormData.fromMap(bodyParams) : json.encode(bodyParams), @@ -42,12 +41,7 @@ class NetworkInterface { }) async { var responseJson; try { - if (isLogin) { - final sharedPreferences = await SharedPreferences.getInstance(); - dio.options.headers['Authorization'] = - 'Token ${sharedPreferences.getString('token')}'; - } - dio.options.headers['content-type'] = 'application/json'; + await setToken(isLogin); final response = await dio.put( '${ApiFlavor.getBaseUrl()}$url', data: formData ? FormData.fromMap(bodyParams) : json.encode(bodyParams), @@ -69,7 +63,7 @@ class NetworkInterface { }) async { var responseJson; try { - dio.options.headers['content-type'] = 'application/json'; + await setToken(isLogin); final response = await dio.get( '${ApiFlavor.getBaseUrl()}$url', ); @@ -82,6 +76,16 @@ class NetworkInterface { return responseJson; } + Future setToken(bool isLogin) async { + if (isLogin) { + final sharedPreferences = await SharedPreferences.getInstance(); + dio.options.headers['Authorization'] = + 'Token ${sharedPreferences.getString('token')}'; + } else { + dio.options.headers.remove('Authorization'); + } + } + dynamic _response(Response response) { switch (response.statusCode) { case 200: diff --git a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart index 08ded62..2054000 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart @@ -46,7 +46,6 @@ class _DetailPostKegiatanPageState extends State { @override Widget build(BuildContext context) { - print('asd ${widget.kegiatan.toJson()}'); return Scaffold( appBar: BisaGoAppBar( title: widget.lokasi.name, diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart index 5af4cc2..438b33f 100644 --- a/lib/page/login/login.dart +++ b/lib/page/login/login.dart @@ -1,13 +1,9 @@ import 'dart:async'; import 'dart:convert'; -import 'package:bisaGo/bloc/cloud_messaging_bloc.dart'; -import 'package:bisaGo/bloc/new_user_bloc.dart'; import 'package:bisaGo/bloc/user_bloc.dart'; -import 'package:bisaGo/model/new_user.dart'; +import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/dashboard/dashboard.dart'; import 'package:bisaGo/page/login/pilih_disabilitas.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -38,9 +34,11 @@ class LoginState extends State { GoogleSignInAccount _currentUser; SharedPreferences sharedPreferences; - FirebaseMessaging _firebaseMessaging; - - CloudMessagingBloc cloudMessagingBloc = CloudMessagingBloc(); + UserBloc _userBloc; + UpdateUserModel updateUser; + String email; + TextEditingController emailController = TextEditingController(); + TextEditingController passwordController = TextEditingController(); @override void initState() { @@ -174,7 +172,7 @@ class LoginState extends State { const EdgeInsets.symmetric(vertical: 10.0)), onPressed: () async { Navigator.of(context).pop(true); - await _updateUser(newUser); + await _updateUser(updateUser); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -226,57 +224,27 @@ class LoginState extends State { ); } - Future _getUserDetail() async { - _userBloc = UserBloc(emailController.text.toString()); - await _userBloc.fetchUserDetail(emailController.text.toString()); - final response = _userBloc.userFromApi.first; - if (response.disabilitas == '-') { - newUser = NewUser( - name: response.name, - password: '', - email: emailController.text.toString(), - phoneNumber: response.phoneNumber, - tanggalLahir: response.tanggalLahir, - jenisKelamin: response.jenisKelamin, - disabilitas: 'Tidak memiliki disabilitas', - pekerjaan: response.pekerjaan, - alamat: response.alamat); - await login(emailController.text.toString(), - passwordController.text.toString(), '', '', ''); - _popUpDisabilitas(); - } else { - await login(emailController.text.toString(), - passwordController.text.toString(), '', '', ''); - } - } - - Future _updateUser(NewUser newUser) async { - _newUserBloc = NewUserBloc(); - final response = await _newUserBloc.updateUser(newUser); - if (response) { + Future _updateUser(UpdateUserModel updateUserModel) async { + final updatedUser = await _userBloc.updateUser(updateUserModel); + if (updatedUser != null) { successUserUpdateDialog(context); } else { failedUserUpdateDialog(context); } - _newUserBloc.dispose(); } Future _validateLoginInput() async { final form = _formKey.currentState; if (_formKey.currentState.validate()) { form.save(); - await _getUserDetail(); + await login(emailController.text.toString(), + passwordController.text.toString(), '', '', ''); } } - NewUserBloc _newUserBloc; - UserBloc _userBloc; - NewUser newUser; - TextEditingController emailController = TextEditingController(); - TextEditingController passwordController = TextEditingController(); - Future login(String email, String password, String google, String accessToken, String name) async { + _userBloc = UserBloc(email: email); var data = {}; if (google.isNotEmpty) { data = { @@ -294,23 +262,30 @@ class LoginState extends State { .post('${ApiFlavor.getBaseUrl()}/api-token-auth/', body: data); if (response.statusCode == 200) { final tokenMap = jsonDecode(response.body); + final loginUser = DetailUserModel.fromJson(tokenMap['user']); + this.email = loginUser.email; + updateUser = loginUser.toUpdateUserModel(); setState(() { sharedPreferences ..setString('token', tokenMap['token']) - ..setString('email', email); + ..setString('user', jsonEncode(tokenMap['user'])); }); - await _requestFCMToken(); successDialog(context); _navigateToDashboard(context); + if (updateUser.disabilitas == '-') { + updateUser.disabilitas = 'Tidak memiliki disabilitas'; + _popUpDisabilitas(); + } } else { failedDialog(context); } + _userBloc.dispose(); } void _navigateToPilihDisabilitas(BuildContext context) { - final route = - MaterialPageRoute(builder: (_) => PilihDisabilitas(newUser: newUser)); + final route = MaterialPageRoute( + builder: (_) => PilihDisabilitas(updateUser: updateUser, email: email)); Navigator.of(context).push(route); } @@ -321,7 +296,7 @@ class LoginState extends State { void _navigateToDashboard(BuildContext context) { final route = MaterialPageRoute(builder: (_) => const Dashboard()); - Navigator.of(context).push(route); + Navigator.of(context).pushReplacement(route); } void successDialog(BuildContext context) { @@ -393,19 +368,6 @@ class LoginState extends State { } } - Future _requestFCMToken() async { - await Firebase.initializeApp(); - - _firebaseMessaging = FirebaseMessaging.instance; - - final fcmToken = await _firebaseMessaging.getToken(); - final sharedPreferences = await SharedPreferences.getInstance(); - final token = sharedPreferences.getString('token'); - if (token != null) { - await cloudMessagingBloc.sendFCMToken(fcmToken, token); - } - } - Future _handleSignOut() async { await _googleSignIn.signOut(); } diff --git a/lib/page/login/pilih_disabilitas.dart b/lib/page/login/pilih_disabilitas.dart index ac7c034..a117141 100644 --- a/lib/page/login/pilih_disabilitas.dart +++ b/lib/page/login/pilih_disabilitas.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:bisaGo/bloc/new_user_bloc.dart'; -import 'package:bisaGo/model/new_user.dart'; +import 'package:bisaGo/bloc/user_bloc.dart'; +import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/dashboard/dashboard.dart'; import 'package:bisaGo/utils/custom_disabilitas_button.dart'; import 'package:flutter/material.dart'; @@ -10,17 +10,18 @@ import 'package:bisaGo/config/styles.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class PilihDisabilitas extends StatefulWidget { - final NewUser newUser; + final UpdateUserModel updateUser; + final String email; - const PilihDisabilitas({Key key, this.newUser}) : super(key: key); + const PilihDisabilitas({Key key, this.updateUser, this.email}) : super(key: key); @override - _PilihDisabilitasState createState() => _PilihDisabilitasState(newUser); + _PilihDisabilitasState createState() => _PilihDisabilitasState(); } class _PilihDisabilitasState extends State { - _PilihDisabilitasState(this.newUser); - + UserBloc _userBloc; String _disabilitas = ''; + @override Widget build(BuildContext context) { return Scaffold( @@ -116,17 +117,19 @@ class _PilihDisabilitasState extends State { } Future _updateUser() async { - newUser.disabilitas = _disabilitas; - bloc = NewUserBloc(); - final response = await bloc.updateUser(newUser); - if (response) { + var updateUser = widget.updateUser; + updateUser.disabilitas = _disabilitas; + updateUser.foto = ''; + _userBloc = UserBloc(email: widget.email); + final updatedUser = await _userBloc.updateUser(updateUser); + if (updatedUser != null) { successDialog(context); await Future.delayed(Duration(seconds: 2)); _navigateToDashboard(context); } else { failedDialog(context); } - bloc.dispose(); + _userBloc.dispose(); } void successDialog(BuildContext context) { @@ -153,9 +156,6 @@ class _PilihDisabilitasState extends State { }); } - NewUser newUser; - NewUserBloc bloc; - void _navigateToDashboard(BuildContext context) { final route = MaterialPageRoute(builder: (_) => const Dashboard()); Navigator.of(context).push(route); diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index a186384..898c065 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'dart:io'; +import 'package:bisaGo/bloc/user_bloc.dart'; import 'package:bisaGo/component/image_holder.dart'; +import 'package:bisaGo/utils/datetime_utils.dart'; import 'package:dio/dio.dart'; -//import 'package:bisaGo/bloc/user_bloc.dart'; -import 'package:bisaGo/bloc/new_user_bloc.dart'; import 'package:bisaGo/component/bisago_appbar.dart'; import 'package:bisaGo/config/strings.dart'; import 'package:bisaGo/config/styles.dart'; @@ -17,11 +17,10 @@ import 'package:bisaGo/utils/validator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:intl/intl.dart'; import 'package:image_picker/image_picker.dart'; class EditProfile extends StatefulWidget { - final UserModel user; + final DetailUserModel user; const EditProfile({@required this.user, Key key}) : super(key: key); @@ -31,8 +30,21 @@ class EditProfile extends StatefulWidget { class _EditProfileState extends State { final GlobalKey _formKey = GlobalKey(); - UserModel user; - NewUserBloc bloc; + + DetailUserModel detailUserModel; + UpdateUserModel updateUserModel; + UserBloc _bloc; + + TextEditingController nameController = TextEditingController(); + TextEditingController phoneController = TextEditingController(); + TextEditingController emailController = TextEditingController(); + TextEditingController alamatController = TextEditingController(); + TextEditingController organisasiController = TextEditingController(); + + String jenisKelaminValue; + DateTime tanggalLahir; + String jenisDisabilitasValue; + String pekerjaanValue; //UserBloc bloc; final picker = ImagePicker(); @@ -58,24 +70,24 @@ class _EditProfileState extends State { }); } - _EditProfileState(this.user); + _EditProfileState(this.detailUserModel); @override void initState() { super.initState(); - nameController.text = user.name; - phoneController.text = user.phoneNumber; - emailController.text = user.email; - alamatController.text = user.alamat; - organisasiController.text = user.organisasiKomunitas; - jenisKelaminValue = user.jenisKelamin; - tanggalLahir = (user.tanggalLahir == '-' - ? '${DateFormat('yyyy-MM-dd').format(DateTime.now())}' - : user.tanggalLahir); - jenisDisabilitasValue = user.disabilitas; - pekerjaanValue = user.pekerjaan; - _rahasiakanData = user.seen; - _imageUrl = user.foto; + nameController.text = detailUserModel.name; + phoneController.text = detailUserModel.phoneNumber; + emailController.text = detailUserModel.email; + alamatController.text = detailUserModel.alamat; + organisasiController.text = detailUserModel.organisasiKomunitas; + jenisKelaminValue = detailUserModel.jenisKelamin; + tanggalLahir = (detailUserModel.tanggalLahir == '-' + ? DateTime.now() + : usDateFormatter.parse(detailUserModel.tanggalLahir)); + jenisDisabilitasValue = detailUserModel.disabilitas; + pekerjaanValue = detailUserModel.pekerjaan; + _rahasiakanData = detailUserModel.seen; + _imageUrl = detailUserModel.foto; } @override @@ -122,7 +134,7 @@ class _EditProfileState extends State { child: ListBody( children: [ CircleAvatar( - key: Key('Avatar ${user.name.split(' ')[0]}'), + key: Key('Avatar ${detailUserModel.name.split(' ')[0]}'), radius: 50, backgroundColor: white, child: ClipOval( @@ -133,7 +145,7 @@ class _EditProfileState extends State { child: Image.file(_image, fit: BoxFit.cover), ) : ((_imageUrl) == null - ? Text(user.name.substring(0, 1), + ? Text(detailUserModel.name.substring(0, 1), style: const TextStyle( fontSize: 45, fontWeight: FontWeight.w900, @@ -198,14 +210,14 @@ class _EditProfileState extends State { ), ), Switch( - value: _rahasiakanData, + value: !_rahasiakanData, focusColor: Colors.green, hoverColor: Colors.green, activeColor: Colors.green, activeTrackColor: Colors.green, onChanged: (value) { setState(() { - _rahasiakanData = value; + _rahasiakanData = !value; }); }, ), @@ -267,16 +279,15 @@ class _EditProfileState extends State { ), onPressed: () { DatePicker.showDatePicker(context, - currentTime: DateTime.parse(tanggalLahir), + currentTime: tanggalLahir, maxTime: DateTime.now(), onChanged: (date) { setState(() { - tanggalLahir = - '${DateFormat('yyyy-MM-dd').format(date)}'; + tanggalLahir = date; }); }); }, child: Text( - tanggalLahir, + idDateFormatter.format(tanggalLahir), style: const TextStyle( fontSize: 15.0, color: Colors.white), ), @@ -342,19 +353,6 @@ class _EditProfileState extends State { ); } - UserModel newUser; - TextEditingController nameController = TextEditingController(); - TextEditingController phoneController = TextEditingController(); - TextEditingController emailController = TextEditingController(); - TextEditingController alamatController = TextEditingController(); - TextEditingController organisasiController = TextEditingController(); - - String jenisKelaminValue; - String tanggalLahir; - String jenisDisabilitasValue; - String pekerjaanValue; - Map newUserData = {}; - dynamic fotoValidate() { dynamic fotoVal = ''; if (_image != null) { @@ -378,11 +376,10 @@ class _EditProfileState extends State { final form = _formKey.currentState; if (_formKey.currentState.validate()) { form.save(); - newUser = UserModel( + updateUserModel = UpdateUserModel( name: nameController.text.toString(), - email: emailController.text.toString(), phoneNumber: phoneController.text.toString(), - tanggalLahir: tanggalLahir, + tanggalLahir: usDateFormatter.format(tanggalLahir), jenisKelamin: jenisKelaminValue ?? '-', disabilitas: jenisDisabilitasValue, pekerjaan: pekerjaanValue ?? '-', @@ -391,35 +388,34 @@ class _EditProfileState extends State { seen: _rahasiakanData, organisasiKomunitas: organisasiController.text, ); - newUserData = newUser.toJson(); if (_image != null) { final fileName = _image.path.split('/').last; - newUserData['foto'] = await MultipartFile.fromFile( + updateUserModel.foto = await MultipartFile.fromFile( _image.path, filename: fileName, ); } //await updateUser(newUser); - await updatePPUser(newUserData); + await updateUser(updateUserModel, detailUserModel.email); } else { failedDialog(context); } } - Future updatePPUser(Map newUserData) async { - bloc = NewUserBloc(); - final response = await bloc.updateUserPFP(newUserData); - if (response.statusCode == 200) { + Future updateUser(UpdateUserModel updateUserModel, String email) async { + _bloc = UserBloc(email:email); + final updatedUser = await _bloc.updateUser(updateUserModel); + if (updatedUser != null) { successDialog(context); Timer(const Duration(seconds: 2), () { //_navigateToProfile(context, newUser); - _navigateToProfile(context, newUserData['email']); + _navigateToProfile(context, updatedUser.email); }); } else { failedDialog(context); } - bloc.dispose(); + _bloc.dispose(); } void successDialog(BuildContext context) { @@ -447,8 +443,7 @@ class _EditProfileState extends State { } void _navigateToProfile(BuildContext context, String email) { - final route = - MaterialPageRoute(builder: (_) => Profile(email: email)); + final route = MaterialPageRoute(builder: (_) => Profile(email: email)); Navigator.of(context).pop(Navigator.pop(context)); Navigator.pop(context); Navigator.of(context).push(route); diff --git a/lib/page/profile/profile.dart b/lib/page/profile/profile.dart index a023513..ca042ff 100644 --- a/lib/page/profile/profile.dart +++ b/lib/page/profile/profile.dart @@ -3,8 +3,11 @@ import 'package:bisaGo/component/bisago_appbar.dart'; import 'package:bisaGo/component/image_holder.dart'; import 'package:bisaGo/config/styles.dart'; import 'package:bisaGo/model/user.dart'; +import 'package:bisaGo/network/data/network_model.dart'; import 'package:bisaGo/page/profile/edit_profile.dart'; +import 'package:bisaGo/utils/datetime_utils.dart'; import 'package:flutter/material.dart'; +import 'package:intl/date_symbol_data_local.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class Profile extends StatefulWidget { @@ -16,20 +19,26 @@ class Profile extends StatefulWidget { Key key, this.isPublic = false, }) : super(key: key); + @override _ProfileState createState() => _ProfileState(email); } class _ProfileState extends State { - UserModel user; + DetailUserModel user; String email; UserBloc _bloc; + bool fetched; + _ProfileState(this.email); @override void initState() { super.initState(); - _bloc = UserBloc(email); + fetched = false; + _bloc = UserBloc(email: email); + _bloc.fetchUserDetail(); + initializeDateFormatting('id_ID'); } @override @@ -50,7 +59,9 @@ class _ProfileState extends State { child: InkWell( key: const Key('Edit User Profile'), onTap: () { - _navigateToEditProfile(context); + if (user != null) { + _navigateToEditProfile(context); + } }, child: const Text( 'Edit', @@ -61,25 +72,34 @@ class _ProfileState extends State { ], ), ), - body: FutureBuilder( - future: _bloc.fetchUser(email), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - user = snapshot.data; - return _createProfilePage(); - } else if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(greenPrimary), - ), - ); + body: StreamBuilder>( + stream: _bloc.userStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + switch (snapshot.data.status) { + case Status.completed: + user = snapshot.data.data; + return _createProfilePage(); + case Status.loading: + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(greenPrimary), + ), + ); + case Status.error: + return Center( + child: Text(snapshot.data.toString()), + ); } - return Container(); - }), + } + return Container(); + }, + ), ); } Widget _createProfilePage() { + var tanggalLahir = toIndonesiaLocale(user.tanggalLahir); return ListView( children: [ Stack( @@ -196,11 +216,7 @@ class _ProfileState extends State { size: 28, color: darkGreen, ), - title: Text( - user.tanggalLahir == '' || - user.tanggalLahir == null - ? '-' - : user.tanggalLahir, + title: Text(tanggalLahir, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w500, diff --git a/lib/page/registrasi/registrasi.dart b/lib/page/registrasi/registrasi.dart index 87f211e..c1817f6 100644 --- a/lib/page/registrasi/registrasi.dart +++ b/lib/page/registrasi/registrasi.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'package:bisaGo/bloc/new_user_bloc.dart'; +import 'package:bisaGo/bloc/user_bloc.dart'; import 'package:bisaGo/config/strings.dart'; -import 'package:bisaGo/model/new_user.dart'; +import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/utils/custom_dropdown.dart'; +import 'package:bisaGo/utils/datetime_utils.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; @@ -14,7 +15,6 @@ import 'package:bisaGo/utils/custom_button.dart'; import 'package:bisaGo/utils/custom_text_field.dart'; import 'package:bisaGo/utils/validator.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:intl/intl.dart'; GoogleSignIn _googleSignIn = GoogleSignIn(scopes: ['email', 'profile']); @@ -28,7 +28,20 @@ class Registrasi extends StatefulWidget { class RegistrasiState extends State { final GlobalKey _formKey = GlobalKey(); - NewUserBloc bloc; + RegisterUserModel registerUserModel; + TextEditingController nameController = TextEditingController(); + TextEditingController phoneController = TextEditingController(); + TextEditingController emailController = TextEditingController(); + TextEditingController alamatController = TextEditingController(); + TextEditingController passwordController = TextEditingController(); + TextEditingController organisasiController = TextEditingController(); + + String _jenisKelaminValue; + String _pekerjaanValue; + String jenisDisabilitas = '-'; + String tanggalLahir = 'Pilih tanggal lahir'; + + UserBloc _bloc; static GoogleSignInAccount _currentUser; @@ -165,10 +178,9 @@ class RegistrasiState extends State { key: const Key('Button Tanggal Lahir'), onPressed: () { DatePicker.showDatePicker(context, - maxTime: DateTime.now(), onChanged: (date) { + maxTime: DateTime.now(), onConfirm: (date) { setState(() { - tanggalLahir = - '${DateFormat('yyyy-MM-dd').format(date)}'; + tanggalLahir = '${idDateFormatter.format(date)}'; }); }); }, @@ -262,12 +274,13 @@ class RegistrasiState extends State { final form = _formKey.currentState; if (_validateTanggalLahir(context) && _formKey.currentState.validate()) { form.save(); - newUser = NewUser( + registerUserModel = RegisterUserModel( name: nameController.text.toString(), password: passwordController.text.toString(), email: emailController.text.toString(), phoneNumber: phoneController.text.toString(), - tanggalLahir: tanggalLahir, + tanggalLahir: fromLocaleToLocale( + idDateFormatter, registerDateFormatter, tanggalLahir), jenisKelamin: _jenisKelaminValue ?? '-', disabilitas: jenisDisabilitas, pekerjaan: _pekerjaanValue ?? '-', @@ -277,7 +290,7 @@ class RegistrasiState extends State { organisasiKomunitas: organisasiController.text == '' ? '-' : organisasiController.text, ); - await createUser(newUser); + await createUser(registerUserModel); } } @@ -296,22 +309,9 @@ class RegistrasiState extends State { return true; } - NewUser newUser; - TextEditingController nameController = TextEditingController(); - TextEditingController phoneController = TextEditingController(); - TextEditingController emailController = TextEditingController(); - TextEditingController alamatController = TextEditingController(); - TextEditingController passwordController = TextEditingController(); - TextEditingController organisasiController = TextEditingController(); - - String _jenisKelaminValue; - String _pekerjaanValue; - String jenisDisabilitas = '-'; - String tanggalLahir = 'Pilih tanggal lahir'; - - Future createUser(NewUser newUser) async { - bloc = NewUserBloc(); - final response = await bloc.registerNewUser(newUser); + Future createUser(RegisterUserModel registerUserModel) async { + _bloc = UserBloc(); + final response = await _bloc.registerNewUser(registerUserModel); if (response) { successDialog(context); await Future.delayed(Duration(seconds: 2)); @@ -319,7 +319,7 @@ class RegistrasiState extends State { } else { failedDialog(context); } - bloc.dispose(); + _bloc.dispose(); } void successDialog(BuildContext context) { diff --git a/lib/repository/user_repository.dart b/lib/repository/user_repository.dart index 3126c97..a37f83d 100644 --- a/lib/repository/user_repository.dart +++ b/lib/repository/user_repository.dart @@ -1,56 +1,41 @@ -import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/network/network_interface.dart'; abstract class BaseUserRepository { - Future fetchUserDetail(String email); - Future createUser(NewUser newUser); - Future updateUser(NewUser newUser); - Future updateUserProfile(Map newUserData); + Future fetchUserDetail(String email); + + Future registerUser(RegisterUserModel registerUserModel); + + Future updateUser(UpdateUserModel updateUserModel, String email); } class UserRepository implements BaseUserRepository { final NetworkInterface _network = NetworkInterface(); @override - Future fetchUserDetail(String email) async { + Future fetchUserDetail(String email) async { final response = await _network.get(url: '/api/user/$email/', isLogin: true); - final data = [response]; - return User( - data.map((user) => UserModel.fromJson(user)).toList()); + return DetailUserModel.fromJson(response); } @override - Future createUser(NewUser newUser) async { - final response = await _network.post( + Future registerUser(RegisterUserModel registerUserModel) async { + return await _network.post( url: '/api/user/register/', isLogin: false, - bodyParams: newUser.toJson(), + bodyParams: registerUserModel.toJson(), ); - return response; } @override - Future updateUserProfile( - Map newUserData, - ) async { - final String email = newUserData['email']; + Future updateUser( + UpdateUserModel updateUserModel, String email) async { final response = await _network.put( url: '/api/user/$email/', - bodyParams: newUserData, - isLogin: true, - ); - return response; - } - - @override - Future updateUser(NewUser newUser) async { - final response = await _network.put( - url: '/api/user/${newUser.email}/', - bodyParams: newUser.toJson(), + bodyParams: updateUserModel.toJson(), isLogin: true, ); - return response; + return DetailUserModel.fromJson(response); } } diff --git a/lib/utils/datetime_utils.dart b/lib/utils/datetime_utils.dart new file mode 100644 index 0000000..247e779 --- /dev/null +++ b/lib/utils/datetime_utils.dart @@ -0,0 +1,27 @@ +import 'package:intl/intl.dart'; + +const userProfileDateFormat = 'dd MMMM yyyy'; +const userRegisterDateFormat = 'yyyy-MM-dd'; + +final idDateFormatter = DateFormat(userProfileDateFormat, 'id_ID'); +final usDateFormatter = DateFormat(userProfileDateFormat, 'en_US'); +final registerDateFormatter = DateFormat(userRegisterDateFormat, 'en_US'); + +String toIndonesiaLocale(String date) { + return fromLocaleToLocale(usDateFormatter, idDateFormatter, date); +} + +String toUnitedStatesLocale(String date) { + return fromLocaleToLocale(idDateFormatter, usDateFormatter, date); +} + +String fromLocaleToLocale( + DateFormat fromLocaleFormatter, DateFormat toLocaleFormatter, String date) { + var tempDate; + try { + tempDate = fromLocaleFormatter.parse(date); + } catch (_) { + return date; + } + return toLocaleFormatter.format(tempDate); +} diff --git a/test/login_test.dart b/test/login_test.dart index 2573a52..bf03c00 100644 --- a/test/login_test.dart +++ b/test/login_test.dart @@ -28,7 +28,7 @@ class MockUserRepository extends Fake implements UserRepository { 'username': 'test@gmail.com', 'name': 'test', 'email': 'test@gmail.com', - 'tanggal_lahir': '2000-01-01', + 'tanggal_lahir': '02 October 2000', 'phone_number': '081234567898', 'jenis_kelamin': 'Laki-Laki', 'disabilitas': '-', @@ -37,8 +37,8 @@ class MockUserRepository extends Fake implements UserRepository { }; @override - Future fetchUserDetail(String email) async { - return Future.value(User([UserModel.fromJson(userData)])); + Future fetchUserDetail(String email) async { + return Future.value(DetailUserModel.fromJson(userData)); } } diff --git a/test/model_test.dart b/test/model_test.dart index 8623d86..9cf17ce 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -1,6 +1,5 @@ import 'package:bisaGo/model/komentar.dart'; import 'package:bisaGo/model/komentar_posting.dart'; -import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/model/user.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -20,17 +19,15 @@ void main() { 'created': '2020-11-18T00:13:00.000' }; final userData = { - 'is_login': true, 'username': 'test@email.com', 'name': 'test', 'email': 'test@email.com', - 'tanggal_lahir': '2000-01-01', + 'tanggal_lahir': '02 October 2000', 'phone_number': '081234567898', 'jenis_kelamin': 'Laki-Laki', 'disabilitas': 'Tidak Memiliki Disabilitas', 'pekerjaan': 'Mahasiswa', 'alamat': 'Tidak Tahu', - 'token': null, 'seen': true, 'foto': '', 'hidden_fields': [], @@ -70,7 +67,7 @@ void main() { 'name': 'test', 'email': 'test@email.com', 'password': '2139809348143123', - 'tanggal_lahir': '2000-01-01', + 'tanggal_lahir': '02 October 2000', 'phone_number': '081234567898', 'jenis_kelamin': 'Laki-Laki', 'disabilitas': 'Tidak Memiliki Disabilitas', @@ -103,15 +100,13 @@ void main() { }); test('User Model fromJson and toJson', () { - final userModel = [UserModel.fromJson(userData)]; - final user = User(userModel); - expect(user.user.length, 1); - expect(userModel.first.toJson(), userData); + final userModel = DetailUserModel.fromJson(userData); + expect(userModel.toJson(), userData); }); test('User Model fromJson and toJson', () { - final newUserModel = NewUser.fromJson(newUserData); - expect(newUserModel, isInstanceOf()); + final newUserModel = RegisterUserModel.fromJson(newUserData); + expect(newUserModel, isInstanceOf()); }); test('Komentar Model toJson', () { diff --git a/test/pilih_disabilitas_test.dart b/test/pilih_disabilitas_test.dart index a149cf3..c5cf410 100644 --- a/test/pilih_disabilitas_test.dart +++ b/test/pilih_disabilitas_test.dart @@ -1,28 +1,24 @@ -import 'package:bisaGo/model/new_user.dart'; +import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/login/pilih_disabilitas.dart'; import 'package:bisaGo/repository/user_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; -import 'package:http/http.dart'; import 'package:mockito/mockito.dart'; class MockUserRepository extends Fake implements UserRepository { @override - Future updateUser(NewUser newUser) async { - final responseBody = {'response': 'User updated'}; - final statusCode = 200; - return Future.value(Response(responseBody.toString(), statusCode)); + Future updateUser(UpdateUserModel updateUserModel, String email) async { + return Future.value(DetailUserModel()); } } void main() { final userData = { - 'is_login': true, 'username': 'test@gmail.com', 'name': 'test', 'email': 'test@gmail.com', - 'tanggal_lahir': '2000-01-01', + 'tanggal_lahir': '02 October 2000', 'phone_number': '081234567898', 'jenis_kelamin': 'Laki-Laki', 'disabilitas': 'Tidak memiliki disabilitas', @@ -96,7 +92,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: PilihDisabilitas( - newUser: NewUser.fromJson(userData), + updateUser: UpdateUserModel.fromJson(userData), ))); final disabilitasIntelektualButton = find.text('Disabilitas Intelektual'); expect(disabilitasIntelektualButton, findsOneWidget); @@ -105,7 +101,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: PilihDisabilitas( - newUser: NewUser.fromJson(userData), + updateUser: UpdateUserModel.fromJson(userData), ))); final disabilitasSensorikButton = find.text('Disabilitas Sensorik'); expect(disabilitasSensorikButton, findsOneWidget); @@ -114,7 +110,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: PilihDisabilitas( - newUser: NewUser.fromJson(userData), + updateUser: UpdateUserModel.fromJson(userData), ))); final disabilitasMentalButton = find.text('Disabilitas Mental'); expect(disabilitasMentalButton, findsOneWidget); @@ -123,7 +119,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: PilihDisabilitas( - newUser: NewUser.fromJson(userData), + updateUser: UpdateUserModel.fromJson(userData), ))); final disabilitasFisikButton = find.text('Disabilitas Fisik'); expect(disabilitasFisikButton, findsOneWidget); diff --git a/test/profile_test.dart b/test/profile_test.dart index bf6512a..e04e40c 100644 --- a/test/profile_test.dart +++ b/test/profile_test.dart @@ -1,5 +1,6 @@ +import 'dart:convert'; + import 'package:bisaGo/component/bisago_drawer.dart'; -import 'package:bisaGo/model/new_user.dart'; import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/profile/edit_profile.dart'; import 'package:bisaGo/page/profile/profile.dart'; @@ -7,7 +8,6 @@ import 'package:bisaGo/repository/user_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; -import 'package:http/http.dart'; import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -17,7 +17,7 @@ class MockUserRepository extends Fake implements UserRepository { 'username': 'test@gmail.com', 'name': 'test', 'email': 'test@gmail.com', - 'tanggal_lahir': '2000-01-01', + 'tanggal_lahir': '02 October 2000', 'phone_number': '081234567898', 'jenis_kelamin': 'Laki-Laki', 'disabilitas': 'Tidak memiliki disabilitas', @@ -28,22 +28,16 @@ class MockUserRepository extends Fake implements UserRepository { }; @override - Future fetchUserDetail(String email) async { - return Future.value(User([UserModel.fromJson(userData)])); + Future fetchUserDetail(String email) async { + return Future.value(DetailUserModel.fromJson(userData)); } @override - Future updateUser(NewUser newUser) async { - var responseBody; - var statusCode; - if (newUser.disabilitas != null) { - responseBody = {'response': 'User updated'}; - statusCode = 200; - } else { - responseBody = {'response': 'Failed to update user'}; - statusCode = 400; + Future updateUser(UpdateUserModel updateUserModel, String email) async { + if (updateUserModel.disabilitas = null) { + return null; } - return Future.value(Response(responseBody.toString(), statusCode)); + return Future.value(DetailUserModel()); } } @@ -54,20 +48,17 @@ void main() { _getIt.registerLazySingleton( () => MockUserRepository()); SharedPreferences.setMockInitialValues( - {'email': 'test@gmail.com', 'token': 'token'}); + {'user':jsonEncode({'email': 'test@gmail.com'}), 'token': 'token'}); }); testWidgets('Dashboard with User', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp(home: BisaGoDrawer())); await tester.pumpAndSettle(); expect(find.text('test@gmail.com'), findsOneWidget); - - await tester.tap(find.text('Profile')); - await tester.pumpAndSettle(); }); testWidgets('Dashboard with no User', (WidgetTester tester) async { - SharedPreferences.setMockInitialValues({'email': 'test@gmail.com'}); + SharedPreferences.setMockInitialValues({'user':jsonEncode({'email': 'test@gmail.com'})}); await tester.pumpWidget(MaterialApp(home: BisaGoDrawer())); await tester.pumpAndSettle(); expect(find.text('test@gmail.com'), findsNothing); @@ -163,7 +154,7 @@ void main() { final userData = { 'name': 'test', 'email': 'test@gmail.com', - 'tanggal_lahir': '2000-01-01', + 'tanggal_lahir': '02 October 2000', 'phone_number': '081234567898', 'jenis_kelamin': 'Laki-laki', 'disabilitas': 'Tidak memiliki disabilitas', @@ -176,7 +167,7 @@ void main() { final userData2 = { 'name': 'test', 'email': '', - 'tanggal_lahir': '2000-01-01', + 'tanggal_lahir': '02 October 2000', 'phone_number': '', 'jenis_kelamin': 'Laki-laki', 'disabilitas': 'Tidak memiliki disabilitas', @@ -194,7 +185,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: EditProfile( - user: UserModel.fromJson(userData), + user: DetailUserModel.fromJson(userData), ))); expect(find.byType(EditProfile), findsOneWidget); expect(find.byKey(headerEditProfilePageKey), findsOneWidget); @@ -214,7 +205,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: EditProfile( - user: UserModel.fromJson(userData), + user: DetailUserModel.fromJson(userData), ))); expect(find.byType(EditProfile), findsOneWidget); expect(find.byKey(headerEditProfilePageKey), findsNothing); @@ -232,7 +223,7 @@ void main() { final name = 'test'; await tester.pumpWidget( - MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + MaterialApp(home: EditProfile(user: DetailUserModel.fromJson(userData)))); await tester.pumpAndSettle(); expect(find.byType(EditProfile), findsOneWidget); //expect(find.byKey(userAvatarKey), findsOneWidget); @@ -245,7 +236,7 @@ void main() { final name = 'mantap'; await tester.pumpWidget( - MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + MaterialApp(home: EditProfile(user: DetailUserModel.fromJson(userData)))); await tester.pumpAndSettle(); expect(find.byType(Profile), findsNothing); expect(find.byKey(userAvatarKey), findsNothing); @@ -259,7 +250,7 @@ void main() { final pekerjaanKey = find.byKey(Key('Dropdown Pekerjaan')); await tester.pumpWidget( - MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + MaterialApp(home: EditProfile(user: DetailUserModel.fromJson(userData)))); await tester.pumpAndSettle(); expect(find.byType(EditProfile), findsOneWidget); expect(jenisKelaminKey, findsOneWidget); @@ -314,7 +305,7 @@ void main() { userData.update('disabilitas', (value) => null); await tester.pumpWidget( - MaterialApp(home: EditProfile(user: UserModel.fromJson(userData2)))); + MaterialApp(home: EditProfile(user: DetailUserModel.fromJson(userData2)))); await tester.pumpAndSettle(); expect(find.byType(EditProfile), findsOneWidget); expect(jenisKelaminKey, findsOneWidget); @@ -372,7 +363,7 @@ void main() { final organisasiKey = find.byKey(Key('Text Field Organisasi')); await tester.pumpWidget( - MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + MaterialApp(home: EditProfile(user: DetailUserModel.fromJson(userData)))); await tester.pumpAndSettle(); await tester.enterText(namaKey, 'Ardian'); @@ -396,7 +387,7 @@ void main() { final organisasiKey = find.byKey(Key('Text Field Organisasi')); await tester.pumpWidget( - MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + MaterialApp(home: EditProfile(user: DetailUserModel.fromJson(userData)))); await tester.pumpAndSettle(); await tester.enterText(namaKey, ''); @@ -415,14 +406,14 @@ void main() { testWidgets('Change Profile Picture Title -- Positive', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + MaterialApp(home: EditProfile(user: DetailUserModel.fromJson(userData)))); expect(find.text('Ubah Foto Profil'), findsOneWidget); }); testWidgets('Change Profile Picture Title -- Negative', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp(home: EditProfile(user: UserModel.fromJson(userData)))); + MaterialApp(home: EditProfile(user: DetailUserModel.fromJson(userData)))); expect(find.text('Edit PFP'), findsNothing); }); }); diff --git a/test/registrasi_test.dart b/test/registrasi_test.dart index 307118a..e632c0f 100644 --- a/test/registrasi_test.dart +++ b/test/registrasi_test.dart @@ -1,19 +1,16 @@ -import 'package:bisaGo/model/new_user.dart'; +import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/repository/user_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:bisaGo/page/registrasi/registrasi.dart'; import 'package:get_it/get_it.dart'; import 'package:mockito/mockito.dart'; -//import 'package:bisaGo/model/user.dart'; -import 'package:http/http.dart'; class MockUserRepository extends Fake implements UserRepository { @override - Future createUser(NewUser newUser) async { - final responseBody = {'response': 'User updated'}; - final statusCode = 200; - return Future.value(Response(responseBody.toString(), statusCode)); + Future registerUser(RegisterUserModel registerUserModel) async { + final responseBody = {'response': 'User registered'}; + return Future.value(responseBody); } } diff --git a/test/user_test.dart b/test/user_test.dart index 14df3fc..cb38719 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -3,17 +3,15 @@ import 'package:flutter_test/flutter_test.dart'; void main() { final userData = { - 'is_login': true, 'username': 'test@gmail.com', 'name': 'test', 'email': 'test@gmail.com', - 'tanggal_lahir': '2000-01-01', + 'tanggal_lahir': '02 October 2000', 'phone_number': '081234567898', 'jenis_kelamin': 'Laki-Laki', 'disabilitas': 'Tidak Memiliki Disabilitas', 'pekerjaan': 'Mahasiswa', 'alamat': 'Tidak Tahu', - 'token': null, 'seen': true, 'foto': '', 'hidden_fields': [], @@ -21,9 +19,7 @@ void main() { }; test('User Model', () { - final userModel = [UserModel.fromJson(userData)]; - final user = User(userModel); - expect(user.user.length, 1); - expect(userModel.first.toJson(), userData); + final userModel = DetailUserModel.fromJson(userData); + expect(userModel.toJson(), userData); }); } -- GitLab From 34f558002cc13ff391c43e8dca524707d1f23515 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Fri, 4 Jun 2021 14:22:14 +0700 Subject: [PATCH 35/45] [RED] new attribute for komentar model added creator_picture attribute --- test/detail_post_test.dart | 2 ++ test/model_test.dart | 3 +++ 2 files changed, 5 insertions(+) diff --git a/test/detail_post_test.dart b/test/detail_post_test.dart index 8a1e83d..7f10f7c 100644 --- a/test/detail_post_test.dart +++ b/test/detail_post_test.dart @@ -70,6 +70,7 @@ void main() { DateTime(2020, 1, 1), 'This is a test', 'test@email.com', + 'test.jpg', ); }); @@ -80,6 +81,7 @@ void main() { DateTime(2020, 1, 1), 'This is a test', 'test@email.com', + 'test.jpg', ); }); diff --git a/test/model_test.dart b/test/model_test.dart index 9cf17ce..d2d712c 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -9,6 +9,7 @@ void main() { 'deskripsi': 'This is a test', 'creator': 'Test', 'creator_email': 'test@email.com', + 'creator_picture': 'test.jpg', 'created': '18-11-2020 00:13:52' }; final returnKomentarPostingData = { @@ -16,6 +17,7 @@ void main() { 'deskripsi': 'This is a test', 'creator': 'Test', 'creator_email': 'test@email.com', + 'creator_picture': 'test.jpg', 'created': '2020-11-18T00:13:00.000' }; final userData = { @@ -84,6 +86,7 @@ void main() { deskripsi: 'This is a test', creator: 'Test', creatorEmail: 'test@email.com', + creatorPicture: 'test.jpg', created: DateTime.now()); expect(komentarPostingModel, isInstanceOf()); expect( -- GitLab From 55a97b0406cda45334b8f86b7737eec23c1e4a93 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Fri, 4 Jun 2021 14:23:33 +0700 Subject: [PATCH 36/45] [GREEN] implemented new komentar model and finalize PBI 10 - new attribute on komentar model 'creator_picture' - implemented viewable profile picture at profile and komentar --- lib/bloc/user_bloc.dart | 2 +- lib/component/bisago_appbar.dart | 11 +- lib/model/komentar_posting.dart | 3 + lib/model/komentar_posting.g.dart | 2 + lib/model/komentar_posting_kegiatan.dart | 3 + lib/model/komentar_posting_kegiatan.g.dart | 2 + .../postingan/detail_post.dart | 41 ++----- lib/page/profile/edit_profile.dart | 49 ++++----- lib/page/profile/full_screen_image.dart | 23 ++++ lib/page/profile/profile.dart | 41 ++----- lib/page/profile/profile_picture.dart | 103 ++++++++++++++++++ lib/utils/profile_utils.dart | 7 ++ pubspec.yaml | 1 + 13 files changed, 199 insertions(+), 89 deletions(-) create mode 100644 lib/page/profile/full_screen_image.dart create mode 100644 lib/page/profile/profile_picture.dart create mode 100644 lib/utils/profile_utils.dart diff --git a/lib/bloc/user_bloc.dart b/lib/bloc/user_bloc.dart index de113fa..3ff261e 100644 --- a/lib/bloc/user_bloc.dart +++ b/lib/bloc/user_bloc.dart @@ -27,7 +27,7 @@ class UserBloc { if (response is DetailUserModel) { userSink.add(NetworkModel.completed(response)); } else { - userSink.add(NetworkModel.error(response.toString())); + userSink.add(NetworkModel.error('Gagal untuk mendapatkan profile')); } } diff --git a/lib/component/bisago_appbar.dart b/lib/component/bisago_appbar.dart index fdf8b47..fbd07f4 100644 --- a/lib/component/bisago_appbar.dart +++ b/lib/component/bisago_appbar.dart @@ -5,17 +5,24 @@ class BisaGoAppBar extends StatelessWidget implements PreferredSizeWidget { final String title; final List actions; final Widget leading; + final backgroundColor; + const BisaGoAppBar( - {this.title = 'bisaGo', this.actions = const [], this.leading, Key key}) + {this.title = 'bisaGo', + this.actions = const [], + this.backgroundColor = greenPrimary, + this.leading, + Key key}) : super(key: key); @override final Size preferredSize = const Size.fromHeight(55); + @override Widget build(BuildContext context) { return AppBar( elevation: 15, centerTitle: true, - backgroundColor: greenPrimary, + backgroundColor: backgroundColor, automaticallyImplyLeading: leading == null, leading: leading, title: Row( diff --git a/lib/model/komentar_posting.dart b/lib/model/komentar_posting.dart index c7064e0..4c56017 100644 --- a/lib/model/komentar_posting.dart +++ b/lib/model/komentar_posting.dart @@ -16,6 +16,8 @@ class KomentarPostingModel { final String creator; @JsonKey(name: 'creator_email') final String creatorEmail; + @JsonKey(name: 'creator_picture') + final String creatorPicture; @JsonKey(fromJson: CustomSerializer.stringToDateTime) final DateTime created; @@ -24,6 +26,7 @@ class KomentarPostingModel { this.deskripsi, this.creator, this.creatorEmail, + this.creatorPicture, this.created, }); diff --git a/lib/model/komentar_posting.g.dart b/lib/model/komentar_posting.g.dart index 5323425..75fc5ca 100644 --- a/lib/model/komentar_posting.g.dart +++ b/lib/model/komentar_posting.g.dart @@ -28,6 +28,7 @@ KomentarPostingModel _$KomentarPostingModelFromJson(Map json) { deskripsi: json['deskripsi'] as String, creator: json['creator'] as String, creatorEmail: json['creator_email'] as String, + creatorPicture: json['creator_picture'] as String, created: CustomSerializer.stringToDateTime(json['created'] as String), ); } @@ -39,5 +40,6 @@ Map _$KomentarPostingModelToJson( 'deskripsi': instance.deskripsi, 'creator': instance.creator, 'creator_email': instance.creatorEmail, + 'creator_picture': instance.creatorPicture, 'created': instance.created?.toIso8601String(), }; diff --git a/lib/model/komentar_posting_kegiatan.dart b/lib/model/komentar_posting_kegiatan.dart index 19ded6e..c537b08 100644 --- a/lib/model/komentar_posting_kegiatan.dart +++ b/lib/model/komentar_posting_kegiatan.dart @@ -15,6 +15,8 @@ class KomentarPostingKegiatanModel { final String creator; @JsonKey(name: 'creator_email') final String creatorEmail; + @JsonKey(name: 'creator_picture') + final String creatorPicture; final String deskripsi; @JsonKey(name: 'created', fromJson: _stringToDateTime) final DateTime created; @@ -25,6 +27,7 @@ class KomentarPostingKegiatanModel { this.deskripsi, this.created, this.creatorEmail, + this.creatorPicture, }); factory KomentarPostingKegiatanModel.fromJson(Map json) => diff --git a/lib/model/komentar_posting_kegiatan.g.dart b/lib/model/komentar_posting_kegiatan.g.dart index f1108b5..521b139 100644 --- a/lib/model/komentar_posting_kegiatan.g.dart +++ b/lib/model/komentar_posting_kegiatan.g.dart @@ -31,6 +31,7 @@ KomentarPostingKegiatanModel _$KomentarPostingKegiatanModelFromJson( deskripsi: json['deskripsi'] as String, created: _stringToDateTime(json['created'] as String), creatorEmail: json['creator_email'] as String, + creatorPicture: json['creator_picture'] as String, ); } @@ -40,6 +41,7 @@ Map _$KomentarPostingKegiatanModelToJson( 'id': instance.id, 'creator': instance.creator, 'creator_email': instance.creatorEmail, + 'creator_picture': instance.creatorPicture, 'deskripsi': instance.deskripsi, 'created': instance.created?.toIso8601String(), }; diff --git a/lib/page/filter_fasilitas/postingan/detail_post.dart b/lib/page/filter_fasilitas/postingan/detail_post.dart index 6f57b2d..98e8dd7 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:bisaGo/config/strings.dart'; import 'package:bisaGo/model/lokasi.dart'; +import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/profile/profile.dart'; +import 'package:bisaGo/page/profile/profile_picture.dart'; import 'package:bisaGo/page/updateInformasi/update_informasi.dart'; import 'package:bisaGo/repository/dynamic_links_service_repository.dart'; import 'package:bisaGo/utils/share_utils.dart'; @@ -318,6 +320,7 @@ class _DetailPostPageState extends State { k.created, k.deskripsi, k.creatorEmail, + k.creatorPicture, ), ) .toList()); @@ -486,34 +489,18 @@ class _DetailPostPageState extends State { DateTime date, String description, String email, + String foto, ) { + final user = DetailUserModel() + ..email = email + ..foto = foto + ..name = name; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => Profile( - email: email, - isPublic: true, - ), - ), - ); - }, - child: CircleAvatar( - backgroundColor: greenPrimary, - child: Text( - _creatorInitials(name), - style: const TextStyle( - color: Colors.white, - ), - ), - ), - ), + ProfilePicture(user, redirectToDetailProfile: true), Padding( padding: const EdgeInsets.all(regularSpace), child: Column( @@ -557,16 +544,6 @@ class _DetailPostPageState extends State { ); } - String _creatorInitials(String name) { - if (name.isEmpty) return ''; - var initials = ''; - for (final i in name.split(' ')) { - initials += '${i[0].toUpperCase()}'; - } - if (initials.length > 2) return initials.substring(0, 2); - return initials; - } - Widget _showIfContains(String disabilitas) { String imageUrl; switch (disabilitas) { diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index 898c065..743613e 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:bisaGo/bloc/user_bloc.dart'; import 'package:bisaGo/component/image_holder.dart'; import 'package:bisaGo/utils/datetime_utils.dart'; +import 'package:bisaGo/utils/profile_utils.dart'; import 'package:dio/dio.dart'; import 'package:bisaGo/component/bisago_appbar.dart'; @@ -136,30 +137,28 @@ class _EditProfileState extends State { CircleAvatar( key: Key('Avatar ${detailUserModel.name.split(' ')[0]}'), radius: 50, - backgroundColor: white, - child: ClipOval( - child: (_image != null) - ? SizedBox( - width: 100, - height: 100, - child: Image.file(_image, fit: BoxFit.cover), - ) - : ((_imageUrl) == null - ? Text(detailUserModel.name.substring(0, 1), - style: const TextStyle( - fontSize: 45, - fontWeight: FontWeight.w900, - color: darkGreen, - fontFamily: 'Comfortaa', - )) - : SizedBox( - width: 100, - height: 100, - child: ImageHolder( - url: _imageUrl, - ), - )), - ), + backgroundColor: greenPrimary, + child: (_image != null) + ? ClipOval( + child: AspectRatio( + aspectRatio: 1, + child: Image.file(_image, fit: BoxFit.cover), + )) + : ((_imageUrl) == null + ? Text(getNameInitials(detailUserModel.name), + style: const TextStyle( + fontSize: 45, + fontWeight: FontWeight.w900, + color: Colors.white, + fontFamily: 'Comfortaa', + )) + : ClipOval( + child: AspectRatio( + aspectRatio: 1, + child: ImageHolder( + url: _imageUrl, + ), + ))), ), Padding( padding: @@ -404,7 +403,7 @@ class _EditProfileState extends State { } Future updateUser(UpdateUserModel updateUserModel, String email) async { - _bloc = UserBloc(email:email); + _bloc = UserBloc(email: email); final updatedUser = await _bloc.updateUser(updateUserModel); if (updatedUser != null) { successDialog(context); diff --git a/lib/page/profile/full_screen_image.dart b/lib/page/profile/full_screen_image.dart new file mode 100644 index 0000000..f8878ff --- /dev/null +++ b/lib/page/profile/full_screen_image.dart @@ -0,0 +1,23 @@ +import 'package:bisaGo/component/bisago_appbar.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class FullScreenImage extends StatelessWidget { + final Widget body; + + FullScreenImage({Key key, this.body}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: BisaGoAppBar( + title: '', + backgroundColor: Colors.transparent, + ), + body: Center( + child: body ?? Container(), + ), + ); + } +} diff --git a/lib/page/profile/profile.dart b/lib/page/profile/profile.dart index ca042ff..1e34422 100644 --- a/lib/page/profile/profile.dart +++ b/lib/page/profile/profile.dart @@ -1,15 +1,17 @@ import 'package:bisaGo/bloc/user_bloc.dart'; import 'package:bisaGo/component/bisago_appbar.dart'; -import 'package:bisaGo/component/image_holder.dart'; import 'package:bisaGo/config/styles.dart'; import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/network/data/network_model.dart'; import 'package:bisaGo/page/profile/edit_profile.dart'; +import 'package:bisaGo/page/profile/profile_picture.dart'; import 'package:bisaGo/utils/datetime_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +const rahasia = Color(0xFF585858); + class Profile extends StatefulWidget { final String email; final bool isPublic; @@ -88,7 +90,7 @@ class _ProfileState extends State { ); case Status.error: return Center( - child: Text(snapshot.data.toString()), + child: Text(snapshot.data.message.toString()), ); } } @@ -120,28 +122,8 @@ class _ProfileState extends State { padding: const EdgeInsets.only( bottom: doubleSpace, ), - child: CircleAvatar( - key: Key('Avatar ${user.name.split(' ')[0]}'), - radius: 50, - backgroundColor: white, - child: ClipOval( - child: (user.foto != null) - ? SizedBox( - width: 100, - height: 100, - child: ImageHolder( - url: user.foto, - ), - ) - : Text(user.name.substring(0, 1), - style: const TextStyle( - fontSize: 45, - fontWeight: FontWeight.w900, - color: darkGreen, - fontFamily: 'Comfortaa', - )), - ), - ), + child: ProfilePicture(user, + radius: 50, fontSize: 50 * 0.7, previewAble: true), ), if (!widget.isPublic) Text( @@ -159,7 +141,7 @@ class _ProfileState extends State { style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: Colors.white, + color: rahasia, fontFamily: 'Comfortaa', ), textAlign: TextAlign.center, @@ -216,11 +198,12 @@ class _ProfileState extends State { size: 28, color: darkGreen, ), - title: Text(tanggalLahir, + title: Text( + tanggalLahir, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Colors.black, + color: rahasia, fontFamily: 'Comfortaa', ), textAlign: TextAlign.left, @@ -237,7 +220,7 @@ class _ProfileState extends State { style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Colors.black, + color: rahasia, fontFamily: 'Comfortaa', ), textAlign: TextAlign.left, @@ -314,7 +297,7 @@ class _ProfileState extends State { style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Colors.black, + color: rahasia, fontFamily: 'Comfortaa', ), textAlign: TextAlign.left, diff --git a/lib/page/profile/profile_picture.dart b/lib/page/profile/profile_picture.dart new file mode 100644 index 0000000..be9efbd --- /dev/null +++ b/lib/page/profile/profile_picture.dart @@ -0,0 +1,103 @@ +import 'dart:math'; + +import 'package:bisaGo/component/image_holder.dart'; +import 'package:bisaGo/config/styles.dart'; +import 'package:bisaGo/model/user.dart'; +import 'package:bisaGo/page/profile/full_screen_image.dart'; +import 'package:bisaGo/page/profile/profile.dart'; +import 'package:bisaGo/utils/profile_utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; + +class ProfilePicture extends StatelessWidget { + final DetailUserModel user; + final bool redirectToDetailProfile; + final bool previewAble; + final double radius; + final double fontSize; + final Color color; + final Color backgroundColor; + final String fontFamily; + final FontWeight fontWeight; + + ProfilePicture(this.user, + {Key key, + this.redirectToDetailProfile = false, + this.previewAble = false, + this.radius = 20, + this.fontSize = 14, + this.color = Colors.white, + this.backgroundColor = greenPrimary, + this.fontFamily = 'Comfortaa', + this.fontWeight = FontWeight.w900}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + if (redirectToDetailProfile) { + _toDetailProfile(context); + } else if (previewAble) { + _toPreviewImage(context); + } + }, + child: _makeAvatar(), + ); + } + + Widget _makeAvatar({double radius, double fontSize}) { + return CircleAvatar( + key: Key('Avatar ${user.name.split(' ')[0]}'), + radius: radius ?? this.radius, + backgroundColor: user.foto != null ? Colors.white : backgroundColor, + child: (user.foto != null) + ? ClipOval( + child: AspectRatio( + aspectRatio: 1, + child: ImageHolder( + url: user.foto, + ), + ), + ) + : Text(getNameInitials(user.name), + style: TextStyle( + fontSize: fontSize ?? this.fontSize, + fontWeight: fontWeight, + color: color, + fontFamily: fontFamily, + )), + ); + } + + Widget _getFullPicture(BuildContext context) { + if (user.foto != null) { + return PhotoView( + imageProvider: NetworkImage(user.foto), + loadingBuilder: (context, event) => const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(greenPrimary), + )), + ); + } + var width = MediaQuery.of(context).size.width; + var height = MediaQuery.of(context).size.height; + var minWidthHeight = min(width, height); + return _makeAvatar(radius: minWidthHeight, fontSize: minWidthHeight * 0.5); + } + + void _toDetailProfile(BuildContext context) => + _toSomewhere(context, (_) => Profile(email: user.email, isPublic: true)); + + void _toPreviewImage(BuildContext context) => _toSomewhere( + context, + (_) => FullScreenImage( + body: _getFullPicture(context), + )); + + void _toSomewhere( + BuildContext context, Function(BuildContext context) builder) => + Navigator.push( + context, MaterialPageRoute(builder: (context) => builder(context))); +} diff --git a/lib/utils/profile_utils.dart b/lib/utils/profile_utils.dart new file mode 100644 index 0000000..49ae8ef --- /dev/null +++ b/lib/utils/profile_utils.dart @@ -0,0 +1,7 @@ + +String getNameInitials(String name) { + if (name.isEmpty) return ''; + final initials = name.split(' ').map((e) => e[0]).join().toUpperCase(); + if (initials.length > 2) return initials.substring(0, 2); + return initials; +} diff --git a/pubspec.yaml b/pubspec.yaml index 14d76cf..e0e2a01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: firebase_messaging: ^8.0.0-dev.15 carousel_slider: ^3.0.0 flushbar: ^1.10.4 + photo_view: ^0.11.1 dev_dependencies: flutter_test: -- GitLab From fb48439a54ee778b14a9b0598e296091d7eddd41 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Fri, 4 Jun 2021 17:03:51 +0700 Subject: [PATCH 37/45] [RED] test for new attribute on DetailUserModel - attribute 'hidden_fields_verbose', 'can_see_hidden_fields', and 'hidden_fields_color' --- test/model_test.dart | 3 +++ test/profile_test.dart | 9 +++++++++ test/user_test.dart | 3 +++ 3 files changed, 15 insertions(+) diff --git a/test/model_test.dart b/test/model_test.dart index d2d712c..aad27f1 100644 --- a/test/model_test.dart +++ b/test/model_test.dart @@ -33,6 +33,9 @@ void main() { 'seen': true, 'foto': '', 'hidden_fields': [], + 'hidden_fields_verbose': [], + 'can_see_hidden_fields': true, + 'hidden_fields_color': '#808080', 'organisasi_komunitas': 'Organisasi', }; final komentarData = { diff --git a/test/profile_test.dart b/test/profile_test.dart index e04e40c..59901d7 100644 --- a/test/profile_test.dart +++ b/test/profile_test.dart @@ -24,6 +24,9 @@ class MockUserRepository extends Fake implements UserRepository { 'pekerjaan': 'Pelajar', 'alamat': 'Tidak Tahu', 'hidden_fields': [], + 'hidden_fields_verbose': [], + 'can_see_hidden_fields': true, + 'hidden_fields_color': '#808080', 'seen': true, }; @@ -162,6 +165,9 @@ void main() { 'alamat': 'Tidak Tahu', 'seen': true, 'hidden_fields': [], + 'hidden_fields_verbose': [], + 'can_see_hidden_fields': true, + 'hidden_fields_color': '#808080', }; final userData2 = { @@ -175,6 +181,9 @@ void main() { 'alamat': '', 'seen': true, 'hidden_fields': [], + 'hidden_fields_verbose': [], + 'can_see_hidden_fields': true, + 'hidden_fields_color': '#808080', }; testWidgets('Edit Profile Page Widget Test -- Positive', diff --git a/test/user_test.dart b/test/user_test.dart index cb38719..0a555d7 100644 --- a/test/user_test.dart +++ b/test/user_test.dart @@ -15,6 +15,9 @@ void main() { 'seen': true, 'foto': '', 'hidden_fields': [], + 'hidden_fields_verbose': [], + 'can_see_hidden_fields': true, + 'hidden_fields_color': '#808080', 'organisasi_komunitas': 'Organisasi', }; -- GitLab From a88251ea19e2bd4822bcef1958a5cbb3cb3424fc Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Fri, 4 Jun 2021 17:08:07 +0700 Subject: [PATCH 38/45] [GREEN] implemented new attribute for DetailUserModel --- lib/model/user.dart | 20 +++++++- lib/model/user.g.dart | 13 +++++- lib/page/profile/edit_profile.dart | 2 +- lib/page/profile/profile.dart | 74 +++++++++++++++++------------- lib/utils/profile_utils.dart | 3 ++ 5 files changed, 75 insertions(+), 37 deletions(-) diff --git a/lib/model/user.dart b/lib/model/user.dart index 8118c25..ae19bea 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:json_annotation/json_annotation.dart'; part 'user.g.dart'; @@ -103,11 +105,21 @@ class DetailUserModel extends BaseUserModel { bool seen; @JsonKey(name: 'hidden_fields') List hiddenFields; + @JsonKey(name: 'hidden_fields_verbose') + List hiddenFieldsVerbose; + @JsonKey(name: 'can_see_hidden_fields') + bool canSeeHiddenFields; + @JsonKey(name: 'hidden_fields_color') + String hiddenFieldsColor; DetailUserModel({ this.username, this.email, this.seen, + this.hiddenFields, + this.hiddenFieldsVerbose, + this.canSeeHiddenFields, + this.hiddenFieldsColor, name, tanggalLahir, phoneNumber, @@ -130,11 +142,15 @@ class DetailUserModel extends BaseUserModel { UpdateUserModel toUpdateUserModel() { var thisData = toJson(); - thisData.remove('username'); - thisData.remove('email'); return UpdateUserModel.fromJson(thisData); } + Color getHiddenFieldsColor() { + final strColor = hiddenFieldsColor.replaceAll('#', '0xff'); + final hexColor = int.parse(strColor); + return Color(hexColor); + } + factory DetailUserModel.fromJson(Map json) => _$DetailUserModelFromJson(json); diff --git a/lib/model/user.g.dart b/lib/model/user.g.dart index f4aac33..27c2d4b 100644 --- a/lib/model/user.g.dart +++ b/lib/model/user.g.dart @@ -71,6 +71,13 @@ DetailUserModel _$DetailUserModelFromJson(Map json) { username: json['username'] as String, email: json['email'] as String, seen: json['seen'] as bool, + hiddenFields: + (json['hidden_fields'] as List)?.map((e) => e as String)?.toList(), + hiddenFieldsVerbose: (json['hidden_fields_verbose'] as List) + ?.map((e) => e as String) + ?.toList(), + canSeeHiddenFields: json['can_see_hidden_fields'] as bool, + hiddenFieldsColor: json['hidden_fields_color'] as String, name: json['name'], tanggalLahir: json['tanggal_lahir'], phoneNumber: json['phone_number'], @@ -80,8 +87,7 @@ DetailUserModel _$DetailUserModelFromJson(Map json) { alamat: json['alamat'], foto: json['foto'], organisasiKomunitas: json['organisasi_komunitas'], - )..hiddenFields = - (json['hidden_fields'] as List)?.map((e) => e as String)?.toList(); + ); } Map _$DetailUserModelToJson(DetailUserModel instance) => @@ -99,4 +105,7 @@ Map _$DetailUserModelToJson(DetailUserModel instance) => 'email': instance.email, 'seen': instance.seen, 'hidden_fields': instance.hiddenFields, + 'hidden_fields_verbose': instance.hiddenFieldsVerbose, + 'can_see_hidden_fields': instance.canSeeHiddenFields, + 'hidden_fields_color': instance.hiddenFieldsColor, }; diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index 743613e..2210b95 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -199,7 +199,7 @@ class _EditProfileState extends State { ), Center( child: Text( - 'Data yang akan dirahasiakan adalah: ${widget.user.hiddenFields.join(', ')}', + 'Data yang akan dirahasiakan adalah: ${widget.user.hiddenFieldsVerbose.join(', ')}', style: TextStyle( fontSize: 10.0, ), diff --git a/lib/page/profile/profile.dart b/lib/page/profile/profile.dart index 1e34422..fe3749a 100644 --- a/lib/page/profile/profile.dart +++ b/lib/page/profile/profile.dart @@ -6,12 +6,11 @@ import 'package:bisaGo/network/data/network_model.dart'; import 'package:bisaGo/page/profile/edit_profile.dart'; import 'package:bisaGo/page/profile/profile_picture.dart'; import 'package:bisaGo/utils/datetime_utils.dart'; +import 'package:bisaGo/utils/profile_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -const rahasia = Color(0xFF585858); - class Profile extends StatefulWidget { final String email; final bool isPublic; @@ -28,6 +27,7 @@ class Profile extends StatefulWidget { class _ProfileState extends State { DetailUserModel user; + DetailUserModel viewUser; String email; UserBloc _bloc; bool fetched; @@ -128,21 +128,22 @@ class _ProfileState extends State { if (!widget.isPublic) Text( 'Halo, ${user.name.split(' ')[0]}!', - style: const TextStyle( + style: TextStyle( fontSize: 26, fontWeight: FontWeight.w900, - color: Colors.white, - fontFamily: 'Comfortaa', + color: + _getFontColor('name', seenColor: Colors.white), + fontFamily: 'Muli', ), textAlign: TextAlign.center, ), Text( user.email, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: rahasia, - fontFamily: 'Comfortaa', + color: _getFontColor('email', seenColor: Colors.white), + fontFamily: 'Muli', ), textAlign: TextAlign.center, ), @@ -183,11 +184,11 @@ class _ProfileState extends State { ), title: Text( user.name, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Colors.black, - fontFamily: 'Comfortaa', + color: _getFontColor('name'), + fontFamily: 'Muli', ), textAlign: TextAlign.left, ), @@ -200,11 +201,11 @@ class _ProfileState extends State { ), title: Text( tanggalLahir, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: rahasia, - fontFamily: 'Comfortaa', + color: _getFontColor('tanggal_lahir'), + fontFamily: 'Muli', ), textAlign: TextAlign.left, ), @@ -217,11 +218,11 @@ class _ProfileState extends State { ), title: Text( user.phoneNumber, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: rahasia, - fontFamily: 'Comfortaa', + color: _getFontColor('phone_number'), + fontFamily: 'Muli', ), textAlign: TextAlign.left, ), @@ -237,11 +238,11 @@ class _ProfileState extends State { user.jenisKelamin == null ? '-' : user.jenisKelamin, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Colors.black, - fontFamily: 'Comfortaa', + color: _getFontColor('jenis_kelamin'), + fontFamily: 'Muli', ), textAlign: TextAlign.left, ), @@ -256,11 +257,11 @@ class _ProfileState extends State { user.disabilitas == '' || user.disabilitas == null ? 'Belum memilih' : user.disabilitas, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Colors.black, - fontFamily: 'Comfortaa', + color: _getFontColor('disabilitas'), + fontFamily: 'Muli', ), textAlign: TextAlign.left, ), @@ -275,11 +276,11 @@ class _ProfileState extends State { user.pekerjaan == '' || user.pekerjaan == null ? '-' : user.pekerjaan, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Colors.black, - fontFamily: 'Comfortaa', + color: _getFontColor('pekerjaan'), + fontFamily: 'Muli', ), textAlign: TextAlign.left, ), @@ -294,11 +295,11 @@ class _ProfileState extends State { user.alamat == '' || user.alamat == null ? '-' : user.alamat, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: rahasia, - fontFamily: 'Comfortaa', + color: _getFontColor('alamat'), + fontFamily: 'Muli', ), textAlign: TextAlign.left, ), @@ -314,11 +315,11 @@ class _ProfileState extends State { user.organisasiKomunitas == null ? '-' : user.organisasiKomunitas, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Colors.black, - fontFamily: 'Comfortaa', + color: _getFontColor('organisasi_komunitas'), + fontFamily: 'Muli', ), textAlign: TextAlign.left, ), @@ -336,6 +337,15 @@ class _ProfileState extends State { ); } + Color _getFontColor(String fieldName, {Color seenColor = seenColor}) { + if (user.canSeeHiddenFields) return seenColor; + final hiddenFields = user.hiddenFields; + if (hiddenFields.contains(fieldName)) { + return user.getHiddenFieldsColor(); + } + return seenColor; + } + void _navigateToEditProfile(BuildContext context) { final route = MaterialPageRoute(builder: (_) => EditProfile(user: user)); Navigator.of(context).push(route); diff --git a/lib/utils/profile_utils.dart b/lib/utils/profile_utils.dart index 49ae8ef..0899015 100644 --- a/lib/utils/profile_utils.dart +++ b/lib/utils/profile_utils.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; + +const seenColor = Colors.black; String getNameInitials(String name) { if (name.isEmpty) return ''; -- GitLab From 57a8321b160e488c549b9b68ae2398c267c5b793 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Fri, 4 Jun 2021 23:59:47 +0700 Subject: [PATCH 39/45] [CHORE] fix login with google --- lib/page/login/login.dart | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart index 438b33f..d33f2fe 100644 --- a/lib/page/login/login.dart +++ b/lib/page/login/login.dart @@ -225,6 +225,7 @@ class LoginState extends State { } Future _updateUser(UpdateUserModel updateUserModel) async { + updateUserModel.foto = ''; final updatedUser = await _userBloc.updateUser(updateUserModel); if (updatedUser != null) { successUserUpdateDialog(context); @@ -349,23 +350,14 @@ class LoginState extends State { } Future _handleSignIn() async { - try { - await _handleSignOut(); - final googleSignInAccount = await _googleSignIn.signIn(); - final googleSignInAuthentication = - await googleSignInAccount.authentication; + await _handleSignOut(); + final googleSignInAccount = await _googleSignIn.signIn(); + final googleSignInAuthentication = + await googleSignInAccount.authentication; - final token = googleSignInAuthentication.accessToken; - await login( - _currentUser.email, '', 'true', token, _currentUser.displayName); - sharedPreferences = await SharedPreferences.getInstance(); - successDialog(context); - Timer(const Duration(seconds: 2), () { - Navigator.pushNamed(context, '/'); - }); - } catch (error) { - failedDialog(context); - } + final token = googleSignInAuthentication.accessToken; + await login( + _currentUser.email, '', 'true', token, _currentUser.displayName); } Future _handleSignOut() async { -- GitLab From 6c420d6994efe8240325d90b345aa2c47bf2d174 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Sat, 5 Jun 2021 00:01:56 +0700 Subject: [PATCH 40/45] [REFACTOR] format file --- lib/page/login/login.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart index d33f2fe..2ad6073 100644 --- a/lib/page/login/login.dart +++ b/lib/page/login/login.dart @@ -352,8 +352,7 @@ class LoginState extends State { Future _handleSignIn() async { await _handleSignOut(); final googleSignInAccount = await _googleSignIn.signIn(); - final googleSignInAuthentication = - await googleSignInAccount.authentication; + final googleSignInAuthentication = await googleSignInAccount.authentication; final token = googleSignInAuthentication.accessToken; await login( -- GitLab From 3188c39fe0525f4e06b245db482dda66265f3abf Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Sat, 5 Jun 2021 14:05:55 +0700 Subject: [PATCH 41/45] [CHORE] fix typo 'Organisasi / Komunitasi' to 'Organisasi / Komunitas' --- lib/page/profile/edit_profile.dart | 2 +- lib/page/registrasi/registrasi.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/page/profile/edit_profile.dart b/lib/page/profile/edit_profile.dart index 2210b95..69f4b32 100644 --- a/lib/page/profile/edit_profile.dart +++ b/lib/page/profile/edit_profile.dart @@ -326,7 +326,7 @@ class _EditProfileState extends State { }, ), CustomTextField( - title: 'Organisasi / Komunitasi', + title: 'Organisasi / Komunitas', required: true, key: const Key('Text Field Organisasi'), controller: organisasiController, diff --git a/lib/page/registrasi/registrasi.dart b/lib/page/registrasi/registrasi.dart index c1817f6..61efcf0 100644 --- a/lib/page/registrasi/registrasi.dart +++ b/lib/page/registrasi/registrasi.dart @@ -210,7 +210,7 @@ class RegistrasiState extends State { }, ), CustomTextField( - title: 'Organisasi / Komunitasi', + title: 'Organisasi / Komunitas', key: const Key('Text Field Organisasi'), controller: organisasiController, ), -- GitLab From 58259cdab1183853eb0ebaaa50790086608732e2 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Sat, 5 Jun 2021 15:14:13 +0700 Subject: [PATCH 42/45] [CHORE] fix wait for 2 seconds after login to show success login dialog --- lib/page/login/login.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/page/login/login.dart b/lib/page/login/login.dart index 2ad6073..9946257 100644 --- a/lib/page/login/login.dart +++ b/lib/page/login/login.dart @@ -273,6 +273,7 @@ class LoginState extends State { }); successDialog(context); + await Future.delayed(const Duration(seconds: 2)); _navigateToDashboard(context); if (updateUser.disabilitas == '-') { updateUser.disabilitas = 'Tidak memiliki disabilitas'; -- GitLab From 67cc76a83a484c796f7bb296f6e112ca710ce528 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Sat, 5 Jun 2021 23:32:27 +0700 Subject: [PATCH 43/45] [CHORE] fix test, now komentar posting have default datetime format --- lib/model/komentar.dart | 7 ++++++- lib/model/komentar.g.dart | 2 +- lib/model/komentar_posting_kegiatan.g.dart | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/model/komentar.dart b/lib/model/komentar.dart index 1efb152..8123816 100644 --- a/lib/model/komentar.dart +++ b/lib/model/komentar.dart @@ -1,3 +1,4 @@ +import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:bisaGo/config/custom_serializer.dart'; @@ -16,7 +17,7 @@ class KomentarModel { final String namaLokasi; final String deskripsi; final String creator; - @JsonKey(name: 'date_time', fromJson: CustomSerializer.stringToDateTime) + @JsonKey(name: 'date_time', fromJson: _stringToDateTime) final DateTime dateTime; final String tag; final List disabilitas; @@ -48,3 +49,7 @@ class KomentarModel { Map toJson() => _$KomentarModelToJson(this); } + +DateTime _stringToDateTime(String date) { + return DateFormat('dd-MM-yyyy hh:mm').parse(date); +} diff --git a/lib/model/komentar.g.dart b/lib/model/komentar.g.dart index 314c20c..85c8277 100644 --- a/lib/model/komentar.g.dart +++ b/lib/model/komentar.g.dart @@ -27,7 +27,7 @@ KomentarModel _$KomentarModelFromJson(Map json) { namaLokasi: json['nama_lokasi'] as String, deskripsi: json['deskripsi'] as String, creator: json['creator'] as String, - dateTime: CustomSerializer.stringToDateTime(json['date_time'] as String), + dateTime: _stringToDateTime(json['date_time'] as String), tag: json['tag'] as String, disabilitas: (json['disabilitas'] as List)?.map((e) => e as String)?.toList(), diff --git a/lib/model/komentar_posting_kegiatan.g.dart b/lib/model/komentar_posting_kegiatan.g.dart index 521b139..0a08508 100644 --- a/lib/model/komentar_posting_kegiatan.g.dart +++ b/lib/model/komentar_posting_kegiatan.g.dart @@ -29,7 +29,7 @@ KomentarPostingKegiatanModel _$KomentarPostingKegiatanModelFromJson( id: json['id'] as int, creator: json['creator'] as String, deskripsi: json['deskripsi'] as String, - created: _stringToDateTime(json['created'] as String), + created: CustomSerializer.stringToDateTime(json['created'] as String), creatorEmail: json['creator_email'] as String, creatorPicture: json['creator_picture'] as String, ); -- GitLab From b16a7f3caad26641a1960d1eeb3ca939ccdda6f2 Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Sun, 6 Jun 2021 14:01:11 +0700 Subject: [PATCH 44/45] [CHORE] Done PBI 10, added previewable profile from kegiatan and komentar kegiatan --- lib/model/kegiatan.dart | 3 + lib/model/kegiatan.g.dart | 2 + lib/model/komentar.dart | 1 - lib/page/filter_fasilitas/kegiatan.dart | 1 + .../postingan/detail_post_kegiatan.dart | 57 ++++++++++--------- 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/lib/model/kegiatan.dart b/lib/model/kegiatan.dart index d224401..eab87fd 100644 --- a/lib/model/kegiatan.dart +++ b/lib/model/kegiatan.dart @@ -17,6 +17,8 @@ class KegiatanModel { String creator; @JsonKey(name: 'nama_kegiatan') String namaKegiatan; + @JsonKey(name: 'creator_email') + String creatorEmail; String penyelenggara; String deskripsi; @JsonKey(name: 'nama_kontak') @@ -38,6 +40,7 @@ class KegiatanModel { this.placeId, this.creator, this.namaKegiatan, + this.creatorEmail, this.penyelenggara, this.deskripsi, this.namaKontak, diff --git a/lib/model/kegiatan.g.dart b/lib/model/kegiatan.g.dart index 90f5b03..ca5b091 100644 --- a/lib/model/kegiatan.g.dart +++ b/lib/model/kegiatan.g.dart @@ -27,6 +27,7 @@ KegiatanModel _$KegiatanModelFromJson(Map json) { placeId: json['place_id'] as String, creator: json['creator'] as String, namaKegiatan: json['nama_kegiatan'] as String, + creatorEmail: json['creator_email'] as String, penyelenggara: json['penyelenggara'] as String, deskripsi: json['deskripsi'] as String, namaKontak: json['nama_kontak'] as String, @@ -45,6 +46,7 @@ Map _$KegiatanModelToJson(KegiatanModel instance) => 'place_id': instance.placeId, 'creator': instance.creator, 'nama_kegiatan': instance.namaKegiatan, + 'creator_email': instance.creatorEmail, 'penyelenggara': instance.penyelenggara, 'deskripsi': instance.deskripsi, 'nama_kontak': instance.namaKontak, diff --git a/lib/model/komentar.dart b/lib/model/komentar.dart index 8123816..68bbeb1 100644 --- a/lib/model/komentar.dart +++ b/lib/model/komentar.dart @@ -1,6 +1,5 @@ import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'package:bisaGo/config/custom_serializer.dart'; part 'komentar.g.dart'; diff --git a/lib/page/filter_fasilitas/kegiatan.dart b/lib/page/filter_fasilitas/kegiatan.dart index 8055ac0..ded35a4 100644 --- a/lib/page/filter_fasilitas/kegiatan.dart +++ b/lib/page/filter_fasilitas/kegiatan.dart @@ -29,6 +29,7 @@ class _KegiatanState extends State { id: widget.kegiatan.id, placeId: widget.kegiatan.placeId, creator: widget.kegiatan.creator, + creatorEmail: widget.kegiatan.creatorEmail, namaKegiatan: widget.kegiatan.namaKegiatan, penyelenggara: widget.kegiatan.penyelenggara, deskripsi: widget.kegiatan.deskripsi, diff --git a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart index c8262c0..b16667c 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart @@ -2,7 +2,10 @@ import 'dart:async'; import 'package:bisaGo/model/kegiatan.dart'; import 'package:bisaGo/model/lokasi.dart'; +import 'package:bisaGo/model/user.dart'; import 'package:bisaGo/page/filter_fasilitas/kegiatan_list_images.dart'; +import 'package:bisaGo/page/profile/profile.dart'; +import 'package:bisaGo/page/profile/profile_picture.dart'; import 'package:bisaGo/repository/dynamic_links_service_repository.dart'; import 'package:bisaGo/utils/share_utils.dart'; import 'package:bisaGo/utils/validator.dart'; @@ -349,14 +352,24 @@ class _DetailPostKegiatanPageState extends State { constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.3), - child: Text( - '${widget.kegiatan.creator}', - key: Key('creator-${widget.kegiatan.creator}'), - overflow: TextOverflow.fade, - softWrap: false, - style: const TextStyle( - fontSize: 12, - fontStyle: FontStyle.italic, + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => Profile( + email: widget.kegiatan.creatorEmail, + isPublic: true))); + }, + child: Text( + '${widget.kegiatan.creator}', + key: Key('creator-${widget.kegiatan.creator}'), + overflow: TextOverflow.fade, + softWrap: false, + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), ), ), ), @@ -411,6 +424,8 @@ class _DetailPostKegiatanPageState extends State { .map((k) => komentarKegiatanPlaceHolder( k.creator, + k.creatorEmail, + k.creatorPicture, k.created, k.deskripsi)) .toList()); @@ -604,21 +619,17 @@ class _DetailPostKegiatanPageState extends State { } Widget komentarKegiatanPlaceHolder( - String name, DateTime created, String description) { + String name, String email, String foto, DateTime created, String description) { + final user = DetailUserModel() + ..email = email + ..foto = foto + ..name = name; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - CircleAvatar( - backgroundColor: greenPrimary, - child: Text( - _creatorInitials(name), - style: const TextStyle( - color: Colors.white, - ), - ), - ), + ProfilePicture(user, redirectToDetailProfile: true), Padding( padding: const EdgeInsets.all(regularSpace), child: Column( @@ -649,16 +660,6 @@ class _DetailPostKegiatanPageState extends State { ); } - String _creatorInitials(String name) { - if (name.isEmpty) return ''; - var initials = ''; - for (final i in name.split(' ')) { - initials += '${i[0].toUpperCase()}'; - } - if (initials.length > 2) return initials.substring(0, 2); - return initials; - } - // Future _updateInformasi() async { // final sharedPreferences = await SharedPreferences.getInstance(); // if (sharedPreferences.getString('token') == null) { -- GitLab From 125059ae2f884d14d6cdad0a50ab28631c938a7a Mon Sep 17 00:00:00 2001 From: ariqbasyar Date: Sun, 6 Jun 2021 14:36:13 +0700 Subject: [PATCH 45/45] [CHORE] redirect to detail profile when click komentar's creator name --- .../postingan/detail_post_kegiatan.dart | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart index b16667c..fba26d5 100644 --- a/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart +++ b/lib/page/filter_fasilitas/postingan/detail_post_kegiatan.dart @@ -635,11 +635,24 @@ class _DetailPostKegiatanPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - name, - style: const TextStyle(fontSize: 18), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => Profile( + email: email, + isPublic: true, + ), + ), + ); + }, + child: Text( + name, + style: const TextStyle(fontSize: 18), + ), ), - Text('${DateFormat('dd MMMM yyy hh:mm').format(created)}', + Text('${DateFormat('dd MMMM yyyy hh:mm').format(created)}', style: const TextStyle(color: grayPrimary, fontSize: 14)) ], ), -- GitLab