diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 06591f6..c41fea5 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -87,5 +87,23 @@ "auto_generate_quiz": "Auto Generate Quiz", "ready_to_compete": "Ready to Compete?", "enter_code_to_join": "Enter the quiz code and show your skills!", - "join_quiz_now": "Join Quiz Now" + "join_quiz_now": "Join Quiz Now", + + "total_solve": "Total Solved", + "personal_info": "Personal Information", + "phone": "Phone Number", + "location": "Location", + "joined": "Joined", + "education": "Education", + "not_set": "Not Set", + "not_available": "Not Available", + "settings": "Settings", + "legal_and_support": "Legal & Support", + "privacy_policy": "Privacy Policy", + "terms_of_service": "Terms of Service", + "help_center": "Help Center", + "contact_us": "Contact Us", + "about_app": "About App", + "version": "Version", + "close": "Close" } diff --git a/assets/translations/id-ID.json b/assets/translations/id-ID.json index 8e02cf2..75b6794 100644 --- a/assets/translations/id-ID.json +++ b/assets/translations/id-ID.json @@ -87,5 +87,23 @@ "ready_to_compete": "Siap untuk Bertanding?", "enter_code_to_join": "Masukkan kode kuis dan tunjukkan kemampuanmu!", - "join_quiz_now": "Gabung Kuis Sekarang" + "join_quiz_now": "Gabung Kuis Sekarang", + + "total_solve": "Total Diselesaikan", + "personal_info": "Informasi Pribadi", + "phone": "Nomor Telepon", + "location": "Lokasi", + "joined": "Bergabung", + "education": "Pendidikan", + "not_set": "Belum Diatur", + "not_available": "Tidak Tersedia", + "settings": "Pengaturan", + "legal_and_support": "Legal & Bantuan", + "privacy_policy": "Kebijakan Privasi", + "terms_of_service": "Syarat dan Ketentuan", + "help_center": "Pusat Bantuan", + "contact_us": "Hubungi Kami", + "about_app": "Tentang Aplikasi", + "version": "Versi", + "close": "Tutup" } diff --git a/assets/translations/ms-MY.json b/assets/translations/ms-MY.json index 32eb62f..4f2558f 100644 --- a/assets/translations/ms-MY.json +++ b/assets/translations/ms-MY.json @@ -84,5 +84,23 @@ "auto_generate_quiz": "Jana Kuiz Automatik", "ready_to_compete": "Bersedia untuk Bertanding?", "enter_code_to_join": "Masukkan kod kuiz dan tunjukkan kemahiran anda!", - "join_quiz_now": "Sertai Kuiz Sekarang" + "join_quiz_now": "Sertai Kuiz Sekarang", + + "total_solve": "Jumlah Diselesaikan", + "personal_info": "Maklumat Peribadi", + "phone": "Nombor Telefon", + "location": "Lokasi", + "joined": "Tarikh Sertai", + "education": "Pendidikan", + "not_set": "Belum Ditentukan", + "not_available": "Tidak Tersedia", + "settings": "Tetapan", + "legal_and_support": "Perundangan & Sokongan", + "privacy_policy": "Dasar Privasi", + "terms_of_service": "Terma Perkhidmatan", + "help_center": "Pusat Bantuan", + "contact_us": "Hubungi Kami", + "about_app": "Mengenai Aplikasi", + "version": "Versi", + "close": "Tutup" } diff --git a/lib/core/endpoint/api_endpoint.dart b/lib/core/endpoint/api_endpoint.dart index 0b2528c..2b38099 100644 --- a/lib/core/endpoint/api_endpoint.dart +++ b/lib/core/endpoint/api_endpoint.dart @@ -1,5 +1,5 @@ class APIEndpoint { - static const String baseUrl = "http://192.168.1.9:5000"; + static const String baseUrl = "http://192.168.110.43:5000"; // static const String baseUrl = "http://103.193.178.121:5000"; static const String api = "$baseUrl/api"; @@ -28,4 +28,5 @@ class APIEndpoint { static const String userData = "/user"; static const String userUpdate = "/user/update"; + static const String userStat = "/user/status"; } diff --git a/lib/data/models/user/user_stat_model.dart b/lib/data/models/user/user_stat_model.dart new file mode 100644 index 0000000..769a73f --- /dev/null +++ b/lib/data/models/user/user_stat_model.dart @@ -0,0 +1,29 @@ +class UserStatModel { + final double avgScore; + final int totalSolve; + final int totalQuiz; + + UserStatModel({ + required this.avgScore, + required this.totalSolve, + required this.totalQuiz, + }); + + // Factory constructor to create an instance from JSON + factory UserStatModel.fromJson(Map json) { + return UserStatModel( + avgScore: (json['avg_score'] as num).toDouble(), + totalSolve: json['total_solve'] as int, + totalQuiz: json['total_quiz'] as int, + ); + } + + // Convert instance to JSON + Map toJson() { + return { + 'avg_score': avgScore, + 'total_solve': totalSolve, + 'total_quiz': totalQuiz, + }; + } +} diff --git a/lib/data/services/user_service.dart b/lib/data/services/user_service.dart index 6c5e9e3..bd90103 100644 --- a/lib/data/services/user_service.dart +++ b/lib/data/services/user_service.dart @@ -4,6 +4,7 @@ import 'package:quiz_app/core/endpoint/api_endpoint.dart'; import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/data/models/base/base_model.dart'; import 'package:quiz_app/data/models/user/user_full_model.dart'; +import 'package:quiz_app/data/models/user/user_stat_model.dart'; import 'package:quiz_app/data/providers/dio_client.dart'; class UserService extends GetxService { @@ -54,4 +55,23 @@ class UserService extends GetxService { return null; } } + + Future?> getUserStat(String id) async { + try { + final response = await _dio.get("${APIEndpoint.userStat}/$id"); + + if (response.statusCode == 200) { + final parsedResponse = BaseResponseModel.fromJson( + response.data, + (data) => UserStatModel.fromJson(data), + ); + return parsedResponse; + } else { + return null; + } + } catch (e) { + logC.e("get user data error: $e"); + return null; + } + } } diff --git a/lib/data/services/user_storage_service.dart b/lib/data/services/user_storage_service.dart index a806829..0ce0641 100644 --- a/lib/data/services/user_storage_service.dart +++ b/lib/data/services/user_storage_service.dart @@ -2,10 +2,19 @@ import 'dart:convert'; import 'package:quiz_app/data/entity/user/user_entity.dart'; import 'package:shared_preferences/shared_preferences.dart'; +/// A lightweight wrapper around [SharedPreferences] that persists +/// the logged‑in user plus UI/feature preferences such as theme and +/// push‑notification opt‑in. class UserStorageService { + // ───────────────────── Keys ───────────────────── static const _userKey = 'user_data'; + static const _darkModeKey = 'pref_dark_mode'; + static const _pushNotifKey = 'pref_push_notification'; + + /// Cached flag used by splash / root to decide initial route. bool isLogged = false; + // ───────────────────── User CRUD ───────────────────── Future saveUser(UserEntity user) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_userKey, jsonEncode(user.toJson())); @@ -14,7 +23,6 @@ class UserStorageService { Future loadUser() async { final prefs = await SharedPreferences.getInstance(); final jsonString = prefs.getString(_userKey); - if (jsonString == null) return null; return UserEntity.fromJson(jsonDecode(jsonString)); } @@ -28,4 +36,29 @@ class UserStorageService { final prefs = await SharedPreferences.getInstance(); return prefs.containsKey(_userKey); } + + // ───────────────────── UI Preferences ───────────────────── + /// Persist the user’s theme choice. + Future setDarkMode(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_darkModeKey, value); + } + + /// Retrieve the stored theme choice. Defaults to *false* (light mode). + Future getDarkMode() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_darkModeKey) ?? false; + } + + /// Persist the user’s push‑notification preference. + Future setPushNotification(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_pushNotifKey, value); + } + + /// Retrieve the stored push‑notification preference. Defaults to *true*. + Future getPushNotification() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_pushNotifKey) ?? true; + } } diff --git a/lib/feature/profile/binding/profile_binding.dart b/lib/feature/profile/binding/profile_binding.dart index 89e85fd..086bfdd 100644 --- a/lib/feature/profile/binding/profile_binding.dart +++ b/lib/feature/profile/binding/profile_binding.dart @@ -1,5 +1,7 @@ import 'package:get/get.dart'; +import 'package:quiz_app/data/controllers/user_controller.dart'; import 'package:quiz_app/data/services/google_auth_service.dart'; +import 'package:quiz_app/data/services/user_service.dart'; import 'package:quiz_app/data/services/user_storage_service.dart'; import 'package:quiz_app/feature/profile/controller/profile_controller.dart'; @@ -7,6 +9,12 @@ class ProfileBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => GoogleAuthService()); - Get.lazyPut(() => ProfileController(Get.find(), Get.find())); + if (!Get.isRegistered()) Get.lazyPut(() => UserService()); + Get.lazyPut(() => ProfileController( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); } } diff --git a/lib/feature/profile/controller/profile_controller.dart b/lib/feature/profile/controller/profile_controller.dart index 7e7d088..042ea38 100644 --- a/lib/feature/profile/controller/profile_controller.dart +++ b/lib/feature/profile/controller/profile_controller.dart @@ -3,34 +3,93 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; +import 'package:quiz_app/core/endpoint/api_endpoint.dart'; import 'package:quiz_app/core/utils/logger.dart'; import 'package:quiz_app/data/controllers/user_controller.dart'; +import 'package:quiz_app/data/models/user/user_stat_model.dart'; import 'package:quiz_app/data/services/google_auth_service.dart'; +import 'package:quiz_app/data/services/user_service.dart'; import 'package:quiz_app/data/services/user_storage_service.dart'; +import 'package:url_launcher/url_launcher.dart'; class ProfileController extends GetxController { - final UserController _userController = Get.find(); - + final UserController _userController; final UserStorageService _userStorageService; final GoogleAuthService _googleAuthService; + final UserService _userService; - ProfileController(this._userStorageService, this._googleAuthService); + ProfileController( + this._userController, + this._userStorageService, + this._googleAuthService, + this._userService, + ); + // User basic info Rx get userName => _userController.userName; Rx get email => _userController.email; Rx get userImage => _userController.userImage; + Rx data = Rx(null); - final totalQuizzes = 12.obs; - final avgScore = 85.obs; + Rx birthDate = "".obs; + Rx phoneNumber = "".obs; + Rx location = "".obs; + Rx joinDate = "".obs; + Rx education = "".obs; + Rx profileImage = null.obs; + + // App settings + Rx notificationsEnabled = true.obs; + Rx darkModeEnabled = false.obs; + Rx soundEffectsEnabled = true.obs; + + // App info + Rx appName = "Quiz App".obs; + Rx appVersion = "1.0.2".obs; + Rx appDescription = "An educational quiz app to test and improve your knowledge across various subjects.".obs; + Rx companyName = "Genso Inc.".obs; + + // URLs for legal pages + final String privacyPolicyUrl = "${APIEndpoint.baseUrl}/privacy-policy"; + final String termsOfServiceUrl = "${APIEndpoint.baseUrl}/terms-of-service"; + final String helpCenterUrl = "${APIEndpoint.baseUrl}/help-center"; + final String supportEmail = "support@quizmaster.com"; + + @override + void onInit() { + super.onInit(); + loadUserStat(); + // In a real app, you would load these from your API or local storage + loadUserProfileData(); + } + + void loadUserProfileData() { + try { + birthDate.value = _userController.userData?.birthDate ?? ""; + phoneNumber.value = _userController.userData?.phone ?? ""; + // joinDate.value = _userController.userData?. ?? ""; + } catch (e, stackTrace) { + logC.e("Failed to load user profile data: $e", stackTrace: stackTrace); + } + } + + void loadUserStat() async { + try { + final result = await _userService.getUserStat(_userController.userData!.id); + if (result != null) { + data.value = result.data; + } + } catch (e, stackTrace) { + logC.e("Failed to load user stat: $e", stackTrace: stackTrace); + } + } void logout() async { try { await _googleAuthService.signOut(); - await _userStorageService.clearUser(); _userController.clearUser(); _userStorageService.isLogged = false; - Get.offAllNamed(AppRoutes.loginPage); } catch (e, stackTrace) { logC.e("Google Sign-Out Error: $e", stackTrace: stackTrace); @@ -47,4 +106,66 @@ class ProfileController extends GetxController { await context.setLocale(locale); Get.updateLocale(locale); } + + // Settings methods + void toggleNotifications() { + notificationsEnabled.value = !notificationsEnabled.value; + // In a real app, you would save this preference to storage + } + + void toggleDarkMode() { + darkModeEnabled.value = !darkModeEnabled.value; + // In a real app, you would update the theme and save preference + } + + void toggleSoundEffects() { + soundEffectsEnabled.value = !soundEffectsEnabled.value; + // In a real app, you would save this preference to storage + } + + void clearCache() { + // Implement cache clearing logic + Get.snackbar( + "Success", + "Cache cleared successfully", + snackPosition: SnackPosition.BOTTOM, + ); + } + + // Legal and support methods + void openPrivacyPolicy() async { + await _launchUrl(privacyPolicyUrl); + } + + void openTermsOfService() async { + await _launchUrl(termsOfServiceUrl); + } + + void openHelpCenter() async { + await _launchUrl(helpCenterUrl); + } + + void contactSupport() async { + final Uri emailUri = Uri( + scheme: 'mailto', + path: supportEmail, + query: 'subject=Support Request&body=Hello, I need help with...', + ); + await _launchUrl(emailUri.toString()); + } + + Future _launchUrl(String urlString) async { + try { + final url = Uri.parse(urlString); + if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { + throw Exception('Could not launch $url'); + } + } catch (e) { + Get.snackbar( + "Error", + "Could not open the link", + snackPosition: SnackPosition.BOTTOM, + ); + } + } } diff --git a/lib/feature/profile/view/components/info_row_card.dart b/lib/feature/profile/view/components/info_row_card.dart new file mode 100644 index 0000000..00fd865 --- /dev/null +++ b/lib/feature/profile/view/components/info_row_card.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:quiz_app/app/const/colors/app_colors.dart'; +import 'package:quiz_app/app/const/text/text_style.dart'; + +class InfoRow extends StatelessWidget { + const InfoRow({super.key, required this.icon, required this.label, required this.value}); + + final IconData icon; + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: AppColors.primaryBlue), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: AppTextStyles.caption), + Text(value, style: AppTextStyles.body), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/feature/profile/view/components/section_header_card.dart b/lib/feature/profile/view/components/section_header_card.dart new file mode 100644 index 0000000..b91f9e5 --- /dev/null +++ b/lib/feature/profile/view/components/section_header_card.dart @@ -0,0 +1,35 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:quiz_app/app/const/colors/app_colors.dart'; +import 'package:quiz_app/app/const/text/text_style.dart'; + +class SectionHeader extends StatelessWidget { + const SectionHeader({super.key, required this.title, required this.icon, this.onEdit}); + + final String title; + final IconData icon; + final VoidCallback? onEdit; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(icon, size: 20, color: AppColors.primaryBlue), + const SizedBox(width: 8), + Text(title, style: AppTextStyles.subtitle.copyWith(fontWeight: FontWeight.bold)), + ], + ), + if (onEdit != null) + IconButton( + icon: Icon(LucideIcons.edit, color: AppColors.primaryBlue), + onPressed: onEdit, + tooltip: context.tr('edit_profile'), + ), + ], + ); + } +} diff --git a/lib/feature/profile/view/profile_view.dart b/lib/feature/profile/view/profile_view.dart index ee948db..7a6ff10 100644 --- a/lib/feature/profile/view/profile_view.dart +++ b/lib/feature/profile/view/profile_view.dart @@ -1,159 +1,241 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import 'package:quiz_app/app/const/text/text_style.dart'; +import 'package:quiz_app/component/app_name.dart'; import 'package:quiz_app/feature/profile/controller/profile_controller.dart'; +import 'package:quiz_app/app/const/colors/app_colors.dart'; +import 'package:quiz_app/feature/profile/view/components/info_row_card.dart'; +import 'package:quiz_app/feature/profile/view/components/section_header_card.dart'; class ProfileView extends GetView { const ProfileView({super.key}); @override Widget build(BuildContext context) { + const cardRadius = BorderRadius.all(Radius.circular(20)); + return Scaffold( - backgroundColor: const Color(0xFFF8F9FB), + backgroundColor: AppColors.background2, body: SafeArea( child: Padding( padding: const EdgeInsets.all(20), - child: Obx(() { - return Column( - children: [ - const SizedBox(height: 20), - _buildAvatar(), - const SizedBox(height: 12), - Text( - controller.userName.value, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - Text( - controller.email.value, - style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - const SizedBox(height: 24), - _buildStats(context), - const SizedBox(height: 32), - _buildActionButton( - context.tr("edit_profile"), - Icons.edit, - controller.editProfile, - ), - const SizedBox(height: 12), - _buildActionButton( - context.tr("change_language"), - Icons.language, - () => _showLanguageDialog(context), - ), - const SizedBox(height: 12), - _buildActionButton( - context.tr("logout"), - Icons.logout, - controller.logout, - isDestructive: true, - ), - ], - ); - }), + child: Obx( + () => SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + _avatar(), + // const SizedBox(height: 16), + // _userHeader(), + const SizedBox(height: 24), + _statsCard(context: context, cardRadius: cardRadius), + const SizedBox(height: 10), + _profileDetails(context: context, cardRadius: cardRadius), + const SizedBox(height: 10), + _settingsSection(context: context, cardRadius: cardRadius), + const SizedBox(height: 10), + _legalSection(context: context, cardRadius: cardRadius), + const SizedBox(height: 20), + ], + ), + ), + ), ), ), ); } - Widget _buildAvatar() { - if (controller.userImage.value != null) { - return CircleAvatar( - radius: 45, - backgroundColor: Colors.blueAccent, - backgroundImage: NetworkImage(controller.userImage.value!), - ); - } else { - return const CircleAvatar( - radius: 45, - backgroundColor: Colors.blueAccent, - child: Icon(Icons.person, size: 50, color: Colors.white), - ); - } - } + // -------------------- UTILITY WIDGETS -------------------- // - Widget _buildStats(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 6, - offset: Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + Widget _statChip(String label, String value) => Column( children: [ - _buildStatItem( - context.tr("total_quiz"), - controller.totalQuizzes.value.toString(), - ), - const SizedBox(width: 16), - _buildStatItem( - context.tr("avg_score"), - "${controller.avgScore.value}%", - ), + Text(value, style: AppTextStyles.statValue), + const SizedBox(height: 4), + Text(label, style: AppTextStyles.caption), ], - ), + ); + + Widget _settingsTile( + BuildContext context, { + required IconData icon, + required String title, + VoidCallback? onTap, + Color? iconColor, + Color? textColor, + }) { + final primary = iconColor ?? AppColors.primaryBlue; + return ListTile( + leading: Icon(icon, color: primary, size: 22), + title: Text(title, style: AppTextStyles.optionText.copyWith(color: textColor ?? AppColors.darkText)), + trailing: Icon(LucideIcons.chevronRight, color: AppColors.softGrayText, size: 18), + onTap: onTap, + dense: true, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ); } - Widget _buildStatItem(String label, String value) { - return Column( - children: [ - Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 13, color: Colors.grey)), - ], - ); - } + // -------------------- SECTIONS -------------------- // - Widget _buildActionButton(String title, IconData icon, VoidCallback onPressed, {bool isDestructive = false}) { - return SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - icon: Icon(icon, color: isDestructive ? Colors.red : Colors.white), - label: Text( - title, - style: TextStyle(color: isDestructive ? Colors.red : Colors.white), + Widget _avatar() => Center( + child: CircleAvatar( + radius: 50, + backgroundColor: AppColors.accentBlue, + foregroundImage: controller.userImage.value != null ? NetworkImage(controller.userImage.value!) : null, + child: controller.userImage.value == null ? Icon(LucideIcons.user, color: AppColors.primaryBlue, size: 40) : null, ), - onPressed: onPressed, - style: ElevatedButton.styleFrom( - backgroundColor: isDestructive ? Colors.red.shade50 : Colors.blueAccent, - padding: const EdgeInsets.symmetric(vertical: 14), - side: isDestructive ? const BorderSide(color: Colors.red) : BorderSide.none, + ); + + // Widget _userHeader() => Column( + // children: [ + // Text(controller.userName.value, style: AppTextStyles.title), + // Text(controller.email.value, style: AppTextStyles.subtitle), + // ], + // ); + + Widget _statsCard({required BuildContext context, required BorderRadius cardRadius}) => Card( + color: Colors.white, + elevation: 1, + shadowColor: AppColors.shadowPrimary, + shape: RoundedRectangleBorder(borderRadius: cardRadius), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _statChip(context.tr('total_quiz'), controller.data.value?.totalQuiz.toString() ?? '0'), + _statChip(context.tr('total_solve'), controller.data.value?.totalSolve.toString() ?? '0'), + _statChip(context.tr('avg_score'), '${controller.data.value?.avgScore ?? 100}%'), + ], + ), ), - ), - ); - } + ); + + Widget _profileDetails({required BuildContext context, required BorderRadius cardRadius}) => Card( + color: Colors.white, + elevation: 1, + shadowColor: AppColors.shadowPrimary, + shape: RoundedRectangleBorder(borderRadius: cardRadius), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), + child: Column( + children: [ + SectionHeader( + title: context.tr('personal_info'), + icon: LucideIcons.userCog, + onEdit: controller.editProfile, + ), + const Divider(height: 1), + const SizedBox(height: 20), + InfoRow(icon: LucideIcons.user, label: context.tr('full_name'), value: controller.userName.value), + InfoRow( + icon: LucideIcons.cake, + label: context.tr('birth_date'), + value: controller.birthDate.value ?? context.tr('not_set'), + ), + InfoRow( + icon: LucideIcons.phone, + label: context.tr('phone'), + value: controller.phoneNumber.value ?? context.tr('not_set'), + ), + InfoRow( + icon: LucideIcons.mapPin, + label: context.tr('location'), + value: controller.location.value ?? context.tr('not_set'), + ), + InfoRow( + icon: LucideIcons.calendar, + label: context.tr('joined'), + value: controller.joinDate.value ?? context.tr('not_available'), + ), + InfoRow( + icon: LucideIcons.graduationCap, + label: context.tr('education'), + value: controller.education.value ?? context.tr('not_set'), + ), + ], + ), + ), + ); + + Widget _settingsSection({required BuildContext context, required BorderRadius cardRadius}) => Card( + color: Colors.white, + elevation: 1, + shadowColor: AppColors.shadowPrimary, + shape: RoundedRectangleBorder(borderRadius: cardRadius), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: SectionHeader(title: context.tr('settings'), icon: LucideIcons.settings), + ), + const Divider(height: 1), + _settingsTile(Get.context!, icon: LucideIcons.languages, title: context.tr('change_language'), onTap: () => _showLanguageDialog(Get.context!)), + _settingsTile(Get.context!, + icon: LucideIcons.logOut, title: context.tr('logout'), iconColor: Colors.red, textColor: Colors.red, onTap: controller.logout), + ], + ), + ), + ); + + Widget _legalSection({required BuildContext context, required BorderRadius cardRadius}) => Card( + color: Colors.white, + elevation: 1, + shadowColor: AppColors.shadowPrimary, + shape: RoundedRectangleBorder(borderRadius: cardRadius), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10), + child: SectionHeader(title: context.tr('legal_and_support'), icon: LucideIcons.shieldQuestion), + ), + const Divider(height: 1), + _settingsTile(Get.context!, icon: LucideIcons.shield, title: context.tr('privacy_policy'), onTap: controller.openPrivacyPolicy), + _settingsTile(Get.context!, icon: LucideIcons.fileText, title: context.tr('terms_of_service'), onTap: controller.openTermsOfService), + _settingsTile(Get.context!, icon: LucideIcons.helpCircle, title: context.tr('help_center'), onTap: controller.openHelpCenter), + _settingsTile(Get.context!, icon: LucideIcons.mail, title: context.tr('contact_us'), onTap: controller.contactSupport), + _settingsTile(Get.context!, icon: LucideIcons.info, title: context.tr('about_app'), onTap: () => _showAboutAppDialog(Get.context!)), + ], + ), + ), + ); void _showLanguageDialog(BuildContext context) { showDialog( context: context, builder: (_) => AlertDialog( - title: Text(context.tr("select_language")), + title: Text(context.tr('select_language'), style: AppTextStyles.title), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( - leading: const Icon(Icons.language), - title: const Text("English"), + leading: Icon(LucideIcons.languages, color: AppColors.primaryBlue), + title: const Text('English'), onTap: () { controller.changeLanguage(context, 'en', 'US'); Navigator.of(context).pop(); }, ), ListTile( - leading: const Icon(Icons.language), - title: const Text("Bahasa Indonesia"), + leading: Icon(LucideIcons.languages, color: AppColors.primaryBlue), + title: const Text('Bahasa Indonesia'), onTap: () { - controller.changeLanguage(context, 'id', "ID"); + controller.changeLanguage(context, 'id', 'ID'); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: Icon(LucideIcons.languages, color: AppColors.primaryBlue), + title: const Text('Malaysia'), + onTap: () { + controller.changeLanguage(context, 'ms', 'MY'); Navigator.of(context).pop(); }, ), @@ -162,4 +244,28 @@ class ProfileView extends GetView { ), ); } + + void _showAboutAppDialog(BuildContext context) { + showDialog( + context: context, + builder: (_) => AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const AppName(), + const SizedBox(height: 16), + Text(controller.appName.value, style: AppTextStyles.title), + Text("${context.tr('version')}: ${controller.appVersion.value}", style: AppTextStyles.caption), + const SizedBox(height: 16), + Text(controller.appDescription.value, textAlign: TextAlign.center, style: AppTextStyles.body), + const SizedBox(height: 16), + Text('© ${DateTime.now().year} ${controller.companyName.value}', style: AppTextStyles.caption), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.tr('close'), style: AppTextStyles.optionText)), + ], + ), + ); + } } diff --git a/pubspec.lock b/pubspec.lock index 2ec5508..ec58578 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -586,6 +586,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c190f99..627a6a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: easy_localization: ^3.0.7+1 percent_indicator: ^4.2.5 connectivity_plus: ^6.1.4 + url_launcher: ^6.3.1 dev_dependencies: flutter_test: