Merge branch 'develop' into fix/quiz-enter

This commit is contained in:
Jannat Patel
2026-01-01 13:16:34 +05:30
committed by GitHub
132 changed files with 10933 additions and 28605 deletions

View File

@@ -148,7 +148,7 @@ const assignmentFilter = computed(() => {
const assignments = createListResource({
doctype: 'LMS Assignment',
fields: ['name', 'title', 'type', 'creation', 'question'],
fields: ['name', 'title', 'type', 'creation', 'question', 'course'],
orderBy: 'modified desc',
cache: ['assignments'],
transform(data) {

View File

@@ -144,6 +144,20 @@
</span>
</div>
</div>
<div
v-if="batch.data.evaluation_end_date && isStudent"
class="text-sm leading-5 bg-surface-amber-1 text-ink-amber-3 p-2 rounded-md mb-10"
>
{{ __('The last day to schedule your evaluations is ') }}
<span class="font-medium">
{{
dayjs(batch.data.evaluation_end_date).format('DD MMMM YYYY')
}} </span
>.
{{
__('Please make sure to schedule your evaluation before this date.')
}}
</div>
<div v-if="dayjs().isSameOrAfter(dayjs(batch.data.start_date))">
<div class="text-ink-gray-7 font-semibold mb-2">
{{ __('Feedback') }}

View File

@@ -90,7 +90,7 @@
v-model="currentCategory"
:options="categories"
:placeholder="__('Category')"
@change="updateBatches()"
@update:modelValue="updateBatches()"
/>
</div>
</div>

View File

@@ -91,6 +91,16 @@
</Button>
</div>
</div>
<p
class="bg-surface-amber-2 text-ink-amber-2 text-sm leading-5 p-2 rounded-md"
>
{{
__(
'Please ensure that the billing name you enter is correct, as it will be used on your invoice.'
)
}}
</p>
</div>
<div class="flex-1 lg:mr-10">
@@ -104,16 +114,22 @@
<FormControl
:label="__('Billing Name')"
v-model="billingDetails.billing_name"
:required="true"
/>
<FormControl
:label="__('Address Line 1')"
v-model="billingDetails.address_line1"
:required="true"
/>
<FormControl
:label="__('Address Line 2')"
v-model="billingDetails.address_line2"
/>
<FormControl :label="__('City')" v-model="billingDetails.city" />
<FormControl
:label="__('City')"
v-model="billingDetails.city"
:required="true"
/>
<FormControl
:label="__('State/Province')"
v-model="billingDetails.state"
@@ -125,20 +141,24 @@
:value="billingDetails.country"
@change="(option) => changeCurrency(option)"
:label="__('Country')"
:required="true"
/>
<FormControl
:label="__('Postal Code')"
v-model="billingDetails.pincode"
:required="true"
/>
<FormControl
:label="__('Phone Number')"
v-model="billingDetails.phone"
:required="true"
/>
<Link
doctype="LMS Source"
:value="billingDetails.source"
@change="(option) => (billingDetails.source = option)"
:label="__('Where did you hear about us?')"
:required="true"
/>
<FormControl
v-if="billingDetails.country == 'India'"
@@ -152,14 +172,29 @@
/>
</div>
</div>
<div class="flex items-center justify-between border-t pt-4 mt-8">
<p class="text-ink-gray-5">
{{
__(
'Make sure to enter the correct billing name as the same will be used in your invoice.'
)
}}
</p>
<div
class="flex flex-col lg:flex-row items-start lg:items-center justify-between border-t pt-4 mt-8 space-y-4 lg:space-y-0"
>
<div>
<FormControl
:label="
__(
'I consent to my personal information being stored for invoicing'
)
"
type="checkbox"
class="leading-6"
v-model="billingDetails.member_consent"
/>
<div
v-if="showConsentWarning"
class="mt-1 text-xs text-ink-red-3"
>
{{
__('Please provide your consent to proceed with the payment')
}}
</div>
</div>
<Button variant="solid" size="md" @click="generatePaymentLink()">
{{ __('Proceed to Payment') }}
</Button>
@@ -194,7 +229,7 @@ import {
toast,
call,
} from 'frappe-ui'
import { reactive, inject, onMounted, computed, ref } from 'vue'
import { reactive, inject, onMounted, computed, ref, watch } from 'vue'
import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
@@ -202,6 +237,7 @@ import { X } from 'lucide-vue-next'
const user = inject('$user')
const { brand } = sessionStore()
const showConsentWarning = ref(false)
onMounted(() => {
const script = document.createElement('script')
@@ -296,6 +332,10 @@ const generatePaymentLink = () => {
if (!billingDetails.source) {
return __('Please let us know where you heard about us from.')
}
if (!billingDetails.member_consent) {
showConsentWarning.value = true
return __('Please provide your consent to proceed with the payment.')
}
return validateAddress()
},
onSuccess(data) {
@@ -406,6 +446,12 @@ const redirectTo = computed(() => {
}
})
watch(billingDetails, () => {
if (billingDetails.member_consent) {
showConsentWarning.value = false
}
})
usePageMeta(() => {
return {
title: __('Billing Details'),

View File

@@ -13,49 +13,68 @@
</router-link>
</header>
<div class="mx-auto w-full max-w-4xl pt-6 pb-10">
<div class="flex flex-col md:flex-row justify-between mb-4 px-3">
<div class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
<div class="flex flex-col md:flex-row justify-between mb-8 px-3">
<div class="text-xl font-semibold text-ink-gray-9 mb-4 md:mb-0">
{{ memberCount }} {{ __('certified members') }}
</div>
<div class="grid grid-cols-2 gap-2">
<FormControl
v-model="nameFilter"
:placeholder="__('Search by Name')"
type="text"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateParticipants()"
/>
<div
v-if="categories.data?.length"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
>
<Select
v-model="currentCategory"
:options="categories.data"
:placeholder="__('Category')"
<div
class="flex flex-col md:flex-row md:items-center space-y-4 md:space-y-0 md:space-x-4"
>
<div class="flex items-center space-x-4">
<FormControl
v-model="nameFilter"
:placeholder="__('Search by Name')"
type="text"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateParticipants()"
/>
<div
v-if="categories.data?.length"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
>
<Select
v-model="currentCategory"
:options="categories.data"
:placeholder="__('Category')"
@update:modelValue="updateParticipants()"
/>
</div>
</div>
<div class="flex items-center space-x-4">
<FormControl
v-model="openToOpportunities"
:label="__('Open to Opportunities')"
type="checkbox"
@change="updateParticipants()"
/>
<FormControl
v-model="hiring"
:label="__('Hiring')"
type="checkbox"
@change="updateParticipants()"
/>
</div>
</div>
</div>
<div v-if="participants.data?.length" class="divide-y">
<template v-for="participant in participants.data">
<div v-if="participants.data?.length" class="">
<template v-for="(participant, index) in participants.data">
<router-link
:to="{
name: 'ProfileCertificates',
name: 'ProfileAbout',
params: {
username: participant.username,
},
}"
class="flex sm:rounded px-3 py-2 sm:h-15 hover:bg-surface-gray-2"
class="flex rounded-md hover:bg-surface-gray-2 px-3"
>
<div class="flex items-center w-full space-x-3">
<Avatar
:image="participant.user_image"
class="size-8 rounded-full object-contain"
:label="participant.full_name"
size="2xl"
/>
<div
class="flex w-full space-x-3 py-2"
:class="{
'border-b': index < participants.data.length - 1,
}"
>
<UserAvatar :user="participant" size="2xl" />
<div class="flex flex-col md:flex-row w-full">
<div class="flex-1">
<div class="text-base font-medium text-ink-gray-8">
@@ -115,10 +134,13 @@ import { computed, inject, onMounted, ref } from 'vue'
import { GraduationCap } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import EmptyState from '@/components/EmptyState.vue'
import UserAvatar from '@/components/UserAvatar.vue'
const currentCategory = ref('')
const filters = ref({})
const currentCategory = ref('')
const nameFilter = ref('')
const openToOpportunities = ref(false)
const hiring = ref(false)
const { brand } = sessionStore()
const memberCount = ref(0)
const dayjs = inject('$dayjs')
@@ -150,7 +172,7 @@ const categories = createListResource({
cache: ['certification_categories'],
auto: true,
transform(data) {
data.unshift({ label: __(''), value: '' })
data.unshift({ label: __(' '), value: ' ' })
return data
},
})
@@ -167,16 +189,19 @@ const updateParticipants = () => {
}
const updateFilters = () => {
if (currentCategory.value) {
filters.value.category = currentCategory.value
} else {
delete filters.value.category
}
if (nameFilter.value) {
filters.value.member_name = ['like', `%${nameFilter.value}%`]
} else {
delete filters.value.member_name
filters.value = {
...(currentCategory.value && {
category: currentCategory.value,
}),
...(nameFilter.value && {
member_name: ['like', `%${nameFilter.value}%`],
}),
...(openToOpportunities.value && {
open_to_opportunities: true,
}),
...(hiring.value && {
hiring: true,
}),
}
}
@@ -185,10 +210,12 @@ const setQueryParams = () => {
let filterKeys = {
category: currentCategory.value,
name: nameFilter.value,
'open-to-opportunities': openToOpportunities.value,
hiring: hiring.value,
}
Object.keys(filterKeys).forEach((key) => {
if (filterKeys[key]) {
if (filterKeys[key] && hasValue(filterKeys[key])) {
queries.set(key, filterKeys[key])
} else {
queries.delete(key)
@@ -201,10 +228,19 @@ const setQueryParams = () => {
)
}
const hasValue = (value) => {
if (typeof value === 'string') {
return value.trim() !== ''
}
return true
}
const setFiltersFromQuery = () => {
let queries = new URLSearchParams(location.search)
nameFilter.value = queries.get('name') || ''
currentCategory.value = queries.get('category') || ''
openToOpportunities.value = queries.get('open-to-opportunities') === 'true'
hiring.value = queries.get('hiring') === 'true'
}
const breadcrumbs = computed(() => [

View File

@@ -75,7 +75,7 @@
v-model="currentCategory"
:options="categories"
:placeholder="__('Category')"
@change="updateCourses()"
@update:modelValue="updateCourses()"
/>
</div>
</div>

View File

@@ -222,12 +222,12 @@ const props = defineProps<{
}>()
const createdCourses = createResource({
url: 'lms.lms.utils.get_created_courses',
url: 'lms.lms.api.get_created_courses',
auto: true,
})
const createdBatches = createResource({
url: 'lms.lms.utils.get_created_batches',
url: 'lms.lms.api.get_created_batches',
auto: true,
})

View File

@@ -74,22 +74,22 @@ const isAdmin = computed(() => {
})
const myLiveClasses = createResource({
url: 'lms.lms.utils.get_my_live_classes',
url: 'lms.lms.api.get_my_live_classes',
auto: !isAdmin.value ? true : false,
})
const adminLiveClasses = createResource({
url: 'lms.lms.utils.get_admin_live_classes',
url: 'lms.lms.api.get_admin_live_classes',
auto: isAdmin.value ? true : false,
})
const adminEvals = createResource({
url: 'lms.lms.utils.get_admin_evals',
url: 'lms.lms.api.get_admin_evals',
auto: isAdmin.value ? true : false,
})
const streakInfo = createResource({
url: 'lms.lms.utils.get_streak_info',
url: 'lms.lms.api.get_streak_info',
auto: true,
})

View File

@@ -161,12 +161,12 @@ const props = defineProps<{
}>()
const myCourses = createResource({
url: 'lms.lms.utils.get_my_courses',
url: 'lms.lms.api.get_my_courses',
auto: true,
})
const myBatches = createResource({
url: 'lms.lms.utils.get_my_batches',
url: 'lms.lms.api.get_my_batches',
auto: true,
})

View File

@@ -196,8 +196,8 @@ const job = createResource({
onSuccess: (data) => {
if (user.data?.name) {
jobApplication.submit()
applicationCount.submit()
}
applicationCount.submit()
},
})

View File

@@ -26,56 +26,72 @@
</header>
<div>
<div
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto p-5"
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto mb-2 p-5"
>
<div class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
{{ __('{0} Open Jobs').format(jobCount) }}
</div>
<div class="flex items-center justify-between space-x-4">
<div class="flex items-center justify-between">
<div class="text-xl font-semibold text-ink-gray-9 md:mb-0">
{{ __('{0} {1} Jobs').format(jobCount, activeTab) }}
</div>
<TabButtons
v-if="tabs.length > 1"
v-model="activeTab"
:buttons="tabs"
class="lg:hidden"
@change="updateJobs"
/>
<FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<Link
v-if="user.data"
doctype="Country"
v-model="country"
:placeholder="__('Country')"
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
/>
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
:placeholder="__('Type')"
@change="updateJobs"
/>
<FormControl
v-model="workMode"
type="select"
:options="workModes"
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
:placeholder="__('Work Mode')"
</div>
<div
class="flex flex-col md:flex-row md:items-center md:space-x-4 space-y-4 md:space-y-0"
>
<TabButtons
v-if="tabs.length > 1"
v-model="activeTab"
:buttons="tabs"
class="hidden lg:block"
@change="updateJobs"
/>
<div class="grid grid-cols-2 gap-4">
<FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="w-full max-w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<Link
v-if="user.data"
doctype="Country"
v-model="country"
:placeholder="__('Country')"
class="w-full"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="w-full"
:placeholder="__('Type')"
@change="updateJobs"
/>
<FormControl
v-model="workMode"
type="select"
:options="workModes"
class="w-full"
:placeholder="__('Work Mode')"
@change="updateJobs"
/>
</div>
</div>
</div>
<div v-if="jobs.data?.length" class="w-full md:w-4/5 mx-auto p-5 pt-0">
@@ -137,6 +153,10 @@ const isModerator = computed(() => {
})
const getClosedJobCount = () => {
if (!user.data?.name) {
return
}
const filters = {
status: 'Closed',
}

View File

@@ -319,7 +319,7 @@
</div>
</div>
<InlineLessonMenu
v-if="lesson.data"
v-if="lesson.data?.name"
v-model="showInlineMenu"
:lesson="lesson.data?.name"
v-model:notes="notes"
@@ -342,6 +342,7 @@ import {
TabButtons,
Tooltip,
usePageMeta,
toast,
} from 'frappe-ui'
import {
computed,
@@ -731,6 +732,7 @@ const updateVideoTime = (video) => {
}
const startTimer = () => {
if (!lesson.data?.membership) return
let timerInterval = setInterval(() => {
timer.value++
if (timer.value == 30) {
@@ -798,6 +800,10 @@ const enrollStudent = () => {
onSuccess() {
window.location.reload()
},
onError(err) {
toast.error(__(err.messages?.[0] || err))
console.error(err)
},
}
)
}

View File

@@ -50,24 +50,68 @@
<div class="mx-auto -mt-10 md:-mt-4 max-w-4xl translate-x-0 px-5">
<div class="flex flex-col md:flex-row items-center">
<div>
<img
v-if="profile.data.user_image"
:src="profile.data.user_image"
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
/>
<UserAvatar
v-else
:user="profile.data"
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
/>
<div class="relative">
<img
v-if="profile.data.user_image"
:src="profile.data.user_image"
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
/>
<div
v-else
class="flex items-center justify-center h-[100px] w-[100px] rounded-full border-4 border-white bg-surface-gray-2 text-3xl font-semibold text-ink-gray-7"
>
{{ profile.data.full_name.charAt(0).toUpperCase() }}
</div>
<Tooltip
v-if="profile.data.open_to"
:text="
profile.data.open_to === 'Opportunities'
? __('Open to Opportunities')
: __('Hiring')
"
placement="right"
>
<div
class="absolute bottom-3 right-1 p-0.5 bg-surface-white rounded-full"
>
<div
class="rounded-full w-fit"
:class="
profile.data.open_to === 'Opportunities'
? 'bg-surface-green-3'
: 'bg-purple-500'
"
>
<BadgeCheckIcon class="text-ink-white size-5" />
</div>
</div>
</Tooltip>
</div>
</div>
<div class="ml-6">
<h2 class="mt-2 text-3xl font-semibold text-ink-gray-9">
<div class="ml-6 mt-5">
<h2 class="text-3xl font-semibold text-ink-gray-9">
{{ profile.data.full_name }}
</h2>
<div class="mt-2 text-base text-ink-gray-7">
<div class="text-base text-ink-gray-7 mt-1">
{{ profile.data.headline }}
</div>
<div class="flex items-center space-x-4 mt-2">
<Twitter
v-if="profile.data.twitter"
class="size-4 text-ink-gray-5 cursor-pointer"
@click="navigateTo(profile.data.twitter)"
/>
<Linkedin
v-if="profile.data.linkedin"
class="size-4 text-ink-gray-5 cursor-pointer"
@click="navigateTo(profile.data.linkedin)"
/>
<Github
v-if="profile.data.github"
class="size-4 text-ink-gray-5 cursor-pointer"
@click="navigateTo(profile.data.github)"
/>
</div>
</div>
<Button
v-if="isSessionUser() && !readOnlyMode"
@@ -81,7 +125,7 @@
</Button>
</div>
<div class="mb-4 mt-6">
<div class="mb-4 mt-10">
<TabButtons
class="inline-block"
:buttons="getTabButtons()"
@@ -104,11 +148,19 @@ import {
call,
createResource,
TabButtons,
Tooltip,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, watch, ref, onMounted, watchEffect } from 'vue'
import { sessionStore } from '@/stores/session'
import { Edit, RefreshCcw } from 'lucide-vue-next'
import {
BadgeCheckIcon,
Edit,
Github,
Linkedin,
RefreshCcw,
Twitter,
} from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router'
import { convertToTitleCase } from '@/utils'
import UserAvatar from '@/components/UserAvatar.vue'
@@ -229,6 +281,10 @@ const reloadUser = () => {
})
}
const navigateTo = (url) => {
window.open(url, '_blank')
}
const breadcrumbs = computed(() => {
let crumbs = [
{

View File

@@ -56,11 +56,13 @@
</template>
<template #body-main>
<div class="w-[250px] text-base">
<img
:src="badge.badge_image"
:alt="badge.badge"
class="bg-surface-gray-2 rounded-t-md h-[200px] mx-auto"
/>
<div class="bg-surface-gray-2 rounded-t-md py-5">
<img
:src="badge.badge_image"
:alt="badge.badge"
class="h-[200px] mx-auto"
/>
</div>
<div class="p-5">
<div class="text-2xl font-semibold mb-2">
{{ badge.badge }}

View File

@@ -11,7 +11,7 @@
</div>
</template>
<template #body-content>
<div v-if="program.data" class="text-base">
<div v-if="program.data" class="text-base text-ink-gray-9">
<div class="bg-surface-blue-2 text-ink-blue-3 p-2 rounded-md leading-5">
<span>
{{
@@ -46,9 +46,9 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
<div
v-for="course in program.data.courses"
class="flex flex-col border p-2 rounded-md h-full"
class="flex flex-col border border-outline-gray-2 p-2 rounded-md h-full"
>
<div class="font-semibold leading-5 mb-2">
<div class="font-semibold text-ink-gray-9 leading-5 mb-2">
{{ course.title }}
</div>
@@ -85,7 +85,7 @@
<div class="flex items-center space-x-1 mt-auto">
<UserAvatar :user="course.instructors[0]" />
<span>
<span class="text-ink-gray-9">
{{ course.instructors[0].full_name }}
</span>
</div>

View File

@@ -17,7 +17,7 @@
@click="openDetails(program.name, category)"
class="border rounded-md p-3 hover:border-outline-gray-3 cursor-pointer"
>
<div class="text-lg font-semibold mb-2">
<div class="text-lg font-semibold text-ink-gray-9 mb-2">
{{ program.name }}
</div>
@@ -40,7 +40,7 @@
<div v-if="Object.keys(program).includes('progress')" class="mt-5">
<ProgressBar :progress="program.progress" />
<div class="text-sm mt-1">
<div class="text-sm text-ink-gray-7 mt-1">
{{ Math.ceil(program.progress) }}% {{ __('completed') }}
</div>
</div>

View File

@@ -0,0 +1,254 @@
<template>
<header
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="[{ label: __('Search') }]" />
</header>
<div class="w-4/6 mx-auto py-5">
<div class="px-2.5">
<TextInput
ref="searchInput"
class="flex-1"
placeholder="Search for a keyword or phrase and press enter"
autocomplete="off"
:model-value="query"
@update:model-value="updateQuery"
@keydown.enter="() => submit()"
>
<template #prefix>
<Search class="w-4 text-ink-gray-5" />
</template>
<template #suffix>
<div class="flex items-center">
<button
v-if="query"
@click="clearSearch"
class="p-1 size-6 grid place-content-center focus:outline-none focus:ring focus:ring-outline-gray-3 rounded"
>
<X class="w-4 text-ink-gray-7" />
</button>
</div>
</template>
</TextInput>
<div
v-if="query && searchResults.length"
class="text-sm text-ink-gray-5 mt-2"
>
{{ searchResults.length }}
{{ searchResults.length === 1 ? __('match') : __('matches') }}
</div>
<div v-else-if="queryChanged" class="text-sm text-ink-gray-5 mt-2">
{{ __('Press enter to search') }}
</div>
<div
v-else-if="query && !searchResults.length"
class="text-sm text-ink-gray-5 mt-2"
>
{{ __('No results found') }}
</div>
</div>
<div class="mt-5">
<div v-if="searchResults.length" class="">
<div
v-for="(result, index) in searchResults"
@click="navigate(result)"
class="rounded-md cursor-pointer hover:bg-surface-gray-2 px-2"
>
<div
class="flex space-x-2 py-3"
:class="{
'border-b': index !== searchResults.length - 1,
}"
>
<Tooltip :text="result.author_info.full_name">
<Avatar
:label="result.author_info.full_name"
:image="result.author_info.user_image"
size="md"
/>
</Tooltip>
<div class="space-y-1 w-full">
<div class="flex items-center">
<div
class="font-medium text-ink-gray-9"
v-html="result.title"
></div>
<div class="text-sm text-ink-gray-5 ml-2">
{{ getDocTypeTitle(result.doctype) }}
</div>
<div
v-if="
result.published_on ||
result.start_date ||
result.creation ||
result.modified
"
class="ml-auto text-sm text-ink-gray-5"
>
{{
dayjs(
result.published_on ||
result.start_date ||
result.creation ||
result.modified
).format('DD MMM YYYY')
}}
</div>
</div>
<div
class="leading-5 text-ink-gray-7"
v-html="result.content"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
Avatar,
Breadcrumbs,
createResource,
debounce,
TextInput,
Tooltip,
usePageMeta,
} from 'frappe-ui'
import { inject, onMounted, ref, watch } from 'vue'
import { Search, X } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { useRouter, useRoute } from 'vue-router'
const query = ref('')
const searchInput = ref<HTMLInputElement | null>(null)
const searchResults = ref<Array<any>>([])
const { brand } = sessionStore()
const router = useRouter()
const route = useRoute()
const queryChanged = ref(false)
const dayjs = inject<any>('$dayjs')
onMounted(() => {
if (router.currentRoute.value.query.q) {
query.value = router.currentRoute.value.query.q as string
submit()
}
})
const updateQuery = (value: string) => {
query.value = value
router.replace({ query: value ? { q: value } : {} })
}
const submit = debounce(() => {
if (query.value.length > 2) {
search.reload()
}
}, 500)
const search = createResource({
url: 'lms.command_palette.search_sqlite',
makeParams: () => ({
query: query.value,
}),
onSuccess() {
generateSearchResults()
},
})
const generateSearchResults = () => {
searchResults.value = []
if (search.data) {
queryChanged.value = false
search.data.forEach((group: any) => {
group.items.forEach((item: any) => {
searchResults.value.push(item)
})
})
sortResults()
}
}
const sortResults = () => {
searchResults.value.sort((a, b) => {
const dateA = new Date(
a.published_on || a.start_date || a.creation || a.modified
).getTime()
const dateB = new Date(
b.published_on || b.start_date || b.creation || b.modified
).getTime()
return dateB - dateA
})
}
const navigate = (result: any) => {
if (result.doctype == 'LMS Course') {
router.push({
name: 'CourseDetail',
params: {
courseName: result.name,
},
})
} else if (result.doctype == 'LMS Batch') {
router.push({
name: 'BatchDetail',
params: {
batchName: result.name,
},
})
} else if (result.doctype == 'Job Opportunity') {
router.push({
name: 'JobDetail',
params: {
job: result.name,
},
})
}
}
watch(query, () => {
if (query.value && query.value != search.params?.query) {
queryChanged.value = true
} else if (!query.value) {
queryChanged.value = false
searchResults.value = []
}
})
watch(
() => route.query.q,
(newQ) => {
if (newQ && newQ !== query.value) {
query.value = newQ as string
submit()
}
}
)
const getDocTypeTitle = (doctype: string) => {
if (doctype === 'LMS Course') {
return __('Course')
} else if (doctype === 'LMS Batch') {
return __('Batch')
} else if (doctype === 'Job Opportunity') {
return __('Job')
} else {
return doctype
}
}
const clearSearch = () => {
query.value = ''
updateQuery('')
}
usePageMeta(() => {
return {
title: __('Search'),
icon: brand.favicon,
}
})
</script>