diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index d43773c..7b56701 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -1,6 +1,7 @@ import 'package:get/get_navigation/src/routes/get_route.dart'; import 'package:quiz_app/app/middleware/auth_middleware.dart'; -import 'package:quiz_app/feature/home/presentation/home_page.dart'; +import 'package:quiz_app/feature/home/binding/home_binding.dart'; +import 'package:quiz_app/feature/home/view/home_page.dart'; import 'package:quiz_app/feature/login/bindings/login_binding.dart'; import 'package:quiz_app/feature/login/view/login_page.dart'; import 'package:quiz_app/feature/register/binding/register_binding.dart'; @@ -28,6 +29,7 @@ class AppPages { GetPage( name: AppRoutes.homePage, page: () => HomeView(), + binding: HomeBinding(), middlewares: [AuthMiddleware()], ), ]; diff --git a/lib/component/app_name.dart b/lib/component/app_name.dart index b0a2205..0154c68 100644 --- a/lib/component/app_name.dart +++ b/lib/component/app_name.dart @@ -1,15 +1,35 @@ import 'package:flutter/material.dart'; class AppName extends StatelessWidget { - const AppName({super.key}); + final double fontSize; + + const AppName({super.key, this.fontSize = 36}); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Text("GEN", style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold)), - Text("SO", style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.red)), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "GEN", + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w700, + color: Color(0xFF172B4D), + letterSpacing: 1.2, + ), + ), + Text( + "SO", + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w700, + color: Color(0xFF0052CC), + letterSpacing: 1.2, + ), + ), ], ); } diff --git a/lib/component/global_button.dart b/lib/component/global_button.dart index 3bd07c5..a7c145c 100644 --- a/lib/component/global_button.dart +++ b/lib/component/global_button.dart @@ -12,12 +12,18 @@ class GlobalButton extends StatelessWidget { width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( - foregroundColor: Colors.black, - backgroundColor: Colors.white, + foregroundColor: Colors.white, + backgroundColor: const Color(0xFF0052CC), + shadowColor: const Color(0x330052CC), + elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), padding: const EdgeInsets.symmetric(vertical: 16), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), onPressed: onPressed, child: Text(text), diff --git a/lib/component/global_text_field.dart b/lib/component/global_text_field.dart index 80fff87..0827170 100644 --- a/lib/component/global_text_field.dart +++ b/lib/component/global_text_field.dart @@ -25,24 +25,30 @@ class GlobalTextField extends StatelessWidget { obscureText: isPassword ? obscureText : false, decoration: InputDecoration( labelText: labelText, + labelStyle: const TextStyle(color: Color(0xFF6B778C), fontSize: 14), + hintText: hintText, + hintStyle: const TextStyle(color: Color(0xFF6B778C), fontSize: 14), + filled: true, + fillColor: const Color(0xFFFAFBFC), // Background soft white + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, + borderSide: BorderSide(color: Colors.transparent), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, + borderSide: BorderSide(color: Colors.transparent), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), - borderSide: BorderSide.none, + borderSide: const BorderSide(color: Color(0xFF0052CC), width: 2), ), - hintText: hintText, - filled: true, - fillColor: const Color.fromARGB(255, 238, 238, 238), suffixIcon: isPassword ? IconButton( - icon: Icon(obscureText ? Icons.visibility_off : Icons.visibility), + icon: Icon( + obscureText ? Icons.visibility_off : Icons.visibility, + color: const Color(0xFF6B778C), + ), onPressed: onToggleVisibility, ) : null, diff --git a/lib/component/label_text_field.dart b/lib/component/label_text_field.dart index 23cd9cc..59f3008 100644 --- a/lib/component/label_text_field.dart +++ b/lib/component/label_text_field.dart @@ -5,20 +5,29 @@ class LabelTextField extends StatelessWidget { final double fontSize; final FontWeight fontWeight; final Alignment alignment; - const LabelTextField( - {super.key, required, required this.label, this.fontSize = 16, this.alignment = Alignment.centerLeft, this.fontWeight = FontWeight.bold}); + final Color? color; + + const LabelTextField({ + super.key, + required this.label, + this.fontSize = 16, + this.fontWeight = FontWeight.bold, + this.alignment = Alignment.centerLeft, + this.color, // Tambahkan warna opsional + }); @override Widget build(BuildContext context) { return Align( alignment: alignment, child: Padding( - padding: EdgeInsets.fromLTRB(10, 5, 0, 5), + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), // padding lebih natural child: Text( label, style: TextStyle( fontSize: fontSize, fontWeight: fontWeight, + color: color ?? const Color(0xFF172B4D), // default modern dark text ), ), ), diff --git a/lib/component/quiz_container_component.dart b/lib/component/quiz_container_component.dart new file mode 100644 index 0000000..0a1d448 --- /dev/null +++ b/lib/component/quiz_container_component.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +class QuizContainerComponent extends StatelessWidget { + const QuizContainerComponent({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Color(0xFFFAFBFC), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Color(0xFFE1E4E8), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: Offset(0, 2), + ) + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Color(0xFF0052CC), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.school, color: Colors.white, size: 28), + ), + const SizedBox(width: 12), + // Quiz Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Physics", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF172B4D), + ), + ), + const SizedBox(height: 4), + const Text( + "created by Akhdan Rabbani", + style: TextStyle( + fontSize: 12, + color: Color(0xFF6B778C), + ), + ), + const SizedBox(height: 8), + Row( + children: const [ + Icon(Icons.format_list_bulleted, size: 14, color: Color(0xFF6B778C)), + SizedBox(width: 4), + Text( + "50 Quizzes", + style: TextStyle(fontSize: 12, color: Color(0xFF6B778C)), + ), + SizedBox(width: 12), + Icon(Icons.access_time, size: 14, color: Color(0xFF6B778C)), + SizedBox(width: 4), + Text( + "1 hr duration", + style: TextStyle(fontSize: 12, color: Color(0xFF6B778C)), + ), + ], + ) + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/core/helper/responsive.dart b/lib/core/helper/responsive.dart new file mode 100644 index 0000000..007718c --- /dev/null +++ b/lib/core/helper/responsive.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class SizeConfig { + late double screenWidth; + late double screenHeight; + double baseSize = 8.0; + + SizeConfig(BuildContext context) { + final mediaQueryData = MediaQuery.of(context); + screenWidth = mediaQueryData.size.width; + screenHeight = mediaQueryData.size.height; + } + + double size(double multiplier) { + return baseSize * multiplier; + } + + double height(double multiplier) { + return screenHeight * (multiplier / 100); + } + + double width(double multiplier) { + return screenWidth * (multiplier / 100); + } +} diff --git a/lib/feature/home/binding/home_binding.dart b/lib/feature/home/binding/home_binding.dart new file mode 100644 index 0000000..d2028f7 --- /dev/null +++ b/lib/feature/home/binding/home_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; +import 'package:quiz_app/data/services/user_storage_service.dart'; +import 'package:quiz_app/feature/home/controller/home_controller.dart'; + +class HomeBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => HomeController(Get.find())); + } +} diff --git a/lib/feature/home/controller/home_controller.dart b/lib/feature/home/controller/home_controller.dart new file mode 100644 index 0000000..aef1787 --- /dev/null +++ b/lib/feature/home/controller/home_controller.dart @@ -0,0 +1,26 @@ +import 'package:get/get.dart'; +import 'package:quiz_app/data/models/login/login_response_model.dart'; +import 'package:quiz_app/data/services/user_storage_service.dart'; + +class HomeController extends GetxController { + final UserStorageService _userStorageService; + + HomeController(this._userStorageService); + + Rx userName = "Dani".obs; + String? userImage; + + @override + void onInit() { + getUserData(); + super.onInit(); + } + + Future getUserData() async { + LoginResponseModel? data = await _userStorageService.loadUser(); + if (data == null) return; + print("User data: ${data.toJson()}"); + userName.value = data.name; + userImage = data.picUrl; + } +} diff --git a/lib/feature/home/presentation/home_page.dart b/lib/feature/home/presentation/home_page.dart deleted file mode 100644 index 68bfbfc..0000000 --- a/lib/feature/home/presentation/home_page.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:quiz_app/core/utils/logger.dart'; - -class HomeView extends StatelessWidget with WidgetsBindingObserver { - const HomeView({super.key}); - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - - logC.i("the state is $state"); - super.didChangeAppLifecycleState(state); - } - - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Text("Home screen"), - ), - ); - } -} diff --git a/lib/feature/home/view/component/button_option.dart b/lib/feature/home/view/component/button_option.dart new file mode 100644 index 0000000..fe1647c --- /dev/null +++ b/lib/feature/home/view/component/button_option.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +class ButtonOption extends StatelessWidget { + final VoidCallback onCreate; + final VoidCallback onCreateRoom; + final VoidCallback onJoinRoom; + + const ButtonOption({ + super.key, + required this.onCreate, + required this.onCreateRoom, + required this.onJoinRoom, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: 220, + margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), + child: Row( + children: [ + Expanded(child: _buildCreateButton()), + const SizedBox(width: 12), + Expanded(child: _buildRoomButtons()), + ], + ), + ); + } + + Widget _buildCreateButton() { + return InkWell( + onTap: onCreate, + borderRadius: BorderRadius.circular(16), + child: SizedBox( + height: double.infinity, + child: _buildButtonContainer( + label: 'Buat Quiz', + gradientColors: [Color(0xFF0052CC), Color(0xFF0367D3)], + icon: Icons.create, + ), + ), + ); + } + + Widget _buildRoomButtons() { + return Column( + children: [ + Expanded( + child: InkWell( + onTap: onCreateRoom, + borderRadius: BorderRadius.circular(16), + child: _buildButtonContainer( + label: 'Buat Room', + gradientColors: [Color(0xFF36B37E), Color(0xFF22C39F)], + icon: Icons.meeting_room, + ), + ), + ), + const SizedBox(height: 12), + Expanded( + child: InkWell( + onTap: onJoinRoom, + borderRadius: BorderRadius.circular(16), + child: _buildButtonContainer( + label: 'Join Room', + gradientColors: [Color(0xFFFFAB00), Color(0xFFFFC107)], + icon: Icons.group, + ), + ), + ), + ], + ); + } + + Widget _buildButtonContainer({ + required String label, + required List gradientColors, + required IconData icon, + }) { + return Container( + alignment: Alignment.bottomLeft, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: gradientColors, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: gradientColors.last.withOpacity(0.4), + blurRadius: 6, + offset: const Offset(2, 4), + ), + ], + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Icon(icon, color: Colors.white, size: 24), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/feature/home/view/component/recomendation_component.dart b/lib/feature/home/view/component/recomendation_component.dart new file mode 100644 index 0000000..a0e4db4 --- /dev/null +++ b/lib/feature/home/view/component/recomendation_component.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:quiz_app/component/quiz_container_component.dart'; + +class RecomendationComponent extends StatelessWidget { + const RecomendationComponent({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _label(), + const SizedBox(height: 10), + QuizContainerComponent(), + const SizedBox(height: 10), + QuizContainerComponent(), + const SizedBox(height: 10), + QuizContainerComponent() + ], + ); + } + + Widget _label() { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + "Quiz Recommendation", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF172B4D), // dark text + ), + ), + ); + } +} diff --git a/lib/feature/home/view/component/search_component.dart b/lib/feature/home/view/component/search_component.dart new file mode 100644 index 0000000..eef2c2f --- /dev/null +++ b/lib/feature/home/view/component/search_component.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +class SearchComponent extends StatelessWidget { + const SearchComponent({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), + decoration: BoxDecoration( + color: const Color(0xFFFAFBFC), // Soft background + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Color(0xFFE1E4E8)), // Light border + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitleSection(), + const SizedBox(height: 12), + _buildCategoryRow(), + const SizedBox(height: 12), + _buildSearchInput(), + ], + ), + ); + } + + Widget _buildTitleSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + "Ready for a new challenge?", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF172B4D), + ), + ), + SizedBox(height: 5), + Text( + "Search or select by category", + style: TextStyle( + fontSize: 14, + color: Color(0xFF6B778C), // Soft gray text + ), + ), + ], + ); + } + + Widget _buildCategoryRow() { + return Row( + children: [ + _buildCategoryComponent("History"), + const SizedBox(width: 8), + _buildCategoryComponent("Science"), + ], + ); + } + + Widget _buildCategoryComponent(String category) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Color(0xFFD6E4FF), // Soft blue chip + borderRadius: BorderRadius.circular(20), + ), + child: Text( + category, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Color(0xFF0052CC), // Primary blue + ), + ), + ); + } + + Widget _buildSearchInput() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + ), + child: const TextField( + decoration: InputDecoration( + hintText: "Search for quizzes...", + hintStyle: TextStyle(color: Color(0xFF6B778C)), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ); + } +} diff --git a/lib/feature/home/view/component/user_gretings.dart b/lib/feature/home/view/component/user_gretings.dart new file mode 100644 index 0000000..8a3ee05 --- /dev/null +++ b/lib/feature/home/view/component/user_gretings.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class UserGretingsComponent extends StatelessWidget { + final String userName; + final String? userImage; + const UserGretingsComponent({super.key, required this.userName, required this.userImage}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (userImage != null) + CircleAvatar( + backgroundImage: NetworkImage(userImage!), + ) + else + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey, + ), + child: Icon( + Icons.person, + color: Colors.white, + size: 30, + ), + ), + SizedBox( + width: 10, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Selamat Siang", + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + "Hello $userName", + style: TextStyle(fontWeight: FontWeight.w500), + ), + ], + ), + Spacer(), + Icon(Icons.notifications), + SizedBox( + width: 10, + ) + ], + ); + } +} diff --git a/lib/feature/home/view/home_page.dart b/lib/feature/home/view/home_page.dart new file mode 100644 index 0000000..794116a --- /dev/null +++ b/lib/feature/home/view/home_page.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:quiz_app/feature/home/controller/home_controller.dart'; +import 'package:quiz_app/feature/home/view/component/button_option.dart'; +import 'package:quiz_app/feature/home/view/component/recomendation_component.dart'; +import 'package:quiz_app/feature/home/view/component/search_component.dart'; +import 'package:quiz_app/feature/home/view/component/user_gretings.dart'; + +class HomeView extends GetView { + const HomeView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Obx( + () => UserGretingsComponent( + userName: controller.userName.value, + userImage: controller.userImage, + ), + ), + const SizedBox(height: 20), + ], + ), + ), + // ButtonOption di luar Padding + ButtonOption( + onCreate: () {}, + onCreateRoom: () {}, + onJoinRoom: () {}, + ), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + SearchComponent(), + const SizedBox(height: 20), + RecomendationComponent(), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/feature/login/view/login_page.dart b/lib/feature/login/view/login_page.dart index 6718e6f..897cdeb 100644 --- a/lib/feature/login/view/login_page.dart +++ b/lib/feature/login/view/login_page.dart @@ -14,41 +14,67 @@ class LoginView extends GetView { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFFFAFBFC), // background soft clean body: SafeArea( child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: ListView( children: [ - Padding(padding: const EdgeInsets.symmetric(vertical: 40), child: AppName()), - LabelTextField( + const SizedBox(height: 40), + const AppName(), + const SizedBox(height: 40), + const LabelTextField( label: "Log In", - fontSize: 24, + fontSize: 28, + fontWeight: FontWeight.bold, + color: Color(0xFF172B4D), ), - const SizedBox(height: 10), - LabelTextField(label: "Email"), + const SizedBox(height: 24), + const LabelTextField( + label: "Email", + color: Color(0xFF6B778C), + fontSize: 14, + ), + const SizedBox(height: 6), GlobalTextField( controller: controller.emailController, + hintText: "Masukkan email anda", ), - const SizedBox(height: 10), - LabelTextField(label: "Password"), + const SizedBox(height: 20), + const LabelTextField( + label: "Password", + color: Color(0xFF6B778C), + fontSize: 14, + ), + const SizedBox(height: 6), Obx( () => GlobalTextField( controller: controller.passwordController, isPassword: true, obscureText: controller.isPasswordHidden.value, onToggleVisibility: controller.togglePasswordVisibility, + hintText: "Masukkan password anda", ), ), - const SizedBox(height: 40), - GlobalButton(onPressed: controller.loginWithEmail, text: "Masuk"), - const SizedBox(height: 20), - LabelTextField(label: "OR", alignment: Alignment.center), - const SizedBox(height: 20), + const SizedBox(height: 32), + GlobalButton( + onPressed: controller.loginWithEmail, + text: "Masuk", + ), + const SizedBox(height: 24), + const LabelTextField( + label: "OR", + alignment: Alignment.center, + color: Color(0xFF6B778C), + ), + const SizedBox(height: 24), GoogleButton( onPress: controller.loginWithGoogle, ), - const SizedBox(height: 20), - RegisterTextButton(onTap: controller.goToRegsPage) + const SizedBox(height: 32), + RegisterTextButton( + onTap: controller.goToRegsPage, + ), ], ), ),