feat: adding new profile interface

This commit is contained in:
akhdanre 2025-05-25 13:01:22 +07:00
parent 7d3f94dee1
commit 1e45cc271b
14 changed files with 631 additions and 125 deletions

View File

@ -87,5 +87,23 @@
"auto_generate_quiz": "Auto Generate Quiz", "auto_generate_quiz": "Auto Generate Quiz",
"ready_to_compete": "Ready to Compete?", "ready_to_compete": "Ready to Compete?",
"enter_code_to_join": "Enter the quiz code and show your skills!", "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"
} }

View File

@ -87,5 +87,23 @@
"ready_to_compete": "Siap untuk Bertanding?", "ready_to_compete": "Siap untuk Bertanding?",
"enter_code_to_join": "Masukkan kode kuis dan tunjukkan kemampuanmu!", "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"
} }

View File

@ -84,5 +84,23 @@
"auto_generate_quiz": "Jana Kuiz Automatik", "auto_generate_quiz": "Jana Kuiz Automatik",
"ready_to_compete": "Bersedia untuk Bertanding?", "ready_to_compete": "Bersedia untuk Bertanding?",
"enter_code_to_join": "Masukkan kod kuiz dan tunjukkan kemahiran anda!", "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"
} }

View File

@ -1,5 +1,5 @@
class APIEndpoint { 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 baseUrl = "http://103.193.178.121:5000";
static const String api = "$baseUrl/api"; static const String api = "$baseUrl/api";
@ -28,4 +28,5 @@ class APIEndpoint {
static const String userData = "/user"; static const String userData = "/user";
static const String userUpdate = "/user/update"; static const String userUpdate = "/user/update";
static const String userStat = "/user/status";
} }

View File

@ -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<String, dynamic> 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<String, dynamic> toJson() {
return {
'avg_score': avgScore,
'total_solve': totalSolve,
'total_quiz': totalQuiz,
};
}
}

View File

@ -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/core/utils/logger.dart';
import 'package:quiz_app/data/models/base/base_model.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_full_model.dart';
import 'package:quiz_app/data/models/user/user_stat_model.dart';
import 'package:quiz_app/data/providers/dio_client.dart'; import 'package:quiz_app/data/providers/dio_client.dart';
class UserService extends GetxService { class UserService extends GetxService {
@ -54,4 +55,23 @@ class UserService extends GetxService {
return null; return null;
} }
} }
Future<BaseResponseModel<UserStatModel>?> getUserStat(String id) async {
try {
final response = await _dio.get("${APIEndpoint.userStat}/$id");
if (response.statusCode == 200) {
final parsedResponse = BaseResponseModel<UserStatModel>.fromJson(
response.data,
(data) => UserStatModel.fromJson(data),
);
return parsedResponse;
} else {
return null;
}
} catch (e) {
logC.e("get user data error: $e");
return null;
}
}
} }

View File

@ -2,10 +2,19 @@ import 'dart:convert';
import 'package:quiz_app/data/entity/user/user_entity.dart'; import 'package:quiz_app/data/entity/user/user_entity.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/// A lightweight wrapper around [SharedPreferences] that persists
/// the loggedin user plus UI/feature preferences such as theme and
/// pushnotification optin.
class UserStorageService { class UserStorageService {
// Keys
static const _userKey = 'user_data'; 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; bool isLogged = false;
// User CRUD
Future<void> saveUser(UserEntity user) async { Future<void> saveUser(UserEntity user) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setString(_userKey, jsonEncode(user.toJson())); await prefs.setString(_userKey, jsonEncode(user.toJson()));
@ -14,7 +23,6 @@ class UserStorageService {
Future<UserEntity?> loadUser() async { Future<UserEntity?> loadUser() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_userKey); final jsonString = prefs.getString(_userKey);
if (jsonString == null) return null; if (jsonString == null) return null;
return UserEntity.fromJson(jsonDecode(jsonString)); return UserEntity.fromJson(jsonDecode(jsonString));
} }
@ -28,4 +36,29 @@ class UserStorageService {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
return prefs.containsKey(_userKey); return prefs.containsKey(_userKey);
} }
// UI Preferences
/// Persist the users theme choice.
Future<void> 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<bool> getDarkMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_darkModeKey) ?? false;
}
/// Persist the users pushnotification preference.
Future<void> setPushNotification(bool value) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_pushNotifKey, value);
}
/// Retrieve the stored pushnotification preference. Defaults to *true*.
Future<bool> getPushNotification() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_pushNotifKey) ?? true;
}
} }

View File

@ -1,5 +1,7 @@
import 'package:get/get.dart'; 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/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/data/services/user_storage_service.dart';
import 'package:quiz_app/feature/profile/controller/profile_controller.dart'; import 'package:quiz_app/feature/profile/controller/profile_controller.dart';
@ -7,6 +9,12 @@ class ProfileBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
Get.lazyPut<GoogleAuthService>(() => GoogleAuthService()); Get.lazyPut<GoogleAuthService>(() => GoogleAuthService());
Get.lazyPut(() => ProfileController(Get.find<UserStorageService>(), Get.find<GoogleAuthService>())); if (!Get.isRegistered<UserService>()) Get.lazyPut(() => UserService());
Get.lazyPut(() => ProfileController(
Get.find<UserController>(),
Get.find<UserStorageService>(),
Get.find<GoogleAuthService>(),
Get.find<UserService>(),
));
} }
} }

View File

@ -3,34 +3,93 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:quiz_app/app/routes/app_pages.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/core/utils/logger.dart';
import 'package:quiz_app/data/controllers/user_controller.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/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/data/services/user_storage_service.dart';
import 'package:url_launcher/url_launcher.dart';
class ProfileController extends GetxController { class ProfileController extends GetxController {
final UserController _userController = Get.find<UserController>(); final UserController _userController;
final UserStorageService _userStorageService; final UserStorageService _userStorageService;
final GoogleAuthService _googleAuthService; final GoogleAuthService _googleAuthService;
final UserService _userService;
ProfileController(this._userStorageService, this._googleAuthService); ProfileController(
this._userController,
this._userStorageService,
this._googleAuthService,
this._userService,
);
// User basic info
Rx<String> get userName => _userController.userName; Rx<String> get userName => _userController.userName;
Rx<String> get email => _userController.email; Rx<String> get email => _userController.email;
Rx<String?> get userImage => _userController.userImage; Rx<String?> get userImage => _userController.userImage;
Rx<UserStatModel?> data = Rx<UserStatModel?>(null);
final totalQuizzes = 12.obs; Rx<String?> birthDate = "".obs;
final avgScore = 85.obs; Rx<String?> phoneNumber = "".obs;
Rx<String?> location = "".obs;
Rx<String?> joinDate = "".obs;
Rx<String?> education = "".obs;
Rx<String?> profileImage = null.obs;
// App settings
Rx<bool> notificationsEnabled = true.obs;
Rx<bool> darkModeEnabled = false.obs;
Rx<bool> soundEffectsEnabled = true.obs;
// App info
Rx<String> appName = "Quiz App".obs;
Rx<String> appVersion = "1.0.2".obs;
Rx<String> appDescription = "An educational quiz app to test and improve your knowledge across various subjects.".obs;
Rx<String> 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 { void logout() async {
try { try {
await _googleAuthService.signOut(); await _googleAuthService.signOut();
await _userStorageService.clearUser(); await _userStorageService.clearUser();
_userController.clearUser(); _userController.clearUser();
_userStorageService.isLogged = false; _userStorageService.isLogged = false;
Get.offAllNamed(AppRoutes.loginPage); Get.offAllNamed(AppRoutes.loginPage);
} catch (e, stackTrace) { } catch (e, stackTrace) {
logC.e("Google Sign-Out Error: $e", stackTrace: stackTrace); logC.e("Google Sign-Out Error: $e", stackTrace: stackTrace);
@ -47,4 +106,66 @@ class ProfileController extends GetxController {
await context.setLocale(locale); await context.setLocale(locale);
Get.updateLocale(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<void> _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,
);
}
}
} }

View File

@ -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),
],
),
),
],
),
);
}
}

View File

@ -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'),
),
],
);
}
}

View File

@ -1,159 +1,241 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/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<ProfileController> { class ProfileView extends GetView<ProfileController> {
const ProfileView({super.key}); const ProfileView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const cardRadius = BorderRadius.all(Radius.circular(20));
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FB), backgroundColor: AppColors.background2,
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Obx(() { child: Obx(
return Column( () => SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ 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), const SizedBox(height: 24),
_buildStats(context), _avatar(),
const SizedBox(height: 32), // const SizedBox(height: 16),
_buildActionButton( // _userHeader(),
context.tr("edit_profile"), const SizedBox(height: 24),
Icons.edit, _statsCard(context: context, cardRadius: cardRadius),
controller.editProfile, 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),
],
), ),
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,
), ),
),
),
);
}
// -------------------- UTILITY WIDGETS -------------------- //
Widget _statChip(String label, String value) => Column(
children: [
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 _buildAvatar() { // -------------------- SECTIONS -------------------- //
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),
);
}
}
Widget _buildStats(BuildContext context) { Widget _avatar() => Center(
return Container( child: CircleAvatar(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), radius: 50,
decoration: BoxDecoration( 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,
),
);
// 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, color: Colors.white,
borderRadius: BorderRadius.circular(16), elevation: 1,
boxShadow: const [ shadowColor: AppColors.shadowPrimary,
BoxShadow( shape: RoundedRectangleBorder(borderRadius: cardRadius),
color: Colors.black12, child: Padding(
blurRadius: 6, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
offset: Offset(0, 2),
),
],
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildStatItem( _statChip(context.tr('total_quiz'), controller.data.value?.totalQuiz.toString() ?? '0'),
context.tr("total_quiz"), _statChip(context.tr('total_solve'), controller.data.value?.totalSolve.toString() ?? '0'),
controller.totalQuizzes.value.toString(), _statChip(context.tr('avg_score'), '${controller.data.value?.avgScore ?? 100}%'),
),
const SizedBox(width: 16),
_buildStatItem(
context.tr("avg_score"),
"${controller.avgScore.value}%",
),
], ],
), ),
),
); );
}
Widget _buildStatItem(String label, String value) { Widget _profileDetails({required BuildContext context, required BorderRadius cardRadius}) => Card(
return Column( 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: [ children: [
Text(value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), SectionHeader(
const SizedBox(height: 4), title: context.tr('personal_info'),
Text(label, style: const TextStyle(fontSize: 13, color: Colors.grey)), 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 _buildActionButton(String title, IconData icon, VoidCallback onPressed, {bool isDestructive = false}) { Widget _settingsSection({required BuildContext context, required BorderRadius cardRadius}) => Card(
return SizedBox( color: Colors.white,
width: double.infinity, elevation: 1,
child: ElevatedButton.icon( shadowColor: AppColors.shadowPrimary,
icon: Icon(icon, color: isDestructive ? Colors.red : Colors.white), shape: RoundedRectangleBorder(borderRadius: cardRadius),
label: Text( child: Padding(
title, padding: const EdgeInsets.symmetric(vertical: 10.0),
style: TextStyle(color: isDestructive ? Colors.red : Colors.white), child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10),
child: SectionHeader(title: context.tr('settings'), icon: LucideIcons.settings),
), ),
onPressed: onPressed, const Divider(height: 1),
style: ElevatedButton.styleFrom( _settingsTile(Get.context!, icon: LucideIcons.languages, title: context.tr('change_language'), onTap: () => _showLanguageDialog(Get.context!)),
backgroundColor: isDestructive ? Colors.red.shade50 : Colors.blueAccent, _settingsTile(Get.context!,
padding: const EdgeInsets.symmetric(vertical: 14), icon: LucideIcons.logOut, title: context.tr('logout'), iconColor: Colors.red, textColor: Colors.red, onTap: controller.logout),
side: isDestructive ? const BorderSide(color: Colors.red) : BorderSide.none, ],
),
),
);
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) { void _showLanguageDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
builder: (_) => AlertDialog( builder: (_) => AlertDialog(
title: Text(context.tr("select_language")), title: Text(context.tr('select_language'), style: AppTextStyles.title),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.language), leading: Icon(LucideIcons.languages, color: AppColors.primaryBlue),
title: const Text("English"), title: const Text('English'),
onTap: () { onTap: () {
controller.changeLanguage(context, 'en', 'US'); controller.changeLanguage(context, 'en', 'US');
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.language), leading: Icon(LucideIcons.languages, color: AppColors.primaryBlue),
title: const Text("Bahasa Indonesia"), title: const Text('Bahasa Indonesia'),
onTap: () { 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(); Navigator.of(context).pop();
}, },
), ),
@ -162,4 +244,28 @@ class ProfileView extends GetView<ProfileController> {
), ),
); );
} }
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)),
],
),
);
}
} }

View File

@ -586,6 +586,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -46,6 +46,7 @@ dependencies:
easy_localization: ^3.0.7+1 easy_localization: ^3.0.7+1
percent_indicator: ^4.2.5 percent_indicator: ^4.2.5
connectivity_plus: ^6.1.4 connectivity_plus: ^6.1.4
url_launcher: ^6.3.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: