Skip to content

Commit 185d080

Browse files
[Horizon ]Add learn tab (#3308)
1 parent cfada7d commit 185d080

File tree

13 files changed

+300
-53
lines changed

13 files changed

+300
-53
lines changed

Core/Core/Features/Modules/APIModule.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ public struct GetModuleItemRequest: APIRequestable {
414414
public typealias Response = APIModuleItem
415415

416416
public enum Include: String {
417-
case content_details
417+
case content_details, estimated_durations
418418
}
419419

420420
public let courseID: String

Core/Core/Features/Modules/GetModuleItem.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,16 @@ public class GetModuleItem: APIUseCase {
3030
public let scope: Scope
3131
public let cacheKey: String?
3232

33-
public init(courseID: String, moduleID: String, itemID: String) {
33+
public init(
34+
courseID: String,
35+
moduleID: String,
36+
itemID: String,
37+
include: [GetModuleItemRequest.Include] = [.content_details]
38+
) {
3439
self.courseID = courseID
3540
self.moduleID = moduleID
3641
self.itemID = itemID
37-
request = GetModuleItemRequest(courseID: courseID, moduleID: moduleID, itemID: itemID, include: [.content_details])
42+
request = GetModuleItemRequest(courseID: courseID, moduleID: moduleID, itemID: itemID, include: include)
3843
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
3944
NSPredicate(key: #keyPath(ModuleItem.courseID), equals: courseID),
4045
NSPredicate(key: #keyPath(ModuleItem.id), equals: itemID)

Horizon/Horizon/Resources/Localizable.xcstrings

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@
245245
},
246246
"Confusing" : {
247247

248+
},
249+
"Congrats! You've completed your course." : {
250+
248251
},
249252
"Copenhagen (+01:00/+02:00)" : {
250253

@@ -905,6 +908,9 @@
905908
},
906909
"View Submission" : {
907910

911+
},
912+
"View your progress and scores on the Learn page." : {
913+
908914
},
909915
"Vilnius (+02:00/+03:00)" : {
910916

@@ -944,6 +950,9 @@
944950
},
945951
"You are submitting an uploaded file. Any content in the text field will be deleted upon submission. Once you submit this attempt, you won’t be able to make any changes." : {
946952

953+
},
954+
"You aren’t currently enrolled in a course." : {
955+
947956
},
948957
"Your Courses" : {
949958

Horizon/Horizon/Sources/Common/Domain/GetCoursesInteractor.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ final class GetCoursesInteractorLive: GetCoursesInteractor {
108108
.collect()
109109
}
110110
}
111+
.map { courses in
112+
courses.sorted {
113+
($0.learningObjectCardViewModel != nil) && ($1.learningObjectCardViewModel == nil)
114+
}
115+
}
111116
.eraseToAnyPublisher()
112117
}
113118

@@ -135,22 +140,30 @@ private extension CDDashboardCourse {
135140
let progress = completionPercentage / 100.0
136141

137142
guard let nextModuleID, let nextModuleItemID else {
138-
return Just(nil)
139-
.eraseToAnyPublisher()
143+
return Just(
144+
DashboardCourse(
145+
name: name,
146+
progress: progress,
147+
courseId: courseID,
148+
learningObjectCardViewModel: nil
149+
)
150+
)
151+
.eraseToAnyPublisher()
140152
}
141153

142154
return ReactiveStore(
143155
useCase: GetModuleItem(
144156
courseID: courseID,
145157
moduleID: nextModuleID,
146-
itemID: nextModuleItemID
158+
itemID: nextModuleItemID,
159+
include: [.content_details, .estimated_durations]
147160
)
148161
)
149162
.getEntities(ignoreCache: true)
150163
.replaceError(with: [])
151164
.compactMap { $0.first }
152165
.map { HModuleItem(from: $0) }
153-
.map { item in
166+
.map { [courseID] item in
154167
let moduleItem = LearningObjectCard(
155168
moduleTitle: item.moduleName ?? "",
156169
learningObjectName: item.title,
@@ -163,6 +176,7 @@ private extension CDDashboardCourse {
163176
return DashboardCourse(
164177
name: name,
165178
progress: progress,
179+
courseId: courseID,
166180
learningObjectCardViewModel: moduleItem
167181
)
168182
}

Horizon/Horizon/Sources/Common/Domain/GetCoursesInteractorPreview.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class GetCoursesInteractorPreview: GetCoursesInteractor {
2727
[.init(
2828
name: "AI Introductions",
2929
progress: 0.2,
30+
courseId: "11",
3031
learningObjectCardViewModel: nil
3132
)
3233
]

Horizon/Horizon/Sources/Features/Dashboard/Data/DashboardCourse.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Foundation
2121
struct DashboardCourse: Identifiable {
2222
let name: String
2323
let progress: Double
24+
let courseId: String
2425
let learningObjectCardViewModel: LearningObjectCard?
2526
var id: String { name }
2627
}

Horizon/Horizon/Sources/Features/Dashboard/View/DashboardView.swift

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -41,54 +41,96 @@ struct DashboardView: View {
4141
refreshAction: viewModel.reload
4242
) { _ in
4343
LazyVStack(spacing: .zero) {
44-
ForEach(viewModel.nextUpViewModels) { nextUpViewModel in
45-
VStack(alignment: .leading, spacing: .zero) {
46-
Text(nextUpViewModel.name)
47-
.huiTypography(.h1)
48-
.foregroundStyle(Color.huiColors.text.title)
49-
.padding(.top, .huiSpaces.space48)
50-
.padding(.bottom, .huiSpaces.space16)
44+
if viewModel.courses.isEmpty, viewModel.state == .data {
45+
Text("You aren’t currently enrolled in a course.", bundle: .horizon)
46+
.padding(.huiSpaces.space24)
47+
.frame(maxWidth: .infinity, alignment: .leading)
48+
.foregroundStyle(Color.huiColors.text.body)
49+
.huiTypography(.h3)
5150

52-
HorizonUI.ProgressBar(
53-
progress: nextUpViewModel.progress,
54-
size: .medium,
55-
numberPosition: .outside
56-
)
57-
58-
if let learningObjectCardViewModel = nextUpViewModel.learningObjectCardViewModel {
59-
Text("Resume Learning", bundle: .horizon)
60-
.huiTypography(.h3)
61-
.foregroundStyle(Color.huiColors.text.title)
62-
.padding(.top, .huiSpaces.space36)
63-
.padding(.bottom, .huiSpaces.space12)
64-
.frame(maxWidth: .infinity, alignment: .leading)
65-
Button {
66-
if let url = learningObjectCardViewModel.url {
67-
viewModel.navigateToCourseDetails(url: url, viewController: viewController)
68-
}
69-
} label: {
70-
HorizonUI.LearningObjectCard(
71-
status: viewModel.getStatus(percent: nextUpViewModel.progress),
72-
moduleTitle: learningObjectCardViewModel.moduleTitle,
73-
learningObjectName: learningObjectCardViewModel.learningObjectName,
74-
duration: learningObjectCardViewModel.estimatedTime,
75-
type: learningObjectCardViewModel.type,
76-
dueDate: learningObjectCardViewModel.dueDate
77-
)
78-
}
79-
}
80-
}
81-
.padding(.horizontal, .huiSpaces.space24)
51+
} else {
52+
contentView(courses: viewModel.courses)
53+
.padding(.bottom, .huiSpaces.space16)
8254
}
8355
}
84-
.padding(.bottom, .huiSpaces.space16)
8556
}
8657
.toolbar(.hidden)
8758
.safeAreaInset(edge: .top, spacing: .zero) { navigationBar }
8859
.scrollIndicators(.hidden, axes: .vertical)
8960
.background(Color.huiColors.surface.pagePrimary)
9061
}
9162

63+
private func contentView(courses: [DashboardCourse]) -> some View {
64+
ForEach(courses) { course in
65+
VStack(alignment: .leading, spacing: .zero) {
66+
courseProgressionView(course: course)
67+
.contentShape(Rectangle())
68+
.onTapGesture {
69+
viewModel.navigateToCourseDetails(
70+
id: course.courseId,
71+
viewController: viewController
72+
)
73+
}
74+
75+
if let learningObjectCardViewModel = course.learningObjectCardViewModel {
76+
learningObjectCard(model: learningObjectCardViewModel, progress: course.progress)
77+
} else {
78+
Text("Congrats! You've completed your course.", bundle: .horizon)
79+
.huiTypography(.h3)
80+
.foregroundStyle(Color.huiColors.text.title)
81+
.padding(.top, .huiSpaces.space32)
82+
Text("View your progress and scores on the Learn page.", bundle: .horizon)
83+
.huiTypography(.p1)
84+
.foregroundStyle(Color.huiColors.text.title)
85+
.padding(.top, .huiSpaces.space12)
86+
}
87+
}
88+
.padding(.horizontal, .huiSpaces.space24)
89+
}
90+
}
91+
92+
private func courseProgressionView(course: DashboardCourse) -> some View {
93+
Group {
94+
Text(course.name)
95+
.huiTypography(.h1)
96+
.foregroundStyle(Color.huiColors.text.title)
97+
.padding(.top, .huiSpaces.space48)
98+
.padding(.bottom, .huiSpaces.space16)
99+
100+
HorizonUI.ProgressBar(
101+
progress: course.progress,
102+
size: .medium,
103+
numberPosition: .outside
104+
)
105+
}
106+
}
107+
108+
private func learningObjectCard(model: LearningObjectCard, progress: Double) -> some View {
109+
VStack(alignment: .leading, spacing: .zero) {
110+
Text("Resume Learning", bundle: .horizon)
111+
.huiTypography(.h3)
112+
.foregroundStyle(Color.huiColors.text.title)
113+
.padding(.top, .huiSpaces.space36)
114+
.padding(.bottom, .huiSpaces.space12)
115+
.frame(maxWidth: .infinity, alignment: .leading)
116+
117+
Button {
118+
if let url = model.url {
119+
viewModel.navigateToItemSequence(url: url, viewController: viewController)
120+
}
121+
} label: {
122+
HorizonUI.LearningObjectCard(
123+
status: viewModel.getStatus(percent: progress),
124+
moduleTitle: model.moduleTitle,
125+
learningObjectName: model.learningObjectName,
126+
duration: model.estimatedTime,
127+
type: model.type,
128+
dueDate: model.dueDate
129+
)
130+
}
131+
}
132+
}
133+
92134
private var navigationBar: some View {
93135
HStack(spacing: .zero) {
94136
HorizonUI.NavigationBar.Leading(logoURL: logoURL)
@@ -101,7 +143,7 @@ struct DashboardView: View {
101143
viewModel.mailDidTap(viewController: viewController)
102144
}
103145
}
104-
.padding(.horizontal, .huiSpaces.space10)
146+
.padding(.horizontal, .huiSpaces.space24)
105147
.padding(.top, .huiSpaces.space10)
106148
.padding(.bottom, .huiSpaces.space4)
107149
.background(Color.huiColors.surface.pagePrimary)

Horizon/Horizon/Sources/Features/Dashboard/View/DashboardViewModel.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class DashboardViewModel {
2727

2828
private(set) var state: InstUI.ScreenState = .loading
2929
var title: String = ""
30-
var nextUpViewModels: [DashboardCourse] = []
30+
var courses: [DashboardCourse] = []
3131

3232
// MARK: - Dependencies
3333

@@ -67,7 +67,7 @@ class DashboardViewModel {
6767

6868
getDashboardCoursesCancellable = getCoursesInteractor.getDashboardCourses(ignoreCache: ignoreCache)
6969
.sink { [weak self] items in
70-
self?.nextUpViewModels = items
70+
self?.courses = items
7171
self?.state = .data
7272
completion?()
7373
}
@@ -90,10 +90,14 @@ class DashboardViewModel {
9090
router.route(to: "/conversations", from: viewController)
9191
}
9292

93-
func navigateToCourseDetails(url: URL, viewController: WeakViewController) {
93+
func navigateToItemSequence(url: URL, viewController: WeakViewController) {
9494
router.route(to: url, from: viewController)
9595
}
9696

97+
func navigateToCourseDetails(id: String, viewController: WeakViewController) {
98+
router.route(to: "/courses/\(id)", from: viewController)
99+
}
100+
97101
func reload(completion: @escaping () -> Void) {
98102
getCourses(
99103
ignoreCache: true,

Horizon/Horizon/Sources/Features/HorizonTabBarController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ final class HorizonTabBarController: UITabBarController, UITabBarControllerDeleg
8282

8383
private func learnTab() -> UIViewController {
8484
let vc = CoreNavigationController(
85-
rootViewController: CoreHostingController(LearnAssembly.makeCoursesView())
85+
rootViewController: LearnAssembly.makeLearnView()
8686
)
8787
vc.tabBarItem.title = String(localized: "Learn", bundle: .horizon)
8888
vc.tabBarItem.image = getHorizonImage(name: "book_2")

Horizon/Horizon/Sources/Features/Learn/CourseDetails/View/CourseDetailsView.swift

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,24 @@ import HorizonUI
2121
import SwiftUI
2222

2323
struct CourseDetailsView: View {
24-
@Bindable private var viewModel: CourseDetailsViewModel
24+
@State private var viewModel: CourseDetailsViewModel
2525
@Environment(\.viewController) private var viewController
26-
@State var selectedTabIndex: Int = 0
26+
@Environment(\.dismiss) private var dismiss
27+
@State private var selectedTabIndex: Int = 0
28+
29+
// MARK: - Dependencies
2730

2831
private let notebookView: NotebookView
32+
private let isBackButtonVisible: Bool
33+
34+
// MARK: - Init
2935

30-
init(viewModel: CourseDetailsViewModel) {
36+
init(
37+
viewModel: CourseDetailsViewModel,
38+
isBackButtonVisible: Bool = true
39+
) {
3140
self.viewModel = viewModel
41+
self.isBackButtonVisible = isBackButtonVisible
3242
self.notebookView = NotebookAssembly.makeView(courseId: viewModel.courseID)
3343
}
3444

@@ -41,13 +51,33 @@ struct CourseDetailsView: View {
4151
.hidden(viewModel.isLoaderVisible)
4252
.background(Color.huiColors.surface.pagePrimary)
4353
.onAppear { viewModel.showTabBar() }
54+
.safeAreaInset(edge: .top, spacing: .zero) { navigationBar }
55+
.toolbar(.hidden)
4456
.overlay {
4557
if viewModel.isLoaderVisible {
4658
HorizonUI.Spinner(size: .small, showBackground: true)
4759
}
4860
}
4961
}
5062

63+
@ViewBuilder
64+
private var navigationBar: some View {
65+
if isBackButtonVisible {
66+
HStack {
67+
Button {
68+
dismiss()
69+
} label: {
70+
Image.huiIcons.arrowBack
71+
.foregroundStyle(Color.huiColors.icon.default)
72+
.frame(width: 44, height: 44, alignment: .leading)
73+
74+
}
75+
Spacer()
76+
}
77+
.padding(.horizontal, .huiSpaces.space24)
78+
}
79+
}
80+
5181
private var headerView: some View {
5282
VStack(alignment: .leading, spacing: .huiSpaces.space16) {
5383
Text(viewModel.course.name)

0 commit comments

Comments
 (0)