first commit

This commit is contained in:
Inshal
2024-05-29 22:34:28 +05:00
commit e63fc41a20
1470 changed files with 174828 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
<script setup>
const { injectSkinClasses } = useSkins()
// This will inject classes in body tag for accurate styling
injectSkinClasses()
// SECTION: Loading Indicator
const isFallbackStateActive = ref(false)
const refLoadingIndicator = ref(null)
watch([
isFallbackStateActive,
refLoadingIndicator,
], () => {
if (isFallbackStateActive.value && refLoadingIndicator.value)
refLoadingIndicator.value.fallbackHandle()
if (!isFallbackStateActive.value && refLoadingIndicator.value)
refLoadingIndicator.value.resolveHandle()
}, { immediate: true })
// !SECTION
</script>
<template>
<AppLoadingIndicator ref="refLoadingIndicator" />
<div class="layout-wrapper layout-blank">
<RouterView #="{Component}">
<Suspense
:timeout="0"
@fallback="isFallbackStateActive = true"
@resolve="isFallbackStateActive = false"
>
<Component :is="Component" />
</Suspense>
</RouterView>
</div>
</template>
<style>
.layout-wrapper.layout-blank {
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,82 @@
<script setup>
import navItems from '@/navigation/horizontal'
import { themeConfig } from '@themeConfig'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavBarNotifications from '@/layouts/components/NavBarNotifications.vue'
import NavSearchBar from '@/layouts/components/NavSearchBar.vue'
import NavbarShortcuts from '@/layouts/components/NavbarShortcuts.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import NavBarI18n from '@core/components/I18n.vue'
import { HorizontalNavLayout } from '@layouts'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
// SECTION: Loading Indicator
const isFallbackStateActive = ref(false)
const refLoadingIndicator = ref(null)
watch([
isFallbackStateActive,
refLoadingIndicator,
], () => {
if (isFallbackStateActive.value && refLoadingIndicator.value)
refLoadingIndicator.value.fallbackHandle()
if (!isFallbackStateActive.value && refLoadingIndicator.value)
refLoadingIndicator.value.resolveHandle()
}, { immediate: true })
// !SECTION
</script>
<template>
<HorizontalNavLayout :nav-items="navItems">
<!-- 👉 navbar -->
<template #navbar>
<RouterLink
to="/"
class="d-flex align-start gap-x-4"
>
<VNodeRenderer :nodes="themeConfig.app.logo" />
<h1 class="leading-normal text-xl text-uppercase">
{{ themeConfig.app.title }}
</h1>
</RouterLink>
<VSpacer />
<NavSearchBar />
<NavBarI18n
v-if="themeConfig.app.i18n.enable && themeConfig.app.i18n.langConfig?.length"
:languages="themeConfig.app.i18n.langConfig"
/>
<NavbarThemeSwitcher />
<NavbarShortcuts />
<NavBarNotifications class="me-2" />
<UserProfile />
</template>
<AppLoadingIndicator ref="refLoadingIndicator" />
<!-- 👉 Pages -->
<RouterView v-slot="{ Component }">
<Suspense
:timeout="0"
@fallback="isFallbackStateActive = true"
@resolve="isFallbackStateActive = false"
>
<Component :is="Component" />
</Suspense>
</RouterView>
<!-- 👉 Footer -->
<template #footer>
<Footer />
</template>
<!-- 👉 Customizer -->
<TheCustomizer />
</HorizontalNavLayout>
</template>

View File

@@ -0,0 +1,82 @@
<script setup>
import navItems from '@/navigation/vertical'
import { themeConfig } from '@themeConfig'
// Components
import Footer from '@/layouts/components/Footer.vue'
import NavBarNotifications from '@/layouts/components/NavBarNotifications.vue'
import NavSearchBar from '@/layouts/components/NavSearchBar.vue'
import NavbarShortcuts from '@/layouts/components/NavbarShortcuts.vue'
import NavbarThemeSwitcher from '@/layouts/components/NavbarThemeSwitcher.vue'
import UserProfile from '@/layouts/components/UserProfile.vue'
import NavBarI18n from '@core/components/I18n.vue'
// @layouts plugin
import { VerticalNavLayout } from '@layouts'
// SECTION: Loading Indicator
const isFallbackStateActive = ref(false)
const refLoadingIndicator = ref(null)
watch([
isFallbackStateActive,
refLoadingIndicator,
], () => {
if (isFallbackStateActive.value && refLoadingIndicator.value)
refLoadingIndicator.value.fallbackHandle()
if (!isFallbackStateActive.value && refLoadingIndicator.value)
refLoadingIndicator.value.resolveHandle()
}, { immediate: true })
// !SECTION
</script>
<template>
<VerticalNavLayout :nav-items="navItems">
<!-- 👉 navbar -->
<template #navbar="{ toggleVerticalOverlayNavActive }">
<div class="d-flex h-100 align-center">
<IconBtn
id="vertical-nav-toggle-btn"
class="ms-n2 d-lg-none"
@click="toggleVerticalOverlayNavActive(true)"
>
<VIcon icon="ri-menu-line" />
</IconBtn>
<NavSearchBar class="ms-lg-n2" />
<VSpacer />
<NavBarI18n
v-if="themeConfig.app.i18n.enable && themeConfig.app.i18n.langConfig?.length"
:languages="themeConfig.app.i18n.langConfig"
/>
<NavbarThemeSwitcher />
<NavbarShortcuts />
<NavBarNotifications class="me-2" />
<UserProfile />
</div>
</template>
<AppLoadingIndicator ref="refLoadingIndicator" />
<!-- 👉 Pages -->
<RouterView v-slot="{ Component }">
<Suspense
:timeout="0"
@fallback="isFallbackStateActive = true"
@resolve="isFallbackStateActive = false"
>
<Component :is="Component" />
</Suspense>
</RouterView>
<!-- 👉 Footer -->
<template #footer>
<Footer />
</template>
<!-- 👉 Customizer -->
<TheCustomizer />
</VerticalNavLayout>
</template>

View File

@@ -0,0 +1,37 @@
<template>
<div class="h-100 d-flex align-center justify-space-between text-medium-emphasis">
<!-- 👉 Footer: left content -->
<span class="d-flex align-center">
&copy;
{{ new Date().getFullYear() }}
Made With
<VIcon
icon="ri-heart-line"
color="error"
size="1.25rem"
class="mx-1"
/>
By <a
href="https://themeselection.com"
target="_blank"
rel="noopener noreferrer"
class="text-primary ms-1"
>ThemeSelection</a>
</span>
<!-- 👉 Footer: right content -->
<span class="d-md-flex gap-x-4 text-primary d-none">
<a
href="https://themeselection.com/license/"
target="noopener noreferrer"
>License</a>
<a
href="https://themeselection.com/"
target="noopener noreferrer"
>More Themes</a>
<a
href="https://demos.themeselection.com/materio-vuetify-vuejs-admin-template/documentation/guide/laravel-integration/folder-structure.html"
target="noopener noreferrer"
>Documentation</a>
</span>
</div>
</template>

View File

@@ -0,0 +1,99 @@
<script setup>
import avatar4 from '@images/avatars/avatar-4.png'
import avatar5 from '@images/avatars/avatar-5.png'
const notifications = ref([
{
id: 1,
img: avatar4,
title: 'Congratulation Flora! 🎉',
subtitle: 'Won the monthly best seller badge',
time: 'Today',
isSeen: true,
},
{
id: 2,
text: 'Cecilia Becker',
title: 'Cecilia Becker',
subtitle: 'Accepted your connection',
time: '12h ago',
isSeen: false,
color: 'primary',
},
{
id: 3,
img: avatar5,
title: 'New message received 👋🏻',
subtitle: 'You have 10 unread messages',
time: '11 Aug',
isSeen: true,
},
{
id: 4,
icon: 'ri-bar-chart-line',
title: 'Monthly report generated',
subtitle: 'July month financial report is generated',
time: 'Apr 24, 10:30 AM',
isSeen: false,
color: 'info',
},
{
id: 5,
text: 'Meta Gadgets',
title: 'Application has been approved 🚀',
subtitle: 'Your Meta Gadgets project application has been approved.',
time: 'Feb 17, 12:17 PM',
isSeen: false,
color: 'success',
},
{
id: 6,
icon: 'ri-mail-line',
title: 'New message from Harry',
subtitle: 'You have new message from Harry',
time: 'Jan 6, 1:48 PM',
isSeen: true,
color: 'error',
},
])
const removeNotification = notificationId => {
notifications.value.forEach((item, index) => {
if (notificationId === item.id)
notifications.value.splice(index, 1)
})
}
const markRead = notificationId => {
notifications.value.forEach(item => {
notificationId.forEach(id => {
if (id === item.id)
item.isSeen = true
})
})
}
const markUnRead = notificationId => {
notifications.value.forEach(item => {
notificationId.forEach(id => {
if (id === item.id)
item.isSeen = false
})
})
}
const handleNotificationClick = notification => {
if (!notification.isSeen)
markRead([notification.id])
}
</script>
<template>
<Notifications
:notifications="notifications"
@remove="removeNotification"
@read="markRead"
@unread="markUnRead"
@click:notification="handleNotificationClick"
/>
</template>

View File

@@ -0,0 +1,301 @@
<script setup>
import Shepherd from 'shepherd.js'
import { withQuery } from 'ufo'
import { useConfigStore } from '@core/stores/config'
defineOptions({
// 👉 Is App Search Bar Visible
inheritAttrs: false,
})
const configStore = useConfigStore()
const isAppSearchBarVisible = ref(false)
// 👉 Default suggestions
const suggestionGroups = [
{
title: 'Popular Searches',
content: [
{
icon: 'ri-bar-chart-line',
title: 'Analytics',
url: { name: 'dashboards-analytics' },
},
{
icon: 'ri-pie-chart-2-line',
title: 'CRM',
url: { name: 'dashboards-crm' },
},
{
icon: 'ri-shopping-bag-3-line',
title: 'eCommerce',
url: { name: 'dashboards-ecommerce' },
},
{
icon: 'ri-car-line',
title: 'Logistics',
url: { name: 'apps-logistics-dashboard' },
},
],
},
{
title: 'Apps & Pages',
content: [
{
icon: 'ri-calendar-line',
title: 'Calendar',
url: { name: 'apps-calendar' },
},
{
icon: 'ri-lock-unlock-line',
title: 'Roles & Permissions',
url: { name: 'apps-roles' },
},
{
icon: 'ri-settings-4-line',
title: 'Account Settings',
url: {
name: 'pages-account-settings-tab',
params: { tab: 'account' },
},
},
{
icon: 'ri-file-copy-line',
title: 'Dialog Examples',
url: { name: 'pages-dialog-examples' },
},
],
},
{
title: 'User Interface',
content: [
{
icon: 'ri-text',
title: 'Typography',
url: { name: 'pages-typography' },
},
{
icon: 'ri-menu-line',
title: 'Accordion',
url: { name: 'components-expansion-panel' },
},
{
icon: 'ri-alert-line',
title: 'Alerts',
url: { name: 'components-alert' },
},
{
icon: 'ri-checkbox-blank-line',
title: 'Cards',
url: { name: 'pages-cards-card-basic' },
},
],
},
{
title: 'Radio & Tables',
content: [
{
icon: 'ri-radio-button-line',
title: 'Radio',
url: { name: 'forms-radio' },
},
{
icon: 'ri-file-text-line',
title: 'Form Layouts',
url: { name: 'forms-form-layouts' },
},
{
icon: 'ri-table-line',
title: 'Table',
url: { name: 'tables-simple-table' },
},
{
icon: 'ri-edit-box-line',
title: 'Editor',
url: { name: 'forms-editors' },
},
],
},
]
// 👉 No Data suggestion
const noDataSuggestions = [
{
title: 'Analytics Dashboard',
icon: 'ri-shopping-cart-line',
url: { name: 'dashboards-analytics' },
},
{
title: 'Account Settings',
icon: 'ri-user-line',
url: {
name: 'pages-account-settings-tab',
params: { tab: 'account' },
},
},
{
title: 'Pricing Page',
icon: 'ri-cash-line',
url: { name: 'pages-pricing' },
},
]
const searchQuery = ref('')
const router = useRouter()
const searchResult = ref([])
const fetchResults = async () => {
const { data } = await useApi(withQuery('/app-bar/search', { q: searchQuery.value }))
searchResult.value = data.value
}
watch(searchQuery, fetchResults)
const redirectToSuggestedOrSearchedPage = selected => {
router.push(selected.url)
isAppSearchBarVisible.value = false
searchQuery.value = ''
}
const LazyAppBarSearch = defineAsyncComponent(() => import('@core/components/AppBarSearch.vue'))
</script>
<template>
<div
class="d-flex align-center cursor-pointer"
v-bind="$attrs"
style="user-select: none;"
@click="isAppSearchBarVisible = !isAppSearchBarVisible"
>
<!-- 👉 Search Trigger button -->
<!-- close active tour while opening search bar using icon -->
<IconBtn @click="Shepherd.activeTour?.cancel()">
<VIcon icon="ri-search-line" />
</IconBtn>
<span
v-if="configStore.appContentLayoutNav === 'vertical'"
class="d-none d-md-flex align-center text-disabled ms-2"
@click="Shepherd.activeTour?.cancel()"
>
<span class="me-3">Search</span>
<span class="meta-key">&#8984;K</span>
</span>
</div>
<!-- 👉 App Bar Search -->
<LazyAppBarSearch
v-model:isDialogVisible="isAppSearchBarVisible"
:search-results="searchResult"
@search="searchQuery = $event"
>
<!-- suggestion -->
<template #suggestions>
<VCardText class="app-bar-search-suggestions pa-12">
<VRow v-if="suggestionGroups">
<VCol
v-for="suggestion in suggestionGroups"
:key="suggestion.title"
cols="12"
sm="6"
>
<p class="custom-letter-spacing text-xs text-disabled text-uppercase py-2 px-4 mb-0">
{{ suggestion.title }}
</p>
<VList class="card-list">
<VListItem
v-for="item in suggestion.content"
:key="item.title"
link
class="app-bar-search-suggestion mx-4 mt-2"
@click="redirectToSuggestedOrSearchedPage(item)"
>
<VListItemTitle>{{ item.title }}</VListItemTitle>
<template #prepend>
<VIcon
:icon="item.icon"
size="20"
class="me-n1"
/>
</template>
</VListItem>
</VList>
</VCol>
</VRow>
</VCardText>
</template>
<!-- no data suggestion -->
<template #noDataSuggestion>
<div class="mt-9">
<span class="d-flex justify-center text-disabled">Try searching for</span>
<h6
v-for="suggestion in noDataSuggestions"
:key="suggestion.title"
class="app-bar-search-suggestion text-h6 font-weight-regular cursor-pointer py-2 px-4"
@click="redirectToSuggestedOrSearchedPage(suggestion)"
>
<VIcon
size="20"
:icon="suggestion.icon"
class="me-2"
/>
<span>{{ suggestion.title }}</span>
</h6>
</div>
</template>
<!-- search result -->
<template #searchResult="{ item }">
<VListSubheader class="text-disabled custom-letter-spacing font-weight-regular ps-4">
{{ item.title }}
</VListSubheader>
<VListItem
v-for="list in item.children"
:key="list.title"
@click="redirectToSuggestedOrSearchedPage(list)"
>
<template #prepend>
<VIcon
size="20"
:icon="list.icon"
class="me-n1"
/>
</template>
<template #append>
<VIcon
size="20"
icon="ri-corner-down-left-line"
class="enter-icon text-medium-emphasis"
/>
</template>
<VListItemTitle>
{{ list.title }}
</VListItemTitle>
</VListItem>
</template>
</LazyAppBarSearch>
</template>
<style lang="scss">
@use "@styles/variables/vuetify.scss";
.meta-key {
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 6px;
block-size: 1.5625rem;
padding-block: 0.125rem;
padding-inline: 0.25rem;
}
.app-bar-search-dialog {
.custom-letter-spacing {
letter-spacing: 0.8px;
}
.card-list {
--v-card-list-gap: 8px;
}
}
</style>

View File

@@ -0,0 +1,47 @@
<script setup>
const shortcuts = [
{
icon: 'ri-calendar-line',
title: 'Calendar',
subtitle: 'Appointments',
to: { name: 'apps-calendar' },
},
{
icon: 'ri-file-list-3-line',
title: 'Invoice App',
subtitle: 'Manage Accounts',
to: { name: 'apps-invoice-list' },
},
{
icon: 'ri-user-line',
title: 'Users',
subtitle: 'Manage Users',
to: { name: 'apps-user-list' },
},
{
icon: 'ri-computer-line',
title: 'Role Management',
subtitle: 'Permission',
to: { name: 'apps-roles' },
},
{
icon: 'ri-pie-chart-2-line',
title: 'Dashboard',
subtitle: 'Dashboard Analytics',
to: { name: 'dashboards-analytics' },
},
{
icon: 'ri-settings-4-line',
title: 'Settings',
subtitle: 'Account Settings',
to: {
name: 'pages-account-settings-tab',
params: { tab: 'account' },
},
},
]
</script>
<template>
<Shortcuts :shortcuts="shortcuts" />
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
const themes = [
{
name: 'system',
icon: 'ri-macbook-line',
},
{
name: 'light',
icon: 'ri-sun-line',
},
{
name: 'dark',
icon: 'ri-moon-clear-line',
},
]
</script>
<template>
<ThemeSwitcher :themes="themes" />
</template>

View File

@@ -0,0 +1,200 @@
<script setup>
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
const router = useRouter()
const ability = useAbility()
// TODO: Get type from backend
const userData = useCookie('userData')
const logout = async () => {
// Remove "accessToken" from cookie
useCookie('accessToken').value = null
// Remove "userData" from cookie
userData.value = null
// Redirect to login page
await router.push('/login')
// We had to remove abilities in then block because if we don't nav menu items mutation is visible while redirecting user to login page
// Remove "userAbilities" from cookie
useCookie('userAbilityRules').value = null
// Reset ability to initial ability
ability.update([])
}
const userProfileList = [
{ type: 'divider' },
{
type: 'navItem',
icon: 'ri-user-line',
title: 'Profile',
to: {
name: 'apps-user-view-id',
params: { id: 21 },
},
},
{
type: 'navItem',
icon: 'ri-settings-4-line',
title: 'Settings',
to: {
name: 'pages-account-settings-tab',
params: { tab: 'account' },
},
},
{
type: 'navItem',
icon: 'ri-file-text-line',
title: 'Billing Plan',
to: {
name: 'pages-account-settings-tab',
params: { tab: 'billing-plans' },
},
badgeProps: {
color: 'error',
content: '4',
},
},
{ type: 'divider' },
{
type: 'navItem',
icon: 'ri-money-dollar-circle-line',
title: 'Pricing',
to: { name: 'pages-pricing' },
},
{
type: 'navItem',
icon: 'ri-question-line',
title: 'FAQ',
to: { name: 'pages-faq' },
},
{ type: 'divider' },
]
</script>
<template>
<VBadge
v-if="userData"
dot
bordered
location="bottom right"
offset-x="3"
offset-y="3"
color="success"
>
<VAvatar
class="cursor-pointer"
size="38"
:color="!(userData && userData.avatar) ? 'primary' : undefined"
:variant="!(userData && userData.avatar) ? 'tonal' : undefined"
>
<VImg
v-if="userData && userData.avatar"
:src="userData.avatar"
/>
<VIcon
v-else
icon="ri-user-line"
/>
<!-- SECTION Menu -->
<VMenu
activator="parent"
width="230"
location="bottom end"
offset="15px"
>
<VList>
<VListItem>
<template #prepend>
<VListItemAction start>
<VBadge
dot
location="bottom right"
offset-x="3"
offset-y="3"
color="success"
>
<VAvatar
:color="!(userData && userData.avatar) ? 'primary' : undefined"
:variant="!(userData && userData.avatar) ? 'tonal' : undefined"
>
<VImg
v-if="userData && userData.avatar"
:src="userData.avatar"
/>
<VIcon
v-else
icon="ri-user-line"
/>
</VAvatar>
</VBadge>
</VListItemAction>
</template>
<h6 class="text-h6 font-weight-medium">
{{ userData.fullName || userData.username }}
</h6>
<VListItemSubtitle class="text-capitalize text-disabled">
{{ userData.role }}
</VListItemSubtitle>
</VListItem>
<PerfectScrollbar :options="{ wheelPropagation: false }">
<template
v-for="item in userProfileList"
:key="item.title"
>
<VListItem
v-if="item.type === 'navItem'"
:to="item.to"
>
<template #prepend>
<VIcon
:icon="item.icon"
size="22"
/>
</template>
<VListItemTitle>{{ item.title }}</VListItemTitle>
<template
v-if="item.badgeProps"
#append
>
<VBadge
inline
v-bind="item.badgeProps"
/>
</template>
</VListItem>
<VDivider
v-else
class="my-1"
/>
</template>
<VListItem>
<VBtn
block
color="error"
size="small"
append-icon="ri-logout-box-r-line"
@click="logout"
>
Logout
</VBtn>
</VListItem>
</PerfectScrollbar>
</VList>
</VMenu>
<!-- !SECTION -->
</VAvatar>
</VBadge>
</template>

View File

@@ -0,0 +1,30 @@
<script setup>
import { useConfigStore } from '@core/stores/config'
import { AppContentLayoutNav } from '@layouts/enums'
import { switchToVerticalNavOnLtOverlayNavBreakpoint } from '@layouts/utils'
const DefaultLayoutWithHorizontalNav = defineAsyncComponent(() => import('./components/DefaultLayoutWithHorizontalNav.vue'))
const DefaultLayoutWithVerticalNav = defineAsyncComponent(() => import('./components/DefaultLayoutWithVerticalNav.vue'))
const configStore = useConfigStore()
// This will switch to vertical nav when define breakpoint is reached when in horizontal nav layout
// Remove below composable usage if you are not using horizontal nav layout in your app
switchToVerticalNavOnLtOverlayNavBreakpoint()
const { layoutAttrs, injectSkinClasses } = useSkins()
injectSkinClasses()
</script>
<template>
<Component
v-bind="layoutAttrs"
:is="configStore.appContentLayoutNav === AppContentLayoutNav.Vertical ? DefaultLayoutWithVerticalNav : DefaultLayoutWithHorizontalNav"
/>
</template>
<style lang="scss">
// As we are using `layouts` plugin we need its styles to be imported
@use "@layouts/styles/default-layout";
</style>