diff --git a/.github/workflows/BuildAPKDebug.yml b/.github/workflows/BuildAPKDebug.yml index 7111896..9be530c 100644 --- a/.github/workflows/BuildAPKDebug.yml +++ b/.github/workflows/BuildAPKDebug.yml @@ -2,11 +2,9 @@ name: Build Mobile Debug Staging on: push: - branches: - # Only run when there's a push / commits on main branch - - "staging" - paths: - - "mobile/**" + tags: + # Can be built anywhere with tags "internal-v*" + - "internal-v*" jobs: BuildDebug: @@ -23,9 +21,9 @@ jobs: with: timezoneLinux: "Asia/Jakarta" - - name: Create Tags + - name: Get Tags id: tags - run: echo "::set-output name=sha_short::${GITHUB_SHA::7}" + run: echo "::set-output name=tag::${GITHUB_REF#refs/*/}" - name: Get Current Date id: date @@ -61,9 +59,8 @@ jobs: - name: "Create Release" uses: ncipollo/release-action@v1 with: - name: "Debug Auto Build - ${{ steps.date.outputs.date }}" - tag: ${{ steps.tags.outputs.sha_short }} - commit: "main" + name: "Debug Auto Build - ${{ steps.tags.outputs.tag }} - ${{ steps.date.outputs.date }}" artifacts: "mobile/artifacts/*.apk" - body: "An auto build on staging at ${{ steps.date.outputs.date }}" + allowUpdates: "true" + body: "An auto build for internal at ${{ steps.date.outputs.date }} for ${{ steps.tags.outputs.tag }}" token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/BuildAPKRelease.yml b/.github/workflows/BuildAPKRelease.yml index a9e3eb0..41efe18 100644 --- a/.github/workflows/BuildAPKRelease.yml +++ b/.github/workflows/BuildAPKRelease.yml @@ -3,10 +3,11 @@ name: Build Mobile Release on: push: branches: - # Only run when there's a push / commits on main branch + # Only can be built on Main - "main" - paths: - - "mobile/**" + tags: + # With a tags "v*" + - "v*" jobs: BuildRelease: @@ -23,9 +24,9 @@ jobs: with: timezoneLinux: "Asia/Jakarta" - - name: Create Tags + - name: Get Tags id: tags - run: echo "::set-output name=sha_short::${GITHUB_SHA::7}" + run: echo "::set-output name=tag::${GITHUB_REF#refs/*/}" - name: Get Current Date id: date @@ -61,9 +62,8 @@ jobs: - name: "Create Release" uses: ncipollo/release-action@v1 with: - name: "Release Auto Build - ${{ steps.date.outputs.date }}" - tag: ${{ steps.tags.outputs.sha_short }} - commit: "main" + name: "Release Auto Build - ${{ steps.tags.outputs.tag }} - ${{ steps.date.outputs.date }}" artifacts: "mobile/artifacts/*.apk" - body: "An auto build on main at ${{ steps.date.outputs.date }}" - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + allowUpdates: "true" + body: "An auto build for release at ${{ steps.date.outputs.date }} for ${{ steps.tags.outputs.tag }}" + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/mobile/lib/api/review.dart b/mobile/lib/api/review.dart index 1bf406e..2cc18b1 100644 --- a/mobile/lib/api/review.dart +++ b/mobile/lib/api/review.dart @@ -64,7 +64,7 @@ Future createReview( if (e.response != null) { var response = e.response!; if (response.statusCode == 401) { - profile.setLoggedOut(); + await profile.setLoggedOut(); return Future.error("Session expired"); } if (response.statusCode == 400) { diff --git a/mobile/lib/api/tikum.dart b/mobile/lib/api/tikum.dart index 5ffce7d..ee3235a 100644 --- a/mobile/lib/api/tikum.dart +++ b/mobile/lib/api/tikum.dart @@ -31,7 +31,7 @@ Future> getMyTikum() async { ); if (response.statusCode == 401) { - profile.setLoggedOut(); + await profile.setLoggedOut(); return Future.error("Session expired"); } @@ -64,7 +64,7 @@ Future deleteMyTikum(int id) async { ); if (response.statusCode == 401) { - profile.setLoggedOut(); + await profile.setLoggedOut(); return Future.error("Session expired"); } @@ -106,7 +106,7 @@ Future createTikum({ if (e.response != null) { var response = e.response!; if (response.statusCode == 401) { - profile.setLoggedOut(); + await profile.setLoggedOut(); return Future.error("Session expired"); } if (response.statusCode == 400) { diff --git a/mobile/lib/api/user.dart b/mobile/lib/api/user.dart index 74b74e8..aa49bb7 100644 --- a/mobile/lib/api/user.dart +++ b/mobile/lib/api/user.dart @@ -20,7 +20,7 @@ Future loginUser(String email, String password) async { var id = decoded["user"]["id"]; var profile = await SecureProfile.getStorage(); - profile.setLoggedIn(id, token); + await profile.setLoggedIn(id, token); } else if (response.statusCode == 400) { return Future.error("Email atau password salah"); } else { @@ -59,7 +59,7 @@ Future> getMyReviewById() async { ); if (response.statusCode == 401) { - profile.setLoggedOut(); + await profile.setLoggedOut(); return Future.error("Session expired, please login again"); } @@ -88,7 +88,7 @@ Future getMyProfileById() async { ); if (response.statusCode == 401) { - profile.setLoggedOut(); + await profile.setLoggedOut(); return Future.error("Session expired, please login again"); } @@ -118,7 +118,7 @@ Future deleteMyReview(int id) async { ); if (response.statusCode == 401) { - profile.setLoggedOut(); + await profile.setLoggedOut(); return Future.error("Session expired, please login again"); } @@ -144,7 +144,7 @@ Future userLogout() async { ); if (res.statusCode == 401) { - profile.setLoggedOut(); + await profile.setLoggedOut(); return Future.error("Session expired, please login again"); } @@ -152,7 +152,7 @@ Future userLogout() async { return Future.error("Failed to logout"); } - profile.setLoggedOut(); + await profile.setLoggedOut(); } Future getUserProfileById(int id) async { diff --git a/mobile/lib/model/profile.dart b/mobile/lib/model/profile.dart index 4a61135..20bbc85 100644 --- a/mobile/lib/model/profile.dart +++ b/mobile/lib/model/profile.dart @@ -1,8 +1,8 @@ import "./user.dart"; -import "./review.dart"; class Profile { - final dynamic avgRating, totalReview; + final double avgRating; + final int totalReview; // final List reviews; final User user; @@ -23,8 +23,8 @@ class Profile { return Profile( user: user, // reviews: json["reviews"], - totalReview: json["total_review"], - avgRating: json["avg_ratings"], + totalReview: json["total_review"].toInt(), + avgRating: json["avg_ratings"].toDouble(), ); } } diff --git a/mobile/lib/model/profile_secure.dart b/mobile/lib/model/profile_secure.dart index 375d869..a0a1ba4 100644 --- a/mobile/lib/model/profile_secure.dart +++ b/mobile/lib/model/profile_secure.dart @@ -63,13 +63,13 @@ class SecureProfile { return userId; } - void setLoggedIn(int userId, String apiKey) async { + Future setLoggedIn(int userId, String apiKey) async { await storage.setInt("user_id", userId); await storage.setString("api_key", apiKey); isLoggedIn = true; } - void setLoggedOut() async { + Future setLoggedOut() async { await storage.remove("api_key"); await storage.remove("user_id"); isLoggedIn = false; diff --git a/mobile/lib/screen/_global/components/image_network.dart b/mobile/lib/screen/_global/components/image_network.dart new file mode 100644 index 0000000..b640115 --- /dev/null +++ b/mobile/lib/screen/_global/components/image_network.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:mobile/screen/_global/components/shimmer/shimmer_container.dart'; + +class ImageNetworkWShimmer extends StatelessWidget { + final String link; + double? width, height; + BoxFit? fit; + ImageNetworkWShimmer( + {Key? key, required this.link, this.width, this.height, this.fit}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Image.network(link, fit: fit, width: width, height: height, + loadingBuilder: (context, child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) return child; + return ShimmerContainer( + width: width, + height: height ?? 300, + ); + }); + } +} diff --git a/mobile/lib/screen/_global/components/profile_card.dart b/mobile/lib/screen/_global/components/profile_card.dart new file mode 100644 index 0000000..cce218c --- /dev/null +++ b/mobile/lib/screen/_global/components/profile_card.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:mobile/model/profile.dart'; +import "dart:math" as math; + +import 'package:mobile/screen/_global/components/image_network.dart'; + +class ProfileCard extends StatelessWidget { + final Profile data; + // final GlobalKey navKey; + final int randomForProfile = math.Random().nextInt(1000); + + ProfileCard({Key? key, required this.data}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 30), + height: 100, + width: 100, + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: ImageNetworkWShimmer( + link: + "https://www.thiswaifudoesnotexist.net/example-$randomForProfile.jpg", + fit: BoxFit.cover, + ), + ), + ), + Text( + data.user.name.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + Container( + margin: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 15, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 20, + ), + child: Column( + // ignore: prefer_const_literals_to_create_immutables + children: [ + const Text("Total Review"), + Text(data.totalReview.toString()), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 20, + ), + child: Column( + // ignore: prefer_const_literals_to_create_immutables + children: [ + const Text("Rataan Rating"), + Text(data.avgRating.toStringAsFixed(2)), + ], + ), + ), + ], + ), + ), + ]), + ); + } +} diff --git a/mobile/lib/screen/_global/components/shimmer/profile_shimmer.dart b/mobile/lib/screen/_global/components/shimmer/profile_shimmer.dart new file mode 100644 index 0000000..4b89ee9 --- /dev/null +++ b/mobile/lib/screen/_global/components/shimmer/profile_shimmer.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:mobile/screen/_global/components/shimmer/shimmer_container.dart'; + +class ShimmerProfile extends StatelessWidget { + const ShimmerProfile({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + margin: const EdgeInsets.symmetric(vertical: 30), + height: 100, + width: 100, + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: const ShimmerContainer( + width: 50, + height: 50, + )), + ), + ShimmerContainer( + width: MediaQuery.of(context).size.width * .4, + ), + Container( + margin: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 15, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 20, + ), + child: Column( + // ignore: prefer_const_literals_to_create_immutables + children: [ + const ShimmerContainer(width: 50), + const SizedBox(height: 5), + ShimmerContainer( + width: MediaQuery.of(context).size.width * .3) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 20, + ), + child: Column( + // ignore: prefer_const_literals_to_create_immutables + children: [ + const ShimmerContainer(width: 50), + const SizedBox(height: 5), + ShimmerContainer( + width: MediaQuery.of(context).size.width * .3) + ], + ), + ), + ], + ), + ), + ]), + ); + } +} diff --git a/mobile/lib/screen/_global/components/shimmer/review_shimmer.dart b/mobile/lib/screen/_global/components/shimmer/review_shimmer.dart new file mode 100644 index 0000000..f7eb47f --- /dev/null +++ b/mobile/lib/screen/_global/components/shimmer/review_shimmer.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:mobile/screen/_global/components/shimmer/shimmer_container.dart'; + +class ShimmerReview extends StatelessWidget { + const ShimmerReview({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + margin: const EdgeInsets.only(left: 10, right: 10, top: 10), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(50), + child: const ShimmerContainer( + width: 40, + height: 40, + )), + const SizedBox( + width: 15, + ), + const ShimmerContainer( + width: 200, + ) + ], + ), + ), + const SizedBox(height: 10), + ShimmerContainer( + width: MediaQuery.of(context).size.width, + height: 200, + ), + Container( + margin: const EdgeInsets.only(left: 10, right: 10, bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 5), + ShimmerContainer( + width: MediaQuery.of(context).size.width * .5, + ), + const SizedBox(height: 10), + ShimmerContainer( + width: MediaQuery.of(context).size.width * .8, + ), + const SizedBox(height: 5), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ShimmerContainer(width: 50)], + ) + ], + )), + const Divider( + color: Colors.black, + height: 2.0, + thickness: 1.0, + ) + ], + ); + } +} diff --git a/mobile/lib/screen/_global/components/shimmer/shimmer_container.dart b/mobile/lib/screen/_global/components/shimmer/shimmer_container.dart new file mode 100644 index 0000000..ec088c3 --- /dev/null +++ b/mobile/lib/screen/_global/components/shimmer/shimmer_container.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class ShimmerContainer extends StatelessWidget { + final double? width, height; + final Decoration? decoration; + const ShimmerContainer({Key? key, this.height, this.width, this.decoration}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey, + highlightColor: Colors.white, + child: Container( + width: width, + height: height, + padding: const EdgeInsets.all(10), + decoration: decoration ?? + BoxDecoration( + color: Colors.black26, borderRadius: BorderRadius.circular(5)), + ), + ); + } +} diff --git a/mobile/lib/screen/_global/components/shimmer/tikum_shimmer.dart b/mobile/lib/screen/_global/components/shimmer/tikum_shimmer.dart new file mode 100644 index 0000000..8d3fa88 --- /dev/null +++ b/mobile/lib/screen/_global/components/shimmer/tikum_shimmer.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:mobile/screen/_global/components/shimmer/shimmer_container.dart'; + +class ShimmerTikum extends StatelessWidget { + const ShimmerTikum({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column(children: [ + Container( + padding: + const EdgeInsets.only(left: 25, right: 25, top: 15, bottom: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(50), + child: const ShimmerContainer( + width: 40, + height: 40, + )), + const SizedBox( + width: 15, + ), + const ShimmerContainer( + width: 200, + ) + ], + ), + const SizedBox( + height: 20, + ), + const ShimmerContainer( + width: 500, + ), + const SizedBox( + height: 25, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerContainer( + width: 200, + ), + SizedBox( + height: 5, + ), + ShimmerContainer( + width: 400, + ), + ], + ), + const SizedBox( + height: 10, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + ShimmerContainer( + width: 200, + ), + SizedBox( + height: 5, + ), + ShimmerContainer( + width: 400, + ), + ], + ), + const SizedBox( + height: 10, + ), + ], + ), + ), + const Divider( + color: Colors.black, + height: 4, + ) + ]); + } +} diff --git a/mobile/lib/screen/form_login/login_screen.dart b/mobile/lib/screen/form_login/login_screen.dart index c09763e..e4ef1f6 100644 --- a/mobile/lib/screen/form_login/login_screen.dart +++ b/mobile/lib/screen/form_login/login_screen.dart @@ -5,7 +5,6 @@ import 'package:mobile/api/user.dart'; import 'package:mobile/screen/form_register/register_screen.dart'; import 'package:mobile/screen/home/home_screen.dart'; import 'package:mobile/utils/show_snackbar.dart'; -import 'package:mobile/model/profile_secure.dart'; class LoginScreenArguments { LoginScreenArguments(); diff --git a/mobile/lib/screen/form_review/form_review_screen.dart b/mobile/lib/screen/form_review/form_review_screen.dart index fc1d45b..3b494f4 100644 --- a/mobile/lib/screen/form_review/form_review_screen.dart +++ b/mobile/lib/screen/form_review/form_review_screen.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; diff --git a/mobile/lib/screen/form_tikum/form_tikum_screen.dart b/mobile/lib/screen/form_tikum/form_tikum_screen.dart index 13a25e4..3a2fbe9 100644 --- a/mobile/lib/screen/form_tikum/form_tikum_screen.dart +++ b/mobile/lib/screen/form_tikum/form_tikum_screen.dart @@ -82,275 +82,273 @@ class _FormTikumScreenState extends State { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - resizeToAvoidBottomInset: false, body: SafeArea( child: Padding( padding: const EdgeInsets.all(12.0), - child: Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - children: [ - Row( - children: [ - TextButton( - style: ButtonStyle( - overlayColor: - MaterialStateProperty.all(Colors.black12)), - onPressed: _handleKembaliButton, - child: const Text( - "Kembali", + child: Stack( + children: [ + TextButton( + style: ButtonStyle( + overlayColor: MaterialStateProperty.all(Colors.black12)), + onPressed: _handleKembaliButton, + child: const Text( + "Kembali", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 15), + ), + ), + Center( + child: SingleChildScrollView( + reverse: true, + child: Form( + key: _formKey, + child: Column( + children: [ + const Text( + "Share Your Tikum", + textAlign: TextAlign.center, style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 15), + fontWeight: FontWeight.bold, fontSize: 30), ), - ), - ], - ), - const Text( - "Share Your Tikum", - textAlign: TextAlign.center, - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 30), - ), - const SizedBox( - height: 20, - ), - const Align( - alignment: Alignment.bottomLeft, - child: Text( - "Tempat Tujuan", - textAlign: TextAlign.start, - ), - ), - const SizedBox( - height: 8, - ), - TextFormField( - controller: _tujuanController, - decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(horizontal: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10)), - hintText: "Enter Name", - ), - ), - const SizedBox( - height: 10, - ), - const Align( - alignment: Alignment.bottomLeft, - child: Text( - "Tempat Kumpul", - textAlign: TextAlign.start, - ), - ), - const SizedBox( - height: 8, - ), - TextFormField( - controller: _kumpulController, - decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(horizontal: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10)), - hintText: "Enter", - ), - ), - const SizedBox( - height: 10, - ), - const Align( - alignment: Alignment.bottomLeft, - child: Text( - "Waktu Kumpul", - textAlign: TextAlign.start, - ), - ), - const SizedBox( - height: 8, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: 200, - child: Flexible( - child: TextFormField( - controller: - _dateController, //editing controller of this TextField - decoration: InputDecoration( - prefixIcon: const Padding( - padding: EdgeInsets.all(0.0), - child: Icon( - Icons.calendar_today, - ), // icon is 48px widget. + const SizedBox( + height: 20, + ), + const Align( + alignment: Alignment.bottomLeft, + child: Text( + "Tempat Tujuan", + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 8, + ), + TextFormField( + controller: _tujuanController, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10)), + hintText: "Enter Name", + ), + ), + const SizedBox( + height: 10, + ), + const Align( + alignment: Alignment.bottomLeft, + child: Text( + "Tempat Kumpul", + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 8, + ), + TextFormField( + controller: _kumpulController, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10)), + hintText: "Enter", + ), + ), + const SizedBox( + height: 10, + ), + const Align( + alignment: Alignment.bottomLeft, + child: Text( + "Waktu Kumpul", + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 8, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 200, + child: Flexible( + child: TextFormField( + controller: + _dateController, //editing controller of this TextField + decoration: InputDecoration( + prefixIcon: const Padding( + padding: EdgeInsets.all(0.0), + child: Icon( + Icons.calendar_today, + ), // icon is 48px widget. + ), + contentPadding: + const EdgeInsets.symmetric( + horizontal: 12), + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10)), + hintText: "Enter Date", + ), + readOnly: + true, //set it true, so that user will not able to edit text + onTap: () async { + DateTime? pickedDate = + await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime( + 2000), //DateTime.now() - not to allow to choose before today. + lastDate: DateTime(2101)); + + if (pickedDate != null) { + print( + pickedDate); //pickedDate output format => 2021-03-10 00:00:00.000 + String formattedDate = + DateFormat('yyyy-MM-dd') + .format(pickedDate); + print( + formattedDate); //formatted date output using intl package => 2021-03-16 + //you can implement different kind of Date Format here according to your requirement + + setState(() { + _dateController.text = + formattedDate; //set output date to TextField value. + }); + } else { + print("Date is not selected"); + } + }, + ), ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10)), - hintText: "Enter Date", ), - readOnly: - true, //set it true, so that user will not able to edit text - onTap: () async { - DateTime? pickedDate = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime( - 2000), //DateTime.now() - not to allow to choose before today. - lastDate: DateTime(2101)); + const SizedBox( + width: 20, + ), + Flexible( + child: TextField( + controller: + _timeController, //editing controller of this TextField + decoration: InputDecoration( + prefixIcon: const Padding( + padding: EdgeInsets.all(0.0), + child: Icon( + Icons.timer, + ), // icon is 48px widget. + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12), + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10)), + hintText: "Time", + ), + readOnly: + true, //set it true, so that user will not able to edit text + onTap: () async { + TimeOfDay? pickedTime = + await showTimePicker( + initialTime: TimeOfDay.now(), + context: context, + ); - if (pickedDate != null) { - print( - pickedDate); //pickedDate output format => 2021-03-10 00:00:00.000 - String formattedDate = - DateFormat('yyyy-MM-dd') - .format(pickedDate); - print( - formattedDate); //formatted date output using intl package => 2021-03-16 - //you can implement different kind of Date Format here according to your requirement + if (pickedTime != null) { + print(pickedTime + .format(context)); //output 10:51 PM + DateTime parsedTime = DateFormat.jm() + .parse(pickedTime + .format(context) + .toString()); + //converting to DateTime so that we can further format on different pattern. + print( + parsedTime); //output 1970-01-01 22:53:00.000 + String formattedTime = DateFormat('HH:mm') + .format(parsedTime); + print(formattedTime); //output 14:59:00 + //DateFormat() is from intl package, you can format the time on any pattern you need. - setState(() { - _dateController.text = - formattedDate; //set output date to TextField value. - }); - } else { - print("Date is not selected"); - } - }, + setState(() { + _timeController.text = + formattedTime; //set the value of text field. + }); + } else { + print("Time is not selected"); + } + }, + ), + ), + ], + ), + const SizedBox( + height: 10, + ), + const Align( + alignment: Alignment.bottomLeft, + child: Text( + "Link Group", + textAlign: TextAlign.start, ), ), - ), - const SizedBox( - width: 20, - ), - Flexible( - child: TextField( - controller: - _timeController, //editing controller of this TextField + const SizedBox( + height: 8, + ), + TextFormField( + controller: _grupController, decoration: InputDecoration( - prefixIcon: const Padding( - padding: EdgeInsets.all(0.0), - child: Icon( - Icons.timer, - ), // icon is 48px widget. - ), contentPadding: const EdgeInsets.symmetric(horizontal: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10)), - hintText: "Time", + hintText: "Enter", ), - readOnly: - true, //set it true, so that user will not able to edit text - onTap: () async { - TimeOfDay? pickedTime = await showTimePicker( - initialTime: TimeOfDay.now(), - context: context, - ); - - if (pickedTime != null) { - print(pickedTime - .format(context)); //output 10:51 PM - DateTime parsedTime = DateFormat.jm().parse( - pickedTime.format(context).toString()); - //converting to DateTime so that we can further format on different pattern. - print( - parsedTime); //output 1970-01-01 22:53:00.000 - String formattedTime = - DateFormat('HH:mm').format(parsedTime); - print(formattedTime); //output 14:59:00 - //DateFormat() is from intl package, you can format the time on any pattern you need. - - setState(() { - _timeController.text = - formattedTime; //set the value of text field. - }); - } else { - print("Time is not selected"); - } - }, ), - ), - ], - ), - const SizedBox( - height: 10, - ), - const Align( - alignment: Alignment.bottomLeft, - child: Text( - "Link Group", - textAlign: TextAlign.start, - ), - ), - const SizedBox( - height: 8, - ), - TextFormField( - controller: _grupController, - decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(horizontal: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10)), - hintText: "Enter", - ), - ), - const SizedBox( - height: 10, - ), - const Align( - alignment: Alignment.bottomLeft, - child: Text( - "Deskripsi", - textAlign: TextAlign.start, - ), - ), - const SizedBox( - height: 8, - ), - TextFormField( - keyboardType: TextInputType.multiline, - maxLines: 4, - controller: _deskripsiController, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 10), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - ), - hintText: "Tulis deskripsi", + const SizedBox( + height: 10, + ), + const Align( + alignment: Alignment.bottomLeft, + child: Text( + "Deskripsi", + textAlign: TextAlign.start, + ), + ), + const SizedBox( + height: 8, + ), + TextFormField( + keyboardType: TextInputType.multiline, + maxLines: 4, + controller: _deskripsiController, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + hintText: "Tulis deskripsi", + ), + ), + const SizedBox( + height: 20, + ), + ElevatedButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.black)), + onPressed: submitTikum, + child: const Text('Submit'), + ), + ], ), ), - const SizedBox( - height: 20, - ), - ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.black)), - onPressed: submitTikum, - // { - // // Validate returns true if the form is valid, or false otherwise. - // if (_formKey.currentState!.validate()) { - // // If the form is valid, display a snackbar. In the real world, - // // you'd often call a server or save the information in a database. - // ScaffoldMessenger.of(context).showSnackBar( - // const SnackBar(content: Text('Processing Data')), - // ); - // } - // }, - child: const Text('Submit'), - ), - ], - ), - ), + ), + ) + ], ), ), ), diff --git a/mobile/lib/screen/home/components/profile/myprofile/card_profile.dart b/mobile/lib/screen/home/components/profile/myprofile/card_profile.dart index 1ae0e66..1f7a91b 100644 --- a/mobile/lib/screen/home/components/profile/myprofile/card_profile.dart +++ b/mobile/lib/screen/home/components/profile/myprofile/card_profile.dart @@ -1,38 +1,66 @@ import 'package:flutter/material.dart'; import 'package:mobile/api/user.dart'; import 'package:mobile/model/review.dart'; +import 'package:mobile/screen/_global/components/image_network.dart'; import "dart:math" as math; import 'package:mobile/screen/review_detail/review_detail_screen.dart'; import 'package:mobile/utils/show_snackbar.dart'; // final GlobalKey _navKey = GlobalKey(); -class TimelineCard extends StatefulWidget { - final ReviewProfile data; + +class MyReviewCard extends StatelessWidget { final int randomForProfile = math.Random().nextInt(1000); - TimelineCard({Key? key, required this.data, int? randomForProfile}) + final ReviewProfile data; + Function? refreshParent; + MyReviewCard({Key? key, required this.data, this.refreshParent}) : super(key: key); - @override - State createState() => _TimelineCardState(); -} - -class _TimelineCardState extends State { - late Future _futureStatus; - - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { + void _handleDelete() async { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Hapus Review'), + content: const Text( + 'Jika anda menghapus review ini akan hilang dari timeline umum dan profile anda!'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + try { + await deleteMyReview(data.id); + ShowSnackBar(context, "Berhasil menghapus post"); + if (refreshParent != null) { + refreshParent!(); + } + } catch (err) { + ShowSnackBar(context, err.toString()); + } + Navigator.pop(context, "OK"); + }, + child: const Text( + 'OK', + style: TextStyle( + color: Colors.red, + ), + ), + ), + ], + ), + ); + } + return Container( margin: const EdgeInsets.only(bottom: 5), child: InkWell( onTap: () { Navigator.pushNamed(context, ReviewDetailScreen.routeName, - arguments: ReviewDetailScreenArguments(id: widget.data.id)); + arguments: ReviewDetailScreenArguments(id: data.id)); }, child: Column( children: [ @@ -44,70 +72,29 @@ class _TimelineCardState extends State { Row(children: [ ClipRRect( borderRadius: BorderRadius.circular(50), - child: Image.network( - "https://www.thiswaifudoesnotexist.net/example-${widget.randomForProfile}.jpg", + child: ImageNetworkWShimmer( + link: + "https://www.thiswaifudoesnotexist.net/example-$randomForProfile.jpg", width: 40, height: 40, ), ), const SizedBox(width: 10), - InkWell( - child: Text(widget.data.nameUser), - splashColor: Colors.transparent, - onTap: () { - print("Open profile"); - }, - ), + Text(data.nameUser) ]), TextButton( - child: const Icon( - Icons.delete, - color: Colors.red, - ), - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: const Text('Hapus Review'), - content: const Text( - 'Jika anda menghapus review ini akan hilang dari timeline umum dan profile anda!'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, 'Cancel'), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - try { - await deleteMyReview(widget.data.id); - ShowSnackBar(context, - "Berhasil menghapus post, harap pindah halaman untuk melihat perubahan"); - } catch (err) { - ShowSnackBar(context, err.toString()); - } - Navigator.pop(context, "OK"); - }, - child: const Text( - 'OK', - style: TextStyle( - color: Colors.red, - ), - ), - ), - ], + child: const Icon( + Icons.delete, + color: Colors.red, ), - ), - // onPressed: () async { - // setState(() { - // _futureStatus = deleteMyReview(widget.data.id); - // }); - // }, - ), + onPressed: _handleDelete), ], )), const SizedBox(height: 10), - Image.network(widget.data.photo[0] == "/" - ? "https://travelliu.yaudahlah.my.id${widget.data.photo}" - : widget.data.photo), + ImageNetworkWShimmer( + link: data.photo[0] == "/" + ? "https://travelliu.yaudahlah.my.id${data.photo}" + : data.photo), Container( margin: const EdgeInsets.only(left: 10, right: 10, bottom: 10), child: Column( @@ -117,12 +104,12 @@ class _TimelineCardState extends State { children: [ const Icon(Icons.star_border_outlined, color: Colors.yellow), - Text(widget.data.rating.toString()), + Text(data.rating.toString()), const SizedBox( width: 10, ), Text( - widget.data.namaTempat, + data.namaTempat, style: const TextStyle(fontWeight: FontWeight.bold), ), ], @@ -132,7 +119,7 @@ class _TimelineCardState extends State { children: [ Expanded( child: Text( - widget.data.review, + data.review, textAlign: TextAlign.left, )) ], diff --git a/mobile/lib/screen/home/components/profile/myprofile/my_profile.dart b/mobile/lib/screen/home/components/profile/myprofile/my_profile.dart index fa085d7..9ce78ec 100644 --- a/mobile/lib/screen/home/components/profile/myprofile/my_profile.dart +++ b/mobile/lib/screen/home/components/profile/myprofile/my_profile.dart @@ -2,30 +2,36 @@ import 'package:flutter/material.dart'; import 'package:mobile/api/user.dart'; import 'package:mobile/model/profile.dart'; import 'package:mobile/model/review.dart'; +import 'package:mobile/screen/_global/components/profile_card.dart'; +import 'package:mobile/screen/_global/components/shimmer/profile_shimmer.dart'; +import 'package:mobile/screen/_global/components/shimmer/review_shimmer.dart'; import 'package:mobile/screen/home/components/profile/myprofile/card_profile.dart'; import 'package:mobile/screen/home/home_screen.dart'; -import "dart:math" as math; import 'package:mobile/utils/show_snackbar.dart'; -class MyProfile extends StatefulWidget { - const MyProfile({ - Key? key, - }) : super(key: key); - @override - State createState() => _MyProfile(); -} +class MyProfile extends StatelessWidget { + const MyProfile({Key? key}) : super(key: key); -class _MyProfile extends State { - late Future> futureReview; - late Future futureProfile; - final int randomForProfile = math.Random().nextInt(1000); @override - void initState() { - futureReview = getMyReviewById(); - futureProfile = getMyProfileById(); - super.initState(); + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + const SizedBox( + height: 10, + ), + _ProfileSection(), + const _ReviewSection(), + ], + ), + ); } +} + +class _ProfileSection extends StatelessWidget { + final Future futureProfile = getMyProfileById(); + _ProfileSection({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -51,151 +57,110 @@ class _MyProfile extends State { } } - ; - - return FutureBuilder>( - future: futureReview, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Center(child: Text(snapshot.error.toString())); - } - if (snapshot.hasData) { - return ListView( - children: [ - const SizedBox( - height: 10, + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + style: TextButton.styleFrom( + primary: Colors.white, + backgroundColor: Colors.red, ), - Row( + onPressed: _handleLogout, + child: Row( mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - style: TextButton.styleFrom( - primary: Colors.white, - backgroundColor: Colors.red, - ), - onPressed: _handleLogout, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - Text( - "logout ", - ), - Icon( - Icons.logout_outlined, - size: 15.0, - ), - ], - ), + children: const [ + Text( + "logout ", ), - const SizedBox( - width: 10, - ) - ], - ), - Column( - children: [ - Container( - margin: const EdgeInsets.symmetric(vertical: 30), - height: 100, - width: 100, - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: Image.network( - "https://www.thiswaifudoesnotexist.net/example-$randomForProfile.jpg", - fit: BoxFit.cover, - ), - ), - ), - FutureBuilder( - future: futureProfile, - builder: (context, snapshot) { - if (snapshot.data == null) { - return const Center( - child: Text("Loading ..."), - ); - } else { - return ProfileCard(data: snapshot.data); - } - }, + Icon( + Icons.logout_outlined, + size: 15.0, ), ], ), - for (var data in snapshot.data!) - TimelineCard( - data: data, - ) - ], - ); - } - return const Center( - child: CircularProgressIndicator(), - ); - }, + ), + const SizedBox( + width: 10, + ) + ], + ), + FutureBuilder( + future: futureProfile, + builder: (context, snapshot) { + if (snapshot.hasData) { + return ProfileCard(data: snapshot.data!); + } + + return SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + child: const Center(child: ShimmerProfile()), + ); + }, + ), + ], ); } } -class ProfileCard extends StatelessWidget { - final data; - // final GlobalKey navKey; - final int randomForProfile = math.Random().nextInt(1000); - ProfileCard({ - Key? key, - required this.data, - // required this.navKey - }); +class _ReviewSection extends StatefulWidget { + const _ReviewSection({Key? key}) : super(key: key); + + @override + State<_ReviewSection> createState() => __ReviewSectionState(); +} + +class __ReviewSectionState extends State<_ReviewSection> { + Future> futureReview = getMyReviewById(); + + void refreshList() { + setState(() { + futureReview = getMyReviewById(); + }); + } @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(bottom: 5), - child: Column(children: [ - Text( - data.user.name.toString(), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 25, - fontWeight: FontWeight.bold, - ), - ), - Container( - margin: const EdgeInsets.symmetric( - vertical: 20, - horizontal: 15, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 20, - ), - child: Column( - // ignore: prefer_const_literals_to_create_immutables - children: [ - const Text("Total Review"), - Text(data.totalReview.toString()), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 20, - ), + return FutureBuilder>( + future: futureReview, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text(snapshot.error.toString())); + } + if (snapshot.hasData) { + if (snapshot.data!.isEmpty) { + return Center( + child: SizedBox( + width: 350, child: Column( - // ignore: prefer_const_literals_to_create_immutables - children: [ - const Text("Rataan Rating"), - Text(data.avgRating.toString()), + children: const [ + Text( + "Oops.. kamu masih belum membagikan apapun", + textAlign: TextAlign.center, + ), + Text("Bagikan cerita perjalananmu sekarang!") ], ), ), + ); + } + + return Column( + children: [ + for (var data in snapshot.data!) + MyReviewCard( + data: data, + refreshParent: refreshList, + ) ], - ), - ), - ]), + ); + } + return Center( + child: Column( + children: [for (int i = 0; i < 5; i++) const ShimmerReview()], + )); + }, ); } } diff --git a/mobile/lib/screen/home/components/tikum/global_tikum.dart b/mobile/lib/screen/home/components/tikum/global_tikum.dart index b5f7a71..d097174 100644 --- a/mobile/lib/screen/home/components/tikum/global_tikum.dart +++ b/mobile/lib/screen/home/components/tikum/global_tikum.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile/api/tikum.dart'; import 'package:mobile/model/tikum.dart'; +import 'package:mobile/screen/_global/components/shimmer/tikum_shimmer.dart'; import 'package:mobile/screen/home/components/tikum/tikum_card.dart'; class GlobalTikum extends StatefulWidget { @@ -36,8 +37,9 @@ class _GlobalTikumState extends State { } else if (snapshot.hasError) { return const Center(child: Text("Error when fetching all reviews")); } - return const Center( - child: CircularProgressIndicator(), + return ListView.builder( + itemCount: 4, + itemBuilder: (context, _) => const ShimmerTikum(), ); }); } diff --git a/mobile/lib/screen/home/components/tikum/my_tikum.dart b/mobile/lib/screen/home/components/tikum/my_tikum.dart index 92d184f..cdea926 100644 --- a/mobile/lib/screen/home/components/tikum/my_tikum.dart +++ b/mobile/lib/screen/home/components/tikum/my_tikum.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile/model/profile_secure.dart'; import 'package:mobile/model/tikum.dart'; +import 'package:mobile/screen/_global/components/shimmer/tikum_shimmer.dart'; import 'package:mobile/screen/home/components/profile/not_loggedin.dart'; import 'package:mobile/api/tikum.dart'; import 'package:mobile/screen/home/components/tikum/mytikum_card.dart'; @@ -30,19 +31,18 @@ class _MyTikumState extends State { builder: (context, snapshot) { if (snapshot.hasData) { if (snapshot.data!.getLoggedInStatus()) { - return MyTikumList(); + return const MyTikumList(); } } - return NotLoggedIn(); + return const NotLoggedIn(); }, ), ); } } -// TODO: Affan ngerjain ini ya untuk ngerender listnya class MyTikumList extends StatefulWidget { - MyTikumList({Key? key}) : super(key: key); + const MyTikumList({Key? key}) : super(key: key); @override State createState() => _MyTikumListState(); @@ -50,30 +50,58 @@ class MyTikumList extends StatefulWidget { class _MyTikumListState extends State { late Future> futureTikumProfile; + + @override void initState() { futureTikumProfile = getMyTikum(); super.initState(); } + void refreshList() { + setState(() { + futureTikumProfile = getMyTikum(); + }); + } + @override Widget build(BuildContext context) { return FutureBuilder>( future: futureTikumProfile, builder: (context, snapshot) { if (snapshot.hasData) { + if (snapshot.data!.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text( + "Wah sepertinya kamu belum membuat \nTitik Kumpul", + textAlign: TextAlign.center, + ), + Text( + "Tikum", + style: TextStyle(fontWeight: FontWeight.bold), + ) + ], + ), + ); + } + return ListView( children: [ for (var data in snapshot.data!) MyTikumCard( tikum: data, + refreshParent: refreshList, ) ], ); } else if (snapshot.hasError) { return const Center(child: Text("Error when fetching all reviews")); } - return const Center( - child: CircularProgressIndicator(), + return ListView.builder( + itemCount: 4, + itemBuilder: (context, _) => const ShimmerTikum(), ); }, ); diff --git a/mobile/lib/screen/home/components/tikum/mytikum_card.dart b/mobile/lib/screen/home/components/tikum/mytikum_card.dart index 9aef774..1bf1456 100644 --- a/mobile/lib/screen/home/components/tikum/mytikum_card.dart +++ b/mobile/lib/screen/home/components/tikum/mytikum_card.dart @@ -1,16 +1,56 @@ import 'package:flutter/material.dart'; import 'package:mobile/model/tikum.dart'; import 'package:intl/intl.dart'; +import 'package:mobile/screen/_global/components/image_network.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:mobile/utils/show_snackbar.dart'; import 'package:mobile/api/tikum.dart'; class MyTikumCard extends StatelessWidget { final TikumProfile tikum; - const MyTikumCard({Key? key, required this.tikum}) : super(key: key); + Function? refreshParent; + MyTikumCard({Key? key, required this.tikum, this.refreshParent}) + : super(key: key); @override Widget build(BuildContext context) { + void _handleDelete() async { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Hapus Titik Kumpul'), + content: const Text( + 'Jika anda menghapus titik kumpul ini akan hilang dari my tikum anda!'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + try { + await deleteMyTikum(tikum.id); + ShowSnackBar(context, "Berhasil menghapus titik kumpul"); + if (refreshParent != null) { + refreshParent!(); + } + } catch (err) { + ShowSnackBar(context, err.toString()); + } + Navigator.pop(context, "OK"); + }, + child: const Text( + 'OK', + style: TextStyle( + color: Colors.red, + ), + ), + ), + ], + ), + ); + } + return Column(children: [ Container( padding: @@ -25,8 +65,9 @@ class MyTikumCard extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(50), - child: Image.network( - "https://travelliu.yaudahlah.my.id/affan-imut.jpeg", + child: ImageNetworkWShimmer( + link: + "https://travelliu.yaudahlah.my.id/affan-imut.jpeg", width: 40, height: 40, ), @@ -38,48 +79,11 @@ class MyTikumCard extends StatelessWidget { ], ), TextButton( - child: const Icon( - Icons.delete, - color: Colors.red, - ), - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - title: const Text('Hapus Titik Kumpul'), - content: const Text( - 'Jika anda menghapus titik kumpul ini akan hilang dari my tikum anda!'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, 'Cancel'), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - try { - await deleteMyTikum(tikum.id); - ShowSnackBar(context, - "Berhasil menghapus titik kumpul, harap pindah halaman untuk melihat perubahan"); - } catch (err) { - ShowSnackBar(context, err.toString()); - } - Navigator.pop(context, "OK"); - }, - child: const Text( - 'OK', - style: TextStyle( - color: Colors.red, - ), - ), - ), - ], + child: const Icon( + Icons.delete, + color: Colors.red, ), - ), - // onPressed: () async { - // setState(() { - // _futureStatus = deleteMyReview(widget.data.id); - // }); - // }, - ), + onPressed: _handleDelete), ], ), const SizedBox( diff --git a/mobile/lib/screen/home/components/tikum/tikum.dart b/mobile/lib/screen/home/components/tikum/tikum.dart index ed35512..9df1d4f 100644 --- a/mobile/lib/screen/home/components/tikum/tikum.dart +++ b/mobile/lib/screen/home/components/tikum/tikum.dart @@ -1,74 +1,80 @@ import 'package:flutter/material.dart'; import 'package:mobile/model/profile_secure.dart'; import 'package:mobile/screen/form_tikum/form_tikum_screen.dart'; -import 'package:mobile/screen/test_screen/test_screen.dart'; import 'package:mobile/screen/home/components/tikum/global_tikum.dart'; import 'package:mobile/screen/home/components/tikum/my_tikum.dart'; -class TikumLayout extends StatefulWidget { - TikumLayout({Key? key}) : super(key: key); +class TikumLayout extends StatelessWidget { + const TikumLayout({Key? key}) : super(key: key); @override - State createState() => _TikumLayoutState(); + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + backgroundColor: Colors.white, + floatingActionButton: const _FloatingActionButtonTikum(), + appBar: AppBar( + backgroundColor: Colors.white, + flexibleSpace: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + TabBar(indicatorColor: Colors.black26, tabs: [ + Tab( + text: "Global Tikum", + ), + Tab( + text: "My Tikum", + ) + ]) + ], + ), + ), + body: TabBarView(children: [GlobalTikum(), MyTikum()]), + ), + ); + } +} + +class _FloatingActionButtonTikum extends StatefulWidget { + const _FloatingActionButtonTikum({Key? key}) : super(key: key); + + @override + State<_FloatingActionButtonTikum> createState() => + __FloatingActionButtonTikumState(); } -class _TikumLayoutState extends State { - late Future futureProfile; +class __FloatingActionButtonTikumState + extends State<_FloatingActionButtonTikum> { + bool isLoggedIn = false; @override void initState() { - futureProfile = SecureProfile.getStorage(); super.initState(); + _checkLoggedIn(); + } + + void _checkLoggedIn() async { + var profile = await SecureProfile.getStorage(); + setState(() { + isLoggedIn = profile.getLoggedInStatus(); + }); } @override Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Scaffold( - backgroundColor: Colors.white, - floatingActionButton: plusFloatingBuilder(futureProfile), - appBar: AppBar( - backgroundColor: Colors.white, - flexibleSpace: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - TabBar(indicatorColor: Colors.black26, tabs: [ - Tab( - text: "Global Tikum", - ), - Tab( - text: "My Tikum", - ) - ]) - ], - ), - ), - body: TabBarView(children: [GlobalTikum(), MyTikum()]), - )); + return !isLoggedIn + ? const SizedBox.shrink() + : FloatingActionButton( + child: const Icon(Icons.add), + heroTag: "Buat Review", + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return const FormTikumScreen(); + })); + }, + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ); } } - -FutureBuilder plusFloatingBuilder(Future future) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - if (snapshot.hasData) { - // Kalau misalkan logged in - if (snapshot.data!.getLoggedInStatus()) { - return FloatingActionButton( - child: const Icon(Icons.add), - heroTag: "Buat Review", - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context) { - return const FormTikumScreen(); - })); - }, - backgroundColor: Colors.black, - foregroundColor: Colors.white, - ); - } - } - return const SizedBox.shrink(); - }); -} diff --git a/mobile/lib/screen/home/components/tikum/tikum_card.dart b/mobile/lib/screen/home/components/tikum/tikum_card.dart index 7ed728c..eb367de 100644 --- a/mobile/lib/screen/home/components/tikum/tikum_card.dart +++ b/mobile/lib/screen/home/components/tikum/tikum_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile/model/tikum.dart'; import 'package:intl/intl.dart'; +import 'package:mobile/screen/_global/components/image_network.dart'; import 'package:url_launcher/url_launcher.dart'; class TikumCard extends StatelessWidget { @@ -20,8 +21,8 @@ class TikumCard extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(50), - child: Image.network( - "https://travelliu.yaudahlah.my.id/affan-imut.jpeg", + child: ImageNetworkWShimmer( + link: "https://travelliu.yaudahlah.my.id/affan-imut.jpeg", width: 40, height: 40, ), diff --git a/mobile/lib/screen/home/components/timeline/timeline.dart b/mobile/lib/screen/home/components/timeline/timeline.dart index 1c223ce..1fa851c 100644 --- a/mobile/lib/screen/home/components/timeline/timeline.dart +++ b/mobile/lib/screen/home/components/timeline/timeline.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:mobile/api/review.dart'; import 'package:mobile/model/profile_secure.dart'; import 'package:mobile/model/review.dart'; +import 'package:mobile/screen/_global/components/shimmer/review_shimmer.dart'; import 'package:mobile/screen/form_review/form_review_screen.dart'; import 'package:mobile/screen/home/components/timeline/timeline_card.dart'; @@ -70,8 +71,9 @@ FutureBuilder> reviewBuilder(Future> future) { } else if (snapshot.hasError) { return const Center(child: Text("Error when fetching all reviews")); } - return const Center( - child: CircularProgressIndicator(), + return ListView.builder( + itemCount: 4, + itemBuilder: (context, _) => const ShimmerReview(), ); }, ); diff --git a/mobile/lib/screen/home/components/timeline/timeline_card.dart b/mobile/lib/screen/home/components/timeline/timeline_card.dart index c6ab855..9222300 100644 --- a/mobile/lib/screen/home/components/timeline/timeline_card.dart +++ b/mobile/lib/screen/home/components/timeline/timeline_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mobile/model/review.dart'; +import 'package:mobile/screen/_global/components/image_network.dart'; import 'package:mobile/screen/form_review/form_review_screen.dart'; import 'package:mobile/screen/profile_detail/profile_detail_screen.dart'; import "dart:math" as math; @@ -15,94 +16,95 @@ class TimelineCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - margin: const EdgeInsets.only(bottom: 5), - child: Container( - child: InkWell( - onTap: () { - Navigator.pushNamed(context, ReviewDetailScreen.routeName, - arguments: ReviewDetailScreenArguments(id: data.id)); - }, - child: Column(children: [ - Container( - margin: - const EdgeInsets.only(left: 10, right: 10, top: 10), - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(50), - child: Image.network( + margin: const EdgeInsets.only(bottom: 5), + child: InkWell( + onTap: () { + Navigator.pushNamed(context, ReviewDetailScreen.routeName, + arguments: ReviewDetailScreenArguments(id: data.id)); + }, + child: Column( + children: [ + Container( + margin: const EdgeInsets.only(left: 10, right: 10, top: 10), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(50), + child: ImageNetworkWShimmer( + link: "https://www.thiswaifudoesnotexist.net/example-$randomForProfile.jpg", - width: 40, - height: 40, - ), - ), - const SizedBox(width: 10), - InkWell( - child: Text(data.user.name), - splashColor: Colors.transparent, - onTap: () { - Navigator.pushNamed( - context, - ProfilePeopleScreen.routeName, - arguments: ProfilePeopleScreenArguments( - id: data.userId), - ); - }, - ) - ], - )), - const SizedBox(height: 10), - Image.network(data.photo[0] == "/" - ? "https://travelliu.yaudahlah.my.id${data.photo}" - : data.photo), - Container( - margin: const EdgeInsets.only( - left: 10, right: 10, bottom: 10), - child: Column( - children: [ - const SizedBox(height: 5), - Row( - children: [ - const Icon(Icons.star_border_outlined, - color: Colors.yellow), - Text(data.rating.toString()), - const SizedBox( - width: 10, - ), - Text( - data.namaTempat, - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - ], - ), - const SizedBox(height: 5), - Row( - children: [ - Expanded( - child: Text( - data.review, - textAlign: TextAlign.left, - )) - ], - ), - const SizedBox( - height: 10, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Icon(Icons.comment), - Text(data.numKomentar.toString()) - ], - ) - ], - )), - const Divider( - color: Colors.black, - height: 2.0, - thickness: 1.0, - ) - ])))); + width: 40, + height: 40, + )), + const SizedBox(width: 10), + InkWell( + child: Text(data.user.name), + splashColor: Colors.transparent, + onTap: () { + Navigator.pushNamed( + context, + ProfilePeopleScreen.routeName, + arguments: + ProfilePeopleScreenArguments(id: data.userId), + ); + }, + ) + ], + )), + const SizedBox(height: 10), + ImageNetworkWShimmer( + link: data.photo[0] == "/" + ? "https://travelliu.yaudahlah.my.id${data.photo}" + : data.photo), + Container( + margin: const EdgeInsets.only(left: 10, right: 10, bottom: 10), + child: Column( + children: [ + const SizedBox(height: 5), + Row( + children: [ + const Icon(Icons.star_border_outlined, + color: Colors.yellow), + Text(data.rating.toString()), + const SizedBox( + width: 10, + ), + Text( + data.namaTempat, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 5), + Row( + children: [ + Expanded( + child: Text( + data.review, + textAlign: TextAlign.left, + )) + ], + ), + const SizedBox( + height: 10, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Icon(Icons.comment), + Text(data.numKomentar.toString()) + ], + ) + ], + )), + const Divider( + color: Colors.black, + height: 2.0, + thickness: 1.0, + ) + ], + ), + ), + ); } } diff --git a/mobile/lib/screen/home/home_screen.dart b/mobile/lib/screen/home/home_screen.dart index 696e070..63a4074 100644 --- a/mobile/lib/screen/home/home_screen.dart +++ b/mobile/lib/screen/home/home_screen.dart @@ -19,7 +19,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { final List section = [ const Timeline(), - TikumLayout(), + const TikumLayout(), ProfileSection() ]; var sectionidx = 0; @@ -34,7 +34,7 @@ class _HomeScreenState extends State { return Scaffold( appBar: AppBar( - actions: [Image(image: AssetImage("assets/Logos.png"))], + actions: const [Image(image: AssetImage("assets/Logos.png"))], title: const Text("Travelliu", style: TextStyle(fontWeight: FontWeight.bold)), backgroundColor: Colors.white, diff --git a/mobile/lib/screen/permission/permission_screen.dart b/mobile/lib/screen/permission/permission_screen.dart index 3a56f74..abf0857 100644 --- a/mobile/lib/screen/permission/permission_screen.dart +++ b/mobile/lib/screen/permission/permission_screen.dart @@ -106,7 +106,7 @@ class _PermissionScreenState extends State { return Scaffold( backgroundColor: Colors.white, body: Padding( - padding: EdgeInsets.all(20), + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/mobile/lib/screen/profile_detail/profile_detail_screen.dart b/mobile/lib/screen/profile_detail/profile_detail_screen.dart index aadaabe..c675aa4 100644 --- a/mobile/lib/screen/profile_detail/profile_detail_screen.dart +++ b/mobile/lib/screen/profile_detail/profile_detail_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import "dart:math" as math; import 'package:mobile/model/profile.dart'; import 'package:mobile/api/user.dart'; +import 'package:mobile/screen/_global/components/profile_card.dart'; +import 'package:mobile/screen/_global/components/shimmer/profile_shimmer.dart'; import 'package:mobile/screen/home/components/profile/myprofile/my_profile.dart'; class ProfilePeopleScreenArguments { @@ -10,8 +12,8 @@ class ProfilePeopleScreenArguments { } class ProfilePeopleScreen extends StatefulWidget { - ProfilePeopleScreen({Key? key}) : super(key: key); - static String routeName = "/profile-detail"; + const ProfilePeopleScreen({Key? key}) : super(key: key); + static const String routeName = "/profile-detail"; @override State createState() => _ProfilePeopleScreenState(); } @@ -30,47 +32,24 @@ class _ProfilePeopleScreenState extends State { var id = arg.id; futureProfile = getUserProfileById(id); - final int randomForProfile = math.Random().nextInt(1000); return Scaffold( appBar: AppBar( title: const Text("Profile Reviewer"), backgroundColor: Colors.white, ), - body: ListView( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 10), - child: Column( - children: [ - Container( - margin: const EdgeInsets.only(top: 200, bottom: 30), - height: 100, - width: 100, - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: Image.network( - "https://www.thiswaifudoesnotexist.net/example-$randomForProfile.jpg", - fit: BoxFit.cover, - ), - ), - ), - FutureBuilder( - future: futureProfile, - builder: (context, snapshot) { - if (snapshot.data == null) { - return const Center( - child: Text("Loading ..."), - ); - } else { - return ProfileCard(data: snapshot.data); - } - }, - ), - ], - ), - ), - ], + body: Center( + child: FutureBuilder( + future: futureProfile, + builder: (context, snapshot) { + if (snapshot.data == null) { + return const ShimmerProfile(); + } else { + return Center( + child: ProfileCard(data: snapshot.data! as Profile)); + } + }, + ), ), ); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 47f6d11..546399f 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -422,6 +422,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index de1dfc2..d3cb542 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: provider: ^6.0.3 permission_handler: ^9.2.0 permission: ^0.1.7 + shimmer: ^2.0.0 dev_dependencies: flutter_test: