Skip to content

Commit d1ed4bf

Browse files
feat: course home pages assignment card
1 parent 8625632 commit d1ed4bf

File tree

7 files changed

+338
-1
lines changed

7 files changed

+338
-1
lines changed

core/src/main/java/org/openedx/core/utils/TimeUtils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ object TimeUtils {
9797
}
9898
}
9999

100+
fun formatToDayMonth(date: Date): String {
101+
val sdf = SimpleDateFormat("MMM dd", Locale.getDefault())
102+
return sdf.format(date)
103+
}
104+
100105
fun getCurrentTime(): Long {
101106
return Calendar.getInstance().timeInMillis
102107
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package org.openedx.course.presentation.home
2+
3+
import androidx.compose.foundation.BorderStroke
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.height
10+
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.foundation.layout.size
12+
import androidx.compose.foundation.layout.width
13+
import androidx.compose.foundation.shape.CircleShape
14+
import androidx.compose.foundation.shape.RoundedCornerShape
15+
import androidx.compose.material.Card
16+
import androidx.compose.material.Icon
17+
import androidx.compose.material.LinearProgressIndicator
18+
import androidx.compose.material.MaterialTheme
19+
import androidx.compose.material.Text
20+
import androidx.compose.material.TextButton
21+
import androidx.compose.material.icons.Icons
22+
import androidx.compose.material.icons.automirrored.filled.Assignment
23+
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
24+
import androidx.compose.material.icons.automirrored.filled.List
25+
import androidx.compose.material.icons.filled.Timer
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.ui.Alignment
28+
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.draw.clip
30+
import androidx.compose.ui.res.stringResource
31+
import androidx.compose.ui.text.font.FontWeight
32+
import androidx.compose.ui.unit.dp
33+
import org.openedx.core.domain.model.Block
34+
import org.openedx.core.ui.theme.appColors
35+
import org.openedx.core.ui.theme.appTypography
36+
import org.openedx.core.utils.TimeUtils
37+
import org.openedx.course.R
38+
import java.util.Date
39+
import org.openedx.core.R as coreR
40+
41+
@Composable
42+
fun AssignmentsHomePagerCardContent(
43+
uiState: CourseHomeUIState.CourseData,
44+
onAssignmentClick: (Block) -> Unit,
45+
onViewAllAssignmentsClick: () -> Unit
46+
) {
47+
val completedAssignments = uiState.courseAssignments.count { it.isCompleted() }
48+
val totalAssignments = uiState.courseAssignments.size
49+
val firstIncompleteAssignment = uiState.courseAssignments.find { !it.isCompleted() }
50+
51+
Column(
52+
modifier = Modifier
53+
.fillMaxWidth()
54+
.padding(16.dp)
55+
) {
56+
// Header with progress
57+
Text(
58+
text = stringResource(R.string.course_container_content_tab_assignment),
59+
style = MaterialTheme.appTypography.titleLarge,
60+
color = MaterialTheme.appColors.textPrimary,
61+
fontWeight = FontWeight.SemiBold
62+
)
63+
Spacer(modifier = Modifier.height(12.dp))
64+
65+
// Progress section
66+
Row(
67+
modifier = Modifier.fillMaxWidth(),
68+
verticalAlignment = Alignment.CenterVertically
69+
) {
70+
Icon(
71+
imageVector = Icons.AutoMirrored.Filled.Assignment,
72+
contentDescription = null,
73+
tint = MaterialTheme.appColors.textPrimary,
74+
modifier = Modifier.size(32.dp)
75+
)
76+
Spacer(modifier = Modifier.width(8.dp))
77+
Text(
78+
text = "$completedAssignments/$totalAssignments",
79+
style = MaterialTheme.appTypography.displaySmall,
80+
color = MaterialTheme.appColors.textPrimary,
81+
fontWeight = FontWeight.Bold
82+
)
83+
Spacer(modifier = Modifier.width(8.dp))
84+
Text(
85+
text = stringResource(R.string.course_assignments_completed),
86+
style = MaterialTheme.appTypography.labelLarge,
87+
color = MaterialTheme.appColors.textPrimaryVariant,
88+
fontWeight = FontWeight.Medium
89+
)
90+
}
91+
92+
Spacer(modifier = Modifier.height(8.dp))
93+
94+
// Progress bar
95+
LinearProgressIndicator(
96+
modifier = Modifier
97+
.fillMaxWidth()
98+
.height(4.dp)
99+
.clip(CircleShape),
100+
progress = if (totalAssignments > 0) completedAssignments.toFloat() / totalAssignments else 0f,
101+
color = MaterialTheme.appColors.progressBarColor,
102+
backgroundColor = MaterialTheme.appColors.progressBarBackgroundColor
103+
)
104+
105+
Spacer(modifier = Modifier.height(20.dp))
106+
107+
// First Incomplete Assignment section
108+
if (firstIncompleteAssignment != null) {
109+
AssignmentCard(
110+
assignment = firstIncompleteAssignment,
111+
onAssignmentClick = onAssignmentClick
112+
)
113+
}
114+
115+
Spacer(modifier = Modifier.height(8.dp))
116+
117+
// View All Assignments button
118+
TextButton(
119+
onClick = onViewAllAssignmentsClick,
120+
modifier = Modifier.fillMaxWidth()
121+
) {
122+
Icon(
123+
imageVector = Icons.AutoMirrored.Filled.List,
124+
contentDescription = null,
125+
tint = MaterialTheme.appColors.primary,
126+
modifier = Modifier.size(20.dp)
127+
)
128+
Spacer(modifier = Modifier.width(8.dp))
129+
Text(
130+
text = stringResource(R.string.course_view_all_assignments),
131+
style = MaterialTheme.appTypography.labelLarge,
132+
color = MaterialTheme.appColors.primary
133+
)
134+
}
135+
}
136+
}
137+
138+
@Composable
139+
private fun AssignmentCard(
140+
assignment: Block,
141+
onAssignmentClick: (Block) -> Unit
142+
) {
143+
val isDuePast = assignment.due != null && assignment.due!! < Date()
144+
145+
// Header text - "Past Due" or "Due Soon"
146+
val headerText = if (isDuePast) {
147+
stringResource(coreR.string.core_date_type_past_due)
148+
} else {
149+
stringResource(R.string.course_due_soon)
150+
}
151+
152+
// Due date status text
153+
val dueDateStatusText = assignment.due?.let { due ->
154+
val formattedDate = TimeUtils.formatToDayMonth(due)
155+
val daysDifference = ((due.time - Date().time) / (1000 * 60 * 60 * 24)).toInt()
156+
when {
157+
daysDifference < 0 -> {
158+
// Past due
159+
val daysPastDue = -daysDifference
160+
stringResource(
161+
R.string.course_days_past_due,
162+
daysPastDue,
163+
formattedDate
164+
)
165+
}
166+
167+
daysDifference == 0 -> {
168+
// Due today
169+
stringResource(
170+
R.string.course_due_today,
171+
formattedDate
172+
)
173+
}
174+
175+
else -> {
176+
// Due in the future
177+
stringResource(
178+
R.string.course_due_in_days,
179+
daysDifference,
180+
formattedDate
181+
)
182+
}
183+
}
184+
} ?: ""
185+
186+
Card(
187+
modifier = Modifier
188+
.fillMaxWidth()
189+
.clickable { onAssignmentClick(assignment) },
190+
backgroundColor = MaterialTheme.appColors.surface,
191+
border = BorderStroke(1.dp, MaterialTheme.appColors.cardViewBorder),
192+
shape = RoundedCornerShape(8.dp),
193+
elevation = 0.dp
194+
) {
195+
Column(
196+
modifier = Modifier
197+
.fillMaxWidth()
198+
.padding(16.dp)
199+
) {
200+
// Header section with icon and status
201+
if (assignment.due != null) {
202+
Row(
203+
verticalAlignment = Alignment.CenterVertically
204+
) {
205+
Icon(
206+
imageVector = Icons.Filled.Timer,
207+
contentDescription = null,
208+
modifier = Modifier.size(24.dp),
209+
tint = MaterialTheme.appColors.warning
210+
)
211+
Spacer(modifier = Modifier.width(8.dp))
212+
Text(
213+
text = headerText,
214+
style = MaterialTheme.appTypography.titleMedium,
215+
color = MaterialTheme.appColors.textPrimary,
216+
fontWeight = FontWeight.Bold
217+
)
218+
}
219+
Spacer(modifier = Modifier.height(8.dp))
220+
}
221+
222+
Row(
223+
modifier = Modifier.fillMaxWidth(),
224+
verticalAlignment = Alignment.CenterVertically
225+
) {
226+
Column(
227+
modifier = Modifier.weight(1f)
228+
) {
229+
// Due date status text
230+
if (dueDateStatusText.isNotEmpty()) {
231+
Text(
232+
text = dueDateStatusText,
233+
style = MaterialTheme.appTypography.labelSmall,
234+
color = MaterialTheme.appColors.primary
235+
)
236+
Spacer(modifier = Modifier.height(4.dp))
237+
}
238+
239+
240+
// Assignment and section name
241+
Text(
242+
text = assignment.assignmentProgress?.assignmentType ?: "",
243+
style = MaterialTheme.appTypography.titleSmall,
244+
color = MaterialTheme.appColors.textPrimary,
245+
fontWeight = FontWeight.Bold
246+
)
247+
Spacer(modifier = Modifier.height(4.dp))
248+
Text(
249+
text = assignment.displayName,
250+
style = MaterialTheme.appTypography.labelSmall,
251+
color = MaterialTheme.appColors.textPrimaryVariant,
252+
)
253+
}
254+
255+
// Chevron arrow
256+
Icon(
257+
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
258+
contentDescription = null,
259+
modifier = Modifier.size(24.dp),
260+
tint = MaterialTheme.appColors.textDark
261+
)
262+
}
263+
}
264+
}
265+
}

course/src/main/java/org/openedx/course/presentation/home/CourseCompletionHomePagerCardContent.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ private fun CourseCompletionHomePagerCardContentPreview() {
165165
),
166166
useRelativeDates = true,
167167
courseVideos = mapOf(),
168+
courseAssignments = emptyList(),
168169
videoPreview = null,
169170
videoProgress = 0f
170171
),

course/src/main/java/org/openedx/course/presentation/home/CourseHomeScreen.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ fun CourseHomeScreen(
149149
)
150150
viewModel.logVideoClick(videoBlock.id)
151151
},
152+
onAssignmentClick = { assignmentBlock ->
153+
viewModel.courseRouter.navigateToCourseContainer(
154+
fragmentManager,
155+
courseId = viewModel.courseId,
156+
unitId = viewModel.getBlockParent(assignmentBlock.id)?.id ?: return@CourseHomeUI,
157+
mode = CourseViewMode.FULL
158+
)
159+
viewModel.logAssignmentClick(assignmentBlock.id)
160+
},
152161
onNavigateToContent = onNavigateToContent
153162
)
154163
}
@@ -165,6 +174,7 @@ private fun CourseHomeUI(
165174
onResetDatesClick: () -> Unit,
166175
onCertificateClick: (String) -> Unit,
167176
onVideoClick: (Block) -> Unit,
177+
onAssignmentClick: (Block) -> Unit,
168178
onNavigateToContent: (CourseContentTab) -> Unit,
169179
) {
170180
val scaffoldState = rememberScaffoldState()
@@ -296,6 +306,16 @@ private fun CourseHomeUI(
296306
)
297307
}
298308

309+
CourseHomePagerTab.ASSIGNMENT -> {
310+
AssignmentsHomePagerCardContent(
311+
uiState = uiState,
312+
onAssignmentClick = onAssignmentClick,
313+
onViewAllAssignmentsClick = {
314+
onNavigateToContent(CourseContentTab.ASSIGNMENTS)
315+
}
316+
)
317+
}
318+
299319
else -> {
300320
Text(tab.name)
301321
}
@@ -367,6 +387,7 @@ private fun CourseHomeScreenPreview() {
367387
),
368388
useRelativeDates = true,
369389
courseVideos = mapOf(),
390+
courseAssignments = emptyList(),
370391
videoPreview = null,
371392
videoProgress = 0f
372393
),
@@ -378,6 +399,7 @@ private fun CourseHomeScreenPreview() {
378399
onResetDatesClick = {},
379400
onCertificateClick = {},
380401
onVideoClick = {},
402+
onAssignmentClick = {},
381403
onNavigateToContent = { _ -> },
382404
)
383405
}
@@ -412,6 +434,7 @@ private fun CourseHomeScreenTabletPreview() {
412434
),
413435
useRelativeDates = true,
414436
courseVideos = mapOf(),
437+
courseAssignments = emptyList(),
415438
videoPreview = null,
416439
videoProgress = 0f
417440
),
@@ -423,6 +446,7 @@ private fun CourseHomeScreenTabletPreview() {
423446
onResetDatesClick = {},
424447
onCertificateClick = {},
425448
onVideoClick = {},
449+
onAssignmentClick = {},
426450
onNavigateToContent = { _ -> },
427451
)
428452
}

course/src/main/java/org/openedx/course/presentation/home/CourseHomeUIState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ sealed class CourseHomeUIState {
2020
val datesBannerInfo: CourseDatesBannerInfo,
2121
val useRelativeDates: Boolean,
2222
val courseVideos: Map<String, List<Block>>,
23+
val courseAssignments: List<Block>,
2324
val videoPreview: VideoPreview?,
2425
val videoProgress: Float,
2526
) : CourseHomeUIState()

0 commit comments

Comments
 (0)