Files
enlight-lms/frontend/src/pages/Profile.vue
2026-02-06 12:53:17 +05:30

318 lines
7.5 KiB
Vue

<template>
<NoPermission v-if="!$user.data" />
<div v-else-if="profile.data">
<header
class="sticky group top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<Button v-if="isSessionUser()" class="invisible group-hover:visible">
<template #icon>
<RefreshCcw
class="w-4 h-4 stroke-1.5 text-ink-gray-7"
@click="reloadUser()"
/>
</template>
</Button>
</header>
<div class="group relative h-[130px] w-full">
<img
v-if="profile.data.cover_image"
:src="profile.data.cover_image"
class="h-[130px] w-full object-cover object-center"
/>
<div
v-else
:class="{ 'bg-surface-gray-2': !profile.data.cover_image }"
class="h-[130px] w-full"
></div>
<div
class="absolute bottom-[30%] md:bottom-0 left-[50%] mb-4 flex -translate-x-1/2 space-x-2 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100"
v-if="isSessionUser()"
>
<EditCoverImage
@select="(imageUrl) => coverImage.submit({ url: imageUrl })"
>
<template v-slot="{ togglePopover }">
<Button
v-if="!readOnlyMode"
variant="outline"
@click="togglePopover()"
>
<template #prefix>
<Edit class="w-4 h-4 stroke-1.5 text-ink-gray-7" />
</template>
{{ __('Edit') }}
</Button>
</template>
</EditCoverImage>
</div>
</div>
<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>
<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 === 'Work'
? __('Open to Work')
: __('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 === 'Work'
? 'bg-surface-green-3'
: 'bg-purple-500'
"
>
<BadgeCheckIcon class="text-ink-white size-5" />
</div>
</div>
</Tooltip>
</div>
</div>
<div class="ml-6 mt-5">
<h2 class="text-3xl font-semibold text-ink-gray-9">
{{ profile.data.full_name }}
</h2>
<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"
class="mt-3 sm:mt-0 md:ml-auto"
@click="editProfile()"
>
<template #prefix>
<Edit class="w-4 h-4 stroke-1.5 text-ink-gray-7" />
</template>
{{ __('Edit Profile') }}
</Button>
</div>
<div class="mb-4 mt-10">
<TabButtons
class="inline-block"
:buttons="getTabButtons()"
v-model="activeTab"
/>
</div>
<router-view :profile="profile" :key="profile.data?.name" />
</div>
</div>
<EditProfile
v-model="showProfileModal"
v-model:reloadProfile="profile"
:profile="profile"
/>
</template>
<script setup>
import {
Breadcrumbs,
Button,
call,
createResource,
TabButtons,
Tooltip,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, watch, ref, onMounted, watchEffect } from 'vue'
import { sessionStore } from '@/stores/session'
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'
import NoPermission from '@/components/NoPermission.vue'
import EditProfile from '@/components/Modals/EditProfile.vue'
import EditCoverImage from '@/components/Modals/EditCoverImage.vue'
const { user, brand } = sessionStore()
const $user = inject('$user')
const route = useRoute()
const router = useRouter()
const activeTab = ref('')
const showProfileModal = ref(false)
const readOnlyMode = window.read_only_mode
const props = defineProps({
username: {
type: String,
required: true,
},
})
onMounted(() => {
if ($user.data) profile.reload()
setActiveTab()
})
const profile = createResource({
url: 'lms.lms.api.get_profile_details',
makeParams() {
return {
username: props.username,
}
},
})
const coverImage = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
return {
doctype: 'User',
name: profile.data?.name,
fieldname: 'cover_image',
value: values.url,
}
},
onSuccess() {
profile.reload()
},
})
const setActiveTab = () => {
let fragments = route.path.split('/')
let sections = ['certificates', 'roles', 'slots', 'schedule']
sections.forEach((section) => {
if (fragments.includes(section)) {
activeTab.value = convertToTitleCase(section)
}
})
if (!activeTab.value) activeTab.value = 'About'
}
watchEffect(() => {
if (activeTab.value) {
let route = {
About: { name: 'ProfileAbout' },
Certificates: { name: 'ProfileCertificates' },
Roles: { name: 'ProfileRoles' },
Slots: { name: 'ProfileEvaluator' },
Schedule: { name: 'ProfileEvaluationSchedule' },
}[activeTab.value]
router.push(route)
}
})
watch(
() => props.username,
() => {
profile.reload()
}
)
const editProfile = () => {
showProfileModal.value = true
}
const isSessionUser = () => {
return $user.data?.email === profile.data?.name
}
const currentUserHasHigherAccess = () => {
return $user.data?.is_evaluator || $user.data?.is_moderator
}
const isEvaluatorOrModerator = () => {
return (
profile.data?.roles?.includes('Batch Evaluator') ||
profile.data?.roles?.includes('Moderator')
)
}
const getTabButtons = () => {
let buttons = [
{ label: __('About'), value: 'About' },
{ label: __('Certificates'), value: 'Certificates' },
]
if ($user.data?.is_moderator) {
buttons.push({ label: __('Roles'), value: 'Roles' })
}
if (currentUserHasHigherAccess() && isEvaluatorOrModerator()) {
buttons.push({ label: __('Slots'), value: 'Slots' })
buttons.push({ label: __('Schedule'), value: 'Schedule' })
}
return buttons
}
const reloadUser = () => {
call('frappe.sessions.clear').then(() => {
$user.reload().then(() => {
profile.reload()
})
})
}
const navigateTo = (url) => {
window.open(url, '_blank')
}
const breadcrumbs = computed(() => {
let crumbs = [
{
label: __('People'),
},
{
label: profile.data?.full_name,
route: {
name: 'Profile',
params: {
username: user.doc?.username,
},
},
},
]
return crumbs
})
usePageMeta(() => {
return {
title: profile.data?.full_name,
icon: brand.favicon,
}
})
</script>