Skip to content

Commit b43da3b

Browse files
authored
[MBL-19461][Student] Add My Courses dashboard widget (#3429)
## Summary - Add new My Courses widget to Student app dashboard showing favorite courses and groups - Implement CourseCard and GroupCard composables matching Figma design specifications - Support grade display (percentage/letter), announcement counts, and synced indicators - Add collapsible sections for courses and groups with persistent expand/collapse state - Support responsive grid layout for tablet devices (1-3 columns based on screen width) - Include color overlay toggle support based on user preferences ## Test plan 1. Launch Student app and navigate to Dashboard 2. Verify the "Favorite Courses and Groups" widget appears 3. Verify courses display with correct name, color, grade (if enabled), and announcement count 4. Verify groups display with parent course name and member count 5. Tap "Courses" or "Groups" header to toggle section collapse/expand 6. Tap "All Courses" button to navigate to course list 7. Tap a course card to navigate to course details 8. Tap a group card to navigate to group details 9. Test on tablet to verify multi-column grid layout 10. Toggle Settings > Course color overlay and verify course cards update refs: MBL-19461 affects: Student release note: Added "My Courses" widget to the dashboard showing your favorite courses and groups with quick access to grades and announcements 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 9dc350f commit b43da3b

File tree

52 files changed

+4698
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4698
-20
lines changed

apps/parent/src/main/java/com/instructure/parentapp/di/DefaultBindingsModule.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import com.instructure.canvasapi2.utils.pageview.PandataInfo
2121
import com.instructure.pandautils.features.dashboard.edit.EditDashboardRepository
2222
import com.instructure.pandautils.features.dashboard.edit.EditDashboardRouter
2323
import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
24+
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetBehavior
25+
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter
2426
import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragmentBehavior
2527
import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository
2628
import com.instructure.pandautils.features.discussion.router.DiscussionRouter
@@ -128,4 +130,14 @@ class DefaultBindingsModule {
128130
fun provideSpeedGraderPostPolicyRouter(): SpeedGraderPostPolicyRouter {
129131
throw NotImplementedError()
130132
}
133+
134+
@Provides
135+
fun provideCoursesWidgetRouter(): CoursesWidgetRouter {
136+
throw NotImplementedError()
137+
}
138+
139+
@Provides
140+
fun provideCoursesWidgetBehavior(): CoursesWidgetBehavior {
141+
throw NotImplementedError()
142+
}
131143
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (C) 2025 - present Instructure, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, version 3 of the License.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.instructure.student.di.feature
18+
19+
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetBehavior
20+
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter
21+
import com.instructure.student.features.dashboard.widget.courses.StudentCoursesWidgetBehavior
22+
import com.instructure.student.features.dashboard.widget.courses.StudentCoursesWidgetRouter
23+
import dagger.Module
24+
import dagger.Provides
25+
import dagger.hilt.InstallIn
26+
import dagger.hilt.android.components.ViewModelComponent
27+
28+
@Module
29+
@InstallIn(ViewModelComponent::class)
30+
class CoursesWidgetModule {
31+
32+
@Provides
33+
fun provideCoursesWidgetRouter(): CoursesWidgetRouter {
34+
return StudentCoursesWidgetRouter()
35+
}
36+
37+
@Provides
38+
fun provideCoursesWidgetBehavior(
39+
studentCoursesWidgetBehavior: StudentCoursesWidgetBehavior
40+
): CoursesWidgetBehavior {
41+
return studentCoursesWidgetBehavior
42+
}
43+
}

apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardFragment.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import com.instructure.canvasapi2.models.CanvasContext
2525
import com.instructure.interactions.router.Route
2626
import com.instructure.pandautils.compose.CanvasTheme
2727
import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
28+
import com.instructure.pandautils.utils.ThemePrefs
29+
import com.instructure.pandautils.utils.ViewStyler
2830
import com.instructure.student.fragment.ParentFragment
2931
import dagger.hilt.android.AndroidEntryPoint
3032
import javax.inject.Inject
@@ -40,7 +42,6 @@ class DashboardFragment : ParentFragment() {
4042
container: ViewGroup?,
4143
savedInstanceState: Bundle?
4244
): View {
43-
applyTheme()
4445
return ComposeView(requireContext()).apply {
4546
setContent {
4647
CanvasTheme {
@@ -50,10 +51,16 @@ class DashboardFragment : ParentFragment() {
5051
}
5152
}
5253

54+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
55+
super.onViewCreated(view, savedInstanceState)
56+
applyTheme()
57+
}
58+
5359
override fun title(): String = ""
5460

5561
override fun applyTheme() {
5662
navigation?.attachNavigationDrawer(this, null)
63+
ViewStyler.setStatusBarDark(requireActivity(), ThemePrefs.primaryColor)
5764
}
5865

5966
companion object {

apps/student/src/main/java/com/instructure/student/features/dashboard/compose/DashboardScreen.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ import com.instructure.pandautils.compose.composables.Loading
5959
import com.instructure.pandautils.features.dashboard.notifications.DashboardRouter
6060
import com.instructure.pandautils.features.dashboard.widget.WidgetMetadata
6161
import com.instructure.pandautils.features.dashboard.widget.courseinvitation.CourseInvitationsWidget
62-
import com.instructure.pandautils.features.dashboard.widget.welcome.WelcomeWidget
62+
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidget
6363
import com.instructure.pandautils.features.dashboard.widget.institutionalannouncements.InstitutionalAnnouncementsWidget
64+
import com.instructure.pandautils.features.dashboard.widget.welcome.WelcomeWidget
6465
import com.instructure.student.R
6566
import com.instructure.student.activity.NavigationActivity
6667
import kotlinx.coroutines.flow.SharedFlow
@@ -110,7 +111,7 @@ fun DashboardScreenContent(
110111
}
111112

112113
Scaffold(
113-
modifier = Modifier.background(colorResource(R.color.backgroundLightest)),
114+
modifier = Modifier.background(colorResource(R.color.backgroundLight)),
114115
topBar = {
115116
CanvasThemedAppBar(
116117
title = stringResource(id = R.string.dashboard),
@@ -125,7 +126,7 @@ fun DashboardScreenContent(
125126
) { paddingValues ->
126127
Box(
127128
modifier = Modifier
128-
.background(colorResource(R.color.backgroundLightest))
129+
.background(colorResource(R.color.backgroundLight))
129130
.padding(paddingValues)
130131
.pullRefresh(pullRefreshState)
131132
.fillMaxSize()
@@ -230,6 +231,7 @@ private fun GetWidgetComposable(
230231
) {
231232
return when (widgetId) {
232233
WidgetMetadata.WIDGET_ID_WELCOME -> WelcomeWidget(refreshSignal = refreshSignal)
234+
WidgetMetadata.WIDGET_ID_COURSES -> CoursesWidget(refreshSignal = refreshSignal, columns = columns)
233235
WidgetMetadata.WIDGET_ID_COURSE_INVITATIONS -> CourseInvitationsWidget(
234236
refreshSignal = refreshSignal,
235237
columns = columns,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (C) 2025 - present Instructure, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, version 3 of the License.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.instructure.student.features.dashboard.widget.courses
18+
19+
import android.content.Context
20+
import android.content.SharedPreferences
21+
import com.instructure.pandautils.domain.usecase.BaseFlowUseCase
22+
import com.instructure.student.util.StudentPrefs
23+
import dagger.hilt.android.qualifiers.ApplicationContext
24+
import kotlinx.coroutines.channels.awaitClose
25+
import kotlinx.coroutines.flow.Flow
26+
import kotlinx.coroutines.flow.callbackFlow
27+
import javax.inject.Inject
28+
29+
class ObserveColorOverlayUseCase @Inject constructor(
30+
@ApplicationContext private val context: Context
31+
) : BaseFlowUseCase<Unit, Boolean>() {
32+
33+
override fun execute(params: Unit): Flow<Boolean> = callbackFlow {
34+
val sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
35+
36+
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
37+
if (key == KEY_HIDE_COURSE_COLOR_OVERLAY) {
38+
trySend(!StudentPrefs.hideCourseColorOverlay)
39+
}
40+
}
41+
42+
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
43+
send(!StudentPrefs.hideCourseColorOverlay)
44+
45+
awaitClose {
46+
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
47+
}
48+
}
49+
50+
companion object {
51+
private const val PREFS_NAME = "candroidSP"
52+
private const val KEY_HIDE_COURSE_COLOR_OVERLAY = "hideCourseColorOverlay"
53+
}
54+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (C) 2025 - present Instructure, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, version 3 of the License.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.instructure.student.features.dashboard.widget.courses
18+
19+
import android.content.Context
20+
import android.content.SharedPreferences
21+
import com.instructure.pandautils.domain.usecase.BaseFlowUseCase
22+
import com.instructure.student.util.StudentPrefs
23+
import dagger.hilt.android.qualifiers.ApplicationContext
24+
import kotlinx.coroutines.channels.awaitClose
25+
import kotlinx.coroutines.flow.Flow
26+
import kotlinx.coroutines.flow.callbackFlow
27+
import javax.inject.Inject
28+
29+
class ObserveGradeVisibilityUseCase @Inject constructor(
30+
@ApplicationContext private val context: Context
31+
) : BaseFlowUseCase<Unit, Boolean>() {
32+
33+
override fun execute(params: Unit): Flow<Boolean> = callbackFlow {
34+
val sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
35+
36+
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
37+
if (key == KEY_SHOW_GRADES_ON_CARD) {
38+
trySend(StudentPrefs.showGradesOnCard)
39+
}
40+
}
41+
42+
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
43+
send(StudentPrefs.showGradesOnCard)
44+
45+
awaitClose {
46+
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
47+
}
48+
}
49+
50+
companion object {
51+
private const val PREFS_NAME = "candroidSP"
52+
private const val KEY_SHOW_GRADES_ON_CARD = "showGradesOnCard"
53+
}
54+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (C) 2025 - present Instructure, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, version 3 of the License.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.instructure.student.features.dashboard.widget.courses
18+
19+
import androidx.fragment.app.FragmentActivity
20+
import com.instructure.canvasapi2.models.Course
21+
import com.instructure.canvasapi2.models.Group
22+
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetBehavior
23+
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter
24+
import kotlinx.coroutines.flow.Flow
25+
import javax.inject.Inject
26+
27+
class StudentCoursesWidgetBehavior @Inject constructor(
28+
private val observeGradeVisibilityUseCase: ObserveGradeVisibilityUseCase,
29+
private val observeColorOverlayUseCase: ObserveColorOverlayUseCase,
30+
private val router: CoursesWidgetRouter
31+
) : CoursesWidgetBehavior {
32+
33+
override fun observeGradeVisibility(): Flow<Boolean> {
34+
return observeGradeVisibilityUseCase(Unit)
35+
}
36+
37+
override fun observeColorOverlay(): Flow<Boolean> {
38+
return observeColorOverlayUseCase(Unit)
39+
}
40+
41+
override fun onCourseClick(activity: FragmentActivity, course: Course) {
42+
router.routeToCourse(activity, course)
43+
}
44+
45+
override fun onGroupClick(activity: FragmentActivity, group: Group) {
46+
router.routeToGroup(activity, group)
47+
}
48+
49+
override fun onManageOfflineContent(activity: FragmentActivity, course: Course) {
50+
router.routeToManageOfflineContent(activity, course)
51+
}
52+
53+
override fun onCustomizeCourse(activity: FragmentActivity, course: Course) {
54+
router.routeToCustomizeCourse(activity, course)
55+
}
56+
57+
override fun onAllCoursesClicked(activity: FragmentActivity) {
58+
router.routeToAllCourses(activity)
59+
}
60+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (C) 2025 - present Instructure, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, version 3 of the License.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.instructure.student.features.dashboard.widget.courses
18+
19+
import androidx.fragment.app.FragmentActivity
20+
import com.instructure.canvasapi2.models.Course
21+
import com.instructure.canvasapi2.models.Group
22+
import com.instructure.pandautils.features.dashboard.edit.EditDashboardFragment
23+
import com.instructure.pandautils.features.dashboard.widget.courses.CoursesWidgetRouter
24+
import com.instructure.student.features.coursebrowser.CourseBrowserFragment
25+
import com.instructure.student.router.RouteMatcher
26+
27+
class StudentCoursesWidgetRouter : CoursesWidgetRouter {
28+
29+
override fun routeToCourse(activity: FragmentActivity, course: Course) {
30+
RouteMatcher.route(activity, CourseBrowserFragment.makeRoute(course))
31+
}
32+
33+
override fun routeToGroup(activity: FragmentActivity, group: Group) {
34+
RouteMatcher.route(activity, CourseBrowserFragment.makeRoute(group))
35+
}
36+
37+
override fun routeToManageOfflineContent(activity: FragmentActivity, course: Course) {
38+
// TODO: Navigate to manage offline content screen
39+
}
40+
41+
override fun routeToCustomizeCourse(activity: FragmentActivity, course: Course) {
42+
// TODO: Navigate to customize course screen (color/nickname)
43+
}
44+
45+
override fun routeToAllCourses(activity: FragmentActivity) {
46+
RouteMatcher.route(activity, EditDashboardFragment.makeRoute())
47+
}
48+
}

0 commit comments

Comments
 (0)