feat: done creating home page and fix the interface

This commit is contained in:
akhdanre 2025-04-26 14:02:22 +07:00
parent 837823f937
commit 9c36507fb7
16 changed files with 609 additions and 55 deletions

View File

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

View File

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

View File

@ -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),

View File

@ -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,

View File

@ -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
),
),
),

View File

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

View File

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

View File

@ -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>(() => HomeController(Get.find<UserStorageService>()));
}
}

View File

@ -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<String> userName = "Dani".obs;
String? userImage;
@override
void onInit() {
getUserData();
super.onInit();
}
Future<void> getUserData() async {
LoginResponseModel? data = await _userStorageService.loadUser();
if (data == null) return;
print("User data: ${data.toJson()}");
userName.value = data.name;
userImage = data.picUrl;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,41 +14,67 @@ class LoginView extends GetView<LoginController> {
@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,
),
],
),
),