feat: lesson page
This commit is contained in:
@@ -25,7 +25,8 @@ import UserDropdown from '@/components/UserDropdown.vue'
|
||||
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { BookOpen, Users, TrendingUp, Search, Bell, Briefcase, Settings } from 'lucide-vue-next'
|
||||
import { BookOpen, Users, TrendingUp, Briefcase } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const links = [
|
||||
{
|
||||
@@ -50,5 +51,5 @@ const links = [
|
||||
},
|
||||
]
|
||||
|
||||
const isSidebarCollapsed = useStorage('sidebar_is_collapsed', false)
|
||||
let isSidebarCollapsed = ref(useStorage("sidebar_is_collapsed", false))
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="shadow rounded-md p-4 h-full" style="min-height: 150px;">
|
||||
<div class="border border-gray-200 rounded-md p-4 h-full" style="min-height: 150px;">
|
||||
<div class="text-xl font-semibold mb-1">
|
||||
{{ batch.title }}
|
||||
</div>
|
||||
@@ -9,11 +9,15 @@
|
||||
<div class="mt-auto">
|
||||
<div class="flex items-center mb-1">
|
||||
<Calendar class="h-4 w-4 stroke-1 mr-2" />
|
||||
{{ dayjs(batch.start_date).format("DD MMM YYYY") }} - {{ dayjs(batch.end_date).format("DD MMM YYYY") }}
|
||||
<span>
|
||||
{{ dayjs(batch.start_date).format("DD MMM YYYY") }} - {{ dayjs(batch.end_date).format("DD MMM YYYY") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Clock class="h-4 w-4 stroke-1 mr-2" />
|
||||
{{ batch.start_time }} - {{ batch.end_time }}
|
||||
<span>
|
||||
{{ formatTime(batch.start_time) }} - {{ formatTime(batch.end_time) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,6 +33,23 @@ const props = defineProps({
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
function formatTime(timeString) {
|
||||
if (!timeString) return "";
|
||||
const [hour, minute] = timeString.split(":").map(Number);
|
||||
|
||||
// Create a Date object with dummy values for day, month, and year
|
||||
const dummyDate = new Date(0, 0, 0, hour, minute);
|
||||
|
||||
// Use Intl.DateTimeFormat to format the time in 12-hour format
|
||||
const formattedTime = new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
}).format(dummyDate);
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.short-introduction {
|
||||
|
||||
11
frontend/src/components/BatchDetail.vue
Normal file
11
frontend/src/components/BatchDetail.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
batchName: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -2,7 +2,12 @@
|
||||
<div class="shadow rounded-md" style="width: 300px;">
|
||||
<iframe v-if="course.data.video_link" :src="video_link" class="rounded-t-md" />
|
||||
<div class="p-5">
|
||||
<Button variant="solid" class="w-full mb-3">
|
||||
<Button v-if="course.data.membership" variant="solid" class="w-full mb-3">
|
||||
<span>
|
||||
{{ __("Continue Learning") }}
|
||||
</span>
|
||||
</Button>
|
||||
<Button v-else variant="solid" class="w-full mb-3" >
|
||||
<span>
|
||||
{{ __("Start Learning") }}
|
||||
</span>
|
||||
@@ -31,6 +36,7 @@
|
||||
<script setup>
|
||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { Button } from "frappe-ui"
|
||||
const props = defineProps({
|
||||
course: {
|
||||
type: Object,
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<template>
|
||||
<div class="text-base mt-10">
|
||||
<div class="text-2xl font-semibold">
|
||||
{{ __("Course Content") }}
|
||||
</div>
|
||||
<div class="text-base">
|
||||
<div class="mt-4">
|
||||
<Disclosure v-slot="{ open }" v-for="chapter in outline.data" :key="chapter.name">
|
||||
<DisclosureButton
|
||||
class="flex w-full px-2 pt-2 pb-2"
|
||||
>
|
||||
<ChevronUp
|
||||
:class="open ? 'rotate-180 transform' : ''"
|
||||
<Disclosure v-slot="{ open }" v-for="(chapter, index) in outline.data" :key="chapter.name">
|
||||
<DisclosureButton class="flex w-full px-2 pt-2 pb-3">
|
||||
<ChevronRight
|
||||
:class="{'rotate-90 transform duration-200' : open, 'duration-200' : !open, 'open': index == 1}"
|
||||
class="h-5 w-5 text-gray-900 stroke-1 mr-2"
|
||||
/>
|
||||
<div class="text-lg font-medium">
|
||||
{{ chapter.title }}
|
||||
</div>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel class="px-10 pb-2">
|
||||
<DisclosurePanel class="px-10 pb-4" :static="index == 0">
|
||||
<div v-for="lesson in chapter.lessons" :key="lesson.name">
|
||||
<div class="flex items-center text-lg mb-4">
|
||||
<div class="flex items-center text-base mb-2">
|
||||
<MonitorPlay v-if="lesson.icon === 'icon-youtube'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
||||
<HelpCircle v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
||||
<FileText v-else-if="lesson.icon === 'icon-list'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
||||
@@ -33,7 +28,7 @@
|
||||
<script setup>
|
||||
import { createResource } from "frappe-ui";
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
|
||||
import { ChevronUp, MonitorPlay, HelpCircle, FileText } from 'lucide-vue-next';
|
||||
import { ChevronRight, MonitorPlay, HelpCircle, FileText } from 'lucide-vue-next';
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
|
||||
@@ -8,8 +8,13 @@ import dayjs from '@/utils/dayjs'
|
||||
import translationPlugin from './translation'
|
||||
import { usersStore } from './stores/user'
|
||||
import { sessionStore } from './stores/session'
|
||||
|
||||
import { FrappeUI, setConfig, frappeRequest, resourcesPlugin } from 'frappe-ui'
|
||||
import {
|
||||
FrappeUI,
|
||||
setConfig,
|
||||
frappeRequest,
|
||||
resourcesPlugin,
|
||||
pageMetaPlugin,
|
||||
} from 'frappe-ui'
|
||||
|
||||
let pinia = createPinia()
|
||||
let app = createApp(App)
|
||||
@@ -20,8 +25,8 @@ app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(resourcesPlugin)
|
||||
app.use(translationPlugin)
|
||||
app.use(pageMetaPlugin)
|
||||
app.provide('$dayjs', dayjs)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
const { userResource } = usersStore()
|
||||
|
||||
@@ -13,22 +13,81 @@
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mx-5 my-10">
|
||||
<div class="grid grid-cols-4 gap-8 mt-5">
|
||||
<BatchCard v-for="batch in batches.data" :batch="batch" />
|
||||
</div>
|
||||
<div class="mx-5 py-5">
|
||||
<Tabs class="overflow-hidden" v-model="tabIndex" :tabs="tabs">
|
||||
<template #tab="{ tab, selected }">
|
||||
<div>
|
||||
<button
|
||||
class="group -mb-px flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
||||
:class="{ 'text-gray-900': selected }">
|
||||
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
||||
{{ __(tab.label) }}
|
||||
<Badge :class="{ 'text-gray-900 border border-gray-900': selected }" variant="subtle" theme="gray"
|
||||
size="sm">
|
||||
{{ tab.count }}
|
||||
</Badge>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ tab }">
|
||||
<div v-if="tab.batches && tab.batches.value.length" class="grid grid-cols-1 sm:grid-cols-3 md:grid-cols-4 gap-8 mt-5">
|
||||
<router-link v-for="batch in tab.batches.value"
|
||||
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }">
|
||||
<BatchCard :batch="batch" />
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-else class="grid flex-1 place-items-center text-xl font-medium text-gray-500">
|
||||
<div class="flex flex-col items-center justify-center mt-4">
|
||||
<div>
|
||||
{{ __("No {0} batches found").format(tab.label.toLowerCase()) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, Breadcrumbs } from "frappe-ui";
|
||||
import { createResource, Breadcrumbs, Button, Tabs, Badge } from "frappe-ui";
|
||||
import { Plus } from "lucide-vue-next"
|
||||
import BatchCard from '@/components/BatchCard.vue';
|
||||
import { inject, ref, computed } from "vue";
|
||||
|
||||
const user = inject("$user")
|
||||
const batches = createResource({
|
||||
url: "lms.lms.utils.get_batches",
|
||||
cache: ["batches"],
|
||||
cache: ["batches", user?.data?.email],
|
||||
auto: true,
|
||||
});
|
||||
console.log(batches)
|
||||
|
||||
const tabIndex = ref(0)
|
||||
const tabs = [
|
||||
{
|
||||
label: "Upcoming",
|
||||
batches: computed(() => batches.data?.upcoming || []),
|
||||
count: computed(() => batches.data?.upcoming?.length),
|
||||
},
|
||||
];
|
||||
|
||||
if (user.data?.is_moderator) {
|
||||
tabs.push({
|
||||
label: "Archived",
|
||||
batches: computed(() => batches.data?.archived),
|
||||
count: computed(() => batches.data?.archived?.length),
|
||||
});
|
||||
tabs.push({
|
||||
label: "Private",
|
||||
batches: computed(() => batches.data?.private),
|
||||
count: computed(() => batches.data?.private?.length),
|
||||
})
|
||||
}
|
||||
if (user.data) {
|
||||
tabs.push({
|
||||
label: "Enrolled",
|
||||
batches: computed(() => batches.data?.enrolled),
|
||||
count: computed(() => batches.data?.enrolled?.length)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -30,7 +30,9 @@
|
||||
</div>
|
||||
·
|
||||
<div class="flex items-center">
|
||||
<BookOpen class="h-4 w-4 text-gray-700 mr-1"/>
|
||||
<span class="mr-1" :class="{ 'avatar-group overlap': course.data.instructors.length > 1 }">
|
||||
<UserAvatar v-for="instructor in course.data.instructors" :user="instructor"/>
|
||||
</span>
|
||||
<span v-if="course.data.instructors.length == 1">
|
||||
{{ course.data.instructors[0].full_name }}
|
||||
</span>
|
||||
@@ -46,7 +48,12 @@
|
||||
<div class="grid grid-cols-[60%,20%] gap-20 mt-10">
|
||||
<div class="">
|
||||
<div v-html="course.data.description" class="course-description"></div>
|
||||
<CourseOutline :courseName="course.data.name"/>
|
||||
<div class="mt-10">
|
||||
<div class="text-2xl font-semibold">
|
||||
{{ __("Course Content") }}
|
||||
</div>
|
||||
<CourseOutline :courseName="course.data.name"/>
|
||||
</div>
|
||||
<CourseReviews :courseName="course.data.name" :avg_rating="course.data.avg_rating"/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -59,10 +66,11 @@
|
||||
<script setup>
|
||||
import { createResource, Breadcrumbs } from "frappe-ui";
|
||||
import { computed } from "vue";
|
||||
import { BookOpen, Users, Star } from 'lucide-vue-next'
|
||||
import { Users, Star } from 'lucide-vue-next'
|
||||
import CourseCardOverlay from '@/components/CourseCardOverlay.vue';
|
||||
import CourseOutline from '@/components/CourseOutline.vue';
|
||||
import CourseReviews from '@/components/CourseReviews.vue';
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
@@ -103,4 +111,13 @@ const breadcrumbs = computed(() => {
|
||||
margin: revert;
|
||||
padding: revert;
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-group .avatar {
|
||||
transition: margin 0.1s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,6 @@
|
||||
<header class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5">
|
||||
<Breadcrumbs class="h-7" :items="[{ label: __('All Courses'), route: { name: 'Courses' } }]" />
|
||||
<div class="flex">
|
||||
<Select class="mr-2" :options="orderOptions" v-model="orderBy" />
|
||||
<Button variant="solid">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4" />
|
||||
@@ -29,7 +28,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ tab }">
|
||||
<div v-if="tab.courses && tab.courses.value.length" class="grid grid-cols-3 gap-8 mt-5">
|
||||
<div v-if="tab.courses && tab.courses.value.length" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5">
|
||||
<router-link v-for="course in tab.courses.value"
|
||||
:to="{ name: 'CourseDetail', params: { courseName: course.name } }">
|
||||
<CourseCard :course="course" />
|
||||
@@ -50,13 +49,12 @@
|
||||
|
||||
<script setup>
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { createListResource, Breadcrumbs, Tabs, Badge, Select, Button } from 'frappe-ui';
|
||||
import { createListResource, Breadcrumbs, Tabs, Badge, Button } from 'frappe-ui';
|
||||
import CourseCard from '@/components/CourseCard.vue';
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { ref, computed, inject } from 'vue'
|
||||
|
||||
const user = inject("$user")
|
||||
|
||||
const courses = createListResource({
|
||||
type: 'list',
|
||||
doctype: 'LMS Course',
|
||||
@@ -65,17 +63,6 @@ const courses = createListResource({
|
||||
auto: true,
|
||||
});
|
||||
|
||||
const is_moderator = computed(() => {
|
||||
if (user.data?.roles?.includes('Moderator')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const is_instructor = computed(() => {
|
||||
return user.data.roles.includes("Course Creator") ? true : false;
|
||||
});
|
||||
|
||||
const tabIndex = ref(0)
|
||||
const tabs = [
|
||||
{
|
||||
@@ -89,7 +76,7 @@ const tabs = [
|
||||
count: computed(() => courses.data?.upcoming?.length),
|
||||
}
|
||||
];
|
||||
console.log(user.data)
|
||||
|
||||
if (user.data) {
|
||||
tabs.push({
|
||||
label: 'Enrolled',
|
||||
@@ -97,7 +84,7 @@ if (user.data) {
|
||||
count: computed(() => courses.data?.enrolled?.length),
|
||||
});
|
||||
|
||||
if (is_moderator.value || is_instructor.value || courses.data?.created?.length) {
|
||||
if (user.data.is_moderator || user.data.is_instructor || courses.data?.created?.length) {
|
||||
tabs.push({
|
||||
label: 'Created',
|
||||
courses: computed(() => courses.data?.created),
|
||||
@@ -105,7 +92,7 @@ if (user.data) {
|
||||
});
|
||||
};
|
||||
|
||||
if (is_moderator.value) {
|
||||
if (user.data.is_moderator) {
|
||||
tabs.push({
|
||||
label: 'Under Review',
|
||||
courses: computed(() => courses.data?.under_review),
|
||||
@@ -113,25 +100,4 @@ if (user.data) {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const orderOptions = [
|
||||
{
|
||||
label: "Sort By",
|
||||
disabled: 1
|
||||
},
|
||||
{
|
||||
label: "Most Popular",
|
||||
value: "enrollment"
|
||||
},
|
||||
{
|
||||
label: "Highest Rated",
|
||||
value: "rating"
|
||||
},
|
||||
{
|
||||
label: "Newest",
|
||||
value: "creation"
|
||||
},
|
||||
];
|
||||
const orderBy = 'enrollment';
|
||||
</script>
|
||||
@@ -1,29 +1,135 @@
|
||||
<template>
|
||||
Lesson Page
|
||||
<div v-if="lesson.data && course.data" class="h-screen text-base">
|
||||
<header class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5">
|
||||
<Breadcrumbs class="h-7" :items="breadcrumbs"/>
|
||||
</header>
|
||||
<div class="grid grid-cols-[70%,30%] h-full">
|
||||
<div class="border-r-2 container pt-5 pb-10">
|
||||
<div class="text-3xl font-semibold">
|
||||
{{ lesson.data.title }}
|
||||
</div>
|
||||
<div class="flex items-center mt-2">
|
||||
<span class="mr-1" :class="{ 'avatar-group overlap': course.data.instructors.length > 1 }">
|
||||
<UserAvatar v-for="instructor in course.data.instructors" :user="instructor"/>
|
||||
</span>
|
||||
<span v-if="course.data.instructors.length == 1">
|
||||
{{ course.data.instructors[0].full_name }}
|
||||
</span>
|
||||
<span v-if="course.data.instructors.length == 2">
|
||||
{{ course.data.instructors[0].first_name }} and {{ course.data.instructors[1].first_name }}
|
||||
</span>
|
||||
<span v-if="course.data.instructors.length > 2">
|
||||
{{ course.data.instructors[0].first_name }} and {{ course.data.instructors.length - 1 }} others
|
||||
</span>
|
||||
</div>
|
||||
<div v-html="lesson.data.rendered_content" class="lesson-content mt-6"></div>
|
||||
</div>
|
||||
<div class="sticky top-10">
|
||||
<div class="bg-gray-50 p-5 border-b-2">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ course.data.title }}
|
||||
</div>
|
||||
<div v-if="user && course.data.membership" class="text-sm mt-3">
|
||||
{{ Math.ceil(course.data.membership.progress) }}% completed
|
||||
</div>
|
||||
<div v-if="user && course.data.membership" class="w-full bg-gray-200 rounded-full h-1 my-2">
|
||||
<div class="bg-gray-900 h-1 rounded-full" :style="{ width: Math.ceil(course.data.membership.progress) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<CourseOutline :courseName="lesson.data.course"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, Button } from "frappe-ui";
|
||||
import { useRoute } from "vue-router";
|
||||
const route = useRoute();
|
||||
console.log(route)
|
||||
import { createResource, Breadcrumbs } from "frappe-ui";
|
||||
import { computed, onMounted, onBeforeMount, onUnmounted, inject } from "vue";
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import CourseOutline from '@/components/CourseOutline.vue';
|
||||
import UserAvatar from '@/components/UserAvatar.vue';
|
||||
|
||||
const user = inject("$user");
|
||||
|
||||
onBeforeMount(() => {
|
||||
console.log("before mount");
|
||||
localStorage.setItem("sidebar_is_collapsed", true);
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
chapterNumber: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
lessonNumber: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
/*
|
||||
|
||||
const lesson = createResource({
|
||||
url: "lms.lms.utils.get_lesson",
|
||||
cache: ["lesson", props.courseName, props.lessonNumber],
|
||||
params: {
|
||||
course: props.courseName,
|
||||
chapter: props.chapterNumber,
|
||||
lesson: props.lessonNumber,
|
||||
},
|
||||
auto: true,
|
||||
}); */
|
||||
</script>
|
||||
});
|
||||
|
||||
const course = createResource({
|
||||
url: "lms.lms.utils.get_course_details",
|
||||
cache: ["course", props.courseName],
|
||||
params: {
|
||||
course: props.courseName
|
||||
},
|
||||
auto: true,
|
||||
});
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let items = [{ label: "All Courses", route: { name: "Courses" } }]
|
||||
items.push({
|
||||
label: course?.data?.title,
|
||||
route: { name: "CourseDetail", params: { course: props.courseName } },
|
||||
})
|
||||
items.push({
|
||||
label: lesson?.data?.title,
|
||||
route: { name: "Lesson", params: { course: props.courseName, chapterNumber: props.chapterNumber, lessonNumber: props.lessonNumber } },
|
||||
})
|
||||
return items
|
||||
});
|
||||
onUnmounted(() => {
|
||||
console.log("unmounted");
|
||||
useStorage("sidebar_is_collapsed", false);
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.youtube-video {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.avatar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-group .avatar {
|
||||
transition: margin 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.lesson-content div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.lesson-content p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
@@ -21,16 +21,21 @@ const routes = [
|
||||
},
|
||||
{
|
||||
// Create a route for path /courses/inventory-management/learn/1.1
|
||||
path: '/courses/:courseName/learn/:lessonNumber',
|
||||
path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber',
|
||||
name: 'Lesson',
|
||||
component: () => import('@/pages/Lesson.vue'),
|
||||
props: {},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/batches',
|
||||
name: 'Batches',
|
||||
component: () => import('@/pages/Batches.vue'),
|
||||
},
|
||||
{
|
||||
path: '/batches/:batchName',
|
||||
name: 'BatchDetail',
|
||||
component: () => import('@/pages/BatchDetail.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
let router = createRouter({
|
||||
|
||||
Reference in New Issue
Block a user