initial commit
This commit is contained in:
396
resources/js/@core/AppBarSearch.vue
Normal file
396
resources/js/@core/AppBarSearch.vue
Normal file
@@ -0,0 +1,396 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import {
|
||||
VList,
|
||||
VListItem,
|
||||
VListSubheader,
|
||||
} from 'vuetify/components/VList'
|
||||
|
||||
const props = defineProps({
|
||||
isDialogVisible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
searchQuery: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
searchResults: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
suggestions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
},
|
||||
noDataSuggestion: {
|
||||
type: Array,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDialogVisible',
|
||||
'update:searchQuery',
|
||||
'itemSelected',
|
||||
])
|
||||
|
||||
const { ctrl_k, meta_k } = useMagicKeys({
|
||||
passive: false,
|
||||
onEventFired(e) {
|
||||
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown')
|
||||
e.preventDefault()
|
||||
},
|
||||
})
|
||||
|
||||
const refSearchList = ref()
|
||||
const searchQuery = ref(structuredClone(toRaw(props.searchQuery)))
|
||||
const refSearchInput = ref()
|
||||
const isLocalDialogVisible = ref(structuredClone(toRaw(props.isDialogVisible)))
|
||||
const searchResults = ref(structuredClone(toRaw(props.searchResults)))
|
||||
|
||||
// 👉 Watching props change
|
||||
watch(props, () => {
|
||||
isLocalDialogVisible.value = structuredClone(toRaw(props.isDialogVisible))
|
||||
searchResults.value = structuredClone(toRaw(props.searchResults))
|
||||
searchQuery.value = structuredClone(toRaw(props.searchQuery))
|
||||
})
|
||||
watch([
|
||||
ctrl_k,
|
||||
meta_k,
|
||||
], () => {
|
||||
isLocalDialogVisible.value = true
|
||||
emit('update:isDialogVisible', true)
|
||||
})
|
||||
|
||||
// 👉 clear search result and close the dialog
|
||||
const clearSearchAndCloseDialog = () => {
|
||||
emit('update:isDialogVisible', false)
|
||||
emit('update:searchQuery', '')
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (!searchQuery.value.length)
|
||||
searchResults.value = []
|
||||
})
|
||||
|
||||
const getFocusOnSearchList = e => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
refSearchList.value?.focus('next')
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
refSearchList.value?.focus('prev')
|
||||
}
|
||||
}
|
||||
|
||||
const dialogModelValueUpdate = val => {
|
||||
emit('update:isDialogVisible', val)
|
||||
emit('update:searchQuery', '')
|
||||
}
|
||||
|
||||
const resolveCategories = val => {
|
||||
if (val === 'dashboards')
|
||||
return 'Dashboards'
|
||||
if (val === 'appsPages')
|
||||
return 'Apps & Pages'
|
||||
if (val === 'userInterface')
|
||||
return 'User Interface'
|
||||
if (val === 'formsTables')
|
||||
return 'Forms Tables'
|
||||
if (val === 'chartsMisc')
|
||||
return 'Charts Misc'
|
||||
|
||||
return 'Misc'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog
|
||||
max-width="600"
|
||||
:model-value="isLocalDialogVisible"
|
||||
:height="$vuetify.display.smAndUp ? '550' : '100%'"
|
||||
:fullscreen="$vuetify.display.width < 600"
|
||||
class="app-bar-search-dialog"
|
||||
@update:model-value="dialogModelValueUpdate"
|
||||
@keyup.esc="clearSearchAndCloseDialog"
|
||||
>
|
||||
<VCard
|
||||
height="100%"
|
||||
width="100%"
|
||||
class="position-relative"
|
||||
>
|
||||
<VCardText
|
||||
class="pt-1"
|
||||
style="min-block-size: 65px;"
|
||||
>
|
||||
<!-- 👉 Search Input -->
|
||||
<VTextField
|
||||
ref="refSearchInput"
|
||||
v-model="searchQuery"
|
||||
autofocus
|
||||
density="comfortable"
|
||||
variant="plain"
|
||||
class="app-bar-autocomplete-box"
|
||||
@keyup.esc="clearSearchAndCloseDialog"
|
||||
@keydown="getFocusOnSearchList"
|
||||
@update:model-value="$emit('update:searchQuery', searchQuery)"
|
||||
>
|
||||
<!-- 👉 Prepend Inner -->
|
||||
<template #prepend-inner>
|
||||
<div class="d-flex align-center text-high-emphasis me-1">
|
||||
<VIcon
|
||||
size="22"
|
||||
icon="tabler-search"
|
||||
class="mt-1"
|
||||
style="opacity: 1;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 👉 Append Inner -->
|
||||
<template #append-inner>
|
||||
<div class="d-flex align-center">
|
||||
<div
|
||||
class="text-base text-disabled cursor-pointer me-1"
|
||||
@click="clearSearchAndCloseDialog"
|
||||
>
|
||||
[esc]
|
||||
</div>
|
||||
|
||||
<IconBtn
|
||||
size="small"
|
||||
@click="clearSearchAndCloseDialog"
|
||||
>
|
||||
<VIcon icon="tabler-x" />
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VTextField>
|
||||
</VCardText>
|
||||
|
||||
<!-- 👉 Divider -->
|
||||
<VDivider />
|
||||
|
||||
<!-- 👉 Perfect Scrollbar -->
|
||||
<PerfectScrollbar
|
||||
:options="{ wheelPropagation: false, suppressScrollX: true }"
|
||||
class="h-100"
|
||||
>
|
||||
<!-- 👉 Search List -->
|
||||
<VList
|
||||
v-show="searchQuery.length && !!searchResults.length"
|
||||
ref="refSearchList"
|
||||
density="compact"
|
||||
class="app-bar-search-list"
|
||||
>
|
||||
<!-- 👉 list Item /List Sub header -->
|
||||
<template
|
||||
v-for="item in searchResults"
|
||||
:key="item.title"
|
||||
>
|
||||
<VListSubheader
|
||||
v-if="'header' in item"
|
||||
class="text-disabled"
|
||||
>
|
||||
{{ resolveCategories(item.title) }}
|
||||
</VListSubheader>
|
||||
|
||||
<template v-else>
|
||||
<slot
|
||||
name="searchResult"
|
||||
:item="item"
|
||||
>
|
||||
<VListItem
|
||||
link
|
||||
@click="$emit('itemSelected', item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
size="20"
|
||||
:icon="item.icon"
|
||||
class="me-3"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="tabler-corner-down-left"
|
||||
class="enter-icon text-disabled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<VListItemTitle>
|
||||
{{ item.title }}
|
||||
</VListItemTitle>
|
||||
</VListItem>
|
||||
</slot>
|
||||
</template>
|
||||
</template>
|
||||
</VList>
|
||||
|
||||
<!-- 👉 Suggestions -->
|
||||
<div
|
||||
v-show="!!searchResults && !searchQuery"
|
||||
class="h-100"
|
||||
>
|
||||
<slot name="suggestions">
|
||||
<VCardText class="app-bar-search-suggestions h-100 pa-10">
|
||||
<VRow
|
||||
v-if="props.suggestions"
|
||||
class="gap-y-4"
|
||||
>
|
||||
<VCol
|
||||
v-for="suggestion in props.suggestions"
|
||||
:key="suggestion.title"
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="ps-6"
|
||||
>
|
||||
<p class="text-xs text-disabled text-uppercase">
|
||||
{{ suggestion.title }}
|
||||
</p>
|
||||
|
||||
<VList class="card-list">
|
||||
<VListItem
|
||||
v-for="item in suggestion.content"
|
||||
:key="item.title"
|
||||
link
|
||||
:title="item.title"
|
||||
class="app-bar-search-suggestion"
|
||||
@click="$emit('itemSelected', item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon
|
||||
:icon="item.icon"
|
||||
size="20"
|
||||
class="me-2"
|
||||
/>
|
||||
</template>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- 👉 No Data found -->
|
||||
<div
|
||||
v-show="!searchResults.length && searchQuery.length"
|
||||
class="h-100"
|
||||
>
|
||||
<slot name="noData">
|
||||
<VCardText class="h-100">
|
||||
<div class="app-bar-search-suggestions d-flex flex-column align-center justify-center text-high-emphasis h-100">
|
||||
<VIcon
|
||||
size="75"
|
||||
icon="tabler-file-x"
|
||||
/>
|
||||
<div class="d-flex align-center flex-wrap justify-center gap-2 text-h6 my-3">
|
||||
<span>No Result For </span>
|
||||
<span>"{{ searchQuery }}"</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="props.noDataSuggestion"
|
||||
class="mt-8"
|
||||
>
|
||||
<span class="d-flex justify-center text-disabled">Try searching for</span>
|
||||
<h6
|
||||
v-for="suggestion in props.noDataSuggestion"
|
||||
:key="suggestion.title"
|
||||
class="app-bar-search-suggestion text-sm font-weight-regular cursor-pointer mt-3"
|
||||
@click="$emit('itemSelected', suggestion)"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
:icon="suggestion.icon"
|
||||
class="me-3"
|
||||
/>
|
||||
<span class="text-sm">{{ suggestion.title }}</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</slot>
|
||||
</div>
|
||||
</PerfectScrollbar>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.app-bar-search-suggestions {
|
||||
.app-bar-search-suggestion {
|
||||
&:hover {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-bar-autocomplete-box {
|
||||
.v-field__input {
|
||||
padding-block-end: 0.425rem;
|
||||
padding-block-start: 1.16rem;
|
||||
}
|
||||
|
||||
.v-field__append-inner,
|
||||
.v-field__prepend-inner {
|
||||
padding-block-start: 0.95rem;
|
||||
}
|
||||
|
||||
.v-field__field input {
|
||||
text-align: start !important;
|
||||
}
|
||||
}
|
||||
|
||||
.app-bar-search-dialog {
|
||||
.v-overlay__scrim {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.v-list-item-title {
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.app-bar-search-list {
|
||||
.v-list-item,
|
||||
.v-list-subheader {
|
||||
font-size: 0.75rem;
|
||||
padding-inline: 1.5rem !important;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
.v-list-item__append {
|
||||
.enter-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
.v-list-item__append {
|
||||
.enter-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-subheader {
|
||||
line-height: 1;
|
||||
min-block-size: auto;
|
||||
padding-block: 0.6875rem 0.3125rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: 16px;
|
||||
}
|
||||
</style>
|
28
resources/js/@core/AppDrawerHeaderSection.vue
Normal file
28
resources/js/@core/AppDrawerHeaderSection.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cancel'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-5 py-3 d-flex align-center">
|
||||
<h3 class="font-weight-medium text-xl">
|
||||
{{ props.title }}
|
||||
</h3>
|
||||
<VSpacer />
|
||||
|
||||
<slot name="beforeClose" />
|
||||
|
||||
<IconBtn @click="$emit('cancel')">
|
||||
<VIcon
|
||||
size="18"
|
||||
icon="tabler-x"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
290
resources/js/@core/AppStepper.vue
Normal file
290
resources/js/@core/AppStepper.vue
Normal file
@@ -0,0 +1,290 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
currentStep: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'horizontal',
|
||||
},
|
||||
iconSize: {
|
||||
type: [
|
||||
String,
|
||||
Number,
|
||||
],
|
||||
required: false,
|
||||
default: 52,
|
||||
},
|
||||
isActiveStepValid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:currentStep'])
|
||||
|
||||
const currentStep = ref(props.currentStep || 0)
|
||||
const activeOrCompletedStepsClasses = computed(() => index => index < currentStep.value ? 'stepper-steps-completed' : index === currentStep.value ? 'stepper-steps-active' : '')
|
||||
const isHorizontalAndNotLastStep = computed(() => index => props.direction === 'horizontal' && props.items.length - 1 !== index)
|
||||
|
||||
// check if validation is enabled
|
||||
const isValidationEnabled = computed(() => {
|
||||
return props.isActiveStepValid !== undefined
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.currentStep !== undefined && props.currentStep < props.items.length && props.currentStep >= 0)
|
||||
currentStep.value = props.currentStep
|
||||
emit('update:currentStep', currentStep.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VSlideGroup
|
||||
v-model="currentStep"
|
||||
class="app-stepper"
|
||||
show-arrows
|
||||
:direction="props.direction"
|
||||
>
|
||||
<VSlideGroupItem
|
||||
v-for="(item, index) in props.items"
|
||||
:key="item.title"
|
||||
:value="index"
|
||||
>
|
||||
<div
|
||||
class="cursor-pointer mx-1"
|
||||
:class="[
|
||||
(!props.isActiveStepValid && (isValidationEnabled)) && 'stepper-steps-invalid',
|
||||
activeOrCompletedStepsClasses(index),
|
||||
]"
|
||||
@click="!isValidationEnabled && emit('update:currentStep', index)"
|
||||
>
|
||||
<!-- SECTION stepper step with icon -->
|
||||
<template v-if="item.icon">
|
||||
<div class="stepper-icon-step text-high-emphasis d-flex align-center gap-2">
|
||||
<!-- 👉 icon and title -->
|
||||
<div
|
||||
class="d-flex align-center gap-4 step-wrapper"
|
||||
:class="[props.direction === 'horizontal' && 'flex-column']"
|
||||
>
|
||||
<div class="stepper-icon">
|
||||
<VIcon
|
||||
:icon="item.icon"
|
||||
:size="item.size || props.iconSize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="stepper-title font-weight-medium mb-0">
|
||||
{{ item.title }}
|
||||
</p>
|
||||
<span
|
||||
v-if="item.subtitle"
|
||||
class="stepper-subtitle"
|
||||
>
|
||||
<span class="text-sm">{{ item.subtitle }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 append chevron -->
|
||||
<VIcon
|
||||
v-if="isHorizontalAndNotLastStep(index)"
|
||||
class="flip-in-rtl stepper-chevron-indicator mx-6"
|
||||
size="24"
|
||||
icon="tabler-chevron-right"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- !SECTION -->
|
||||
|
||||
<!-- SECTION stepper step without icon -->
|
||||
<template v-else>
|
||||
<div class="d-flex align-center gap-x-2">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<div
|
||||
class="d-flex align-center justify-center"
|
||||
style="block-size: 24px; inline-size: 24px;"
|
||||
>
|
||||
<!-- 👉 custom circle icon -->
|
||||
<template v-if="index >= currentStep">
|
||||
<div
|
||||
v-if="(!isValidationEnabled || props.isActiveStepValid || index !== currentStep)"
|
||||
class="stepper-step-indicator"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
v-else
|
||||
icon="tabler-alert-circle"
|
||||
size="24"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 👉 step completed icon -->
|
||||
|
||||
<VIcon
|
||||
v-else
|
||||
icon="custom-check-circle"
|
||||
class="stepper-step-icon"
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Step Number -->
|
||||
<h4 class="text-h4 step-number">
|
||||
{{ (index + 1).toString().padStart(2, '0') }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<!-- 👉 title and subtitle -->
|
||||
<div style="line-height: 0;">
|
||||
<h6 class="text-sm font-weight-medium step-title">
|
||||
{{ item.title }}
|
||||
</h6>
|
||||
|
||||
<span
|
||||
v-if="item.subtitle"
|
||||
class="text-xs step-subtitle"
|
||||
>
|
||||
{{ item.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 👉 stepper step line -->
|
||||
<div
|
||||
v-if="isHorizontalAndNotLastStep(index)"
|
||||
class="stepper-step-line"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- !SECTION -->
|
||||
</div>
|
||||
</VSlideGroupItem>
|
||||
</VSlideGroup>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.app-stepper {
|
||||
// 👉 stepper step with bg color
|
||||
&.stepper-icon-step-bg {
|
||||
.stepper-icon-step {
|
||||
.step-wrapper {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.stepper-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.3125rem;
|
||||
background-color: rgba(var(--v-theme-on-surface), var(--v-selected-opacity));
|
||||
block-size: 2.5rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
inline-size: 2.5rem;
|
||||
margin-inline-end: 0.3rem;
|
||||
}
|
||||
|
||||
.stepper-title,
|
||||
.stepper-subtitle {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.stepper-title {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.stepper-subtitle {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-steps-active {
|
||||
.stepper-icon-step {
|
||||
.stepper-icon {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
color: rgba(var(--v-theme-on-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-steps-completed {
|
||||
.stepper-icon-step {
|
||||
.stepper-icon {
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
color: rgba(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 stepper step with icon and default
|
||||
.v-slide-group__content {
|
||||
justify-content: center;
|
||||
row-gap: 1.5rem;
|
||||
|
||||
.stepper-step-indicator {
|
||||
border: 0.3125rem solid rgb(var(--v-theme-primary));
|
||||
border-radius: 50%;
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
block-size: 1.25rem;
|
||||
inline-size: 1.25rem;
|
||||
opacity: var(--v-activated-opacity);
|
||||
}
|
||||
|
||||
.stepper-step-line {
|
||||
border-radius: 0.1875rem;
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
block-size: 0.1875rem;
|
||||
inline-size: 3.75rem;
|
||||
opacity: var(--v-activated-opacity);
|
||||
}
|
||||
|
||||
.stepper-chevron-indicator {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
}
|
||||
|
||||
.stepper-steps-completed,
|
||||
.stepper-steps-active {
|
||||
.stepper-icon-step,
|
||||
.stepper-step-icon {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
|
||||
.stepper-step-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-steps-completed {
|
||||
.stepper-step-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stepper-chevron-indicator {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
|
||||
.stepper-steps-invalid.stepper-steps-active {
|
||||
.stepper-icon-step,
|
||||
.step-number,
|
||||
.step-title,
|
||||
.step-subtitle {
|
||||
color: rgb(var(--v-theme-error)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
56
resources/js/@core/I18n.vue
Normal file
56
resources/js/@core/I18n.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
languages: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
location: {
|
||||
type: null,
|
||||
required: false,
|
||||
default: 'bottom end',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const { locale } = useI18n({ useScope: 'global' })
|
||||
|
||||
watch(locale, val => {
|
||||
document.documentElement.setAttribute('lang', val)
|
||||
})
|
||||
|
||||
const currentLang = ref(['en'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn>
|
||||
<VIcon
|
||||
size="26"
|
||||
icon="tabler-language"
|
||||
/>
|
||||
|
||||
<!-- Menu -->
|
||||
<VMenu
|
||||
activator="parent"
|
||||
:location="props.location"
|
||||
offset="14px"
|
||||
>
|
||||
<!-- List -->
|
||||
<VList
|
||||
v-model:selected="currentLang"
|
||||
min-width="175px"
|
||||
>
|
||||
<!-- List item -->
|
||||
<VListItem
|
||||
v-for="lang in props.languages"
|
||||
:key="lang.i18nLang"
|
||||
:value="lang.i18nLang"
|
||||
@click="locale = lang.i18nLang; $emit('change', lang.i18nLang)"
|
||||
>
|
||||
<!-- Language label -->
|
||||
<VListItemTitle>{{ lang.label }}</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
31
resources/js/@core/MoreBtn.vue
Normal file
31
resources/js/@core/MoreBtn.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
menuList: {
|
||||
type: Array,
|
||||
required: false,
|
||||
},
|
||||
itemProps: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn
|
||||
density="compact"
|
||||
color="disabled"
|
||||
>
|
||||
<VIcon icon="tabler-dots-vertical" />
|
||||
|
||||
<VMenu
|
||||
v-if="props.menuList"
|
||||
activator="parent"
|
||||
>
|
||||
<VList
|
||||
:items="props.menuList"
|
||||
:item-props="props.itemProps"
|
||||
/>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</template>
|
173
resources/js/@core/Notifications.vue
Normal file
173
resources/js/@core/Notifications.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script setup>
|
||||
import { avatarText } from '@core/utils/formatters';
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar';
|
||||
|
||||
const props = defineProps({
|
||||
notifications: {
|
||||
type: Array,
|
||||
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" />
|
||||
<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 start>
|
||||
<VAvatar size="40"
|
||||
:color="notification.color && notification.icon ? notification.color : undefined"
|
||||
:image="notification.img || undefined"
|
||||
:icon="notification.icon || undefined"
|
||||
:variant="notification.img ? undefined : 'tonal'">
|
||||
<span v-if="notification.text">{{ avatarText(notification.text) }}</span>
|
||||
</VAvatar>
|
||||
</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>
|
40
resources/js/@core/ScrollToTop.vue
Normal file
40
resources/js/@core/ScrollToTop.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
const { y } = useWindowScroll()
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VScaleTransition
|
||||
style="transform-origin: center;"
|
||||
class="scroll-to-top d-print-none"
|
||||
>
|
||||
<VBtn
|
||||
v-show="y > 200"
|
||||
icon
|
||||
density="comfortable"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
<VIcon
|
||||
size="22"
|
||||
icon="tabler-arrow-up"
|
||||
/>
|
||||
</VBtn>
|
||||
</VScaleTransition>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.scroll-to-top {
|
||||
position: fixed !important;
|
||||
|
||||
// To keep button on top of v-layout. E.g. Email app
|
||||
z-index: 999;
|
||||
inset-block-end: 5%;
|
||||
inset-inline-end: 25px;
|
||||
}
|
||||
</style>
|
367
resources/js/@core/TheCustomizer.vue
Normal file
367
resources/js/@core/TheCustomizer.vue
Normal file
@@ -0,0 +1,367 @@
|
||||
<script setup>
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { staticPrimaryColor } from '@/plugins/vuetify/theme'
|
||||
import { useThemeConfig } from '@core/composable/useThemeConfig'
|
||||
import {
|
||||
RouteTransitions,
|
||||
Skins,
|
||||
} from '@core/enums'
|
||||
import {
|
||||
AppContentLayoutNav,
|
||||
ContentWidth,
|
||||
FooterType,
|
||||
NavbarType,
|
||||
} from '@layouts/enums'
|
||||
// import { themeConfig } from '@themeConfig'
|
||||
|
||||
const isNavDrawerOpen = ref(false)
|
||||
const { theme, skin, appRouteTransition, navbarType, footerType, isVerticalNavCollapsed, isVerticalNavSemiDark, appContentWidth, appContentLayoutNav, isAppRtl, isNavbarBlurEnabled, isLessThanOverlayNavBreakpoint } = useThemeConfig()
|
||||
|
||||
// 👉 Primary Color
|
||||
const vuetifyTheme = useTheme()
|
||||
|
||||
// const vuetifyThemesName = Object.keys(vuetifyTheme.themes.value)
|
||||
const initialThemeColors = JSON.parse(JSON.stringify(vuetifyTheme.current.value.colors))
|
||||
|
||||
const colors = [
|
||||
'primary',
|
||||
'secondary',
|
||||
'success',
|
||||
'info',
|
||||
'warning',
|
||||
'error',
|
||||
]
|
||||
|
||||
const setPrimaryColor = color => {
|
||||
const currentThemeName = vuetifyTheme.name.value
|
||||
|
||||
vuetifyTheme.themes.value[currentThemeName].colors.primary = color
|
||||
localStorage.setItem(`${ themeConfig.app.title }-${ currentThemeName }ThemePrimaryColor`, color)
|
||||
localStorage.setItem(`${ themeConfig.app.title }-initial-loader-color`, color)
|
||||
}
|
||||
|
||||
const getBoxColor = (color, index) => index ? color : staticPrimaryColor
|
||||
const { width: windowWidth } = useWindowSize()
|
||||
|
||||
const headerValues = computed(() => {
|
||||
const entries = Object.entries(NavbarType)
|
||||
if (appContentLayoutNav.value === AppContentLayoutNav.Horizontal)
|
||||
return entries.filter(([_, val]) => val !== NavbarType.Hidden)
|
||||
|
||||
return entries
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="!isLessThanOverlayNavBreakpoint(windowWidth)">
|
||||
<VBtn
|
||||
icon
|
||||
size="small"
|
||||
class="app-customizer-toggler rounded-s-lg rounded-0"
|
||||
style="z-index: 1001;"
|
||||
@click="isNavDrawerOpen = true"
|
||||
>
|
||||
<VIcon
|
||||
size="22"
|
||||
icon="tabler-settings"
|
||||
/>
|
||||
</VBtn>
|
||||
|
||||
<VNavigationDrawer
|
||||
v-model="isNavDrawerOpen"
|
||||
temporary
|
||||
border="0"
|
||||
location="end"
|
||||
width="400"
|
||||
:scrim="false"
|
||||
class="app-customizer"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<div class="customizer-heading d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<h6 class="text-h6">
|
||||
THEME CUSTOMIZER
|
||||
</h6>
|
||||
<span class="text-body-1">Customize & Preview in Real Time</span>
|
||||
</div>
|
||||
<IconBtn @click="isNavDrawerOpen = false">
|
||||
<VIcon
|
||||
icon="tabler-x"
|
||||
size="20"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar
|
||||
tag="ul"
|
||||
:options="{ wheelPropagation: false }"
|
||||
>
|
||||
<!-- SECTION Theming -->
|
||||
<CustomizerSection
|
||||
title="THEMING"
|
||||
:divider="false"
|
||||
>
|
||||
<!-- 👉 Skin -->
|
||||
<h6 class="text-base font-weight-regular">
|
||||
Skins
|
||||
</h6>
|
||||
<VRadioGroup
|
||||
v-model="skin"
|
||||
inline
|
||||
>
|
||||
<VRadio
|
||||
v-for="[key, val] in Object.entries(Skins)"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:value="val"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
|
||||
<!-- 👉 Theme -->
|
||||
<h6 class="mt-3 text-base font-weight-regular">
|
||||
Theme
|
||||
</h6>
|
||||
<VRadioGroup
|
||||
v-model="theme"
|
||||
inline
|
||||
>
|
||||
<VRadio
|
||||
v-for="themeOption in ['system', 'light', 'dark']"
|
||||
:key="themeOption"
|
||||
:label="themeOption"
|
||||
:value="themeOption"
|
||||
class="text-capitalize"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
|
||||
<!-- 👉 Primary color -->
|
||||
<h6 class="mt-3 text-base font-weight-regular">
|
||||
Primary Color
|
||||
</h6>
|
||||
<div class="d-flex gap-x-4 mt-2">
|
||||
<div
|
||||
v-for="(color, index) in colors"
|
||||
:key="color"
|
||||
style=" border-radius: 0.5rem; block-size: 2.5rem;inline-size: 2.5rem; transition: all 0.25s ease;"
|
||||
:style="{ backgroundColor: getBoxColor(initialThemeColors[color], index) }"
|
||||
class="cursor-pointer d-flex align-center justify-center"
|
||||
:class="{ 'elevation-4': vuetifyTheme.current.value.colors.primary === getBoxColor(initialThemeColors[color], index) }"
|
||||
@click="setPrimaryColor(getBoxColor(initialThemeColors[color], index))"
|
||||
>
|
||||
<VFadeTransition>
|
||||
<VIcon
|
||||
v-show="vuetifyTheme.current.value.colors.primary === (getBoxColor(initialThemeColors[color], index))"
|
||||
icon="tabler-check"
|
||||
color="white"
|
||||
/>
|
||||
</VFadeTransition>
|
||||
</div>
|
||||
</div>
|
||||
</CustomizerSection>
|
||||
<!-- !SECTION -->
|
||||
|
||||
<!-- SECTION LAYOUT -->
|
||||
<CustomizerSection title="LAYOUT">
|
||||
<!-- 👉 Content Width -->
|
||||
<h6 class="text-base font-weight-regular">
|
||||
Content width
|
||||
</h6>
|
||||
<VRadioGroup
|
||||
v-model="appContentWidth"
|
||||
inline
|
||||
>
|
||||
<VRadio
|
||||
v-for="[key, val] in Object.entries(ContentWidth)"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:value="val"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
<!-- 👉 Navbar Type -->
|
||||
<h6 class="mt-3 text-base font-weight-regular">
|
||||
{{ appContentLayoutNav === AppContentLayoutNav.Vertical ? 'Navbar' : 'Header' }} Type
|
||||
</h6>
|
||||
<VRadioGroup
|
||||
v-model="navbarType"
|
||||
inline
|
||||
>
|
||||
<VRadio
|
||||
v-for="[key, val] in headerValues"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:value="val"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
<!-- 👉 Footer Type -->
|
||||
<h6 class="mt-3 text-base font-weight-regular">
|
||||
Footer Type
|
||||
</h6>
|
||||
<VRadioGroup
|
||||
v-model="footerType"
|
||||
inline
|
||||
>
|
||||
<VRadio
|
||||
v-for="[key, val] in Object.entries(FooterType)"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:value="val"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
<!-- 👉 Navbar blur -->
|
||||
<div class="mt-4 d-flex align-center justify-space-between">
|
||||
<VLabel
|
||||
for="customizer-navbar-blur"
|
||||
class="text-high-emphasis"
|
||||
>
|
||||
Navbar Blur
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="customizer-navbar-blur"
|
||||
v-model="isNavbarBlurEnabled"
|
||||
class="ms-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CustomizerSection>
|
||||
<!-- !SECTION -->
|
||||
|
||||
<!-- SECTION Menu -->
|
||||
<CustomizerSection title="MENU">
|
||||
<!-- 👉 Menu Type -->
|
||||
<h6 class="text-base font-weight-regular">
|
||||
Menu Type
|
||||
</h6>
|
||||
<VRadioGroup
|
||||
v-model="appContentLayoutNav"
|
||||
inline
|
||||
>
|
||||
<VRadio
|
||||
v-for="[key, val] in Object.entries(AppContentLayoutNav)"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:value="val"
|
||||
/>
|
||||
</VRadioGroup>
|
||||
|
||||
<!-- 👉 Collapsed Menu -->
|
||||
<div
|
||||
v-if="appContentLayoutNav === AppContentLayoutNav.Vertical"
|
||||
class="mt-4 d-flex align-center justify-space-between"
|
||||
>
|
||||
<VLabel
|
||||
for="customizer-menu-collapsed"
|
||||
class="text-high-emphasis"
|
||||
>
|
||||
Collapsed Menu
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="customizer-menu-collapsed"
|
||||
v-model="isVerticalNavCollapsed"
|
||||
class="ms-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Semi Dark Menu -->
|
||||
<div
|
||||
class="mt-4 align-center justify-space-between"
|
||||
:class="vuetifyTheme.global.name.value === 'light' && appContentLayoutNav === AppContentLayoutNav.Vertical ? 'd-flex' : 'd-none'"
|
||||
>
|
||||
<VLabel
|
||||
for="customizer-menu-semi-dark"
|
||||
class="text-high-emphasis"
|
||||
>
|
||||
Semi Dark Menu
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="customizer-menu-semi-dark"
|
||||
v-model="isVerticalNavSemiDark"
|
||||
class="ms-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CustomizerSection>
|
||||
<!-- !SECTION -->
|
||||
|
||||
<!-- SECTION MISC -->
|
||||
<CustomizerSection title="MISC">
|
||||
<!-- 👉 RTL -->
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<VLabel
|
||||
for="customizer-rtl"
|
||||
class="text-high-emphasis"
|
||||
>
|
||||
RTL
|
||||
</VLabel>
|
||||
<div>
|
||||
<VSwitch
|
||||
id="customizer-rtl"
|
||||
v-model="isAppRtl"
|
||||
class="ms-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 Route Transition -->
|
||||
<div class="mt-6">
|
||||
<VRow>
|
||||
<VCol
|
||||
cols="5"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<VLabel
|
||||
for="route-transition"
|
||||
class="text-high-emphasis"
|
||||
>
|
||||
Router Transition
|
||||
</VLabel>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="7">
|
||||
<AppSelect
|
||||
id="route-transition"
|
||||
v-model="appRouteTransition"
|
||||
:items="Object.entries(RouteTransitions).map(([key, value]) => ({ key, value }))"
|
||||
item-title="key"
|
||||
item-value="value"
|
||||
single-line
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</div>
|
||||
</CustomizerSection>
|
||||
<!-- !SECTION -->
|
||||
</PerfectScrollbar>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.app-customizer {
|
||||
.customizer-section {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.customizer-heading {
|
||||
padding-block: 0.875rem;
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
|
||||
.v-navigation-drawer__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.app-customizer-toggler {
|
||||
position: fixed !important;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
</style>
|
43
resources/js/@core/ThemeSwitcher.vue
Normal file
43
resources/js/@core/ThemeSwitcher.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { useThemeConfig } from '@core/composable/useThemeConfig'
|
||||
|
||||
const props = defineProps({
|
||||
themes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { theme } = useThemeConfig()
|
||||
|
||||
const {
|
||||
state: currentThemeName,
|
||||
next: getNextThemeName,
|
||||
index: currentThemeIndex,
|
||||
} = useCycleList(props.themes.map(t => t.name), { initialValue: theme.value })
|
||||
|
||||
const changeTheme = () => {
|
||||
theme.value = getNextThemeName()
|
||||
}
|
||||
|
||||
// Update icon if theme is changed from other sources
|
||||
watch(theme, val => {
|
||||
currentThemeName.value = val
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconBtn @click="changeTheme">
|
||||
<VIcon
|
||||
size="26"
|
||||
:icon="props.themes[currentThemeIndex].icon"
|
||||
/>
|
||||
<VTooltip
|
||||
activator="parent"
|
||||
open-delay="1000"
|
||||
scroll-strategy="close"
|
||||
>
|
||||
<span class="text-capitalize">{{ currentThemeName }}</span>
|
||||
</VTooltip>
|
||||
</IconBtn>
|
||||
</template>
|
56
resources/js/@core/app-form-elements/AppAutocomplete.vue
Normal file
56
resources/js/@core/app-form-elements/AppAutocomplete.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'AppAutocomplete',
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
// const { class: _class, label, variant: _, ...restAttrs } = useAttrs()
|
||||
const elementId = computed(() => {
|
||||
const attrs = useAttrs()
|
||||
const _elementIdToken = attrs.id || attrs.label
|
||||
|
||||
return _elementIdToken ? `app-autocomplete-${ _elementIdToken }-${ Math.random().toString(36).slice(2, 7) }` : undefined
|
||||
})
|
||||
|
||||
const label = computed(() => useAttrs().label)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="app-autocomplete flex-grow-1"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<VLabel
|
||||
v-if="label"
|
||||
:for="elementId"
|
||||
class="mb-1 text-body-2 text-high-emphasis"
|
||||
:text="label"
|
||||
/>
|
||||
<VAutocomplete
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
class: null,
|
||||
label: undefined,
|
||||
id: elementId,
|
||||
variant: 'outlined',
|
||||
menuProps: {
|
||||
contentClass: [
|
||||
'app-inner-list',
|
||||
'app-autocomplete__content',
|
||||
'v-autocomplete__content',
|
||||
],
|
||||
},
|
||||
}"
|
||||
>
|
||||
<template
|
||||
v-for="(_, name) in $slots"
|
||||
#[name]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="name"
|
||||
v-bind="slotProps || {}"
|
||||
/>
|
||||
</template>
|
||||
</VAutocomplete>
|
||||
</div>
|
||||
</template>
|
57
resources/js/@core/app-form-elements/AppCombobox.vue
Normal file
57
resources/js/@core/app-form-elements/AppCombobox.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'AppCombobox',
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const elementId = computed(() => {
|
||||
const attrs = useAttrs()
|
||||
const _elementIdToken = attrs.id || attrs.label
|
||||
|
||||
return _elementIdToken ? `app-combobox-${ _elementIdToken }-${ Math.random().toString(36).slice(2, 7) }` : undefined
|
||||
})
|
||||
|
||||
const label = computed(() => useAttrs().label)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="app-combobox flex-grow-1"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<VLabel
|
||||
v-if="label"
|
||||
:for="elementId"
|
||||
class="mb-1 text-body-2 text-high-emphasis"
|
||||
:text="label"
|
||||
/>
|
||||
|
||||
<VCombobox
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
class: null,
|
||||
label: undefined,
|
||||
variant: 'outlined',
|
||||
id: elementId,
|
||||
menuProps: {
|
||||
contentClass: [
|
||||
'app-inner-list',
|
||||
'app-combobox__content',
|
||||
'v-combobox__content',
|
||||
$attrs.multiple !== undefined ? 'v-list-select-multiple' : '',
|
||||
],
|
||||
},
|
||||
}"
|
||||
>
|
||||
<template
|
||||
v-for="(_, name) in $slots"
|
||||
#[name]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="name"
|
||||
v-bind="slotProps || {}"
|
||||
/>
|
||||
</template>
|
||||
</VCombobox>
|
||||
</div>
|
||||
</template>
|
501
resources/js/@core/app-form-elements/AppDateTimePicker.vue
Normal file
501
resources/js/@core/app-form-elements/AppDateTimePicker.vue
Normal file
@@ -0,0 +1,501 @@
|
||||
<script setup>
|
||||
import FlatPickr from 'vue-flatpickr-component'
|
||||
import { useTheme } from 'vuetify'
|
||||
import {
|
||||
VField,
|
||||
filterFieldProps,
|
||||
makeVFieldProps,
|
||||
} from 'vuetify/lib/components/VField/VField'
|
||||
import {
|
||||
VInput,
|
||||
makeVInputProps,
|
||||
} from 'vuetify/lib/components/VInput/VInput'
|
||||
|
||||
import { filterInputAttrs } from 'vuetify/lib/util/helpers'
|
||||
import { useThemeConfig } from '@core/composable/useThemeConfig'
|
||||
|
||||
const props = defineProps({
|
||||
autofocus: Boolean,
|
||||
counter: [
|
||||
Boolean,
|
||||
Number,
|
||||
String,
|
||||
],
|
||||
counterValue: Function,
|
||||
prefix: String,
|
||||
placeholder: String,
|
||||
persistentPlaceholder: Boolean,
|
||||
persistentCounter: Boolean,
|
||||
suffix: String,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
modelModifiers: Object,
|
||||
...makeVInputProps({
|
||||
density: 'compact',
|
||||
hideDetails: 'auto',
|
||||
}),
|
||||
...makeVFieldProps({
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
}),
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'click:control',
|
||||
'mousedown:control',
|
||||
'update:focused',
|
||||
'update:modelValue',
|
||||
'click:clear',
|
||||
])
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const attrs = useAttrs()
|
||||
const [rootAttrs, compAttrs] = filterInputAttrs(attrs)
|
||||
|
||||
const [{
|
||||
modelValue: _,
|
||||
...inputProps
|
||||
}] = VInput.filterProps(props)
|
||||
|
||||
const [fieldProps] = filterFieldProps(props)
|
||||
const refFlatPicker = ref()
|
||||
const { focused } = useFocus(refFlatPicker)
|
||||
const isCalendarOpen = ref(false)
|
||||
const isInlinePicker = ref(false)
|
||||
|
||||
// flat picker prop manipulation
|
||||
if (compAttrs.config && compAttrs.config.inline) {
|
||||
isInlinePicker.value = compAttrs.config.inline
|
||||
Object.assign(compAttrs, { altInputClass: 'inlinePicker' })
|
||||
}
|
||||
|
||||
const onClear = el => {
|
||||
el.stopPropagation()
|
||||
nextTick(() => {
|
||||
emit('update:modelValue', '')
|
||||
emit('click:clear', el)
|
||||
})
|
||||
}
|
||||
|
||||
const { theme } = useThemeConfig()
|
||||
const vuetifyTheme = useTheme()
|
||||
const vuetifyThemesName = Object.keys(vuetifyTheme.themes.value)
|
||||
|
||||
// Themes class added to flat-picker component for light and dark support
|
||||
const updateThemeClassInCalendar = () => {
|
||||
|
||||
// ℹ️ Flatpickr don't render it's instance in mobile and device simulator
|
||||
if (!refFlatPicker.value.fp.calendarContainer)
|
||||
return
|
||||
vuetifyThemesName.forEach(t => {
|
||||
refFlatPicker.value.fp.calendarContainer.classList.remove(`v-theme--${ t }`)
|
||||
})
|
||||
refFlatPicker.value.fp.calendarContainer.classList.add(`v-theme--${ vuetifyTheme.global.name.value }`)
|
||||
}
|
||||
|
||||
watch(theme, updateThemeClassInCalendar)
|
||||
onMounted(() => {
|
||||
updateThemeClassInCalendar()
|
||||
})
|
||||
|
||||
const emitModelValue = val => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
|
||||
const elementId = computed(() => {
|
||||
const _elementIdToken = fieldProps.id || fieldProps.label
|
||||
|
||||
return _elementIdToken ? `app-picker-field-${ _elementIdToken }-${ Math.random().toString(36).slice(2, 7) }` : undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-picker-field">
|
||||
<!-- v-input -->
|
||||
<VLabel
|
||||
v-if="fieldProps.label"
|
||||
class="mb-1 text-body-2 text-high-emphasis"
|
||||
:for="elementId"
|
||||
:text="fieldProps.label"
|
||||
/>
|
||||
|
||||
<VInput
|
||||
v-bind="{ ...inputProps, ...rootAttrs }"
|
||||
:model-value="modelValue"
|
||||
:hide-details="props.hideDetails"
|
||||
:class="[{
|
||||
'v-text-field--prefixed': props.prefix,
|
||||
'v-text-field--suffixed': props.suffix,
|
||||
'v-text-field--flush-details': ['plain', 'underlined'].includes(props.variant),
|
||||
}, props.class]"
|
||||
class="position-relative v-text-field"
|
||||
:style="props.style"
|
||||
>
|
||||
<template #default="{ id, isDirty, isValid, isDisabled }">
|
||||
<!-- v-field -->
|
||||
<VField
|
||||
v-bind="{ ...fieldProps, label: undefined }"
|
||||
:id="id.value"
|
||||
role="textbox"
|
||||
:active="focused || isDirty.value || isCalendarOpen"
|
||||
:focused="focused || isCalendarOpen"
|
||||
:dirty="isDirty.value || props.dirty"
|
||||
:error="isValid.value === false"
|
||||
:disabled="isDisabled.value"
|
||||
@click:clear="onClear"
|
||||
>
|
||||
<template #default="{ props: vFieldProps }">
|
||||
<div v-bind="vFieldProps">
|
||||
<!-- flat-picker -->
|
||||
<FlatPickr
|
||||
v-if="!isInlinePicker"
|
||||
v-bind="compAttrs"
|
||||
:id="elementId"
|
||||
ref="refFlatPicker"
|
||||
:model-value="modelValue"
|
||||
:placeholder="props.placeholder"
|
||||
class="flat-picker-custom-style"
|
||||
:disabled="isReadonly.value"
|
||||
@on-open="isCalendarOpen = true"
|
||||
@on-close="isCalendarOpen = false"
|
||||
@update:model-value="emitModelValue"
|
||||
/>
|
||||
|
||||
<!-- simple input for inline prop -->
|
||||
<input
|
||||
v-if="isInlinePicker"
|
||||
:value="modelValue"
|
||||
:placeholder="props.placeholder"
|
||||
class="flat-picker-custom-style"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</VField>
|
||||
</template>
|
||||
</VInput>
|
||||
|
||||
<!-- flat picker for inline props -->
|
||||
<FlatPickr
|
||||
v-if="isInlinePicker"
|
||||
v-bind="compAttrs"
|
||||
ref="refFlatPicker"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitModelValue"
|
||||
@on-open="isCalendarOpen = true"
|
||||
@on-close="isCalendarOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
@use "flatpickr/dist/flatpickr.css";
|
||||
@use "@core/scss/base/mixins";
|
||||
|
||||
.flat-picker-custom-style {
|
||||
position: absolute;
|
||||
color: inherit;
|
||||
inline-size: 100%;
|
||||
inset: 0;
|
||||
outline: none;
|
||||
padding-block: 0;
|
||||
padding-inline: var(--v-field-padding-start);
|
||||
}
|
||||
|
||||
$heading-color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
$body-color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
$disabled-color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity));
|
||||
|
||||
// hide the input when your picker is inline
|
||||
input[altinputclass="inlinePicker"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-calendar {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
inline-size: 16.625rem;
|
||||
margin-block-start: 0.1875rem;
|
||||
|
||||
@include mixins.elevation(4);
|
||||
|
||||
.flatpickr-rContainer {
|
||||
.flatpickr-weekdays {
|
||||
block-size: 2.125rem;
|
||||
padding-inline: 0.875rem;
|
||||
}
|
||||
|
||||
.flatpickr-days {
|
||||
min-inline-size: 16.625rem;
|
||||
|
||||
.dayContainer {
|
||||
justify-content: center !important;
|
||||
inline-size: 16.625rem;
|
||||
min-inline-size: 16.625rem;
|
||||
padding-block-end: 0.75rem;
|
||||
padding-block-start: 0;
|
||||
|
||||
.flatpickr-day {
|
||||
block-size: 2.125rem;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 2.125rem;
|
||||
margin-block-start: 0 !important;
|
||||
max-inline-size: 2.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flatpickr-day {
|
||||
color: $body-color;
|
||||
|
||||
&.today {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
|
||||
&:hover {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background: transparent;
|
||||
color: $body-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.selected,
|
||||
&.selected:hover {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background: rgb(var(--v-theme-primary));
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
|
||||
@include mixins.elevation(2);
|
||||
}
|
||||
|
||||
&.inRange,
|
||||
&.inRange:hover {
|
||||
border: none;
|
||||
background: rgba(var(--v-theme-primary), var(--v-activated-opacity)) !important;
|
||||
box-shadow: none !important;
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
&.startRange {
|
||||
@include mixins.elevation(2);
|
||||
}
|
||||
|
||||
&.endRange {
|
||||
@include mixins.elevation(2);
|
||||
}
|
||||
|
||||
&.startRange,
|
||||
&.endRange,
|
||||
&.startRange:hover,
|
||||
&.endRange:hover {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background: rgb(var(--v-theme-primary));
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
}
|
||||
|
||||
&.selected.startRange + .endRange:not(:nth-child(7n + 1)),
|
||||
&.startRange.startRange + .endRange:not(:nth-child(7n + 1)),
|
||||
&.endRange.startRange + .endRange:not(:nth-child(7n + 1)) {
|
||||
box-shadow: -10px 0 0 rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
&.flatpickr-disabled,
|
||||
&.prevMonthDay:not(.startRange,.inRange),
|
||||
&.nextMonthDay:not(.endRange,.inRange) {
|
||||
opacity: var(--v-disabled-opacity);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.flatpickr-weekday {
|
||||
color: $heading-color;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.flatpickr-days {
|
||||
inline-size: 16.625rem;
|
||||
}
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-months {
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
|
||||
.flatpickr-prev-month,
|
||||
.flatpickr-next-month {
|
||||
fill: $body-color;
|
||||
|
||||
&:hover i,
|
||||
&:hover svg {
|
||||
fill: $body-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flatpickr-current-month span.cur-month {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
&.open {
|
||||
// Open calendar above overlay
|
||||
z-index: 2401;
|
||||
}
|
||||
|
||||
&.hasTime.open {
|
||||
.flatpickr-time {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.flatpickr-hour,
|
||||
.flatpickr-minute,
|
||||
.flatpickr-am-pm {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-theme--dark .flatpickr-calendar {
|
||||
box-shadow: 0 3px 14px 0 rgb(15 20 34 / 38%);
|
||||
}
|
||||
|
||||
// Time picker hover & focus bg color
|
||||
.flatpickr-time input:hover,
|
||||
.flatpickr-time .flatpickr-am-pm:hover,
|
||||
.flatpickr-time input:focus,
|
||||
.flatpickr-time .flatpickr-am-pm:focus {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// Time picker
|
||||
.flatpickr-time {
|
||||
.flatpickr-am-pm,
|
||||
.flatpickr-time-separator,
|
||||
input {
|
||||
color: $body-color;
|
||||
}
|
||||
|
||||
.numInputWrapper {
|
||||
span {
|
||||
&.arrowUp {
|
||||
&::after {
|
||||
border-block-end-color: rgb(var(--v-border-color));
|
||||
}
|
||||
}
|
||||
|
||||
&.arrowDown {
|
||||
&::after {
|
||||
border-block-start-color: rgb(var(--v-border-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Added bg color for flatpickr input only as it has default readonly attribute
|
||||
.flatpickr-input[readonly],
|
||||
.flatpickr-input ~ .form-control[readonly],
|
||||
.flatpickr-human-friendly[readonly] {
|
||||
background-color: inherit;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
// week sections
|
||||
.flatpickr-weekdays {
|
||||
margin-block: 12px;
|
||||
}
|
||||
|
||||
// Month and year section
|
||||
.flatpickr-current-month {
|
||||
.flatpickr-monthDropdown-months {
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.flatpickr-monthDropdown-months,
|
||||
.numInputWrapper {
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
color: $heading-color;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease-out;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flatpickr-monthDropdown-month {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
.numInput.cur-year {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flatpickr-day.flatpickr-disabled,
|
||||
.flatpickr-day.flatpickr-disabled:hover {
|
||||
color: $body-color;
|
||||
}
|
||||
|
||||
.flatpickr-months {
|
||||
padding-block: 0.75rem;
|
||||
padding-inline: 1rem;
|
||||
|
||||
.flatpickr-prev-month,
|
||||
.flatpickr-next-month {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 5rem;
|
||||
background: rgba(var(--v-theme-surface-variant), var(--v-selected-opacity));
|
||||
block-size: 1.75rem;
|
||||
inline-size: 1.75rem;
|
||||
inset-block-start: 0.75rem !important;
|
||||
margin-block: 0.1875rem;
|
||||
padding-block: 0.25rem;
|
||||
padding-inline: 0.4375rem;
|
||||
}
|
||||
|
||||
.flatpickr-next-month {
|
||||
inset-inline-end: 1.05rem !important;
|
||||
}
|
||||
|
||||
.flatpickr-prev-month {
|
||||
/* stylelint-disable-next-line liberty/use-logical-spec */
|
||||
right: 3.8rem;
|
||||
left: unset !important;
|
||||
}
|
||||
|
||||
.flatpickr-month {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
block-size: 2.125rem;
|
||||
|
||||
.flatpickr-current-month {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
block-size: 1.75rem;
|
||||
inset-inline-start: 0;
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update hour font-weight
|
||||
.flatpickr-time input.flatpickr-hour {
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
78
resources/js/@core/app-form-elements/AppOtpInput.vue
Normal file
78
resources/js/@core/app-form-elements/AppOtpInput.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
totalInput: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 6,
|
||||
},
|
||||
default: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['updateOtp'])
|
||||
|
||||
const digits = ref([])
|
||||
const refOtpComp = ref(null)
|
||||
|
||||
digits.value = props.default.split('')
|
||||
|
||||
const defaultStyle = { style: 'max-width: 54px; text-align: center;' }
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleKeyDown = (event, index) => {
|
||||
if (event.code !== 'Tab' && event.code !== 'ArrowRight' && event.code !== 'ArrowLeft')
|
||||
event.preventDefault()
|
||||
if (event.code === 'Backspace') {
|
||||
digits.value[index - 1] = ''
|
||||
if (refOtpComp.value !== null && index > 1) {
|
||||
const inputEl = refOtpComp.value.children[index - 2].querySelector('input')
|
||||
if (inputEl)
|
||||
inputEl.focus()
|
||||
}
|
||||
}
|
||||
const numberRegExp = /^([0-9])$/
|
||||
if (numberRegExp.test(event.key)) {
|
||||
digits.value[index - 1] = event.key
|
||||
if (refOtpComp.value !== null && index !== 0 && index < refOtpComp.value.children.length) {
|
||||
const inputEl = refOtpComp.value.children[index].querySelector('input')
|
||||
if (inputEl)
|
||||
inputEl.focus()
|
||||
}
|
||||
}
|
||||
emit('updateOtp', digits.value.join(''))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h6 class="text-h6 mb-3">
|
||||
Type your 6 digit security code
|
||||
</h6>
|
||||
<div
|
||||
ref="refOtpComp"
|
||||
class="d-flex align-center gap-4"
|
||||
>
|
||||
<AppTextField
|
||||
v-for="i in props.totalInput"
|
||||
:key="i"
|
||||
:model-value="digits[i - 1]"
|
||||
v-bind="defaultStyle"
|
||||
maxlength="1"
|
||||
@keydown="handleKeyDown($event, i)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-field__field {
|
||||
input {
|
||||
padding: 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
49
resources/js/@core/app-form-elements/AppSelect.vue
Normal file
49
resources/js/@core/app-form-elements/AppSelect.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'AppSelect',
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const elementId = computed(() => {
|
||||
const attrs = useAttrs()
|
||||
const _elementIdToken = attrs.id || attrs.label
|
||||
|
||||
return _elementIdToken ? `app-select-${ _elementIdToken }-${ Math.random().toString(36).slice(2, 7) }` : undefined
|
||||
})
|
||||
|
||||
const label = computed(() => useAttrs().label)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="app-select flex-grow-1"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<VLabel
|
||||
v-if="label"
|
||||
:for="elementId"
|
||||
class="mb-1 text-body-2 text-high-emphasis"
|
||||
:text="label"
|
||||
/>
|
||||
<VSelect
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
class: null,
|
||||
label: undefined,
|
||||
variant: 'outlined',
|
||||
id: elementId,
|
||||
menuProps: { contentClass: ['app-inner-list', 'app-select__content', 'v-select__content', $attrs.multiple !== undefined ? 'v-list-select-multiple' : ''] },
|
||||
}"
|
||||
>
|
||||
<template
|
||||
v-for="(_, name) in $slots"
|
||||
#[name]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="name"
|
||||
v-bind="slotProps || {}"
|
||||
/>
|
||||
</template>
|
||||
</VSelect>
|
||||
</div>
|
||||
</template>
|
48
resources/js/@core/app-form-elements/AppTextField.vue
Normal file
48
resources/js/@core/app-form-elements/AppTextField.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'AppTextField',
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const elementId = computed(() => {
|
||||
const attrs = useAttrs()
|
||||
const _elementIdToken = attrs.id || attrs.label
|
||||
|
||||
return _elementIdToken ? `app-text-field-${ _elementIdToken }-${ Math.random().toString(36).slice(2, 7) }` : undefined
|
||||
})
|
||||
|
||||
const label = computed(() => useAttrs().label)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="app-text-field flex-grow-1"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<VLabel
|
||||
v-if="label"
|
||||
:for="elementId"
|
||||
class="mb-1 text-body-2 text-high-emphasis"
|
||||
:text="label"
|
||||
/>
|
||||
<VTextField
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
class: null,
|
||||
label: undefined,
|
||||
variant: 'outlined',
|
||||
id: elementId,
|
||||
}"
|
||||
>
|
||||
<template
|
||||
v-for="(_, name) in $slots"
|
||||
#[name]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="name"
|
||||
v-bind="slotProps || {}"
|
||||
/>
|
||||
</template>
|
||||
</VTextField>
|
||||
</div>
|
||||
</template>
|
49
resources/js/@core/app-form-elements/AppTextarea.vue
Normal file
49
resources/js/@core/app-form-elements/AppTextarea.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'AppTextarea',
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
// const { class: _class, label, variant: _, ...restAttrs } = useAttrs()
|
||||
const elementId = computed(() => {
|
||||
const attrs = useAttrs()
|
||||
const _elementIdToken = attrs.id || attrs.label
|
||||
|
||||
return _elementIdToken ? `app-textarea-${ _elementIdToken }-${ Math.random().toString(36).slice(2, 7) }` : undefined
|
||||
})
|
||||
|
||||
const label = computed(() => useAttrs().label)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="app-textarea flex-grow-1"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<VLabel
|
||||
v-if="label"
|
||||
:for="elementId"
|
||||
class="mb-1 text-body-2 text-high-emphasis"
|
||||
:text="label"
|
||||
/>
|
||||
<VTextarea
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
class: null,
|
||||
label: undefined,
|
||||
variant: 'outlined',
|
||||
id: elementId,
|
||||
}"
|
||||
>
|
||||
<template
|
||||
v-for="(_, name) in $slots"
|
||||
#[name]="slotProps"
|
||||
>
|
||||
<slot
|
||||
:name="name"
|
||||
v-bind="slotProps || {}"
|
||||
/>
|
||||
</template>
|
||||
</VTextarea>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
selectedCheckbox: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
checkboxContent: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
gridColumn: {
|
||||
type: null,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:selectedCheckbox'])
|
||||
|
||||
const selectedOption = ref(structuredClone(toRaw(props.selectedCheckbox)))
|
||||
|
||||
watch(selectedOption, () => {
|
||||
emit('update:selectedCheckbox', selectedOption.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow
|
||||
v-if="props.checkboxContent && selectedOption"
|
||||
v-model="selectedOption"
|
||||
>
|
||||
<VCol
|
||||
v-for="item in props.checkboxContent"
|
||||
:key="item.title"
|
||||
v-bind="gridColumn"
|
||||
>
|
||||
<VLabel
|
||||
class="custom-input custom-checkbox-icon rounded cursor-pointer"
|
||||
:class="selectedOption.includes(item.value) ? 'active' : ''"
|
||||
>
|
||||
<slot :item="item">
|
||||
<div class="d-flex flex-column align-center text-center gap-2">
|
||||
<VIcon
|
||||
v-bind="item.icon"
|
||||
class="text-high-emphasis"
|
||||
/>
|
||||
|
||||
<h6 class="cr-title text-base">
|
||||
{{ item.title }}
|
||||
</h6>
|
||||
<p class="text-sm clamp-text mb-0">
|
||||
{{ item.desc }}
|
||||
</p>
|
||||
</div>
|
||||
</slot>
|
||||
<div>
|
||||
<VCheckbox
|
||||
v-model="selectedOption"
|
||||
:value="item.value"
|
||||
/>
|
||||
</div>
|
||||
</VLabel>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-checkbox-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
|
||||
.v-checkbox {
|
||||
margin-block-end: -0.375rem;
|
||||
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cr-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-checkbox-icon {
|
||||
.v-checkbox {
|
||||
margin-block-end: -0.375rem;
|
||||
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
81
resources/js/@core/app-form-elements/CustomRadios.vue
Normal file
81
resources/js/@core/app-form-elements/CustomRadios.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
selectedRadio: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
radioContent: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
gridColumn: {
|
||||
type: null,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:selectedRadio'])
|
||||
|
||||
const selectedOption = ref(structuredClone(toRaw(props.selectedRadio)))
|
||||
|
||||
watch(selectedOption, () => {
|
||||
emit('update:selectedRadio', selectedOption.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRadioGroup
|
||||
v-if="props.radioContent"
|
||||
v-model="selectedOption"
|
||||
>
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="item in props.radioContent"
|
||||
:key="item.title"
|
||||
v-bind="gridColumn"
|
||||
>
|
||||
<VLabel
|
||||
class="custom-input custom-radio rounded cursor-pointer"
|
||||
:class="selectedOption === item.value ? 'active' : ''"
|
||||
>
|
||||
<div>
|
||||
<VRadio :value="item.value" />
|
||||
</div>
|
||||
<slot :item="item">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<h6 class="cr-title text-base">
|
||||
{{ item.title }}
|
||||
</h6>
|
||||
<VSpacer />
|
||||
<span
|
||||
v-if="item.subtitle"
|
||||
class="text-disabled text-base"
|
||||
>{{ item.subtitle }}</span>
|
||||
</div>
|
||||
<p class="text-sm mb-0">
|
||||
{{ item.desc }}
|
||||
</p>
|
||||
</div>
|
||||
</slot>
|
||||
</VLabel>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VRadioGroup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-radio {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.375rem;
|
||||
|
||||
.v-radio {
|
||||
margin-block-start: -0.25rem;
|
||||
}
|
||||
|
||||
.cr-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
selectedRadio: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
radioContent: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
gridColumn: {
|
||||
type: null,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:selectedRadio'])
|
||||
|
||||
const selectedOption = ref(structuredClone(toRaw(props.selectedRadio)))
|
||||
|
||||
watch(selectedOption, () => {
|
||||
emit('update:selectedRadio', selectedOption.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRadioGroup
|
||||
v-if="props.radioContent"
|
||||
v-model="selectedOption"
|
||||
>
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="item in props.radioContent"
|
||||
:key="item.title"
|
||||
v-bind="gridColumn"
|
||||
>
|
||||
<VLabel
|
||||
class="custom-input custom-radio-icon rounded cursor-pointer"
|
||||
:class="selectedOption === item.value ? 'active' : ''"
|
||||
>
|
||||
<slot :item="item">
|
||||
<div class="d-flex flex-column align-center text-center gap-2">
|
||||
<VIcon
|
||||
v-bind="item.icon"
|
||||
class="text-high-emphasis"
|
||||
/>
|
||||
<h6 class="cr-title text-base">
|
||||
{{ item.title }}
|
||||
</h6>
|
||||
|
||||
<p class="text-sm mb-0 clamp-text">
|
||||
{{ item.desc }}
|
||||
</p>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<div>
|
||||
<VRadio :value="item.value" />
|
||||
</div>
|
||||
</VLabel>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VRadioGroup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-radio-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
|
||||
.v-radio {
|
||||
margin-block-end: -0.25rem;
|
||||
}
|
||||
|
||||
.cr-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-radio-icon {
|
||||
.v-radio {
|
||||
margin-block-end: -0.25rem;
|
||||
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
selectedRadio: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
radioContent: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
gridColumn: {
|
||||
type: null,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:selectedRadio'])
|
||||
|
||||
const selectedOption = ref(structuredClone(toRaw(props.selectedRadio)))
|
||||
|
||||
watch(selectedOption, () => {
|
||||
emit('update:selectedRadio', selectedOption.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRadioGroup
|
||||
v-if="props.radioContent"
|
||||
v-model="selectedOption"
|
||||
>
|
||||
<VRow>
|
||||
<VCol
|
||||
v-for="item in props.radioContent"
|
||||
:key="item.bgImage"
|
||||
v-bind="gridColumn"
|
||||
>
|
||||
<VLabel
|
||||
class="custom-input custom-radio rounded cursor-pointer w-100"
|
||||
:class="selectedOption === item.value ? 'active' : ''"
|
||||
>
|
||||
<img
|
||||
:src="item.bgImage"
|
||||
alt="bg-img"
|
||||
class="custom-radio-image"
|
||||
>
|
||||
<VRadio :value="item.value" />
|
||||
</VLabel>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VRadioGroup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-radio {
|
||||
padding: 0;
|
||||
border-width: 2px;
|
||||
|
||||
.custom-radio-image {
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
min-inline-size: 100%;
|
||||
}
|
||||
|
||||
.v-radio {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
162
resources/js/@core/cards/AppCardActions.vue
Normal file
162
resources/js/@core/cards/AppCardActions.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
noActions: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
actionCollapsed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
actionRefresh: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
actionRemove: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'collapsed',
|
||||
'refresh',
|
||||
'trash',
|
||||
])
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const isContentCollapsed = ref(props.collapsed)
|
||||
const isCardRemoved = ref(false)
|
||||
const isOverlayVisible = ref(false)
|
||||
|
||||
// hiding overlay
|
||||
const hideOverlay = () => {
|
||||
isOverlayVisible.value = false
|
||||
}
|
||||
|
||||
// trigger collapse
|
||||
const triggerCollapse = () => {
|
||||
isContentCollapsed.value = !isContentCollapsed.value
|
||||
emit('collapsed', isContentCollapsed.value)
|
||||
}
|
||||
|
||||
// trigger refresh
|
||||
const triggerRefresh = () => {
|
||||
isOverlayVisible.value = true
|
||||
emit('refresh', hideOverlay)
|
||||
}
|
||||
|
||||
// trigger removal
|
||||
const triggeredRemove = () => {
|
||||
isCardRemoved.value = true
|
||||
emit('trash')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VExpandTransition>
|
||||
<!-- TODO remove div when transition work with v-card components: https://github.com/vuetifyjs/vuetify/issues/15111 -->
|
||||
<div v-if="!isCardRemoved">
|
||||
<VCard v-bind="$attrs">
|
||||
<VCardItem>
|
||||
<VCardTitle v-if="props.title || $slots.title">
|
||||
<!-- 👉 Title slot and prop -->
|
||||
<slot name="title">
|
||||
{{ props.title }}
|
||||
</slot>
|
||||
</VCardTitle>
|
||||
|
||||
<template #append>
|
||||
<!-- 👉 Before actions slot -->
|
||||
<div>
|
||||
<slot name="before-actions" />
|
||||
|
||||
<!-- SECTION Actions buttons -->
|
||||
|
||||
<!-- 👉 Collapse button -->
|
||||
<IconBtn
|
||||
v-if="(!(actionRemove || actionRefresh) || actionCollapsed) && !noActions"
|
||||
@click="triggerCollapse"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="tabler-chevron-up"
|
||||
:style="{ transform: isContentCollapsed ? 'rotate(-180deg)' : null }"
|
||||
style="transition-duration: 0.28s;"
|
||||
/>
|
||||
</IconBtn>
|
||||
|
||||
<!-- 👉 Overlay button -->
|
||||
<IconBtn
|
||||
v-if="(!(actionRemove || actionCollapsed) || actionRefresh) && !noActions"
|
||||
@click="triggerRefresh"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="tabler-refresh"
|
||||
/>
|
||||
</IconBtn>
|
||||
|
||||
<!-- 👉 Close button -->
|
||||
<IconBtn
|
||||
v-if="(!(actionRefresh || actionCollapsed) || actionRemove) && !noActions"
|
||||
@click="triggeredRemove"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="tabler-x"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
<!-- !SECTION -->
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 👉 card content -->
|
||||
<VExpandTransition>
|
||||
<div
|
||||
v-show="!isContentCollapsed"
|
||||
class="v-card-content"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
|
||||
<!-- 👉 Overlay -->
|
||||
<VOverlay
|
||||
v-model="isOverlayVisible"
|
||||
contained
|
||||
persistent
|
||||
class="align-center justify-center"
|
||||
>
|
||||
<VProgressCircular indeterminate />
|
||||
</VOverlay>
|
||||
</VCard>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.v-card-item {
|
||||
+.v-card-content {
|
||||
.v-card-text:first-child {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
128
resources/js/@core/cards/AppCardCode.vue
Normal file
128
resources/js/@core/cards/AppCardCode.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import 'prismjs'
|
||||
import 'prismjs/themes/prism-tomorrow.css'
|
||||
import Prism from 'vue-prism-component'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
code: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
codeLanguage: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'markup',
|
||||
},
|
||||
noPadding: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const preferredCodeLanguage = useStorage('preferredCodeLanguage', 'ts')
|
||||
const isCodeShown = ref(false)
|
||||
const { copy, copied } = useClipboard({ source: computed(() => props.code[preferredCodeLanguage.value]) })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ props.title }}</VCardTitle>
|
||||
<template #append>
|
||||
<IconBtn
|
||||
size="small"
|
||||
:color="isCodeShown ? 'primary' : 'default'"
|
||||
:class="isCodeShown ? '' : 'text-disabled'"
|
||||
@click="isCodeShown = !isCodeShown"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="tabler-code"
|
||||
/>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VCardItem>
|
||||
<slot v-if="noPadding" />
|
||||
<VCardText v-else>
|
||||
<slot />
|
||||
</VCardText>
|
||||
<VExpandTransition>
|
||||
<div v-show="isCodeShown">
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="d-flex gap-y-3 flex-column">
|
||||
<div class="d-flex justify-end">
|
||||
<VBtnToggle
|
||||
v-model="preferredCodeLanguage"
|
||||
mandatory
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
>
|
||||
<VBtn
|
||||
size="x-small"
|
||||
value="ts"
|
||||
:color="preferredCodeLanguage === 'ts' ? 'primary' : 'default'"
|
||||
>
|
||||
<VIcon
|
||||
size="x-large"
|
||||
icon="custom-typescript"
|
||||
:color="preferredCodeLanguage === 'ts' ? 'primary' : 'secondary'"
|
||||
/>
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="x-small"
|
||||
value="js"
|
||||
:color="preferredCodeLanguage === 'js' ? 'primary' : 'default'"
|
||||
>
|
||||
<VIcon
|
||||
size="x-large"
|
||||
icon="custom-javascript"
|
||||
:color="preferredCodeLanguage === 'js' ? 'primary' : 'secondary'"
|
||||
/>
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
</div>
|
||||
|
||||
<div class="position-relative">
|
||||
<Prism
|
||||
:key="props.code[preferredCodeLanguage]"
|
||||
:language="props.codeLanguage"
|
||||
:style="$vuetify.locale.isRtl ? 'text-align: right' : 'text-align: left'"
|
||||
>
|
||||
{{ props.code[preferredCodeLanguage] }}
|
||||
</Prism>
|
||||
<IconBtn
|
||||
class="position-absolute app-card-code-copy-icon"
|
||||
color="white"
|
||||
@click="() => { copy() }"
|
||||
>
|
||||
<VIcon
|
||||
:icon="copied ? 'tabler-check' : 'tabler-copy'"
|
||||
size="20"
|
||||
/>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</VCardText>
|
||||
</div>
|
||||
</VExpandTransition>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@styles/variables/_vuetify.scss";
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
border-radius: vuetify.$card-border-radius;
|
||||
}
|
||||
|
||||
.app-card-code-copy-icon {
|
||||
inset-block-start: 1.2em;
|
||||
inset-inline-end: 0.8em;
|
||||
}
|
||||
</style>
|
41
resources/js/@core/cards/CardStatisticsHorizontal.vue
Normal file
41
resources/js/@core/cards/CardStatisticsHorizontal.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
stats: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="d-flex align-center flex-wrap">
|
||||
<span class="text-h5">{{ props.stats }}</span>
|
||||
</div>
|
||||
<span class="text-body-2">{{ props.title }}</span>
|
||||
</div>
|
||||
|
||||
<VAvatar
|
||||
:icon="props.icon"
|
||||
:color="props.color"
|
||||
:size="42"
|
||||
variant="tonal"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
61
resources/js/@core/cards/CardStatisticsVertical.vue
Normal file
61
resources/js/@core/cards/CardStatisticsVertical.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
stats: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
series: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
chartOptions: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardText class="d-flex flex-column pb-0">
|
||||
<VAvatar
|
||||
v-if="props.icon"
|
||||
size="42"
|
||||
variant="tonal"
|
||||
:color="props.color"
|
||||
:icon="props.icon"
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<h6 class="text-lg font-weight-medium">
|
||||
{{ props.stats }}
|
||||
</h6>
|
||||
<span class="text-sm">{{ props.title }}</span>
|
||||
</VCardText>
|
||||
|
||||
<VueApexCharts
|
||||
:series="props.series"
|
||||
:options="props.chartOptions"
|
||||
:height="props.height"
|
||||
/>
|
||||
</VCard>
|
||||
</template>
|
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>
|
23
resources/js/@core/composable/useResponsiveSidebar.js
Normal file
23
resources/js/@core/composable/useResponsiveSidebar.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
export const useResponsiveLeftSidebar = (mobileBreakpoint = undefined) => {
|
||||
const { mdAndDown, name: currentBreakpoint } = useDisplay()
|
||||
const _mobileBreakpoint = mobileBreakpoint || mdAndDown
|
||||
const isLeftSidebarOpen = ref(true)
|
||||
|
||||
const setInitialValue = () => {
|
||||
isLeftSidebarOpen.value = !_mobileBreakpoint.value
|
||||
}
|
||||
|
||||
|
||||
// Set the initial value of sidebar
|
||||
setInitialValue()
|
||||
watch(currentBreakpoint, () => {
|
||||
// Reset left sidebar
|
||||
isLeftSidebarOpen.value = !_mobileBreakpoint.value
|
||||
})
|
||||
|
||||
return {
|
||||
isLeftSidebarOpen,
|
||||
}
|
||||
}
|
681
resources/js/@core/libs/apex-chart/apexCharConfig.js
Normal file
681
resources/js/@core/libs/apex-chart/apexCharConfig.js
Normal file
@@ -0,0 +1,681 @@
|
||||
import { hexToRgb } from '@layouts/utils'
|
||||
|
||||
|
||||
// 👉 Colors variables
|
||||
const colorVariables = themeColors => {
|
||||
const themeSecondaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['medium-emphasis-opacity']})`
|
||||
const themeDisabledTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['disabled-opacity']})`
|
||||
const themeBorderColor = `rgba(${hexToRgb(String(themeColors.variables['border-color']))},${themeColors.variables['border-opacity']})`
|
||||
const themePrimaryTextColor = `rgba(${hexToRgb(themeColors.colors['on-surface'])},${themeColors.variables['high-emphasis-opacity']})`
|
||||
|
||||
return { themeSecondaryTextColor, themeDisabledTextColor, themeBorderColor, themePrimaryTextColor }
|
||||
}
|
||||
|
||||
export const getScatterChartConfig = themeColors => {
|
||||
const scatterColors = {
|
||||
series1: '#ff9f43',
|
||||
series2: '#7367f0',
|
||||
series3: '#28c76f',
|
||||
}
|
||||
|
||||
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
zoom: {
|
||||
type: 'xy',
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'left',
|
||||
markers: { offsetX: -3 },
|
||||
labels: { colors: themeSecondaryTextColor },
|
||||
itemMargin: {
|
||||
vertical: 3,
|
||||
horizontal: 10,
|
||||
},
|
||||
},
|
||||
colors: [scatterColors.series1, scatterColors.series2, scatterColors.series3],
|
||||
grid: {
|
||||
borderColor: themeBorderColor,
|
||||
xaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
tickAmount: 10,
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { color: themeBorderColor },
|
||||
crosshairs: {
|
||||
stroke: { color: themeBorderColor },
|
||||
},
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
formatter: val => parseFloat(val).toFixed(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getLineChartSimpleConfig = themeColors => {
|
||||
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
zoom: { enabled: false },
|
||||
toolbar: { show: false },
|
||||
},
|
||||
colors: ['#ff9f43'],
|
||||
stroke: { curve: 'straight' },
|
||||
dataLabels: { enabled: false },
|
||||
markers: {
|
||||
strokeWidth: 7,
|
||||
strokeOpacity: 1,
|
||||
colors: ['#ff9f43'],
|
||||
strokeColors: ['#fff'],
|
||||
},
|
||||
grid: {
|
||||
padding: { top: -10 },
|
||||
borderColor: themeBorderColor,
|
||||
xaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
custom(data) {
|
||||
return `<div class='bar-chart pa-2'>
|
||||
<span>${data.series[data.seriesIndex][data.dataPointIndex]}%</span>
|
||||
</div>`
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { color: themeBorderColor },
|
||||
crosshairs: {
|
||||
stroke: { color: themeBorderColor },
|
||||
},
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
categories: [
|
||||
'7/12',
|
||||
'8/12',
|
||||
'9/12',
|
||||
'10/12',
|
||||
'11/12',
|
||||
'12/12',
|
||||
'13/12',
|
||||
'14/12',
|
||||
'15/12',
|
||||
'16/12',
|
||||
'17/12',
|
||||
'18/12',
|
||||
'19/12',
|
||||
'20/12',
|
||||
'21/12',
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getBarChartConfig = themeColors => {
|
||||
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
colors: ['#00cfe8'],
|
||||
dataLabels: { enabled: false },
|
||||
plotOptions: {
|
||||
bar: {
|
||||
borderRadius: 8,
|
||||
barHeight: '30%',
|
||||
horizontal: true,
|
||||
startingShape: 'rounded',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: themeBorderColor,
|
||||
xaxis: {
|
||||
lines: { show: false },
|
||||
},
|
||||
padding: {
|
||||
top: -10,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { color: themeBorderColor },
|
||||
categories: ['MON, 11', 'THU, 14', 'FRI, 15', 'MON, 18', 'WED, 20', 'FRI, 21', 'MON, 23'],
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getCandlestickChartConfig = themeColors => {
|
||||
const candlestickColors = {
|
||||
series1: '#28c76f',
|
||||
series2: '#ea5455',
|
||||
}
|
||||
|
||||
const { themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
plotOptions: {
|
||||
bar: { columnWidth: '40%' },
|
||||
candlestick: {
|
||||
colors: {
|
||||
upward: candlestickColors.series1,
|
||||
downward: candlestickColors.series2,
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
padding: { top: -10 },
|
||||
borderColor: themeBorderColor,
|
||||
xaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
tooltip: { enabled: true },
|
||||
crosshairs: {
|
||||
stroke: { color: themeBorderColor },
|
||||
},
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { color: themeBorderColor },
|
||||
crosshairs: {
|
||||
stroke: { color: themeBorderColor },
|
||||
},
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getRadialBarChartConfig = themeColors => {
|
||||
const radialBarColors = {
|
||||
series1: '#fdd835',
|
||||
series2: '#32baff',
|
||||
series3: '#00d4bd',
|
||||
series4: '#7367f0',
|
||||
series5: '#FFA1A1',
|
||||
}
|
||||
|
||||
const { themeSecondaryTextColor, themePrimaryTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
stroke: { lineCap: 'round' },
|
||||
labels: ['Comments', 'Replies', 'Shares'],
|
||||
legend: {
|
||||
show: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
colors: themeSecondaryTextColor,
|
||||
},
|
||||
markers: {
|
||||
offsetX: -3,
|
||||
},
|
||||
itemMargin: {
|
||||
vertical: 3,
|
||||
horizontal: 10,
|
||||
},
|
||||
},
|
||||
colors: [radialBarColors.series1, radialBarColors.series2, radialBarColors.series4],
|
||||
plotOptions: {
|
||||
radialBar: {
|
||||
hollow: { size: '30%' },
|
||||
track: {
|
||||
margin: 15,
|
||||
background: themeColors.colors['grey-100'],
|
||||
},
|
||||
dataLabels: {
|
||||
name: {
|
||||
fontSize: '2rem',
|
||||
},
|
||||
value: {
|
||||
fontSize: '1rem',
|
||||
color: themeSecondaryTextColor,
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
fontWeight: 400,
|
||||
label: 'Comments',
|
||||
fontSize: '1.125rem',
|
||||
color: themePrimaryTextColor,
|
||||
formatter(w) {
|
||||
const totalValue = w.globals.seriesTotals.reduce((a, b) => {
|
||||
return a + b
|
||||
}, 0) / w.globals.series.length
|
||||
|
||||
if (totalValue % 1 === 0)
|
||||
return `${totalValue}%`
|
||||
else
|
||||
return `${totalValue.toFixed(2)}%`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
top: -30,
|
||||
bottom: -25,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getDonutChartConfig = themeColors => {
|
||||
const donutColors = {
|
||||
series1: '#fdd835',
|
||||
series2: '#00d4bd',
|
||||
series3: '#826bf8',
|
||||
series4: '#32baff',
|
||||
series5: '#ffa1a1',
|
||||
}
|
||||
|
||||
const { themeSecondaryTextColor, themePrimaryTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
stroke: { width: 0 },
|
||||
labels: ['Operational', 'Networking', 'Hiring', 'R&D'],
|
||||
colors: [donutColors.series1, donutColors.series5, donutColors.series3, donutColors.series2],
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: val => `${parseInt(val, 10)}%`,
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
markers: { offsetX: -3 },
|
||||
labels: { colors: themeSecondaryTextColor },
|
||||
itemMargin: {
|
||||
vertical: 3,
|
||||
horizontal: 10,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
labels: {
|
||||
show: true,
|
||||
name: {
|
||||
fontSize: '1.5rem',
|
||||
},
|
||||
value: {
|
||||
fontSize: '1.5rem',
|
||||
color: themeSecondaryTextColor,
|
||||
formatter: val => `${parseInt(val, 10)}`,
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
fontSize: '1.5rem',
|
||||
label: 'Operational',
|
||||
formatter: () => '31%',
|
||||
color: themePrimaryTextColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 992,
|
||||
options: {
|
||||
chart: {
|
||||
height: 380,
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
breakpoint: 576,
|
||||
options: {
|
||||
chart: {
|
||||
height: 320,
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
labels: {
|
||||
show: true,
|
||||
name: {
|
||||
fontSize: '1rem',
|
||||
},
|
||||
value: {
|
||||
fontSize: '1rem',
|
||||
},
|
||||
total: {
|
||||
fontSize: '1rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
export const getAreaChartSplineConfig = themeColors => {
|
||||
const areaColors = {
|
||||
series3: '#e0cffe',
|
||||
series2: '#b992fe',
|
||||
series1: '#ab7efd',
|
||||
}
|
||||
|
||||
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
tooltip: { shared: false },
|
||||
dataLabels: { enabled: false },
|
||||
stroke: {
|
||||
show: false,
|
||||
curve: 'straight',
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'left',
|
||||
labels: { colors: themeSecondaryTextColor },
|
||||
markers: {
|
||||
offsetY: 1,
|
||||
offsetX: -3,
|
||||
},
|
||||
itemMargin: {
|
||||
vertical: 3,
|
||||
horizontal: 10,
|
||||
},
|
||||
},
|
||||
colors: [areaColors.series3, areaColors.series2, areaColors.series1],
|
||||
fill: {
|
||||
opacity: 1,
|
||||
type: 'solid',
|
||||
},
|
||||
grid: {
|
||||
show: true,
|
||||
borderColor: themeBorderColor,
|
||||
xaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { color: themeBorderColor },
|
||||
crosshairs: {
|
||||
stroke: { color: themeBorderColor },
|
||||
},
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
categories: [
|
||||
'7/12',
|
||||
'8/12',
|
||||
'9/12',
|
||||
'10/12',
|
||||
'11/12',
|
||||
'12/12',
|
||||
'13/12',
|
||||
'14/12',
|
||||
'15/12',
|
||||
'16/12',
|
||||
'17/12',
|
||||
'18/12',
|
||||
'19/12',
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getColumnChartConfig = themeColors => {
|
||||
const columnColors = {
|
||||
series1: '#826af9',
|
||||
series2: '#d2b0ff',
|
||||
bg: '#f8d3ff',
|
||||
}
|
||||
|
||||
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
chart: {
|
||||
offsetX: -10,
|
||||
stacked: true,
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
fill: { opacity: 1 },
|
||||
dataLabels: { enabled: false },
|
||||
colors: [columnColors.series1, columnColors.series2],
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'left',
|
||||
labels: { colors: themeSecondaryTextColor },
|
||||
markers: {
|
||||
offsetY: 1,
|
||||
offsetX: -3,
|
||||
},
|
||||
itemMargin: {
|
||||
vertical: 3,
|
||||
horizontal: 10,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
show: true,
|
||||
colors: ['transparent'],
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '15%',
|
||||
colors: {
|
||||
backgroundBarRadius: 10,
|
||||
backgroundBarColors: [columnColors.bg, columnColors.bg, columnColors.bg, columnColors.bg, columnColors.bg],
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
borderColor: themeBorderColor,
|
||||
xaxis: {
|
||||
lines: { show: true },
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
axisBorder: { show: false },
|
||||
axisTicks: { color: themeBorderColor },
|
||||
categories: ['7/12', '8/12', '9/12', '10/12', '11/12', '12/12', '13/12', '14/12', '15/12'],
|
||||
crosshairs: {
|
||||
stroke: { color: themeBorderColor },
|
||||
},
|
||||
labels: {
|
||||
style: { colors: themeDisabledTextColor },
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 600,
|
||||
options: {
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '35%',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
export const getHeatMapChartConfig = themeColors => {
|
||||
const { themeSecondaryTextColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
},
|
||||
dataLabels: { enabled: false },
|
||||
stroke: {
|
||||
colors: [themeColors.colors.surface],
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
colors: themeSecondaryTextColor,
|
||||
},
|
||||
markers: {
|
||||
offsetY: 0,
|
||||
offsetX: -3,
|
||||
},
|
||||
itemMargin: {
|
||||
vertical: 3,
|
||||
horizontal: 10,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
heatmap: {
|
||||
enableShades: false,
|
||||
colorScale: {
|
||||
ranges: [
|
||||
{ to: 10, from: 0, name: '0-10', color: '#b9b3f8' },
|
||||
{ to: 20, from: 11, name: '10-20', color: '#aba4f6' },
|
||||
{ to: 30, from: 21, name: '20-30', color: '#9d95f5' },
|
||||
{ to: 40, from: 31, name: '30-40', color: '#8f85f3' },
|
||||
{ to: 50, from: 41, name: '40-50', color: '#8176f2' },
|
||||
{ to: 60, from: 51, name: '50-60', color: '#7367f0' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
padding: { top: -20 },
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: themeDisabledTextColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
labels: { show: false },
|
||||
axisTicks: { show: false },
|
||||
axisBorder: { show: false },
|
||||
},
|
||||
}
|
||||
}
|
||||
export const getRadarChartConfig = themeColors => {
|
||||
const radarColors = {
|
||||
series1: '#9b88fa',
|
||||
series2: '#ffa1a1',
|
||||
}
|
||||
|
||||
const { themeSecondaryTextColor, themeBorderColor, themeDisabledTextColor } = colorVariables(themeColors)
|
||||
|
||||
return {
|
||||
chart: {
|
||||
parentHeightOffset: 0,
|
||||
toolbar: { show: false },
|
||||
dropShadow: {
|
||||
top: 1,
|
||||
blur: 8,
|
||||
left: 1,
|
||||
opacity: 0.2,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
markers: { size: 0 },
|
||||
fill: { opacity: [1, 0.8] },
|
||||
colors: [radarColors.series1, radarColors.series2],
|
||||
stroke: {
|
||||
width: 0,
|
||||
show: false,
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: themeSecondaryTextColor,
|
||||
},
|
||||
markers: {
|
||||
offsetX: -3,
|
||||
},
|
||||
itemMargin: {
|
||||
vertical: 3,
|
||||
horizontal: 10,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
radar: {
|
||||
polygons: {
|
||||
strokeColors: themeBorderColor,
|
||||
connectorColors: themeBorderColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
show: false,
|
||||
padding: {
|
||||
top: -20,
|
||||
bottom: -20,
|
||||
},
|
||||
},
|
||||
yaxis: { show: false },
|
||||
xaxis: {
|
||||
categories: ['Battery', 'Brand', 'Camera', 'Memory', 'Storage', 'Display', 'OS', 'Price'],
|
||||
labels: {
|
||||
style: {
|
||||
colors: [
|
||||
themeDisabledTextColor,
|
||||
themeDisabledTextColor,
|
||||
themeDisabledTextColor,
|
||||
themeDisabledTextColor,
|
||||
themeDisabledTextColor,
|
||||
themeDisabledTextColor,
|
||||
themeDisabledTextColor,
|
||||
themeDisabledTextColor,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
172
resources/js/@core/scss/base/_components.scss
Normal file
172
resources/js/@core/scss/base/_components.scss
Normal file
@@ -0,0 +1,172 @@
|
||||
@use "mixins";
|
||||
@use "@layouts/styles/placeholders";
|
||||
@use "@layouts/styles/mixins" as layoutMixins;
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// 👉 Avatar group
|
||||
.v-avatar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
&:not(:first-child) {
|
||||
margin-inline-start: -0.8rem;
|
||||
}
|
||||
|
||||
transition: transform 0.25s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
transform: translateY(-5px) scale(1.05);
|
||||
|
||||
@include mixins.elevation(3);
|
||||
}
|
||||
}
|
||||
|
||||
> .v-avatar {
|
||||
border: 2px solid rgb(var(--v-theme-surface));
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Button outline with default color border color
|
||||
.v-alert--variant-outlined,
|
||||
.v-avatar--variant-outlined,
|
||||
.v-btn.v-btn--variant-outlined,
|
||||
.v-card--variant-outlined,
|
||||
.v-chip--variant-outlined,
|
||||
.v-list-item--variant-outlined {
|
||||
&:not([class*="text-"]) {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
&.text-default {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Custom Input
|
||||
.v-label.custom-input {
|
||||
padding: 1rem;
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
opacity: 1;
|
||||
white-space: normal;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--v-border-color), 0.25);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
|
||||
.v-icon {
|
||||
color: rgb(var(--v-theme-primary)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Datatable
|
||||
.v-data-table-footer__pagination {
|
||||
@include layoutMixins.rtl {
|
||||
.v-btn {
|
||||
.v-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dialog responsive width
|
||||
.v-dialog {
|
||||
// dialog custom close btn
|
||||
.v-dialog-close-btn {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
|
||||
inset-block-start: 0.9375rem;
|
||||
inset-inline-end: 0.9375rem;
|
||||
|
||||
.v-btn__overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.v-card {
|
||||
@extend %style-scroll-bar;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.v-dialog {
|
||||
&.v-dialog-sm,
|
||||
&.v-dialog-lg,
|
||||
&.v-dialog-xl {
|
||||
.v-overlay__content {
|
||||
inline-size: 565px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.v-dialog {
|
||||
&.v-dialog-lg,
|
||||
&.v-dialog-xl {
|
||||
.v-overlay__content {
|
||||
inline-size: 865px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1264px) {
|
||||
.v-dialog.v-dialog-xl {
|
||||
.v-overlay__content {
|
||||
inline-size: 1165px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v-tab with pill support
|
||||
|
||||
.v-tabs.v-tabs-pill {
|
||||
.v-tab.v-btn {
|
||||
border-radius: 0.25rem !important;
|
||||
transition: none;
|
||||
|
||||
.v-tab__slider {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loop for all colors bg
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
.v-tabs.v-tabs-pill {
|
||||
.v-slide-group-item--active.v-tab--selected.text-#{$color-name} {
|
||||
background-color: rgb(var(--v-theme-#{$color-name}));
|
||||
color: rgb(var(--v-theme-on-#{$color-name})) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ We are make even width of all v-timeline body
|
||||
.v-timeline--vertical.v-timeline {
|
||||
.v-timeline-item {
|
||||
.v-timeline-item__body {
|
||||
justify-self: stretch !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Switch
|
||||
.v-switch .v-selection-control:not(.v-selection-control--dirty) .v-switch__thumb {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
// 👉 Textarea
|
||||
.v-textarea .v-field__input {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-mask-image: none !important;
|
||||
mask-image: none !important;
|
||||
}
|
16
resources/js/@core/scss/base/_dark.scss
Normal file
16
resources/js/@core/scss/base/_dark.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// ————————————————————————————————————
|
||||
// * ——— Perfect Scrollbar
|
||||
// ————————————————————————————————————
|
||||
|
||||
.v-application.v-theme--dark {
|
||||
.ps__rail-y,
|
||||
.ps__rail-x {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.ps__thumb-y {
|
||||
background-color: variables.$plugin-ps-thumb-y-dark;
|
||||
}
|
||||
}
|
103
resources/js/@core/scss/base/_default-layout-w-vertical-nav.scss
Normal file
103
resources/js/@core/scss/base/_default-layout-w-vertical-nav.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@core/scss/base/placeholders" as *;
|
||||
@use "@core/scss/template/placeholders" as *;
|
||||
@use "misc";
|
||||
@use "@core/scss/base/mixins";
|
||||
|
||||
$header: ".layout-navbar";
|
||||
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
$header: ".layout-navbar .navbar-content-container";
|
||||
}
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
// SECTION Layout Navbar
|
||||
// 👉 Elevated navbar
|
||||
@if variables.$vertical-nav-navbar-style == "elevated" {
|
||||
// Add transition
|
||||
#{$header} {
|
||||
transition: padding 0.2s ease, background-color 0.18s ease;
|
||||
}
|
||||
|
||||
// If navbar is contained => Add border radius to header
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
#{$header} {
|
||||
border-radius: 0 0 variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
}
|
||||
|
||||
// Scrolled styles for sticky navbar
|
||||
@at-root {
|
||||
/* ℹ️ This html selector with not selector is required when:
|
||||
dialog is opened and window don't have any scroll. This removes window-scrolled class from layout and out style broke
|
||||
*/
|
||||
html.v-overlay-scroll-blocked:not([style*="--v-body-scroll-y: 0px;"]) .layout-navbar-sticky,
|
||||
&.window-scrolled.layout-navbar-sticky {
|
||||
|
||||
#{$header} {
|
||||
@extend %default-layout-vertical-nav-scrolled-sticky-elevated-nav;
|
||||
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
|
||||
}
|
||||
|
||||
.navbar-blur#{$header} {
|
||||
@extend %blurry-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Floating navbar
|
||||
@else if variables.$vertical-nav-navbar-style == "floating" {
|
||||
// ℹ️ Regardless of navbar is contained or not => Apply overlay to .layout-navbar
|
||||
.layout-navbar {
|
||||
&.navbar-blur {
|
||||
@extend %default-layout-vertical-nav-floating-navbar-overlay;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.layout-navbar-sticky) {
|
||||
#{$header} {
|
||||
margin-block-start: variables.$vertical-nav-floating-navbar-top;
|
||||
}
|
||||
}
|
||||
|
||||
#{$header} {
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness;
|
||||
}
|
||||
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
|
||||
@extend %default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled;
|
||||
}
|
||||
|
||||
.navbar-blur#{$header} {
|
||||
@extend %blurry-bg;
|
||||
}
|
||||
}
|
||||
|
||||
// !SECTION
|
||||
|
||||
// 👉 Layout footer
|
||||
.layout-footer {
|
||||
$ele-layout-footer: &;
|
||||
|
||||
.footer-content-container {
|
||||
border-radius: variables.$default-layout-with-vertical-nav-navbar-footer-roundness variables.$default-layout-with-vertical-nav-navbar-footer-roundness 0 0;
|
||||
|
||||
// Sticky footer
|
||||
@at-root {
|
||||
// ℹ️ .layout-footer-sticky#{$ele-layout-footer} => .layout-footer-sticky.layout-wrapper.layout-nav-type-vertical .layout-footer
|
||||
.layout-footer-sticky#{$ele-layout-footer} {
|
||||
.footer-content-container {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
padding-block: 0;
|
||||
padding-inline: 1.2rem;
|
||||
|
||||
@include mixins.elevation(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
resources/js/@core/scss/base/_default-layout.scss
Normal file
16
resources/js/@core/scss/base/_default-layout.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
@use "@core/scss/base/placeholders";
|
||||
@use "@core/scss/base/variables";
|
||||
|
||||
.layout-vertical-nav,
|
||||
.layout-horizontal-nav {
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
@if variables.$navbar-high-emphasis-text {
|
||||
@extend %layout-navbar;
|
||||
}
|
||||
}
|
40
resources/js/@core/scss/base/_index.scss
Normal file
40
resources/js/@core/scss/base/_index.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
@use "sass:map";
|
||||
|
||||
// Layout
|
||||
@use "vertical-nav";
|
||||
@use "default-layout";
|
||||
@use "default-layout-w-vertical-nav";
|
||||
|
||||
// Layouts package
|
||||
@use "layouts";
|
||||
|
||||
// Components
|
||||
@use "components";
|
||||
|
||||
// Utilities
|
||||
@use "utilities";
|
||||
|
||||
// Misc
|
||||
@use "misc";
|
||||
|
||||
// Dark
|
||||
@use "dark";
|
||||
|
||||
// libs
|
||||
@use "libs/perfect-scrollbar";
|
||||
|
||||
a {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Vuetify 3 don't provide margin bottom style like vuetify 2
|
||||
p {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
// Iconify icon size
|
||||
svg.iconify {
|
||||
block-size: 1em;
|
||||
inline-size: 1em;
|
||||
}
|
63
resources/js/@core/scss/base/_layouts.scss
Normal file
63
resources/js/@core/scss/base/_layouts.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
/* ℹ️ This styles extends the existing layout package's styles for handling cases that aren't related to layouts package */
|
||||
|
||||
/*
|
||||
ℹ️ When we use v-layout as immediate first child of `.page-content-container`, it adds display:flex and page doesn't get contained height
|
||||
*/
|
||||
// .layout-wrapper.layout-nav-type-vertical {
|
||||
// &.layout-content-height-fixed {
|
||||
// .page-content-container {
|
||||
// > .v-layout:first-child > :not(.v-navigation-drawer):first-child {
|
||||
// flex-grow: 1;
|
||||
// block-size: 100%;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
&.layout-content-height-fixed {
|
||||
.page-content-container {
|
||||
> .v-layout:first-child {
|
||||
overflow: hidden;
|
||||
min-block-size: 100%;
|
||||
|
||||
> .v-main {
|
||||
// overflow-y: auto;
|
||||
|
||||
.v-main__wrap > :first-child {
|
||||
block-size: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ Let div/v-layout take full height. E.g. Email App
|
||||
.layout-wrapper.layout-nav-type-horizontal {
|
||||
&.layout-content-height-fixed {
|
||||
> .layout-page-content {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Floating navbar styles
|
||||
@if variables.$vertical-nav-navbar-style == "floating" {
|
||||
// ℹ️ Add spacing above navbar if navbar is floating (was in %layout-navbar-sticky placeholder)
|
||||
.layout-wrapper.layout-nav-type-vertical.layout-navbar-sticky {
|
||||
.layout-navbar {
|
||||
inset-block-start: variables.$vertical-nav-floating-navbar-top;
|
||||
}
|
||||
|
||||
/*
|
||||
ℹ️ If it's floating navbar
|
||||
Add `vertical-nav-floating-navbar-top` as margin top to .layout-page-content
|
||||
*/
|
||||
.layout-page-content {
|
||||
margin-block-start: variables.$vertical-nav-floating-navbar-top;
|
||||
}
|
||||
}
|
||||
}
|
20
resources/js/@core/scss/base/_misc.scss
Normal file
20
resources/js/@core/scss/base/_misc.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
// ℹ️ scrollable-content allows creating fixed header and scrollable content for VNavigationDrawer (Used when perfect scrollbar is used)
|
||||
.scrollable-content {
|
||||
&.v-navigation-drawer {
|
||||
.v-navigation-drawer__content {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ adding styling for code tag
|
||||
code {
|
||||
border-radius: 3px;
|
||||
color: rgb(var(--v-code-color));
|
||||
font-size: 90%;
|
||||
font-weight: 400;
|
||||
padding-block: 0.2em;
|
||||
padding-inline: 0.4em;
|
||||
}
|
63
resources/js/@core/scss/base/_mixins.scss
Normal file
63
resources/js/@core/scss/base/_mixins.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
@use "sass:map";
|
||||
@use "@styles/variables/_vuetify.scss";
|
||||
|
||||
@mixin elevation($z, $important: false) {
|
||||
box-shadow: map.get(vuetify.$shadow-key-umbra, $z), map.get(vuetify.$shadow-key-penumbra, $z), map.get(vuetify.$shadow-key-ambient, $z) if($important, !important, null);
|
||||
}
|
||||
|
||||
// #region before-pseudo
|
||||
// ℹ️ This mixin is inspired from vuetify for adding hover styles via before pseudo element
|
||||
@mixin before-pseudo() {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
background: currentcolor;
|
||||
block-size: 100%;
|
||||
content: "";
|
||||
inline-size: 100%;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion before-pseudo
|
||||
|
||||
@mixin bordered-skin($component, $border-property: "border", $important: false) {
|
||||
#{$component} {
|
||||
// background-color: rgb(var(--v-theme-background));
|
||||
box-shadow: none !important;
|
||||
#{$border-property}: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) if($important, !important, null);
|
||||
}
|
||||
}
|
||||
|
||||
// #region selected-states
|
||||
// ℹ️ Inspired from vuetify's active-states mixin
|
||||
// focus => 0.12 & selected => 0.08
|
||||
@mixin selected-states($selector) {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:hover
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
&:focus-visible
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
&:focus {
|
||||
#{$selector} {
|
||||
opacity: calc(var(--v-selected-opacity) + var(--v-focus-opacity) * var(--v-theme-overlay-multiplier));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion selected-states
|
152
resources/js/@core/scss/base/_utilities.scss
Normal file
152
resources/js/@core/scss/base/_utilities.scss
Normal file
@@ -0,0 +1,152 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
|
||||
// 👉 Demo spacers
|
||||
// TODO: Use vuetify SCSS variable here
|
||||
$card-spacer-content: 16px;
|
||||
|
||||
.demo-space-x {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-block-start: -$card-spacer-content;
|
||||
|
||||
& > * {
|
||||
margin-block-start: $card-spacer-content;
|
||||
margin-inline-end: $card-spacer-content;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-space-y {
|
||||
& > * {
|
||||
margin-block-end: $card-spacer-content;
|
||||
|
||||
&:last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Card match height
|
||||
.match-height.v-row {
|
||||
.v-card {
|
||||
block-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Whitespace
|
||||
.whitespace-no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 👉 Colors
|
||||
|
||||
/*
|
||||
ℹ️ Vuetify is applying `.text-white` class to badge icon but don't provide its styles
|
||||
Moreover, we also use this class in some places
|
||||
|
||||
ℹ️ In vuetify 2 with `$color-pack: false` SCSS var config this class was getting generated but this is not the case in v3
|
||||
|
||||
ℹ️ We also need !important to get correct color in badge icon
|
||||
*/
|
||||
.text-white {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.bg-var-theme-background {
|
||||
background-color: rgba(var(--v-theme-on-background), var(--v-hover-opacity)) !important;
|
||||
}
|
||||
|
||||
// [/^bg-light-(\w+)$/, ([, w]) => ({ backgroundColor: `rgba(var(--v-theme-${w}), var(--v-activated-opacity))` })],
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
.bg-light-#{$color-name} {
|
||||
background-color: rgba(var(--v-theme-#{$color-name}), var(--v-activated-opacity)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 clamp text
|
||||
.clamp-text {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.leading-normal {
|
||||
line-height: normal !important;
|
||||
}
|
||||
|
||||
// 👉 for rtl only
|
||||
.flip-in-rtl {
|
||||
@include layoutsMixins.rtl {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Carousel
|
||||
.carousel-delimiter-top-end {
|
||||
.v-carousel__controls {
|
||||
justify-content: end;
|
||||
block-size: 40px;
|
||||
inset-block-start: 0;
|
||||
padding-inline: 1rem;
|
||||
|
||||
.v-btn--icon.v-btn--density-default {
|
||||
block-size: calc(var(--v-btn-height) + -10px);
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
inline-size: calc(var(--v-btn-height) + -8px);
|
||||
|
||||
&.v-btn--active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.v-btn__overlay {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.v-ripple__container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v-btn__content {
|
||||
.v-icon {
|
||||
block-size: 8px !important;
|
||||
inline-size: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
|
||||
&.dots-active-#{$color-name} {
|
||||
.v-carousel__controls {
|
||||
.v-btn--active {
|
||||
color: rgb(var(--v-theme-#{$color-name})) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-timeline-item {
|
||||
.app-timeline-title {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.3125rem;
|
||||
}
|
||||
|
||||
.app-timeline-meta {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
font-size: 12px;
|
||||
line-height: 0.875rem;
|
||||
}
|
||||
|
||||
.app-timeline-text {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 14px;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
}
|
90
resources/js/@core/scss/base/_utils.scss
Normal file
90
resources/js/@core/scss/base/_utils.scss
Normal file
@@ -0,0 +1,90 @@
|
||||
@use "sass:map";
|
||||
@use "sass:list";
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// Thanks: https://css-tricks.com/snippets/sass/deep-getset-maps/
|
||||
@function map-deep-get($map, $keys...) {
|
||||
@each $key in $keys {
|
||||
$map: map.get($map, $key);
|
||||
}
|
||||
|
||||
@return $map;
|
||||
}
|
||||
|
||||
@function map-deep-set($map, $keys, $value) {
|
||||
$maps: ($map,);
|
||||
$result: null;
|
||||
|
||||
// If the last key is a map already
|
||||
// Warn the user we will be overriding it with $value
|
||||
@if type-of(nth($keys, -1)) == "map" {
|
||||
@warn "The last key you specified is a map; it will be overrided with `#{$value}`.";
|
||||
}
|
||||
|
||||
// If $keys is a single key
|
||||
// Just merge and return
|
||||
@if length($keys) == 1 {
|
||||
@return map-merge($map, ($keys: $value));
|
||||
}
|
||||
|
||||
// Loop from the first to the second to last key from $keys
|
||||
// Store the associated map to this key in the $maps list
|
||||
// If the key doesn't exist, throw an error
|
||||
@for $i from 1 through length($keys) - 1 {
|
||||
$current-key: list.nth($keys, $i);
|
||||
$current-map: list.nth($maps, -1);
|
||||
$current-get: map.get($current-map, $current-key);
|
||||
|
||||
@if not $current-get {
|
||||
@error "Key `#{$key}` doesn't exist at current level in map.";
|
||||
}
|
||||
|
||||
$maps: list.append($maps, $current-get);
|
||||
}
|
||||
|
||||
// Loop from the last map to the first one
|
||||
// Merge it with the previous one
|
||||
@for $i from length($maps) through 1 {
|
||||
$current-map: list.nth($maps, $i);
|
||||
$current-key: list.nth($keys, $i);
|
||||
$current-val: if($i == list.length($maps), $value, $result);
|
||||
$result: map.map-merge($current-map, ($current-key: $current-val));
|
||||
}
|
||||
|
||||
// Return result
|
||||
@return $result;
|
||||
}
|
||||
|
||||
// font size utility classes
|
||||
@each $name, $size in variables.$font-sizes {
|
||||
.text-#{$name} {
|
||||
font-size: $size;
|
||||
line-height: map.get(variables.$font-line-height, $name);
|
||||
}
|
||||
}
|
||||
|
||||
// truncate utility class
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// gap utility class
|
||||
@each $name, $size in variables.$gap {
|
||||
.gap-#{$name} {
|
||||
gap: $size;
|
||||
}
|
||||
|
||||
.gap-x-#{$name} {
|
||||
column-gap: $size;
|
||||
}
|
||||
|
||||
.gap-y-#{$name} {
|
||||
row-gap: $size;
|
||||
}
|
||||
}
|
||||
|
||||
.list-none {
|
||||
list-style-type: none;
|
||||
}
|
197
resources/js/@core/scss/base/_variables.scss
Normal file
197
resources/js/@core/scss/base/_variables.scss
Normal file
@@ -0,0 +1,197 @@
|
||||
@use "vuetify/lib/styles/tools/functions" as *;
|
||||
|
||||
/*
|
||||
TODO: Add docs on when to use placeholder vs when to use SASS variable
|
||||
|
||||
Placeholder
|
||||
- When we want to keep customization to our self between templates use it
|
||||
|
||||
Variables
|
||||
- When we want to allow customization from both user and our side
|
||||
- You can also use variable for consistency (e.g. mx 1 rem should be applied to both vertical nav items and vertical nav header)
|
||||
*/
|
||||
|
||||
@forward "@layouts/styles/variables" with (
|
||||
// Adjust z-index so vertical nav & overlay stays on top of v-layout in v-main. E.g. Email app
|
||||
$layout-vertical-nav-z-index: 1004,
|
||||
$layout-overlay-z-index: 1003,
|
||||
);
|
||||
@use "@layouts/styles/variables" as *;
|
||||
|
||||
// 👉 Default layout
|
||||
|
||||
$navbar-high-emphasis-text: true !default;
|
||||
|
||||
// @forward "@layouts/styles/variables" with (
|
||||
// $layout-vertical-nav-width: 350px !default,
|
||||
// );
|
||||
|
||||
$theme-colors-name: (
|
||||
"primary",
|
||||
"secondary",
|
||||
"error",
|
||||
"info",
|
||||
"success",
|
||||
"warning"
|
||||
) !default;
|
||||
|
||||
// 👉 Default layout with vertical nav
|
||||
|
||||
$default-layout-with-vertical-nav-navbar-footer-roundness: 10px !default;
|
||||
|
||||
// 👉 Vertical nav
|
||||
$vertical-nav-background-color-rgb: var(--v-theme-background) !default;
|
||||
$vertical-nav-background-color: rgb(#{$vertical-nav-background-color-rgb}) !default;
|
||||
|
||||
// ℹ️ This is used to keep consistency between nav items and nav header left & right margin
|
||||
// This is used by nav items & nav header
|
||||
$vertical-nav-horizontal-spacing: 1rem !default;
|
||||
$vertical-nav-horizontal-padding: 0.75rem !default;
|
||||
|
||||
// Vertical nav header height. Mostly we will align it with navbar height;
|
||||
$vertical-nav-header-height: $layout-vertical-nav-navbar-height !default;
|
||||
$vertical-nav-navbar-elevation: 3 !default;
|
||||
$vertical-nav-navbar-style: "elevated" !default; // options: elevated, floating
|
||||
$vertical-nav-floating-navbar-top: 1rem !default;
|
||||
|
||||
// Vertical nav header padding
|
||||
$vertical-nav-header-padding: 1rem $vertical-nav-horizontal-padding !default;
|
||||
$vertical-nav-header-inline-spacing: $vertical-nav-horizontal-spacing !default;
|
||||
|
||||
// Move logo when vertical nav is mini (collapsed but not hovered)
|
||||
$vertical-nav-header-logo-translate-x-when-vertical-nav-mini: -4px !default;
|
||||
|
||||
// Space between logo and title
|
||||
$vertical-nav-header-logo-title-spacing: 0.9rem !default;
|
||||
|
||||
// Section title margin top (when its not first child)
|
||||
$vertical-nav-section-title-mt: 1.5rem !default;
|
||||
|
||||
// Section title margin bottom
|
||||
$vertical-nav-section-title-mb: 0.5rem !default;
|
||||
|
||||
// Vertical nav icons
|
||||
$vertical-nav-items-icon-size: 1.5rem !default;
|
||||
$vertical-nav-items-nested-icon-size: 0.9rem !default;
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.5rem !default;
|
||||
|
||||
// Transition duration for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-duration: 0.15s !default;
|
||||
|
||||
// Timing function for nav group arrow
|
||||
$vertical-nav-nav-group-arrow-transition-timing-function: ease-in-out !default;
|
||||
|
||||
// 👉 Horizontal nav
|
||||
|
||||
/*
|
||||
❗ Heads up
|
||||
==================
|
||||
Here we assume we will always use shorthand property which will apply same padding on four side
|
||||
This is because this have been used as value of top property by `.popper-content`
|
||||
*/
|
||||
$horizontal-nav-padding: 0.6875rem !default;
|
||||
|
||||
// Gap between top level horizontal nav items
|
||||
$horizontal-nav-top-level-items-gap: 4px !default;
|
||||
|
||||
// Horizontal nav icons
|
||||
$horizontal-nav-items-icon-size: 1.5rem !default;
|
||||
$horizontal-nav-third-level-icon-size: 0.9rem !default;
|
||||
$horizontal-nav-items-icon-margin-inline-end: 0.625rem !default;
|
||||
|
||||
// ℹ️ We used SCSS variable because we want to allow users to update max height of popper content
|
||||
// 120px is combined height of navbar & horizontal nav
|
||||
$horizontal-nav-popper-content-max-height: calc((var(--vh, 1vh) * 100) - 120px - 4rem) !default;
|
||||
|
||||
// ℹ️ This variable is used for horizontal nav popper content's `margin-top` and "The bridge"'s height. We need to sync both values.
|
||||
$horizontal-nav-popper-content-top: calc($horizontal-nav-padding + 0.375rem) !default;
|
||||
|
||||
// 👉 Plugins
|
||||
|
||||
$plugin-ps-thumb-y-dark: rgba(var(--v-theme-surface-variant), 0.35) !default;
|
||||
|
||||
// 👉 Vuetify
|
||||
|
||||
// Used in src/@core/scss/base/libs/vuetify/_overrides.scss
|
||||
$vuetify-reduce-default-compact-button-icon-size: true !default;
|
||||
|
||||
// 👉 Custom variables
|
||||
// for utility classes
|
||||
$font-sizes: () !default;
|
||||
$font-sizes: map-deep-merge(
|
||||
(
|
||||
"xs": 0.75rem,
|
||||
"sm": 0.875rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.25rem,
|
||||
"2xl": 1.5rem,
|
||||
"3xl": 1.875rem,
|
||||
"4xl": 2.25rem,
|
||||
"5xl": 3rem,
|
||||
"6xl": 3.75rem,
|
||||
"7xl": 4.5rem,
|
||||
"8xl": 6rem,
|
||||
"9xl": 8rem
|
||||
),
|
||||
$font-sizes
|
||||
);
|
||||
|
||||
// line height
|
||||
$font-line-height: () !default;
|
||||
$font-line-height: map-deep-merge(
|
||||
(
|
||||
"xs": 1rem,
|
||||
"sm": 1.25rem,
|
||||
"base": 1.5rem,
|
||||
"lg": 1.75rem,
|
||||
"xl": 1.75rem,
|
||||
"2xl": 2rem,
|
||||
"3xl": 2.25rem,
|
||||
"4xl": 2.5rem,
|
||||
"5xl": 1,
|
||||
"6xl": 1,
|
||||
"7xl": 1,
|
||||
"8xl": 1,
|
||||
"9xl": 1
|
||||
),
|
||||
$font-line-height
|
||||
);
|
||||
|
||||
// gap utility class
|
||||
$gap: () !default;
|
||||
$gap: map-deep-merge(
|
||||
(
|
||||
"0": 0,
|
||||
"1": 0.25rem,
|
||||
"2": 0.5rem,
|
||||
"3": 0.75rem,
|
||||
"4": 1rem,
|
||||
"5": 1.25rem,
|
||||
"6":1.5rem,
|
||||
"7": 1.75rem,
|
||||
"8": 2rem,
|
||||
"9": 2.25rem,
|
||||
"10": 2.5rem,
|
||||
"11": 2.75rem,
|
||||
"12": 3rem,
|
||||
"14": 3.5rem,
|
||||
"16": 4rem,
|
||||
"20": 5rem,
|
||||
"24": 6rem,
|
||||
"28": 7rem,
|
||||
"32": 8rem,
|
||||
"36": 9rem,
|
||||
"40": 10rem,
|
||||
"44": 11rem,
|
||||
"48": 12rem,
|
||||
"52": 13rem,
|
||||
"56": 14rem,
|
||||
"60": 15rem,
|
||||
"64": 16rem,
|
||||
"72": 18rem,
|
||||
"80": 20rem,
|
||||
"96": 24rem
|
||||
),
|
||||
$gap
|
||||
);
|
250
resources/js/@core/scss/base/_vertical-nav.scss
Normal file
250
resources/js/@core/scss/base/_vertical-nav.scss
Normal file
@@ -0,0 +1,250 @@
|
||||
@use "@core/scss/base/placeholders" as *;
|
||||
@use "@core/scss/template/placeholders" as *;
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@core/scss/base/mixins" as mixins;
|
||||
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
|
||||
|
||||
.layout-nav-type-vertical {
|
||||
// 👉 Layout Vertical nav
|
||||
.layout-vertical-nav {
|
||||
$sl-layout-nav-type-vertical: &;
|
||||
|
||||
@extend %nav;
|
||||
|
||||
@at-root {
|
||||
// ℹ️ Add styles for collapsed vertical nav
|
||||
.layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}.hovered {
|
||||
@include mixins.elevation(6);
|
||||
}
|
||||
}
|
||||
|
||||
background-color: variables.$vertical-nav-background-color;
|
||||
|
||||
// 👉 Nav header
|
||||
.nav-header {
|
||||
overflow: hidden;
|
||||
padding: variables.$vertical-nav-header-padding;
|
||||
margin-inline: variables.$vertical-nav-header-inline-spacing;
|
||||
min-block-size: variables.$vertical-nav-header-height;
|
||||
|
||||
// TEMPLATE: Check if we need to move this to master
|
||||
.app-logo {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.25s ease-in-out;
|
||||
|
||||
@at-root {
|
||||
// Move logo a bit to align center with the icons in vertical nav mini variant
|
||||
.layout-vertical-nav-collapsed#{$sl-layout-nav-type-vertical}:not(.hovered) .nav-header .app-logo {
|
||||
transform: translateX(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini);
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(-(variables.$vertical-nav-header-logo-translate-x-when-vertical-nav-mini));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-title {
|
||||
margin-inline-start: variables.$vertical-nav-header-logo-title-spacing;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
@extend %nav-header-action;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Nav items shadow
|
||||
.vertical-nav-items-shadow {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background:
|
||||
linear-gradient(
|
||||
rgb(#{variables.$vertical-nav-background-color-rgb}) 5%,
|
||||
rgba(#{variables.$vertical-nav-background-color-rgb}, 75%) 45%,
|
||||
rgba(#{variables.$vertical-nav-background-color-rgb}, 20%) 80%,
|
||||
transparent
|
||||
);
|
||||
block-size: 55px;
|
||||
inline-size: 100%;
|
||||
inset-block-start: calc(#{variables.$vertical-nav-header-height} - 2px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
will-change: opacity;
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
}
|
||||
|
||||
&.scrolled {
|
||||
.vertical-nav-items-shadow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ps__rail-y {
|
||||
// ℹ️ Setting z-index: 1 will make perfect scrollbar thumb appear on top of vertical nav items shadow
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// 👉 Nav section title
|
||||
.nav-section-title {
|
||||
@extend %vertical-nav-item;
|
||||
@extend %vertical-nav-section-title;
|
||||
|
||||
margin-block-end: variables.$vertical-nav-section-title-mb;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-block-start: variables.$vertical-nav-section-title-mt;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
margin-inline: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Nav item badge
|
||||
.nav-item-badge {
|
||||
@extend %vertical-nav-item-badge;
|
||||
}
|
||||
|
||||
// 👉 Nav group & Link
|
||||
.nav-link,
|
||||
.nav-group {
|
||||
overflow: hidden;
|
||||
|
||||
> :first-child {
|
||||
@extend %vertical-nav-item;
|
||||
@extend %vertical-nav-item-interactive;
|
||||
}
|
||||
|
||||
.nav-item-icon {
|
||||
@extend %vertical-nav-items-icon;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: var(--v-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Vertical nav link
|
||||
.nav-link {
|
||||
@extend %nav-link;
|
||||
|
||||
> .router-link-exact-active {
|
||||
@extend %nav-link-active;
|
||||
}
|
||||
|
||||
> a {
|
||||
// Adds before psudo element to style hover state
|
||||
@include mixins.before-pseudo;
|
||||
|
||||
// Adds vuetify states
|
||||
@include vuetifyStates.states($active: false);
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Vertical nav group
|
||||
.nav-group {
|
||||
// Reduce the size of icon if link/group is inside group
|
||||
.nav-group,
|
||||
.nav-link {
|
||||
.nav-item-icon {
|
||||
@extend %vertical-nav-items-nested-icon;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide icons after 2nd level
|
||||
& .nav-group {
|
||||
.nav-link,
|
||||
.nav-group {
|
||||
.nav-item-icon {
|
||||
@extend %vertical-nav-items-icon-after-2nd-level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-group-arrow {
|
||||
flex-shrink: 0;
|
||||
transform-origin: center;
|
||||
transition: transform variables.$vertical-nav-nav-group-arrow-transition-duration variables.$vertical-nav-nav-group-arrow-transition-timing-function;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
// Rotate arrow icon if group is opened
|
||||
&.open {
|
||||
> .nav-group-label .nav-group-arrow {
|
||||
transform: rotateZ(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Nav group label
|
||||
> :first-child {
|
||||
// Adds before psudo element to style hover state
|
||||
@include mixins.before-pseudo;
|
||||
|
||||
// Adds vuetify states
|
||||
@include vuetifyStates.states($active: false);
|
||||
}
|
||||
|
||||
// Active & open states for nav group label
|
||||
&.active,
|
||||
&.open {
|
||||
> :first-child {
|
||||
@extend %vertical-nav-group-open-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SECTION: Transitions
|
||||
.vertical-nav-section-title-enter-active,
|
||||
.vertical-nav-section-title-leave-active {
|
||||
transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.vertical-nav-section-title-enter-from,
|
||||
.vertical-nav-section-title-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(15px);
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.transition-slide-x-enter-active,
|
||||
.transition-slide-x-leave-active {
|
||||
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out;
|
||||
}
|
||||
|
||||
.transition-slide-x-enter-from,
|
||||
.transition-slide-x-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-15px);
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(15px);
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-nav-app-title-enter-active,
|
||||
.vertical-nav-app-title-leave-active {
|
||||
transition: opacity 0.1s ease-in-out, transform 0.12s ease-in-out;
|
||||
}
|
||||
|
||||
.vertical-nav-app-title-enter-from,
|
||||
.vertical-nav-app-title-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-15px);
|
||||
|
||||
@include layoutsMixins.rtl {
|
||||
transform: translateX(15px);
|
||||
}
|
||||
}
|
||||
|
||||
// !SECTION
|
35
resources/js/@core/scss/base/libs/_perfect-scrollbar.scss
Normal file
35
resources/js/@core/scss/base/libs/_perfect-scrollbar.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
$ps-size: 0.25rem;
|
||||
$ps-hover-size: 0.375rem;
|
||||
$ps-track-size: 0.5rem;
|
||||
|
||||
.ps__thumb-y {
|
||||
inline-size: $ps-size;
|
||||
inset-inline-end: 0.0625rem;
|
||||
}
|
||||
|
||||
.ps__thumb-x {
|
||||
block-size: $ps-size !important;
|
||||
}
|
||||
|
||||
.ps__rail-x {
|
||||
background: transparent !important;
|
||||
block-size: $ps-track-size;
|
||||
}
|
||||
|
||||
.ps__rail-y {
|
||||
background: transparent !important;
|
||||
inline-size: $ps-track-size !important;
|
||||
inset-inline-end: 0.125rem !important;
|
||||
inset-inline-start: unset !important;
|
||||
}
|
||||
|
||||
.ps__rail-y.ps--clicking .ps__thumb-y,
|
||||
.ps__rail-y:focus > .ps__thumb-y,
|
||||
.ps__rail-y:hover > .ps__thumb-y {
|
||||
inline-size: $ps-hover-size;
|
||||
}
|
||||
|
||||
.ps__thumb-x,
|
||||
.ps__thumb-y {
|
||||
background-color: rgb(var(--v-theme-perfect-scrollbar-thumb)) !important;
|
||||
}
|
1
resources/js/@core/scss/base/libs/vuetify/_index.scss
Normal file
1
resources/js/@core/scss/base/libs/vuetify/_index.scss
Normal file
@@ -0,0 +1 @@
|
||||
@use "overrides";
|
287
resources/js/@core/scss/base/libs/vuetify/_overrides.scss
Normal file
287
resources/js/@core/scss/base/libs/vuetify/_overrides.scss
Normal file
@@ -0,0 +1,287 @@
|
||||
@use "@core/scss/base/utils";
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
// 👉 Application
|
||||
// ℹ️ We need accurate vh in mobile devices as well
|
||||
.v-application__wrap {
|
||||
/* stylelint-disable-next-line liberty/use-logical-spec */
|
||||
min-height: calc(var(--vh, 1vh) * 100);
|
||||
}
|
||||
|
||||
// 👉 Typography
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.text-h1,
|
||||
.text-h2,
|
||||
.text-h3,
|
||||
.text-h4,
|
||||
.text-h5,
|
||||
.text-h6,
|
||||
.text-button,
|
||||
.text-overline,
|
||||
.v-card-title {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.v-application,
|
||||
.text-body-1,
|
||||
.text-body-2,
|
||||
.text-subtitle-1,
|
||||
.text-subtitle-2 {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
|
||||
// 👉 Grid
|
||||
// Remove margin-bottom of v-input_details inside grid (validation error message)
|
||||
.v-row {
|
||||
.v-col,
|
||||
[class^="v-col-*"] {
|
||||
.v-input__details {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Button
|
||||
@if variables.$vuetify-reduce-default-compact-button-icon-size {
|
||||
.v-btn--density-compact.v-btn--size-default {
|
||||
.v-btn__content > svg {
|
||||
block-size: 22px;
|
||||
font-size: 22px;
|
||||
inline-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Card
|
||||
// Removes padding-top for immediately placed v-card-text after itself
|
||||
.v-card-text {
|
||||
& + & {
|
||||
padding-block-start: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
👉 Checkbox & Radio Ripple
|
||||
|
||||
TODO Checkbox and switch component. Remove it when vuetify resolve the extra spacing: https://github.com/vuetifyjs/vuetify/issues/15519
|
||||
We need this because form elements likes checkbox and switches are by default set to height of textfield height which is way big than we want
|
||||
Tested with checkbox & switches
|
||||
*/
|
||||
.v-checkbox.v-input,
|
||||
.v-switch.v-input {
|
||||
--v-input-control-height: auto;
|
||||
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.v-selection-control--density-comfortable {
|
||||
&.v-checkbox-btn,
|
||||
&.v-radio,
|
||||
&.v-radio-btn {
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.5625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-selection-control--density-compact {
|
||||
&.v-radio,
|
||||
&.v-radio-btn,
|
||||
&.v-checkbox-btn {
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.3125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-selection-control--density-default {
|
||||
&.v-checkbox-btn,
|
||||
&.v-radio,
|
||||
&.v-radio-btn {
|
||||
.v-selection-control__wrapper {
|
||||
margin-inline-start: -0.6875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-radio-group {
|
||||
.v-selection-control-group {
|
||||
.v-radio:not(:last-child) {
|
||||
margin-inline-end: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
👉 Tabs
|
||||
Disable tab transition
|
||||
|
||||
This is for tabs where we don't have card wrapper to tabs and have multiple cards as tab content.
|
||||
|
||||
This class will disable transition and adds `overflow: unset` on `VWindow` to allow spreading shadow
|
||||
*/
|
||||
.disable-tab-transition {
|
||||
overflow: unset !important;
|
||||
|
||||
.v-window__container {
|
||||
block-size: auto !important;
|
||||
}
|
||||
|
||||
.v-window-item:not(.v-window-item--active) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.v-window__container .v-window-item {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 List
|
||||
.v-list {
|
||||
// Set icons opacity to .87
|
||||
.v-list-item__prepend > .v-icon,
|
||||
.v-list-item__append > .v-icon {
|
||||
opacity: var(--v-high-emphasis-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Card list
|
||||
|
||||
/*
|
||||
ℹ️ Custom class
|
||||
|
||||
Remove list spacing inside card
|
||||
|
||||
This is because card title gets padding of 20px and list item have padding of 16px. Moreover, list container have padding-bottom as well.
|
||||
*/
|
||||
.card-list {
|
||||
--v-card-list-gap: 20px;
|
||||
|
||||
&.v-list {
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
min-block-size: unset;
|
||||
min-block-size: auto !important;
|
||||
padding-block: 0 !important;
|
||||
padding-inline: 0 !important;
|
||||
|
||||
> .v-ripple__container {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-block-end: var(--v-card-list-gap) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item:hover,
|
||||
.v-list-item:focus,
|
||||
.v-list-item:active,
|
||||
.v-list-item.active {
|
||||
> .v-list-item__overlay {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Divider
|
||||
.v-divider {
|
||||
color: rgb(var(--v-border-color));
|
||||
}
|
||||
|
||||
// 👉 DataTable
|
||||
.v-data-table {
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
.v-checkbox-btn .v-selection-control__wrapper {
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.v-selection-control {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.v-pagination {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
.v-data-table-footer {
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
|
||||
// 👉 v-field
|
||||
.v-field:hover .v-field__outline {
|
||||
--v-field-border-opacity: var(--v-medium-emphasis-opacity);
|
||||
}
|
||||
|
||||
// 👉 VLabel
|
||||
.v-label {
|
||||
opacity: 1 !important;
|
||||
|
||||
&:not(.v-field-label--floating) {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Overlay
|
||||
.v-overlay__scrim,
|
||||
.v-navigation-drawer__scrim {
|
||||
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity)) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
// 👉 VMessages
|
||||
.v-messages {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 👉 Alert close btn
|
||||
.v-alert__close {
|
||||
.v-btn--icon .v-icon {
|
||||
--v-icon-size-multiplier: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Badge icon alignment
|
||||
.v-badge__badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 👉 Btn focus outline style removed
|
||||
.v-btn:focus-visible::after {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
// .v-select chip spacing for slot
|
||||
.v-input:not(.v-select--chips) .v-select__selection {
|
||||
.v-chip {
|
||||
margin-block: 2px var(--select-chips-margin-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 VCard and VList subtitle color
|
||||
.v-card-subtitle,
|
||||
.v-list-item-subtitle {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
|
||||
// 👉 placeholders
|
||||
.v-field__input {
|
||||
@at-root {
|
||||
& input::placeholder,
|
||||
input#{&}::placeholder,
|
||||
textarea#{&}::placeholder {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity)) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
49
resources/js/@core/scss/base/libs/vuetify/_variables.scss
Normal file
49
resources/js/@core/scss/base/libs/vuetify/_variables.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
// 👉 Shadow opacities
|
||||
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
|
||||
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
|
||||
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
|
||||
|
||||
// 👉 Card transition properties
|
||||
$card-transition-property-custom: box-shadow, opacity;
|
||||
|
||||
@forward "vuetify/settings" with (
|
||||
// 👉 General settings
|
||||
$color-pack: false !default,
|
||||
|
||||
// 👉 Shadow opacity
|
||||
$shadow-key-umbra-opacity: $shadow-key-umbra-opacity-custom !default,
|
||||
$shadow-key-penumbra-opacity: $shadow-key-penumbra-opacity-custom !default,
|
||||
$shadow-key-ambient-opacity: $shadow-key-ambient-opacity-custom !default,
|
||||
|
||||
// 👉 Card
|
||||
$card-color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !default,
|
||||
$card-elevation: 6 !default,
|
||||
$card-title-line-height: 1.6 !default,
|
||||
$card-actions-min-height: unset !default,
|
||||
$card-text-padding: 1.25rem !default,
|
||||
$card-item-padding: 1.25rem !default,
|
||||
$card-actions-padding: 0 12px 12px !default,
|
||||
$card-transition-property: $card-transition-property-custom !default,
|
||||
$card-subtitle-opacity: 1 !default,
|
||||
|
||||
// 👉 Expansion Panel
|
||||
$expansion-panel-active-title-min-height: 48px !default,
|
||||
|
||||
// 👉 List
|
||||
$list-item-icon-margin-end: 16px !default,
|
||||
$list-item-icon-margin-start: 16px !default,
|
||||
$list-item-subtitle-opacity: 1 !default,
|
||||
|
||||
// 👉 Tooltip
|
||||
$tooltip-background-color: rgba(59, 55, 68, 0.9) !default,
|
||||
$tooltip-text-color: rgb(var(--v-theme-on-primary)) !default,
|
||||
$tooltip-font-size: 0.75rem !default,
|
||||
|
||||
$button-icon-density: ("default": 2, "comfortable": 0, "compact": -1 ) !default,
|
||||
|
||||
// 👉 VTimeline
|
||||
$timeline-dot-size: 34px !default,
|
||||
|
||||
// 👉 VOverlay
|
||||
$overlay-opacity: 1 !default,
|
||||
);
|
@@ -0,0 +1,46 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "misc";
|
||||
@use "@core/scss/base/mixins";
|
||||
|
||||
%default-layout-vertical-nav-scrolled-sticky-elevated-nav {
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
|
||||
@include mixins.elevation(variables.$vertical-nav-navbar-elevation);
|
||||
|
||||
// If navbar is contained => Squeeze navbar content on scroll
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
padding-inline: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
%default-layout-vertical-nav-floating-navbar-overlay {
|
||||
isolation: isolate;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
/* stylelint-enable */
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--v-theme-background), 70%) 44%,
|
||||
rgba(var(--v-theme-background), 43%) 73%,
|
||||
rgba(var(--v-theme-background), 0%)
|
||||
);
|
||||
background-repeat: repeat;
|
||||
block-size: calc(variables.$layout-vertical-nav-navbar-height + variables.$vertical-nav-floating-navbar-top + 0.5rem);
|
||||
content: "";
|
||||
inset-block-start: -(variables.$vertical-nav-floating-navbar-top);
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
-webkit-mask: linear-gradient(black, black 18%, transparent 100%);
|
||||
mask: linear-gradient(black, black 18%, transparent 100%);
|
||||
/* stylelint-enable */
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
%layout-navbar {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
5
resources/js/@core/scss/base/placeholders/_index.scss
Normal file
5
resources/js/@core/scss/base/placeholders/_index.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@forward "vertical-nav";
|
||||
@forward "nav";
|
||||
@forward "default-layout";
|
||||
@forward "default-layout-vertical-nav";
|
||||
@forward "misc";
|
7
resources/js/@core/scss/base/placeholders/_misc.scss
Normal file
7
resources/js/@core/scss/base/placeholders/_misc.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
%blurry-bg {
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
/* stylelint-enable */
|
||||
background-color: rgb(var(--v-theme-surface), 0.9);
|
||||
}
|
33
resources/js/@core/scss/base/placeholders/_nav.scss
Normal file
33
resources/js/@core/scss/base/placeholders/_nav.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
@use "@core/scss/base/mixins";
|
||||
|
||||
// ℹ️ This is common style that needs to be applied to both navs
|
||||
%nav {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
|
||||
.nav-item-title {
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Active nav link styles for horizontal & vertical nav
|
||||
|
||||
For horizontal nav it will be only applied to top level nav items
|
||||
For vertical nav it will be only applied to nav links (not nav groups)
|
||||
*/
|
||||
%nav-link-active {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
|
||||
@include mixins.elevation(3);
|
||||
}
|
||||
|
||||
%nav-link {
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
81
resources/js/@core/scss/base/placeholders/_vertical-nav.scss
Normal file
81
resources/js/@core/scss/base/placeholders/_vertical-nav.scss
Normal file
@@ -0,0 +1,81 @@
|
||||
@use "@core/scss/base/mixins";
|
||||
@use "@configured-variables" as variables;
|
||||
@use "vuetify/lib/styles/tools/states" as vuetifyStates;
|
||||
|
||||
%nav-header-action {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
// Nav items styles (including section title)
|
||||
%vertical-nav-item {
|
||||
margin-block: 0;
|
||||
margin-inline: variables.$vertical-nav-horizontal-spacing;
|
||||
padding-block: 0;
|
||||
padding-inline: variables.$vertical-nav-horizontal-padding;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// This is same as `%vertical-nav-item` except section title is excluded
|
||||
%vertical-nav-item-interactive {
|
||||
border-radius: 0.4rem;
|
||||
block-size: 2.75rem;
|
||||
|
||||
/*
|
||||
ℹ️ We will use `margin-block-end` instead of `margin-block` to give more space for shadow to appear.
|
||||
With `margin-block`, due to small space (space gets divided between top & bottom) shadow cuts
|
||||
*/
|
||||
margin-block-end: 0.375rem;
|
||||
}
|
||||
|
||||
// Common styles for nav item icon styles
|
||||
// ℹ️ Nav group's children icon styles are not here (Adjusts height, width & margin)
|
||||
%vertical-nav-items-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: variables.$vertical-nav-items-icon-size;
|
||||
margin-inline-end: variables.$vertical-nav-items-icon-margin-inline-end;
|
||||
}
|
||||
|
||||
// ℹ️ Icon styling for icon nested inside another nav item (2nd level)
|
||||
%vertical-nav-items-nested-icon {
|
||||
/*
|
||||
ℹ️ `margin-inline` will be (normal icon font-size - small icon font-size) / 2
|
||||
(1.5rem - 0.9rem) / 2 => 0.6rem / 2 => 0.3rem
|
||||
*/
|
||||
$vertical-nav-items-nested-icon-margin-inline: calc((variables.$vertical-nav-items-icon-size - variables.$vertical-nav-items-nested-icon-size) / 2);
|
||||
|
||||
font-size: variables.$vertical-nav-items-nested-icon-size;
|
||||
margin-inline-end: $vertical-nav-items-nested-icon-margin-inline + variables.$vertical-nav-items-icon-margin-inline-end;
|
||||
margin-inline-start: $vertical-nav-items-nested-icon-margin-inline;
|
||||
}
|
||||
|
||||
%vertical-nav-items-icon-after-2nd-level {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// Open & Active nav group styles
|
||||
%vertical-nav-group-open-active {
|
||||
@include mixins.selected-states("&::before");
|
||||
}
|
||||
|
||||
// Section title
|
||||
%vertical-nav-section-title {
|
||||
// ℹ️ Setting height will prevent jerking when text & icon is toggled
|
||||
block-size: 1.5rem;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
// Vertical nav item badge styles
|
||||
%vertical-nav-item-badge {
|
||||
display: inline-block;
|
||||
border-radius: 1.5rem;
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
padding-block: 0.25em;
|
||||
padding-inline: 0.55em;
|
||||
text-align: center;
|
||||
vertical-align: baseline;
|
||||
white-space: nowrap;
|
||||
}
|
69
resources/js/@core/scss/template/_components.scss
Normal file
69
resources/js/@core/scss/template/_components.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
|
||||
// 👉 VExpansionPanel
|
||||
.v-expansion-panel-title,
|
||||
.v-expansion-panel-title--active,
|
||||
.v-expansion-panel-title:hover,
|
||||
.v-expansion-panel-title:focus,
|
||||
.v-expansion-panel-title:focus-visible,
|
||||
.v-expansion-panel-title--active:focus,
|
||||
.v-expansion-panel-title--active:hover {
|
||||
.v-expansion-panel-title__overlay {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Set Elevation
|
||||
.v-expansion-panels {
|
||||
.v-expansion-panel {
|
||||
.v-expansion-panel__shadow {
|
||||
@include mixins_elevation.elevation(3);
|
||||
}
|
||||
}
|
||||
|
||||
.v-expansion-panel-text__wrapper {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Timeline outlined variant
|
||||
.v-timeline-item {
|
||||
.v-timeline-divider__dot {
|
||||
.v-timeline-divider__inner-dot {
|
||||
box-shadow: 0 0 0 0.1875rem rgb(var(--v-theme-on-surface-variant));
|
||||
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
|
||||
&.bg-#{$color-name} {
|
||||
box-shadow: 0 0 0 0.1875rem rgba(var(--v-theme-#{$color-name}), 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Timeline Outlined style
|
||||
.v-timeline-variant-outlined.v-timeline {
|
||||
.v-timeline-divider__dot {
|
||||
.v-timeline-divider__inner-dot {
|
||||
box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-on-surface-variant));
|
||||
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
background-color: rgb(var(--v-theme-surface)) !important;
|
||||
|
||||
&.bg-#{$color-name} {
|
||||
box-shadow: inset 0 0 0 0.125rem rgb(var(--v-theme-#{$color-name}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 v-tab with pill support
|
||||
.v-tabs.v-tabs-pill {
|
||||
.v-tab.v-btn {
|
||||
border-radius: 0.375rem !important;
|
||||
}
|
||||
}
|
37
resources/js/@core/scss/template/_dark.scss
Normal file
37
resources/js/@core/scss/template/_dark.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
.v-application {
|
||||
// vertical nav
|
||||
&.v-theme--dark .layout-nav-type-vertical,
|
||||
.v-theme-provider.v-theme--dark {
|
||||
.layout-vertical-nav {
|
||||
// nav-link and nav-group style for dark
|
||||
.nav-link .router-link-exact-active,
|
||||
.nav-group.active:not(.nav-group .nav-group) > :first-child {
|
||||
background-color: rgb(var(--v-theme-primary)) !important;
|
||||
color: rgb(var(--v-theme-on-primary)) !important;
|
||||
|
||||
&::before {
|
||||
z-index: -1;
|
||||
color: rgb(var(--v-global-theme-primary));
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
.nav-link {
|
||||
.router-link-exact-active {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
|
||||
&::before {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
color: inherit;
|
||||
opacity: var(--v-hover-opacity) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
@use "vuetify/lib/styles/tools/elevation" as elevation;
|
||||
|
||||
.layout-wrapper.layout-nav-type-vertical {
|
||||
// 👉 Layout footer
|
||||
.layout-footer {
|
||||
$ele-layout-footer: &;
|
||||
|
||||
.footer-content-container {
|
||||
// Sticky footer
|
||||
@at-root {
|
||||
// ℹ️ .layout-footer-sticky#{$ele-layout-footer} => .layout-footer-sticky.layout-wrapper.layout-nav-type-vertical .layout-footer
|
||||
.layout-footer-sticky#{$ele-layout-footer} {
|
||||
.footer-content-container {
|
||||
@include elevation.elevation(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
41
resources/js/@core/scss/template/_utils.scss
Normal file
41
resources/js/@core/scss/template/_utils.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
@use "sass:string";
|
||||
|
||||
/*
|
||||
ℹ️ This function is helpful when we have multi dimensional value
|
||||
|
||||
Assume we have padding variable `$nav-padding-horizontal: 10px;`
|
||||
With above variable let's say we use it in some style:
|
||||
```scss
|
||||
.selector {
|
||||
margin-left: $nav-padding-horizontal;
|
||||
}
|
||||
```
|
||||
|
||||
Now, problem is we can also have value as `$nav-padding-horizontal: 10px 15px;`
|
||||
In this case above style will be invalid.
|
||||
|
||||
This function will extract the left most value from the variable value.
|
||||
|
||||
$nav-padding-horizontal: 10px; => 10px;
|
||||
$nav-padding-horizontal: 10px 15px; => 10px;
|
||||
|
||||
This is safe:
|
||||
```scss
|
||||
.selector {
|
||||
margin-left: get-first-value($nav-padding-horizontal);
|
||||
}
|
||||
```
|
||||
*/
|
||||
@function get-first-value($var) {
|
||||
$start-at: string.index(#{$var}, " ");
|
||||
|
||||
@if $start-at {
|
||||
@return string.slice(
|
||||
#{$var},
|
||||
0,
|
||||
$start-at
|
||||
);
|
||||
} @else {
|
||||
@return $var;
|
||||
}
|
||||
}
|
57
resources/js/@core/scss/template/_variables.scss
Normal file
57
resources/js/@core/scss/template/_variables.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
@use "sass:map";
|
||||
@use "utils";
|
||||
|
||||
$vertical-nav-horizontal-padding-margin-custom: 1.91rem;
|
||||
|
||||
// ℹ️ We created this SCSS var to extract the start padding
|
||||
// Docs: https://sass-lang.com/documentation/modules/string
|
||||
// $vertical-nav-horizontal-padding => 0 8px;
|
||||
// string.index(#{$vertical-nav-horizontal-padding}, " ") + 1 => 2
|
||||
// string.index(#{$vertical-nav-horizontal-padding}, " ") => 1
|
||||
// string.slice(0 8px, 2, -1) => 8px => $card-actions-padding-x
|
||||
|
||||
$vertical-nav-horizontal-padding-start: utils.get-first-value($vertical-nav-horizontal-padding-margin-custom) !default;
|
||||
|
||||
@forward "@core/scss/base/variables" with (
|
||||
// 👉 Default layout with vertical nav
|
||||
$default-layout-with-vertical-nav-navbar-footer-roundness: 8px !default,
|
||||
|
||||
// 👉 Vertical nav
|
||||
$layout-vertical-nav-collapsed-width: 84px !default,
|
||||
$vertical-nav-background-color-rgb: var(--v-theme-surface) !default,
|
||||
$vertical-nav-items-nested-icon-size: 0.5rem !default,
|
||||
$vertical-nav-horizontal-padding: 0.9375rem 0.625rem !default,
|
||||
$vertical-nav-header-inline-spacing: 0 !default,
|
||||
$vertical-nav-header-padding: 1rem 2.2rem !default,
|
||||
|
||||
// Section title margin top (when its not first child)
|
||||
$vertical-nav-section-title-mt: 1.4rem !default,
|
||||
|
||||
// Section title margin bottom
|
||||
$vertical-nav-section-title-mb: 0.65rem !default,
|
||||
|
||||
// Vertical nav icons
|
||||
$vertical-nav-items-icon-size: 1.375rem !default,
|
||||
$vertical-nav-navbar-style: "floating" !default, // options: elevated, floating
|
||||
$vertical-nav-floating-navbar-top: 0.75rem !default,
|
||||
$vertical-nav-items-icon-margin-inline-end: 0.625rem !default,
|
||||
|
||||
// 👉 Horizontal nav
|
||||
/*
|
||||
❗ Heads up
|
||||
==================
|
||||
Here we assume we will always use shorthand property which will apply same padding on four side
|
||||
This is because this have been used as value of top property by `.popper-content`
|
||||
*/
|
||||
$horizontal-nav-padding: 0.625rem !default,
|
||||
|
||||
// Horizontal nav icons
|
||||
$horizontal-nav-items-icon-size: 1.375rem !default,
|
||||
$horizontal-nav-third-level-icon-size: 0.5rem !default,
|
||||
$horizontal-nav-items-icon-margin-inline-end: 0.5rem !default,
|
||||
);
|
||||
|
||||
$slider-thumb-label-color: rgb(117, 117, 117) !default;
|
||||
|
||||
// vertical nav header
|
||||
$vertical-nav-header-margin-top: 0.75rem !default;
|
103
resources/js/@core/scss/template/_vertical-nav.scss
Normal file
103
resources/js/@core/scss/template/_vertical-nav.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
@use "@core/scss/template/placeholders" as *;
|
||||
@use "vuetify/lib/styles/tools/elevation" as elevation;
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
$divider-gap: 0.75rem;
|
||||
|
||||
// vertical nav app title
|
||||
.layout-nav-type-vertical {
|
||||
.layout-vertical-nav {
|
||||
@include elevation.elevation(3);
|
||||
|
||||
// 👉 Nav header
|
||||
.nav-header {
|
||||
margin-block-start: variables.$vertical-nav-header-margin-top;
|
||||
|
||||
.app-title-wrapper {
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
padding-block-start: 0.25rem;
|
||||
|
||||
// ℹ️ Reduce with width of the thumb in vertical nav menu so we can clearly see active indicator
|
||||
.ps__thumb-y {
|
||||
inline-size: 0.125rem;
|
||||
}
|
||||
|
||||
.ps__rail-y.ps--clicking .ps__thumb-y,
|
||||
.ps__rail-y:focus > .ps__thumb-y,
|
||||
.ps__rail-y:hover > .ps__thumb-y {
|
||||
inline-size: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
// nav-section-title's line
|
||||
.title-text {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
column-gap: $divider-gap;
|
||||
|
||||
&::before {
|
||||
flex: 0 1 calc(variables.$vertical-nav-horizontal-padding-start - $divider-gap);
|
||||
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
content: "";
|
||||
margin-inline-start: -#{variables.$vertical-nav-horizontal-padding-start};
|
||||
}
|
||||
}
|
||||
|
||||
// Active status indicator
|
||||
.nav-link .router-link-exact-active,
|
||||
.nav-group.active:not(.nav-group .nav-group) > :first-child {
|
||||
&::after {
|
||||
position: absolute;
|
||||
background-color: rgb(var(--v-global-theme-primary));
|
||||
block-size: 2.625rem;
|
||||
border-end-start-radius: 0.375rem;
|
||||
border-start-start-radius: 0.375rem;
|
||||
content: "";
|
||||
inline-size: 0.25rem;
|
||||
inset-inline-end: - variables.$vertical-nav-horizontal-spacing;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 Vertical nav link
|
||||
.nav-group {
|
||||
.nav-link {
|
||||
> .router-link-exact-active {
|
||||
@extend %nav-link-nested-active;
|
||||
|
||||
// active status indicator removed
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Active & open states for nav group label
|
||||
&.open:not(.active),
|
||||
.nav-group.active {
|
||||
> :first-child {
|
||||
&.nav-group-label {
|
||||
svg,
|
||||
.nav-item-title {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nav-group active
|
||||
&.active:not(.nav-group .nav-group) {
|
||||
> :first-child {
|
||||
@extend %vertical-nav-group-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
resources/js/@core/scss/template/index.scss
Normal file
11
resources/js/@core/scss/template/index.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
@use "@core/scss/base";
|
||||
|
||||
// Layout
|
||||
@use "vertical-nav";
|
||||
@use "default-layout-w-vertical-nav";
|
||||
|
||||
// Components
|
||||
@use "components";
|
||||
|
||||
// Dark
|
||||
@use "dark";
|
95
resources/js/@core/scss/template/libs/apex-chart.scss
Normal file
95
resources/js/@core/scss/template/libs/apex-chart.scss
Normal file
@@ -0,0 +1,95 @@
|
||||
@use "@styles/variables/_vuetify.scss" as vuetify;
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
@use "@layouts/styles/mixins" as layoutsMixins;
|
||||
|
||||
.v-application .apexcharts-canvas {
|
||||
&line[stroke="transparent"] {
|
||||
display: "none";
|
||||
}
|
||||
|
||||
.apexcharts-tooltip {
|
||||
@include mixins_elevation.elevation(3);
|
||||
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
|
||||
.apexcharts-tooltip-title {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-surface));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.apexcharts-theme-light {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
&.apexcharts-theme-dark {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.apexcharts-tooltip-series-group:first-of-type {
|
||||
padding-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-grey-50));
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
|
||||
&::after {
|
||||
border-block-end-color: rgb(var(--v-theme-grey-50));
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-block-end-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-yaxistooltip {
|
||||
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
background: rgb(var(--v-theme-grey-50));
|
||||
|
||||
&::after {
|
||||
border-inline-start-color: rgb(var(--v-theme-grey-50));
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-inline-start-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-xaxistooltip-text,
|
||||
.apexcharts-yaxistooltip-text {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.apexcharts-yaxis .apexcharts-yaxis-texts-g .apexcharts-yaxis-label {
|
||||
@include layoutsMixins.rtl {
|
||||
text-anchor: start;
|
||||
}
|
||||
}
|
||||
|
||||
.apexcharts-text,
|
||||
.apexcharts-tooltip-text,
|
||||
.apexcharts-datalabel-label,
|
||||
.apexcharts-datalabel,
|
||||
.apexcharts-xaxistooltip-text,
|
||||
.apexcharts-yaxistooltip-text,
|
||||
.apexcharts-legend-text {
|
||||
font-family: vuetify.$body-font-family !important;
|
||||
}
|
||||
|
||||
.apexcharts-pie-label {
|
||||
fill: white;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.apexcharts-marker {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.apexcharts-legend-marker {
|
||||
margin-inline-end: 0.3875rem !important;
|
||||
}
|
||||
}
|
267
resources/js/@core/scss/template/libs/full-calendar.scss
Normal file
267
resources/js/@core/scss/template/libs/full-calendar.scss
Normal file
@@ -0,0 +1,267 @@
|
||||
@use "@core/scss/base/mixins";
|
||||
|
||||
.v-application .fc {
|
||||
--fc-today-bg-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||
--fc-border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
--fc-neutral-bg-color: rgb(var(--v-theme-background));
|
||||
--fc-list-event-hover-bg-color: rgba(var(--v-theme-on-surface), 0.02);
|
||||
--fc-page-bg-color: rgb(var(--v-theme-surface));
|
||||
--fc-event-border-color: currentcolor;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.fc-timegrid-divider {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.fc-col-header-cell-cushion {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-toolbar-title {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.fc-event-time {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.fc-event-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.fc-timegrid-event {
|
||||
.fc-event-title {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-prev-button {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.fc-prev-button,
|
||||
.fc-next-button {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.fc-col-header .fc-col-header-cell .fc-col-header-cell-cushion {
|
||||
padding: 0.5rem;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.fc-timegrid .fc-timegrid-slots .fc-timegrid-slot {
|
||||
block-size: 3rem;
|
||||
}
|
||||
|
||||
// Removed double border on left in list view
|
||||
.fc-list {
|
||||
border-inline-start: none;
|
||||
font-size: 0.875rem;
|
||||
|
||||
.fc-list-day-cushion.fc-cell-shaded {
|
||||
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fc-list-event-time,
|
||||
.fc-list-event-title {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
|
||||
.fc-list-day .fc-list-day-text,
|
||||
.fc-list-day .fc-list-day-side-text {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-timegrid-axis {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
font-size: 0.75rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.fc-timegrid-slot-label-frame {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.fc-header-toolbar {
|
||||
flex-wrap: wrap;
|
||||
margin: 1.25rem;
|
||||
column-gap: 0.5rem;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.fc-toolbar-chunk {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.fc-button-group {
|
||||
.fc-button-primary {
|
||||
&,
|
||||
&:hover,
|
||||
&:not(.disabled):active {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.fc-button-group {
|
||||
border: 0.0625rem solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 0.375rem;
|
||||
|
||||
.fc-button {
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.0187rem;
|
||||
padding-inline: 1rem;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-inline-end: 0.0625rem solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
&.fc-button-active {
|
||||
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-toolbar-title {
|
||||
display: inline-block;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fc-scrollgrid-section {
|
||||
th {
|
||||
border-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar content container
|
||||
.fc-view-harness {
|
||||
min-block-size: 40.625rem;
|
||||
}
|
||||
|
||||
.fc-event {
|
||||
border-color: transparent;
|
||||
cursor: pointer;
|
||||
margin-block-end: 0.3rem;
|
||||
padding-block: 0.1875rem;
|
||||
padding-inline: 0.3125rem;
|
||||
}
|
||||
|
||||
.fc-event-main {
|
||||
color: inherit;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
|
||||
tbody[role="rowgroup"] {
|
||||
> tr > td[role="presentation"] {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-scrollgrid {
|
||||
border-inline-start: none;
|
||||
}
|
||||
|
||||
.fc-daygrid-day {
|
||||
padding: 0.3125rem;
|
||||
}
|
||||
|
||||
.fc-daygrid-day-number {
|
||||
padding-block: 0.5rem;
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.fc-list-event-dot {
|
||||
color: inherit;
|
||||
|
||||
--fc-event-border-color: currentcolor;
|
||||
}
|
||||
|
||||
.fc-list-event {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.fc-popover {
|
||||
@include mixins.elevation(3);
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
.fc-popover-header,
|
||||
.fc-popover-body {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.fc-popover-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 sidebar toggler
|
||||
.fc-toolbar-chunk {
|
||||
.fc-button-group {
|
||||
align-items: center;
|
||||
|
||||
.fc-button .fc-icon {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
// ℹ️ Below two `background-image` styles contains static color due to browser limitation of not parsing the css var inside CSS url()
|
||||
.fc-drawerToggler-button {
|
||||
display: none;
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(94,86,105,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
block-size: 1.5625rem;
|
||||
font-size: 0;
|
||||
inline-size: 1.5625rem;
|
||||
margin-inline-end: 0.25rem;
|
||||
|
||||
@media (max-width: 1264px) {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.v-theme--dark & {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ℹ️ Workaround of https://github.com/fullcalendar/fullcalendar/issues/6407
|
||||
.fc-col-header,
|
||||
.fc-daygrid-body,
|
||||
.fc-scrollgrid-sync-table,
|
||||
.fc-timegrid-body,
|
||||
.fc-timegrid-body table {
|
||||
inline-size: 100% !important;
|
||||
}
|
||||
}
|
195
resources/js/@core/scss/template/libs/vuetify/_overrides.scss
Normal file
195
resources/js/@core/scss/template/libs/vuetify/_overrides.scss
Normal file
@@ -0,0 +1,195 @@
|
||||
@use "@configured-variables" as variables;
|
||||
@use "@styles/variables/vuetify";
|
||||
@use "vuetify/lib/styles/tools/_elevation" as mixins_elevation;
|
||||
|
||||
// 👉 Typography
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.text-h1,
|
||||
.text-h2,
|
||||
.text-h3,
|
||||
.text-h4,
|
||||
.text-h5,
|
||||
.text-h6,
|
||||
.text-body-1,
|
||||
.text-subtitle-1,
|
||||
.text-button,
|
||||
.v-card-title {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-high-emphasis-opacity));
|
||||
}
|
||||
|
||||
.v-application,
|
||||
.text-body-2,
|
||||
.text-subtitle-2,
|
||||
.text-overline {
|
||||
color: rgba(var(--v-theme-on-background), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
|
||||
// 👉 Button
|
||||
.v-btn {
|
||||
.v-icon {
|
||||
--v-icon-size-multiplier: 0.953;
|
||||
}
|
||||
|
||||
&--icon .v-icon {
|
||||
--v-icon-size-multiplier: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Alert
|
||||
// custom icon style
|
||||
$alert-prepend-icon-font-size: 1.125rem !important;
|
||||
|
||||
.v-alert:not(.v-alert--prominent) {
|
||||
.v-alert__prepend {
|
||||
padding: 0.25rem;
|
||||
border-radius: 1rem;
|
||||
background-color: #fff;
|
||||
|
||||
.v-icon {
|
||||
block-size: $alert-prepend-icon-font-size;
|
||||
font-size: $alert-prepend-icon-font-size;
|
||||
inline-size: $alert-prepend-icon-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $color-name in variables.$theme-colors-name {
|
||||
.v-alert {
|
||||
|
||||
&:not(.v-alert--prominent).text-#{$color-name},
|
||||
&:not(.v-alert--prominent).bg-#{$color-name} {
|
||||
.v-alert__prepend {
|
||||
border: 3px solid rgb(var(--v-theme-#{$color-name}), 0.4);
|
||||
color: rgba(var(--v-theme-#{$color-name})) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&--variant-outlined:not(.v-alert--prominent),
|
||||
&--variant-tonal:not(.v-alert--prominent),
|
||||
&--variant-plain:not(.v-alert--prominent) {
|
||||
&.bg-#{$color-name},
|
||||
&.text-#{$color-name} {
|
||||
.v-alert__prepend {
|
||||
background-color: rgb(var(--v-theme-#{$color-name}));
|
||||
box-shadow: 0 0 0 3px rgba(var(--v-theme-#{$color-name}), 0.4);
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 VAvatar
|
||||
.v-avatar {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
// 👉 VChip
|
||||
.v-chip {
|
||||
line-height: normal;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.v-chip.v-chip--size-default .v-avatar {
|
||||
font-size: 0.8125rem;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
// 👉 VTooltip
|
||||
.v-tooltip {
|
||||
.v-overlay__content {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 VMenu
|
||||
.v-menu.v-overlay {
|
||||
.v-overlay__content {
|
||||
.v-list {
|
||||
.v-list-item--density-default {
|
||||
min-block-size: 2.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 VTabs
|
||||
.v-tabs--vertical:not(.v-tabs-pill) {
|
||||
border-inline-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
|
||||
.v-tab__slider {
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.v-tabs.v-tabs-pill:not(.v-tabs--stacked) {
|
||||
&.v-tabs--density-default {
|
||||
--v-tabs-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 VSliderThumb
|
||||
.v-slider-thumb__surface {
|
||||
border: 3px solid rgb(var(--v-theme-surface));
|
||||
|
||||
&::before {
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.v-slider-thumb__label {
|
||||
background: variables.$slider-thumb-label-color;
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
}
|
||||
|
||||
.v-slider-thumb__label::before {
|
||||
color: variables.$slider-thumb-label-color;
|
||||
}
|
||||
|
||||
// 👉 VTimeline
|
||||
.v-timeline {
|
||||
.v-timeline-item:not(:last-child) {
|
||||
.v-timeline-item__body {
|
||||
margin-block-end: 0.95rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 VDatatable
|
||||
.v-data-table {
|
||||
th {
|
||||
background: rgb(var(--v-table-header-background)) !important;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500 !important;
|
||||
letter-spacing: 0.17px !important;
|
||||
text-transform: uppercase !important;
|
||||
|
||||
.v-data-table-header__content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 VTable
|
||||
.v-table {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important;
|
||||
|
||||
th {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
font-size: 0.75rem;
|
||||
text-align: center !important;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:first-child {
|
||||
text-align: start !important;
|
||||
}
|
||||
}
|
||||
}
|
237
resources/js/@core/scss/template/libs/vuetify/_variables.scss
Normal file
237
resources/js/@core/scss/template/libs/vuetify/_variables.scss
Normal file
@@ -0,0 +1,237 @@
|
||||
$shadow-key-umbra-opacity-custom: var(--v-shadow-key-umbra-opacity);
|
||||
$shadow-key-penumbra-opacity-custom: var(--v-shadow-key-penumbra-opacity);
|
||||
$shadow-key-ambient-opacity-custom: var(--v-shadow-key-ambient-opacity);
|
||||
/* stylelint-disable max-line-length */
|
||||
$font-family-custom: "Public Sans", sans-serif, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
/* stylelint-enable max-line-length */
|
||||
|
||||
@forward "../../../base/libs/vuetify/variables" with (
|
||||
// 👉 font-family
|
||||
$body-font-family: $font-family-custom !default,
|
||||
|
||||
// 👉 border-radius
|
||||
$border-radius-root: 6px !default,
|
||||
|
||||
$shadow-key-umbra: (
|
||||
0: (0 0 0 0 var(--v-shadow-key-umbra-opacity)),
|
||||
1: (0 2px 1px -1px var(--v-shadow-key-umbra-opacity)),
|
||||
2: (0 3px 1px -2px var(--v-shadow-key-umbra-opacity)),
|
||||
3: (0 1px 6px -2px var(--v-shadow-key-umbra-opacity)),
|
||||
4: (0 1px 7px -2px var(--v-shadow-key-umbra-opacity)),
|
||||
5: (0 3px 5px -1px var(--v-shadow-key-umbra-opacity)),
|
||||
6: (0 2px 9px -2px var(--v-shadow-key-umbra-opacity)),
|
||||
7: (0 4px 5px -2px var(--v-shadow-key-umbra-opacity)),
|
||||
8: (0 5px 5px -3px var(--v-shadow-key-umbra-opacity)),
|
||||
9: (0 5px 6px -3px var(--v-shadow-key-umbra-opacity)),
|
||||
10: (0 6px 6px -3px var(--v-shadow-key-umbra-opacity)),
|
||||
11: (0 6px 7px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
12: (0 7px 8px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
13: (0 7px 8px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
14: (0 7px 9px -4px var(--v-shadow-key-umbra-opacity)),
|
||||
15: (0 8px 9px -5px var(--v-shadow-key-umbra-opacity)),
|
||||
16: (0 8px 10px -5px var(--v-shadow-key-umbra-opacity)),
|
||||
17: (0 8px 11px -5px var(--v-shadow-key-umbra-opacity)),
|
||||
18: (0 9px 11px -5px var(--v-shadow-key-umbra-opacity)),
|
||||
19: (0 9px 12px -6px var(--v-shadow-key-umbra-opacity)),
|
||||
20: (0 10px 13px -6px var(--v-shadow-key-umbra-opacity)),
|
||||
21: (0 10px 13px -6px var(--v-shadow-key-umbra-opacity)),
|
||||
22: (0 10px 14px -6px var(--v-shadow-key-umbra-opacity)),
|
||||
23: (0 11px 14px -7px var(--v-shadow-key-umbra-opacity)),
|
||||
24: (0 11px 15px -7px var(--v-shadow-key-umbra-opacity))
|
||||
) !default,
|
||||
|
||||
$shadow-key-penumbra: (
|
||||
0: (0 0 0 0 $shadow-key-penumbra-opacity-custom),
|
||||
1: (0 1px 1px 0 $shadow-key-penumbra-opacity-custom),
|
||||
2: (0 2px 2px 0 $shadow-key-penumbra-opacity-custom),
|
||||
3: (0 2px 6px 1px $shadow-key-penumbra-opacity-custom),
|
||||
4: (0 3px 7px 1px $shadow-key-penumbra-opacity-custom),
|
||||
5: (0 5px 8px 0 $shadow-key-penumbra-opacity-custom),
|
||||
6: (0 4px 9px 1px $shadow-key-penumbra-opacity-custom),
|
||||
7: (0 7px 10px 1px $shadow-key-penumbra-opacity-custom),
|
||||
8: (0 8px 10px 1px $shadow-key-penumbra-opacity-custom),
|
||||
9: (0 9px 12px 1px $shadow-key-penumbra-opacity-custom),
|
||||
10: (0 10px 14px 1px $shadow-key-penumbra-opacity-custom),
|
||||
11: (0 11px 15px 1px $shadow-key-penumbra-opacity-custom),
|
||||
12: (0 12px 17px 2px $shadow-key-penumbra-opacity-custom),
|
||||
13: (0 13px 19px 2px $shadow-key-penumbra-opacity-custom),
|
||||
14: (0 14px 21px 2px $shadow-key-penumbra-opacity-custom),
|
||||
15: (0 15px 22px 2px $shadow-key-penumbra-opacity-custom),
|
||||
16: (0 16px 24px 2px $shadow-key-penumbra-opacity-custom),
|
||||
17: (0 17px 26px 2px $shadow-key-penumbra-opacity-custom),
|
||||
18: (0 18px 28px 2px $shadow-key-penumbra-opacity-custom),
|
||||
19: (0 19px 29px 2px $shadow-key-penumbra-opacity-custom),
|
||||
20: (0 20px 31px 3px $shadow-key-penumbra-opacity-custom),
|
||||
21: (0 21px 33px 3px $shadow-key-penumbra-opacity-custom),
|
||||
22: (0 22px 35px 3px $shadow-key-penumbra-opacity-custom),
|
||||
23: (0 23px 36px 3px $shadow-key-penumbra-opacity-custom),
|
||||
24: (0 24px 38px 3px $shadow-key-penumbra-opacity-custom)
|
||||
) !default,
|
||||
|
||||
$shadow-key-ambient: (
|
||||
0: (0 0 0 0 $shadow-key-ambient-opacity-custom),
|
||||
1: (0 1px 3px 0 $shadow-key-ambient-opacity-custom),
|
||||
2: (0 1px 5px 0 $shadow-key-ambient-opacity-custom),
|
||||
3: (0 1px 4px 2px $shadow-key-ambient-opacity-custom),
|
||||
4: (0 1px 4px 2px $shadow-key-ambient-opacity-custom),
|
||||
5: (0 1px 14px 0 $shadow-key-ambient-opacity-custom),
|
||||
6: (0 2px 6px 4px $shadow-key-ambient-opacity-custom),
|
||||
7: (0 2px 16px 1px $shadow-key-ambient-opacity-custom),
|
||||
8: (0 3px 14px 2px $shadow-key-ambient-opacity-custom),
|
||||
9: (0 3px 16px 2px $shadow-key-ambient-opacity-custom),
|
||||
10: (0 4px 18px 3px $shadow-key-ambient-opacity-custom),
|
||||
11: (0 4px 20px 3px $shadow-key-ambient-opacity-custom),
|
||||
12: (0 5px 22px 4px $shadow-key-ambient-opacity-custom),
|
||||
13: (0 5px 24px 4px $shadow-key-ambient-opacity-custom),
|
||||
14: (0 5px 26px 4px $shadow-key-ambient-opacity-custom),
|
||||
15: (0 6px 28px 5px $shadow-key-ambient-opacity-custom),
|
||||
16: (0 6px 30px 5px $shadow-key-ambient-opacity-custom),
|
||||
17: (0 6px 32px 5px $shadow-key-ambient-opacity-custom),
|
||||
18: (0 7px 34px 6px $shadow-key-ambient-opacity-custom),
|
||||
19: (0 7px 36px 6px $shadow-key-ambient-opacity-custom),
|
||||
20: (0 8px 38px 7px $shadow-key-ambient-opacity-custom),
|
||||
21: (0 8px 40px 7px $shadow-key-ambient-opacity-custom),
|
||||
22: (0 8px 42px 7px $shadow-key-ambient-opacity-custom),
|
||||
23: (0 9px 44px 8px $shadow-key-ambient-opacity-custom),
|
||||
24: (0 9px 46px 8px $shadow-key-ambient-opacity-custom)
|
||||
) !default,
|
||||
|
||||
// 👉 Typography
|
||||
$typography: (
|
||||
"h1": (
|
||||
"weight": 500,
|
||||
"line-height": 7rem,
|
||||
"letter-spacing": -0.0938rem,
|
||||
),
|
||||
"h2": (
|
||||
"weight": 500,
|
||||
"line-height": 4.5rem,
|
||||
"letter-spacing": -0.0313rem,
|
||||
),
|
||||
"h3": (
|
||||
"weight": 500,
|
||||
"line-height": 3.5rem,
|
||||
),
|
||||
"h4": (
|
||||
"weight": 500,
|
||||
"letter-spacing": 0.0156rem,
|
||||
),
|
||||
"h5": (
|
||||
"weight": 500,
|
||||
),
|
||||
"h6": (
|
||||
"letter-spacing": 0.0094rem,
|
||||
),
|
||||
"subtitle-1": (
|
||||
"letter-spacing": 0.0094rem,
|
||||
),
|
||||
"subtitle-2": (
|
||||
"line-height": 1.3125rem,
|
||||
"letter-spacing": 0.0063rem,
|
||||
),
|
||||
"body-1": (
|
||||
"letter-spacing": 0.0094rem,
|
||||
),
|
||||
"body-2": (
|
||||
"line-height": 1.3125rem,
|
||||
"letter-spacing": 0.0094rem,
|
||||
),
|
||||
"caption": (
|
||||
"line-height": 0.875rem,
|
||||
"letter-spacing": 0.025rem,
|
||||
),
|
||||
"button": (
|
||||
"line-height": 1.5rem,
|
||||
"letter-spacing": 0.025rem,
|
||||
),
|
||||
"overline": (
|
||||
"weight": 400,
|
||||
"line-height": 0.875rem,
|
||||
"letter-spacing": 0.0625rem,
|
||||
),
|
||||
) !default,
|
||||
|
||||
// 👉 Alert
|
||||
$alert-density: ("default": 0, "comfortable": -0.625, "compact": -2) !default,
|
||||
$alert-title-font-size: 1rem !default,
|
||||
$alert-title-line-height: 1.5rem !default,
|
||||
$alert-prepend-margin-inline-end: 0.75rem !default,
|
||||
|
||||
// 👉 Badges
|
||||
$badge-dot-height: 0.5rem !default,
|
||||
$badge-dot-width: 0.5rem !default,
|
||||
|
||||
// 👉 Button
|
||||
$button-height: 38px !default,
|
||||
$button-icon-density: ("default": 2.5, "comfortable": 0, "compact": -1.5) !default,
|
||||
$button-card-actions-padding: 0 12px !default,
|
||||
|
||||
// 👉 Chip
|
||||
$chip-font-size: 13px !default,
|
||||
$chip-close-size: 22px !default,
|
||||
$chip-label-border-radius: 4px !default,
|
||||
$chip-density: ("default": 0, "comfortable": -1, "compact": -2) !default,
|
||||
|
||||
// 👉 Dialog
|
||||
$dialog-card-header-padding: 20px 20px 0 !default,
|
||||
$dialog-card-text-padding: 20px !default,
|
||||
$dialog-elevation: 16 !default,
|
||||
|
||||
// 👉 Expansion Panel
|
||||
$expansion-panel-title-padding: 14px 20px !default,
|
||||
$expansion-panel-title-font-size: 1rem !default,
|
||||
$expansion-panel-active-title-min-height: 51px !default,
|
||||
$expansion-panel-title-min-height: 51px !default,
|
||||
$expansion-panel-text-padding: 6px 20px 20px !default,
|
||||
|
||||
// 👉 List
|
||||
$list-item-icon-margin-end: 12px !default,
|
||||
|
||||
// 👉 Pagination
|
||||
$pagination-item-margin: 0.2rem !default,
|
||||
|
||||
// 👉 Snackbar
|
||||
$snackbar-border-radius: 8px !default,
|
||||
$snackbar-btn-padding: 0 12px !default,
|
||||
$snackbar-background: rgb(var(--v-snackbar-background)) !default,
|
||||
$snackbar-color: rgb(var(--v-snackbar-color)) !default,
|
||||
|
||||
// 👉 Tooltip
|
||||
$tooltip-background-color: rgba(var(--v-tooltip-background),var(--v-tooltip-opacity)) !default,
|
||||
$tooltip-padding: 4px 8px !default,
|
||||
$tooltip-line-height: 16px !default,
|
||||
$tooltip-font-size: 11px !default,
|
||||
|
||||
// 👉 Timeline
|
||||
$timeline-dot-divider-background: transparent !default,
|
||||
$timeline-divider-line-thickness: 1px !default,
|
||||
$timeline-item-padding: 16px !default,
|
||||
|
||||
// 👉 input
|
||||
$input-details-padding-above: 3px !default,
|
||||
$text-field-details-padding-inline: 14px !default,
|
||||
|
||||
// 👉 combobox
|
||||
$combobox-content-elevation: 6 !default,
|
||||
|
||||
// 👉 Range slider
|
||||
$slider-track-active-size: 4px !default,
|
||||
$slider-thumb-label-height: 29px !default,
|
||||
$slider-thumb-label-padding: 4px 12px !default,
|
||||
$slider-thumb-label-font-size: 12px !default,
|
||||
$slider-track-border-radius: 12px !default,
|
||||
|
||||
// 👉 Card
|
||||
$card-item-padding: 1.5rem !default,
|
||||
$card-border-radius: 0.5rem !default,
|
||||
$card-text-padding: 1.5rem !default,
|
||||
$card-text-font-size: 1rem !default,
|
||||
$card-title-padding: 0.5rem 1.5rem !default,
|
||||
$card-subtitle-padding: 0 1.5rem !default,
|
||||
$card-prepend-padding-inline-end: 0.625rem !default,
|
||||
$card-append-padding-inline-start: 0.625rem !default,
|
||||
|
||||
// 👉 Button Group
|
||||
$btn-group-height: 38px !default,
|
||||
);
|
2
resources/js/@core/scss/template/libs/vuetify/index.scss
Normal file
2
resources/js/@core/scss/template/libs/vuetify/index.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@use "@core/scss/base/libs/vuetify";
|
||||
@use "overrides";
|
14
resources/js/@core/scss/template/pages/misc.scss
Normal file
14
resources/js/@core/scss/template/pages/misc.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.layout-blank {
|
||||
.misc-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.25rem;
|
||||
min-block-size: calc(var(--vh, 1vh) * 100);
|
||||
}
|
||||
|
||||
.misc-avatar {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
45
resources/js/@core/scss/template/pages/page-auth.scss
Normal file
45
resources/js/@core/scss/template/pages/page-auth.scss
Normal file
@@ -0,0 +1,45 @@
|
||||
.layout-blank {
|
||||
.auth-wrapper {
|
||||
min-block-size: calc(var(--vh, 1vh) * 100);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
z-index: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.auth-v1-top-shape,
|
||||
.auth-v1-bottom-shape {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.auth-v1-top-shape {
|
||||
block-size: 148px;
|
||||
inline-size: 148px;
|
||||
inset-block-start: -2.5rem;
|
||||
inset-inline-end: -2.5rem;
|
||||
}
|
||||
|
||||
.auth-v1-bottom-shape {
|
||||
block-size: 240px;
|
||||
inline-size: 240px;
|
||||
inset-block-end: -4.5rem;
|
||||
inset-inline-start: -3rem;
|
||||
}
|
||||
|
||||
.auth-illustration {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.skin--bordered {
|
||||
.auth-card-v2 {
|
||||
border-inline-start: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)) !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
@use "vuetify/lib/styles/tools/elevation" as elevation;
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
%default-layout-vertical-nav-floating-navbar-and-sticky-elevated-navbar-scrolled {
|
||||
// If navbar is contained => Squeeze navbar content on scroll
|
||||
@if variables.$layout-vertical-nav-navbar-is-contained {
|
||||
padding-inline: 1.5rem;
|
||||
|
||||
@include elevation.elevation(4);
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
@forward "vertical-nav";
|
||||
@forward "nav";
|
||||
@forward "default-layout-vertical-nav";
|
33
resources/js/@core/scss/template/placeholders/_nav.scss
Normal file
33
resources/js/@core/scss/template/placeholders/_nav.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
// ℹ️ This is common style that needs to be applied to both navs
|
||||
%nav {
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
}
|
||||
|
||||
/*
|
||||
Active nav link styles for horizontal & vertical nav
|
||||
|
||||
For horizontal nav it will be only applied to top level nav items
|
||||
For vertical nav it will be only applied to nav links (not nav groups)
|
||||
*/
|
||||
%nav-link-active {
|
||||
--v-activated-opacity: 0.16;
|
||||
|
||||
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
|
||||
box-shadow: none;
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
// style for vertical nav nested icon
|
||||
%nav-link-nested-active {
|
||||
background-color: transparent !important;
|
||||
box-shadow: none;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !important;
|
||||
font-weight: 600;
|
||||
|
||||
// style for nested dot icon
|
||||
.nav-item-icon {
|
||||
color: rgb(var(--v-global-theme-primary)) !important;
|
||||
filter: drop-shadow(rgb(var(--v-global-theme-primary)) 0 0 2px);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
// Open & Active nav group styles
|
||||
%vertical-nav-group-active {
|
||||
--v-theme-overlay-multiplier: 2;
|
||||
|
||||
color: rgb(var(--v-global-theme-primary));
|
||||
|
||||
&:hover {
|
||||
--v-theme-overlay-multiplier: 4;
|
||||
}
|
||||
}
|
||||
|
||||
// nav-group and nav-link border radius
|
||||
%vertical-nav-item-interactive {
|
||||
border-radius: 0.375rem;
|
||||
margin-block-end: 0.125rem;
|
||||
}
|
||||
|
||||
// ℹ️ Icon styling for icon nested inside another nav item (2nd level)
|
||||
%vertical-nav-items-nested-icon {
|
||||
transition: transform 0.25s ease-in-out 0s;
|
||||
}
|
2
resources/js/@core/utils/external_api.js
Normal file
2
resources/js/@core/utils/external_api.js
Normal file
File diff suppressed because one or more lines are too long
46
resources/js/@core/utils/formatters.js
Normal file
46
resources/js/@core/utils/formatters.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { isToday } from './index'
|
||||
|
||||
export const avatarText = value => {
|
||||
if (!value)
|
||||
return ''
|
||||
const nameArray = value.split(' ')
|
||||
|
||||
return nameArray.map(word => word.charAt(0).toUpperCase()).join('')
|
||||
}
|
||||
|
||||
// TODO: Try to implement this: https://twitter.com/fireship_dev/status/1565424801216311297
|
||||
export const kFormatter = num => {
|
||||
const regex = /\B(?=(\d{3})+(?!\d))/g
|
||||
|
||||
return Math.abs(num) > 9999 ? `${Math.sign(num) * +((Math.abs(num) / 1000).toFixed(1))}k` : Math.abs(num).toFixed(0).replace(regex, ',')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format and return date in Humanize format
|
||||
* Intl docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format
|
||||
* Intl Constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
* @param {String} value date to format
|
||||
* @param {Intl.DateTimeFormatOptions} formatting Intl object to format with
|
||||
*/
|
||||
export const formatDate = (value, formatting = { month: 'short', day: 'numeric', year: 'numeric' }) => {
|
||||
if (!value)
|
||||
return value
|
||||
|
||||
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return short human friendly month representation of date
|
||||
* Can also convert date to only time if date is of today (Better UX)
|
||||
* @param {String} value date to format
|
||||
* @param {Boolean} toTimeForCurrentDay Shall convert to time if day is today/current
|
||||
*/
|
||||
export const formatDateToMonthShort = (value, toTimeForCurrentDay = true) => {
|
||||
const date = new Date(value)
|
||||
let formatting = { month: 'short', day: 'numeric' }
|
||||
if (toTimeForCurrentDay && isToday(date))
|
||||
formatting = { hour: 'numeric', minute: 'numeric' }
|
||||
|
||||
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
|
||||
}
|
||||
export const prefixWithPlus = value => value > 0 ? `+${value}` : value
|
31
resources/js/@core/utils/index.js
Normal file
31
resources/js/@core/utils/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// 👉 IsEmpty
|
||||
export const isEmpty = value => {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return true
|
||||
|
||||
return !!(Array.isArray(value) && value.length === 0)
|
||||
}
|
||||
|
||||
// 👉 IsNullOrUndefined
|
||||
export const isNullOrUndefined = value => {
|
||||
return value === null || value === undefined
|
||||
}
|
||||
|
||||
// 👉 IsEmptyArray
|
||||
export const isEmptyArray = arr => {
|
||||
return Array.isArray(arr) && arr.length === 0
|
||||
}
|
||||
|
||||
// 👉 IsObject
|
||||
export const isObject = obj => obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj)
|
||||
export const isToday = date => {
|
||||
const today = new Date()
|
||||
|
||||
return (
|
||||
/* eslint-disable operator-linebreak */
|
||||
date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear()
|
||||
/* eslint-enable */
|
||||
)
|
||||
}
|
208
resources/js/@core/utils/validators.js
Normal file
208
resources/js/@core/utils/validators.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { isEmpty, isEmptyArray, isNullOrUndefined } from './index';
|
||||
|
||||
// 👉 Required Validator
|
||||
export const requiredValidator = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'This field is required'
|
||||
|
||||
return !!String(value).trim().length || 'This field is required'
|
||||
}
|
||||
export const cardNumberValidator = value => {
|
||||
// Adjust the regex based on your credit card number pattern
|
||||
const cardNumberPattern = /^(\d{14}|\d{15}|\d{16})$/;
|
||||
|
||||
return cardNumberPattern.test(value) || 'Invalid credit card number';
|
||||
};
|
||||
export const requiredGender = (value) => !!value || 'Gender is required'
|
||||
export const requiredLicenseNumber = (value) => !!value || 'Medical License Number is required'
|
||||
export const requiredYearsofExperience = (value) => !!value || 'Years of Experience is required'
|
||||
export const requiredSpecialty = (value) => !!value || 'Practice or Provider of Specialty is required'
|
||||
export const requiredFirstName = (value) => !!value || 'First Name is required'
|
||||
export const requiredZip = (value) => !!value || 'Zip Code is required'
|
||||
|
||||
export const expiryValidator = value => {
|
||||
// Check if the format is MM/YY
|
||||
const formatRegex = /^(0[1-9]|1[0-2])\/\d{2}$/;
|
||||
if (!formatRegex.test(value)) {
|
||||
return 'Invalid date format. Please use MM/YY';
|
||||
}
|
||||
|
||||
// Check if the date is not expired (assuming the current date is 01/24 for example)
|
||||
const currentDate = new Date();
|
||||
const currentYear = currentDate.getFullYear() % 100;
|
||||
const currentMonth = currentDate.getMonth() + 1;
|
||||
|
||||
const [inputMonth, inputYear] = value.split('/').map(Number);
|
||||
|
||||
if (inputYear < currentYear || (inputYear === currentYear && inputMonth < currentMonth)) {
|
||||
return 'The card has expired';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
export const cvvValidator = value => {
|
||||
return /^\d{3}$/.test(value) || 'Must be a 3-digit number';
|
||||
}
|
||||
export const requiredAddress = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Address is required'
|
||||
|
||||
return !!String(value).trim().length || 'Address is required'
|
||||
}
|
||||
export const requiredLocation = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Location is required'
|
||||
|
||||
return !!String(value).trim().length || 'Location is required'
|
||||
}
|
||||
export const requiredCity = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'City is required'
|
||||
|
||||
return !!String(value).trim().length || 'City is required'
|
||||
}
|
||||
export const requiredPassword = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Password field is required'
|
||||
|
||||
return !!String(value).trim().length || 'Password field is required'
|
||||
}
|
||||
export const requiredConfirm = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Confirm Password field is required'
|
||||
|
||||
return !!String(value).trim().length || ' Confirm Password field is required'
|
||||
}
|
||||
export const requiredName = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Name field is required'
|
||||
|
||||
return !!String(value).trim().length || 'Name is required'
|
||||
}
|
||||
export const requiredLastName = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Last Name field is required'
|
||||
|
||||
return !!String(value).trim().length || ' Last Name is required'
|
||||
}
|
||||
export const requiredPhone = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Phone is required'
|
||||
|
||||
return !!String(value).trim().length || ' Phone is required'
|
||||
}
|
||||
|
||||
export const requiredEmail = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Email field is required'
|
||||
|
||||
return !!String(value).trim().length || 'Email is required'
|
||||
}
|
||||
export const requiredState = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'State field is required'
|
||||
|
||||
return !!String(value).trim().length || 'State is required'
|
||||
}
|
||||
export const requiredDate = value => {
|
||||
if (isNullOrUndefined(value) || isEmptyArray(value) || value === false)
|
||||
return 'Date of Birth field is required'
|
||||
|
||||
return !!String(value).trim().length || 'Date of Birth is required'
|
||||
}
|
||||
// 👉 Email Validator
|
||||
export const emailValidator = value => {
|
||||
if (isEmpty(value))
|
||||
return true
|
||||
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
if (Array.isArray(value))
|
||||
return value.every(val => re.test(String(val))) || 'The Email field must be a valid email'
|
||||
|
||||
return re.test(String(value)) || 'The Email field must be a valid email'
|
||||
}
|
||||
|
||||
// 👉 Password Validator
|
||||
export const passwordValidator = password => {
|
||||
const regExp = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%&*()]).{8,}/
|
||||
const validPassword = regExp.test(password)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line operator-linebreak
|
||||
validPassword ||
|
||||
'Field must contain at least one uppercase, lowercase, special character and digit with min 8 chars')
|
||||
}
|
||||
|
||||
// 👉 Confirm Password Validator
|
||||
export const confirmedValidator = (value, target) => value === target || 'The Confirm Password field confirmation does not match'
|
||||
|
||||
// 👉 Between Validator
|
||||
export const betweenValidator = (value, min, max) => {
|
||||
const valueAsNumber = Number(value)
|
||||
|
||||
return (Number(min) <= valueAsNumber && Number(max) >= valueAsNumber) || `Enter number between ${min} and ${max}`
|
||||
}
|
||||
|
||||
// 👉 Integer Validator
|
||||
export const integerValidator = value => {
|
||||
if (isEmpty(value))
|
||||
return true
|
||||
if (Array.isArray(value))
|
||||
return value.every(val => /^-?[0-9]+$/.test(String(val))) || 'This field must be an integer'
|
||||
|
||||
return /^-?[0-9]+$/.test(String(value)) || 'This field must be an integer'
|
||||
}
|
||||
|
||||
// 👉 Regex Validator
|
||||
export const regexValidator = (value, regex) => {
|
||||
if (isEmpty(value))
|
||||
return true
|
||||
let regeX = regex
|
||||
if (typeof regeX === 'string')
|
||||
regeX = new RegExp(regeX)
|
||||
if (Array.isArray(value))
|
||||
return value.every(val => regexValidator(val, regeX))
|
||||
|
||||
return regeX.test(String(value)) || 'The Regex field format is invalid'
|
||||
}
|
||||
|
||||
// 👉 Alpha Validator
|
||||
export const alphaValidator = value => {
|
||||
if (isEmpty(value))
|
||||
return true
|
||||
|
||||
return /^[A-Z]*$/i.test(String(value)) || 'The Alpha field may only contain alphabetic characters'
|
||||
}
|
||||
|
||||
// 👉 URL Validator
|
||||
export const urlValidator = value => {
|
||||
if (isEmpty(value))
|
||||
return true
|
||||
const re = /^(http[s]?:\/\/){0,1}(www\.){0,1}[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,5}[\.]{0,1}/
|
||||
|
||||
return re.test(String(value)) || 'URL is invalid'
|
||||
}
|
||||
|
||||
// 👉 Length Validator
|
||||
export const lengthValidator = (value, length) => {
|
||||
if (isEmpty(value))
|
||||
return true
|
||||
|
||||
return String(value).length === length || `The Min Character field must be at least ${length} characters`
|
||||
}
|
||||
|
||||
// 👉 Alpha-dash Validator
|
||||
export const alphaDashValidator = value => {
|
||||
if (isEmpty(value))
|
||||
return true
|
||||
const valueAsString = String(value)
|
||||
|
||||
return /^[0-9A-Z_-]*$/i.test(valueAsString) || 'All Character are not valid'
|
||||
}
|
||||
|
||||
export const validUSAPhone = value => {
|
||||
if (isEmpty(value))
|
||||
return true
|
||||
const valueAsString = String(value)
|
||||
|
||||
return /^\(\d{3}\)\s\d{3}-\d{4}$/i.test(valueAsString) || 'Phone are not valid'
|
||||
}
|
BIN
resources/js/@fake-db/.DS_Store
vendored
Normal file
BIN
resources/js/@fake-db/.DS_Store
vendored
Normal file
Binary file not shown.
696
resources/js/@fake-db/app-bar-search/index.js
Normal file
696
resources/js/@fake-db/app-bar-search/index.js
Normal file
@@ -0,0 +1,696 @@
|
||||
// ** Mock Adapter
|
||||
import mock from '@/@fake-db/mock'
|
||||
|
||||
const database = [
|
||||
{
|
||||
id: 1,
|
||||
url: { name: 'dashboards-analytics' },
|
||||
icon: 'tabler-timeline',
|
||||
title: 'Analytics Dashboard',
|
||||
category: 'dashboards',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
url: { name: 'dashboards-ecommerce' },
|
||||
icon: 'tabler-shopping-cart',
|
||||
title: 'eCommerce Dashboard',
|
||||
category: 'dashboards',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
url: { name: 'dashboards-crm' },
|
||||
icon: 'tabler-shopping-cart',
|
||||
title: 'CRM Dashboard',
|
||||
category: 'dashboards',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: { name: 'apps-email' },
|
||||
icon: 'tabler-mail',
|
||||
title: 'Email',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: { name: 'apps-chat' },
|
||||
icon: 'tabler-message',
|
||||
title: 'Chat',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
url: { name: 'apps-calendar' },
|
||||
icon: 'tabler-calendar',
|
||||
title: 'Calendar',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
url: { name: 'apps-invoice-list' },
|
||||
icon: 'tabler-list',
|
||||
title: 'Invoice List',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
url: { name: 'apps-invoice-preview-id', params: { id: '5036' } },
|
||||
icon: 'tabler-file-description',
|
||||
title: 'Invoice Preview',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
url: { name: 'apps-invoice-edit-id', params: { id: '5036' } },
|
||||
icon: 'tabler-file-pencil',
|
||||
title: 'Invoice Edit',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
url: { name: 'apps-invoice-add' },
|
||||
icon: 'tabler-file-plus',
|
||||
title: 'Invoice Add',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
url: { name: 'apps-user-list' },
|
||||
icon: 'tabler-user',
|
||||
title: 'User List',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
url: { name: 'apps-user-view-id', params: { id: 21 } },
|
||||
icon: 'tabler-eye',
|
||||
title: 'User View',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
url: { name: 'pages-help-center' },
|
||||
icon: 'tabler-help',
|
||||
title: 'Help Center',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
url: { name: 'pages-user-profile-tab', params: { tab: 'profile' } },
|
||||
icon: 'tabler-user',
|
||||
title: 'User Profile - Profile',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
url: { name: 'pages-account-settings-tab', params: { tab: 'account' } },
|
||||
icon: 'tabler-user',
|
||||
title: 'Account Settings - Account',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
url: { name: 'pages-account-settings-tab', params: { tab: 'security' } },
|
||||
icon: 'tabler-lock-open',
|
||||
title: 'Account Settings - Security',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
url: { name: 'pages-account-settings-tab', params: { tab: 'billing-plans' } },
|
||||
icon: 'tabler-currency-dollar',
|
||||
title: 'Account Settings - Billing',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
url: { name: 'pages-account-settings-tab', params: { tab: 'notification' } },
|
||||
icon: 'tabler-bell',
|
||||
title: 'Account Settings - Notifications',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
url: { name: 'pages-account-settings-tab', params: { tab: 'connection' } },
|
||||
icon: 'tabler-link',
|
||||
title: 'Account Settings - Connections',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
url: { name: 'pages-pricing' },
|
||||
icon: 'tabler-currency-dollar',
|
||||
title: 'Pricing',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
url: { name: 'pages-faq' },
|
||||
icon: 'tabler-help',
|
||||
title: 'FAQ',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
url: { name: 'pages-misc-coming-soon' },
|
||||
icon: 'tabler-clock',
|
||||
title: 'Coming Soon',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
url: { name: 'pages-misc-under-maintenance' },
|
||||
icon: 'tabler-settings',
|
||||
title: 'Under Maintenance',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 26,
|
||||
url: { name: 'pages-misc-not-found' },
|
||||
icon: 'tabler-alert-circle',
|
||||
title: 'Page Not Found - 404',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 27,
|
||||
url: { name: 'pages-misc-not-authorized' },
|
||||
icon: 'tabler-users',
|
||||
title: 'Not Authorized - 401',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 28,
|
||||
url: { name: 'pages-misc-internal-server-error' },
|
||||
icon: 'tabler-list',
|
||||
title: 'Server Error - 500',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 29,
|
||||
url: { name: 'pages-authentication-login-v1' },
|
||||
icon: 'tabler-login',
|
||||
title: 'Login V1',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 30,
|
||||
url: { name: 'pages-authentication-login-v2' },
|
||||
icon: 'tabler-login',
|
||||
title: 'Login V2',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 31,
|
||||
url: { name: 'pages-authentication-register-v1' },
|
||||
icon: 'tabler-user-plus',
|
||||
title: 'Register V1',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
url: { name: 'pages-authentication-register-v2' },
|
||||
icon: 'tabler-user-plus',
|
||||
title: 'Register V2',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 42,
|
||||
icon: 'tabler-mail',
|
||||
category: 'appsPages',
|
||||
title: 'Verify Email V1',
|
||||
url: { name: 'pages-authentication-verify-email-v1' },
|
||||
},
|
||||
{
|
||||
id: 43,
|
||||
icon: 'tabler-mail',
|
||||
category: 'appsPages',
|
||||
title: 'Verify Email V2',
|
||||
url: { name: 'pages-authentication-verify-email-v2' },
|
||||
},
|
||||
{
|
||||
id: 35,
|
||||
url: { name: 'pages-authentication-forgot-password-v1' },
|
||||
icon: 'tabler-lock',
|
||||
title: 'Forgot Password V1',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 36,
|
||||
url: { name: 'pages-authentication-forgot-password-v2' },
|
||||
icon: 'tabler-lock',
|
||||
title: 'Forgot Password V2',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
url: { name: 'pages-authentication-reset-password-v1' },
|
||||
icon: 'tabler-lock',
|
||||
title: 'Reset Password V1',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 38,
|
||||
url: { name: 'pages-authentication-reset-password-v2' },
|
||||
icon: 'tabler-lock',
|
||||
title: 'Reset Password V2',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 48,
|
||||
icon: 'tabler-devices',
|
||||
category: 'appsPages',
|
||||
title: 'Two Steps V1',
|
||||
url: { name: 'pages-authentication-two-steps-v1' },
|
||||
},
|
||||
{
|
||||
id: 49,
|
||||
icon: 'tabler-devices',
|
||||
category: 'appsPages',
|
||||
title: 'Two Steps V2',
|
||||
url: { name: 'pages-authentication-two-steps-v2' },
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
url: { name: 'pages-typography' },
|
||||
icon: 'tabler-baseline',
|
||||
title: 'Typography',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 42,
|
||||
url: { name: 'pages-icons' },
|
||||
icon: 'tabler-brand-google',
|
||||
title: 'Icons',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 43,
|
||||
url: { name: 'pages-cards-card-basic' },
|
||||
icon: 'tabler-cards',
|
||||
title: 'Card Basic',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 56,
|
||||
url: { name: 'pages-cards-card-advance' },
|
||||
icon: 'tabler-cards',
|
||||
title: 'Card Advance',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 58,
|
||||
url: { name: 'pages-cards-card-statistics' },
|
||||
icon: 'tabler-chart-bar',
|
||||
title: 'Card Statistics',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 59,
|
||||
url: { name: 'pages-cards-card-widgets' },
|
||||
icon: 'tabler-id',
|
||||
title: 'Card Widgets',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 46,
|
||||
url: { name: 'pages-cards-card-actions' },
|
||||
icon: 'tabler-square-plus',
|
||||
title: 'Card Actions',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 47,
|
||||
url: { name: 'components-alert' },
|
||||
icon: 'tabler-alert-triangle',
|
||||
title: 'Alerts',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 48,
|
||||
url: { name: 'components-avatar' },
|
||||
icon: 'tabler-user-circle',
|
||||
title: 'Avatars',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 49,
|
||||
url: { name: 'components-badge' },
|
||||
icon: 'tabler-bell',
|
||||
title: 'Badges',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 50,
|
||||
url: { name: 'components-button' },
|
||||
icon: 'tabler-hand-click',
|
||||
title: 'Buttons',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 51,
|
||||
url: { name: 'components-chip' },
|
||||
icon: 'tabler-box',
|
||||
title: 'Chips',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 52,
|
||||
url: { name: 'components-dialog' },
|
||||
icon: 'tabler-square',
|
||||
title: 'Dialogs',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 53,
|
||||
url: { name: 'components-list' },
|
||||
icon: 'tabler-list',
|
||||
title: 'List',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 54,
|
||||
url: { name: 'components-menu' },
|
||||
icon: 'tabler-menu-2',
|
||||
title: 'Menu',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 55,
|
||||
url: { name: 'components-pagination' },
|
||||
icon: 'tabler-player-skip-forward',
|
||||
title: 'Pagination',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 56,
|
||||
url: { name: 'components-progress-circular' },
|
||||
icon: 'tabler-adjustments-alt',
|
||||
title: 'Progress Circular',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 83,
|
||||
url: { name: 'components-progress-linear' },
|
||||
icon: 'tabler-adjustments-alt',
|
||||
title: 'Progress Linear',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 57,
|
||||
url: { name: 'components-expansion-panel' },
|
||||
icon: 'tabler-fold',
|
||||
title: 'Expansion Panel',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 58,
|
||||
url: { name: 'components-snackbar' },
|
||||
icon: 'tabler-message',
|
||||
title: 'Snackbar',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 59,
|
||||
url: { name: 'components-tabs' },
|
||||
icon: 'tabler-square-plus',
|
||||
title: 'Tabs',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 60,
|
||||
url: { name: 'components-timeline' },
|
||||
icon: 'tabler-timeline-event',
|
||||
title: 'Timeline',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 61,
|
||||
url: { name: 'components-tooltip' },
|
||||
icon: 'tabler-message-chatbot',
|
||||
title: 'Tooltip',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 62,
|
||||
url: { name: 'forms-textfield' },
|
||||
icon: 'tabler-arrow-rotary-last-left',
|
||||
title: 'TextField',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 63,
|
||||
url: { name: 'forms-select' },
|
||||
icon: 'tabler-list-check',
|
||||
title: 'Select',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 64,
|
||||
url: { name: 'forms-checkbox' },
|
||||
icon: 'tabler-checkbox',
|
||||
title: 'Checkbox',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 65,
|
||||
url: { name: 'forms-radio' },
|
||||
icon: 'tabler-circle-dot',
|
||||
title: 'Radio',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 66,
|
||||
url: { name: 'forms-combobox' },
|
||||
icon: 'tabler-checkbox',
|
||||
title: 'Combobox',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 67,
|
||||
url: { name: 'forms-date-time-picker' },
|
||||
icon: 'tabler-calendar',
|
||||
title: 'Date Time picker',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 68,
|
||||
url: { name: 'forms-textarea' },
|
||||
icon: 'tabler-forms',
|
||||
title: 'Textarea',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 70,
|
||||
url: { name: 'forms-switch' },
|
||||
icon: 'tabler-toggle-left',
|
||||
title: 'Switch',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 71,
|
||||
url: { name: 'forms-file-input' },
|
||||
icon: 'tabler-upload',
|
||||
title: 'File Input',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 72,
|
||||
url: { name: 'forms-rating' },
|
||||
icon: 'tabler-star',
|
||||
title: 'Form Rating',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 73,
|
||||
url: { name: 'forms-slider' },
|
||||
icon: 'tabler-hand-click',
|
||||
title: 'Slider',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 74,
|
||||
url: { name: 'forms-range-slider' },
|
||||
icon: 'tabler-adjustments',
|
||||
title: 'Range Slider',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 75,
|
||||
url: { name: 'forms-form-layouts' },
|
||||
icon: 'tabler-box',
|
||||
title: 'Form Layouts',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 76,
|
||||
url: { name: 'forms-form-validation' },
|
||||
icon: 'tabler-checkbox',
|
||||
title: 'Form Validation',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 77,
|
||||
url: { name: 'charts-apex-chart' },
|
||||
icon: 'tabler-chart-line',
|
||||
title: 'Apex Charts',
|
||||
category: 'chartsMisc',
|
||||
},
|
||||
{
|
||||
id: 78,
|
||||
url: { name: 'charts-chartjs' },
|
||||
icon: 'tabler-chart-area',
|
||||
title: 'ChartJS',
|
||||
category: 'chartsMisc',
|
||||
},
|
||||
{
|
||||
id: 79,
|
||||
url: { name: 'access-control' },
|
||||
icon: 'tabler-shield',
|
||||
title: 'Access Control (ACL)',
|
||||
category: 'chartsMisc',
|
||||
},
|
||||
{
|
||||
id: 80,
|
||||
url: { name: 'pages-dialog-examples' },
|
||||
icon: 'tabler-square',
|
||||
title: 'Dialog Examples',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 81,
|
||||
url: { name: 'forms-custom-input' },
|
||||
icon: 'tabler-list-details',
|
||||
title: 'Custom Input',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 82,
|
||||
url: { name: 'forms-autocomplete' },
|
||||
icon: 'tabler-align-left',
|
||||
title: 'Autocomplete',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 83,
|
||||
url: { name: 'extensions-tour' },
|
||||
icon: 'mdi-cube-outline',
|
||||
title: 'Tour',
|
||||
category: 'userInterface',
|
||||
},
|
||||
{
|
||||
id: 84,
|
||||
url: { name: 'pages-authentication-register-multi-steps' },
|
||||
icon: 'tabler-user-plus',
|
||||
title: 'Register Multi-Steps',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 85,
|
||||
url: { name: 'wizard-examples-checkout' },
|
||||
icon: 'tabler-shopping-cart',
|
||||
title: 'Wizard - Checkout',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 86,
|
||||
url: { name: 'wizard-examples-create-deal' },
|
||||
icon: 'tabler-gift',
|
||||
title: 'Wizard - create deal',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 87,
|
||||
url: { name: 'wizard-examples-property-listing' },
|
||||
icon: 'tabler-home',
|
||||
title: 'Wizard - Property Listing',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 88,
|
||||
url: { name: 'apps-roles' },
|
||||
icon: 'tabler-shield',
|
||||
title: 'Roles',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 89,
|
||||
url: { name: 'apps-permissions' },
|
||||
icon: 'tabler-shield',
|
||||
title: 'Permissions',
|
||||
category: 'appsPages',
|
||||
},
|
||||
{
|
||||
id: 90,
|
||||
url: { name: 'tables-data-table' },
|
||||
icon: 'mdi-table',
|
||||
title: 'Data Table',
|
||||
category: 'formsTables',
|
||||
},
|
||||
{
|
||||
id: 91,
|
||||
url: { name: 'tables-simple-table' },
|
||||
icon: 'mdi-table',
|
||||
title: 'Simple Table',
|
||||
category: 'formsTables',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
// ** GET Search Data
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
mock.onGet('/app-bar/search').reply(config => {
|
||||
const { q = '' } = config.params
|
||||
const queryLowered = q.toLowerCase()
|
||||
|
||||
const exactData = {
|
||||
dashboards: [],
|
||||
appsPages: [],
|
||||
userInterface: [],
|
||||
formsTables: [],
|
||||
chartsMisc: [],
|
||||
}
|
||||
|
||||
const includeData = {
|
||||
dashboards: [],
|
||||
appsPages: [],
|
||||
userInterface: [],
|
||||
formsTables: [],
|
||||
chartsMisc: [],
|
||||
}
|
||||
|
||||
database.forEach(obj => {
|
||||
const isMatched = obj.title.toLowerCase().startsWith(queryLowered)
|
||||
if (isMatched && exactData[obj.category].length < 5)
|
||||
exactData[obj.category].push(obj)
|
||||
})
|
||||
database.forEach(obj => {
|
||||
const isMatched = !obj.title.toLowerCase().startsWith(queryLowered) && obj.title.toLowerCase().includes(queryLowered)
|
||||
if (isMatched && includeData[obj.category].length < 5)
|
||||
includeData[obj.category].push(obj)
|
||||
})
|
||||
|
||||
const categoriesCheck = []
|
||||
|
||||
Object.keys(exactData).forEach(category => {
|
||||
if (exactData[category].length > 0)
|
||||
categoriesCheck.push(category)
|
||||
})
|
||||
if (categoriesCheck.length === 0) {
|
||||
Object.keys(includeData).forEach(category => {
|
||||
if (includeData[category].length > 0)
|
||||
categoriesCheck.push(category)
|
||||
})
|
||||
}
|
||||
const resultsLength = categoriesCheck.length === 1 ? 5 : 3
|
||||
const mergedData = []
|
||||
|
||||
Object.keys(exactData).forEach(element => {
|
||||
if (exactData[element].length || includeData[element].length) {
|
||||
const r = exactData[element].concat(includeData[element]).slice(0, resultsLength)
|
||||
|
||||
r.unshift({ header: element, title: element })
|
||||
mergedData.push(...r)
|
||||
}
|
||||
})
|
||||
|
||||
return [200, [...mergedData]]
|
||||
})
|
385
resources/js/@fake-db/apps/chat.js
Normal file
385
resources/js/@fake-db/apps/chat.js
Normal file
@@ -0,0 +1,385 @@
|
||||
import mock from '@/@fake-db/mock'
|
||||
import { genId } from '@/@fake-db/utils'
|
||||
|
||||
// Images
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar4 from '@images/avatars/avatar-4.png'
|
||||
import avatar5 from '@images/avatars/avatar-5.png'
|
||||
import avatar6 from '@images/avatars/avatar-6.png'
|
||||
import avatar8 from '@images/avatars/avatar-8.png'
|
||||
|
||||
const previousDay = new Date(new Date().getTime() - 24 * 60 * 60 * 1000)
|
||||
const dayBeforePreviousDay = new Date(new Date().getTime() - 24 * 60 * 60 * 1000 * 2)
|
||||
|
||||
const database = {
|
||||
profileUser: {
|
||||
id: 11,
|
||||
avatar: avatar1,
|
||||
fullName: 'John Doe',
|
||||
role: 'admin',
|
||||
about: 'Dessert chocolate cake lemon drops jujubes. Biscuit cupcake ice cream bear claw brownie marshmallow.',
|
||||
status: 'online',
|
||||
settings: {
|
||||
isTwoStepAuthVerificationEnabled: true,
|
||||
isNotificationsOn: false,
|
||||
},
|
||||
},
|
||||
contacts: [
|
||||
{
|
||||
id: 1,
|
||||
fullName: 'Gavin Griffith',
|
||||
role: 'Frontend Developer',
|
||||
about: 'Cake pie jelly jelly beans. Marzipan lemon drops halvah cake. Pudding cookie lemon drops icing',
|
||||
avatar: avatar5,
|
||||
status: 'offline',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fullName: 'Harriet McBride',
|
||||
role: 'UI/UX Designer',
|
||||
about: 'Toffee caramels jelly-o tart gummi bears cake I love ice cream lollipop. Sweet liquorice croissant candy danish dessert icing. Cake macaroon gingerbread toffee sweet.',
|
||||
avatar: avatar2,
|
||||
status: 'busy',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
fullName: 'Danny Conner',
|
||||
role: 'Town planner',
|
||||
about: 'Soufflé soufflé caramels sweet roll. Jelly lollipop sesame snaps bear claw jelly beans sugar plum sugar plum.',
|
||||
avatar: '',
|
||||
status: 'busy',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
fullName: 'Janie West',
|
||||
role: 'Data scientist',
|
||||
about: 'Chupa chups candy canes chocolate bar marshmallow liquorice muffin. Lemon drops oat cake tart liquorice tart cookie. Jelly-o cookie tootsie roll halvah.',
|
||||
avatar: '',
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
fullName: 'Bryan Murray',
|
||||
role: 'Dietitian',
|
||||
about: 'Cake pie jelly jelly beans. Marzipan lemon drops halvah cake. Pudding cookie lemon drops icing',
|
||||
avatar: avatar5,
|
||||
status: 'busy',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
fullName: 'Albert Underwood',
|
||||
role: 'Marketing executive',
|
||||
about: 'Toffee caramels jelly-o tart gummi bears cake I love ice cream lollipop. Sweet liquorice croissant candy danish dessert icing. Cake macaroon gingerbread toffee sweet.',
|
||||
avatar: avatar6,
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
fullName: 'Adele Ross',
|
||||
role: 'Special educational needs teacher',
|
||||
about: 'Biscuit powder oat cake donut brownie ice cream I love soufflé. I love tootsie roll I love powder tootsie roll.',
|
||||
avatar: '',
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
fullName: 'Mark Berry',
|
||||
role: 'Advertising copywriter',
|
||||
about: 'Bear claw ice cream lollipop gingerbread carrot cake. Brownie gummi bears chocolate muffin croissant jelly I love marzipan wafer.',
|
||||
avatar: avatar3,
|
||||
status: 'away',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
fullName: 'Joseph Evans',
|
||||
role: 'Designer, television/film set',
|
||||
about: 'Gummies gummi bears I love candy icing apple pie I love marzipan bear claw. I love tart biscuit I love candy canes pudding chupa chups liquorice croissant.',
|
||||
avatar: avatar8,
|
||||
status: 'offline',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
fullName: 'Blake Carter',
|
||||
role: 'Building surveyor',
|
||||
about: 'Cake pie jelly jelly beans. Marzipan lemon drops halvah cake. Pudding cookie lemon drops icing',
|
||||
avatar: avatar4,
|
||||
status: 'away',
|
||||
},
|
||||
],
|
||||
chats: [
|
||||
{
|
||||
id: 1,
|
||||
userId: 2,
|
||||
unseenMsgs: 0,
|
||||
messages: [
|
||||
{
|
||||
message: 'Hi',
|
||||
time: 'Mon Dec 10 2018 07:45:00 GMT+0000 (GMT)',
|
||||
senderId: 11,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Hello. How can I help You?',
|
||||
time: 'Mon Dec 11 2018 07:45:15 GMT+0000 (GMT)',
|
||||
senderId: 2,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Can I get details of my last transaction I made last month? 🤔',
|
||||
time: 'Mon Dec 11 2018 07:46:10 GMT+0000 (GMT)',
|
||||
senderId: 11,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'We need to check if we can provide you such information.',
|
||||
time: 'Mon Dec 11 2018 07:45:15 GMT+0000 (GMT)',
|
||||
senderId: 2,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'I will inform you as I get update on this.',
|
||||
time: 'Mon Dec 11 2018 07:46:15 GMT+0000 (GMT)',
|
||||
senderId: 2,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'If it takes long you can mail me at my mail address.',
|
||||
time: String(dayBeforePreviousDay),
|
||||
senderId: 11,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: false,
|
||||
isSeen: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: 1,
|
||||
unseenMsgs: 1,
|
||||
messages: [
|
||||
{
|
||||
message: 'How can we help? We\'re here for you!',
|
||||
time: 'Mon Dec 10 2018 07:45:00 GMT+0000 (GMT)',
|
||||
senderId: 11,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Hey John, I am looking for the best admin template. Could you please help me to find it out?',
|
||||
time: 'Mon Dec 10 2018 07:45:23 GMT+0000 (GMT)',
|
||||
senderId: 1,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'It should use nice Framework.',
|
||||
time: 'Mon Dec 10 2018 07:45:55 GMT+0000 (GMT)',
|
||||
senderId: 1,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Absolutely!',
|
||||
time: 'Mon Dec 10 2018 07:46:00 GMT+0000 (GMT)',
|
||||
senderId: 11,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Our admin is the responsive admin template.!',
|
||||
time: 'Mon Dec 10 2018 07:46:05 GMT+0000 (GMT)',
|
||||
senderId: 11,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Looks clean and fresh UI. 😍',
|
||||
time: 'Mon Dec 10 2018 07:46:23 GMT+0000 (GMT)',
|
||||
senderId: 1,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'It\'s perfect for my next project.',
|
||||
time: 'Mon Dec 10 2018 07:46:33 GMT+0000 (GMT)',
|
||||
senderId: 1,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'How can I purchase it?',
|
||||
time: 'Mon Dec 10 2018 07:46:43 GMT+0000 (GMT)',
|
||||
senderId: 1,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'Thanks, From our official site 😇',
|
||||
time: 'Mon Dec 10 2018 07:46:53 GMT+0000 (GMT)',
|
||||
senderId: 11,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'I will purchase it for sure. 👍',
|
||||
time: String(previousDay),
|
||||
senderId: 1,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: true,
|
||||
isSeen: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------
|
||||
// GET: Return Chats Contacts and Contacts
|
||||
// ------------------------------------------------
|
||||
mock.onGet('/apps/chat/chats-and-contacts').reply(config => {
|
||||
const { q = '' } = config.params
|
||||
const qLowered = q.toLowerCase()
|
||||
|
||||
const chatsContacts = database.chats
|
||||
.map(chat => {
|
||||
const contact = JSON.parse(JSON.stringify(database.contacts.find(c => c.id === chat.userId)))
|
||||
|
||||
contact.chat = { id: chat.id, unseenMsgs: chat.unseenMsgs, lastMessage: chat.messages.at(-1) }
|
||||
|
||||
return contact
|
||||
})
|
||||
.reverse()
|
||||
|
||||
const profileUserData = database.profileUser
|
||||
|
||||
const response = {
|
||||
chatsContacts: chatsContacts.filter(c => c.fullName.toLowerCase().includes(qLowered)),
|
||||
contacts: database.contacts.filter(c => c.fullName.toLowerCase().includes(qLowered)),
|
||||
profileUser: profileUserData,
|
||||
}
|
||||
|
||||
return [200, response]
|
||||
})
|
||||
|
||||
// ------------------------------------------------
|
||||
// GET: Return Single Chat
|
||||
// ------------------------------------------------
|
||||
mock.onGet('/apps/chat/users/profile-user').reply(() => [200, database.profileUser])
|
||||
|
||||
// ------------------------------------------------
|
||||
// GET: Return Single Chat
|
||||
// ------------------------------------------------
|
||||
mock.onGet(/\/apps\/chat\/chats\/\d+/).reply(config => {
|
||||
// Get user id from URL
|
||||
const userId = Number(config.url?.substring(config.url.lastIndexOf('/') + 1))
|
||||
const chat = database.chats.find(c => c.userId === userId)
|
||||
if (chat)
|
||||
chat.unseenMsgs = 0
|
||||
|
||||
return [
|
||||
200,
|
||||
{
|
||||
chat,
|
||||
contact: database.contacts.find(c => c.id === userId),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// ------------------------------------------------
|
||||
// POST: Add new chat message
|
||||
// ------------------------------------------------
|
||||
mock.onPost(/\/apps\/chat\/chats\/\d+/).reply(config => {
|
||||
// Get user id from URL
|
||||
const contactId = Number(config.url?.substring(config.url.lastIndexOf('/') + 1))
|
||||
|
||||
// Get message from post data
|
||||
const { message, senderId } = JSON.parse(config.data)
|
||||
let activeChat = database.chats.find(chat => chat.userId === contactId)
|
||||
|
||||
const newMessageData = {
|
||||
message,
|
||||
time: String(new Date()),
|
||||
senderId,
|
||||
feedback: {
|
||||
isSent: true,
|
||||
isDelivered: false,
|
||||
isSeen: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// If there's new chat for user create one
|
||||
let isNewChat = false
|
||||
if (activeChat === undefined) {
|
||||
isNewChat = true
|
||||
database.chats.push({
|
||||
id: genId(database.chats),
|
||||
userId: contactId,
|
||||
unseenMsgs: 0,
|
||||
messages: [],
|
||||
})
|
||||
activeChat = database.chats.at(-1)
|
||||
}
|
||||
else {
|
||||
activeChat.messages.push(newMessageData)
|
||||
}
|
||||
const response = { msg: newMessageData }
|
||||
if (isNewChat)
|
||||
response.chat = activeChat
|
||||
|
||||
return [201, response]
|
||||
})
|
2139
resources/js/@fake-db/apps/email.js
Normal file
2139
resources/js/@fake-db/apps/email.js
Normal file
File diff suppressed because it is too large
Load Diff
1041
resources/js/@fake-db/apps/invoice.js
Normal file
1041
resources/js/@fake-db/apps/invoice.js
Normal file
File diff suppressed because it is too large
Load Diff
97
resources/js/@fake-db/apps/permissions.js
Normal file
97
resources/js/@fake-db/apps/permissions.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import mock from '@/@fake-db/mock'
|
||||
import { paginateArray } from '@/@fake-db/utils'
|
||||
|
||||
const data = {
|
||||
permissions: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Management',
|
||||
assignedTo: ['administrator'],
|
||||
createdDate: '14 Apr 2021, 8:43 PM',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
assignedTo: ['administrator'],
|
||||
name: 'Manage Billing & Roles',
|
||||
createdDate: '16 Sep 2021, 5:20 PM',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Add & Remove Users',
|
||||
createdDate: '14 Oct 2021, 10:20 AM',
|
||||
assignedTo: ['administrator', 'manager'],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Project Planning',
|
||||
createdDate: '14 Oct 2021, 10:20 AM',
|
||||
assignedTo: ['administrator', 'users', 'support'],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Manage Email Sequences',
|
||||
createdDate: '23 Aug 2021, 2:00 PM',
|
||||
assignedTo: ['administrator', 'users', 'support'],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Client Communication',
|
||||
createdDate: '15 Apr 2021, 11:30 AM',
|
||||
assignedTo: ['administrator', 'manager'],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Only View',
|
||||
createdDate: '04 Dec 2021, 8:15 PM',
|
||||
assignedTo: ['administrator', 'restricted-user'],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Financial Management',
|
||||
createdDate: '25 Feb 2021, 10:30 AM',
|
||||
assignedTo: ['administrator', 'manager'],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Manage Others\' Tasks',
|
||||
createdDate: '04 Nov 2021, 11:45 AM',
|
||||
assignedTo: ['administrator', 'support'],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------
|
||||
// GET: Return Permissions List
|
||||
// ------------------------------------------------
|
||||
mock.onGet('/apps/permissions/data').reply(config => {
|
||||
const { q = '', options = {} } = config.params ?? {}
|
||||
const { sortBy = '', page = 1, itemsPerPage = 10 } = options
|
||||
const sort = JSON.parse(JSON.stringify(sortBy))
|
||||
const queryLowered = q.toLowerCase()
|
||||
let filteredData = data.permissions.filter(permissions => permissions.name.toLowerCase().includes(queryLowered)
|
||||
|| permissions.createdDate.toLowerCase().includes(queryLowered)
|
||||
|| permissions.assignedTo.some(i => i.toLowerCase().startsWith(queryLowered)))
|
||||
|
||||
// Sorting invoices
|
||||
if (sort.length && sort[0]?.key === 'name') {
|
||||
filteredData = filteredData.sort((a, b) => {
|
||||
if (sort[0]?.order === 'asc')
|
||||
return a.name.localeCompare(b.name)
|
||||
|
||||
return b.name.localeCompare(a.name)
|
||||
})
|
||||
}
|
||||
|
||||
// total pages
|
||||
const totalPages = Math.ceil(filteredData.length / itemsPerPage)
|
||||
|
||||
return [
|
||||
200,
|
||||
{
|
||||
permissions: paginateArray(filteredData, itemsPerPage, page),
|
||||
totalPermissions: filteredData.length,
|
||||
totalPages,
|
||||
},
|
||||
]
|
||||
})
|
773
resources/js/@fake-db/apps/user-list.js
Normal file
773
resources/js/@fake-db/apps/user-list.js
Normal file
@@ -0,0 +1,773 @@
|
||||
import mock from '@/@fake-db/mock'
|
||||
import { genId, paginateArray } from '@/@fake-db/utils'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar4 from '@images/avatars/avatar-4.png'
|
||||
import avatar5 from '@images/avatars/avatar-5.png'
|
||||
import avatar6 from '@images/avatars/avatar-6.png'
|
||||
import avatar7 from '@images/avatars/avatar-7.png'
|
||||
import avatar8 from '@images/avatars/avatar-8.png'
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
fullName: 'Galen Slixby',
|
||||
company: 'Yotz PVT LTD',
|
||||
role: 'editor',
|
||||
country: 'El Salvador',
|
||||
contact: '(479) 232-9151',
|
||||
email: 'gslixby0@abc.net.au',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'inactive',
|
||||
billing: 'Auto Debit',
|
||||
avatar: '',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fullName: 'Halsey Redmore',
|
||||
company: 'Skinder PVT LTD',
|
||||
role: 'author',
|
||||
country: 'Albania',
|
||||
contact: '(472) 607-9137',
|
||||
email: 'hredmore1@imgur.com',
|
||||
currentPlan: 'team',
|
||||
status: 'pending',
|
||||
avatar: avatar1,
|
||||
billing: 'Manual - Paypal',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
fullName: 'Marjory Sicely',
|
||||
company: 'Oozz PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Russia',
|
||||
contact: '(321) 264-4599',
|
||||
email: 'msicely2@who.int',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'active',
|
||||
avatar: avatar1,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
fullName: 'Cyrill Risby',
|
||||
company: 'Oozz PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'China',
|
||||
contact: '(923) 690-6806',
|
||||
email: 'crisby3@wordpress.com',
|
||||
currentPlan: 'team',
|
||||
status: 'inactive',
|
||||
avatar: avatar3,
|
||||
billing: 'Manual - Credit Card',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
fullName: 'Maggy Hurran',
|
||||
company: 'Aimbo PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Pakistan',
|
||||
contact: '(669) 914-1078',
|
||||
email: 'mhurran4@yahoo.co.jp',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'pending',
|
||||
avatar: avatar1,
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
fullName: 'Silvain Halstead',
|
||||
company: 'Jaxbean PVT LTD',
|
||||
role: 'author',
|
||||
country: 'China',
|
||||
contact: '(958) 973-3093',
|
||||
email: 'shalstead5@shinystat.com',
|
||||
currentPlan: 'company',
|
||||
status: 'active',
|
||||
avatar: '',
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
fullName: 'Breena Gallemore',
|
||||
company: 'Jazzy PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Canada',
|
||||
contact: '(825) 977-8152',
|
||||
email: 'bgallemore6@boston.com',
|
||||
currentPlan: 'company',
|
||||
status: 'pending',
|
||||
avatar: '',
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
fullName: 'Kathryne Liger',
|
||||
company: 'Pixoboo PVT LTD',
|
||||
role: 'author',
|
||||
country: 'France',
|
||||
contact: '(187) 440-0934',
|
||||
email: 'kliger7@vinaora.com',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'pending',
|
||||
avatar: avatar4,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
fullName: 'Franz Scotfurth',
|
||||
company: 'Tekfly PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'China',
|
||||
contact: '(978) 146-5443',
|
||||
email: 'fscotfurth8@dailymotion.com',
|
||||
currentPlan: 'team',
|
||||
status: 'pending',
|
||||
avatar: avatar2,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
fullName: 'Jillene Bellany',
|
||||
company: 'Gigashots PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Jamaica',
|
||||
contact: '(589) 284-6732',
|
||||
email: 'jbellany9@kickstarter.com',
|
||||
currentPlan: 'company',
|
||||
status: 'inactive',
|
||||
avatar: avatar5,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
fullName: 'Jonah Wharlton',
|
||||
company: 'Eare PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'United States',
|
||||
contact: '(176) 532-6824',
|
||||
email: 'jwharltona@oakley.com',
|
||||
currentPlan: 'team',
|
||||
status: 'inactive',
|
||||
avatar: avatar4,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
fullName: 'Seth Hallam',
|
||||
company: 'Yakitri PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Peru',
|
||||
contact: '(234) 464-0600',
|
||||
email: 'shallamb@hugedomains.com',
|
||||
currentPlan: 'team',
|
||||
status: 'pending',
|
||||
avatar: avatar5,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
fullName: 'Yoko Pottie',
|
||||
company: 'Leenti PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Philippines',
|
||||
contact: '(907) 284-5083',
|
||||
email: 'ypottiec@privacy.gov.au',
|
||||
currentPlan: 'basic',
|
||||
status: 'inactive',
|
||||
avatar: avatar7,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
fullName: 'Maximilianus Krause',
|
||||
company: 'Digitube PVT LTD',
|
||||
role: 'author',
|
||||
country: 'Democratic Republic of the Congo',
|
||||
contact: '(167) 135-7392',
|
||||
email: 'mkraused@stanford.edu',
|
||||
currentPlan: 'team',
|
||||
status: 'active',
|
||||
avatar: avatar6,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
fullName: 'Zsazsa McCleverty',
|
||||
company: 'Kaymbo PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'France',
|
||||
contact: '(317) 409-6565',
|
||||
email: 'zmcclevertye@soundcloud.com',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'active',
|
||||
avatar: avatar2,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
fullName: 'Bentlee Emblin',
|
||||
company: 'Yambee PVT LTD',
|
||||
role: 'author',
|
||||
country: 'Spain',
|
||||
contact: '(590) 606-1056',
|
||||
email: 'bemblinf@wired.com',
|
||||
currentPlan: 'company',
|
||||
status: 'active',
|
||||
avatar: avatar6,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
fullName: 'Brockie Myles',
|
||||
company: 'Wikivu PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Poland',
|
||||
contact: '(553) 225-9905',
|
||||
email: 'bmylesg@amazon.com',
|
||||
currentPlan: 'basic',
|
||||
status: 'active',
|
||||
avatar: '',
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
fullName: 'Bertha Biner',
|
||||
company: 'Twinte PVT LTD',
|
||||
role: 'editor',
|
||||
country: 'Yemen',
|
||||
contact: '(901) 916-9287',
|
||||
email: 'bbinerh@mozilla.com',
|
||||
currentPlan: 'team',
|
||||
status: 'active',
|
||||
avatar: avatar7,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
fullName: 'Travus Bruntjen',
|
||||
company: 'Cogidoo PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'France',
|
||||
contact: '(524) 586-6057',
|
||||
email: 'tbruntjeni@sitemeter.com',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'active',
|
||||
avatar: '',
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
fullName: 'Wesley Burland',
|
||||
company: 'Bubblemix PVT LTD',
|
||||
role: 'editor',
|
||||
country: 'Honduras',
|
||||
contact: '(569) 683-1292',
|
||||
email: 'wburlandj@uiuc.edu',
|
||||
currentPlan: 'team',
|
||||
status: 'inactive',
|
||||
avatar: avatar6,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
fullName: 'Selina Kyle',
|
||||
company: 'Wayne Enterprises',
|
||||
role: 'admin',
|
||||
country: 'USA',
|
||||
contact: '(829) 537-0057',
|
||||
email: 'irena.dubrovna@wayne.com',
|
||||
currentPlan: 'team',
|
||||
status: 'active',
|
||||
avatar: avatar1,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
fullName: 'Jameson Lyster',
|
||||
company: 'Quaxo PVT LTD',
|
||||
role: 'editor',
|
||||
country: 'Ukraine',
|
||||
contact: '(593) 624-0222',
|
||||
email: 'jlysterl@guardian.co.uk',
|
||||
currentPlan: 'company',
|
||||
status: 'inactive',
|
||||
avatar: avatar8,
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
fullName: 'Kare Skitterel',
|
||||
company: 'Ainyx PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Poland',
|
||||
contact: '(254) 845-4107',
|
||||
email: 'kskitterelm@ainyx.com',
|
||||
currentPlan: 'basic',
|
||||
status: 'pending',
|
||||
avatar: avatar3,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
fullName: 'Cleavland Hatherleigh',
|
||||
company: 'Flipopia PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'Brazil',
|
||||
contact: '(700) 783-7498',
|
||||
email: 'chatherleighn@washington.edu',
|
||||
currentPlan: 'team',
|
||||
status: 'pending',
|
||||
avatar: avatar2,
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
fullName: 'Adeline Micco',
|
||||
company: 'Topicware PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'France',
|
||||
contact: '(227) 598-1841',
|
||||
email: 'amiccoo@whitehouse.gov',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'pending',
|
||||
avatar: '',
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 26,
|
||||
fullName: 'Hugh Hasson',
|
||||
company: 'Skinix PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'China',
|
||||
contact: '(582) 516-1324',
|
||||
email: 'hhassonp@bizjournals.com',
|
||||
currentPlan: 'basic',
|
||||
status: 'inactive',
|
||||
avatar: avatar4,
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 27,
|
||||
fullName: 'Germain Jacombs',
|
||||
company: 'Youopia PVT LTD',
|
||||
role: 'editor',
|
||||
country: 'Zambia',
|
||||
contact: '(137) 467-5393',
|
||||
email: 'gjacombsq@jigsy.com',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'active',
|
||||
avatar: avatar5,
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 28,
|
||||
fullName: 'Bree Kilday',
|
||||
company: 'Jetpulse PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Portugal',
|
||||
contact: '(412) 476-0854',
|
||||
email: 'bkildayr@mashable.com',
|
||||
currentPlan: 'team',
|
||||
status: 'active',
|
||||
avatar: '',
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 29,
|
||||
fullName: 'Candice Pinyon',
|
||||
company: 'Kare PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Sweden',
|
||||
contact: '(170) 683-1520',
|
||||
email: 'cpinyons@behance.net',
|
||||
currentPlan: 'team',
|
||||
status: 'active',
|
||||
avatar: avatar7,
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 30,
|
||||
fullName: 'Isabel Mallindine',
|
||||
company: 'Voomm PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Slovenia',
|
||||
contact: '(332) 803-1983',
|
||||
email: 'imallindinet@shinystat.com',
|
||||
currentPlan: 'team',
|
||||
status: 'pending',
|
||||
avatar: '',
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 31,
|
||||
fullName: 'Gwendolyn Meineken',
|
||||
company: 'Oyondu PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'Moldova',
|
||||
contact: '(551) 379-7460',
|
||||
email: 'gmeinekenu@hc360.com',
|
||||
currentPlan: 'basic',
|
||||
status: 'pending',
|
||||
avatar: avatar1,
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
fullName: 'Rafaellle Snowball',
|
||||
company: 'Fivespan PVT LTD',
|
||||
role: 'editor',
|
||||
country: 'Philippines',
|
||||
contact: '(974) 829-0911',
|
||||
email: 'rsnowballv@indiegogo.com',
|
||||
currentPlan: 'basic',
|
||||
status: 'pending',
|
||||
avatar: avatar5,
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 33,
|
||||
fullName: 'Rochette Emer',
|
||||
company: 'Thoughtworks PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'North Korea',
|
||||
contact: '(841) 889-3339',
|
||||
email: 'remerw@blogtalkradio.com',
|
||||
currentPlan: 'basic',
|
||||
status: 'active',
|
||||
avatar: avatar8,
|
||||
billing: 'Manual - Paypal',
|
||||
},
|
||||
{
|
||||
id: 34,
|
||||
fullName: 'Ophelie Fibbens',
|
||||
company: 'Jaxbean PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Indonesia',
|
||||
contact: '(764) 885-7351',
|
||||
email: 'ofibbensx@booking.com',
|
||||
currentPlan: 'company',
|
||||
status: 'active',
|
||||
avatar: avatar4,
|
||||
billing: 'Manual - Paypal',
|
||||
},
|
||||
{
|
||||
id: 35,
|
||||
fullName: 'Stephen MacGilfoyle',
|
||||
company: 'Browseblab PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Japan',
|
||||
contact: '(350) 589-8520',
|
||||
email: 'smacgilfoyley@bigcartel.com',
|
||||
currentPlan: 'company',
|
||||
status: 'pending',
|
||||
avatar: '',
|
||||
billing: 'Manual - Paypal',
|
||||
},
|
||||
{
|
||||
id: 36,
|
||||
fullName: 'Bradan Rosebotham',
|
||||
company: 'Agivu PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Belarus',
|
||||
contact: '(882) 933-2180',
|
||||
email: 'brosebothamz@tripadvisor.com',
|
||||
currentPlan: 'team',
|
||||
status: 'inactive',
|
||||
avatar: '',
|
||||
billing: 'Manual - Paypal',
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
fullName: 'Skip Hebblethwaite',
|
||||
company: 'Katz PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'Canada',
|
||||
contact: '(610) 343-1024',
|
||||
email: 'shebblethwaite10@arizona.edu',
|
||||
currentPlan: 'company',
|
||||
status: 'inactive',
|
||||
avatar: avatar1,
|
||||
billing: 'Manual - Paypal',
|
||||
},
|
||||
{
|
||||
id: 38,
|
||||
fullName: 'Moritz Piccard',
|
||||
company: 'Twitternation PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Croatia',
|
||||
contact: '(365) 277-2986',
|
||||
email: 'mpiccard11@vimeo.com',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'inactive',
|
||||
avatar: avatar1,
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 39,
|
||||
fullName: 'Tyne Widmore',
|
||||
company: 'Yombu PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Finland',
|
||||
contact: '(531) 731-0928',
|
||||
email: 'twidmore12@bravesites.com',
|
||||
currentPlan: 'team',
|
||||
status: 'pending',
|
||||
avatar: '',
|
||||
billing: 'Manual - Credit Card',
|
||||
},
|
||||
{
|
||||
id: 40,
|
||||
fullName: 'Florenza Desporte',
|
||||
company: 'Kamba PVT LTD',
|
||||
role: 'author',
|
||||
country: 'Ukraine',
|
||||
contact: '(312) 104-2638',
|
||||
email: 'fdesporte13@omniture.com',
|
||||
currentPlan: 'company',
|
||||
status: 'active',
|
||||
avatar: avatar6,
|
||||
billing: 'Manual - Credit Card',
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
fullName: 'Edwina Baldetti',
|
||||
company: 'Dazzlesphere PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Haiti',
|
||||
contact: '(315) 329-3578',
|
||||
email: 'ebaldetti14@theguardian.com',
|
||||
currentPlan: 'team',
|
||||
status: 'pending',
|
||||
avatar: '',
|
||||
billing: 'Manual - Cash',
|
||||
},
|
||||
{
|
||||
id: 42,
|
||||
fullName: 'Benedetto Rossiter',
|
||||
company: 'Mybuzz PVT LTD',
|
||||
role: 'editor',
|
||||
country: 'Indonesia',
|
||||
contact: '(323) 175-6741',
|
||||
email: 'brossiter15@craigslist.org',
|
||||
currentPlan: 'team',
|
||||
status: 'inactive',
|
||||
avatar: '',
|
||||
billing: 'Manual - Credit Card',
|
||||
},
|
||||
{
|
||||
id: 43,
|
||||
fullName: 'Micaela McNirlan',
|
||||
company: 'Tambee PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'Indonesia',
|
||||
contact: '(242) 952-0916',
|
||||
email: 'mmcnirlan16@hc360.com',
|
||||
currentPlan: 'basic',
|
||||
status: 'inactive',
|
||||
avatar: '',
|
||||
billing: 'Manual - Credit Card',
|
||||
},
|
||||
{
|
||||
id: 44,
|
||||
fullName: 'Vladamir Koschek',
|
||||
company: 'Centimia PVT LTD',
|
||||
role: 'author',
|
||||
country: 'Guatemala',
|
||||
contact: '(531) 758-8335',
|
||||
email: 'vkoschek17@abc.net.au',
|
||||
currentPlan: 'team',
|
||||
status: 'active',
|
||||
avatar: '',
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 45,
|
||||
fullName: 'Corrie Perot',
|
||||
company: 'Flipopia PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'China',
|
||||
contact: '(659) 385-6808',
|
||||
email: 'cperot18@goo.ne.jp',
|
||||
currentPlan: 'team',
|
||||
status: 'pending',
|
||||
avatar: avatar3,
|
||||
billing: 'Manual - Paypal',
|
||||
},
|
||||
{
|
||||
id: 46,
|
||||
fullName: 'Saunder Offner',
|
||||
company: 'Skalith PVT LTD',
|
||||
role: 'maintainer',
|
||||
country: 'Poland',
|
||||
contact: '(200) 586-2264',
|
||||
email: 'soffner19@mac.com',
|
||||
currentPlan: 'enterprise',
|
||||
status: 'pending',
|
||||
avatar: '',
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 47,
|
||||
fullName: 'Karena Courtliff',
|
||||
company: 'Feedfire PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'China',
|
||||
contact: '(478) 199-0020',
|
||||
email: 'kcourtliff1a@bbc.co.uk',
|
||||
currentPlan: 'basic',
|
||||
status: 'active',
|
||||
avatar: avatar1,
|
||||
billing: 'Manual - Credit Card',
|
||||
},
|
||||
{
|
||||
id: 48,
|
||||
fullName: 'Onfre Wind',
|
||||
company: 'Thoughtmix PVT LTD',
|
||||
role: 'admin',
|
||||
country: 'Ukraine',
|
||||
contact: '(344) 262-7270',
|
||||
email: 'owind1b@yandex.ru',
|
||||
currentPlan: 'basic',
|
||||
status: 'pending',
|
||||
avatar: '',
|
||||
billing: 'Manual - Credit Card',
|
||||
},
|
||||
{
|
||||
id: 49,
|
||||
fullName: 'Paulie Durber',
|
||||
company: 'Babbleblab PVT LTD',
|
||||
role: 'subscriber',
|
||||
country: 'Sweden',
|
||||
contact: '(694) 676-1275',
|
||||
email: 'pdurber1c@gov.uk',
|
||||
currentPlan: 'team',
|
||||
status: 'inactive',
|
||||
avatar: '',
|
||||
billing: 'Auto Debit',
|
||||
},
|
||||
{
|
||||
id: 50,
|
||||
fullName: 'Beverlie Krabbe',
|
||||
company: 'Kaymbo PVT LTD',
|
||||
role: 'editor',
|
||||
country: 'China',
|
||||
contact: '(397) 294-5153',
|
||||
email: 'bkrabbe1d@home.pl',
|
||||
currentPlan: 'company',
|
||||
status: 'active',
|
||||
avatar: avatar2,
|
||||
billing: 'Manual - Credit Card',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
// 👉 return users
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
mock.onGet('/apps/users/list').reply(config => {
|
||||
const { q = '', role = null, plan = null, status = null, options = {} } = config.params ?? {}
|
||||
const { sortBy = '', itemsPerPage = 10, page = 1 } = options
|
||||
const queryLower = q.toLowerCase()
|
||||
|
||||
// filter users
|
||||
let filteredUsers = users.filter(user => ((user.fullName.toLowerCase().includes(queryLower) || user.email.toLowerCase().includes(queryLower)) && user.role === (role || user.role) && user.currentPlan === (plan || user.currentPlan) && user.status === (status || user.status))).reverse()
|
||||
|
||||
// sort users
|
||||
const sort = JSON.parse(JSON.stringify(sortBy))
|
||||
if (sort.length) {
|
||||
if (sort[0]?.key === 'user') {
|
||||
filteredUsers = filteredUsers.sort((a, b) => {
|
||||
if (sort[0]?.order === 'asc')
|
||||
return a.fullName.localeCompare(b.fullName)
|
||||
else
|
||||
return b.fullName.localeCompare(a.fullName)
|
||||
})
|
||||
}
|
||||
if (sort[0]?.key === 'billing') {
|
||||
filteredUsers = filteredUsers.sort((a, b) => {
|
||||
if (sort[0]?.order === 'asc')
|
||||
return a.billing.localeCompare(b.billing)
|
||||
else
|
||||
return b.billing.localeCompare(a.billing)
|
||||
})
|
||||
}
|
||||
if (sort[0]?.key === 'role') {
|
||||
filteredUsers = filteredUsers.sort((a, b) => {
|
||||
if (sort[0]?.order === 'asc')
|
||||
return a.role.localeCompare(b.role)
|
||||
else
|
||||
return b.role.localeCompare(a.role)
|
||||
})
|
||||
}
|
||||
if (sort[0]?.key === 'plan') {
|
||||
filteredUsers = filteredUsers.sort((a, b) => {
|
||||
if (sort[0]?.order === 'asc')
|
||||
return a.currentPlan.localeCompare(b.currentPlan)
|
||||
else
|
||||
return b.currentPlan.localeCompare(a.currentPlan)
|
||||
})
|
||||
}
|
||||
if (sort[0]?.key === 'status') {
|
||||
filteredUsers = filteredUsers.sort((a, b) => {
|
||||
if (sort[0]?.order === 'asc')
|
||||
return a.status.localeCompare(b.status)
|
||||
else
|
||||
return b.status.localeCompare(a.status)
|
||||
})
|
||||
}
|
||||
}
|
||||
const totalUsers = filteredUsers.length
|
||||
|
||||
// total pages
|
||||
const totalPages = Math.ceil(totalUsers / itemsPerPage)
|
||||
|
||||
return [200, { users: paginateArray(filteredUsers, itemsPerPage, page), totalPages, totalUsers, page: page > Math.ceil(totalUsers / itemsPerPage) ? 1 : page }]
|
||||
})
|
||||
|
||||
// 👉 Add user
|
||||
mock.onPost('/apps/users/user').reply(config => {
|
||||
const { user } = JSON.parse(config.data)
|
||||
|
||||
user.id = genId(users)
|
||||
users.push(user)
|
||||
|
||||
return [201, { user }]
|
||||
})
|
||||
|
||||
// 👉 Get Single user
|
||||
mock.onGet(/\/apps\/users\/\d+/).reply(config => {
|
||||
// Get event id from URL
|
||||
const userId = config.url?.substring(config.url.lastIndexOf('/') + 1)
|
||||
|
||||
// Convert Id to number
|
||||
const Id = Number(userId)
|
||||
const userIndex = users.findIndex(e => e.id === Id)
|
||||
const user = users[userIndex]
|
||||
|
||||
Object.assign(user, {
|
||||
taskDone: 1230,
|
||||
projectDone: 568,
|
||||
taxId: 'Tax-8894',
|
||||
language: 'English',
|
||||
})
|
||||
if (user)
|
||||
return [200, user]
|
||||
|
||||
return [404]
|
||||
})
|
||||
mock.onDelete(/\/apps\/users\/\d+/).reply(config => {
|
||||
// Get user id from URL
|
||||
const userId = config.url?.substring(config.url.lastIndexOf('/') + 1)
|
||||
|
||||
// Convert Id to number
|
||||
const Id = Number(userId)
|
||||
const userIndex = users.findIndex(e => e.id === Id)
|
||||
if (userIndex >= 0) {
|
||||
users.splice(userIndex, 1)
|
||||
|
||||
return [200]
|
||||
}
|
||||
|
||||
return [400]
|
||||
})
|
17
resources/js/@fake-db/db.js
Normal file
17
resources/js/@fake-db/db.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import './app-bar-search'
|
||||
import './apps/user-list'
|
||||
import './jwt'
|
||||
import mock from './mock'
|
||||
import './pages/datatable'
|
||||
|
||||
|
||||
// Apps
|
||||
import './apps/chat'
|
||||
|
||||
import './apps/invoice'
|
||||
import './apps/permissions'
|
||||
|
||||
// Dashboard
|
||||
|
||||
// forwards the matched request over network
|
||||
mock.onAny().passThrough()
|
162
resources/js/@fake-db/jwt/index.js
Normal file
162
resources/js/@fake-db/jwt/index.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import mock from '@/@fake-db/mock'
|
||||
import { genId } from '@/@fake-db/utils'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
|
||||
|
||||
// TODO: Use jsonwebtoken pkg
|
||||
// ℹ️ Created from https://jwt.io/ using HS256 algorithm
|
||||
// ℹ️ We didn't created it programmatically because jsonwebtoken package have issues with esm support. View Issues: https://github.com/auth0/node-jsonwebtoken/issues/655
|
||||
const userTokens = [
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.fhc3wykrAnRpcKApKhXiahxaOe8PSHatad31NuIZ0Zg',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.cat2xMrZLn0FwicdGtZNzL7ifDTAKWB0k1RurSWjdnw',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.PGOfMaZA_T9W05vMj5FYXG5d47soSPJD1WuxeUfw4L4',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NH0.d_9aq2tpeA9-qpqO0X4AmW6gU2UpWkXwc04UJYFWiZE',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NX0.ocO77FbjOSU1-JQ_BilEZq2G_M8bCiB10KYqtfkv1ss',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Nn0.YgQILRqZy8oefhTZgJJfiEzLmhxQT_Bd2510OvrrwB8',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6N30.KH9RmOWIYv_HONxajg7xBIJXHEUvSdcBygFtS2if8Jk',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OH0.shrp-oMHkVAkiMkv_aIvSx3k6Jk-X7TrH5UeufChz_g',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OX0.9JD1MR3ZkwHzhl4mOHH6lGG8hOVNZqDNH6UkFzjCqSE',
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTB9.txWLuN4QT5PqTtgHmlOiNerIu5Do51PpYOiZutkyXYg',
|
||||
]
|
||||
|
||||
|
||||
// ❗ These two secrets shall be in .env file and not in any other file
|
||||
// const jwtSecret = 'dd5f3089-40c3-403d-af14-d0c228b05cb4'
|
||||
const database = [
|
||||
{
|
||||
id: 1,
|
||||
fullName: 'John Doe',
|
||||
username: 'johndoe',
|
||||
password: 'admin',
|
||||
avatar: avatar1,
|
||||
email: 'admin@demo.com',
|
||||
role: 'admin',
|
||||
abilities: [
|
||||
{
|
||||
action: 'manage',
|
||||
subject: 'all',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
fullName: 'Jane Doe',
|
||||
username: 'janedoe',
|
||||
password: 'client',
|
||||
avatar: avatar2,
|
||||
email: 'client@demo.com',
|
||||
role: 'client',
|
||||
abilities: [
|
||||
{
|
||||
action: 'read',
|
||||
subject: 'Auth',
|
||||
},
|
||||
{
|
||||
action: 'read',
|
||||
subject: 'AclDemo',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
mock.onPost('/auth/login').reply(request => {
|
||||
const { email, password } = JSON.parse(request.data)
|
||||
let errors = {
|
||||
email: ['Something went wrong'],
|
||||
}
|
||||
const user = database.find(u => u.email === email && u.password === password)
|
||||
if (user) {
|
||||
try {
|
||||
const accessToken = userTokens[user.id]
|
||||
|
||||
// We are duplicating user here
|
||||
const userData = { ...user }
|
||||
|
||||
const userOutData = Object.fromEntries(Object.entries(userData)
|
||||
.filter(([key, _]) => !(key === 'password' || key === 'abilities')))
|
||||
|
||||
const response = {
|
||||
userAbilities: userData.abilities,
|
||||
accessToken,
|
||||
userData: userOutData,
|
||||
}
|
||||
|
||||
|
||||
// const accessToken = jwt.sign({ id: user.id }, jwtSecret)
|
||||
return [200, response]
|
||||
}
|
||||
catch (e) {
|
||||
errors = { email: [e] }
|
||||
}
|
||||
}
|
||||
else {
|
||||
errors = {
|
||||
email: ['Email or Password is Invalid'],
|
||||
}
|
||||
}
|
||||
|
||||
return [400, { errors }]
|
||||
})
|
||||
mock.onPost('/auth/register').reply(request => {
|
||||
const { username, email, password } = JSON.parse(request.data)
|
||||
|
||||
// If not any of data is missing return 400
|
||||
if (!(username && email && password))
|
||||
return [400]
|
||||
const isEmailAlreadyInUse = database.find(user => user.email === email)
|
||||
const isUsernameAlreadyInUse = database.find(user => user.username === username)
|
||||
|
||||
const errors = {
|
||||
password: !password ? ['Please enter password'] : null,
|
||||
email: (() => {
|
||||
if (!email)
|
||||
return ['Please enter your email.']
|
||||
if (isEmailAlreadyInUse)
|
||||
return ['This email is already in use.']
|
||||
|
||||
return null
|
||||
})(),
|
||||
username: (() => {
|
||||
if (!username)
|
||||
return ['Please enter your username.']
|
||||
if (isUsernameAlreadyInUse)
|
||||
return ['This username is already in use.']
|
||||
|
||||
return null
|
||||
})(),
|
||||
}
|
||||
|
||||
if (!errors.username && !errors.email) {
|
||||
// Calculate user id
|
||||
const userData = {
|
||||
id: genId(database),
|
||||
email,
|
||||
password,
|
||||
username,
|
||||
fullName: '',
|
||||
role: 'admin',
|
||||
abilities: [
|
||||
{
|
||||
action: 'manage',
|
||||
subject: 'all',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
database.push(userData)
|
||||
|
||||
const accessToken = userTokens[userData.id]
|
||||
const { password: _, abilities, ...user } = userData
|
||||
|
||||
const response = {
|
||||
userData: user,
|
||||
accessToken,
|
||||
userAbilities: abilities,
|
||||
}
|
||||
|
||||
return [200, response]
|
||||
}
|
||||
|
||||
return [400, { error: errors }]
|
||||
})
|
6
resources/js/@fake-db/mock.js
Normal file
6
resources/js/@fake-db/mock.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import MockAdapter from 'axios-mock-adapter'
|
||||
import axios from '@axios'
|
||||
|
||||
// This sets the mock adapter on the axios instance
|
||||
const mock = new MockAdapter(axios)
|
||||
export default mock
|
1310
resources/js/@fake-db/pages/datatable.js
Normal file
1310
resources/js/@fake-db/pages/datatable.js
Normal file
File diff suppressed because it is too large
Load Diff
576
resources/js/@fake-db/pages/help-center.js
Normal file
576
resources/js/@fake-db/pages/help-center.js
Normal file
@@ -0,0 +1,576 @@
|
||||
import mock from '@/@fake-db/mock'
|
||||
import { themeConfig } from '@themeConfig'
|
||||
|
||||
// Images
|
||||
import discord from '@images/svg/discord.svg'
|
||||
import gift from '@images/svg/gift.svg'
|
||||
import keyboard from '@images/svg/keyboard.svg'
|
||||
import laptop from '@images/svg/laptop.svg'
|
||||
import lightbulb from '@images/svg/lightbulb.svg'
|
||||
import rocket from '@images/svg/rocket.svg'
|
||||
|
||||
const data = {
|
||||
popularArticles: [
|
||||
{
|
||||
slug: 'getting-started',
|
||||
title: 'Getting Started',
|
||||
img: rocket,
|
||||
subtitle: 'Whether you\'re new or you\'re a power user, this article will',
|
||||
},
|
||||
{
|
||||
slug: 'first-steps',
|
||||
title: 'First Steps',
|
||||
img: gift,
|
||||
subtitle: 'Are you a new customer wondering how to get started?',
|
||||
},
|
||||
{
|
||||
slug: 'external-content',
|
||||
title: 'Add External Content',
|
||||
img: keyboard,
|
||||
subtitle: 'This article will show you how to expand the functionality of App',
|
||||
},
|
||||
],
|
||||
categories: [
|
||||
{
|
||||
icon: 'tabler-rocket',
|
||||
avatarColor: 'success',
|
||||
slug: 'getting-started',
|
||||
title: 'Getting Started',
|
||||
subCategories: [
|
||||
{
|
||||
slug: 'account',
|
||||
icon: 'tabler-box',
|
||||
title: 'Account',
|
||||
articles: [
|
||||
{
|
||||
slug: 'changing-your-username',
|
||||
title: 'Changing your username?',
|
||||
content: '<p>You can change your username to another username that is not currently in use. If the username you want is not available, consider other names or unique variations. Using a number, hyphen, or an alternative spelling might help you find a similar username that\'s still available.</p> <p>After changing your username, your old username becomes available for anyone else to claim. Most references to your repositories under the old username automatically change to the new username. However, some links to your profile won\'t automatically redirect.</p><p>You can change your username to another username that is not currently in use. If the username you want is not available, consider other names or unique variations. Using a number, hyphen, or an alternative spelling might help you find a similar username that\'s still available.</p> <p>After changing your username, your old username becomes available for anyone else to claim. Most references to your repositories under the old username automatically change to the new username. However, some links to your profile won\'t automatically redirect.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'changing-your-primary-email-address',
|
||||
title: 'Changing your primary email address?',
|
||||
content: '<p>You can change the email address associated with your personal account at any time from account settings.</p> <p><strong>Note:</strong> You cannot change your primary email address to an email that is already set to be your backup email address.</p><p>You can change the email address associated with your personal account at any time from account settings.</p> <p><strong>Note:</strong> You cannot change your primary email address to an email that is already set to be your backup email address.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'changing-your-profile-picture',
|
||||
title: 'Changing your profile picture?',
|
||||
content: '<p>You can change your profile from account settings any time.</p> <p><strong>Note:</strong> Your profile picture should be a PNG, JPG, or GIF file, and it must be less than 1 MB in size and smaller than 3000 by 3000 pixels. For the best quality rendering, we recommend keeping the image at about 500 by 500 pixels.<p>You can change your profile from account settings any time.</p> <p><strong>Note:</strong> Your profile picture should be a PNG, JPG, or GIF file, and it must be less than 1 MB in size and smaller than 3000 by 3000 pixels. For the best quality rendering, we recommend keeping the image at about 500 by 500 pixels.',
|
||||
},
|
||||
{
|
||||
slug: 'setting-your-profile-to-private',
|
||||
title: 'Setting your profile to private?',
|
||||
content: '<p>A private profile displays only limited information, and hides some activity.</p> <p>To hide parts of your profile page, you can make your profile private. This also hides your activity in various social features on the website. A private profile hides information from all users, and there is currently no option to allow specified users to see your activity.</p> <p>You can change your profile to private in account settings.</p> <p>A private profile displays only limited information, and hides some activity.</p> <p>To hide parts of your profile page, you can make your profile private. This also hides your activity in various social features on the website. A private profile hides information from all users, and there is currently no option to allow specified users to see your activity.</p> <p>You can change your profile to private in account settings.</p> ',
|
||||
},
|
||||
{
|
||||
slug: 'deleting-your-personal-account',
|
||||
title: 'Deleting your personal account?',
|
||||
content: '<p>Deleting your personal account removes data associated with your account.</p> <p>When you delete your account we stop billing you. The email address associated with the account becomes available for use with a different account on website. After 90 days, the account name also becomes available to anyone else to use on a new account.</p><p>Deleting your personal account removes data associated with your account.</p> <p>When you delete your account we stop billing you. The email address associated with the account becomes available for use with a different account on website. After 90 days, the account name also becomes available to anyone else to use on a new account.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'authentication',
|
||||
title: 'Authentication',
|
||||
icon: 'tabler-lock',
|
||||
articles: [
|
||||
{
|
||||
slug: 'how-to-create-a-strong-password',
|
||||
title: 'How to create a strong password?',
|
||||
content: '<p>A strong password is a unique word or phrase a hacker cannot easily guess or crack.</p> <p>To keep your account secure, we recommend you to have a password with at least Eight characters, a number, a lowercase letter & an uppercase character.</p><p>A strong password is a unique word or phrase a hacker cannot easily guess or crack.</p> <p>To keep your account secure, we recommend you to have a password with at least Eight characters, a number, a lowercase letter & an uppercase character.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'what-is-2FA',
|
||||
title: 'What is Two-Factor Authentication?',
|
||||
content: '<p>Two-factor authentication (2FA) is an extra layer of security used when logging into websites or apps. With 2FA, you have to log in with your username and password and provide another form of authentication that only you know or have access to.</p> <p>For our app, the second form of authentication is a code that\'s generated by an application on your mobile device or sent as a text message (SMS). After you enable 2FA, App generates an authentication code any time someone attempts to sign into your account. The only way someone can sign into your account is if they know both your password and have access to the authentication code on your phone.</p><p>Two-factor authentication (2FA) is an extra layer of security used when logging into websites or apps. With 2FA, you have to log in with your username and password and provide another form of authentication that only you know or have access to.</p> <p>For our app, the second form of authentication is a code that\'s generated by an application on your mobile device or sent as a text message (SMS). After you enable 2FA, App generates an authentication code any time someone attempts to sign into your account. The only way someone can sign into your account is if they know both your password and have access to the authentication code on your phone.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-recover-account-if-you-lose-your-2fa-credentials',
|
||||
title: 'How to recover account if you lose your 2fa credentials?',
|
||||
content: '<p>If you lose access to your two-factor authentication credentials, you can use your recovery codes, or another recovery option, to regain access to your account.</p> <p><strong>Warning:</strong> For security reasons, Our Support may not be able to restore access to accounts with two-factor authentication enabled if you lose your two-factor authentication credentials or lose access to your account recovery methods.</p><p>If you lose access to your two-factor authentication credentials, you can use your recovery codes, or another recovery option, to regain access to your account.</p> <p><strong>Warning:</strong> For security reasons, Our Support may not be able to restore access to accounts with two-factor authentication enabled if you lose your two-factor authentication credentials or lose access to your account recovery methods.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-review-security-logs',
|
||||
title: 'How to review security logs?',
|
||||
content: '<p>You can review the security log for your personal account to better understand actions you\'ve performed and actions others have performed that involve you.</p> <p>You can refer your security log from the settings.</p><p>You can review the security log for your personal account to better understand actions you\'ve performed and actions others have performed that involve you.</p> <p>You can refer your security log from the settings.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'billing',
|
||||
title: 'Billing',
|
||||
icon: 'tabler-currency-dollar',
|
||||
articles: [
|
||||
{
|
||||
slug: 'how-to-update-payment-method',
|
||||
title: 'How to update payment method?',
|
||||
content: '<p>You can add a payment method to your account or update your account\'s existing payment method at any time.</p> <p>You can pay with a credit card or with a PayPal account. When you update your payment method for your account\'s subscription, your new payment method is automatically added to your other subscriptions for paid products.</p><p>You can add a payment method to your account or update your account\'s existing payment method at any time.</p> <p>You can pay with a credit card or with a PayPal account. When you update your payment method for your account\'s subscription, your new payment method is automatically added to your other subscriptions for paid products.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-check-billing-date',
|
||||
title: 'How to check billing date?',
|
||||
content: '<p>You can view your account\'s subscription, your other paid features and products, and your next billing date in your account\'s billing settings.</p><p>You can view your account\'s subscription, your other paid features and products, and your next billing date in your account\'s billing settings.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-change-billing-cycle',
|
||||
title: 'How to change billing cycle?',
|
||||
content: '<p>You can change your billing cycle from the account settings billing section.</p> <p>When you change your billing cycle\'s duration, your GitHub subscription, along with any other paid features and products, will be moved to your new billing cycle on your next billing date.</p><p>You can change your billing cycle from the account settings billing section.</p> <p>When you change your billing cycle\'s duration, your GitHub subscription, along with any other paid features and products, will be moved to your new billing cycle on your next billing date.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'where-can-i-view-and-download-payment-receipt',
|
||||
title: 'Where can i view and download payment receipt?',
|
||||
content: '<p>You can view your payment from the account settings billing section.</p> <p>You\'ll also a have a option to download or share your payment receipt from the billing section.</p><p>You can view your payment from the account settings billing section.</p> <p>You\'ll also a have a option to download or share your payment receipt from the billing section.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-set-billing-email',
|
||||
title: 'How to set billing email?',
|
||||
content: '<p>Your personal account\'s primary email is where we send receipts and other billing-related communication.</p> <p>Your primary email address is the first email listed in your account email settings. We also use your primary email address as our billing email address.</p> <p>If you\'d like to change your billing email you can do it from account settings.</p><p>Your personal account\'s primary email is where we send receipts and other billing-related communication.</p> <p>Your primary email address is the first email listed in your account email settings. We also use your primary email address as our billing email address.</p> <p>If you\'d like to change your billing email you can do it from account settings.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'orders',
|
||||
title: 'Orders',
|
||||
avatarColor: 'info',
|
||||
icon: 'tabler-box',
|
||||
subCategories: [
|
||||
{
|
||||
slug: 'processing-orders',
|
||||
title: 'Processing orders',
|
||||
icon: 'tabler-box',
|
||||
articles: [
|
||||
{
|
||||
slug: 'what-happens-when-you-receive-an-online-order',
|
||||
title: 'What happens when you receive an online order?',
|
||||
content: '<p>When you receive an online order, you\'ll receive a new order notification by email.</p> <p>You\'ll be able to see that order on the orders page.</p><p>When you receive an online order, you\'ll receive a new order notification by email.</p> <p>You\'ll be able to see that order on the orders page.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'what-happens-when-you-process-an-order',
|
||||
title: 'What happens when you process an order?',
|
||||
content: '<p>When you process an order, The Orders page will show the order with a payment status of Paid or Partially paid.</p> <p>If the customer provided their email address, then they receive a receipt by email.</p><p>When you process an order, The Orders page will show the order with a payment status of Paid or Partially paid.</p> <p>If the customer provided their email address, then they receive a receipt by email.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-cancel-an-order',
|
||||
title: 'How to cancel an order?',
|
||||
content: '<p>Canceling an order indicates that you are halting order processing. For example, if a customer requests a cancellation or you suspect the order is fraudulent, then you can cancel the order to help prevent staff or fulfillment services from continuing work on the order. You can also cancel an order if an item was ordered and isn\'t available.</p> <p>You can cancel an order by clicking the cancel button on orders page.</p><p>Canceling an order indicates that you are halting order processing. For example, if a customer requests a cancellation or you suspect the order is fraudulent, then you can cancel the order to help prevent staff or fulfillment services from continuing work on the order. You can also cancel an order if an item was ordered and isn\'t available.</p> <p>You can cancel an order by clicking the cancel button on orders page.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'whats-the-status-of-my-order',
|
||||
title: 'What\'s the Status of My Order?',
|
||||
content: '<p>You can check the shipping status of your order on website or the app. If the seller added a tracking number, you can use that to get detailed information about the package\'s movement through the shipping carrier.</p><p>You\'ll see the shipping status on the orders page. You\'ll also see an estimated delivery date which should give you an idea of when you can expect the order to arrive, and a tracking number if it\'s available for your order.</p><p>You can check the shipping status of your order on website or the app. If the seller added a tracking number, you can use that to get detailed information about the package\'s movement through the shipping carrier.</p><p>You\'ll see the shipping status on the orders page. You\'ll also see an estimated delivery date which should give you an idea of when you can expect the order to arrive, and a tracking number if it\'s available for your order.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-return-or-exchange-an-item',
|
||||
title: 'How to Return or Exchange an Item?',
|
||||
content: '<p>If you need to return or exchange an item, the seller you purchased your order from is the best person to help you. Each seller manages their own orders, and makes decisions about cancellations, refunds, and returns.</p><p>Sellers aren\'t required to accept returns, exchanges, or provide a refund unless stated in their shop policies. Go to the shop\'s homepage and scroll to the bottom to see the shop\'s policies.</p><p>If you need to return or exchange an item, the seller you purchased your order from is the best person to help you. Each seller manages their own orders, and makes decisions about cancellations, refunds, and returns.</p><p>Sellers aren\'t required to accept returns, exchanges, or provide a refund unless stated in their shop policies. Go to the shop\'s homepage and scroll to the bottom to see the shop\'s policies.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'payments',
|
||||
title: 'Payments',
|
||||
icon: 'tabler-currency-dollar',
|
||||
articles: [
|
||||
{
|
||||
slug: 'how-do-i-get-paid',
|
||||
title: 'How do i get paid?',
|
||||
content: '<p>When you set up a payment provider to accept credit card payments, each payment must be processed, so there is usually a delay between when the customer pays for their order and when you receive the payment. After the payment is processed, the purchase amount will be transferred to your merchant account.</p><p>When you set up a payment provider to accept credit card payments, each payment must be processed, so there is usually a delay between when the customer pays for their order and when you receive the payment. After the payment is processed, the purchase amount will be transferred to your merchant account.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-often-do-i-get-paid',
|
||||
title: 'How often do I get paid?',
|
||||
content: '<p>If you use our payment system, then you can check your pay period to see when you receive payouts from credit card orders. Other payment providers have their own rules on when you receive payouts for credit card orders. Check with your provider to find out how often you will be paid.</p> <p>After the payout is sent, it might not be received by your bank right away. It can take a few days after the payout is sent for it to be deposited into your bank account. Check with your bank if you find your payouts are being delayed.</p><p>If you use our payment system, then you can check your pay period to see when you receive payouts from credit card orders. Other payment providers have their own rules on when you receive payouts for credit card orders. Check with your provider to find out how often you will be paid.</p> <p>After the payout is sent, it might not be received by your bank right away. It can take a few days after the payout is sent for it to be deposited into your bank account. Check with your bank if you find your payouts are being delayed.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-much-do-i-get-paid',
|
||||
title: 'How much do I get paid?',
|
||||
content: '<p>You can be charged several third-party transaction fees for online transactions. For credit card transactions, the issuer, the acquirer, and the credit card company all charge a small fee for using their services.</p><p>You aren\'t charged third-party transaction fees for orders processed through our payment system. You pay credit card processing fees, depending on your subscription plan. If you\'re using a third-party payment provider with us, then you\'re charged a third-party transaction fee.</p><p>You can be charged several third-party transaction fees for online transactions. For credit card transactions, the issuer, the acquirer, and the credit card company all charge a small fee for using their services.</p><p>You aren\'t charged third-party transaction fees for orders processed through our payment system. You pay credit card processing fees, depending on your subscription plan. If you\'re using a third-party payment provider with us, then you\'re charged a third-party transaction fee.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'cant-complete-payment-on-paypal',
|
||||
title: 'Can\'t Complete Payment on PayPal?',
|
||||
content: '<p>PayPal uses various security measures to protect their users. Because of this, PayPal may occasionally prohibit a buyer from submitting payment to a seller through PayPal.</p><p>If you\'re ultimately unable to submit payment, try working with the seller to determine an alternative payment method. Learn how to contact a seller.</p><p>PayPal uses various security measures to protect their users. Because of this, PayPal may occasionally prohibit a buyer from submitting payment to a seller through PayPal.</p><p>If you\'re ultimately unable to submit payment, try working with the seller to determine an alternative payment method. Learn how to contact a seller.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'why-is-my-order-is-still-processing',
|
||||
title: 'Why is my order is still processing?',
|
||||
content: '<p>If you received an email saying that your order is still processing, it means that your purchase is being screened by our third-party partner. All Payments orders are screened to ensure that the orders are legitimate and to protect from possible fraud.</p><p>Most orders are processed in under 72 hours. You\'ll receive a confirmation email when the review is complete.</p><p>If you received an email saying that your order is still processing, it means that your purchase is being screened by our third-party partner. All Payments orders are screened to ensure that the orders are legitimate and to protect from possible fraud.</p><p>Most orders are processed in under 72 hours. You\'ll receive a confirmation email when the review is complete.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'tabler-refresh',
|
||||
slug: 'returns-refunds-replacements',
|
||||
title: 'Returns, Refunds and Replacements',
|
||||
articles: [
|
||||
{
|
||||
slug: 'what-can-i-return',
|
||||
title: 'What can I return?',
|
||||
content: '<p>You may request returns for most items you buy from the sellers listed on the website. However, you can only return items explicitly identified as "returnable" on the product detail page and/or our policy and within the ‘return window\' period.</p> <p> Please refer to the website Returns policy. to know which categories are "non-returnable" and the specific return windows for categories eligible for return.</p><ul><li>Physically damaged</li><li>Has missing parts or accessories</li><li>Defective</li><li>Different from its description on the product detail page on the website</li></ul><p>You may request returns for most items you buy from the sellers listed on the website. However, you can only return items explicitly identified as "returnable" on the product detail page and/or our policy and within the \'return window\' period.</p> <p> Please refer to the website Returns policy. to know which categories are "non-returnable" and the specific return windows for categories eligible for return.</p><ul><li>Physically damaged</li><li>Has missing parts or accessories</li><li>Defective</li><li>Different from its description on the product detail page on the website</li></ul>',
|
||||
},
|
||||
{
|
||||
slug: 'when-will-i-get-my-refund',
|
||||
title: 'When will I get my refund?',
|
||||
content: '<p>Following are the refund processing timelines after the item is received by Amazon or the Seller notifies us of the receipt of the return:</p><ul><li><strong>Wallet:</strong> 2 hours</li><li><strong>Credit/Debit Card:</strong> 2-4 Business Days</li><li><strong>Bank Account:</strong> 2-4 Business Days</li></ul><p>Following are the refund processing timelines after the item is received by Amazon or the Seller notifies us of the receipt of the return:</p><ul><li><strong>Wallet:</strong> 2 hours</li><li><strong>Credit/Debit Card:</strong> 2-4 Business Days</li><li><strong>Bank Account:</strong> 2-4 Business Days</li></ul>',
|
||||
},
|
||||
{
|
||||
slug: 'can-my-order-be-replaced',
|
||||
title: 'Can my order be replaced?',
|
||||
content: '<p>If the item you ordered arrived in a physically damaged/ defective condition or is different from their description on the product detail page, or has missing parts or accessories, it will be eligible for a free replacement as long as the exact item is available with the same seller.</p><p>If the item you ordered arrived in a physically damaged/ defective condition or is different from their description on the product detail page, or has missing parts or accessories, it will be eligible for a free replacement as long as the exact item is available with the same seller.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'tabler-users',
|
||||
slug: 'safety-security',
|
||||
avatarColor: 'primary',
|
||||
title: 'Safety and Security',
|
||||
subCategories: [
|
||||
{
|
||||
slug: 'hacked-accounts',
|
||||
icon: 'tabler-shield',
|
||||
title: 'Security and hacked accounts',
|
||||
articles: [
|
||||
{
|
||||
slug: 'has-my-account-been-compromised',
|
||||
title: 'Has my account been compromised?',
|
||||
content: '<p>Have you:</p><ul><li>Noticed unexpected posts by your account</li><li>Seen unintended Direct Messages sent from your account</li><li>Observed other account behaviors you didn\'t make or approve (like following, unfollowing, or blocking)</li></ul>. <p>If you\'ve answered yes to any of the above, please change your password and Revoke connections to third-party applications</p><p>Have you:</p><ul><li>Noticed unexpected posts by your account</li><li>Seen unintended Direct Messages sent from your account</li><li>Observed other account behaviors you didn\'t make or approve (like following, unfollowing, or blocking)</li></ul>. <p>If you\'ve answered yes to any of the above, please change your password and Revoke connections to third-party applications</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-keep-my-account-safe',
|
||||
title: 'How to keep my account safe?',
|
||||
content: '<p>To help keep your account secure, we recommend the following best practices:</p><ul><li>Use a strong password that you don\'t reuse on other websites.</li><li>Use two-factor authentication.</li><li>Require email and phone number to request a reset password link or code.</li><li>Be cautious of suspicious links and always make sure you\'re on our website before you enter your login information.</li><li>Never give your username and password out to third parties, especially those promising to get you followers, make you money, or verify you.</li></ul><p>To help keep your account secure, we recommend the following best practices:</p><ul><li>Use a strong password that you don\'t reuse on other websites.</li><li>Use two-factor authentication.</li><li>Require email and phone number to request a reset password link or code.</li><li>Be cautious of suspicious links and always make sure you\'re on our website before you enter your login information.</li><li>Never give your username and password out to third parties, especially those promising to get you followers, make you money, or verify you.</li></ul>',
|
||||
},
|
||||
{
|
||||
slug: 'help-with-my-hacked-account',
|
||||
title: 'Help with my hacked account',
|
||||
content: '<p>If you think you\'ve been hacked and you\'re unable to log in with your username and password, please take the following two steps:</p><ol><li><p>Request a password reset</p> <p>Reset your password by requesting an email from the password reset form. Try entering both your username and email address, and be sure to check for the reset email at the address associated with your account.</p></li><li><p>Contact Support if you still require assistance</p><p>If you still can\'t log in, contact us by submitting a Support Request. Be sure to use the email address you associated with the hacked account; we\'ll then send additional information and instructions to that email address. When submitting your support request please Include both your username and the date you last had access to your account.</p></li></ol><p>If you think you\'ve been hacked and you\'re unable to log in with your username and password, please take the following two steps:</p><ol><li><p>Request a password reset</p> <p>Reset your password by requesting an email from the password reset form. Try entering both your username and email address, and be sure to check for the reset email at the address associated with your account.</p></li><li><p>Contact Support if you still require assistance</p><p>If you still can\'t log in, contact us by submitting a Support Request. Be sure to use the email address you associated with the hacked account; we\'ll then send additional information and instructions to that email address. When submitting your support request please Include both your username and the date you last had access to your account.</p></li></ol>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'privacy',
|
||||
title: 'Privacy',
|
||||
icon: 'tabler-lock',
|
||||
articles: [
|
||||
{
|
||||
slug: 'what-is-visible-on-my-profile',
|
||||
title: 'What is visible on my profile?',
|
||||
content: '<p>Most of the profile information you provide us is always public, like your biography, location, website, and picture. For certain profile information fields we provide you with visibility settings to select who can see this information in your profile.</p><p>If you provide us with profile information and you don\'t see a visibility setting, that information is public.</p><p>Most of the profile information you provide us is always public, like your biography, location, website, and picture. For certain profile information fields we provide you with visibility settings to select who can see this information in your profile.</p><p>If you provide us with profile information and you don\'t see a visibility setting, that information is public.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'should-i-turn-on-precise-location',
|
||||
title: 'Should I turn on precise location?',
|
||||
content: '<p>Enabling precise location through our official app allows us to collect, store, and use your precise location, such as GPS information. This allows us to provide, develop, and improve a variety of our services, including but not limited to:</p><ul><li>Delivery of content, including posts and advertising, that is better tailored to your location.</li><li>Delivery of location-specific trends.</li><li>Showing your followers the location you are posting from as part of your post, if you decide to geo-tag your post.</li></ul><p>Enabling precise location through our official app allows us to collect, store, and use your precise location, such as GPS information. This allows us to provide, develop, and improve a variety of our services, including but not limited to:</p><ul><li>Delivery of content, including posts and advertising, that is better tailored to your location.</li><li>Delivery of location-specific trends.</li><li>Showing your followers the location you are posting from as part of your post, if you decide to geo-tag your post.</li></ul>',
|
||||
},
|
||||
{
|
||||
slug: 'what-location-information-is-displayed',
|
||||
title: 'What location information is displayed?',
|
||||
content: '<ul><li>All geolocation information begins as a location (latitude and longitude), sent from your browser or device. We won\'t show any location information unless you\'ve opted in to the feature, and have allowed your device or browser to transmit your coordinates to us.</li><li>If you have chosen to attach location information to your Posts, your selected location label is displayed underneath the text of the Post.</li><li>When you use the in-app camera on iOS and Android to attach a photo or video to your post and toggle on the option to tag your precise location, that post will include both the location label of your choice and your device\'s precise location (latitude and longitude), which can be found via API. Your precise location may be more specific than the location label you select. This is helpful when sharing on-the-ground moments.</li></ul><ul><li>All geolocation information begins as a location (latitude and longitude), sent from your browser or device. We won\'t show any location information unless you\'ve opted in to the feature, and have allowed your device or browser to transmit your coordinates to us.</li><li>If you have chosen to attach location information to your Posts, your selected location label is displayed underneath the text of the Post.</li><li>When you use the in-app camera on iOS and Android to attach a photo or video to your post and toggle on the option to tag your precise location, that post will include both the location label of your choice and your device\'s precise location (latitude and longitude), which can be found via API. Your precise location may be more specific than the location label you select. This is helpful when sharing on-the-ground moments.</li></ul>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'spam-fake-accounts',
|
||||
title: 'Spam and fake accounts',
|
||||
icon: 'tabler-mail',
|
||||
articles: [
|
||||
{
|
||||
slug: 'how-to-detect-fake-email',
|
||||
title: 'How to detect fake email?',
|
||||
content: `<p>We will only send you emails from @${themeConfig.app.title}.com or @t.${themeConfig.app.title}.com. However, some people may receive fake or suspicious emails that look like they were sent by US. These emails might include malicious attachments or links to spam or phishing websites. Please know that we will never send emails with attachments or request your password by email.</p><p>We will only send you emails from @${themeConfig.app.title}.com or @t.${themeConfig.app.title}.com. However, some people may receive fake or suspicious emails that look like they were sent by US. These emails might include malicious attachments or links to spam or phishing websites. Please know that we will never send emails with attachments or request your password by email.</p>`,
|
||||
},
|
||||
{
|
||||
slug: 'how-do-I-report-an-impersonation-violation',
|
||||
title: 'How do I report an impersonation violation?',
|
||||
content: '<p>If you believe an account is posing as you or your brand, you or your authorized representative can file a report in our support Center.</p><p>If you believe an account is misusing the identity of somebody else, you can flag it as a bystander by reporting directly from the account\'s profile.</p><p>If you believe an account is posing as you or your brand, you or your authorized representative can file a report in our support Center.</p><p>If you believe an account is misusing the identity of somebody else, you can flag it as a bystander by reporting directly from the account\'s profile.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'someone-is-using-my-email-address-what-can-i-do',
|
||||
title: 'Someone is using my email address, what can I do?',
|
||||
content: '<p>Are you trying to create a new account, but you\'re told your email address or phone number is already in use? This support article outlines how your email address may already be in use and how to resolve the issue.</p><p>Are you trying to create a new account, but you\'re told your email address or phone number is already in use? This support article outlines how your email address may already be in use and how to resolve the issue.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
avatarColor: 'error',
|
||||
icon: 'tabler-clipboard',
|
||||
slug: 'rules-policies',
|
||||
title: 'Rules and Policies',
|
||||
subCategories: [
|
||||
{
|
||||
slug: 'general',
|
||||
title: 'General',
|
||||
icon: 'tabler-globe',
|
||||
articles: [
|
||||
{
|
||||
slug: 'our-rules',
|
||||
title: 'Our Rules',
|
||||
content: '<p>Our purpose is to serve the public conversation. Violence, harassment and other similar types of behavior discourage people from expressing themselves, and ultimately diminish the value of global public conversation. Our rules are to ensure all people can participate in the public conversation freely and safely.</p><p>Our purpose is to serve the public conversation. Violence, harassment and other similar types of behavior discourage people from expressing themselves, and ultimately diminish the value of global public conversation. Our rules are to ensure all people can participate in the public conversation freely and safely.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'what-is-username-squatting-policy',
|
||||
title: 'What is username squatting policy?',
|
||||
content: '<p>Username squatting is prohibited by the Rules.</p><p>Please note that if an account has had no updates, no profile image, and there is no intent to mislead, it typically means there\'s no name-squatting or impersonation. Note that we will not release squatted usernames except in cases of trademark infringement. If your report involves trademark infringement, please consult those policies for instructions for reporting these accounts.</p><p>Attempts to sell, buy, or solicit other forms of payment in exchange for usernames are also violations and may result in permanent account suspension.</p><p>Username squatting is prohibited by the Rules.</p><p>Please note that if an account has had no updates, no profile image, and there is no intent to mislead, it typically means there\'s no name-squatting or impersonation. Note that we will not release squatted usernames except in cases of trademark infringement. If your report involves trademark infringement, please consult those policies for instructions for reporting these accounts.</p><p>Attempts to sell, buy, or solicit other forms of payment in exchange for usernames are also violations and may result in permanent account suspension.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'third-party-advertising-in-video-content',
|
||||
title: 'Third-party advertising in video content',
|
||||
content: '<p>You may not submit, post, or display any video content on or through our services that includes third-party advertising, such as pre-roll video ads or sponsorship graphics, without our prior consent.</p><p><strong>Note:</strong> we may need to change these rules from time to time in order to support our goal of promoting a healthy public conversation</p><p>You may not submit, post, or display any video content on or through our services that includes third-party advertising, such as pre-roll video ads or sponsorship graphics, without our prior consent.</p><p><strong>Note:</strong> we may need to change these rules from time to time in order to support our goal of promoting a healthy public conversation</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'tabler-registered',
|
||||
slug: 'intellectual-property',
|
||||
title: 'Intellectual property',
|
||||
articles: [
|
||||
{
|
||||
slug: 'what-is-your-trademark-policy',
|
||||
title: 'What is your trademark policy?',
|
||||
content: '<p><strong>You may not violate others\' intellectual property rights, including copyright and trademark.</strong></p><p>A trademark is a word, logo, phrase, or device that distinguishes a trademark holder\'s good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner.</p><p><strong>You may not violate others\' intellectual property rights, including copyright and trademark.</strong></p><p>A trademark is a word, logo, phrase, or device that distinguishes a trademark holder\'s good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'what-are-counterfeit-goods',
|
||||
title: 'What are counterfeit goods?',
|
||||
content: '<p>Counterfeit goods are goods, including digital goods, that are promoted, sold, or otherwise distributed using a trademark or brand that is identical to, or substantially indistinguishable from, the registered trademark or brand of another, without authorization from the trademark or brand owner. Counterfeit goods attempt to deceive consumers into believing the counterfeit is a genuine product of the brand owner, or to represent themselves as faux, replicas or imitations of the genuine product.</p><p>Counterfeit goods are goods, including digital goods, that are promoted, sold, or otherwise distributed using a trademark or brand that is identical to, or substantially indistinguishable from, the registered trademark or brand of another, without authorization from the trademark or brand owner. Counterfeit goods attempt to deceive consumers into believing the counterfeit is a genuine product of the brand owner, or to represent themselves as faux, replicas or imitations of the genuine product.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'what-types-of-copyright-complaints-do-you-respond-to',
|
||||
title: 'What types of copyright complaints do you respond to?',
|
||||
content: '<p>We respond to copyright complaints submitted under the Digital Millennium Copyright Act (“DMCA”). Section 512 of the DMCA outlines the statutory requirements necessary for formally reporting copyright infringement, as well as providing instructions on how an affected party can appeal a removal by submitting a compliant counter-notice.</p><p>If you are concerned about the use of your brand or entity\'s name, please review our trademark policy. If you are concerned about a parody, newsfeed, commentary, or fan account, please see the relevant policy here. These are generally not copyright issues.</p><p>We respond to copyright complaints submitted under the Digital Millennium Copyright Act (“DMCA”). Section 512 of the DMCA outlines the statutory requirements necessary for formally reporting copyright infringement, as well as providing instructions on how an affected party can appeal a removal by submitting a compliant counter-notice.</p><p>If you are concerned about the use of your brand or entity\'s name, please review our trademark policy. If you are concerned about a parody, newsfeed, commentary, or fan account, please see the relevant policy here. These are generally not copyright issues.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'tabler-clipboard',
|
||||
slug: 'guidelines-for-law-enforcement',
|
||||
title: 'Guidelines for law enforcement',
|
||||
articles: [
|
||||
{
|
||||
slug: 'does-we-have-access-to-user-generated-photos-or-videos',
|
||||
title: 'Does we have access to user-generated photos or videos?',
|
||||
content: `<p>We provide photo hosting for some image uploads (i.e., pic.${themeConfig.app.title}.com images) as well as account profile photos, and header photos. However, We are not the sole photo provider for images that may appear on the platform. More information about posting photos on platform.</p><p>We provide photo hosting for some image uploads (i.e., pic.${themeConfig.app.title}.com images) as well as account profile photos, and header photos. However, We are not the sole photo provider for images that may appear on the platform. More information about posting photos on platform.</p>`,
|
||||
},
|
||||
{
|
||||
slug: 'data-controller',
|
||||
title: 'Data Controller',
|
||||
content: '<p>For people who live in the United States or any other country outside of the European Union or the European Economic Area, the data controller responsible for personal data, Inc. based in San Francisco, California. For people who live in the European Union or the European Economic Area, the data controller is our International Unlimited Company based in Dublin, Ireland.</p><p>For people who live in the United States or any other country outside of the European Union or the European Economic Area, the data controller responsible for personal data, Inc. based in San Francisco, California. For people who live in the European Union or the European Economic Area, the data controller is our International Unlimited Company based in Dublin, Ireland.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'requests-for-Twitter-account-information',
|
||||
title: 'Requests for Twitter account information',
|
||||
content: '<p>Requests for user account information from law enforcement should be directed to us, Inc. in San Francisco, California or Twitter International Unlimited Company in Dublin, Ireland. We respond to valid legal process issued in compliance with applicable law.</p><p>Requests for user account information from law enforcement should be directed to us, Inc. in San Francisco, California or Twitter International Unlimited Company in Dublin, Ireland. We respond to valid legal process issued in compliance with applicable law.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'chats',
|
||||
title: 'Chats',
|
||||
avatarColor: 'warning',
|
||||
icon: 'tabler-message',
|
||||
subCategories: [
|
||||
{
|
||||
slug: 'general',
|
||||
title: 'General',
|
||||
icon: 'tabler-globe',
|
||||
articles: [
|
||||
{
|
||||
slug: 'what-is-forwarding-limits',
|
||||
title: 'What is forwarding limit?',
|
||||
content: '<p>You can forward a message with up to five chats at one time. If a message has already been forwarded, you can forward it to up to five chats, including a maximum of one group.</p><p>However, when a message is forwarded through a chain of five or more chats, meaning it\'s at least five forwards away from its original sender, a double arrow icon and "Forwarded many times" label will be displayed. These messages can only be forwarded to one chat at a time, as a way to help keep conversations on platform intimate and personal. This also helps slow down the spread of rumors, viral messages, and fake news.</p><p>You can forward a message with up to five chats at one time. If a message has already been forwarded, you can forward it to up to five chats, including a maximum of one group.</p><p>However, when a message is forwarded through a chain of five or more chats, meaning it\'s at least five forwards away from its original sender, a double arrow icon and "Forwarded many times" label will be displayed. These messages can only be forwarded to one chat at a time, as a way to help keep conversations on platform intimate and personal. This also helps slow down the spread of rumors, viral messages, and fake news.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'what-is-last-seen-and-online',
|
||||
title: 'What is last seen & online?',
|
||||
content: '<p>Last seen and online tell you the last time your contacts used the app, or if they\'re online.</p><p>If a contact is online, they have th app open in the foreground on their device and are connected to the Internet. However, it doesn\'t necessarily mean the contact has read your message.</p><p>Last seen and online tell you the last time your contacts used the app, or if they\'re online.</p><p>If a contact is online, they have th app open in the foreground on their device and are connected to the Internet. However, it doesn\'t necessarily mean the contact has read your message.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-reply-to-a-message',
|
||||
title: 'How to reply to a message?',
|
||||
content: '<p>You can use the reply feature when responding to a specific message in an individual or group chat.</p><p>Tap and hold the message, then tap Reply. Enter your response and tap Send. Alternatively, swipe right on the message to reply.</p><p>You can use the reply feature when responding to a specific message in an individual or group chat.</p><p>Tap and hold the message, then tap Reply. Enter your response and tap Send. Alternatively, swipe right on the message to reply.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'features',
|
||||
title: 'Features',
|
||||
icon: 'tabler-star',
|
||||
articles: [
|
||||
{
|
||||
slug: 'how-to-send-disappearing-messages',
|
||||
title: 'How to send disappearing messages?',
|
||||
content: '<p>Disappearing messages is an optional feature you can turn on for more privacy.</p><p>When you enable disappearing messages, you can set messages to disappear 24 hours, 7 days, or 90 days after the time they are sent. The most recent selection only controls new messages in the chat. You can choose to turn disappearing messages on for all of your chats, or select specific chats. This setting won\'t affect messages you previously sent or received in the chat. In an individual chat, either user can turn disappearing messages on or off. In a group chat, any group participants can turn disappearing messages on or off. However, a group admin can change group settings to allow only admins to turn disappearing messages on or off.</p><p>Disappearing messages is an optional feature you can turn on for more privacy.</p><p>When you enable disappearing messages, you can set messages to disappear 24 hours, 7 days, or 90 days after the time they are sent. The most recent selection only controls new messages in the chat. You can choose to turn disappearing messages on for all of your chats, or select specific chats. This setting won\'t affect messages you previously sent or received in the chat. In an individual chat, either user can turn disappearing messages on or off. In a group chat, any group participants can turn disappearing messages on or off. However, a group admin can change group settings to allow only admins to turn disappearing messages on or off.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'can-i-send-view-once-messages',
|
||||
title: 'Can I send view once messages?',
|
||||
content: '<p>For added privacy, you can now send photos and videos that disappear from your chat after the recipient has opened them once. To use view once, please update the app to the latest version available for your device.</p><p>For added privacy, you can now send photos and videos that disappear from your chat after the recipient has opened them once. To use view once, please update the app to the latest version available for your device.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-pin-a-chat',
|
||||
title: 'How to pin a chat?',
|
||||
content: '<p>The pin chat feature allows you to pin up to three specific chats to the top of your chats list so you can quickly find them.</p><p>On <strong>iPhone</strong>: Swipe right on the chat you want to pin, then tap Pin.</p><p>On <strong>Android</strong>: Tap and hold the chat you want to pin, then tap Pin chat</p><p>The pin chat feature allows you to pin up to three specific chats to the top of your chats list so you can quickly find them.</p><p>On <strong>iPhone</strong>: Swipe right on the chat you want to pin, then tap Pin.</p><p>On <strong>Android</strong>: Tap and hold the chat you want to pin, then tap Pin chat</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'encryption',
|
||||
title: 'Encryption',
|
||||
icon: 'tabler-lock',
|
||||
articles: [
|
||||
{
|
||||
slug: 'what-is-end-to-end-encrypted-backup',
|
||||
title: 'What is end-to-end encrypted backup?',
|
||||
content: '<p>End-to-end encryption ensures only you and the person you\'re communicating with can read or listen to what is sent, and nobody in between, not even us. With end-to-end encrypted backup, you can also add that same layer of protection to your backup on iCloud or Google Drive.</p><p>End-to-end encryption ensures only you and the person you\'re communicating with can read or listen to what is sent, and nobody in between, not even us. With end-to-end encrypted backup, you can also add that same layer of protection to your backup on iCloud or Google Drive.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'can-i-change-password-for-end-to-end-encrypted-backup',
|
||||
title: 'Can I change password for end-to-end encrypted backup?',
|
||||
content: '<p>When you create an end-to-end encrypted backup, your messages and media are stored in the cloud and secured by a password or a 64-digit encryption key. Your password can be changed at any time as long as you have access to your previous password or key.</p><p><strong>Note:</strong> You won\'t be able to restore your backup if you lose your chats and forget your password or key. We can\'t reset your password or restore your backup for you.</p><p>When you create an end-to-end encrypted backup, your messages and media are stored in the cloud and secured by a password or a 64-digit encryption key. Your password can be changed at any time as long as you have access to your previous password or key.</p><p><strong>Note:</strong> You won\'t be able to restore your backup if you lose your chats and forget your password or key. We can\'t reset your password or restore your backup for you.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'can-i-turnoff-end-to-end-encrypted-backup',
|
||||
title: 'Can I turnoff end-to-end encrypted backup?',
|
||||
content: '<p>You can choose to turn off end-to-end encrypted backup by using your password or key, or by authenticating with your biometrics or device PIN. If you turn off end-to-end encrypted backup, your messages and media will no longer back up to the cloud unless you set them up to do so.</p><p>You can choose to turn off end-to-end encrypted backup by using your password or key, or by authenticating with your biometrics or device PIN. If you turn off end-to-end encrypted backup, your messages and media will no longer back up to the cloud unless you set them up to do so.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'connections',
|
||||
title: 'Connections',
|
||||
avatarColor: 'secondary',
|
||||
icon: 'tabler-link',
|
||||
subCategories: [
|
||||
{
|
||||
slug: 'conversations',
|
||||
title: 'Conversations',
|
||||
icon: 'tabler-message',
|
||||
articles: [
|
||||
{
|
||||
slug: 'how-to-send-messages-to-connections',
|
||||
title: 'How to send messages to connections?',
|
||||
content: '<p>You can send a message to your connections directly from the messaging page or connections page.</p><p>The sent message will be visible in the recipient\'s message list and possibly in their email, depending on their app notification settings.</p><p>You can send a message to your connections directly from the messaging page or connections page.</p><p>The sent message will be visible in the recipient\'s message list and possibly in their email, depending on their app notification settings.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-edit-or-delete-a-sent-message-within-a-conversation',
|
||||
title: 'How to edit or delete a sent message within a conversation?',
|
||||
content: '<p>You can edit or delete a text only message you send on app.</p><p><strong>Note:</strong>You can only edit or delete a message within 60 minutes of sending the message.</p><p>You can edit or delete a text only message you send on app.</p><p><strong>Note:</strong>You can only edit or delete a message within 60 minutes of sending the message.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-delete-a-message',
|
||||
title: 'How to delete a message?',
|
||||
content: '<p>A conversation thread starts when a message is sent to one or more people via app messaging. You can delete conversation threads individually or in bulk.</p><p><strong>Important:</strong>You can\'t restore or access deleted messages. <strong>The conversation thread will only be deleted from your inbox and not from the recipient\'s.</strong></p><p>A conversation thread starts when a message is sent to one or more people via app messaging. You can delete conversation threads individually or in bulk.</p><p><strong>Important:</strong>You can\'t restore or access deleted messages. <strong>The conversation thread will only be deleted from your inbox and not from the recipient\'s.</strong></p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'jobs',
|
||||
title: 'Jobs',
|
||||
icon: 'tabler-briefcase',
|
||||
articles: [
|
||||
{
|
||||
slug: 'find-relevant-jobs-through-social-hiring-and-meeting-the-team',
|
||||
title: 'Find relevant jobs through social hiring and meeting the team?',
|
||||
content: '<p>We have introduced two features that will help both job seekers and hirers fully engage with the power of their platform.</p> <ul><li>The #social hiring feature will notify members when a first- or second-degree connection is hiring for a relevant job. When a network connection posts a relevant job on app or adds a #hiring frame to their profile picture, app will notify the job seeker. From there, job seekers will be able to view open jobs that people in their network are hiring for.</li><li>When a member clicks on the job\'s details page, they will see the “Meet the Hiring Team” feature. Members will be able to connect and message the entire team listed in this section, including the job poster.</li></ul><p>These features will allow members to find jobs through their connections and stand out to the hiring team. As a result, the hiring team will also be able to reach more potential candidates through their network.</p><p>We have introduced two features that will help both job seekers and hirers fully engage with the power of their platform.</p> <ul><li>The #social hiring feature will notify members when a first- or second-degree connection is hiring for a relevant job. When a network connection posts a relevant job on app or adds a #hiring frame to their profile picture, app will notify the job seeker. From there, job seekers will be able to view open jobs that people in their network are hiring for.</li><li>When a member clicks on the job\'s details page, they will see the “Meet the Hiring Team” feature. Members will be able to connect and message the entire team listed in this section, including the job poster.</li></ul><p>These features will allow members to find jobs through their connections and stand out to the hiring team. As a result, the hiring team will also be able to reach more potential candidates through their network.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-does-the-app-determine-when-a-job-is-relevant',
|
||||
title: 'How does the app determine when a job is relevant?',
|
||||
content: '<p>We will notify job seekers when someone in their network is hiring for a job that matches their current job title or industry listed in your profile or open to work preferences.</p><p>We will notify job seekers when someone in their network is hiring for a job that matches their current job title or industry listed in your profile or open to work preferences.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-can-job-seekers-receive-these-notifications',
|
||||
title: 'How can job seekers receive these notifications?',
|
||||
content: '<p>Members will automatically receive notifications without having to opt in. To turn off the notification, click the three dots next to the notification and select Turn off.</p><p>Members will automatically receive notifications without having to opt in. To turn off the notification, click the three dots next to the notification and select Turn off.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'people',
|
||||
title: 'People',
|
||||
icon: 'tabler-users',
|
||||
articles: [
|
||||
{
|
||||
slug: 'how-to-import-and-invite-your-email-contacts',
|
||||
title: 'How to import and invite your email contacts?',
|
||||
content: '<p>You can build your network by importing a list of your contacts you already know on the app. This will run a one-time upload of your address book contacts, as well as their detailed contact information. We periodically import and store details about your address book contacts to suggest relevant contacts for you to connect with, to show you relevant updates, and for other uses explained in our Privacy Policy. We\'ll never email anyone without your permission.</p><p>You can build your network by importing a list of your contacts you already know on the app. This will run a one-time upload of your address book contacts, as well as their detailed contact information. We periodically import and store details about your address book contacts to suggest relevant contacts for you to connect with, to show you relevant updates, and for other uses explained in our Privacy Policy. We\'ll never email anyone without your permission.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'various-ways-to-connect-with-people',
|
||||
title: 'Various ways to connect with people?',
|
||||
content: '<p>Building your network is a great way to stay in touch with alumni, colleagues, and recruiters, as well as connect with new, professional opportunities. A primary email address is mandatory to send invitations. Members become 1st-degree connections when they accept your invitation.</p><p>First-degree connections are given access to any information you\'ve displayed on your profile. To ensure an optimal site experience, the members can have a maximum of 30,000 1st-degree connections.</p><p>Building your network is a great way to stay in touch with alumni, colleagues, and recruiters, as well as connect with new, professional opportunities. A primary email address is mandatory to send invitations. Members become 1st-degree connections when they accept your invitation.</p><p>First-degree connections are given access to any information you\'ve displayed on your profile. To ensure an optimal site experience, the members can have a maximum of 30,000 1st-degree connections.</p>',
|
||||
},
|
||||
{
|
||||
slug: 'how-to-follow-or-unfollow-people',
|
||||
title: 'How to follow or unfollow people?',
|
||||
content: '<p>When you follow someone, new content posted or shared by the person will be displayed in your feed. If you no longer wish to see the content of someone in your feed, you can always unfollow this person.</p><p>You can find people to follow from your feed, the Notifications tab, My Network page, or from the Search bar at the top of the page.</p><p>Unfollowing a person will hide all updates from them on your feed. If you\'re connected to a person and choose to unfollow them, you\'ll remain connected, but won\'t see their updates. They won\'t be notified that you\'ve unfollowed them. The members will receive a notification if you begin following them again.</p><p>When you follow someone, new content posted or shared by the person will be displayed in your feed. If you no longer wish to see the content of someone in your feed, you can always unfollow this person.</p><p>You can find people to follow from your feed, the Notifications tab, My Network page, or from the Search bar at the top of the page.</p><p>Unfollowing a person will hide all updates from them on your feed. If you\'re connected to a person and choose to unfollow them, you\'ll remain connected, but won\'t see their updates. They won\'t be notified that you\'ve unfollowed them. The members will receive a notification if you begin following them again.</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
keepLearning: [
|
||||
{
|
||||
slug: 'blogging',
|
||||
title: 'Blogging',
|
||||
img: laptop,
|
||||
subtitle: 'Expert tips & tools to improve your website or online store using blog.',
|
||||
},
|
||||
{
|
||||
slug: 'inspiration-center',
|
||||
title: 'Inspiration Center',
|
||||
img: lightbulb,
|
||||
subtitle: 'inspiration from experts to help you start and grow your big ideas.',
|
||||
},
|
||||
{
|
||||
slug: 'community',
|
||||
title: 'Community',
|
||||
img: discord,
|
||||
subtitle: 'A group of people living in the same place or having a particular.',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
mock.onGet('/pages/help-center/landing').reply(() => {
|
||||
const allArticles = []
|
||||
|
||||
data.categories.map(category => category.subCategories.map(subCategory => subCategory.articles.map(article => allArticles.push(article))))
|
||||
|
||||
return [
|
||||
200,
|
||||
{ allArticles, categories: data.categories, popularArticles: data.popularArticles, keepLearning: data.keepLearning },
|
||||
]
|
||||
})
|
||||
mock.onGet('/pages/help-center/subcategory').reply(config => {
|
||||
const { category, subcategory } = config.params
|
||||
const filteredData = data.categories.filter(item => item.slug === category)
|
||||
|
||||
return [
|
||||
200,
|
||||
{
|
||||
data: filteredData[0],
|
||||
categories: data.categories,
|
||||
activeTab: subcategory || filteredData[0].subCategories[0].slug,
|
||||
},
|
||||
]
|
||||
})
|
||||
mock.onGet('/pages/help-center/article').reply(config => {
|
||||
const { article, category, subcategory } = config.params
|
||||
const activeCategory = data.categories.filter(item => item.slug === category)[0]
|
||||
const activeSubcategory = activeCategory.subCategories.filter(item => item.slug === subcategory)[0] || activeCategory.subCategories[0]
|
||||
const activeArticle = activeSubcategory.articles.filter(item => item.slug === article)[0]
|
||||
|
||||
return [200, { activeArticle, activeSubcategory, categories: data.categories, articles: activeSubcategory.articles }]
|
||||
})
|
19
resources/js/@fake-db/utils.js
Normal file
19
resources/js/@fake-db/utils.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export const paginateArray = (array, perPage, page) => array.slice((page - 1) * perPage, page * perPage)
|
||||
|
||||
// pagination meta
|
||||
export const paginationMeta = computed(() => {
|
||||
return (options, total) => {
|
||||
const start = (options.page - 1) * options.itemsPerPage + 1
|
||||
const end = Math.min(options.page * options.itemsPerPage, total)
|
||||
|
||||
return `Showing ${start} to ${end} of ${total} entries`
|
||||
}
|
||||
})
|
||||
export const genId = array => {
|
||||
const { length } = array
|
||||
let lastIndex = 0
|
||||
if (length)
|
||||
lastIndex = Number(array[length - 1]?.id) + 1
|
||||
|
||||
return lastIndex || (length + 1)
|
||||
}
|
244
resources/js/@iconify/build-icons.js
Normal file
244
resources/js/@iconify/build-icons.js
Normal file
@@ -0,0 +1,244 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* This is an advanced example for creating icon bundles for Iconify SVG Framework.
|
||||
*
|
||||
* It creates a bundle from:
|
||||
* - All SVG files in a directory.
|
||||
* - Custom JSON files.
|
||||
* - Iconify icon sets.
|
||||
* - SVG framework.
|
||||
*
|
||||
* This example uses Iconify Tools to import and clean up icons.
|
||||
* For Iconify Tools documentation visit https://docs.iconify.design/tools/tools2/
|
||||
*/
|
||||
const node_fs_1 = require("node:fs");
|
||||
const node_path_1 = require("node:path");
|
||||
// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify
|
||||
const tools_1 = require("@iconify/tools");
|
||||
const utils_1 = require("@iconify/utils");
|
||||
const sources = {
|
||||
svg: [
|
||||
{
|
||||
dir: 'resources/images/iconify-svg',
|
||||
monotone: false,
|
||||
prefix: 'custom',
|
||||
},
|
||||
// {
|
||||
// dir: 'emojis',
|
||||
// monotone: false,
|
||||
// prefix: 'emoji',
|
||||
// },
|
||||
],
|
||||
icons: [
|
||||
// 'mdi:home',
|
||||
// 'mdi:account',
|
||||
// 'mdi:login',
|
||||
// 'mdi:logout',
|
||||
// 'octicon:book-24',
|
||||
// 'octicon:code-square-24',
|
||||
],
|
||||
json: [
|
||||
// Custom JSON file
|
||||
// 'json/gg.json',
|
||||
// Iconify JSON file (@iconify/json is a package name, /json/ is directory where files are, then filename)
|
||||
require.resolve('@iconify-json/bx/icons.json'),
|
||||
require.resolve('@iconify-json/bxs/icons.json'),
|
||||
require.resolve('@iconify-json/bxl/icons.json'),
|
||||
{
|
||||
filename: require.resolve('@iconify-json/mdi/icons.json'),
|
||||
icons: [
|
||||
'file-remove-outline',
|
||||
'translate',
|
||||
'vuetify',
|
||||
'information-variant',
|
||||
'arrow-top-right',
|
||||
'arrow-bottom-right',
|
||||
'arrow-bottom-left',
|
||||
'arrow-top-left',
|
||||
'arrow-collapse-all',
|
||||
'arrow-down-left',
|
||||
'web',
|
||||
'cpu-32-bit',
|
||||
'alpha-r',
|
||||
'alpha-g',
|
||||
'alpha-b',
|
||||
'map-marker-off-outline',
|
||||
'alpha-t-box-outline',
|
||||
'form-select',
|
||||
'account-cog-outline',
|
||||
'laptop',
|
||||
],
|
||||
},
|
||||
// Custom file with only few icons
|
||||
// {
|
||||
// filename: require.resolve('@iconify-json/line-md/icons.json'),
|
||||
// icons: [
|
||||
// 'home-twotone-alt',
|
||||
// 'github',
|
||||
// 'document-list',
|
||||
// 'document-code',
|
||||
// 'image-twotone',
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
};
|
||||
// Iconify component (this changes import statement in generated file)
|
||||
// Available options: '@iconify/react' for React, '@iconify/vue' for Vue 3, '@iconify/vue2' for Vue 2, '@iconify/svelte' for Svelte
|
||||
const component = '@iconify/vue';
|
||||
// Set to true to use require() instead of import
|
||||
const commonJS = false;
|
||||
// File to save bundle to
|
||||
const target = (0, node_path_1.join)(__dirname, 'icons-bundle.js');
|
||||
/**
|
||||
* Do stuff!
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
(async function () {
|
||||
let bundle = commonJS
|
||||
? `const { addCollection } = require('${component}');\n\n`
|
||||
: `import { addCollection } from '${component}';\n\n`;
|
||||
// Create directory for output if missing
|
||||
const dir = (0, node_path_1.dirname)(target);
|
||||
try {
|
||||
await node_fs_1.promises.mkdir(dir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
//
|
||||
}
|
||||
/**
|
||||
* Convert sources.icons to sources.json
|
||||
*/
|
||||
if (sources.icons) {
|
||||
const sourcesJSON = sources.json ? sources.json : (sources.json = []);
|
||||
// Sort icons by prefix
|
||||
const organizedList = organizeIconsList(sources.icons);
|
||||
for (const prefix in organizedList) {
|
||||
const filename = require.resolve(`@iconify/json/json/${prefix}.json`);
|
||||
sourcesJSON.push({
|
||||
filename,
|
||||
icons: organizedList[prefix],
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Bundle JSON files
|
||||
*/
|
||||
if (sources.json) {
|
||||
for (let i = 0; i < sources.json.length; i++) {
|
||||
const item = sources.json[i];
|
||||
// Load icon set
|
||||
const filename = typeof item === 'string' ? item : item.filename;
|
||||
let content = JSON.parse(await node_fs_1.promises.readFile(filename, 'utf8'));
|
||||
// Filter icons
|
||||
if (typeof item !== 'string' && item.icons?.length) {
|
||||
const filteredContent = (0, utils_1.getIcons)(content, item.icons);
|
||||
if (!filteredContent)
|
||||
throw new Error(`Cannot find required icons in ${filename}`);
|
||||
content = filteredContent;
|
||||
}
|
||||
// Remove metadata and add to bundle
|
||||
removeMetaData(content);
|
||||
(0, utils_1.minifyIconSet)(content);
|
||||
bundle += `addCollection(${JSON.stringify(content)});\n`;
|
||||
console.log(`Bundled icons from ${filename}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Custom SVG
|
||||
*/
|
||||
if (sources.svg) {
|
||||
for (let i = 0; i < sources.svg.length; i++) {
|
||||
const source = sources.svg[i];
|
||||
// Import icons
|
||||
const iconSet = await (0, tools_1.importDirectory)(source.dir, {
|
||||
prefix: source.prefix,
|
||||
});
|
||||
// Validate, clean up, fix palette and optimise
|
||||
await iconSet.forEach(async (name, type) => {
|
||||
if (type !== 'icon')
|
||||
return;
|
||||
// Get SVG instance for parsing
|
||||
const svg = iconSet.toSVG(name);
|
||||
if (!svg) {
|
||||
// Invalid icon
|
||||
iconSet.remove(name);
|
||||
return;
|
||||
}
|
||||
// Clean up and optimise icons
|
||||
try {
|
||||
// Clean up icon code
|
||||
await (0, tools_1.cleanupSVG)(svg);
|
||||
if (source.monotone) {
|
||||
// Replace color with currentColor, add if missing
|
||||
// If icon is not monotone, remove this code
|
||||
await (0, tools_1.parseColors)(svg, {
|
||||
defaultColor: 'currentColor',
|
||||
callback: (attr, colorStr, color) => {
|
||||
return (!color || (0, tools_1.isEmptyColor)(color))
|
||||
? colorStr
|
||||
: 'currentColor';
|
||||
},
|
||||
});
|
||||
}
|
||||
// Optimise
|
||||
await (0, tools_1.runSVGO)(svg);
|
||||
}
|
||||
catch (err) {
|
||||
// Invalid icon
|
||||
console.error(`Error parsing ${name} from ${source.dir}:`, err);
|
||||
iconSet.remove(name);
|
||||
return;
|
||||
}
|
||||
// Update icon from SVG instance
|
||||
iconSet.fromSVG(name, svg);
|
||||
});
|
||||
console.log(`Bundled ${iconSet.count()} icons from ${source.dir}`);
|
||||
// Export to JSON
|
||||
const content = iconSet.export();
|
||||
bundle += `addCollection(${JSON.stringify(content)});\n`;
|
||||
}
|
||||
}
|
||||
// Save to file
|
||||
await node_fs_1.promises.writeFile(target, bundle, 'utf8');
|
||||
console.log(`Saved ${target} (${bundle.length} bytes)`);
|
||||
})().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
/**
|
||||
* Remove metadata from icon set
|
||||
*/
|
||||
function removeMetaData(iconSet) {
|
||||
const props = [
|
||||
'info',
|
||||
'chars',
|
||||
'categories',
|
||||
'themes',
|
||||
'prefixes',
|
||||
'suffixes',
|
||||
];
|
||||
props.forEach(prop => {
|
||||
delete iconSet[prop];
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Sort icon names by prefix
|
||||
*/
|
||||
function organizeIconsList(icons) {
|
||||
const sorted = Object.create(null);
|
||||
icons.forEach(icon => {
|
||||
const item = (0, utils_1.stringToIcon)(icon);
|
||||
if (!item)
|
||||
return;
|
||||
const prefix = item.prefix;
|
||||
const prefixList = sorted[prefix]
|
||||
? sorted[prefix]
|
||||
: (sorted[prefix] = []);
|
||||
const name = item.name;
|
||||
if (!prefixList.includes(name))
|
||||
prefixList.push(name);
|
||||
});
|
||||
return sorted;
|
||||
}
|
7
resources/js/@iconify/icons-bundle.js
Normal file
7
resources/js/@iconify/icons-bundle.js
Normal file
File diff suppressed because one or more lines are too long
16
resources/js/@iconify/tsconfig.json
Normal file
16
resources/js/@iconify/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "CommonJS",
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"composite": false,
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": [
|
||||
"./*.js"
|
||||
]
|
||||
}
|
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>
|
3
resources/js/@layouts/styles/_classes.scss
Normal file
3
resources/js/@layouts/styles/_classes.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
35
resources/js/@layouts/styles/_default-layout.scss
Normal file
35
resources/js/@layouts/styles/_default-layout.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
// These are styles which are both common in layout w/ vertical nav & horizontal nav
|
||||
@use "@layouts/styles/rtl";
|
||||
@use "@layouts/styles/placeholders";
|
||||
@use "@layouts/styles/mixins";
|
||||
@use "@configured-variables" as variables;
|
||||
|
||||
html,
|
||||
body {
|
||||
min-block-size: 100%;
|
||||
}
|
||||
|
||||
.layout-page-content {
|
||||
@include mixins.boxed-content(true);
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
// TODO: Use grid gutter variable here
|
||||
padding-block: 1.5rem;
|
||||
}
|
||||
|
||||
.layout-footer {
|
||||
.footer-content-container {
|
||||
block-size: variables.$layout-vertical-nav-footer-height;
|
||||
}
|
||||
|
||||
.layout-footer-sticky & {
|
||||
position: sticky;
|
||||
inset-block-end: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.layout-footer-hidden & {
|
||||
display: none;
|
||||
}
|
||||
}
|
10
resources/js/@layouts/styles/_global.scss
Normal file
10
resources/js/@layouts/styles/_global.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: inherit;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user