Skip to content

Commit 23800e9

Browse files
Catalogue: Continued restoration of catalogue features - refs #6235
1 parent 41336bc commit 23800e9

File tree

14 files changed

+901
-146
lines changed

14 files changed

+901
-146
lines changed

assets/vue/components/Login.vue

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,17 +113,26 @@ const requires2FA = ref(false)
113113
redirectNotAuthenticated()
114114
115115
async function onSubmitLoginForm() {
116-
const response = await performLogin({
117-
login: login.value,
118-
password: password.value,
119-
totp: requires2FA.value ? totp.value : null,
120-
_remember_me: remember.value,
121-
})
122-
123-
if (response.requires2FA) {
124-
requires2FA.value = true
125-
} else {
126-
await router.replace({ name: "Home" })
116+
try {
117+
const response = await performLogin({
118+
login: login.value,
119+
password: password.value,
120+
totp: requires2FA.value ? totp.value : null,
121+
_remember_me: remember.value,
122+
})
123+
124+
if (!response) {
125+
console.warn("[Login] No response from performLogin.")
126+
return
127+
}
128+
129+
if (response.requires2FA) {
130+
requires2FA.value = true
131+
} else {
132+
await router.replace({ name: "Home" })
133+
}
134+
} catch (error) {
135+
console.error("[Login] performLogin failed:", error)
127136
}
128137
}
129138
</script>

assets/vue/components/course/CatalogueCourseCard.vue

Lines changed: 187 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,65 @@
11
<template>
22
<div
3-
class="course-card hover:shadow-lg transition duration-300 rounded-2xl overflow-hidden border border-gray-300 bg-white flex flex-col"
3+
class="course-card relative hover:shadow-lg transition duration-300 rounded-2xl overflow-hidden border border-gray-300 bg-white flex flex-col"
44
>
5+
<div
6+
v-if="course.categories?.length"
7+
class="absolute top-2 left-2 flex flex-wrap gap-1 z-30"
8+
>
9+
<span
10+
v-for="cat in course.categories"
11+
:key="cat.id"
12+
class="bg-support-5 text-white text-xs font-bold px-2 py-0.5 rounded"
13+
>
14+
{{ cat.title }}
15+
</span>
16+
</div>
17+
<span
18+
v-if="course.courseLanguage"
19+
class="absolute top-0 right-0 bg-support-4 text-white text-xs px-2 py-0.5 font-semibold rounded-bl-lg z-20"
20+
>
21+
{{ course.courseLanguage }}
22+
</span>
23+
24+
<Button
25+
v-if="allowDescription && showInfoPopup"
26+
icon="pi pi-info-circle"
27+
@click="showDescriptionDialog = true"
28+
class="absolute top-10 left-2 z-20"
29+
size="small"
30+
text
31+
aria-label="Course info"
32+
/>
33+
<router-link
34+
v-if="imageLink"
35+
:to="imageLink"
36+
>
37+
<img
38+
:src="course.illustrationUrl"
39+
:alt="course.title"
40+
class="w-full object-cover"
41+
/>
42+
</router-link>
543
<img
44+
v-else
645
:src="course.illustrationUrl"
746
:alt="course.title"
847
class="w-full object-cover"
948
/>
1049
<div class="p-4 flex flex-col flex-grow gap-2">
11-
<h3 class="text-xl font-semibold text-gray-800">{{ course.title }}</h3>
12-
<p class="text-sm text-gray-600 line-clamp-3">{{ course.description }}</p>
13-
50+
<router-link
51+
v-if="showTitle && titleLink"
52+
:to="titleLink"
53+
class="text-xl font-semibold"
54+
>
55+
{{ course.title }}
56+
</router-link>
57+
<h3
58+
v-else-if="showTitle"
59+
class="text-xl font-semibold"
60+
>
61+
{{ course.title }}
62+
</h3>
1463
<div
1564
v-if="course.duration"
1665
class="text-sm text-gray-700"
@@ -33,33 +82,15 @@
3382
<strong>{{ $t("Price") }}:</strong>
3483
{{ course.price > 0 ? "S/. " + course.price.toFixed(2) : $t("Free") }}
3584
</div>
36-
37-
<div
38-
v-if="course.categories?.length"
39-
class="flex flex-wrap gap-1"
40-
>
41-
<span
42-
v-for="cat in course.categories"
43-
:key="cat.id"
44-
class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full"
45-
>
46-
{{ cat.title }}
47-
</span>
48-
</div>
49-
50-
<div class="text-sm text-gray-700">
51-
<strong>{{ $t("Language") }}:</strong> {{ course.courseLanguage }}
52-
</div>
53-
5485
<div
5586
v-if="course.teachers?.length"
5687
class="text-sm text-gray-700"
5788
>
5889
<strong>{{ $t("Teachers") }}:</strong>
5990
{{ course.teachers.map((t) => t.user.fullName).join(", ") }}
6091
</div>
61-
6292
<Rating
93+
v-if="props.currentUserId"
6394
:model-value="course.userVote?.vote || 0"
6495
:stars="5"
6596
:cancel="false"
@@ -79,6 +110,15 @@
79110
</span>
80111
</div>
81112

113+
<div
114+
v-for="field in cardExtraFields"
115+
:key="field.variable"
116+
class="text-sm text-gray-700"
117+
>
118+
<strong>{{ field.display_text }}:</strong>
119+
{{ course.extra_fields?.[field.variable] ?? "-" }}
120+
</div>
121+
82122
<div class="mt-auto pt-2">
83123
<router-link
84124
v-if="course.visibility === 3 || (course.visibility === 2 && isUserInCourse)"
@@ -125,26 +165,55 @@
125165
</div>
126166
</div>
127167
</div>
168+
<Dialog
169+
v-model:visible="showDescriptionDialog"
170+
:header="course.title"
171+
modal
172+
class="w-96"
173+
>
174+
<p class="text-sm text-gray-700 whitespace-pre-line">
175+
{{ course.description || $t("No description available") }}
176+
</p>
177+
</Dialog>
128178
</template>
129179
<script setup>
130180
import Rating from "primevue/rating"
131181
import Button from "primevue/button"
132182
import { computed, ref } from "vue"
133183
import courseRelUserService from "../../services/courseRelUserService"
134-
import { useRouter } from "vue-router"
184+
import { useRoute, useRouter } from "vue-router"
135185
import { useNotification } from "../../composables/notification"
186+
import Dialog from "primevue/dialog"
187+
import { usePlatformConfig } from "../../store/platformConfig"
188+
189+
const platformConfigStore = usePlatformConfig()
190+
const showDescriptionDialog = ref(false)
191+
192+
const allowDescription = computed(
193+
() => platformConfigStore.getSetting("course.show_courses_descriptions_in_catalog") !== "false",
194+
)
136195
137196
const props = defineProps({
138197
course: Object,
139-
currentUserId: Number,
198+
currentUserId: {
199+
type: Number,
200+
default: null,
201+
},
202+
showTitle: {
203+
type: Boolean,
204+
default: true,
205+
},
206+
cardExtraFields: { type: Array, default: () => [] },
140207
})
141208
142209
const emit = defineEmits(["rate", "subscribed"])
143210
144211
const router = useRouter()
212+
const route = useRoute()
145213
const { showErrorNotification, showSuccessNotification } = useNotification()
146214
147215
const isUserInCourse = computed(() => {
216+
if (!props.currentUserId) return false
148217
return props.course.users?.some((user) => user.user.id === props.currentUserId)
149218
})
150219
@@ -160,21 +229,108 @@ const emitRating = (event) => {
160229
161230
const subscribing = ref(false)
162231
const subscribeToCourse = async () => {
232+
if (!props.currentUserId) {
233+
showErrorNotification("You must be logged in to subscribe to a course.")
234+
return
235+
}
236+
163237
try {
164238
subscribing.value = true
165239
166-
const response = await courseRelUserService.subscribe({
167-
userId: props.currentUserId,
168-
courseId: props.course.id,
169-
})
240+
const useAutoSession =
241+
platformConfigStore.getSetting("session.catalog_course_subscription_in_user_s_session") === "true"
242+
243+
let sessionId = null
244+
245+
if (useAutoSession) {
246+
const response = await courseRelUserService.autoSubscribeCourse(props.course.id)
247+
sessionId = response?.sessionId
248+
249+
if (!sessionId) {
250+
throw new Error("No session ID returned after subscription.")
251+
}
252+
} else {
253+
const response = await courseRelUserService.subscribe({
254+
userId: props.currentUserId,
255+
courseId: props.course.id,
256+
})
257+
258+
const userIdFromResponse = response?.user?.["@id"]?.split("/")?.pop()
259+
260+
emit("subscribed", {
261+
courseId: props.course.id,
262+
newUser: { user: { id: Number(userIdFromResponse) } },
263+
})
264+
}
170265
171-
emit("subscribed", { courseId: props.course.id, newUser: response })
172266
showSuccessNotification("You have successfully subscribed to this course.")
173-
router.push({ name: "CourseHome", params: { id: props.course.id } })
267+
268+
await router.push({
269+
name: "CourseHome",
270+
params: {
271+
id: props.course.id,
272+
},
273+
query: sessionId ? { sid: sessionId } : {},
274+
})
174275
} catch (e) {
276+
console.error("Subscription error:", e)
175277
showErrorNotification("Failed to subscribe to the course.")
176278
} finally {
177279
subscribing.value = false
178280
}
179281
}
282+
283+
function routeExists(name) {
284+
return router.getRoutes().some((route) => route.name === name)
285+
}
286+
287+
const linkSettings = computed(() => {
288+
const settings = platformConfigStore.getSetting("course.course_catalog_settings")
289+
const result = settings?.link_settings ?? {}
290+
console.log("Link settings:", result)
291+
return result
292+
})
293+
294+
const imageLink = computed(() => {
295+
const routeName =
296+
linkSettings.value.image_url === "course_home"
297+
? "CourseHome"
298+
: linkSettings.value.image_url === "course_about"
299+
? "CourseAbout"
300+
: null
301+
302+
if (routeName && routeExists(routeName)) {
303+
return { name: routeName, params: { id: props.course.id } }
304+
}
305+
306+
if (routeName) {
307+
console.warn(`[CatalogueCourseCard] Route '${routeName}' does not exist.`)
308+
}
309+
310+
return null
311+
})
312+
313+
const titleLink = computed(() => {
314+
const routeName = linkSettings.value.title_url === "course_home" ? "CourseHome" : null
315+
316+
if (routeName && routeExists(routeName)) {
317+
return { name: routeName, params: { id: props.course.id } }
318+
}
319+
320+
if (routeName) {
321+
console.warn(`[CatalogueCourseCard] Route '${routeName}' does not exist.`)
322+
}
323+
324+
return null
325+
})
326+
327+
const showInfoPopup = computed(() => {
328+
const allowed = ["course_description_popup"]
329+
const value = linkSettings.value.info_url
330+
if (value && !allowed.includes(value)) {
331+
console.warn(`[CatalogueCourseCard] info_url '${value}' is not a recognized option.`)
332+
return false
333+
}
334+
return value === "course_description_popup"
335+
})
180336
</script>

assets/vue/components/layout/TopbarNotLoggedIn.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ const menuItems = computed(() => {
6363
})
6464
}
6565
66+
const showCatalogueLink =
67+
platformConfigStore.getSetting("course.course_catalog_published") !== "false" &&
68+
platformConfigStore.getSetting("course.catalog_hide_public_link") !== "true" &&
69+
platformConfigStore.getSetting("display.allow_students_to_browse_courses") !== "false"
70+
71+
if (showCatalogueLink) {
72+
items.splice(1, 0, {
73+
label: t("Browse courses"),
74+
url: router.resolve({ name: "CatalogueCourses" }).href,
75+
})
76+
}
77+
6678
console.log("Menu Items:", items)
6779
return items
6880
})

0 commit comments

Comments
 (0)