onufitness_mobile/lib/screens/rise/views/detailed_rise_screen.dart
2026-01-13 11:36:24 +05:30

715 lines
33 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:onufitness/constants/asset_constants.dart';
import 'package:onufitness/constants/color_constant.dart';
import 'package:onufitness/constants/constant.dart';
import 'package:onufitness/constants/text_constant.dart';
import 'package:onufitness/screens/rise/controllers/rise_controller.dart';
import 'package:onufitness/screens/rise/models/get_challenges_response_model.dart';
import 'package:onufitness/screens/rise/widgets/input_challenge_performance.dart';
import 'package:onufitness/utils/helper_function.dart';
import 'package:onufitness/widgets/Buttons/custom_social_login_button.dart';
import 'package:onufitness/widgets/appbars/custom_appbar.dart';
import 'package:onufitness/widgets/Buttons/custom_submit_button.dart';
// Create a model class for better data structure
class TaskDetail {
final String label;
final String value;
TaskDetail({required this.label, required this.value});
}
class ChallengeDetailsScreen extends StatefulWidget {
final int index;
final ChallengeItem challengeItem;
RiseController riseController;
ChallengeDetailsScreen({
super.key,
required this.index,
required this.challengeItem,
required this.riseController,
});
@override
ActivityDetailsScreenState createState() => ActivityDetailsScreenState();
}
class ActivityDetailsScreenState extends State<ChallengeDetailsScreen> {
bool isExpanded = false;
RxBool isJoinedSuccessful = false.obs;
String formatDate(String? dateString) {
if (dateString == null || dateString.isEmpty) {
return 'N/A';
}
try {
DateTime dateTime = DateTime.parse(dateString);
return DateFormat('MM-dd-yyyy').format(dateTime);
} catch (e) {
try {
DateFormat inputFormat = DateFormat('yyyy-MM-dd');
DateTime dateTime = inputFormat.parse(dateString);
return DateFormat('MM-dd-yyyy').format(dateTime);
} catch (e) {
return dateString;
}
}
}
List<TaskDetail> get taskDetails {
final challenge = widget.challengeItem;
final firstTask =
challenge.tasks?.isNotEmpty == true ? challenge.tasks![0] : null;
return [
TaskDetail(label: 'Task Name', value: firstTask?.taskTitle ?? 'N/A'),
TaskDetail(
label: 'Challenge Category',
value: challenge.fitnessGoalTitle ?? 'N/A',
),
TaskDetail(
label: 'Target',
value:
"${firstTask?.targetValue?.toString()} ${firstTask?.taskUnit?.toString()}",
),
TaskDetail(
label: 'Start Date',
value: formatDate(challenge.startDate?.toString()),
),
TaskDetail(
label: 'End Date',
value: formatDate(challenge.endDate?.toString()),
),
];
}
late ScrollController _participantsScrollController;
@override
void initState() {
super.initState();
isJoinedSuccessful.value == false;
_participantsScrollController = ScrollController();
_participantsScrollController.addListener(_onParticipantsScroll);
// Load participants when screen initializes
if (widget.challengeItem.challengeId != null) {
widget.riseController.fetchChallengeParticipants(
challengeId: widget.challengeItem.challengeId!,
isRefresh: true,
);
}
}
@override
void dispose() {
_participantsScrollController.removeListener(_onParticipantsScroll);
_participantsScrollController.dispose();
super.dispose();
}
void _onParticipantsScroll() {
if (_participantsScrollController.position.pixels >=
_participantsScrollController.position.maxScrollExtent - 200) {
widget.riseController.loadMoreChallengeParticipants();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
backgroundColor: Colors.white,
title: widget.challengeItem.challengeTitle ?? ".",
textColor: appbarTextColor,
titleFontSize: appBarHeardingText,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Get.back();
},
),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(15.w),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
child:
widget.challengeItem.challengeImageName != null &&
widget.challengeItem.challengeImageName!.isNotEmpty
? Image.network(
widget.challengeItem.challengeImageName!,
width: double.infinity,
height: 150.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return defaultBannerImage(
context: context,
title: widget.challengeItem.challengeTitle ?? ".",
);
},
)
: defaultBannerImage(
context: context,
title: widget.challengeItem.challengeTitle ?? ".",
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: containerBorderColor),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20.r),
bottomRight: Radius.circular(20.r),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
flex: 7,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.challengeItem.challengeTitle ?? "",
style: TextStyle(
fontSize: smallSizeText,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 4.h),
Text(
"${widget.challengeItem.fitnessGoalTitle ?? ""}${widget.challengeItem.totalParticipants ?? ""} Participants",
style: TextStyle(
fontSize: verySmallSizeText,
color: greyTextColor1,
),
),
SizedBox(height: 12.h),
],
),
),
Divider(color: containerBorderColor),
(widget.challengeItem.isChallengeJoined == false &&
isJoinedSuccessful.value == false)
? Expanded(
flex: 3,
child: Obx(
() => CustomSubmitButton(
height: 35.h,
backgroundColor: Color(primaryColor),
text: "Join",
textColor: Colors.black,
fontWeight: FontWeight.w600,
onPressed: () async {
bool joinResult = await widget
.riseController
.joinChallenge(
challengeID:
widget
.challengeItem
.challengeId!,
);
if (joinResult) {
// Update both states immediately
setState(() {
widget
.challengeItem
.isChallengeJoined = true;
});
isJoinedSuccessful.value = true;
// Refresh the challenges list
widget.riseController
.fetchOngoingChallenges(
isRefresh: true,
);
}
},
isLoading: widget
.riseController
.isJoiningChallengeLoading
.contains(
widget.challengeItem.challengeId,
),
),
),
)
: CustomSocialLoginButton(
width: isTablet ? 120.w : 100.w,
height: 35.h,
text: "Joined",
fontSize: smallSizeText,
onPressed: null,
),
],
),
Divider(color: containerBorderColor),
Text(
'About Challenge',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
Text(
widget.challengeItem.challengeDescription ?? "",
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
),
SizedBox(height: 16.h),
Divider(color: containerBorderColor),
SizedBox(height: 16.h),
Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:
taskDetails.map((detail) {
return Padding(
padding: EdgeInsets.symmetric(
vertical: 5.w,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
detail.label,
style: customTextStyle.copyWith(
fontSize: smallSizeText,
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
detail.value,
style: customTextStyle.copyWith(
fontSize: verySmallSizeText,
fontWeight: FontWeight.w400,
color: greyTextColor1,
),
),
),
],
),
);
}).toList(),
),
SizedBox(height: 30.h),
if ((widget.challengeItem.isChallengeJoined == true ||
isJoinedSuccessful.value == true) &&
widget.challengeItem.isChallengeStarted == true &&
widget.challengeItem.isChallengeEnded == false)
CustomSubmitButton(
backgroundColor: Colors.black,
text: "Give Your Input Here",
onPressed: () {
showModalBottomSheet(
context: context,
// backgroundColor: Colors.transparent,
isScrollControlled: true,
builder:
(context) => SafeArea(
child: Padding(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(
context,
).viewInsets.bottom,
),
child:
ChallengePerformanceInputBottomSheet(
challengeTask:
widget.challengeItem.tasks!,
),
),
),
);
},
),
],
),
SizedBox(height: 16.h),
],
),
),
SizedBox(height: 20.h),
InkWell(
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: containerBorderColor),
borderRadius: BorderRadius.circular(20.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Challenge Rankings',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
),
],
),
if (isExpanded) ...[
SizedBox(height: 20.h),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"Participants",
style: TextStyle(
color: greyTextColor1,
fontSize: smallSizeText,
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 20.h),
// Real participants leaderboard list with scroll pagination
Obx(() {
if (widget
.riseController
.isParticipantsLoading
.value &&
widget
.riseController
.challengeParticipants
.isEmpty) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 20.h),
child: CircularProgressIndicator(),
),
);
}
if (widget
.riseController
.challengeParticipants
.isEmpty) {
return Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 20.h),
child: Text(
'No participants found',
style: TextStyle(
color: greyTextColor1,
fontSize: mediumSizeText,
),
),
),
);
}
// Sort participants by leaderboard score in descending order
var sortedParticipants =
widget.riseController.challengeParticipants
.toList()
..sort(
(a, b) => (b.leaderboardScore ?? 0)
.compareTo(a.leaderboardScore ?? 0),
);
return SizedBox(
height: 300.h,
child: ListView.builder(
controller: _participantsScrollController,
itemCount:
sortedParticipants.length +
(widget
.riseController
.participantsHasMoreData
.value
? 1
: 0),
itemBuilder: (context, index) {
// Show loading indicator at the end if more data available
if (index == sortedParticipants.length) {
return widget
.riseController
.isParticipantsLoading
.value
? Padding(
padding: EdgeInsets.symmetric(
vertical: 10.h,
),
child: Center(
child: CircularProgressIndicator(),
),
)
: SizedBox.shrink();
}
final participant = sortedParticipants[index];
return Padding(
padding: EdgeInsets.symmetric(
vertical: 12.h,
),
child: Row(
children: [
Stack(
alignment: Alignment.center,
children: [
CircleAvatar(
radius: 24.r,
backgroundColor: lightGreyColor,
),
ClipOval(
child: Image.network(
participant.profilePicture !=
null &&
participant
.profilePicture!
.isNotEmpty
? participant
.profilePicture!
: '', // empty string will trigger errorBuilder
width: 48.r,
height: 48.r,
fit: BoxFit.cover,
loadingBuilder: (
context,
child,
loadingProgress,
) {
if (loadingProgress == null) {
return child;
}
return Container(
width: 25.r,
height: 25.r,
alignment: Alignment.center,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<
Color
>(Colors.grey),
),
);
},
errorBuilder: (
context,
error,
stackTrace,
) {
return Image.asset(
AssetConstants
.dummyUserImage,
width: 48.r,
height: 48.r,
fit: BoxFit.cover,
);
},
),
),
],
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'${participant.fullName ?? {participant.firstName}} (${participant.leaderboardScore ?? 0} Point)',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: smallSizeText,
color: Colors.black,
),
),
SizedBox(height: 8.h),
Container(
height: 4.h,
width: double.infinity,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(
10.r,
),
color: Colors.grey.shade300,
),
child: Stack(
children: [
FractionallySizedBox(
alignment:
Alignment.centerLeft,
widthFactor:
(participant
.challengeProgressPercentage ??
0) /
100,
child: Container(
height: 5.h,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(
10.r,
),
gradient: LinearGradient(
colors: [
Color(
primaryColor,
),
Color(
darkGreyColor,
),
],
),
),
),
),
],
),
),
],
),
),
SizedBox(width: 16.w),
Column(
children: [
if (index < 3) ...[
if (index == 0)
Image.asset(
AssetConstants.challengeRank1,
height:
isTablet ? 30.h : 25.h,
width: isTablet ? 30.w : 25.w,
),
if (index == 1)
Image.asset(
AssetConstants.challengeRank2,
height:
isTablet ? 30.h : 25.h,
width: isTablet ? 30.w : 25.w,
),
if (index == 2)
Image.asset(
AssetConstants.challengeRank3,
height:
isTablet ? 30.h : 25.h,
width: isTablet ? 30.w : 25.w,
),
SizedBox(height: 5.h),
] else
SizedBox(
height: 30.h,
), // Space to align with medal positions
Text(
"${participant.challengeProgressPercentage ?? 0}",
style: TextStyle(
fontSize: regularSizeText,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
),
],
),
);
},
),
);
}),
],
],
),
),
),
],
),
Container(
padding: EdgeInsetsDirectional.only(
bottom: MediaQuery.of(context).viewPadding.bottom,
),
color: Colors.white,
),
],
),
),
);
}
}
Widget defaultBannerImage({
required BuildContext context,
required String title,
}) {
return Container(
width: double.infinity,
height: 150.h,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.white, Color(primaryColor).withValues(alpha: 0.5)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
child: Center(
child: Text(
title.isNotEmpty ? title[0].toUpperCase() : '',
style: TextStyle(
fontSize: 60.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
shadows: [
Shadow(blurRadius: 4, color: Colors.black26, offset: Offset(2, 2)),
],
),
),
),
);
}