initial commit
This commit is contained in:
87
resources/js/@layouts/components/TransitionExpand.vue
Normal file
87
resources/js/@layouts/components/TransitionExpand.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<!-- Thanks: https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/ -->
|
||||
|
||||
<script>
|
||||
import { Transition } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TransitionExpand',
|
||||
setup(_, { slots }) {
|
||||
const onEnter = element => {
|
||||
const width = getComputedStyle(element).width
|
||||
|
||||
element.style.width = width
|
||||
element.style.position = 'absolute'
|
||||
element.style.visibility = 'hidden'
|
||||
element.style.height = 'auto'
|
||||
|
||||
const height = getComputedStyle(element).height
|
||||
|
||||
element.style.width = ''
|
||||
element.style.position = ''
|
||||
element.style.visibility = ''
|
||||
element.style.height = '0px'
|
||||
|
||||
// Force repaint to make sure the
|
||||
// animation is triggered correctly.
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
getComputedStyle(element).height
|
||||
|
||||
// Trigger the animation.
|
||||
// We use `requestAnimationFrame` because we need
|
||||
// to make sure the browser has finished
|
||||
// painting after setting the `height`
|
||||
// to `0` in the line above.
|
||||
requestAnimationFrame(() => {
|
||||
element.style.height = height
|
||||
})
|
||||
}
|
||||
|
||||
const onAfterEnter = element => {
|
||||
element.style.height = 'auto'
|
||||
}
|
||||
|
||||
const onLeave = element => {
|
||||
const height = getComputedStyle(element).height
|
||||
|
||||
element.style.height = height
|
||||
|
||||
// Force repaint to make sure the
|
||||
// animation is triggered correctly.
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
getComputedStyle(element).height
|
||||
requestAnimationFrame(() => {
|
||||
element.style.height = '0px'
|
||||
})
|
||||
}
|
||||
|
||||
return () => h(h(Transition), {
|
||||
name: 'expand',
|
||||
onEnter,
|
||||
onAfterEnter,
|
||||
onLeave,
|
||||
}, () => slots.default?.())
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
overflow: hidden;
|
||||
transition: block-size var(--expand-transition-duration, 0.25s) ease;
|
||||
}
|
||||
|
||||
.expand-enter-from,
|
||||
.expand-leave-to {
|
||||
block-size: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
transform: translateZ(0);
|
||||
will-change: block-size;
|
||||
}
|
||||
</style>
|
188
resources/js/@layouts/components/VerticalNav.vue
Normal file
188
resources/js/@layouts/components/VerticalNav.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<script setup>
|
||||
import axios from '@axios';
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
||||
import { useDisplay } from 'vuetify';
|
||||
const currentUser = ref(localStorage.getItem('user_role'));
|
||||
const seetingPlanLogo = ref();
|
||||
const props = defineProps({
|
||||
tag: {
|
||||
type: [
|
||||
String,
|
||||
null,
|
||||
],
|
||||
required: false,
|
||||
default: 'aside',
|
||||
},
|
||||
isOverlayNavActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
toggleIsOverlayNavActive: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { mdAndDown } = useDisplay()
|
||||
const refNav = ref()
|
||||
const route = useRoute()
|
||||
|
||||
watch(() => route.path, () => {
|
||||
props.toggleIsOverlayNavActive(false)
|
||||
})
|
||||
onMounted(async () => {
|
||||
|
||||
|
||||
let setting = await axios.post('/api/settings', {})
|
||||
// console.log(setting.data)
|
||||
seetingPlanLogo.value = '/assets/logo/' + setting.data.logo
|
||||
})
|
||||
const isVerticalNavScrolled = ref(false)
|
||||
const updateIsVerticalNavScrolled = val => isVerticalNavScrolled.value = val
|
||||
|
||||
const handleNavScroll = evt => {
|
||||
isVerticalNavScrolled.value = evt.target.scrollTop > 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Component :is="props.tag" ref="refNav" class="layout-vertical-nav" :class="[
|
||||
{
|
||||
'visible': isOverlayNavActive,
|
||||
'scrolled': isVerticalNavScrolled,
|
||||
'overlay-nav': mdAndDown,
|
||||
},
|
||||
]">
|
||||
<!-- 👉 Header -->
|
||||
<div class="nav-header px-0 py-3">
|
||||
<slot name="nav-header">
|
||||
<RouterLink to="/provider/dashboard" v-if="currentUser == 'agent'"
|
||||
class="app-logo d-flex align-center gap-x-3 app-title-wrapper">
|
||||
<!-- <div class="d-flex " /> -->
|
||||
|
||||
|
||||
<h1 class="leading-normal text-primary">
|
||||
<VImg :src="seetingPlanLogo" width="150" height="50" class="logo-img" />
|
||||
</h1>
|
||||
</RouterLink>
|
||||
<RouterLink to="/" v-if="currentUser == 'patient'"
|
||||
class="app-logo d-flex align-center gap-x-3 app-title-wrapper">
|
||||
<!-- <div class="d-flex " /> -->
|
||||
|
||||
<h1 class="leading-normal text-primary" style="margin-right: 27px;">
|
||||
<VImg :src="seetingPlanLogo" width="150" height="50" class="logo-img" />
|
||||
</h1>
|
||||
</RouterLink>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="before-nav-items">
|
||||
<div class="vertical-nav-items-shadow" />
|
||||
</slot>
|
||||
<slot name="nav-items" :update-is-vertical-nav-scrolled="updateIsVerticalNavScrolled">
|
||||
<PerfectScrollbar tag="ul" class="nav-items" :options="{ wheelPropagation: false }"
|
||||
@ps-scroll-y="handleNavScroll">
|
||||
<slot />
|
||||
</PerfectScrollbar>
|
||||
</slot>
|
||||
|
||||
<slot name="after-nav-items" />
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@layouts/styles/mixins";
|
||||
|
||||
.logo-img {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
|
||||
.nav-header.px-0.py-3 {
|
||||
margin: 1px 45px !important;
|
||||
}
|
||||
|
||||
.layout-nav-type-vertical .layout-vertical-nav .nav-link .router-link-exact-active:after,
|
||||
.layout-nav-type-vertical .layout-vertical-nav .nav-group.active:not(.nav-group .nav-group)>:first-child:after {
|
||||
position: absolute;
|
||||
background-color: rgb(var(--v-theme-yellow-theme-button)) !important;
|
||||
block-size: 2.625rem;
|
||||
border-end-start-radius: .375rem;
|
||||
border-start-start-radius: .375rem;
|
||||
content: "";
|
||||
inline-size: .25rem;
|
||||
inset-inline-end: -1rem;
|
||||
|
||||
}
|
||||
|
||||
.nav-header {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
// 👉 Vertical Nav
|
||||
.layout-vertical-nav {
|
||||
position: fixed;
|
||||
z-index: variables.$layout-vertical-nav-z-index;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
block-size: 100%;
|
||||
inline-size: variables.$layout-vertical-nav-width;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
transition: transform 0.25s ease-in-out, inline-size 0.25s ease-in-out, box-shadow 0.25s ease-in-out;
|
||||
will-change: transform, inline-size;
|
||||
background-color: rgb(var(--v-theme-yellow)) !important;
|
||||
|
||||
.nav-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.header-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.app-title-wrapper {
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
block-size: 100%;
|
||||
padding-left: 0px;
|
||||
// ℹ️ We no loner needs this overflow styles as perfect scrollbar applies it
|
||||
// overflow-x: hidden;
|
||||
|
||||
// // ℹ️ We used `overflow-y` instead of `overflow` to mitigate overflow x. Revert back if any issue found.
|
||||
// overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-item-title {
|
||||
overflow: hidden;
|
||||
margin-inline-end: auto;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 👉 Collapsed
|
||||
.layout-vertical-nav-collapsed & {
|
||||
&:not(.hovered) {
|
||||
inline-size: variables.$layout-vertical-nav-collapsed-width;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Overlay nav
|
||||
&.overlay-nav {
|
||||
&:not(.visible) {
|
||||
transform: translateX(-#{variables.$layout-vertical-nav-width});
|
||||
|
||||
@include mixins.rtl {
|
||||
transform: translateX(variables.$layout-vertical-nav-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
184
resources/js/@layouts/components/VerticalNavLayout.vue
Normal file
184
resources/js/@layouts/components/VerticalNavLayout.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<script>
|
||||
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
export default defineComponent({
|
||||
setup(props, { slots }) {
|
||||
const isOverlayNavActive = ref(false)
|
||||
const isLayoutOverlayVisible = ref(false)
|
||||
const toggleIsOverlayNavActive = useToggle(isOverlayNavActive)
|
||||
const route = useRoute()
|
||||
const { mdAndDown } = useDisplay()
|
||||
|
||||
|
||||
// ℹ️ This is alternative to below two commented watcher
|
||||
// We want to show overlay if overlay nav is visible and want to hide overlay if overlay is hidden and vice versa.
|
||||
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
|
||||
|
||||
return () => {
|
||||
// 👉 Vertical nav
|
||||
const verticalNav = h(VerticalNav, { isOverlayNavActive: isOverlayNavActive.value, toggleIsOverlayNavActive }, {
|
||||
'nav-header': () => slots['vertical-nav-header']?.(),
|
||||
'before-nav-items': () => slots['before-vertical-nav-items']?.(),
|
||||
'default': () => slots['vertical-nav-content']?.(),
|
||||
'after-nav-items': () => slots['after-vertical-nav-items']?.(),
|
||||
})
|
||||
|
||||
|
||||
// 👉 Navbar
|
||||
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
|
||||
h('div', { class: 'navbar-content-container' }, slots.navbar?.({
|
||||
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
|
||||
})),
|
||||
])
|
||||
|
||||
const main = h('main', { class: 'layout-page-content' }, h('div', { class: 'page-content-container' }, slots.default?.()))
|
||||
|
||||
|
||||
// 👉 Footer
|
||||
const footer = h('footer', { class: 'layout-footer' }, [
|
||||
h('div', { class: 'footer-content-container' }, slots.footer?.()),
|
||||
])
|
||||
|
||||
|
||||
// 👉 Overlay
|
||||
const layoutOverlay = h('div', {
|
||||
class: ['layout-overlay', { visible: isLayoutOverlayVisible.value }],
|
||||
onClick: () => { isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value },
|
||||
})
|
||||
|
||||
return h('div', {
|
||||
class: [
|
||||
'layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid',
|
||||
mdAndDown.value && 'layout-overlay-nav',
|
||||
route.meta.layoutWrapperClasses,
|
||||
],
|
||||
}, [
|
||||
verticalNav,
|
||||
h('div', { class: 'layout-content-wrapper' }, [
|
||||
navbar,
|
||||
main,
|
||||
footer,
|
||||
]),
|
||||
layoutOverlay,
|
||||
])
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@layouts/styles/placeholders";
|
||||
@use "@layouts/styles/mixins";
|
||||
|
||||
.bg-primary {
|
||||
background-color: rgb(var(--v-theme-yellow-theme-button)) !important;
|
||||
}
|
||||
|
||||
.layout-nav-type-vertical .layout-vertical-nav .nav-link>.router-link-exact-active {
|
||||
--v-activated-opacity: 0.16;
|
||||
background-color: rgb(var(--v-theme-yellow-theme-button));
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
|
||||
block-size: 100%;
|
||||
|
||||
.layout-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-block-size: calc(var(--vh, 1vh) * 100);
|
||||
transition: padding-inline-start 0.2s ease-in-out;
|
||||
will-change: padding-inline-start;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
|
||||
|
||||
.navbar-content-container {
|
||||
block-size: variables.$layout-vertical-nav-navbar-height;
|
||||
}
|
||||
|
||||
@at-root {
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
.layout-navbar {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
@include mixins.boxed-content;
|
||||
}
|
||||
|
||||
@else {
|
||||
.navbar-content-container {
|
||||
@include mixins.boxed-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.layout-navbar-sticky .layout-navbar {
|
||||
@extend %layout-navbar-sticky;
|
||||
}
|
||||
|
||||
&.layout-navbar-hidden .layout-navbar {
|
||||
@extend %layout-navbar-hidden;
|
||||
}
|
||||
|
||||
// 👉 Footer
|
||||
.layout-footer {
|
||||
@include mixins.boxed-content;
|
||||
}
|
||||
|
||||
// 👉 Layout overlay
|
||||
.layout-overlay {
|
||||
position: fixed;
|
||||
z-index: variables.$layout-overlay-z-index;
|
||||
background-color: rgb(0 0 0 / 60%);
|
||||
cursor: pointer;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
will-change: transform;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.layout-overlay-nav) .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-width;
|
||||
}
|
||||
|
||||
// Adjust right column pl when vertical nav is collapsed
|
||||
&.layout-vertical-nav-collapsed .layout-content-wrapper {
|
||||
padding-inline-start: variables.$layout-vertical-nav-collapsed-width;
|
||||
}
|
||||
|
||||
// 👉 Content height fixed
|
||||
&.layout-content-height-fixed {
|
||||
.layout-content-wrapper {
|
||||
max-block-size: calc(var(--vh) * 100);
|
||||
}
|
||||
|
||||
.layout-page-content {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.page-content-container {
|
||||
inline-size: 100%;
|
||||
|
||||
> :first-child {
|
||||
max-block-size: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
82
resources/js/@layouts/components/VerticalNavLink.vue
Normal file
82
resources/js/@layouts/components/VerticalNavLink.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isDropdownOpen = ref(false);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isDropdownOpen.value = !isDropdownOpen.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="nav-link" :class="{ disabled: item.disabled, open: isDropdownOpen }">
|
||||
<Component :is="item.to ? 'RouterLink' : 'a'" :to="item.to" :href="item.href"
|
||||
@click.prevent="item.children && toggleDropdown()">
|
||||
<VIcon :icon="item.icon" class="nav-item-icon" />
|
||||
<span class="nav-item-title">{{ item.title }}</span>
|
||||
<VIcon v-if="item.children" :icon="isDropdownOpen ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
class="dropdown-icon" />
|
||||
</Component>
|
||||
|
||||
<transition name="dropdown-transition">
|
||||
<ul v-if="item.children && isDropdownOpen" class="dropdown-menu1">
|
||||
<li v-for="(child, index) in item.children" :key="index">
|
||||
<VerticalNavLink :item="child">
|
||||
<template #default="{ item }">
|
||||
<VIcon :icon="item.icon" class="nav-item-icon" />
|
||||
<span class="nav-item-title">{{ item.title }}</span>
|
||||
</template>
|
||||
</VerticalNavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-vertical-nav {
|
||||
.nav-link a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: rgb(var(--v-theme-yellow-theme-button)) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu1 {
|
||||
padding-left: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
margin-left: auto;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.nav-link.open .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-transition-enter-active,
|
||||
.dropdown-transition-leave-active {
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
max-height: 200px;
|
||||
/* Adjust this value based on the maximum height of your dropdown menu */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-transition-enter-from,
|
||||
.dropdown-transition-leave-to {
|
||||
max-height: 0;
|
||||
}
|
||||
</style>
|
21
resources/js/@layouts/components/VerticalNavSectionTitle.vue
Normal file
21
resources/js/@layouts/components/VerticalNavSectionTitle.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="nav-section-title">
|
||||
<div class="title-wrapper">
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<span
|
||||
class="title-text"
|
||||
v-text="item.heading"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
Reference in New Issue
Block a user