Merge branch 'develop' into fix/quiz-enter
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
v-model="currentCategory"
|
||||
:options="categories"
|
||||
:placeholder="__('Category')"
|
||||
@change="updateBatches()"
|
||||
@update:modelValue="updateBatches()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
v-model="currentCategory"
|
||||
:options="categories"
|
||||
:placeholder="__('Category')"
|
||||
@change="updateCourses()"
|
||||
@update:modelValue="updateCourses()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -196,8 +196,8 @@ const job = createResource({
|
||||
onSuccess: (data) => {
|
||||
if (user.data?.name) {
|
||||
jobApplication.submit()
|
||||
applicationCount.submit()
|
||||
}
|
||||
applicationCount.submit()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
254
frontend/src/pages/Search/Search.vue
Normal file
254
frontend/src/pages/Search/Search.vue
Normal 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>
|
||||
Reference in New Issue
Block a user