develop #1
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<void> saveUser(UserEntity user) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_userKey, jsonEncode(user.toJson()));
|
||||
|
@ -14,7 +23,6 @@ class UserStorageService {
|
|||
Future<UserEntity?> 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<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 user’s push‑notification preference.
|
||||
Future<void> setPushNotification(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_pushNotifKey, value);
|
||||
}
|
||||
|
||||
/// Retrieve the stored push‑notification preference. Defaults to *true*.
|
||||
Future<bool> getPushNotification() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_pushNotifKey) ?? true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>(() => 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>(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<UserController>();
|
||||
|
||||
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<String> get userName => _userController.userName;
|
||||
Rx<String> get email => _userController.email;
|
||||
Rx<String?> get userImage => _userController.userImage;
|
||||
Rx<UserStatModel?> data = Rx<UserStatModel?>(null);
|
||||
|
||||
final totalQuizzes = 12.obs;
|
||||
final avgScore = 85.obs;
|
||||
Rx<String?> birthDate = "".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 {
|
||||
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<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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<ProfileController> {
|
||||
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<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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
64
pubspec.lock
64
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue