mirror of
https://github.com/frappe/lms.git
synced 2026-04-19 22:52:29 +03:00
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="course.title"
|
||||
class="flex flex-col h-full rounded-md overflow-auto text-ink-gray-9"
|
||||
class="flex flex-col h-full rounded-md overflow-auto text-ink-gray-9 bg-surface-cards"
|
||||
style="min-height: 350px"
|
||||
>
|
||||
<div
|
||||
@@ -10,7 +10,7 @@
|
||||
course.image
|
||||
? { backgroundImage: `url('${encodeURI(course.image)}')` }
|
||||
: {
|
||||
backgroundImage: getGradientColor(),
|
||||
backgroundImage: gradientColor,
|
||||
backgroundBlendMode: 'screen',
|
||||
}
|
||||
"
|
||||
@@ -137,6 +137,8 @@ import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Tooltip } from 'frappe-ui'
|
||||
import { formatAmount } from '@/utils'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { computed, watch } from 'vue'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
@@ -151,12 +153,12 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const getGradientColor = () => {
|
||||
let theme = localStorage.getItem('theme') == 'dark' ? 'darkMode' : 'lightMode'
|
||||
const gradientColor = computed(() => {
|
||||
let themeMode = theme.value === 'dark' ? 'darkMode' : 'lightMode'
|
||||
let color = props.course.card_gradient?.toLowerCase() || 'blue'
|
||||
let colorMap = colors[theme][color]
|
||||
let colorMap = colors[themeMode][color]
|
||||
return `linear-gradient(to top right, black, ${colorMap[400]})`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.course-card-pills {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
import { getSidebarLinks } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { call } from 'frappe-ui'
|
||||
import { watch, ref, onMounted } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { usersStore } from '@/stores/user'
|
||||
@@ -68,26 +68,13 @@ let { isLoggedIn } = sessionStore()
|
||||
const { sidebarSettings } = useSettings()
|
||||
const router = useRouter()
|
||||
let { userResource } = usersStore()
|
||||
const sidebarLinks = ref(getSidebarLinks())
|
||||
const sidebarLinks = ref([])
|
||||
const otherLinks = ref([])
|
||||
const showMenu = ref(false)
|
||||
const menu = ref(null)
|
||||
const isModerator = ref(false)
|
||||
const isInstructor = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
sidebarSettings.reload(
|
||||
{},
|
||||
{
|
||||
onSuccess(data) {
|
||||
destructureSidebarLinks()
|
||||
filterLinksToShow(data)
|
||||
addOtherLinks()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const handleOutsideClick = (e) => {
|
||||
if (menu.value && !menu.value.contains(e.target)) {
|
||||
showMenu.value = false
|
||||
@@ -126,65 +113,57 @@ const filterLinksToShow = (data) => {
|
||||
|
||||
const addOtherLinks = () => {
|
||||
if (user) {
|
||||
otherLinks.value.push({
|
||||
label: 'Notifications',
|
||||
icon: 'Bell',
|
||||
to: 'Notifications',
|
||||
})
|
||||
otherLinks.value.push({
|
||||
label: 'Profile',
|
||||
icon: 'UserRound',
|
||||
})
|
||||
otherLinks.value.push({
|
||||
label: 'Log out',
|
||||
icon: 'LogOut',
|
||||
})
|
||||
addLink('Notifications', 'Bell', 'Notifications')
|
||||
addLink('Profile', 'UserRound')
|
||||
addLink('Log out', 'LogOut')
|
||||
} else {
|
||||
otherLinks.value.push({
|
||||
label: 'Log in',
|
||||
icon: 'LogIn',
|
||||
})
|
||||
addLink('Log in', 'LogIn')
|
||||
}
|
||||
}
|
||||
|
||||
watch(userResource, () => {
|
||||
if (userResource.data) {
|
||||
isModerator.value = userResource.data.is_moderator
|
||||
isInstructor.value = userResource.data.is_instructor
|
||||
addPrograms()
|
||||
if (isModerator.value || isInstructor.value) {
|
||||
addProgrammingExercises()
|
||||
addQuizzes()
|
||||
addAssignments()
|
||||
const addLink = (label, icon, to = '') => {
|
||||
if (otherLinks.value.some((link) => link.label === label)) return
|
||||
otherLinks.value.push({
|
||||
label: label,
|
||||
icon: icon,
|
||||
to: to,
|
||||
})
|
||||
}
|
||||
|
||||
const updateSidebarLinks = () => {
|
||||
sidebarLinks.value = getSidebarLinks(true)
|
||||
destructureSidebarLinks()
|
||||
sidebarSettings.reload(
|
||||
{},
|
||||
{
|
||||
onSuccess: async (data) => {
|
||||
filterLinksToShow(data)
|
||||
await addPrograms()
|
||||
if (isModerator.value || isInstructor.value) {
|
||||
addQuizzes()
|
||||
addAssignments()
|
||||
addProgrammingExercises()
|
||||
}
|
||||
addOtherLinks()
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const addQuizzes = () => {
|
||||
otherLinks.value.push({
|
||||
label: 'Quizzes',
|
||||
icon: 'CircleHelp',
|
||||
to: 'Quizzes',
|
||||
})
|
||||
addLink('Quizzes', 'CircleHelp', 'Quizzes')
|
||||
}
|
||||
|
||||
const addAssignments = () => {
|
||||
otherLinks.value.push({
|
||||
label: 'Assignments',
|
||||
icon: 'Pencil',
|
||||
to: 'Assignments',
|
||||
})
|
||||
addLink('Assignments', 'Pencil', 'Assignments')
|
||||
}
|
||||
|
||||
const addProgrammingExercises = () => {
|
||||
otherLinks.value.push({
|
||||
label: 'Programming Exercises',
|
||||
icon: 'Code',
|
||||
to: 'ProgrammingExercises',
|
||||
})
|
||||
addLink('Programming Exercises', 'Code', 'ProgrammingExercises')
|
||||
}
|
||||
|
||||
const addPrograms = async () => {
|
||||
if (sidebarLinks.value.some((link) => link.label === 'Programs')) return
|
||||
let canAddProgram = await checkIfCanAddProgram()
|
||||
if (!canAddProgram) return
|
||||
let activeFor = ['Programs', 'ProgramDetail']
|
||||
@@ -198,7 +177,21 @@ const addPrograms = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
userResource,
|
||||
async () => {
|
||||
await userResource.promise
|
||||
if (userResource.data) {
|
||||
isModerator.value = userResource.data.is_moderator
|
||||
isInstructor.value = userResource.data.is_instructor
|
||||
}
|
||||
updateSidebarLinks()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const checkIfCanAddProgram = async () => {
|
||||
if (!userResource.data) return false
|
||||
if (isModerator.value || isInstructor.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import { sessionStore } from '@/stores/session'
|
||||
import { call, Dropdown, toast } from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import { applyTheme, toggleTheme, theme } from '@/utils/theme'
|
||||
import { usersStore } from '@/stores/user'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
||||
@@ -94,7 +95,6 @@ let { userResource } = usersStore()
|
||||
const settingsStore = useSettings()
|
||||
let { isLoggedIn } = sessionStore()
|
||||
const showSettingsModal = ref(false)
|
||||
const theme = ref('light')
|
||||
const frappeCloudBaseEndpoint = 'https://frappecloud.com'
|
||||
const $dialog = createDialog
|
||||
|
||||
@@ -106,9 +106,8 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
theme.value = localStorage.getItem('theme') || 'light'
|
||||
if (['light', 'dark'].includes(theme.value)) {
|
||||
document.documentElement.setAttribute('data-theme', theme.value)
|
||||
applyTheme(theme.value)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -119,13 +118,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const toggleTheme = () => {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme')
|
||||
theme.value = currentTheme === 'dark' ? 'light' : 'dark'
|
||||
document.documentElement.setAttribute('data-theme', theme.value)
|
||||
localStorage.setItem('theme', theme.value)
|
||||
}
|
||||
|
||||
const userDropdownOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -403,8 +403,8 @@ export function getUserTimezone() {
|
||||
}
|
||||
}
|
||||
|
||||
export function getSidebarLinks() {
|
||||
let links = getSidebarItems()
|
||||
export function getSidebarLinks(forMobile = false) {
|
||||
let links = getSidebarItems(forMobile)
|
||||
|
||||
links.forEach((link) => {
|
||||
link.items = link.items.filter((item) => {
|
||||
@@ -419,7 +419,7 @@ export function getSidebarLinks() {
|
||||
return links
|
||||
}
|
||||
|
||||
const getSidebarItems = () => {
|
||||
const getSidebarItems = (forMobile = false) => {
|
||||
const { userResource } = usersStore()
|
||||
const { settings } = useSettings()
|
||||
|
||||
@@ -441,7 +441,7 @@ const getSidebarItems = () => {
|
||||
icon: 'Search',
|
||||
to: 'Search',
|
||||
condition: () => {
|
||||
return userResource?.data
|
||||
return !forMobile && userResource?.data
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -449,7 +449,7 @@ const getSidebarItems = () => {
|
||||
icon: 'Bell',
|
||||
to: 'Notifications',
|
||||
condition: () => {
|
||||
return userResource?.data
|
||||
return !forMobile && userResource?.data
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -476,7 +476,7 @@ const getSidebarItems = () => {
|
||||
activeFor: ['Programs', 'ProgramDetail'],
|
||||
await: true,
|
||||
condition: () => {
|
||||
return checkIfCanAddProgram()
|
||||
return checkIfCanAddProgram(forMobile)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -514,7 +514,8 @@ const getSidebarItems = () => {
|
||||
: settings.data?.contact_us_email,
|
||||
condition: () => {
|
||||
return (
|
||||
(settings?.data?.contact_us_email &&
|
||||
(!forMobile &&
|
||||
settings?.data?.contact_us_email &&
|
||||
userResource?.data) ||
|
||||
settings?.data?.contact_us_url
|
||||
)
|
||||
@@ -531,7 +532,7 @@ const getSidebarItems = () => {
|
||||
icon: 'CircleHelp',
|
||||
to: 'Quizzes',
|
||||
condition: () => {
|
||||
return isAdmin()
|
||||
return !forMobile && isAdmin()
|
||||
},
|
||||
activeFor: [
|
||||
'Quizzes',
|
||||
@@ -546,7 +547,7 @@ const getSidebarItems = () => {
|
||||
icon: 'Pencil',
|
||||
to: 'Assignments',
|
||||
condition: () => {
|
||||
return isAdmin()
|
||||
return !forMobile && isAdmin()
|
||||
},
|
||||
activeFor: [
|
||||
'Assignments',
|
||||
@@ -559,7 +560,7 @@ const getSidebarItems = () => {
|
||||
icon: 'Code',
|
||||
to: 'ProgrammingExercises',
|
||||
condition: () => {
|
||||
return isAdmin()
|
||||
return !forMobile && isAdmin()
|
||||
},
|
||||
activeFor: [
|
||||
'ProgrammingExercises',
|
||||
@@ -581,10 +582,11 @@ const isAdmin = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const checkIfCanAddProgram = () => {
|
||||
const checkIfCanAddProgram = (forMobile = false) => {
|
||||
const { userResource } = usersStore()
|
||||
const { programs } = useSettings()
|
||||
if (!userResource.data) return false
|
||||
if (forMobile) return false
|
||||
if (userResource?.data?.is_moderator || userResource?.data?.is_instructor) {
|
||||
return true
|
||||
}
|
||||
|
||||
16
frontend/src/utils/theme.ts
Normal file
16
frontend/src/utils/theme.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const theme = ref<'light' | 'dark'>(localStorage.getItem('theme') as 'light' | 'dark' || 'light')
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme: 'light' | 'dark' = theme.value === 'dark' ? 'light' : 'dark'
|
||||
applyTheme(newTheme)
|
||||
}
|
||||
|
||||
const applyTheme = (value: 'light' | 'dark') => {
|
||||
document.documentElement.setAttribute('data-theme', value)
|
||||
localStorage.setItem('theme', value)
|
||||
theme.value = value
|
||||
}
|
||||
|
||||
export { applyTheme, toggleTheme, theme }
|
||||
@@ -35,10 +35,10 @@ class LMSCertificate(Document):
|
||||
custom_template = frappe.db.get_single_value("LMS Settings", "certification_template")
|
||||
|
||||
args = {
|
||||
"student_name": self.member_name,
|
||||
"member_name": self.member_name,
|
||||
"course_name": self.course,
|
||||
"course_title": frappe.db.get_value("LMS Course", self.course, "title"),
|
||||
"certificate_name": self.name,
|
||||
"name": self.name,
|
||||
"template": self.template,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p>
|
||||
{{ _("Dear ") }} {{ student_name }},
|
||||
{{ _("Dear ") }} {{ member_name }},
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
@@ -10,7 +10,7 @@
|
||||
{{ _("With this certification, you can now showcase your updated skills and share your achievement with your colleagues and on LinkedIn. To access your certificate, please click on the link provided below. Make sure you are logged in to the portal.") }}
|
||||
</p>
|
||||
<br>
|
||||
<a href="/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name={{certificate_name}}&format={{template | urlencode }}">{{ _("Certificate Link") }}</a>
|
||||
<a href="/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name={{name}}&format={{template | urlencode }}">{{ _("Certificate Link") }}</a>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("Once again, congratulations on this significant accomplishment.")}}
|
||||
|
||||
124
lms/workspace_sidebar/learning.json
Normal file
124
lms/workspace_sidebar/learning.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"app": "lms",
|
||||
"creation": "2026-04-06 18:02:13.124002",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar",
|
||||
"header_icon": "book",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Courses",
|
||||
"link_to": "LMS Course",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Enrollments",
|
||||
"link_to": "LMS Enrollment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Course Reviews",
|
||||
"link_to": "LMS Course Review",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Batches",
|
||||
"link_to": "LMS Batch",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Batch Enrollments",
|
||||
"link_to": "LMS Batch Enrollment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Batch Feedback",
|
||||
"link_to": "LMS Batch Feedback",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Evaluation Requests",
|
||||
"link_to": "LMS Certificate Request",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Evaluations",
|
||||
"link_to": "LMS Certificate Evaluation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Certificates",
|
||||
"link_to": "LMS Certificate",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-04-06 18:04:32.990958",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"name": "Learning",
|
||||
"owner": "sayali@frappe.io",
|
||||
"standard": 1,
|
||||
"title": "Learning"
|
||||
}
|
||||
Reference in New Issue
Block a user