initial commit
This commit is contained in:
28
resources/js/@core/components/MoreBtn.vue
Normal file
28
resources/js/@core/components/MoreBtn.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
menuList: {
|
||||
type: Array,
|
||||
required: false,
|
||||
},
|
||||
itemProps: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn>
|
||||
<VIcon icon="bx-dots-vertical" />
|
||||
|
||||
<VMenu
|
||||
v-if="props.menuList"
|
||||
activator="parent"
|
||||
>
|
||||
<VList
|
||||
:items="props.menuList"
|
||||
:item-props="props.itemProps"
|
||||
/>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
177
resources/js/@core/components/Notifications.vue
Normal file
177
resources/js/@core/components/Notifications.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
||||
|
||||
const props = defineProps({
|
||||
notifications: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isPopup: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
badgeProps: {
|
||||
type: null,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
location: {
|
||||
type: null,
|
||||
required: false,
|
||||
default: 'bottom end',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'read',
|
||||
'unread',
|
||||
'remove',
|
||||
'click:notification',
|
||||
])
|
||||
|
||||
const isAllMarkRead = computed(() => props.notifications.some(item => item.isSeen === false))
|
||||
|
||||
const markAllReadOrUnread = () => {
|
||||
const allNotificationsIds = props.notifications.map(item => item.id)
|
||||
if (!isAllMarkRead.value)
|
||||
emit('unread', allNotificationsIds)
|
||||
else
|
||||
emit('read', allNotificationsIds)
|
||||
}
|
||||
|
||||
const totalUnseenNotifications = computed(() => {
|
||||
return props.notifications.filter(item => item.isSeen === false).length
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn id="notification-btn">
|
||||
<VBadge v-bind="props.badgeProps" :model-value="props.notifications.some(n => !n.isSeen)" color="error"
|
||||
:content="totalUnseenNotifications" class="notification-badge">
|
||||
<VIcon size="26" icon="tabler-bell" />
|
||||
</VBadge>
|
||||
|
||||
<VMenu activator="parent" width="380px" :location="props.location" offset="14px"
|
||||
:close-on-content-click="false">
|
||||
<VCard class="d-flex flex-column">
|
||||
<!-- 👉 Header -->
|
||||
<VCardItem class="notification-section">
|
||||
<VCardTitle class="text-lg">
|
||||
Notifications
|
||||
</VCardTitle>
|
||||
|
||||
<template #append>
|
||||
<IconBtn v-show="props.notifications.length" @click="markAllReadOrUnread">
|
||||
<VIcon :icon="!isAllMarkRead ? 'tabler-mail' : 'tabler-mail-opened'" />
|
||||
|
||||
<VTooltip activator="parent" location="start">
|
||||
{{ !isAllMarkRead ? 'Mark all as unread' : 'Mark all as read' }}
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- 👉 Notifications list -->
|
||||
<PerfectScrollbar :options="{ wheelPropagation: false }" style="max-block-size: 23.75rem;">
|
||||
<VList class="notification-list rounded-0 py-0">
|
||||
<template v-for="(notification, index) in props.notifications" :key="notification.title">
|
||||
<VDivider v-if="index > 0" class="m-0" />
|
||||
<VListItem link lines="one" min-height="66px" class="list-item-hover-class"
|
||||
@click="$emit('click:notification', notification)">
|
||||
<!-- Slot: Prepend -->
|
||||
<!-- Handles Avatar: Image, Icon, Text -->
|
||||
<template #prepend>
|
||||
<VListItemAction>
|
||||
<!-- <VAvatar size="40"
|
||||
:color="notification.color && notification.icon ? notification.color : undefined"
|
||||
:image="notification.img || undefined"
|
||||
:icon="notification.icon || undefined">
|
||||
<span v-if="notification.text">{{ avatarText(notification.text) }}</span>
|
||||
</VAvatar> -->
|
||||
<VIcon icon="tabler-bell" size="small"></VIcon>
|
||||
</VListItemAction>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>{{ notification.title }}</VListItemTitle>
|
||||
<VListItemSubtitle>{{ notification.subtitle }}</VListItemSubtitle>
|
||||
<span class="text-xs text-disabled">{{ notification.time }}</span>
|
||||
|
||||
<!-- Slot: Append -->
|
||||
<template #append>
|
||||
<div class="d-flex flex-column align-center gap-4">
|
||||
<!-- <VBadge dot :color="!notification.isSeen ? 'primary' : '#a8aaae'"
|
||||
:class="`${notification.isSeen ? 'visible-in-hover' : ''} ms-1`"
|
||||
@click.stop="$emit(notification.isSeen ? 'unread' : 'read', [notification.id])" /> -->
|
||||
|
||||
<!-- <div style="block-size: 28px; inline-size: 28px;">
|
||||
<IconBtn size="small" class="visible-in-hover"
|
||||
@click="$emit('remove', notification.id)">
|
||||
<VIcon size="20" icon="tabler-x" />
|
||||
</IconBtn>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
|
||||
<VListItem v-show="!props.notifications.length" class="text-center text-medium-emphasis"
|
||||
style="block-size: 56px;">
|
||||
<VListItemTitle>No Notification Found!</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</PerfectScrollbar>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<!-- 👉 Footer -->
|
||||
<!-- <VCardActions v-show="props.notifications.length" class="notification-footer">
|
||||
<VBtn block>
|
||||
View All Notifications
|
||||
</VBtn>
|
||||
</VCardActions> -->
|
||||
</VCard>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.notification-section {
|
||||
padding: 14px !important;
|
||||
}
|
||||
|
||||
.notification-footer {
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.list-item-hover-class {
|
||||
.visible-in-hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.visible-in-hover {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-list.v-list {
|
||||
.v-list-item {
|
||||
border-radius: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Badge Style Override for Notification Badge
|
||||
.notification-badge {
|
||||
.v-badge__badge {
|
||||
/* stylelint-disable-next-line liberty/use-logical-spec */
|
||||
min-width: 18px;
|
||||
padding: 0;
|
||||
block-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
43
resources/js/@core/components/ThemeSwitcher.vue
Normal file
43
resources/js/@core/components/ThemeSwitcher.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { useTheme } from 'vuetify'
|
||||
|
||||
const props = defineProps({
|
||||
themes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
name: themeName,
|
||||
global: globalTheme,
|
||||
} = useTheme()
|
||||
|
||||
const {
|
||||
state: currentThemeName,
|
||||
next: getNextThemeName,
|
||||
index: currentThemeIndex,
|
||||
} = useCycleList(props.themes.map(t => t.name), { initialValue: themeName })
|
||||
|
||||
const changeTheme = () => {
|
||||
globalTheme.name.value = getNextThemeName()
|
||||
}
|
||||
|
||||
// Update icon if theme is changed from other sources
|
||||
watch(() => globalTheme.name.value, val => {
|
||||
currentThemeName.value = val
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn @click="changeTheme">
|
||||
<VIcon :icon="props.themes[currentThemeIndex].icon" />
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
open-delay="1000"
|
||||
scroll-strategy="close"
|
||||
>
|
||||
<span class="text-capitalize">{{ currentThemeName }}</span>
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
</template>
|
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
import { kFormatter } from '@core/utils/formatters'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
stats: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
change: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center">
|
||||
<VAvatar
|
||||
size="44"
|
||||
rounded
|
||||
:color="props.color"
|
||||
variant="tonal"
|
||||
class="me-4"
|
||||
>
|
||||
<VIcon
|
||||
:icon="props.icon"
|
||||
size="30"
|
||||
/>
|
||||
</VAvatar>
|
||||
|
||||
<div>
|
||||
<span class="text-caption">{{ props.title }}</span>
|
||||
<div class="d-flex align-center flex-wrap">
|
||||
<span class="text-h6 font-weight-semibold">{{ kFormatter(props.stats) }}</span>
|
||||
<div
|
||||
v-if="props.change"
|
||||
:class="`${isPositive ? 'text-success' : 'text-error'} mt-1`"
|
||||
>
|
||||
<VIcon :icon="isPositive ? 'bx-chevron-up' : 'bx-chevron-down'" />
|
||||
<span class="text-caption font-weight-semibold">{{ Math.abs(props.change) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
@@ -0,0 +1,95 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subcontent: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
change: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center" style="padding: 0;">
|
||||
<img width="100%" :src="props.image" alt="image">
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<p class="mb-1 mt-3">
|
||||
{{ props.title }}
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<b>{{ props.content }}</b>
|
||||
</p>
|
||||
</VCardText>
|
||||
<VCardText class="bg-secondary">
|
||||
<p class="mb-1 pt-2">
|
||||
|
||||
Introducing FemExcelle Hormone Replacement Therapy (HRT) for women, now live! Explore our new "refer and earn"
|
||||
initiative: Refer a woman to FemExcelle, and she'll enjoy a 75% discount on the initial cost. Plus, for each
|
||||
referral, you'll receive a $79 credit towards your HGH account. Simply click and share the link below to
|
||||
begin referring today. Visit FemExcelle for more details
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
<VCard class="mt-2">
|
||||
<VCardText class="d-flex align-center" style="padding: 0;">
|
||||
<img width="100%" :src="props.image" alt="image">
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<p class="mb-1 mt-3">
|
||||
<b>Recommend a Friend for Hormone Replacement Therapy Transform a Life</b>
|
||||
</p>
|
||||
<p class="mb-1 mt-2">
|
||||
They'll receive a $70 discount on their initial purchase, and you'll receive all the gratitude!
|
||||
</p>
|
||||
<p class="mt-2 mb-0">
|
||||
<RouterLink to="/overview">
|
||||
<VBtn class="text-capitalize">Recommend a Friend</VBtn>
|
||||
</RouterLink>
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<VCard class="mt-2">
|
||||
<VCardText class="d-flex align-center" style="padding: 0;">
|
||||
<img width="100%" :src="props.image" alt="image">
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<p class="mb-1 mt-3">
|
||||
<b>Regain Control of Your Life with Erectile Dysfunction Treatment</b>
|
||||
</p>
|
||||
<p class="mb-1 mt-2">
|
||||
Choices crafted to ensure men are prepared when the moment arrives.
|
||||
</p>
|
||||
<p>
|
||||
Now providing treatment in every state across the nation, we are thrilled to assist individuals in reclaiming
|
||||
their lives! Begin your journey today by completing our complimentary hormone assessment.
|
||||
</p>
|
||||
<p class="mt-2 mb-0">
|
||||
<RouterLink to="/overview">
|
||||
<VBtn class="text-capitalize"> Read More</VBtn>
|
||||
</RouterLink>
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
stats: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
change: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
},
|
||||
})
|
||||
|
||||
const isPositive = controlledComputed(() => props.change, () => Math.sign(props.change) === 1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard class="overflow-visible">
|
||||
<div class="d-flex position-relative">
|
||||
<VCardText>
|
||||
<h6 class="text-base font-weight-semibold mb-4">
|
||||
{{ props.title }}
|
||||
</h6>
|
||||
<div class="d-flex align-center flex-wrap mb-4">
|
||||
<h5 class="text-h5 font-weight-semibold me-2">
|
||||
{{ props.stats }}
|
||||
</h5>
|
||||
<span
|
||||
class="text-caption"
|
||||
:class="isPositive ? 'text-success' : 'text-error'"
|
||||
>
|
||||
{{ isPositive ? `+${props.change}` : props.change }}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<VChip
|
||||
v-if="props.subtitle"
|
||||
size="small"
|
||||
:color="props.color"
|
||||
>
|
||||
{{ props.subtitle }}
|
||||
</VChip>
|
||||
</VCardText>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<div class="illustrator-img">
|
||||
<VImg
|
||||
v-if="props.image"
|
||||
:src="props.image"
|
||||
:width="110"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.illustrator-img {
|
||||
position: absolute;
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 5%;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user