feat: add feature about detail and prepare repsponse

This commit is contained in:
pahmiudahgede 2025-05-15 16:27:07 +07:00
parent 0fbbb807d9
commit 9bbfd4529a
18 changed files with 531 additions and 229 deletions

View File

@ -10,4 +10,7 @@ export 'package:rijig_mobile/features/auth/service/logout_service.dart';
export 'package:rijig_mobile/features/auth/service/otp_service.dart';
export 'package:rijig_mobile/globaldata/trash/trash_repository.dart';
export 'package:rijig_mobile/globaldata/trash/trash_service.dart';
export 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart';
export 'package:rijig_mobile/globaldata/trash/trash_viewmodel.dart';
export 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart';
export 'package:rijig_mobile/features/home/repositories/about_repository.dart';
export 'package:rijig_mobile/features/home/service/about_service.dart';

View File

@ -7,5 +7,9 @@ void init() {
sl.registerFactory(() => OtpViewModel(OtpService(OtpRepository())));
sl.registerFactory(() => LogoutViewModel(LogoutService(LogoutRepository())));
sl.registerFactory(() => TrashViewModel(TrashCategoryService(TrashCategoryRepository())));
sl.registerFactory(
() => TrashViewModel(TrashCategoryService(TrashCategoryRepository())),
);
sl.registerFactory(() => AboutViewModel(AboutService(AboutRepository())));
sl.registerFactory(() => AboutDetailViewModel(AboutService(AboutRepository())));
}

View File

@ -39,5 +39,13 @@ final router = GoRouter(
),
GoRoute(path: '/cart', builder: (context, state) => CartScreen()),
GoRoute(path: '/profil', builder: (context, state) => ProfilScreen()),
GoRoute(
path: '/aboutdetail',
builder: (context, state) {
dynamic data = state.extra;
return AboutDetailScreenComp(data: data);
},
),
],
);

View File

@ -2,12 +2,13 @@ export 'package:go_router/go_router.dart';
export 'package:rijig_mobile/core/utils/navigation.dart';
export 'package:rijig_mobile/features/activity/activity_screen.dart';
export 'package:rijig_mobile/features/cart/cart_screen.dart';
export 'package:rijig_mobile/features/home/home_screen.dart';
export 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart';
export 'package:rijig_mobile/features/profil/profil_screen.dart';
export 'package:rijig_mobile/features/requestpick/requestpickup_screen.dart';
export 'package:rijig_mobile/features/requestpick/presentation/screen/requestpickup_screen.dart';
export 'package:rijig_mobile/features/auth/presentation/screen/inputpin_screen.dart';
export 'package:rijig_mobile/features/auth/presentation/screen/login_screen.dart';
export 'package:rijig_mobile/features/auth/presentation/screen/otp_screen.dart';
export 'package:rijig_mobile/features/auth/presentation/screen/verifpin_screen.dart';
export 'package:rijig_mobile/features/launch/onboardingpage_screen.dart';
export 'package:rijig_mobile/features/launch/splash_screen.dart';
export 'package:rijig_mobile/features/home/presentation/components/about_detail_comp.dart';

View File

@ -4,7 +4,7 @@ import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/features/activity/activity_screen.dart';
import 'package:rijig_mobile/features/cart/cart_screen.dart';
import 'package:rijig_mobile/features/home/home_screen.dart';
import 'package:rijig_mobile/features/home/presentation/screen/home_screen.dart';
import 'package:rijig_mobile/features/profil/profil_screen.dart';
import 'package:shared_preferences/shared_preferences.dart';

View File

@ -1,222 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:iconsax_flutter/iconsax_flutter.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/features/home/components/product_card.dart';
import 'package:rijig_mobile/features/home/model/product.dart';
import 'package:rijig_mobile/widget/card_withicon.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: whiteColor,
body: SafeArea(
child: SingleChildScrollView(
padding: PaddingCustom().paddingHorizontalVertical(16, 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Rijig",
style: Tulisan.heading(color: primaryColor),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Iconsax.notification),
Gap(10),
Icon(Iconsax.message_2),
],
),
],
),
),
],
),
Gap(20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CardWithIcon(
icon: Icons.account_circle,
text: 'Users',
number: '245',
onTap: () {},
),
CardWithIcon(
icon: Icons.shopping_cart,
text: 'Orders',
number: '178',
onTap: () {},
),
],
),
Gap(20),
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Important!",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
const Gap(15),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
SpecialOfferCard(
image: "assets/image/Image Banner 2.png",
category: "Smartphone",
numOfBrands: 18,
press: () {},
),
Gap(10),
SpecialOfferCard(
image: "assets/image/Image Banner 3.png",
category: "Fashion",
numOfBrands: 24,
press: () {},
),
],
),
),
],
),
Gap(20),
Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Artikel",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...List.generate(demoProducts.length, (index) {
if (demoProducts[index].isPopular) {
return Padding(
padding: const EdgeInsets.only(left: 20),
child: ProductCard(
product: demoProducts[index],
onPress: () {},
),
);
}
return const SizedBox.shrink();
}),
const SizedBox(width: 20),
],
),
),
],
),
],
),
),
),
);
}
}
class SpecialOfferCard extends StatelessWidget {
const SpecialOfferCard({
super.key,
required this.category,
required this.image,
required this.numOfBrands,
required this.press,
});
final String category, image;
final int numOfBrands;
final GestureTapCallback press;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: press,
child: SizedBox(
width: 242,
height: 100,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
Image.asset(image, fit: BoxFit.cover),
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black54,
Colors.black38,
Colors.black26,
Colors.transparent,
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Text.rich(
TextSpan(
style: const TextStyle(color: Colors.white),
children: [
TextSpan(
text: "$category\n",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
TextSpan(text: "$numOfBrands Brands"),
],
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,55 @@
class AboutModel {
final String id;
final String title;
final String coverImage;
final String createdAt;
final String updatedAt;
AboutModel({
required this.id,
required this.title,
required this.coverImage,
required this.createdAt,
required this.updatedAt,
});
factory AboutModel.fromJson(Map<String, dynamic> json) {
return AboutModel(
id: json['id'],
title: json['title'],
coverImage: json['cover_image'],
createdAt: json['created_at'],
updatedAt: json['updated_at'],
);
}
}
class AboutDetailModel {
final String id;
final String aboutId; // nullable jika perlu
final String imageDetail;
final String description;
final String createdAt;
final String updatedAt;
AboutDetailModel({
required this.id,
required this.aboutId,
required this.imageDetail,
required this.description,
required this.createdAt,
required this.updatedAt,
});
factory AboutDetailModel.fromJson(Map<String, dynamic> json) {
return AboutDetailModel(
id: json['id'],
aboutId: json['about_id'] ?? '', // Use empty string or null as fallback
imageDetail: json['image_detail'],
description: json['description'],
createdAt: json['created_at'],
updatedAt: json['updated_at'],
);
}
}

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:provider/provider.dart';
import 'package:rijig_mobile/core/router.dart';
import 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart';
import 'package:rijig_mobile/features/requestpick/presentation/screen/requestpickup_screen.dart';
class AboutComponent extends StatefulWidget {
const AboutComponent({super.key});
@override
AboutComponentState createState() => AboutComponentState();
}
class AboutComponentState extends State<AboutComponent> {
int _current = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<AboutViewModel>(context, listen: false).getAboutList();
});
}
@override
Widget build(BuildContext context) {
final String? baseUrl = dotenv.env["BASE_URL"];
return Consumer<AboutViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading) {
return ListView.builder(
itemCount: 1,
itemBuilder: (context, index) {
return SkeletonCard();
},
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
if (viewModel.aboutList == null || viewModel.aboutList!.isEmpty) {
return Center(child: Text("No data available"));
}
List<Map<String, dynamic>> imageSliders =
viewModel.aboutList!.map((about) {
return {
"iconPath": "$baseUrl${about.coverImage}",
"route": about.id,
"title": about.title,
};
}).toList();
return Column(
children: [
CarouselSlider(
items:
imageSliders.map((imageData) {
return InkWell(
child: Container(
height: 150,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(imageData["iconPath"]),
fit: BoxFit.cover,
),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: Stack(
children: [
Positioned(
top: 10,
left: 10,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
color: Colors.black.withOpacity(0.5),
child: Text(
imageData["title"],
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
onTap: () {
debugPrint("Tapped on ${imageData['route']}");
// imageData["route"];
router.push("/aboutdetail", extra: imageData["route"]);
},
);
}).toList(),
options: CarouselOptions(
autoPlay: true,
autoPlayInterval: Duration(seconds: 8),
enlargeCenterPage: true,
height: 150,
onPageChanged: (index, reason) {
setState(() {
_current = index;
});
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children:
imageSliders.asMap().entries.map((entry) {
return GestureDetector(
child: Container(
width: 8.0,
height: 8.0,
margin: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 4.0,
),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: (Theme.of(context).brightness ==
Brightness.dark
? Colors.blue
: Colors.blue)
.withOpacity(_current == entry.key ? 0.9 : 0.2),
),
),
);
}).toList(),
),
],
);
},
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:provider/provider.dart';
import 'package:rijig_mobile/features/home/presentation/viewmodel/about_vmod.dart';
import 'package:rijig_mobile/features/requestpick/presentation/screen/requestpickup_screen.dart';
import 'package:rijig_mobile/widget/appbar.dart';
class AboutDetailScreenComp extends StatefulWidget {
final String data;
const AboutDetailScreenComp({super.key, required this.data});
@override
State<AboutDetailScreenComp> createState() => _AboutDetailScreenCompState();
}
class _AboutDetailScreenCompState extends State<AboutDetailScreenComp> {
@override
void initState() {
super.initState();
context.read<AboutDetailViewModel>().getDetail(widget.data);
}
@override
Widget build(BuildContext context) {
final String? baseurl = dotenv.env['BASE_URL'];
return Scaffold(
appBar: CustomAppBar(judul: "About Detail"),
body: Consumer<AboutDetailViewModel>(
builder: (context, vm, _) {
if (vm.isLoading) {
return ListView.builder(
itemCount: 2,
itemBuilder: (context, index) {
return SkeletonCard();
},
);
}
if (vm.errorMessage != null) return Text(vm.errorMessage!);
return ListView.builder(
itemCount: vm.details.length,
itemBuilder: (context, index) {
final detail = vm.details[index];
return Card(
margin: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network("$baseurl${detail.imageDetail}"),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(detail.description),
),
],
),
);
},
);
},
),
);
}
}

View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:iconsax_flutter/iconsax_flutter.dart';
import 'package:rijig_mobile/core/utils/guide.dart';
import 'package:rijig_mobile/features/home/presentation/components/about_comp.dart';
import 'package:rijig_mobile/widget/card_withicon.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: whiteColor,
body: SafeArea(
child: SingleChildScrollView(
padding: PaddingCustom().paddingHorizontalVertical(16, 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Rijig",
style: Tulisan.heading(color: primaryColor),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Iconsax.notification),
Gap(10),
Icon(Iconsax.message_2),
],
),
],
),
),
],
),
Gap(20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CardWithIcon(
icon: Icons.account_circle,
text: 'Users',
number: '245',
onTap: () {},
),
CardWithIcon(
icon: Icons.shopping_cart,
text: 'Orders',
number: '178',
onTap: () {},
),
],
),
Gap(20),
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Important!",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
const Gap(15),
Container(
height:
250, // Tentukan tinggi yang sesuai untuk AboutComponent
child: AboutComponent(),
),
],
),
Gap(20),
Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Artikel",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:rijig_mobile/features/home/service/about_service.dart';
import 'package:rijig_mobile/features/home/model/about_model.dart';
class AboutViewModel extends ChangeNotifier {
final AboutService _aboutService;
AboutViewModel(this._aboutService);
bool isLoading = false;
String? errorMessage;
List<AboutModel>? aboutList;
Future<void> getAboutList() async {
isLoading = true;
errorMessage = null;
notifyListeners();
try {
aboutList = await _aboutService.getAboutList();
} catch (e) {
errorMessage = "Error: ${e.toString()}";
}
isLoading = false;
notifyListeners();
}
}
class AboutDetailViewModel extends ChangeNotifier {
final AboutService service;
AboutDetailViewModel(this.service);
bool isLoading = false;
String? errorMessage;
List<AboutDetailModel> details = [];
Future<void> getDetail(String aboutId) async {
isLoading = true;
notifyListeners();
try {
details = await service.getAboutDetail(aboutId);
} catch (e) {
errorMessage = "Failed to fetch: $e";
}
isLoading = false;
notifyListeners();
}
}

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:rijig_mobile/core/api/api_services.dart';
import 'package:rijig_mobile/features/home/model/about_model.dart';
class AboutRepository {
final Https _https = Https();
Future<List<AboutModel>> getAboutList() async {
final response = await _https.get('/about');
debugPrint("response about: $response");
final List data = response['data'] ?? [];
return data.map((e) => AboutModel.fromJson(e)).toList();
}
Future<List<AboutDetailModel>> getAboutDetail(String id) async {
final response = await _https.get('/about/$id');
debugPrint("response about detail: $response");
final List aboutDetail = response['data']['about_detail'] ?? [];
return aboutDetail.map((e) => AboutDetailModel.fromJson(e)).toList();
}
}

View File

@ -0,0 +1,24 @@
import 'package:rijig_mobile/features/home/repositories/about_repository.dart';
import 'package:rijig_mobile/features/home/model/about_model.dart';
class AboutService {
final AboutRepository _aboutRepository;
AboutService(this._aboutRepository);
Future<List<AboutModel>> getAboutList() async {
try {
return await _aboutRepository.getAboutList();
} catch (e) {
throw Exception('Failed to load About list: $e');
}
}
Future<List<AboutDetailModel>> getAboutDetail(String id) async {
try {
return await _aboutRepository.getAboutDetail(id);
} catch (e) {
throw Exception('Failed to load About Detail: $e');
}
}
}

View File

@ -14,7 +14,7 @@ class RequestPickScreen extends StatelessWidget {
// ignore: use_build_context_synchronously
Provider.of<TrashViewModel>(context, listen: false).loadCategories();
});
final String? _baseUrl = dotenv.env["BASE_URL"];
final String? baseUrl = dotenv.env["BASE_URL"];
return Scaffold(
appBar: CustomAppBar(judul: "Pilih sampah"),
@ -49,7 +49,7 @@ class RequestPickScreen extends StatelessWidget {
),
child: ListTile(
leading: Image.network(
"$_baseUrl${category.icon}",
"$baseUrl${category.icon}",
width: 50,
height: 50,
fit: BoxFit.cover,

View File

@ -31,6 +31,8 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => sl<LogoutViewModel>()),
ChangeNotifierProvider(create: (_) => sl<TrashViewModel>()),
ChangeNotifierProvider(create: (_) => sl<AboutViewModel>()),
ChangeNotifierProvider(create: (_) => sl<AboutDetailViewModel>()),
],
child: ScreenUtilInit(
designSize: const Size(375, 812),

View File

@ -25,6 +25,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
carousel_slider:
dependency: "direct main"
description:
name: carousel_slider
sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
characters:
dependency: transitive
description:
@ -150,6 +158,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_carousel_widget:
dependency: "direct main"
description:
name: flutter_carousel_widget
sha256: "6473e6df04bfafea70efd58251fe5945d5aa8d19461575c1b9d83643f08e0c77"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
flutter_dotenv:
dependency: "direct main"
description:
@ -573,6 +589,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
smooth_page_indicator:
dependency: "direct main"
description:
name: smooth_page_indicator
sha256: b21ebb8bc39cf72d11c7cfd809162a48c3800668ced1c9da3aade13a32cf6c1c
url: "https://pub.dev"
source: hosted
version: "1.2.1"
source_span:
dependency: transitive
description:

View File

@ -8,12 +8,14 @@ environment:
sdk: ^3.7.2
dependencies:
carousel_slider: ^5.0.0
concentric_transition: ^1.0.3
connectivity_plus: ^6.1.4
cupertino_icons: ^1.0.8
device_info_plus: ^11.4.0
flutter:
sdk: flutter
flutter_carousel_widget: ^3.1.0
flutter_dotenv: ^5.2.1
flutter_screenutil: ^5.9.3
flutter_secure_storage: ^9.2.4
@ -30,6 +32,7 @@ dependencies:
provider: ^6.1.4
shared_preferences: ^2.3.3
shimmer: ^3.0.0
smooth_page_indicator: ^1.2.1
uuid: ^4.5.1
dev_dependencies: